2020-11-06 17:45:31 +00:00
package main
import (
"database/sql"
2020-11-16 13:18:14 +00:00
"errors"
2020-11-06 17:45:31 +00:00
"fmt"
"net/http"
"strings"
"time"
2021-06-18 12:32:03 +00:00
2022-02-23 20:33:02 +00:00
"go.goblog.app/app/pkgs/bufferpool"
2021-06-28 20:17:18 +00:00
"go.goblog.app/app/pkgs/contenttype"
2020-11-06 17:45:31 +00:00
)
type webmentionStatus string
const (
webmentionStatusVerified webmentionStatus = "verified"
webmentionStatusApproved webmentionStatus = "approved"
2021-01-30 18:37:26 +00:00
webmentionPath = "/webmention"
2020-11-06 17:45:31 +00:00
)
type mention struct {
2021-11-18 21:56:30 +00:00
ID int
Source string
NewSource string
Target string
2021-11-19 16:36:03 +00:00
NewTarget string
2021-11-19 21:17:15 +00:00
Url string
2021-11-18 21:56:30 +00:00
Created int64
Title string
Content string
Author string
Status webmentionStatus
Submentions [ ] * mention
2020-11-06 17:45:31 +00:00
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) initWebmention ( ) {
2020-11-17 21:10:13 +00:00
// Add hooks
hookFunc := func ( p * post ) {
2021-07-14 13:44:57 +00:00
_ = a . sendWebmentions ( p )
2020-11-17 21:10:13 +00:00
}
2021-06-06 12:39:42 +00:00
a . pPostHooks = append ( a . pPostHooks , hookFunc )
a . pUpdateHooks = append ( a . pUpdateHooks , hookFunc )
a . pDeleteHooks = append ( a . pDeleteHooks , hookFunc )
2022-01-03 12:55:44 +00:00
a . pUndeleteHooks = append ( a . pUndeleteHooks , hookFunc )
2020-11-17 21:10:13 +00:00
// Start verifier
2021-06-06 12:39:42 +00:00
a . initWebmentionQueue ( )
2020-11-06 17:45:31 +00:00
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) handleWebmention ( w http . ResponseWriter , r * http . Request ) {
2021-08-03 19:32:02 +00:00
m , err := a . extractMention ( r )
2020-11-06 17:45:31 +00:00
if err != nil {
2021-08-03 19:32:02 +00:00
a . debug ( "Error extracting webmention:" , err . Error ( ) )
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusBadRequest )
2020-11-06 17:45:31 +00:00
return
}
2021-11-19 16:36:03 +00:00
hasShortPrefix := a . cfg . Server . ShortPublicAddress != "" && strings . HasPrefix ( m . Target , a . cfg . Server . ShortPublicAddress )
hasLongPrefix := strings . HasPrefix ( m . Target , a . cfg . Server . PublicAddress )
if ! hasShortPrefix && ! hasLongPrefix {
2021-08-03 19:32:02 +00:00
a . debug ( "Webmention target not allowed:" , m . Target )
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "target not allowed" , http . StatusBadRequest )
2020-11-06 17:45:31 +00:00
return
}
2021-12-08 10:08:33 +00:00
if m . Target == m . Source {
a . debug ( "Webmention target and source are the same:" , m . Target )
a . serveError ( w , r , "target and source are the same" , http . StatusBadRequest )
return
}
2021-06-06 12:39:42 +00:00
if err = a . queueMention ( m ) ; err != nil {
2021-08-03 19:32:02 +00:00
a . debug ( "Failed to queue webmention" , err . Error ( ) )
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusInternalServerError )
2020-11-06 17:45:31 +00:00
return
}
w . WriteHeader ( http . StatusAccepted )
_ , _ = fmt . Fprint ( w , "Webmention accepted" )
2021-08-03 19:32:02 +00:00
a . debug ( "Accepted webmention:" , m . Source , m . Target )
2020-11-06 17:45:31 +00:00
}
2021-08-03 19:32:02 +00:00
func ( a * goBlog ) extractMention ( r * http . Request ) ( * mention , error ) {
if ct := r . Header . Get ( contentType ) ; ! strings . Contains ( ct , contenttype . WWWForm ) {
a . debug ( "New webmention request with wrong content type:" , ct )
2021-04-23 17:36:57 +00:00
return nil , errors . New ( "unsupported Content-Type" )
2020-11-16 13:18:14 +00:00
}
err := r . ParseForm ( )
if err != nil {
return nil , err
}
source := r . Form . Get ( "source" )
2022-03-28 16:02:16 +00:00
target := r . Form . Get ( "target" )
2020-11-16 13:18:14 +00:00
if source == "" || target == "" || ! isAbsoluteURL ( source ) || ! isAbsoluteURL ( target ) {
2021-08-03 19:32:02 +00:00
a . debug ( "Invalid webmention request, source:" , source , "target:" , target )
2021-04-23 17:36:57 +00:00
return nil , errors . New ( "invalid request" )
2020-11-16 13:18:14 +00:00
}
return & mention {
2020-11-25 11:36:14 +00:00
Source : source ,
Target : target ,
Created : time . Now ( ) . Unix ( ) ,
2020-11-16 13:18:14 +00:00
} , nil
}
2021-11-19 16:36:03 +00:00
func ( db * database ) webmentionExists ( m * mention ) bool {
2020-11-06 17:45:31 +00:00
result := 0
2022-08-09 15:25:22 +00:00
row , err := db . QueryRow (
2021-11-19 16:36:03 +00:00
`
select exists (
select 1
from webmentions
where
2022-03-28 16:02:16 +00:00
lowerunescaped ( source ) in ( lowerunescaped ( @ source ) , lowerunescaped ( @ newsource ) )
and lowerunescaped ( target ) in ( lowerunescaped ( @ target ) , lowerunescaped ( @ newtarget ) )
2021-11-19 16:36:03 +00:00
)
` ,
sql . Named ( "source" , m . Source ) , sql . Named ( "newsource" , defaultIfEmpty ( m . NewSource , m . Source ) ) ,
sql . Named ( "target" , m . Target ) , sql . Named ( "newtarget" , defaultIfEmpty ( m . NewTarget , m . Target ) ) ,
)
2020-11-09 15:40:12 +00:00
if err != nil {
return false
}
if err = row . Scan ( & result ) ; err != nil {
2020-11-06 17:45:31 +00:00
return false
}
return result == 1
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) createWebmention ( source , target string ) ( err error ) {
return a . queueMention ( & mention {
2020-11-25 11:36:14 +00:00
Source : source ,
2022-03-28 16:02:16 +00:00
Target : target ,
2020-11-25 11:36:14 +00:00
Created : time . Now ( ) . Unix ( ) ,
} )
2020-11-06 17:45:31 +00:00
}
2021-08-09 11:09:45 +00:00
func ( db * database ) insertWebmention ( m * mention , status webmentionStatus ) error {
2022-08-09 15:25:22 +00:00
_ , err := db . Exec (
2021-08-09 11:09:45 +00:00
`
2021-11-19 21:17:15 +00:00
insert into webmentions ( source , target , url , created , status , title , content , author )
2022-03-28 16:02:16 +00:00
values ( @ source , lowerunescaped ( @ target ) , @ url , @ created , @ status , @ title , @ content , @ author )
2021-08-09 11:09:45 +00:00
` ,
sql . Named ( "source" , m . Source ) ,
sql . Named ( "target" , m . Target ) ,
2021-11-19 21:17:15 +00:00
sql . Named ( "url" , m . Url ) ,
2021-08-09 11:09:45 +00:00
sql . Named ( "created" , m . Created ) ,
sql . Named ( "status" , status ) ,
sql . Named ( "title" , m . Title ) ,
sql . Named ( "content" , m . Content ) ,
sql . Named ( "author" , m . Author ) ,
)
return err
}
2021-11-19 16:36:03 +00:00
func ( db * database ) updateWebmention ( m * mention , newStatus webmentionStatus ) error {
2022-08-09 15:25:22 +00:00
_ , err := db . Exec ( `
2021-11-19 16:36:03 +00:00
update webmentions
set
source = @ newsource ,
2022-03-28 16:02:16 +00:00
target = lowerunescaped ( @ newtarget ) ,
2021-11-19 21:17:15 +00:00
url = @ url ,
2021-11-19 16:36:03 +00:00
status = @ status ,
title = @ title ,
content = @ content ,
author = @ author
where
2022-03-28 16:02:16 +00:00
lowerunescaped ( source ) in ( lowerunescaped ( @ source ) , lowerunescaped ( @ newsource2 ) )
and lowerunescaped ( target ) in ( lowerunescaped ( @ target ) , lowerunescaped ( @ newtarget2 ) )
2021-11-19 16:36:03 +00:00
` ,
sql . Named ( "newsource" , defaultIfEmpty ( m . NewSource , m . Source ) ) ,
sql . Named ( "newtarget" , defaultIfEmpty ( m . NewTarget , m . Target ) ) ,
2021-11-19 21:17:15 +00:00
sql . Named ( "url" , m . Url ) ,
2021-11-19 16:36:03 +00:00
sql . Named ( "status" , newStatus ) ,
sql . Named ( "title" , m . Title ) ,
sql . Named ( "content" , m . Content ) ,
sql . Named ( "author" , m . Author ) ,
sql . Named ( "source" , m . Source ) ,
sql . Named ( "newsource2" , defaultIfEmpty ( m . NewSource , m . Source ) ) ,
sql . Named ( "target" , m . Target ) ,
sql . Named ( "newtarget2" , defaultIfEmpty ( m . NewTarget , m . Target ) ) ,
)
return err
}
func ( db * database ) deleteWebmentionId ( id int ) error {
2022-08-09 15:25:22 +00:00
_ , err := db . Exec ( "delete from webmentions where id = @id" , sql . Named ( "id" , id ) )
2020-11-06 17:45:31 +00:00
return err
}
2022-11-23 21:16:56 +00:00
func ( db * database ) deleteWebmentionUUrl ( uUrl string ) error {
_ , err := db . Exec ( "delete from webmentions where url = @url" , sql . Named ( "url" , uUrl ) )
return err
}
2021-11-19 16:36:03 +00:00
func ( db * database ) deleteWebmention ( m * mention ) error {
2022-08-09 15:25:22 +00:00
_ , err := db . Exec (
2022-03-28 16:02:16 +00:00
"delete from webmentions where lowerunescaped(source) in (lowerunescaped(@source), lowerunescaped(@newsource)) and lowerunescaped(target) in (lowerunescaped(@target), lowerunescaped(@newtarget))" ,
2021-11-19 16:36:03 +00:00
sql . Named ( "source" , m . Source ) ,
sql . Named ( "newsource" , defaultIfEmpty ( m . NewSource , m . Source ) ) ,
sql . Named ( "target" , m . Target ) ,
sql . Named ( "newtarget" , defaultIfEmpty ( m . NewTarget , m . Target ) ) ,
)
return err
}
func ( db * database ) approveWebmentionId ( id int ) error {
2022-08-09 15:25:22 +00:00
_ , err := db . Exec ( "update webmentions set status = ? where id = ?" , webmentionStatusApproved , id )
2020-11-06 17:45:31 +00:00
return err
}
2021-11-19 16:36:03 +00:00
func ( a * goBlog ) reverifyWebmentionId ( id int ) error {
2021-06-06 12:39:42 +00:00
m , err := a . db . getWebmentions ( & webmentionsRequestConfig {
2021-05-23 18:11:48 +00:00
id : id ,
limit : 1 ,
} )
if err != nil {
return err
}
if len ( m ) > 0 {
2021-06-06 12:39:42 +00:00
err = a . queueMention ( m [ 0 ] )
2021-05-23 18:11:48 +00:00
}
2021-05-24 07:12:46 +00:00
return err
2021-05-23 18:11:48 +00:00
}
2020-11-06 17:45:31 +00:00
type webmentionsRequestConfig struct {
2021-01-30 18:37:26 +00:00
target string
status webmentionStatus
2021-05-24 08:09:37 +00:00
sourcelike string
2021-05-23 18:11:48 +00:00
id int
2021-01-30 18:37:26 +00:00
asc bool
offset , limit int
2021-11-18 21:56:30 +00:00
submentions bool
2020-11-06 17:45:31 +00:00
}
2022-03-16 07:28:03 +00:00
func buildWebmentionsQuery ( config * webmentionsRequestConfig ) ( query string , args [ ] any ) {
2022-02-23 20:33:02 +00:00
queryBuilder := bufferpool . Get ( )
defer bufferpool . Put ( queryBuilder )
2021-11-19 21:17:15 +00:00
queryBuilder . WriteString ( "select id, source, target, url, created, title, content, author, status from webmentions " )
2020-11-06 17:45:31 +00:00
if config != nil {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( "where 1" )
2021-05-24 08:09:37 +00:00
if config . target != "" {
2022-03-28 16:02:16 +00:00
queryBuilder . WriteString ( " and lowerunescaped(target) = lowerunescaped(@target)" )
2020-11-09 15:40:12 +00:00
args = append ( args , sql . Named ( "target" , config . target ) )
2021-05-24 08:09:37 +00:00
}
if config . status != "" {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( " and status = @status" )
2020-11-09 15:40:12 +00:00
args = append ( args , sql . Named ( "status" , config . status ) )
2021-05-24 08:09:37 +00:00
}
if config . sourcelike != "" {
2022-03-28 16:02:16 +00:00
queryBuilder . WriteString ( " and lowerunescaped(source) like ('%' || lowerunescaped(@sourcelike) || '%')" )
2021-08-09 11:09:45 +00:00
args = append ( args , sql . Named ( "sourcelike" , config . sourcelike ) )
2021-05-24 08:09:37 +00:00
}
if config . id != 0 {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( " and id = @id" )
2021-05-23 18:11:48 +00:00
args = append ( args , sql . Named ( "id" , config . id ) )
2020-11-06 17:45:31 +00:00
}
}
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( " order by created " )
2020-11-11 15:58:44 +00:00
if config . asc {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( "asc" )
} else {
queryBuilder . WriteString ( "desc" )
2020-11-11 15:58:44 +00:00
}
2021-01-30 18:37:26 +00:00
if config . limit != 0 || config . offset != 0 {
2021-07-23 15:26:14 +00:00
queryBuilder . WriteString ( " limit @limit offset @offset" )
2021-01-30 18:37:26 +00:00
args = append ( args , sql . Named ( "limit" , config . limit ) , sql . Named ( "offset" , config . offset ) )
}
2021-07-23 15:26:14 +00:00
return queryBuilder . String ( ) , args
2021-01-30 18:37:26 +00:00
}
2021-06-06 12:39:42 +00:00
func ( db * database ) getWebmentions ( config * webmentionsRequestConfig ) ( [ ] * mention , error ) {
2021-01-30 18:37:26 +00:00
mentions := [ ] * mention { }
query , args := buildWebmentionsQuery ( config )
2022-08-09 15:25:22 +00:00
rows , err := db . Query ( query , args ... )
2020-11-06 17:45:31 +00:00
if err != nil {
return nil , err
}
for rows . Next ( ) {
m := & mention { }
2021-11-19 21:17:15 +00:00
err = rows . Scan ( & m . ID , & m . Source , & m . Target , & m . Url , & m . Created , & m . Title , & m . Content , & m . Author , & m . Status )
2020-11-06 17:45:31 +00:00
if err != nil {
return nil , err
}
2021-11-19 21:17:15 +00:00
if m . Url == "" {
m . Url = m . Source
}
2021-11-18 21:56:30 +00:00
if config . submentions {
m . Submentions , err = db . getWebmentions ( & webmentionsRequestConfig {
target : m . Source ,
2021-11-19 05:27:43 +00:00
submentions : false , // prevent infinite recursion
2021-11-18 21:56:30 +00:00
asc : config . asc ,
status : config . status ,
} )
if err != nil {
return nil , err
}
}
2020-11-06 17:45:31 +00:00
mentions = append ( mentions , m )
}
return mentions , nil
}
2021-01-30 18:37:26 +00:00
2021-06-18 12:32:03 +00:00
func ( db * database ) getWebmentionsByAddress ( address string ) [ ] * mention {
2021-11-22 15:56:40 +00:00
if address == "" {
return nil
}
2021-06-18 12:32:03 +00:00
mentions , _ := db . getWebmentions ( & webmentionsRequestConfig {
2021-11-18 21:56:30 +00:00
target : address ,
status : webmentionStatusApproved ,
asc : true ,
submentions : true ,
2021-06-18 12:32:03 +00:00
} )
return mentions
}
2021-06-06 12:39:42 +00:00
func ( db * database ) countWebmentions ( config * webmentionsRequestConfig ) ( count int , err error ) {
2021-01-30 18:37:26 +00:00
query , params := buildWebmentionsQuery ( config )
query = "select count(*) from (" + query + ")"
2022-08-09 15:25:22 +00:00
row , err := db . QueryRow ( query , params ... )
2021-01-30 18:37:26 +00:00
if err != nil {
return
}
err = row . Scan ( & count )
return
}