gosora/routes.go
Azareal 8f2f47e8aa Added the In-Progress Widget Manager UI.
Added the IsoCode field to phrase files.
Rewrote a good portion of the widget system logic.
Added some tests for the widget system.
Added the Online Users widget.
Added a few sealed incomplete widgets like the Search & Filter Widget.
Added the AllUsers method to WsHubImpl for Online Users. Please don't abuse it.

Added the optional *DBTableKey field to AddColumn.
Added the panel_analytics_time_range template to reduce the amount of duplication.
Failed registrations now show up in red in the registration logs for Nox.
Failed logins now show up in red in the login logs for Nox.
Added basic h2 CSS to the other themes.
Added .show_on_block_edit and .hide_on_block_edit to the other themes.
Updated contributing.
Updated a bunch of dates to 2019.
Replaced tblKey{} with nil where possible.
Switched out some &s for &s to reduce the number of possible bugs.
Fixed a bug with selector messages where the inspector would get really jittery due to unnecessary DOM updates.
Moved header.Zone and associated fields to the bottom of ViewTopic to reduce the chances of problems arising.
Added the ZoneData field to *Header.
Added IDs to the items in the forum list template.
Split the fetchPhrases function into the initPhrases and fetchPhrases functions in init.js
Added .colstack_sub_head.
Fixed the CSS in the menu list.
Removed an inline style from the simple topic like and unlike buttons.
Removed an inline style from the simple topic IP button.
Simplified the LoginRequired error handler.
Fixed a typo in the comment prior to DatabaseError()
Reduce the number of false leaves for WebSocket page transitions.
Added the error zone.
De-duped the logic in WsHubImpl.getUsers.
Fixed a potential widget security issue.

Added twenty new phrases.
Added the wid column to the widgets table.

You will need to run the patcher / updater for this commit.
2019-01-21 22:27:59 +10:00

246 lines
7.4 KiB
Go

/*
*
* Gosora Route Handlers
* Copyright Azareal 2016 - 2019
*
*/
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"log"
"net/http"
"strconv"
"strings"
"unicode"
"github.com/Azareal/Gosora/common"
"github.com/Azareal/Gosora/common/phrases"
)
// A blank list to fill out that parameter in Page for routes which don't use it
var tList []interface{}
var successJSONBytes = []byte(`{"success":"1"}`)
// TODO: Refactor this
// TODO: Use the phrase system
var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}],"msgCount":0}`)
// TODO: Refactor this endpoint
// TODO: Move this into the routes package
func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// 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 common.PreErrorJS("Bad Form", w, r)
}
action := r.FormValue("action")
if action != "get" && action != "set" {
return common.PreErrorJS("Invalid Action", w, r)
}
switch r.FormValue("module") {
// TODO: Split this into it's own function
case "dismiss-alert":
asid, err := strconv.Atoi(r.FormValue("asid"))
if err != nil {
return common.PreErrorJS("Invalid asid", w, r)
}
res, err := stmts.deleteActivityStreamMatch.Exec(user.ID, asid)
if err != nil {
return common.InternalError(err, w, r)
}
count, err := res.RowsAffected()
if err != nil {
return common.InternalError(err, w, r)
}
// Don't want to throw an internal error due to a socket closing
if common.EnableWebsockets && count > 0 {
_ = common.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","asid":`+strconv.Itoa(asid)+`}`)
}
// TODO: Split this into it's own function
case "alerts": // A feed of events tailored for a specific user
if !user.Loggedin {
w.Write(phraseLoginAlerts)
return nil
}
var msglist string
var msgCount int
err = stmts.getActivityCountByWatcher.QueryRow(user.ID).Scan(&msgCount)
if err == ErrNoRows {
return common.PreErrorJS("Couldn't find the parent topic", w, r)
} else if err != nil {
return common.InternalErrorJS(err, w, r)
}
rows, err := stmts.getActivityFeedByWatcher.Query(user.ID)
if err != nil {
return common.InternalErrorJS(err, w, r)
}
defer rows.Close()
var actors []int
var alerts []common.Alert
for rows.Next() {
var alert common.Alert
err = rows.Scan(&alert.ASID, &alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID)
if err != nil {
return common.InternalErrorJS(err, w, r)
}
alerts = append(alerts, alert)
actors = append(actors, alert.ActorID)
}
err = rows.Err()
if err != nil {
return common.InternalErrorJS(err, w, r)
}
// 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 := common.Users.BulkGetMap(actors)
if err != nil {
log.Print("actors:", actors)
return common.InternalErrorJS(err, w, r)
}
var ok bool
for _, alert := range alerts {
alert.Actor, ok = list[alert.ActorID]
if !ok {
return common.InternalErrorJS(errors.New("No such actor"), w, r)
}
res, err := common.BuildAlert(alert, user)
if err != nil {
return common.LocalErrorJS(err.Error(), w, r)
}
msglist += res + ","
}
if len(msglist) != 0 {
msglist = msglist[0 : len(msglist)-1]
}
_, _ = w.Write([]byte(`{"msgs":[` + msglist + `],"msgCount":` + strconv.Itoa(msgCount) + `}`))
default:
return common.PreErrorJS("Invalid Module", w, r)
}
return nil
}
// TODO: Remove this line after we move routeAPIPhrases to the routes package
var cacheControlMaxAge = "max-age=" + strconv.Itoa(int(common.Day))
// 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
// TODO: Move to the routes package
func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// 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")
h.Set("Cache-Control", cacheControlMaxAge) //Cache-Control: max-age=31536000
err := r.ParseForm()
if err != nil {
return common.PreErrorJS("Bad Form", w, r)
}
query := r.FormValue("query")
if query == "" {
return common.PreErrorJS("No query provided", w, r)
}
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 != '_' {
return common.PreErrorJS("No symbols allowed, only - and _", w, r)
}
}
negations = append(negations, queryBit)
} else {
for _, char := range queryBit {
if !unicode.IsLetter(char) && char != '-' && char != '_' {
return common.PreErrorJS("No symbols allowed, only - and _", w, r)
}
}
positives = append(positives, queryBit)
}
}
if len(positives) == 0 {
return common.PreErrorJS("You haven't requested any phrases", w, r)
}
var plist map[string]string
// 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 {
// ! Constrain it to topic and status phrases for now
if !strings.HasPrefix(positive, "topic") && !strings.HasPrefix(positive, "status") && !strings.HasPrefix(positive, "alerts") {
return common.PreErrorJS("Not implemented!", w, r)
}
pPhrases, ok := phrases.GetTmplPhrasesByPrefix(positive)
if !ok {
return common.PreErrorJS("No such prefix", w, r)
}
for name, phrase := range pPhrases {
plist[name] = phrase
}
}
} else {
// ! Constrain it to topic and status phrases for now
if !strings.HasPrefix(positives[0], "topic") && !strings.HasPrefix(positives[0], "status") && !strings.HasPrefix(positives[0], "alerts") {
return common.PreErrorJS("Not implemented!", w, r)
}
pPhrases, ok := phrases.GetTmplPhrasesByPrefix(positives[0])
if !ok {
return common.PreErrorJS("No such prefix", w, r)
}
plist = pPhrases
}
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 common.InternalError(err, w, r)
}
w.Write(jsonBytes)
return nil
}
// 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?
func routeJSAntispam(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
h := sha256.New()
h.Write([]byte(common.JSTokenBox.Load().(string)))
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
}