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) }