From 52c8be4173f9b725275e3db40b9bd5006769d892 Mon Sep 17 00:00:00 2001 From: Azareal Date: Thu, 22 Nov 2018 17:21:43 +1000 Subject: [PATCH] Variables which are computed multiple times in templates should only be computed once now. Enabled the Watchdog Goroutine. Moved a couple of parser tests into their own file. Eliminated the hard-coded URLs in TestParser() Refactored the alert system so that a specific actor is only loaded once rather than once for each alert. This isn't a problem in practice due to the user cache, but it might be on high traffic sites. Don't run HandleServerSync() on single server setups. Don't load the user in GetTopicUser(), if the topic creator is the current user. Fixed a bug in the template generator where endloop nodes were being pushed instead of endif ones. --- common/alerts.go | 100 ++++++++++++++---------- common/files.go | 9 +++ common/tasks.go | 4 +- common/templates/context.go | 39 ++++++++++ common/templates/templates.go | 111 +++++++++++++++++++++++--- common/topic.go | 23 +++--- common/ws_hub.go | 13 ++-- main.go | 12 ++- misc_test.go | 134 -------------------------------- parser_test.go | 142 ++++++++++++++++++++++++++++++++++ routes.go | 38 ++++++--- routes/reply.go | 8 +- routes/topic.go | 6 +- 13 files changed, 412 insertions(+), 227 deletions(-) create mode 100644 parser_test.go diff --git a/common/alerts.go b/common/alerts.go index cc045d52..417c8f3a 100644 --- a/common/alerts.go +++ b/common/alerts.go @@ -12,10 +12,21 @@ import ( "strconv" "strings" - "github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/common/phrases" + "github.com/Azareal/Gosora/query_gen" ) +type Alert struct { + ASID int + ActorID int + TargetUserID int + Event string + ElementType string + ElementID int + + Actor *User +} + type AlertStmts struct { addActivity *sql.Stmt notifyWatchers *sql.Stmt @@ -50,81 +61,82 @@ func escapeTextInJson(in string) string { return strings.Replace(in, "/", "\\/", -1) } -func BuildAlert(asid int, event string, elementType string, actorID int, targetUserID int, elementID int, user User /* The current user */) (string, error) { +func BuildAlert(alert Alert, user User /* The current user */) (out string, err error) { var targetUser *User - - actor, err := Users.Get(actorID) - if err != nil { - return "", errors.New(phrases.GetErrorPhrase("alerts_no_actor")) + if alert.Actor == nil { + alert.Actor, err = Users.Get(alert.ActorID) + if err != nil { + return "", errors.New(phrases.GetErrorPhrase("alerts_no_actor")) + } } - /*if elementType != "forum" { - targetUser, err = users.Get(targetUserID) + /*if alert.ElementType != "forum" { + targetUser, err = users.Get(alert.TargetUserID) if err != nil { LocalErrorJS("Unable to find the target user",w,r) return } }*/ - if event == "friend_invite" { - return buildAlertString(phrases.GetTmplPhrase("alerts.new_friend_invite"), []string{actor.Name}, actor.Link, actor.Avatar, asid), nil + if alert.Event == "friend_invite" { + return buildAlertString(phrases.GetTmplPhrase("alerts.new_friend_invite"), []string{alert.Actor.Name}, alert.Actor.Link, alert.Actor.Avatar, alert.ASID), nil } // Not that many events for us to handle in a forum - if elementType == "forum" { - if event == "reply" { - topic, err := Topics.Get(elementID) + if alert.ElementType == "forum" { + if alert.Event == "reply" { + topic, err := Topics.Get(alert.ElementID) if err != nil { - DebugLogf("Unable to find linked topic %d", elementID) + DebugLogf("Unable to find linked topic %d", alert.ElementID) return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic")) } // Store the forum ID in the targetUser column instead of making a new one? o.O // Add an additional column for extra information later on when we add the ability to link directly to posts. We don't need the forum data for now... - return buildAlertString(phrases.GetTmplPhrase("alerts.forum_new_topic"), []string{actor.Name, topic.Title}, topic.Link, actor.Avatar, asid), nil + return buildAlertString(phrases.GetTmplPhrase("alerts.forum_new_topic"), []string{alert.Actor.Name, topic.Title}, topic.Link, alert.Actor.Avatar, alert.ASID), nil } - return buildAlertString(phrases.GetTmplPhrase("alerts.forum_unknown_action"), []string{actor.Name}, "", actor.Avatar, asid), nil + return buildAlertString(phrases.GetTmplPhrase("alerts.forum_unknown_action"), []string{alert.Actor.Name}, "", alert.Actor.Avatar, alert.ASID), nil } var url, area string - var phraseName = "alerts." + elementType - switch elementType { + var phraseName = "alerts." + alert.ElementType + switch alert.ElementType { case "topic": - topic, err := Topics.Get(elementID) + topic, err := Topics.Get(alert.ElementID) if err != nil { - DebugLogf("Unable to find linked topic %d", elementID) + DebugLogf("Unable to find linked topic %d", alert.ElementID) return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic")) } url = topic.Link area = topic.Title - if targetUserID == user.ID { + if alert.TargetUserID == user.ID { phraseName += "_own" } case "user": - targetUser, err = Users.Get(elementID) + targetUser, err = Users.Get(alert.ElementID) if err != nil { - DebugLogf("Unable to find target user %d", elementID) + DebugLogf("Unable to find target user %d", alert.ElementID) return "", errors.New(phrases.GetErrorPhrase("alerts_no_target_user")) } area = targetUser.Name url = targetUser.Link - if targetUserID == user.ID { + if alert.TargetUserID == user.ID { phraseName += "_own" } case "post": - topic, err := TopicByReplyID(elementID) + topic, err := TopicByReplyID(alert.ElementID) if err != nil { return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply")) } url = topic.Link area = topic.Title - if targetUserID == user.ID { + if alert.TargetUserID == user.ID { phraseName += "_own" } default: return "", errors.New(phrases.GetErrorPhrase("alerts_invalid_elementtype")) } - switch event { + switch alert.Event { case "like": phraseName += "_like" case "mention": @@ -133,7 +145,7 @@ func BuildAlert(asid int, event string, elementType string, actorID int, targetU phraseName += "_reply" } - return buildAlertString(phrases.GetTmplPhrase(phraseName), []string{actor.Name, area}, url, actor.Avatar, asid), nil + return buildAlertString(phrases.GetTmplPhrase(phraseName), []string{alert.Actor.Name, area}, url, alert.Actor.Avatar, alert.ASID), nil } func buildAlertString(msg string, sub []string, path string, avatar string, asid int) string { @@ -160,22 +172,25 @@ func AddActivityAndNotifyAll(actor int, targetUser int, event string, elementTyp return NotifyWatchers(lastID) } -func AddActivityAndNotifyTarget(actor int, targetUser int, event string, elementType string, elementID int) error { - res, err := alertStmts.addActivity.Exec(actor, targetUser, event, elementType, elementID) - if err != nil { - return err - } - lastID, err := res.LastInsertId() - if err != nil { - return err - } - err = NotifyOne(targetUser, lastID) +func AddActivityAndNotifyTarget(alert Alert) error { + res, err := alertStmts.addActivity.Exec(alert.ActorID, alert.TargetUserID, alert.Event, alert.ElementType, alert.ElementID) if err != nil { return err } + lastID, err := res.LastInsertId() + if err != nil { + return err + } + + err = NotifyOne(alert.TargetUserID, lastID) + if err != nil { + return err + } + alert.ASID = int(lastID) + // Live alerts, if the target is online and WebSockets is enabled - _ = WsHub.pushAlert(targetUser, int(lastID), event, elementType, actor, targetUser, elementID) + _ = WsHub.pushAlert(alert.TargetUserID, alert) return nil } @@ -221,13 +236,12 @@ func notifyWatchers(asid int64) { return } - var actorID, targetUserID, elementID int - var event, elementType string - err = alertStmts.getActivityEntry.QueryRow(asid).Scan(&actorID, &targetUserID, &event, &elementType, &elementID) + var alert = Alert{ASID: int(asid)} + err = alertStmts.getActivityEntry.QueryRow(asid).Scan(&alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID) if err != nil && err != ErrNoRows { LogError(err) return } - _ = WsHub.pushAlerts(uids, int(asid), event, elementType, actorID, targetUserID, elementID) + _ = WsHub.pushAlerts(uids, alert) } diff --git a/common/files.go b/common/files.go index fba5adfc..60cfa3a7 100644 --- a/common/files.go +++ b/common/files.go @@ -147,6 +147,13 @@ func (list SFileList) JSTmplInit() error { data[braceAt] = ' ' // Blank this one too } }) + each(" = []byte(", func(index int) { + braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') + // TODO: Make sure we don't go onto the next line in case someone misplaced a brace + if hasEndBrace { + data[braceAt] = ' ' // Blank it + } + }) each("w.Write(", func(index int) { braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') // TODO: Make sure we don't go onto the next line in case someone misplaced a brace @@ -185,6 +192,8 @@ func (list SFileList) JSTmplInit() error { data = replace(data, shortName+"_tmpl_phrase_id = RegisterTmplPhraseNames([]string{", "[") data = replace(data, "var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let plist = tmplPhrases[\""+tmplName+"\"];") //data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];\nconsole.log('tmplName:','"+tmplName+"')\nconsole.log('phrases:', phrases);") + data = replace(data, "var cached_var_", "let cached_var_") + data = replace(data, " = []byte(", " = ") data = replace(data, "if ", "if(") data = replace(data, "return nil", "return out") data = replace(data, " )", ")") diff --git a/common/tasks.go b/common/tasks.go index f1957047..97e39c96 100644 --- a/common/tasks.go +++ b/common/tasks.go @@ -96,9 +96,9 @@ func HandleExpiredScheduledGroups() error { // TODO: Does this even work? func HandleServerSync() error { // We don't want to run any unnecessary queries when there is nothing to synchronise - /*if Config.ServerCount > 1 { + if Config.ServerCount == 1 { return nil - }*/ + } var lastUpdate time.Time err := taskStmts.getSync.QueryRow().Scan(&lastUpdate) diff --git a/common/templates/context.go b/common/templates/context.go index 68ab718e..53329e58 100644 --- a/common/templates/context.go +++ b/common/templates/context.go @@ -4,6 +4,11 @@ import ( "reflect" ) +// For use in generated code +type FragLite struct { + Body string +} + type Fragment struct { Body string TemplateName string @@ -23,6 +28,7 @@ type CContext struct { VarHolder string HoldReflect reflect.Value TemplateName string + LoopDepth int OutBuf *[]OutBufferFrame } @@ -35,3 +41,36 @@ func (con *CContext) PushText(body string, fragIndex int, fragOutIndex int) (ind *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "text", con.TemplateName, fragIndex, fragOutIndex}) return len(*con.OutBuf) - 1 } + +func (con *CContext) PushPhrase(body string, langIndex int) (index int) { + *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "lang", con.TemplateName, langIndex, nil}) + return len(*con.OutBuf) - 1 +} + +func (con *CContext) StartLoop(body string) (index int) { + con.LoopDepth++ + return con.Push("startloop", body) +} + +func (con *CContext) EndLoop(body string) (index int) { + return con.Push("endloop", body) +} + +func (con *CContext) StartTemplate(body string) (index int) { + *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "starttemplate", con.TemplateName, nil, nil}) + return len(*con.OutBuf) - 1 +} + +func (con *CContext) EndTemplate(body string) (index int) { + return con.Push("endtemplate", body) +} + +func (con *CContext) AttachVars(vars string, index int) { + outBuf := *con.OutBuf + node := outBuf[index] + if node.Type != "starttemplate" && node.Type != "startloop" { + panic("not a starttemplate node") + } + node.Body += vars + outBuf[index] = node +} diff --git a/common/templates/templates.go b/common/templates/templates.go index 8fa71476..3f1e90b3 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -189,7 +189,10 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe c.TemplateFragmentCount = make(map[string]int) } + startIndex := con.StartTemplate("") c.rootIterate(c.templateList[fname], con) + con.EndTemplate("") + c.afterTemplate(con, startIndex) c.TemplateFragmentCount[fname] = c.fragmentCursor[fname] + 1 _, ok := c.FragOnce[fname] @@ -280,6 +283,9 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe skipBlock.Frags[frame.Extra.(int)] = skipBlock.LastCount } writeTextFrame(frame.TemplateName, frame.Extra.(int)-skip) + } else if frame.Type == "varsub" || frame.Type == "cvarsub" { + c.detail(frame.Type + " frame") + fout += "w.Write(" + frame.Body + ")\n" } else { c.detail(frame.Type + " frame") fout += frame.Body @@ -425,12 +431,24 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) { var startIf = func(item reflect.Value, useCopy bool) { con.Push("startif", "if len("+expr+") != 0 {\n") - con.Push("startloop", "for _, item := range "+expr+" {\n") + startIndex := con.StartLoop("for _, item := range " + expr + " {\n") ccon := con - ccon.VarHolder = "item" + var depth string + if ccon.VarHolder == "item" { + depth = strings.TrimPrefix(ccon.VarHolder, "item") + if depth != "" { + idepth, err := strconv.Atoi(depth) + if err != nil { + panic(err) + } + depth = strconv.Itoa(idepth + 1) + } + } + ccon.VarHolder = "item" + depth ccon.HoldReflect = item c.compileSwitch(ccon, node.List) - con.Push("endloop", "}\n") + con.EndLoop("}\n") + c.afterTemplate(con, startIndex) if node.ElseList != nil { con.Push("endif", "}") con.Push("startelse", " else {\n") @@ -440,7 +458,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) { c.compileSwitch(ccon, node.ElseList) con.Push("endelse", "}\n") } else { - con.Push("endloop", "}\n") + con.Push("endif", "}\n") } } @@ -1098,10 +1116,10 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V base = "[]byte(strconv.Itoa(" + varname + "))" case reflect.Bool: con.Push("startif", "if "+varname+" {\n") - con.Push("varsub", "w.Write([]byte(\"true\"))") + con.Push("varsub", "[]byte(\"true\")") con.Push("endif", "} ") con.Push("startelse", "else {\n") - con.Push("varsub", "w.Write([]byte(\"false\"))") + con.Push("varsub", "[]byte(\"false\")") con.Push("endelse", "}\n") return case reflect.String: @@ -1121,7 +1139,6 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V fmt.Println("Unknown Type:", val.Type().Name()) panic("-- I don't know what this variable's type is o.o\n") } - base = "w.Write(" + base + ")\n" c.detail("base: ", base) if assLines == "" { con.Push("varsub", base) @@ -1179,13 +1196,18 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod c.templateList[fname] = tree subtree := c.templateList[fname] c.detail("subtree.Root", subtree.Root) - c.localVars[fname] = make(map[string]VarItemReflect) c.localVars[fname]["."] = VarItemReflect{".", con.VarHolder, con.HoldReflect} c.fragmentCursor[fname] = 0 - con.Push("starttemplate", "{\n") + + var startBit, endBit string + if con.LoopDepth != 0 { + startBit = "{\n" + endBit = "}\n" + } + con.StartTemplate(startBit) c.rootIterate(subtree, con) - con.Push("endtemplate", "}\n") + con.EndTemplate(endBit) c.TemplateFragmentCount[fname] = c.fragmentCursor[fname] + 1 _, ok := c.FragOnce[fname] @@ -1194,6 +1216,75 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod } } +func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) { + c.dumpCall("afterTemplate", con, startIndex) + defer c.retCall("afterTemplate") + + var loopDepth = 0 + var outBuf = *con.OutBuf + var varcounts = make(map[string]int) + var loopStart = startIndex + + if outBuf[startIndex].Type == "startloop" && (len(outBuf) > startIndex+1) { + loopStart++ + } + + // Exclude varsubs within loops for now + for i := loopStart; i < len(outBuf); i++ { + item := outBuf[i] + c.detail("item:", item) + if item.Type == "startloop" { + loopDepth++ + c.detail("loopDepth:", loopDepth) + } else if item.Type == "endloop" { + loopDepth-- + c.detail("loopDepth:", loopDepth) + if loopDepth == -1 { + break + } + } else if item.Type == "varsub" && loopDepth == 0 { + count := varcounts[item.Body] + varcounts[item.Body] = count + 1 + c.detail("count " + strconv.Itoa(count) + " for " + item.Body) + c.detail("loopDepth:", loopDepth) + } + } + + var varstr string + var i int + var varmap = make(map[string]int) + for name, count := range varcounts { + if count > 1 { + varstr += "var cached_var_" + strconv.Itoa(i) + " = " + name + "\n" + varmap[name] = i + i++ + } + } + + // Exclude varsubs within loops for now + loopDepth = 0 + for i := loopStart; i < len(outBuf); i++ { + item := outBuf[i] + if item.Type == "startloop" { + loopDepth++ + } else if item.Type == "endloop" { + loopDepth-- + if loopDepth == -1 { + break + } + } else if item.Type == "varsub" && loopDepth == 0 { + index, ok := varmap[item.Body] + if ok { + item.Body = "cached_var_" + strconv.Itoa(index) + item.Type = "cvarsub" + outBuf[i] = item + } + } + } + + con.AttachVars(varstr, startIndex) +} + // TODO: Should we rethink the way the log methods work or their names? func (c *CTemplateSet) detail(args ...interface{}) { diff --git a/common/topic.go b/common/topic.go index 2ed673ec..6275c5e2 100644 --- a/common/topic.go +++ b/common/topic.go @@ -345,17 +345,18 @@ func TopicByReplyID(rid int) (*Topic, error) { // TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser // TODO: Load LastReplyAt everywhere in here? -func GetTopicUser(tid int) (TopicUser, error) { +func GetTopicUser(user *User, tid int) (tu TopicUser, err error) { tcache := Topics.GetCache() ucache := Users.GetCache() if tcache != nil && ucache != nil { topic, err := tcache.Get(tid) if err == nil { - user, err := Users.Get(topic.CreatedBy) - if err != nil { - return TopicUser{ID: tid}, err + if topic.CreatedBy != user.ID { + user, err = Users.Get(topic.CreatedBy) + if err != nil { + return TopicUser{ID: tid}, err + } } - // We might be better off just passing separate topic and user structs to the caller? return copyTopicToTopicUser(topic, user), nil } else if ucache.Length() < ucache.GetCapacity() { @@ -363,16 +364,18 @@ func GetTopicUser(tid int) (TopicUser, error) { if err != nil { return TopicUser{ID: tid}, err } - user, err := Users.Get(topic.CreatedBy) - if err != nil { - return TopicUser{ID: tid}, err + if topic.CreatedBy != user.ID { + user, err = Users.Get(topic.CreatedBy) + if err != nil { + return TopicUser{ID: tid}, err + } } return copyTopicToTopicUser(topic, user), nil } } - tu := TopicUser{ID: tid} - err := topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.Poll, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level) + tu = TopicUser{ID: tid} + err = topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.Poll, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level) tu.Avatar, tu.MicroAvatar = BuildAvatar(tu.CreatedBy, tu.Avatar) tu.Link = BuildTopicURL(NameToSlug(tu.Title), tu.ID) tu.UserLink = BuildProfileURL(NameToSlug(tu.CreatedByName), tu.CreatedBy) diff --git a/common/ws_hub.go b/common/ws_hub.go index 8fe4a532..6e2bbc7f 100644 --- a/common/ws_hub.go +++ b/common/ws_hub.go @@ -340,21 +340,19 @@ func (hub *WsHubImpl) PushMessage(targetUser int, msg string) error { return wsUser.WriteAll(msg) } -func (hub *WsHubImpl) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { +func (hub *WsHubImpl) pushAlert(targetUser int, alert Alert) error { wsUser, err := hub.getUser(targetUser) if err != nil { return err } - - alert, err := BuildAlert(asid, event, elementType, actorID, targetUserID, elementID, *wsUser.User) + astr, err := BuildAlert(alert, *wsUser.User) if err != nil { return err } - - return wsUser.WriteAll(alert) + return wsUser.WriteAll(astr) } -func (hub *WsHubImpl) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { +func (hub *WsHubImpl) pushAlerts(users []int, alert Alert) error { wsUsers, err := hub.getUsers(users) if err != nil { return err @@ -365,8 +363,7 @@ func (hub *WsHubImpl) pushAlerts(users []int, asid int, event string, elementTyp if wsUser == nil { continue } - - alert, err := BuildAlert(asid, event, elementType, actorID, targetUserID, elementID, *wsUser.User) + alert, err := BuildAlert(alert, *wsUser.User) if err != nil { errs = append(errs, err) } diff --git a/main.go b/main.go index 7565d0d5..0ef5e787 100644 --- a/main.go +++ b/main.go @@ -377,7 +377,7 @@ func main() { go tickLoop(thumbChan, halfSecondTicker, secondTicker, fifteenMinuteTicker, hourTicker) // Resource Management Goroutine - /*go func() { + go func() { ucache := common.Users.GetCache() tcache := common.Topics.GetCache() if ucache == nil && tcache == nil { @@ -393,17 +393,15 @@ func main() { if ucache != nil { ucap := ucache.GetCapacity() if ucache.Length() <= ucap || common.Users.GlobalCount() <= ucap { - countNotDealloc = false + couldNotDealloc = false continue } - lastEvictedCount = ucache.DeallocOverflow(countNotDealloc) - countNotDealloc = (lastEvictedCount == 0) - } else { - countNotDealloc = false + lastEvictedCount = ucache.DeallocOverflow(couldNotDealloc) + couldNotDealloc = (lastEvictedCount == 0) } } } - }()*/ + }() log.Print("Initialising the router") router, err = NewGenRouter(http.FileServer(http.Dir("./uploads"))) diff --git a/misc_test.go b/misc_test.go index 1c5ab8dd..c93150d7 100644 --- a/misc_test.go +++ b/misc_test.go @@ -1213,137 +1213,3 @@ func TestWordCount(t *testing.T) { } } } - -func TestPreparser(t *testing.T) { - var msgList = &METriList{nil} - - // Note: The open tag is evaluated without knowledge of the close tag for efficiency and simplicity, so the parser autofills the associated close tag when it finds an open tag without a partner - msgList.Add("", "") - msgList.Add(" ", "") - msgList.Add(" hi", "hi") - msgList.Add("hi ", "hi") - msgList.Add("hi", "hi") - msgList.Add(":grinning:", "😀") - msgList.Add("😀", "😀") - msgList.Add(" ", "") - msgList.Add("

", "") - msgList.Add("

", "") - msgList.Add("

", "") - - msgList.Add("<", "<") - msgList.Add(">", ">") - msgList.Add("", "<meow>") - msgList.Add("<", "&lt;") - msgList.Add("&", "&") - - // Note: strings.TrimSpace strips newlines, if there's nothing before or after them - msgList.Add("
", "") - msgList.Add("
", "") - msgList.Add("\\n", "\n", "") - msgList.Add("\\n\\n", "\n\n", "") - msgList.Add("\\n\\n\\n", "\n\n\n", "") - msgList.Add("\\r\\n", "\r\n", "") // Windows style line ending - msgList.Add("\\n\\r", "\n\r", "") - - msgList.Add("ho
ho", "ho\n\nho") - msgList.Add("ho
ho", "ho\n\nho") - msgList.Add("ho\\nho", "ho\nho", "ho\nho") - msgList.Add("ho\\n\\nho", "ho\n\nho", "ho\n\nho") - //msgList.Add("ho\\n\\n\\n\\nho", "ho\n\n\n\nho", "ho\n\n\nho") - msgList.Add("ho\\r\\nho", "ho\r\nho", "ho\nho") // Windows style line ending - msgList.Add("ho\\n\\rho", "ho\n\rho", "ho\nho") - - msgList.Add("", "") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("
hi
", "<div>hi</div>") - msgList.Add("hi", "hi") // This is stripped since the editor (Trumbowyg) likes blasting useless spans - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add(">hi", ">hi") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi</b>") - msgList.Add("", "</b>") - msgList.Add("", "</del>") - msgList.Add("", "</strong>") - msgList.Add("", "") - msgList.Add("hi", "hi") - msgList.Add("hi", "hi") - msgList.Add("", "") - msgList.Add("", "") - msgList.Add("", "") - msgList.Add("<>", "<></>") - msgList.Add("<>", "</><>") - msgList.Add("<>", "<>") - msgList.Add("", "</>") - msgList.Add("@", "@") - msgList.Add("@Admin", "@1") - msgList.Add("@Bah", "@Bah") - msgList.Add(" @Admin", "@1") - msgList.Add("\n@Admin", "@1") - msgList.Add("@Admin\n", "@1") - msgList.Add("@Admin\ndd", "@1\ndd") - msgList.Add("d@Admin", "d@Admin") - //msgList.Add("byte 0", string([]byte{0}), "") - msgList.Add("byte 'a'", string([]byte{'a'}), "a") - //msgList.Add("byte 255", string([]byte{255}), "") - //msgList.Add("rune 0", string([]rune{0}), "") - // TODO: Do a test with invalid UTF-8 input - - for _, item := range msgList.Items { - res := common.PreparseMessage(item.Msg) - if res != item.Expects { - if item.Name != "" { - t.Error("Name: ", item.Name) - } - t.Error("Testing string '" + item.Msg + "'") - t.Error("Bad output:", "'"+res+"'") - //t.Error("Ouput in bytes:", []byte(res)) - t.Error("Expected:", "'"+item.Expects+"'") - } - } -} - -func TestParser(t *testing.T) { - var msgList = &METriList{nil} - - msgList.Add("//github.com/Azareal/Gosora", "//github.com/Azareal/Gosora") - msgList.Add("https://github.com/Azareal/Gosora", "https://github.com/Azareal/Gosora") - msgList.Add("http://github.com/Azareal/Gosora", "http://github.com/Azareal/Gosora") - msgList.Add("//github.com/Azareal/Gosora\n", "//github.com/Azareal/Gosora
") - msgList.Add("\n//github.com/Azareal/Gosora", "
//github.com/Azareal/Gosora") - msgList.Add("\n//github.com/Azareal/Gosora\n", "
//github.com/Azareal/Gosora
") - msgList.Add("//github.com/Azareal/Gosora\n//github.com/Azareal/Gosora", "//github.com/Azareal/Gosora
//github.com/Azareal/Gosora") - msgList.Add("//github.com/Azareal/Gosora\n\n//github.com/Azareal/Gosora", "//github.com/Azareal/Gosora

//github.com/Azareal/Gosora") - msgList.Add("//"+common.Site.URL, "//"+common.Site.URL+"") - msgList.Add("//"+common.Site.URL+"\n", "//"+common.Site.URL+"
") - msgList.Add("//"+common.Site.URL+"\n//"+common.Site.URL, "//"+common.Site.URL+"
//"+common.Site.URL+"") - - msgList.Add("#tid-1", "#tid-1") - msgList.Add("https://github.com/Azareal/Gosora/#tid-1", "https://github.com/Azareal/Gosora/#tid-1") - msgList.Add("#fid-1", "#fid-1") - msgList.Add("@1", "@Admin") - msgList.Add("@0", "[Invalid Profile]") - msgList.Add("@-1", "[Invalid Profile]1") - - for _, item := range msgList.Items { - res := common.ParseMessage(item.Msg, 1, "forums") - if res != item.Expects { - if item.Name != "" { - t.Error("Name: ", item.Name) - } - t.Error("Testing string '" + item.Msg + "'") - t.Error("Bad output:", "'"+res+"'") - t.Error("Expected:", "'"+item.Expects+"'") - } - } -} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 00000000..f097f44e --- /dev/null +++ b/parser_test.go @@ -0,0 +1,142 @@ +package main + +import ( + "testing" + + "github.com/Azareal/Gosora/common" +) + +func TestPreparser(t *testing.T) { + var msgList = &METriList{nil} + + // Note: The open tag is evaluated without knowledge of the close tag for efficiency and simplicity, so the parser autofills the associated close tag when it finds an open tag without a partner + msgList.Add("", "") + msgList.Add(" ", "") + msgList.Add(" hi", "hi") + msgList.Add("hi ", "hi") + msgList.Add("hi", "hi") + msgList.Add(":grinning:", "😀") + msgList.Add("😀", "😀") + msgList.Add(" ", "") + msgList.Add("

", "") + msgList.Add("

", "") + msgList.Add("

", "") + + msgList.Add("<", "<") + msgList.Add(">", ">") + msgList.Add("", "<meow>") + msgList.Add("<", "&lt;") + msgList.Add("&", "&") + + // Note: strings.TrimSpace strips newlines, if there's nothing before or after them + msgList.Add("
", "") + msgList.Add("
", "") + msgList.Add("\\n", "\n", "") + msgList.Add("\\n\\n", "\n\n", "") + msgList.Add("\\n\\n\\n", "\n\n\n", "") + msgList.Add("\\r\\n", "\r\n", "") // Windows style line ending + msgList.Add("\\n\\r", "\n\r", "") + + msgList.Add("ho
ho", "ho\n\nho") + msgList.Add("ho
ho", "ho\n\nho") + msgList.Add("ho\\nho", "ho\nho", "ho\nho") + msgList.Add("ho\\n\\nho", "ho\n\nho", "ho\n\nho") + //msgList.Add("ho\\n\\n\\n\\nho", "ho\n\n\n\nho", "ho\n\n\nho") + msgList.Add("ho\\r\\nho", "ho\r\nho", "ho\nho") // Windows style line ending + msgList.Add("ho\\n\\rho", "ho\n\rho", "ho\nho") + + msgList.Add("", "") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("
hi
", "<div>hi</div>") + msgList.Add("hi", "hi") // This is stripped since the editor (Trumbowyg) likes blasting useless spans + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add(">hi", ">hi") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi</b>") + msgList.Add("
", "</b>") + msgList.Add("", "</del>") + msgList.Add("", "</strong>") + msgList.Add("", "") + msgList.Add("hi", "hi") + msgList.Add("hi", "hi") + msgList.Add("", "") + msgList.Add("", "") + msgList.Add("", "") + msgList.Add("<>", "<></>") + msgList.Add("<>", "</><>") + msgList.Add("<>", "<>") + msgList.Add("", "</>") + msgList.Add("@", "@") + msgList.Add("@Admin", "@1") + msgList.Add("@Bah", "@Bah") + msgList.Add(" @Admin", "@1") + msgList.Add("\n@Admin", "@1") + msgList.Add("@Admin\n", "@1") + msgList.Add("@Admin\ndd", "@1\ndd") + msgList.Add("d@Admin", "d@Admin") + //msgList.Add("byte 0", string([]byte{0}), "") + msgList.Add("byte 'a'", string([]byte{'a'}), "a") + //msgList.Add("byte 255", string([]byte{255}), "") + //msgList.Add("rune 0", string([]rune{0}), "") + // TODO: Do a test with invalid UTF-8 input + + for _, item := range msgList.Items { + res := common.PreparseMessage(item.Msg) + if res != item.Expects { + if item.Name != "" { + t.Error("Name: ", item.Name) + } + t.Error("Testing string '" + item.Msg + "'") + t.Error("Bad output:", "'"+res+"'") + //t.Error("Ouput in bytes:", []byte(res)) + t.Error("Expected:", "'"+item.Expects+"'") + } + } +} + +func TestParser(t *testing.T) { + var msgList = &METriList{nil} + + url := "github.com/Azareal/Gosora" + msgList.Add("//"+url, "//"+url+"") + msgList.Add("https://"+url, "https://"+url+"") + msgList.Add("http://"+url, "http://"+url+"") + msgList.Add("//"+url+"\n", "//"+url+"
") + msgList.Add("\n//"+url, "
//"+url+"") + msgList.Add("\n//"+url+"\n", "
//"+url+"
") + msgList.Add("//"+url+"\n//"+url, "//"+url+"
//"+url+"") + msgList.Add("//"+url+"\n\n//"+url, "//"+url+"

//"+url+"") + msgList.Add("//"+common.Site.URL, "//"+common.Site.URL+"") + msgList.Add("//"+common.Site.URL+"\n", "//"+common.Site.URL+"
") + msgList.Add("//"+common.Site.URL+"\n//"+common.Site.URL, "//"+common.Site.URL+"
//"+common.Site.URL+"") + + msgList.Add("#tid-1", "#tid-1") + msgList.Add("https://"+url+"/#tid-1", "https://"+url+"/#tid-1") + msgList.Add("#fid-1", "#fid-1") + msgList.Add("@1", "@Admin") + msgList.Add("@0", "[Invalid Profile]") + msgList.Add("@-1", "[Invalid Profile]1") + + for _, item := range msgList.Items { + res := common.ParseMessage(item.Msg, 1, "forums") + if res != item.Expects { + if item.Name != "" { + t.Error("Name: ", item.Name) + } + t.Error("Testing string '" + item.Msg + "'") + t.Error("Bad output:", "'"+res+"'") + t.Error("Expected:", "'"+item.Expects+"'") + } + } +} diff --git a/routes.go b/routes.go index facc1652..5a3429d2 100644 --- a/routes.go +++ b/routes.go @@ -10,6 +10,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "net/http" "strconv" "strings" @@ -65,10 +66,8 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R return nil } - var msglist, event, elementType string - var asid, actorID, targetUserID, elementID int + 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) @@ -82,22 +81,43 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R } defer rows.Close() + var actors []int + var alerts []common.Alert for rows.Next() { - err = rows.Scan(&asid, &actorID, &targetUserID, &event, &elementType, &elementID) + 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) } - res, err := common.BuildAlert(asid, event, elementType, actorID, targetUserID, elementID, user) - if err != nil { - return common.LocalErrorJS(err.Error(), w, r) - } - msglist += res + "," + 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 { + 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] } diff --git a/routes/reply.go b/routes/reply.go index 5c62198b..1a3882f3 100644 --- a/routes/reply.go +++ b/routes/reply.go @@ -328,7 +328,9 @@ func ProfileReplyCreateSubmit(w http.ResponseWriter, r *http.Request, user commo return common.InternalError(err, w, r) } - err = common.AddActivityAndNotifyTarget(user.ID, profileOwner.ID, "reply", "user", profileOwner.ID) + // ! Be careful about leaking per-route permission state with &user + alert := common.Alert{0, user.ID, profileOwner.ID, "reply", "user", profileOwner.ID, &user} + err = common.AddActivityAndNotifyTarget(alert) if err != nil { return common.InternalError(err, w, r) } @@ -462,7 +464,9 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user common.User, s return common.InternalErrorJSQ(err, w, r, isJs) } - err = common.AddActivityAndNotifyTarget(user.ID, reply.CreatedBy, "like", "post", rid) + // ! Be careful about leaking per-route permission state with &user + alert := common.Alert{0, user.ID, reply.CreatedBy, "like", "post", rid, &user} + err = common.AddActivityAndNotifyTarget(alert) if err != nil { return common.InternalErrorJSQ(err, w, r, isJs) } diff --git a/routes/topic.go b/routes/topic.go index fc2bd92f..259e553f 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -45,7 +45,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header } // Get the topic... - topic, err := common.GetTopicUser(tid) + topic, err := common.GetTopicUser(&user, tid) if err == sql.ErrNoRows { return common.NotFound(w, r, nil) // TODO: Can we add a simplified invocation of headerVars here? This is likely to be an extremely common NotFound } else if err != nil { @@ -822,7 +822,9 @@ func LikeTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, s return common.InternalErrorJSQ(err, w, r, isJs) } - err = common.AddActivityAndNotifyTarget(user.ID, topic.CreatedBy, "like", "topic", tid) + // ! Be careful about leaking per-route permission state with &user + alert := common.Alert{0, user.ID, topic.CreatedBy, "like", "topic", tid, &user} + err = common.AddActivityAndNotifyTarget(alert) if err != nil { return common.InternalErrorJSQ(err, w, r, isJs) }