2017-09-03 04:50:31 +00:00
/ *
*
2022-02-21 03:32:53 +00:00
* Gosora Route Handlers
* 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 (
2022-02-21 03:53:13 +00:00
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
"unicode"
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/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 { }
2019-12-31 21:57:54 +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
2019-05-06 04:04:00 +00:00
var phraseLoginAlerts = [ ] byte ( ` { "msgs":[ { "msg":"Login to see your alerts","path":"/accounts/login"}],"count":0} ` )
2020-04-20 23:59:23 +00:00
var alertStrPool = sync . Pool { }
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
2020-03-18 09:21:34 +00:00
func routeAPI ( w http . ResponseWriter , r * http . Request , user * c . User ) c . RouteError {
2022-02-21 03:32:53 +00:00
// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
w . Header ( ) . Set ( "Content-Type" , "application/json" )
err := r . ParseForm ( )
if err != nil {
return c . PreErrorJS ( "Bad Form" , w , r )
}
action := r . FormValue ( "a" )
if action == "" {
action = "get"
}
if action != "get" && action != "set" {
return c . PreErrorJS ( "Invalid Action" , w , r )
}
switch r . FormValue ( "m" ) {
// TODO: Split this into it's own function
case "dismiss-alert" :
id , err := strconv . Atoi ( r . FormValue ( "id" ) )
if err != nil {
return c . PreErrorJS ( "Invalid id" , w , r )
}
res , err := stmts . deleteActivityStreamMatch . Exec ( user . ID , id )
if err != nil {
return c . InternalError ( err , w , r )
}
count , err := res . RowsAffected ( )
if err != nil {
return c . InternalError ( err , w , r )
}
// Don't want to throw an internal error due to a socket closing
if c . EnableWebsockets && count > 0 {
c . DismissAlert ( user . ID , id )
}
w . Write ( successJSONBytes )
// TODO: Split this into it's own function
case "alerts" : // A feed of events tailored for a specific user
if ! user . Loggedin {
h := w . Header ( )
if gzw , ok := w . ( c . GzipResponseWriter ) ; ok {
w = gzw . ResponseWriter
h . Del ( "Content-Encoding" )
}
etag := "\"1583653869-n\""
//etag = c.StartEtag
h . Set ( "ETag" , etag )
if match := r . Header . Get ( "If-None-Match" ) ; match != "" {
if strings . Contains ( match , etag ) {
w . WriteHeader ( http . StatusNotModified )
return nil
}
}
w . Write ( phraseLoginAlerts )
return nil
}
var count int
err = stmts . getActivityCountByWatcher . QueryRow ( user . ID ) . Scan ( & count )
if err == ErrNoRows {
return c . PreErrorJS ( "Unable to get the activity count" , w , r )
} else if err != nil {
return c . InternalErrorJS ( err , w , r )
}
if count == 0 {
if gzw , ok := w . ( c . GzipResponseWriter ) ; ok {
w = gzw . ResponseWriter
w . Header ( ) . Del ( "Content-Encoding" )
}
_ , _ = io . WriteString ( w , ` { } ` )
return nil
}
rCreatedAt , _ := strconv . ParseInt ( r . FormValue ( "t" ) , 10 , 64 )
rCount , _ := strconv . Atoi ( r . FormValue ( "c" ) )
//log.Print("rCreatedAt:", rCreatedAt)
//log.Print("rCount:", rCount)
var actors [ ] int
var alerts [ ] * c . Alert
var createdAt time . Time
var topCreatedAt int64
if count != 0 {
rows , err := stmts . getActivityFeedByWatcher . Query ( user . ID , 12 )
if err != nil {
return c . InternalErrorJS ( err , w , r )
}
defer rows . Close ( )
for rows . Next ( ) {
al := & c . Alert { }
err = rows . Scan ( & al . ASID , & al . ActorID , & al . TargetUserID , & al . Event , & al . ElementType , & al . ElementID , & createdAt )
if err != nil {
return c . InternalErrorJS ( err , w , r )
}
uCreatedAt := createdAt . Unix ( )
//log.Print("uCreatedAt", uCreatedAt)
//if rCreatedAt == 0 || rCreatedAt < uCreatedAt {
alerts = append ( alerts , al )
actors = append ( actors , al . ActorID )
//}
if uCreatedAt > topCreatedAt {
topCreatedAt = uCreatedAt
}
}
if err = rows . Err ( ) ; err != nil {
return c . InternalErrorJS ( err , w , r )
}
}
if len ( alerts ) == 0 || ( rCreatedAt != 0 && rCreatedAt >= topCreatedAt && count == rCount ) {
if gzw , ok := w . ( c . GzipResponseWriter ) ; ok {
w = gzw . ResponseWriter
w . Header ( ) . Del ( "Content-Encoding" )
}
_ , _ = io . WriteString ( w , ` { } ` )
return nil
}
// 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
list , err := c . Users . BulkGetMap ( actors )
if err != nil {
log . Print ( "actors:" , actors )
return c . InternalErrorJS ( err , w , r )
}
var sb * strings . Builder
ii := alertStrPool . Get ( )
if ii == nil {
sb = & strings . Builder { }
} else {
sb = ii . ( * strings . Builder )
sb . Reset ( )
}
sb . Grow ( c . AlertsGrowHint + ( len ( alerts ) * ( c . AlertsGrowHint2 + 1 ) ) - 1 )
sb . WriteString ( ` { "msgs":[ ` )
var ok bool
for i , alert := range alerts {
if i != 0 {
sb . WriteRune ( ',' )
}
alert . Actor , ok = list [ alert . ActorID ]
if ! ok {
return c . InternalErrorJS ( errors . New ( "No such actor" ) , w , r )
}
err := c . BuildAlertSb ( sb , alert , user )
if err != nil {
return c . LocalErrorJS ( err . Error ( ) , w , r )
}
}
sb . WriteString ( ` ],"count": ` )
sb . WriteString ( strconv . Itoa ( count ) )
sb . WriteString ( ` ,"tc": ` )
//rCreatedAt
sb . WriteString ( strconv . Itoa ( int ( topCreatedAt ) ) )
sb . WriteRune ( '}' )
_ , _ = io . WriteString ( w , sb . String ( ) )
alertStrPool . Put ( sb )
default :
return c . PreErrorJS ( "Invalid Module" , w , r )
}
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 {
2022-02-21 03:32:53 +00:00
"topic" ,
"status" ,
"alerts" ,
"paginator" ,
"analytics" ,
2019-04-27 06:32:26 +00:00
2022-02-21 03:32:53 +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
}
2020-03-18 09:21:34 +00:00
func routeAPIPhrases ( w http . ResponseWriter , r * http . Request , user * c . User ) c . RouteError {
2022-02-21 03:32:53 +00:00
// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
h := w . Header ( )
h . Set ( "Content-Type" , "application/json" )
err := r . ParseForm ( )
if err != nil {
return c . PreErrorJS ( "Bad Form" , w , r )
}
query := r . FormValue ( "q" )
if query == "" {
return c . PreErrorJS ( "No query provided" , w , r )
}
var negations , positives [ ] string
for _ , queryBit := range strings . Split ( query , "," ) {
queryBit = strings . TrimSpace ( queryBit )
if queryBit [ 0 ] == '!' && len ( queryBit ) > 1 {
queryBit = strings . TrimPrefix ( queryBit , "!" )
for _ , ch := range queryBit {
if ! unicode . IsLetter ( ch ) && ch != '-' && ch != '_' {
return c . PreErrorJS ( "No symbols allowed, only - and _" , w , r )
}
}
negations = append ( negations , queryBit )
} else {
for _ , ch := range queryBit {
if ! unicode . IsLetter ( ch ) && ch != '-' && ch != '_' {
return c . PreErrorJS ( "No symbols allowed, only - and _" , w , r )
}
}
positives = append ( positives , queryBit )
}
}
if len ( positives ) == 0 {
return c . PreErrorJS ( "You haven't requested any phrases" , w , r )
}
h . Set ( "Cache-Control" , cacheControlMaxAge ) //Cache-Control: max-age=31536000
var etag string
_ , ok := w . ( c . GzipResponseWriter )
if ok {
etag = "\"" + strconv . FormatInt ( phrases . GetCurrentLangPack ( ) . ModTime . Unix ( ) , 10 ) + "-ng\""
} else {
etag = "\"" + strconv . FormatInt ( phrases . GetCurrentLangPack ( ) . ModTime . Unix ( ) , 10 ) + "-n\""
}
var plist map [ string ] string
var notModified , private bool
posLoop := func ( positive string ) c . RouteError {
// ! Constrain it to a subset of phrases for now
for _ , item := range phraseWhitelist {
if strings . HasPrefix ( positive , item ) {
// TODO: Break this down into smaller security boundaries based on control panel sections?
// TODO: Do we have to be so strict with panel phrases?
if strings . HasPrefix ( positive , "panel" ) {
private = true
ok = user . IsSuperMod
} else {
ok = true
if notModified {
return nil
}
w . Header ( ) . Set ( "ETag" , etag )
match := r . Header . Get ( "If-None-Match" )
if match != "" && strings . Contains ( match , etag ) {
notModified = true
return nil
}
}
break
}
}
if ! ok {
return c . PreErrorJS ( "Outside of phrase prefix whitelist" , w , r )
}
return nil
}
// A little optimisation to avoid copying entries from one map to the other, if we don't have to mutate it
if len ( positives ) > 1 {
plist = make ( map [ string ] string )
for _ , positive := range positives {
rerr := posLoop ( positive )
if rerr != nil {
return rerr
}
pPhrases , ok := phrases . GetTmplPhrasesByPrefix ( positive )
if ! ok {
return c . PreErrorJS ( "No such prefix" , w , r )
}
for name , phrase := range pPhrases {
plist [ name ] = phrase
}
}
} else {
rerr := posLoop ( positives [ 0 ] )
if rerr != nil {
return rerr
}
pPhrases , ok := phrases . GetTmplPhrasesByPrefix ( positives [ 0 ] )
if ! ok {
return c . PreErrorJS ( "No such prefix" , w , r )
}
plist = pPhrases
}
if private {
w . Header ( ) . Set ( "Cache-Control" , "private" )
} else if notModified {
w . WriteHeader ( http . StatusNotModified )
return nil
}
for _ , negation := range negations {
for name , _ := range plist {
if strings . HasPrefix ( name , negation ) {
delete ( plist , name )
}
}
}
// 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
jsonBytes , err := json . Marshal ( plist )
if err != nil {
return c . InternalError ( err , w , r )
}
w . Write ( jsonBytes )
return nil
2018-06-24 13:49:29 +00:00
}
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?
2020-03-18 09:21:34 +00:00
func routeJSAntispam ( w http . ResponseWriter , r * http . Request , user * c . User ) c . RouteError {
2022-02-21 03:32:53 +00:00
h := sha256 . New ( )
h . Write ( [ ] byte ( c . JSTokenBox . Load ( ) . ( string ) ) )
h . Write ( [ ] byte ( user . GetIP ( ) ) )
jsToken := hex . EncodeToString ( h . Sum ( nil ) )
2018-07-05 09:54:01 +00:00
2022-02-21 03:32:53 +00:00
innerCode := "`document.getElementByld('golden-watch').value='" + jsToken + "';`"
io . WriteString ( w , ` let hihi= ` + innerCode + ` ;hihi=hihi.replace('ld','Id');eval(hihi); ` )
2018-07-05 09:54:01 +00:00
2022-02-21 03:32:53 +00:00
return nil
2018-07-05 09:54:01 +00:00
}