/* * * Gosora Route Handlers * Copyright Azareal 2016 - 2018 * */ package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "net/http" "strconv" "strings" "unicode" "./common" ) // A blank list to fill out that parameter in Page for routes which don't use it var tList []interface{} //var nList []string 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"}]}`) // TODO: Refactor this endpoint 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") { case "dismiss-alert": asid, err := strconv.Atoi(r.FormValue("asid")) if err != nil { return common.PreErrorJS("Invalid asid", w, r) } _, err = stmts.deleteActivityStreamMatch.Exec(user.ID, asid) if err != nil { return common.InternalError(err, w, r) } case "alerts": // A feed of events tailored for a specific user if !user.Loggedin { w.Write(phraseLoginAlerts) return nil } var msglist, event, elementType string var asid, actorID, targetUserID, elementID int 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() for rows.Next() { err = rows.Scan(&asid, &actorID, &targetUserID, &event, &elementType, &elementID) if err != nil { return common.InternalErrorJS(err, w, r) } res, err := common.BuildAlert(asid, event, elementType, actorID, targetUserID, elementID, user) if err != nil { return common.LocalErrorJS(err.Error(), w, r) } msglist += res + "," } err = rows.Err() if err != nil { return common.InternalErrorJS(err, w, r) } 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: 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 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 w.Header().Set("Content-Type", "application/json") 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 phrases 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 { phrases = 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") { return common.PreErrorJS("Not implemented!", w, r) } pPhrases, ok := common.GetTmplPhrasesByPrefix(positive) if !ok { return common.PreErrorJS("No such prefix", w, r) } for name, phrase := range pPhrases { phrases[name] = phrase } } } else { // ! Constrain it to topic and status phrases for now if !strings.HasPrefix(positives[0], "topic") && !strings.HasPrefix(positives[0], "status") { return common.PreErrorJS("Not implemented!", w, r) } pPhrases, ok := common.GetTmplPhrasesByPrefix(positives[0]) if !ok { return common.PreErrorJS("No such prefix", w, r) } phrases = pPhrases } for _, negation := range negations { for name, _ := range phrases { if strings.HasPrefix(name, negation) { delete(phrases, 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(phrases) 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 }