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 { }
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} ` )
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 {
Cascade delete attachments properly.
Cascade delete replied to topic events for replies properly.
Cascade delete likes on topic posts properly.
Cascade delete replies and their children properly.
Recalculate user stats properly when items are deleted.
Users can now unlike topic opening posts.
Add a recalculator to fix abnormalities across upgrades.
Try fixing a last_ip daily update bug.
Add Existable interface.
Add Delete method to LikeStore.
Add Each, Exists, Create, CountUser, CountMegaUser and CountBigUser methods to ReplyStore.
Add CountUser, CountMegaUser, CountBigUser methods to TopicStore.
Add Each method to UserStore.
Add Add, Delete and DeleteResource methods to SubscriptionStore.
Add Delete, DeleteByParams, DeleteByParamsExtra and AidsByParamsExtra methods to ActivityStream.
Add Exists method to ProfileReplyStore.
Add DropColumn, RenameColumn and ChangeColumn to the database adapters.
Shorten ipaddress column names to ip.
- topics table.
- replies table
- users_replies table.
- polls_votes table.
Add extra column to activity_stream table.
Fix an issue upgrading sites to MariaDB 10.3 from older versions of Gosora. Please report any other issues you find.
You need to run the updater / patcher for this commit.
2020-01-31 07:22:08 +00:00
c . DismissAlert ( user . ID , 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 ( ) {
2019-12-31 21:57:54 +00:00
var al c . Alert
err = rows . Scan ( & al . ASID , & al . ActorID , & al . TargetUserID , & al . Event , & al . ElementType , & al . ElementID , & createdAt )
2019-05-11 23:07:24 +00:00
if err != nil {
return c . InternalErrorJS ( err , w , r )
}
uCreatedAt := createdAt . Unix ( )
//log.Print("uCreatedAt", uCreatedAt)
//if rCreatedAt == 0 || rCreatedAt < uCreatedAt {
2019-12-31 21:57:54 +00:00
alerts = append ( alerts , al )
actors = append ( actors , al . ActorID )
2019-05-11 23:07:24 +00:00
//}
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
}
2020-03-04 04:31:11 +00:00
if count == 0 || len ( alerts ) == 0 || ( rCreatedAt != 0 && rCreatedAt >= topCreatedAt && count == rCount ) {
_ , _ = io . WriteString ( w , ` { } ` )
return nil
}
2018-11-22 07:21:43 +00:00
var ok bool
2020-03-04 04:31:11 +00:00
var sb strings . Builder
2020-03-04 23:56:45 +00:00
sb . Grow ( c . AlertsGrowHint + ( len ( alerts ) * c . AlertsGrowHint2 ) )
2020-03-04 04:31:11 +00:00
sb . WriteString ( ` { "msgs":[ ` )
for i , alert := range alerts {
if i != 0 {
sb . WriteRune ( ',' )
}
2018-11-22 07:21:43 +00:00
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
}
2020-03-04 04:31:11 +00:00
err := c . BuildAlertSb ( & sb , 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
}
2017-09-03 04:50:31 +00:00
}
2020-03-04 04:31:11 +00:00
sb . WriteString ( ` ],"count": ` )
sb . WriteString ( strconv . Itoa ( count ) )
sb . WriteString ( ` ,"tc": ` )
sb . WriteString ( strconv . Itoa ( int ( topCreatedAt ) ) )
sb . WriteRune ( '}' )
2019-05-11 23:07:24 +00:00
2020-03-04 04:31:11 +00:00
_ , _ = io . WriteString ( w , sb . String ( ) )
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 22:09:05 +00:00
var notModified , private bool
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-07-11 22:09:05 +00:00
// TODO: Do we have to be so strict with panel phrases?
2019-05-11 23:07:24 +00:00
if strings . HasPrefix ( positive , "panel" ) {
2019-07-11 22:09:05 +00:00
private = true
2019-04-28 10:08:05 +00:00
ok = user . IsSuperMod
} else {
ok = true
2019-07-11 22:09:05 +00:00
if notModified {
return nil
}
2019-04-28 10:08:05 +00:00
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
}
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 22:09:05 +00:00
if private {
w . Header ( ) . Set ( "Cache-Control" , "private" )
} else if notModified {
2019-07-11 21:51:50 +00:00
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 ) ) )
2019-12-31 21:57:54 +00:00
h . Write ( [ ] byte ( user . GetIP ( ) ) )
2018-07-05 09:54:01 +00:00
jsToken := hex . EncodeToString ( h . Sum ( nil ) )
2019-12-31 21:57:54 +00:00
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
}