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"
2019-05-11 23:07:24 +00:00
"io"
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"
2019-05-11 23:07:24 +00:00
"time"
2018-06-24 13:49:29 +00:00
"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
2019-05-06 04:04:00 +00:00
var phraseLoginAlerts = [ ] byte ( ` { "msgs":[ { "msg":"Login to see your alerts","path":"/accounts/login"}],"count":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" )
2019-05-06 04:04:00 +00:00
if action == "" {
action = "get"
}
2017-02-28 09:27:28 +00:00
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" :
2019-05-07 02:33:33 +00:00
id , err := strconv . Atoi ( r . FormValue ( "id" ) )
2017-09-03 04:50:31 +00:00
if err != nil {
2019-05-07 02:33:33 +00:00
return c . PreErrorJS ( "Invalid id" , w , r )
2017-09-03 04:50:31 +00:00
}
2019-05-07 02:33:33 +00:00
res , err := stmts . deleteActivityStreamMatch . Exec ( user . ID , id )
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 {
2019-05-07 02:33:33 +00:00
_ = c . WsHub . PushMessage ( user . ID , ` { "event":"dismiss-alert","id": ` + strconv . Itoa ( id ) + ` } ` )
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 {
2019-05-01 23:14:07 +00:00
var etag string
_ , ok := w . ( c . GzipResponseWriter )
if ok {
2019-05-11 23:07:24 +00:00
etag = "\"" + strconv . FormatInt ( c . StartTime . Unix ( ) , 10 ) + "-ng\""
2019-05-01 23:14:07 +00:00
} else {
2019-05-11 23:07:24 +00:00
etag = "\"" + strconv . FormatInt ( c . StartTime . Unix ( ) , 10 ) + "-n\""
2019-05-01 23:14:07 +00:00
}
w . Header ( ) . Set ( "ETag" , etag )
if match := r . Header . Get ( "If-None-Match" ) ; match != "" {
if strings . Contains ( match , etag ) {
w . WriteHeader ( http . StatusNotModified )
return nil
}
}
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
2019-05-06 04:04:00 +00:00
var count int
err = stmts . getActivityCountByWatcher . QueryRow ( user . ID ) . Scan ( & count )
2017-09-03 04:50:31 +00:00
if err == ErrNoRows {
2019-05-09 10:15:09 +00:00
return c . PreErrorJS ( "Unable to get the activity count" , 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
}
2019-05-11 23:07:24 +00:00
rCreatedAt , _ := strconv . ParseInt ( r . FormValue ( "t" ) , 10 , 64 )
rCount , _ := strconv . Atoi ( r . FormValue ( "c" ) )
//log.Print("rCreatedAt:", rCreatedAt)
//log.Print("rCount:", rCount)
2018-11-22 07:21:43 +00:00
var actors [ ] int
2019-04-19 07:25:49 +00:00
var alerts [ ] c . Alert
2019-05-11 23:07:24 +00:00
var createdAt time . Time
var topCreatedAt int64
if count != 0 {
rows , err := stmts . getActivityFeedByWatcher . Query ( user . ID )
if err != nil {
return c . InternalErrorJS ( err , w , r )
}
defer rows . Close ( )
for rows . Next ( ) {
var alert c . Alert
err = rows . Scan ( & alert . ASID , & alert . ActorID , & alert . TargetUserID , & alert . Event , & alert . ElementType , & alert . 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 , alert )
actors = append ( actors , alert . ActorID )
//}
if uCreatedAt > topCreatedAt {
topCreatedAt = uCreatedAt
}
}
err = rows . Err ( )
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
}
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
}
2019-05-09 10:15:09 +00:00
var msglist string
//var sb strings.Builder
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
}
2019-05-09 10:15:09 +00:00
//sb.Write(res)
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 ]
}
2019-05-11 23:07:24 +00:00
if count == 0 || msglist == "" || ( rCreatedAt != 0 && rCreatedAt >= topCreatedAt && count == rCount ) {
2019-05-09 10:15:09 +00:00
_ , _ = io . WriteString ( w , ` { } ` )
} else {
2019-05-11 23:07:24 +00:00
_ , _ = io . WriteString ( w , ` { "msgs":[ ` + msglist + ` ],"count": ` + strconv . Itoa ( count ) + ` ,"tc": ` + strconv . Itoa ( int ( topCreatedAt ) ) + ` } ` )
2019-05-09 10:15:09 +00:00
}
2017-09-03 04:50:31 +00:00
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" )
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
}
2019-07-11 21:38:06 +00:00
query := r . FormValue ( "q" )
2018-06-24 13:49:29 +00:00
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
}
2019-07-11 21:38:06 +00:00
var negations , positives [ ] string
for _ , queryBit := range strings . Split ( query , "," ) {
2018-06-24 13:49:29 +00:00
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
}
2019-07-11 21:38:06 +00:00
h . Set ( "Cache-Control" , cacheControlMaxAge ) //Cache-Control: max-age=31536000
2018-06-24 13:49:29 +00:00
2019-04-28 10:08:05 +00:00
var etag string
_ , ok := w . ( c . GzipResponseWriter )
if ok {
2019-05-11 23:07:24 +00:00
etag = "\"" + strconv . FormatInt ( c . StartTime . Unix ( ) , 10 ) + "-g\""
2019-04-28 10:08:05 +00:00
} else {
2019-05-11 23:07:24 +00:00
etag = "\"" + strconv . FormatInt ( c . StartTime . Unix ( ) , 10 ) + "\""
2019-04-28 10:08:05 +00:00
}
2019-05-11 23:07:24 +00:00
2018-11-01 06:43:56 +00:00
var plist map [ string ] string
2019-07-11 21:51:50 +00:00
var notModified = false
2019-04-28 10:08:05 +00:00
var 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?
2019-05-11 23:07:24 +00:00
if strings . HasPrefix ( positive , "panel" ) {
2019-04-28 10:08:05 +00:00
w . Header ( ) . Set ( "Cache-Control" , "private" )
ok = user . IsSuperMod
} else {
ok = true
w . Header ( ) . Set ( "ETag" , etag )
2019-07-11 21:38:06 +00:00
match := r . Header . Get ( "If-None-Match" )
2019-07-11 21:51:50 +00:00
if match != "" && strings . Contains ( match , etag ) {
notModified = true
2019-07-11 21:38:06 +00:00
return nil
2019-04-27 06:32:26 +00:00
}
2019-02-10 05:52:26 +00:00
}
2019-04-28 10:08:05 +00:00
break
2019-02-10 05:52:26 +00:00
}
2019-04-28 10:08:05 +00:00
}
if ! ok {
return c . PreErrorJS ( "Outside of phrase prefix whitelist" , w , r )
}
return nil
}
2019-04-27 06:32:26 +00:00
2019-04-28 10:08:05 +00:00
// 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
}
2019-07-11 21:51:50 +00:00
if notModified {
break
}
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-04-28 10:08:05 +00:00
rerr := posLoop ( positives [ 0 ] )
if rerr != nil {
return rerr
2019-02-10 05:52: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
}
2019-07-11 21:51:50 +00:00
if notModified {
w . WriteHeader ( http . StatusNotModified )
return nil
}
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 + "';`"
2019-05-11 23:07:24 +00:00
io . WriteString ( w , ` let hihi = ` + innerCode + ` ;
2018-07-05 09:54:01 +00:00
hihi = hihi . replace ( ' ld ',' Id ' ) ;
2019-05-06 04:04:00 +00:00
eval ( hihi ) ; ` )
2018-07-05 09:54:01 +00:00
return nil
}