2017-09-03 04:50:31 +00:00
/ *
*
2017-09-13 15:09:13 +00:00
* Gosora Route Handlers
2019-02-10 05:52:26 +00:00
* Copyright Azareal 2016 - 2020
2017-09-03 04:50:31 +00:00
*
* /
2016-12-02 07:38:54 +00:00
package main
2017-05-11 13:04:43 +00:00
import (
2018-07-05 09:54:01 +00:00
"crypto/sha256"
"encoding/hex"
2018-06-24 13:49:29 +00:00
"encoding/json"
2018-11-22 07:21:43 +00:00
"errors"
2018-12-06 11:54:20 +00:00
"log"
2017-05-11 13:04:43 +00:00
"net/http"
2017-09-03 04:50:31 +00:00
"strconv"
2018-06-24 13:49:29 +00:00
"strings"
"unicode"
2017-06-19 08:06:54 +00:00
2019-04-19 07:25:49 +00:00
c "github.com/Azareal/Gosora/common"
2018-11-01 06:43:56 +00:00
"github.com/Azareal/Gosora/common/phrases"
2017-05-11 13:04:43 +00:00
)
2017-04-05 14:05:37 +00:00
2016-12-02 07:38:54 +00:00
// A blank list to fill out that parameter in Page for routes which don't use it
2016-12-18 12:56:06 +00:00
var tList [ ] interface { }
2017-09-03 04:50:31 +00:00
var successJSONBytes = [ ] byte ( ` { "success":"1"} ` )
2016-12-02 07:38:54 +00:00
2017-11-23 05:37:08 +00:00
// TODO: Refactor this
2018-05-15 05:59:52 +00:00
// TODO: Use the phrase system
2018-11-30 03:02:20 +00:00
var phraseLoginAlerts = [ ] byte ( ` { "msgs":[ { "msg":"Login to see your alerts","path":"/accounts/login"}],"msgCount":0} ` )
2017-09-03 04:50:31 +00:00
2017-11-23 05:37:08 +00:00
// TODO: Refactor this endpoint
2018-12-27 05:42:41 +00:00
// TODO: Move this into the routes package
2019-04-19 07:25:49 +00:00
func routeAPI ( w http . ResponseWriter , r * http . Request , user c . User ) c . RouteError {
2017-11-23 05:37:08 +00:00
// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
2017-09-03 04:50:31 +00:00
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2017-02-28 09:27:28 +00:00
err := r . ParseForm ( )
if err != nil {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "Bad Form" , w , r )
2017-02-28 09:27:28 +00:00
}
2017-05-29 14:52:37 +00:00
2017-02-28 09:27:28 +00:00
action := r . FormValue ( "action" )
if action != "get" && action != "set" {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "Invalid Action" , w , r )
2017-02-28 09:27:28 +00:00
}
2017-05-29 14:52:37 +00:00
2018-05-15 05:59:52 +00:00
switch r . FormValue ( "module" ) {
2018-12-27 05:42:41 +00:00
// TODO: Split this into it's own function
2017-09-03 04:50:31 +00:00
case "dismiss-alert" :
asid , err := strconv . Atoi ( r . FormValue ( "asid" ) )
if err != nil {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "Invalid asid" , w , r )
2017-09-03 04:50:31 +00:00
}
2018-09-08 13:50:15 +00:00
res , err := stmts . deleteActivityStreamMatch . Exec ( user . ID , asid )
2017-09-03 04:50:31 +00:00
if err != nil {
2019-04-19 07:25:49 +00:00
return c . InternalError ( err , w , r )
2017-09-03 04:50:31 +00:00
}
2018-09-08 13:50:15 +00:00
count , err := res . RowsAffected ( )
if err != nil {
2019-04-19 07:25:49 +00:00
return c . InternalError ( err , w , r )
2018-09-08 13:50:15 +00:00
}
// Don't want to throw an internal error due to a socket closing
2019-04-19 07:25:49 +00:00
if c . EnableWebsockets && count > 0 {
_ = c . WsHub . PushMessage ( user . ID , ` { "event":"dismiss-alert","asid": ` + strconv . Itoa ( asid ) + ` } ` )
2018-09-08 13:50:15 +00:00
}
2019-03-16 11:31:10 +00:00
w . Write ( successJSONBytes )
2018-12-27 05:42:41 +00:00
// TODO: Split this into it's own function
2017-09-03 04:50:31 +00:00
case "alerts" : // A feed of events tailored for a specific user
if ! user . Loggedin {
2017-09-10 16:57:22 +00:00
w . Write ( phraseLoginAlerts )
2017-10-30 09:57:08 +00:00
return nil
2017-09-03 04:50:31 +00:00
}
2017-05-29 14:52:37 +00:00
2018-11-22 07:21:43 +00:00
var msglist string
2017-09-03 04:50:31 +00:00
var msgCount int
2017-11-05 09:55:34 +00:00
err = stmts . getActivityCountByWatcher . QueryRow ( user . ID ) . Scan ( & msgCount )
2017-09-03 04:50:31 +00:00
if err == ErrNoRows {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "Couldn't find the parent topic" , w , r )
2017-09-03 04:50:31 +00:00
} else if err != nil {
2019-04-19 07:25:49 +00:00
return c . InternalErrorJS ( err , w , r )
2017-09-03 04:50:31 +00:00
}
2017-11-05 09:55:34 +00:00
rows , err := stmts . getActivityFeedByWatcher . Query ( user . ID )
2017-09-03 04:50:31 +00:00
if err != nil {
2019-04-19 07:25:49 +00:00
return c . InternalErrorJS ( err , w , r )
2017-09-03 04:50:31 +00:00
}
defer rows . Close ( )
2017-05-29 14:52:37 +00:00
2018-11-22 07:21:43 +00:00
var actors [ ] int
2019-04-19 07:25:49 +00:00
var alerts [ ] c . Alert
2017-09-03 04:50:31 +00:00
for rows . Next ( ) {
2019-04-19 07:25:49 +00:00
var alert c . Alert
2018-11-22 07:21:43 +00:00
err = rows . Scan ( & alert . ASID , & alert . ActorID , & alert . TargetUserID , & alert . Event , & alert . ElementType , & alert . ElementID )
2017-02-28 09:27:28 +00:00
if err != nil {
2019-04-19 07:25:49 +00:00
return c . InternalErrorJS ( err , w , r )
2017-02-28 09:27:28 +00:00
}
2018-11-22 07:21:43 +00:00
alerts = append ( alerts , alert )
actors = append ( actors , alert . ActorID )
2017-09-03 04:50:31 +00:00
}
err = rows . Err ( )
if err != nil {
2019-04-19 07:25:49 +00:00
return c . InternalErrorJS ( err , w , r )
2017-09-03 04:50:31 +00:00
}
2018-11-22 07:21:43 +00:00
// Might not want to error here, if the account was deleted properly, we might want to figure out how we should handle deletions in general
2019-04-19 07:25:49 +00:00
list , err := c . Users . BulkGetMap ( actors )
2018-11-22 07:21:43 +00:00
if err != nil {
2018-12-06 11:54:20 +00:00
log . Print ( "actors:" , actors )
2019-04-19 07:25:49 +00:00
return c . InternalErrorJS ( err , w , r )
2018-11-22 07:21:43 +00:00
}
var ok bool
for _ , alert := range alerts {
alert . Actor , ok = list [ alert . ActorID ]
if ! ok {
2019-04-19 07:25:49 +00:00
return c . InternalErrorJS ( errors . New ( "No such actor" ) , w , r )
2018-11-22 07:21:43 +00:00
}
2019-04-19 07:25:49 +00:00
res , err := c . BuildAlert ( alert , user )
2018-11-22 07:21:43 +00:00
if err != nil {
2019-04-19 07:25:49 +00:00
return c . LocalErrorJS ( err . Error ( ) , w , r )
2018-11-22 07:21:43 +00:00
}
msglist += res + ","
}
2017-09-03 04:50:31 +00:00
if len ( msglist ) != 0 {
msglist = msglist [ 0 : len ( msglist ) - 1 ]
}
_ , _ = w . Write ( [ ] byte ( ` { "msgs":[ ` + msglist + ` ],"msgCount": ` + strconv . Itoa ( msgCount ) + ` } ` ) )
default :
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "Invalid Module" , w , r )
2017-02-28 09:27:28 +00:00
}
2017-10-30 09:57:08 +00:00
return nil
2017-02-28 09:27:28 +00:00
}
2018-06-24 13:49:29 +00:00
2018-08-13 10:34:00 +00:00
// TODO: Remove this line after we move routeAPIPhrases to the routes package
2019-04-19 07:25:49 +00:00
var cacheControlMaxAge = "max-age=" + strconv . Itoa ( int ( c . Day ) )
2018-08-13 10:34:00 +00:00
2018-06-24 13:49:29 +00:00
// TODO: Be careful with exposing the panel phrases here, maybe move them into a different namespace? We also need to educate the admin that phrases aren't necessarily secret
2018-08-13 10:34:00 +00:00
// TODO: Move to the routes package
2019-02-10 05:52:26 +00:00
var phraseWhitelist = [ ] string {
"topic" ,
"status" ,
"alerts" ,
"paginator" ,
2019-03-03 02:28:17 +00:00
"analytics" ,
2019-04-27 06:32:26 +00:00
"panel" , // We're going to handle this specially below as this is a security boundary
2019-02-10 05:52:26 +00:00
}
2019-04-19 07:25:49 +00:00
func routeAPIPhrases ( w http . ResponseWriter , r * http . Request , user c . User ) c . RouteError {
2018-06-24 13:49:29 +00:00
// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
2018-08-13 10:34:00 +00:00
h := w . Header ( )
h . Set ( "Content-Type" , "application/json" )
h . Set ( "Cache-Control" , cacheControlMaxAge ) //Cache-Control: max-age=31536000
2018-06-24 13:49:29 +00:00
err := r . ParseForm ( )
if err != nil {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "Bad Form" , w , r )
2018-06-24 13:49:29 +00:00
}
query := r . FormValue ( "query" )
if query == "" {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "No query provided" , w , r )
2018-06-24 13:49:29 +00:00
}
var negations [ ] string
var positives [ ] string
queryBits := strings . Split ( query , "," )
for _ , queryBit := range queryBits {
queryBit = strings . TrimSpace ( queryBit )
if queryBit [ 0 ] == '!' && len ( queryBit ) > 1 {
queryBit = strings . TrimPrefix ( queryBit , "!" )
for _ , char := range queryBit {
if ! unicode . IsLetter ( char ) && char != '-' && char != '_' {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "No symbols allowed, only - and _" , w , r )
2018-06-24 13:49:29 +00:00
}
}
negations = append ( negations , queryBit )
} else {
for _ , char := range queryBit {
if ! unicode . IsLetter ( char ) && char != '-' && char != '_' {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "No symbols allowed, only - and _" , w , r )
2018-06-24 13:49:29 +00:00
}
}
positives = append ( positives , queryBit )
}
}
if len ( positives ) == 0 {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "You haven't requested any phrases" , w , r )
2018-06-24 13:49:29 +00:00
}
2018-11-01 06:43:56 +00:00
var plist map [ string ] string
2018-06-24 13:49:29 +00:00
// A little optimisation to avoid copying entries from one map to the other, if we don't have to mutate it
2019-02-10 05:52:26 +00:00
// TODO: Reduce the amount of duplication here
2018-06-24 13:49:29 +00:00
if len ( positives ) > 1 {
2018-11-01 06:43:56 +00:00
plist = make ( map [ string ] string )
2018-06-24 13:49:29 +00:00
for _ , positive := range positives {
2019-02-10 05:52:26 +00:00
// ! Constrain it to a subset of phrases for now
var ok = false
for _ , item := range phraseWhitelist {
if strings . HasPrefix ( positive , item ) {
2019-04-27 06:32:26 +00:00
// TODO: Break this down into smaller security boundaries based on control panel sections?
if strings . HasPrefix ( positive , "panel" ) {
if user . IsSuperMod {
ok = true
w . Header ( ) . Set ( "Cache-Control" , "private" )
}
} else {
ok = true
}
2019-02-10 05:52:26 +00:00
break
}
}
if ! ok {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "Outside of phrase prefix whitelist" , w , r )
2018-06-24 13:49:29 +00:00
}
2019-04-27 06:32:26 +00:00
2018-11-01 06:43:56 +00:00
pPhrases , ok := phrases . GetTmplPhrasesByPrefix ( positive )
2018-06-24 13:49:29 +00:00
if ! ok {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "No such prefix" , w , r )
2018-06-24 13:49:29 +00:00
}
for name , phrase := range pPhrases {
2018-11-01 06:43:56 +00:00
plist [ name ] = phrase
2018-06-24 13:49:29 +00:00
}
}
} else {
2019-02-10 05:52:26 +00:00
// ! Constrain it to a subset of phrases for now
var ok = false
for _ , item := range phraseWhitelist {
if strings . HasPrefix ( positives [ 0 ] , item ) {
2019-04-27 06:32:26 +00:00
// TODO: Break this down into smaller security boundaries based on control panel sections?
if strings . HasPrefix ( positives [ 0 ] , "panel" ) {
if user . IsSuperMod {
ok = true
w . Header ( ) . Set ( "Cache-Control" , "private" )
}
} else {
ok = true
}
2019-02-10 05:52:26 +00:00
break
}
}
if ! ok {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "Outside of phrase prefix whitelist" , w , r )
2018-06-24 13:49:29 +00:00
}
2019-04-27 06:32:26 +00:00
2018-11-01 06:43:56 +00:00
pPhrases , ok := phrases . GetTmplPhrasesByPrefix ( positives [ 0 ] )
2018-06-24 13:49:29 +00:00
if ! ok {
2019-04-19 07:25:49 +00:00
return c . PreErrorJS ( "No such prefix" , w , r )
2018-06-24 13:49:29 +00:00
}
2018-11-01 06:43:56 +00:00
plist = pPhrases
2018-06-24 13:49:29 +00:00
}
for _ , negation := range negations {
2018-11-01 06:43:56 +00:00
for name , _ := range plist {
2018-06-24 13:49:29 +00:00
if strings . HasPrefix ( name , negation ) {
2018-11-01 06:43:56 +00:00
delete ( plist , name )
2018-06-24 13:49:29 +00:00
}
}
}
// TODO: Cache the output of this, especially for things like topic, so we don't have to waste more time than we need on this
2018-11-01 06:43:56 +00:00
jsonBytes , err := json . Marshal ( plist )
2018-06-24 13:49:29 +00:00
if err != nil {
2019-04-19 07:25:49 +00:00
return c . InternalError ( err , w , r )
2018-06-24 13:49:29 +00:00
}
w . Write ( jsonBytes )
return nil
}
2018-07-05 09:54:01 +00:00
// A dedicated function so we can shake things up every now and then to make the token harder to parse
// TODO: Are we sure we want to do this by ID, just in case we reuse this and have multiple antispams on the page?
2019-04-19 07:25:49 +00:00
func routeJSAntispam ( w http . ResponseWriter , r * http . Request , user c . User ) c . RouteError {
2018-07-05 09:54:01 +00:00
h := sha256 . New ( )
2019-04-19 07:25:49 +00:00
h . Write ( [ ] byte ( c . JSTokenBox . Load ( ) . ( string ) ) )
2018-07-05 09:54:01 +00:00
h . Write ( [ ] byte ( user . LastIP ) )
jsToken := hex . EncodeToString ( h . Sum ( nil ) )
var innerCode = "`document.getElementByld('golden-watch').value = '" + jsToken + "';`"
w . Write ( [ ] byte ( ` let hihi = ` + innerCode + ` ;
hihi = hihi . replace ( ' ld ',' Id ' ) ;
eval ( hihi ) ; ` ) )
return nil
}