2018-01-20 06:50:29 +00:00
package routes
import (
2022-02-21 03:53:13 +00:00
"database/sql"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
2022-02-21 03:32:53 +00:00
2022-02-21 03:53:13 +00:00
c "git.tuxpa.in/a/gosora/common"
"git.tuxpa.in/a/gosora/common/counters"
p "git.tuxpa.in/a/gosora/common/phrases"
qgen "git.tuxpa.in/a/gosora/query_gen"
2018-01-20 06:50:29 +00:00
)
2018-12-27 09:12:30 +00:00
type ReplyStmts struct {
2022-02-21 03:32:53 +00:00
createReplyPaging * sql . Stmt
2018-12-27 09:12:30 +00:00
}
var replyStmts ReplyStmts
// TODO: Move this statement somewhere else
func init ( ) {
2022-02-21 03:32:53 +00:00
c . DbInits . Add ( func ( acc * qgen . Accumulator ) error {
replyStmts = ReplyStmts {
createReplyPaging : acc . Select ( "replies" ) . Cols ( "rid" ) . Where ( "rid >= ? - 1 AND tid=?" ) . Orderby ( "rid ASC" ) . Prepare ( ) ,
}
return acc . FirstError ( )
} )
2018-12-27 09:12:30 +00:00
}
2018-12-28 02:08:35 +00:00
type JsonReply struct {
2022-02-21 03:32:53 +00:00
Content string
2018-12-28 02:08:35 +00:00
}
2020-03-18 09:21:34 +00:00
func CreateReplySubmit ( w http . ResponseWriter , r * http . Request , user * c . User ) c . RouteError {
2022-02-21 03:32:53 +00:00
// TODO: Use this
js := r . FormValue ( "js" ) == "1"
tid , err := strconv . Atoi ( r . PostFormValue ( "tid" ) )
if err != nil {
return c . PreErrorJSQ ( "Failed to convert the Topic ID" , w , r , js )
}
topic , err := c . Topics . Get ( tid )
if err == sql . ErrNoRows {
return c . PreErrorJSQ ( "Couldn't find the parent topic" , w , r , js )
} else if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
// TODO: Add hooks to make use of headerLite
lite , ferr := c . SimpleForumUserCheck ( w , r , user , topic . ParentID )
if ferr != nil {
return ferr
}
if ! user . Perms . ViewTopic || ! user . Perms . CreateReply {
return c . NoPermissionsJSQ ( w , r , user , js )
}
if topic . IsClosed && ! user . Perms . CloseTopic {
return c . NoPermissionsJSQ ( w , r , user , js )
}
content := c . PreparseMessage ( r . PostFormValue ( "content" ) )
// TODO: Fully parse the post and put that in the parsed column
rid , err := c . Rstore . Create ( topic , content , user . GetIP ( ) , user . ID )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
reply , err := c . Rstore . Get ( rid )
if err != nil {
return c . LocalErrorJSQ ( "Unable to load the reply" , w , r , user , js )
}
// Handle the file attachments
// TODO: Stop duplicating this code
if user . Perms . UploadFiles {
_ , rerr := uploadAttachment ( w , r , user , topic . ParentID , "forums" , rid , "replies" , strconv . Itoa ( topic . ID ) )
if rerr != nil {
return rerr
}
}
if r . PostFormValue ( "has_poll" ) == "1" {
maxPollOptions := 10
pollInputItems := make ( map [ int ] string )
for key , values := range r . Form {
//c.DebugDetail("key: ", key)
//c.DebugDetailf("values: %+v\n", values)
if ! strings . HasPrefix ( key , "pollinputitem[" ) {
continue
}
halves := strings . Split ( key , "[" )
if len ( halves ) != 2 {
return c . LocalErrorJSQ ( "Malformed pollinputitem" , w , r , user , js )
}
halves [ 1 ] = strings . TrimSuffix ( halves [ 1 ] , "]" )
index , err := strconv . Atoi ( halves [ 1 ] )
if err != nil {
return c . LocalErrorJSQ ( "Malformed pollinputitem" , w , r , user , js )
}
for _ , value := range values {
// If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack
_ , exists := pollInputItems [ index ]
// TODO: Should we use SanitiseBody instead to keep the newlines?
if ! exists && len ( c . SanitiseSingleLine ( value ) ) != 0 {
pollInputItems [ index ] = c . SanitiseSingleLine ( value )
if len ( pollInputItems ) >= maxPollOptions {
break
}
}
}
}
// Make sure the indices are sequential to avoid out of bounds issues
seqPollInputItems := make ( map [ int ] string )
for i := 0 ; i < len ( pollInputItems ) ; i ++ {
seqPollInputItems [ i ] = pollInputItems [ i ]
}
pollType := 0 // Basic single choice
_ , err := c . Polls . Create ( reply , pollType , seqPollInputItems )
if err != nil {
return c . LocalErrorJSQ ( "Failed to add poll to reply" , w , r , user , js ) // TODO: Might need to be an internal error as it could leave phantom polls?
}
}
_ = c . Rstore . GetCache ( ) . Remove ( reply . ID )
err = c . Forums . UpdateLastTopic ( tid , user . ID , topic . ParentID )
if err != nil && err != sql . ErrNoRows {
return c . InternalErrorJSQ ( err , w , r , js )
}
c . AddActivityAndNotifyAll ( c . Alert { ActorID : user . ID , TargetUserID : topic . CreatedBy , Event : "reply" , ElementType : "topic" , ElementID : tid , Extra : strconv . Itoa ( rid ) } )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
err = user . IncreasePostStats ( c . WordCount ( content ) , false )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
nTopic , err := c . Topics . Get ( tid )
if err == sql . ErrNoRows {
return c . PreErrorJSQ ( "Couldn't find the parent topic" , w , r , js )
} else if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
page := c . LastPage ( nTopic . PostCount , c . Config . ItemsPerPage )
rows , err := replyStmts . createReplyPaging . Query ( reply . ID , topic . ID )
if err != nil && err != sql . ErrNoRows {
return c . InternalErrorJSQ ( err , w , r , js )
}
defer rows . Close ( )
var rids [ ] int
for rows . Next ( ) {
var rid int
if err := rows . Scan ( & rid ) ; err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
rids = append ( rids , rid )
}
if err := rows . Err ( ) ; err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
if len ( rids ) == 0 {
return c . NotFoundJSQ ( w , r , nil , js )
}
if page > 1 {
var offset int
if rids [ 0 ] == reply . ID {
offset = 1
} else if len ( rids ) == 2 && rids [ 1 ] == reply . ID {
offset = 2
}
page = c . LastPage ( nTopic . PostCount - ( len ( rids ) + offset ) , c . Config . ItemsPerPage )
}
counters . PostCounter . Bump ( )
skip , rerr := lite . Hooks . VhookSkippable ( "action_end_create_reply" , reply . ID , user )
if skip || rerr != nil {
return rerr
}
prid , _ := strconv . Atoi ( r . FormValue ( "prid" ) )
if js && ( prid == 0 || rids [ 0 ] == prid ) {
outBytes , err := json . Marshal ( JsonReply { c . ParseMessage ( reply . Content , topic . ParentID , "forums" , user . ParseSettings , user ) } )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
w . Write ( outBytes )
} else {
var spage string
if page > 1 {
spage = "?page=" + strconv . Itoa ( page )
}
http . Redirect ( w , r , "/topic/" + strconv . Itoa ( tid ) + spage + "#post-" + strconv . Itoa ( reply . ID ) , http . StatusSeeOther )
}
return nil
2018-03-08 03:59:47 +00:00
}
2018-01-20 06:50:29 +00:00
// TODO: Disable stat updates in posts handled by plugin_guilds
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
2020-04-27 12:41:55 +00:00
func ReplyEditSubmit ( w http . ResponseWriter , r * http . Request , u * c . User , srid string ) c . RouteError {
2022-02-21 03:32:53 +00:00
js := r . PostFormValue ( "js" ) == "1"
reply , topic , lite , ferr := ReplyActPre ( w , r , u , srid , js )
if ferr != nil {
return ferr
}
if ! u . Perms . ViewTopic || ! u . Perms . EditReply {
return c . NoPermissionsJSQ ( w , r , u , js )
}
if topic . IsClosed && ! u . Perms . CloseTopic {
return c . NoPermissionsJSQ ( w , r , u , js )
}
err := reply . SetPost ( r . PostFormValue ( "edit_item" ) )
if err == sql . ErrNoRows {
return c . PreErrorJSQ ( "The parent topic doesn't exist." , w , r , js )
} else if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
if ! c . Rstore . Exists ( reply . ID ) {
return c . PreErrorJSQ ( "The updated reply doesn't exist." , w , r , js )
}
skip , rerr := lite . Hooks . VhookSkippable ( "action_end_edit_reply" , reply . ID , u )
if skip || rerr != nil {
return rerr
}
if ! js {
http . Redirect ( w , r , "/topic/" + strconv . Itoa ( topic . ID ) + "#reply-" + strconv . Itoa ( reply . ID ) , http . StatusSeeOther )
} else {
outBytes , err := json . Marshal ( JsonReply { c . ParseMessage ( reply . Content , topic . ParentID , "forums" , u . ParseSettings , u ) } )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
w . Write ( outBytes )
}
return nil
2018-01-20 06:50:29 +00:00
}
// TODO: Refactor this
// TODO: Disable stat updates in posts handled by plugin_guilds
2020-04-27 12:41:55 +00:00
func ReplyDeleteSubmit ( w http . ResponseWriter , r * http . Request , u * c . User , srid string ) c . RouteError {
2022-02-21 03:32:53 +00:00
js := r . PostFormValue ( "js" ) == "1"
reply , _ , lite , ferr := ReplyActPre ( w , r , u , srid , js )
if ferr != nil {
return ferr
}
if reply . CreatedBy != u . ID {
if ! u . Perms . ViewTopic || ! u . Perms . DeleteReply {
return c . NoPermissionsJSQ ( w , r , u , js )
}
}
if e := reply . Delete ( ) ; e != nil {
return c . InternalErrorJSQ ( e , w , r , js )
}
skip , rerr := lite . Hooks . VhookSkippable ( "action_end_delete_reply" , reply . ID , u )
if skip || rerr != nil {
return rerr
}
//log.Printf("Reply #%d was deleted by c.User #%d", rid, u.ID)
if ! js {
http . Redirect ( w , r , "/topic/" + strconv . Itoa ( reply . ParentID ) , http . StatusSeeOther )
} else {
w . Write ( successJSONBytes )
}
// ? - What happens if an error fires after a redirect...?
/ * creator , e := c . Users . Get ( reply . CreatedBy )
if e == nil {
e = creator . DecreasePostStats ( c . WordCount ( reply . Content ) , false )
if e != nil {
return c . InternalErrorJSQ ( e , w , r , js )
}
} else if e != sql . ErrNoRows {
return c . InternalErrorJSQ ( e , w , r , js )
} * /
e := c . ModLogs . Create ( "delete" , reply . ParentID , "reply" , u . GetIP ( ) , u . ID )
if e != nil {
return c . InternalErrorJSQ ( e , w , r , js )
}
return nil
2018-01-20 06:50:29 +00:00
}
2018-12-31 09:03:49 +00:00
// TODO: Avoid uploading this again if the attachment already exists? They'll resolve to the same hash either way, but we could save on some IO / bandwidth here
// TODO: Enforce the max request limit on all of this topic's attachments
// TODO: Test this route
2020-03-18 09:21:34 +00:00
func AddAttachToReplySubmit ( w http . ResponseWriter , r * http . Request , u * c . User , srid string ) c . RouteError {
2022-02-21 03:32:53 +00:00
reply , topic , lite , ferr := ReplyActPre ( w , r , u , srid , true )
if ferr != nil {
return ferr
}
if ! u . Perms . ViewTopic || ! u . Perms . EditReply || ! u . Perms . UploadFiles {
return c . NoPermissionsJS ( w , r , u )
}
if topic . IsClosed && ! u . Perms . CloseTopic {
return c . NoPermissionsJS ( w , r , u )
}
// Handle the file attachments
pathMap , rerr := uploadAttachment ( w , r , u , topic . ParentID , "forums" , reply . ID , "replies" , strconv . Itoa ( topic . ID ) )
if rerr != nil {
// TODO: This needs to be a JS error...
return rerr
}
if len ( pathMap ) == 0 {
return c . InternalErrorJS ( errors . New ( "no paths for attachment add" ) , w , r )
}
skip , rerr := lite . Hooks . VhookSkippable ( "action_end_add_attach_to_reply" , reply . ID , u )
if skip || rerr != nil {
return rerr
}
var elemStr string
for path , aids := range pathMap {
elemStr += "\"" + path + "\":\"" + aids + "\","
}
if len ( elemStr ) > 1 {
elemStr = elemStr [ : len ( elemStr ) - 1 ]
}
w . Write ( [ ] byte ( ` { "success":1,"elems": { ` + elemStr + ` }} ` ) )
return nil
2018-12-31 09:03:49 +00:00
}
// TODO: Reduce the amount of duplication between this and RemoveAttachFromTopicSubmit
2020-03-18 09:21:34 +00:00
func RemoveAttachFromReplySubmit ( w http . ResponseWriter , r * http . Request , u * c . User , srid string ) c . RouteError {
2022-02-21 03:32:53 +00:00
reply , topic , lite , ferr := ReplyActPre ( w , r , u , srid , true )
if ferr != nil {
return ferr
}
if ! u . Perms . ViewTopic || ! u . Perms . EditReply {
return c . NoPermissionsJS ( w , r , u )
}
if topic . IsClosed && ! u . Perms . CloseTopic {
return c . NoPermissionsJS ( w , r , u )
}
saids := strings . Split ( r . PostFormValue ( "aids" ) , "," )
if len ( saids ) == 0 {
return c . LocalErrorJS ( "No aids provided" , w , r )
}
for _ , said := range saids {
aid , err := strconv . Atoi ( said )
if err != nil {
return c . LocalErrorJS ( p . GetErrorPhrase ( "id_must_be_integer" ) , w , r )
}
rerr := deleteAttachment ( w , r , u , aid , true )
if rerr != nil {
// TODO: This needs to be a JS error...
return rerr
}
}
skip , rerr := lite . Hooks . VhookSkippable ( "action_end_remove_attach_from_reply" , reply . ID , u )
if skip || rerr != nil {
return rerr
}
w . Write ( successJSONBytes )
return nil
2018-12-31 09:03:49 +00:00
}
2021-03-24 11:24:41 +00:00
func ReplyActPre ( w http . ResponseWriter , r * http . Request , u * c . User , srid string , js bool ) ( rep * c . Reply , t * c . Topic , l * c . HeaderLite , ferr c . RouteError ) {
2022-02-21 03:32:53 +00:00
rid , err := strconv . Atoi ( srid )
if err != nil {
return rep , t , l , c . PreErrorJSQ ( "The provided Reply ID is not a valid number." , w , r , js )
}
rep , err = c . Rstore . Get ( rid )
if err == sql . ErrNoRows {
return rep , t , l , c . PreErrorJSQ ( "The linked reply doesn't exist." , w , r , js )
} else if err != nil {
return rep , t , l , c . InternalErrorJSQ ( err , w , r , js )
}
t , err = rep . Topic ( )
if err == sql . ErrNoRows {
return rep , t , l , c . PreErrorJSQ ( "The parent topic doesn't exist." , w , r , js )
} else if err != nil {
return rep , t , l , c . InternalErrorJSQ ( err , w , r , js )
}
// TODO: Add hooks to make use of headerLite
l , ferr = c . SimpleForumUserCheck ( w , r , u , t . ParentID )
return rep , t , l , ferr
2021-03-24 11:24:41 +00:00
}
func ReplyLikeSubmit ( w http . ResponseWriter , r * http . Request , u * c . User , srid string ) c . RouteError {
2022-02-21 03:32:53 +00:00
js := r . PostFormValue ( "js" ) == "1"
reply , _ , lite , ferr := ReplyActPre ( w , r , u , srid , js )
if ferr != nil {
return ferr
}
if ! u . Perms . ViewTopic || ! u . Perms . LikeItem {
return c . NoPermissionsJSQ ( w , r , u , js )
}
if reply . CreatedBy == u . ID {
return c . LocalErrorJSQ ( "You can't like your own replies" , w , r , u , js )
}
_ , err := c . Users . Get ( reply . CreatedBy )
if err != nil && err != sql . ErrNoRows {
return c . LocalErrorJSQ ( "The target user doesn't exist" , w , r , u , js )
} else if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
err = reply . Like ( u . ID )
if err == c . ErrAlreadyLiked {
return c . LocalErrorJSQ ( "You've already liked this!" , w , r , u , js )
} else if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
// ! Be careful about leaking per-route permission state with user ptr
alert := c . Alert { ActorID : u . ID , TargetUserID : reply . CreatedBy , Event : "like" , ElementType : "post" , ElementID : reply . ID , Actor : u }
err = c . AddActivityAndNotifyTarget ( alert )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
skip , rerr := lite . Hooks . VhookSkippable ( "action_end_like_reply" , reply . ID , u )
if skip || rerr != nil {
return rerr
}
return actionSuccess ( w , r , "/topic/" + strconv . Itoa ( reply . ParentID ) , js )
2018-05-15 05:59:52 +00:00
}
2020-01-31 10:48:55 +00:00
2020-04-27 12:41:55 +00:00
func ReplyUnlikeSubmit ( w http . ResponseWriter , r * http . Request , u * c . User , srid string ) c . RouteError {
2022-02-21 03:32:53 +00:00
js := r . PostFormValue ( "js" ) == "1"
reply , _ , lite , ferr := ReplyActPre ( w , r , u , srid , js )
if ferr != nil {
return ferr
}
if ! u . Perms . ViewTopic || ! u . Perms . LikeItem {
return c . NoPermissionsJSQ ( w , r , u , js )
}
_ , err := c . Users . Get ( reply . CreatedBy )
if err != nil && err != sql . ErrNoRows {
return c . LocalErrorJSQ ( "The target user doesn't exist" , w , r , u , js )
} else if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
err = reply . Unlike ( u . ID )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
// TODO: Better coupling between the two params queries
aids , err := c . Activity . AidsByParams ( "like" , reply . ID , "post" )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
for _ , aid := range aids {
c . DismissAlert ( reply . CreatedBy , aid )
}
err = c . Activity . DeleteByParams ( "like" , reply . ID , "post" )
if err != nil {
return c . InternalErrorJSQ ( err , w , r , js )
}
skip , rerr := lite . Hooks . VhookSkippable ( "action_end_unlike_reply" , reply . ID , u )
if skip || rerr != nil {
return rerr
}
return actionSuccess ( w , r , "/topic/" + strconv . Itoa ( reply . ParentID ) , js )
2020-01-31 10:48:55 +00:00
}