From 017bce9c09990225c9a975ec670a5ed89b1a30e2 Mon Sep 17 00:00:00 2001 From: Azareal Date: Sat, 3 Feb 2018 05:47:14 +0000 Subject: [PATCH] Replaced the user agent parser with a faster and more flexible one. More progress on poll posts. Improved the poll UI on the other themes. De-duplicated some avatar logic. Added Exec() to accInsertBuilder. Moved routeOverview to routes.Overview Moved routeCustomPage to routes.CustomPage Moved routeTopicID to routes.ViewTopic Moved routeLogin to routes.AccountLogin Moved routeRegister to routes.AccountRegister Moved routeLoginSubmit to routes.AccountLoginSubmit Moved routeRegisterSubmit to routes.AccountRegisterSubmit We now track the Android Chrome user agent. We now track the UCBrowser user agent. We now track the zgrab user agent. Fixed a few cases where Googlebot wasn't tracked properly. Moved routeStatic to routes.StaticFile --- common/reply.go | 10 +- common/user.go | 19 +- extend/guilds/lib/guilds.go | 8 +- gen_mssql.go | 16 - gen_mysql.go | 14 - gen_router.go | 243 +++++++++------ member_routes.go | 52 +++- panel_routes.go | 9 +- public/global.js | 6 +- query_gen/lib/acc_builders.go | 8 + query_gen/main.go | 4 - router_gen/main.go | 132 +++++--- router_gen/routes.go | 14 +- routes.go | 431 +-------------------------- routes/account.go | 171 +++++++++++ routes/misc.go | 96 ++++++ routes/topic.go | 167 +++++++++++ template_list.go | 291 +++++++++++------- template_topic.go | 262 +++++++++------- templates/create_topic.html | 12 +- templates/forum.html | 14 +- templates/topic.html | 44 ++- templates/topic_alt.html | 23 +- templates/topics.html | 18 +- themes/cosora/public/main.css | 4 + themes/shadow/public/main.css | 70 ++++- themes/tempra-simple/public/main.css | 63 +++- 27 files changed, 1296 insertions(+), 905 deletions(-) create mode 100644 routes/misc.go diff --git a/common/reply.go b/common/reply.go index 362bcbba..a12c9fc5 100644 --- a/common/reply.go +++ b/common/reply.go @@ -78,7 +78,7 @@ func init() { isLiked: acc.Select("likes").Columns("targetItem").Where("sentBy = ? and targetItem = ? and targetType = 'replies'").Prepare(), createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy").Fields("?,?,?,?").Prepare(), edit: acc.Update("replies").Set("content = ?, parsed_content = ?").Where("rid = ? AND poll = 0").Prepare(), - setPoll: acc.Update("replies").Set("content = '', parsed_content = '', poll = ?").Where("rid = ? AND poll = 0").Prepare(), + setPoll: acc.Update("replies").Set("poll = ?").Where("rid = ? AND poll = 0").Prepare(), delete: acc.Delete("replies").Where("rid = ?").Prepare(), addLikesToReply: acc.Update("replies").Set("likeCount = likeCount + ?").Where("rid = ?").Prepare(), removeRepliesFromTopic: acc.Update("topics").Set("postCount = postCount - ?").Where("tid = ?").Prepare(), @@ -142,6 +142,14 @@ func (reply *Reply) Topic() (*Topic, error) { return Topics.Get(reply.ParentID) } +func (reply *Reply) GetID() int { + return reply.ID +} + +func (reply *Reply) GetTable() string { + return "replies" +} + // Copy gives you a non-pointer concurrency safe copy of the reply func (reply *Reply) Copy() Reply { return *reply diff --git a/common/user.go b/common/user.go index 6c5e8096..71285bd9 100644 --- a/common/user.go +++ b/common/user.go @@ -102,13 +102,7 @@ func init() { } func (user *User) Init() { - if user.Avatar != "" { - if user.Avatar[0] == '.' { - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar - } - } else { - user.Avatar = strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(user.ID), 1) - } + user.Avatar = BuildAvatar(user.ID, user.Avatar) user.Link = BuildProfileURL(NameToSlug(user.Name), user.ID) user.Tag = Groups.DirtyGet(user.Group).Tag user.InitPerms() @@ -355,6 +349,17 @@ func (user *User) InitPerms() { } } +// ? Make this part of *User? +func BuildAvatar(uid int, avatar string) string { + if avatar != "" { + if avatar[0] == '.' { + return "/uploads/avatar_" + strconv.Itoa(uid) + avatar + } + return avatar + } + return strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(uid), 1) +} + func BcryptCheckPassword(realPassword string, password string, salt string) (err error) { return bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password+salt)) } diff --git a/extend/guilds/lib/guilds.go b/extend/guilds/lib/guilds.go index f44b6e33..78035934 100644 --- a/extend/guilds/lib/guilds.go +++ b/extend/guilds/lib/guilds.go @@ -333,13 +333,7 @@ func RouteMemberList(w http.ResponseWriter, r *http.Request, user common.User) c return common.InternalError(err, w, r) } guildMember.Link = common.BuildProfileURL(common.NameToSlug(guildMember.User.Name), guildMember.User.ID) - if guildMember.User.Avatar != "" { - if guildMember.User.Avatar[0] == '.' { - guildMember.User.Avatar = "/uploads/avatar_" + strconv.Itoa(guildMember.User.ID) + guildMember.User.Avatar - } - } else { - guildMember.User.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(guildMember.User.ID), 1) - } + guildMember.User.Avatar = common.BuildAvatar(guildMember.User.ID, guildMember.User.Avatar) guildMember.JoinedAt, _ = common.RelativeTimeFromString(guildMember.JoinedAt) if guildItem.Owner == guildMember.User.ID { guildMember.RankString = "Owner" diff --git a/gen_mssql.go b/gen_mssql.go index 76bfac36..279c1925 100644 --- a/gen_mssql.go +++ b/gen_mssql.go @@ -25,7 +25,6 @@ type Stmts struct { groupEntryExists *sql.Stmt getForumTopicsOffset *sql.Stmt getAttachment *sql.Stmt - getTopicRepliesOffset *sql.Stmt getTopicList *sql.Stmt getTopicReplies *sql.Stmt getForumTopics *sql.Stmt @@ -34,7 +33,6 @@ type Stmts struct { createReport *sql.Stmt addActivity *sql.Stmt notifyOne *sql.Stmt - addEmail *sql.Stmt addForumPermsToForum *sql.Stmt addPlugin *sql.Stmt addTheme *sql.Stmt @@ -184,13 +182,6 @@ func _gen_mssql() (err error) { return err } - log.Print("Preparing getTopicRepliesOffset statement.") - stmts.getTopicRepliesOffset, err = db.Prepare("SELECT [replies].[rid],[replies].[content],[replies].[createdBy],[replies].[createdAt],[replies].[lastEdit],[replies].[lastEditBy],[users].[avatar],[users].[name],[users].[group],[users].[url_prefix],[users].[url_name],[users].[level],[replies].[ipaddress],[replies].[likeCount],[replies].[actionType] FROM [replies] LEFT JOIN [users] ON [replies].[createdBy] = [users].[uid] WHERE [replies].[tid] = ?1 ORDER BY replies.rid ASC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY") - if err != nil { - log.Print("Bad Query: ","SELECT [replies].[rid],[replies].[content],[replies].[createdBy],[replies].[createdAt],[replies].[lastEdit],[replies].[lastEditBy],[users].[avatar],[users].[name],[users].[group],[users].[url_prefix],[users].[url_name],[users].[level],[replies].[ipaddress],[replies].[likeCount],[replies].[actionType] FROM [replies] LEFT JOIN [users] ON [replies].[createdBy] = [users].[uid] WHERE [replies].[tid] = ?1 ORDER BY replies.rid ASC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY") - return err - } - log.Print("Preparing getTopicList statement.") stmts.getTopicList, err = db.Prepare("SELECT [topics].[tid],[topics].[title],[topics].[content],[topics].[createdBy],[topics].[is_closed],[topics].[sticky],[topics].[createdAt],[topics].[parentID],[users].[name],[users].[avatar] FROM [topics] LEFT JOIN [users] ON [topics].[createdBy] = [users].[uid] ORDER BY topics.sticky DESC,topics.lastReplyAt DESC,topics.createdBy DESC") if err != nil { @@ -247,13 +238,6 @@ func _gen_mssql() (err error) { return err } - log.Print("Preparing addEmail statement.") - stmts.addEmail, err = db.Prepare("INSERT INTO [emails] ([email],[uid],[validated],[token]) VALUES (?,?,?,?)") - if err != nil { - log.Print("Bad Query: ","INSERT INTO [emails] ([email],[uid],[validated],[token]) VALUES (?,?,?,?)") - return err - } - log.Print("Preparing addForumPermsToForum statement.") stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO [forums_permissions] ([gid],[fid],[preset],[permissions]) VALUES (?,?,?,?)") if err != nil { diff --git a/gen_mysql.go b/gen_mysql.go index 9f3fb318..6c4c5ce8 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -27,7 +27,6 @@ type Stmts struct { groupEntryExists *sql.Stmt getForumTopicsOffset *sql.Stmt getAttachment *sql.Stmt - getTopicRepliesOffset *sql.Stmt getTopicList *sql.Stmt getTopicReplies *sql.Stmt getForumTopics *sql.Stmt @@ -36,7 +35,6 @@ type Stmts struct { createReport *sql.Stmt addActivity *sql.Stmt notifyOne *sql.Stmt - addEmail *sql.Stmt addForumPermsToForum *sql.Stmt addPlugin *sql.Stmt addTheme *sql.Stmt @@ -170,12 +168,6 @@ func _gen_mysql() (err error) { return err } - log.Print("Preparing getTopicRepliesOffset statement.") - stmts.getTopicRepliesOffset, err = db.Prepare("SELECT `replies`.`rid`, `replies`.`content`, `replies`.`createdBy`, `replies`.`createdAt`, `replies`.`lastEdit`, `replies`.`lastEditBy`, `users`.`avatar`, `users`.`name`, `users`.`group`, `users`.`url_prefix`, `users`.`url_name`, `users`.`level`, `replies`.`ipaddress`, `replies`.`likeCount`, `replies`.`actionType` FROM `replies` LEFT JOIN `users` ON `replies`.`createdBy` = `users`.`uid` WHERE `replies`.`tid` = ? ORDER BY replies.rid ASC LIMIT ?,?") - if err != nil { - return err - } - log.Print("Preparing getTopicList statement.") stmts.getTopicList, err = db.Prepare("SELECT `topics`.`tid`, `topics`.`title`, `topics`.`content`, `topics`.`createdBy`, `topics`.`is_closed`, `topics`.`sticky`, `topics`.`createdAt`, `topics`.`parentID`, `users`.`name`, `users`.`avatar` FROM `topics` LEFT JOIN `users` ON `topics`.`createdBy` = `users`.`uid` ORDER BY topics.sticky DESC,topics.lastReplyAt DESC,topics.createdBy DESC") if err != nil { @@ -224,12 +216,6 @@ func _gen_mysql() (err error) { return err } - log.Print("Preparing addEmail statement.") - stmts.addEmail, err = db.Prepare("INSERT INTO `emails`(`email`,`uid`,`validated`,`token`) VALUES (?,?,?,?)") - if err != nil { - return err - } - log.Print("Preparing addForumPermsToForum statement.") stmts.addForumPermsToForum, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) VALUES (?,?,?,?)") if err != nil { diff --git a/gen_router.go b/gen_router.go index 2ca26219..88cc173f 100644 --- a/gen_router.go +++ b/gen_router.go @@ -17,8 +17,8 @@ var ErrNoRoute = errors.New("That route doesn't exist.") // TODO: What about the /uploads/ route? x.x var RouteMap = map[string]interface{}{ "routeAPI": routeAPI, - "routeOverview": routeOverview, - "routeCustomPage": routeCustomPage, + "routes.Overview": routes.Overview, + "routes.CustomPage": routes.CustomPage, "routeForums": routeForums, "routeForum": routeForum, "routeChangeTheme": routeChangeTheme, @@ -92,7 +92,7 @@ var RouteMap = map[string]interface{}{ "routes.UnlockTopicSubmit": routes.UnlockTopicSubmit, "routes.MoveTopicSubmit": routes.MoveTopicSubmit, "routeLikeTopicSubmit": routeLikeTopicSubmit, - "routeTopicID": routeTopicID, + "routes.ViewTopic": routes.ViewTopic, "routeCreateReplySubmit": routeCreateReplySubmit, "routes.ReplyEditSubmit": routes.ReplyEditSubmit, "routes.ReplyDeleteSubmit": routes.ReplyDeleteSubmit, @@ -102,11 +102,11 @@ var RouteMap = map[string]interface{}{ "routes.ProfileReplyDeleteSubmit": routes.ProfileReplyDeleteSubmit, "routes.PollVote": routes.PollVote, "routes.PollResults": routes.PollResults, - "routeLogin": routeLogin, - "routeRegister": routeRegister, + "routes.AccountLogin": routes.AccountLogin, + "routes.AccountRegister": routes.AccountRegister, "routeLogout": routeLogout, - "routeLoginSubmit": routeLoginSubmit, - "routeRegisterSubmit": routeRegisterSubmit, + "routes.AccountLoginSubmit": routes.AccountLoginSubmit, + "routes.AccountRegisterSubmit": routes.AccountRegisterSubmit, "routeDynamic": routeDynamic, "routeUploads": routeUploads, "BadRoute": BadRoute, @@ -115,8 +115,8 @@ var RouteMap = map[string]interface{}{ // ! NEVER RELY ON THESE REMAINING THE SAME BETWEEN COMMITS var routeMapEnum = map[string]int{ "routeAPI": 0, - "routeOverview": 1, - "routeCustomPage": 2, + "routes.Overview": 1, + "routes.CustomPage": 2, "routeForums": 3, "routeForum": 4, "routeChangeTheme": 5, @@ -190,7 +190,7 @@ var routeMapEnum = map[string]int{ "routes.UnlockTopicSubmit": 73, "routes.MoveTopicSubmit": 74, "routeLikeTopicSubmit": 75, - "routeTopicID": 76, + "routes.ViewTopic": 76, "routeCreateReplySubmit": 77, "routes.ReplyEditSubmit": 78, "routes.ReplyDeleteSubmit": 79, @@ -200,19 +200,19 @@ var routeMapEnum = map[string]int{ "routes.ProfileReplyDeleteSubmit": 83, "routes.PollVote": 84, "routes.PollResults": 85, - "routeLogin": 86, - "routeRegister": 87, + "routes.AccountLogin": 86, + "routes.AccountRegister": 87, "routeLogout": 88, - "routeLoginSubmit": 89, - "routeRegisterSubmit": 90, + "routes.AccountLoginSubmit": 89, + "routes.AccountRegisterSubmit": 90, "routeDynamic": 91, "routeUploads": 92, "BadRoute": 93, } var reverseRouteMapEnum = map[int]string{ 0: "routeAPI", - 1: "routeOverview", - 2: "routeCustomPage", + 1: "routes.Overview", + 2: "routes.CustomPage", 3: "routeForums", 4: "routeForum", 5: "routeChangeTheme", @@ -286,7 +286,7 @@ var reverseRouteMapEnum = map[int]string{ 73: "routes.UnlockTopicSubmit", 74: "routes.MoveTopicSubmit", 75: "routeLikeTopicSubmit", - 76: "routeTopicID", + 76: "routes.ViewTopic", 77: "routeCreateReplySubmit", 78: "routes.ReplyEditSubmit", 79: "routes.ReplyDeleteSubmit", @@ -296,11 +296,11 @@ var reverseRouteMapEnum = map[int]string{ 83: "routes.ProfileReplyDeleteSubmit", 84: "routes.PollVote", 85: "routes.PollResults", - 86: "routeLogin", - 87: "routeRegister", + 86: "routes.AccountLogin", + 87: "routes.AccountRegister", 88: "routeLogout", - 89: "routeLoginSubmit", - 90: "routeRegisterSubmit", + 89: "routes.AccountLoginSubmit", + 90: "routes.AccountRegisterSubmit", 91: "routeDynamic", 92: "routeUploads", 93: "BadRoute", @@ -313,18 +313,22 @@ var agentMapEnum = map[string]int{ "safari": 4, "edge": 5, "internetexplorer": 6, - "googlebot": 7, - "yandex": 8, - "bing": 9, - "baidu": 10, - "duckduckgo": 11, - "discord": 12, - "cloudflarealwayson": 13, - "uptimebot": 14, - "lynx": 15, - "blank": 16, - "malformed": 17, - "suspicious": 18, + "androidchrome": 7, + "mobilesafari": 8, + "ucbrowser": 9, + "googlebot": 10, + "yandex": 11, + "bing": 12, + "baidu": 13, + "duckduckgo": 14, + "discord": 15, + "cloudflare": 16, + "uptimebot": 17, + "lynx": 18, + "blank": 19, + "malformed": 20, + "suspicious": 21, + "zgrab": 22, } var reverseAgentMapEnum = map[int]string{ 0: "unknown", @@ -334,19 +338,51 @@ var reverseAgentMapEnum = map[int]string{ 4: "safari", 5: "edge", 6: "internetexplorer", - 7: "googlebot", - 8: "yandex", - 9: "bing", - 10: "baidu", - 11: "duckduckgo", - 12: "discord", - 13: "cloudflarealwayson", - 14: "uptimebot", - 15: "lynx", - 16: "blank", - 17: "malformed", - 18: "suspicious", + 7: "androidchrome", + 8: "mobilesafari", + 9: "ucbrowser", + 10: "googlebot", + 11: "yandex", + 12: "bing", + 13: "baidu", + 14: "duckduckgo", + 15: "discord", + 16: "cloudflare", + 17: "uptimebot", + 18: "lynx", + 19: "blank", + 20: "malformed", + 21: "suspicious", + 22: "zgrab", } +var markToAgent = map[string]string{ + "OPR":"opera", + "Chrome":"chrome", + "Firefox":"firefox", + "MSIE":"internetexplorer", + //"Trident":"internetexplorer", + "Edge":"edge", + "Lynx":"lynx", // There's a rare android variant of lynx which isn't covered by this + "UCBrowser":"ucbrowser", + + "Google":"googlebot", + "Googlebot":"googlebot", + "yandex": "yandex", // from the URL + "DuckDuckBot":"duckduckgo", + "Baiduspider":"baidu", + "bingbot":"bing", + "BingPreview":"bing", + "CloudFlare":"cloudflare", // Track alwayson specifically in case there are other bots? + "Uptimebot":"uptimebot", + "Discordbot":"discord", + + "zgrab":"zgrab", +} +/*var agentRank = map[string]int{ + "opera":9, + "chrome":8, + "safari":1, +}*/ // TODO: Stop spilling these into the package scope? func init() { @@ -420,7 +456,7 @@ func (router *GenRouter) DumpRequest(req *http.Request) { func (router *GenRouter) SuspiciousRequest(req *http.Request) { log.Print("Suspicious Request") router.DumpRequest(req) - common.AgentViewCounter.Bump(18) + common.AgentViewCounter.Bump(21) } // TODO: Pass the default route or config struct to the router rather than accessing it via a package global @@ -448,7 +484,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Write([]byte("")) log.Print("Malformed Request") router.DumpRequest(req) - common.AgentViewCounter.Bump(17) + common.AgentViewCounter.Bump(20) return } @@ -475,7 +511,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } if common.Dev.SuperDebug { - log.Print("before routeStatic") + log.Print("before routes.StaticFile") log.Print("Method: ", req.Method) for key, value := range req.Header { for _, vvalue := range value { @@ -493,7 +529,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { if prefix == "/static" { req.URL.Path += extraData - routeStatic(w, req) + routes.StaticFile(w, req) return } if common.Dev.SuperDebug { @@ -506,49 +542,65 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Track the user agents. Unfortunately, everyone pretends to be Mozilla, so this'll be a little less efficient than I would like. // TODO: Add a setting to disable this? // TODO: Use a more efficient detector instead of smashing every possible combination in - ua := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36")) // Noise, no one's going to be running this and it complicates implementing an efficient UA parser, particularly the more efficient right-to-left one I have in mind - switch { - case strings.Contains(ua,"Google"): - common.AgentViewCounter.Bump(7) - case strings.Contains(ua,"Yandex"): - common.AgentViewCounter.Bump(8) - case strings.Contains(ua,"bingbot"), strings.Contains(ua,"BingPreview"): - common.AgentViewCounter.Bump(9) - case strings.Contains(ua,"OPR"): // Pretends to be Chrome, needs to run before that - common.AgentViewCounter.Bump(3) - case strings.Contains(ua,"Edge"): - common.AgentViewCounter.Bump(5) - case strings.Contains(ua,"Chrome"): - common.AgentViewCounter.Bump(2) - case strings.Contains(ua,"Firefox"): - common.AgentViewCounter.Bump(1) - case strings.Contains(ua,"Safari"): - common.AgentViewCounter.Bump(4) - case strings.Contains(ua,"MSIE"): - common.AgentViewCounter.Bump(6) - case strings.Contains(ua,"Baiduspider"): - common.AgentViewCounter.Bump(10) - case strings.Contains(ua,"DuckDuckBot"): - common.AgentViewCounter.Bump(11) - case strings.Contains(ua,"Discordbot"): - common.AgentViewCounter.Bump(12) - case strings.Contains(ua,"Lynx"): - common.AgentViewCounter.Bump(15) - case strings.Contains(ua,"CloudFlare-AlwaysOnline"): - common.AgentViewCounter.Bump(13) - case strings.Contains(ua,"Uptimebot"): - common.AgentViewCounter.Bump(14) - case ua == "": - common.AgentViewCounter.Bump(16) + ua := strings.TrimSpace(strings.Replace(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36","",-1)) // Noise, no one's going to be running this and it would require some sort of agent ranking system to determine which identifier should be prioritised over another + if ua == "" { + common.AgentViewCounter.Bump(19) if common.Dev.DebugMode { log.Print("Blank UA: ", req.UserAgent()) router.DumpRequest(req) } - default: - common.AgentViewCounter.Bump(0) + } else { + // WIP UA Parser + var indices []int + var items []string + var buffer []rune + for index, item := range ua { + if (item > 64 && item < 91) || (item > 96 && item < 123) { + buffer = append(buffer, item) + } else if len(buffer) != 0 { + items = append(items, string(buffer)) + indices = append(indices, index - 1) + buffer = buffer[:0] + } + } + + // Iterate over this in reverse as the real UA tends to be on the right side + var agent string + for i := len(items) - 1; i >= 0; i-- { + fAgent, ok := markToAgent[items[i]] + if ok { + agent = fAgent + if agent != "safari" { + break + } + } + } + if common.Dev.DebugMode { - log.Print("Unknown UA: ", req.UserAgent()) - router.DumpRequest(req) + log.Print("parsed agent: ",agent) + } + + // Special handling + switch(agent) { + case "chrome": + for _, mark := range items { + if mark == "Android" { + agent = "androidchrome" + break + } + } + case "zgrab": + router.SuspiciousRequest(req) + } + + if agent == "" { + common.AgentViewCounter.Bump(0) + if common.Dev.DebugMode { + log.Print("Unknown UA: ", req.UserAgent()) + router.DumpRequest(req) + } + } else { + common.AgentViewCounter.Bump(agentMapEnum[agent]) } } @@ -572,13 +624,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } case "/overview": common.RouteViewCounter.Bump(1) - err = routeOverview(w,req,user) + err = routes.Overview(w,req,user) if err != nil { router.handleError(err,w,req,user) } case "/pages": common.RouteViewCounter.Bump(2) - err = routeCustomPage(w,req,user,extraData) + err = routes.CustomPage(w,req,user,extraData) if err != nil { router.handleError(err,w,req,user) } @@ -1278,7 +1330,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { err = routeLikeTopicSubmit(w,req,user,extraData) default: common.RouteViewCounter.Bump(76) - err = routeTopicID(w,req,user, extraData) + err = routes.ViewTopic(w,req,user, extraData) } if err != nil { router.handleError(err,w,req,user) @@ -1433,10 +1485,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch(req.URL.Path) { case "/accounts/login/": common.RouteViewCounter.Bump(86) - err = routeLogin(w,req,user) + err = routes.AccountLogin(w,req,user) case "/accounts/create/": common.RouteViewCounter.Bump(87) - err = routeRegister(w,req,user) + err = routes.AccountRegister(w,req,user) case "/accounts/logout/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -1460,7 +1512,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } common.RouteViewCounter.Bump(89) - err = routeLoginSubmit(w,req,user) + err = routes.AccountLoginSubmit(w,req,user) case "/accounts/create/submit/": err = common.ParseForm(w,req,user) if err != nil { @@ -1469,7 +1521,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } common.RouteViewCounter.Bump(90) - err = routeRegisterSubmit(w,req,user) + err = routes.AccountRegisterSubmit(w,req,user) } if err != nil { router.handleError(err,w,req,user) @@ -1537,8 +1589,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } + // TODO: Log all bad routes for the admin to figure out where users are going wrong? lowerPath := strings.ToLower(req.URL.Path) - if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") { + if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") || strings.Contains(lowerPath,"//") || strings.Contains(lowerPath,"\\\\") { router.SuspiciousRequest(req) } common.RouteViewCounter.Bump(93) diff --git a/member_routes.go b/member_routes.go index 8b1f8271..6dd43aba 100644 --- a/member_routes.go +++ b/member_routes.go @@ -107,11 +107,61 @@ func routeCreateReplySubmit(w http.ResponseWriter, r *http.Request, user common. content := common.PreparseMessage(r.PostFormValue("reply-content")) // TODO: Fully parse the post and put that in the parsed column - _, err = common.Rstore.Create(topic, content, user.LastIP, user.ID) + rid, err := common.Rstore.Create(topic, content, user.LastIP, user.ID) if err != nil { return common.InternalError(err, w, r) } + reply, err := common.Rstore.Get(rid) + if err != nil { + return common.LocalError("Unable to load the reply", w, r, user) + } + if r.PostFormValue("has_poll") == "1" { + var maxPollOptions = 10 + var pollInputItems = make(map[int]string) + for key, values := range r.Form { + //if common.Dev.SuperDebug { + log.Print("key: ", key) + log.Printf("values: %+v\n", values) + //} + for _, value := range values { + if strings.HasPrefix(key, "pollinputitem[") { + halves := strings.Split(key, "[") + if len(halves) != 2 { + return common.LocalError("Malformed pollinputitem", w, r, user) + } + halves[1] = strings.TrimSuffix(halves[1], "]") + + index, err := strconv.Atoi(halves[1]) + if err != nil { + return common.LocalError("Malformed pollinputitem", w, r, user) + } + + // If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack + _, exists := pollInputItems[index] + if !exists && len(html.EscapeString(value)) != 0 { + pollInputItems[index] = html.EscapeString(value) + if len(pollInputItems) >= maxPollOptions { + break + } + } + } + } + } + + // Make sure the indices are sequential to avoid out of bounds issues + var seqPollInputItems = make(map[int]string) + for i := 0; i < len(pollInputItems); i++ { + seqPollInputItems[i] = pollInputItems[i] + } + + pollType := 0 // Basic single choice + _, err := common.Polls.Create(reply, pollType, seqPollInputItems) + if err != nil { + return common.LocalError("Failed to add poll to reply", w, r, user) // TODO: Might need to be an internal error as it could leave phantom polls? + } + } + err = common.Forums.UpdateLastTopic(tid, user.ID, topic.ParentID) if err != nil && err != ErrNoRows { return common.InternalError(err, w, r) diff --git a/panel_routes.go b/panel_routes.go index 1bc94edd..412e0098 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -1501,14 +1501,7 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user common.User) c } puser.InitPerms() - if puser.Avatar != "" { - if puser.Avatar[0] == '.' { - puser.Avatar = "/uploads/avatar_" + strconv.Itoa(puser.ID) + puser.Avatar - } - } else { - puser.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(puser.ID), 1) - } - + puser.Avatar = common.BuildAvatar(puser.ID, puser.Avatar) if common.Groups.DirtyGet(puser.Group).Tag != "" { puser.Tag = common.Groups.DirtyGet(puser.Group).Tag } else { diff --git a/public/global.js b/public/global.js index 21b73d53..1647517c 100644 --- a/public/global.js +++ b/public/global.js @@ -590,7 +590,7 @@ $(document).ready(function(){ if(dataPollInput == undefined) return; if(dataPollInput != (pollInputIndex-1)) return; - $(".poll_content_row .formitem").append("
"); + $(".poll_content_row .formitem").append("
"); pollInputIndex++; console.log("new pollInputIndex: ", pollInputIndex); $(".pollinputinput").off("click"); @@ -605,9 +605,11 @@ $(document).ready(function(){ $(".pollinputinput").click(addPollInput); }); + //id="poll_results_{{.Poll.ID}}" class="poll_results auto_hide" $(".poll_results_button").click(function(){ let pollID = $(this).attr("data-poll-id"); $("#poll_results_" + pollID + " .user_content").html("
"); + $("#poll_results_" + pollID).removeClass("auto_hide"); fetch("/poll/results/" + pollID, { credentials: 'same-origin' }).then((response) => response.text()).catch((error) => console.error("Error:",error)).then((rawData) => { @@ -618,7 +620,7 @@ $(document).ready(function(){ Chartist.Pie('#poll_results_chart_' + pollID, { series: data, }, { - height: '100%', + height: '120px', }); }) }); diff --git a/query_gen/lib/acc_builders.go b/query_gen/lib/acc_builders.go index 10aa5703..12920e1f 100644 --- a/query_gen/lib/acc_builders.go +++ b/query_gen/lib/acc_builders.go @@ -123,6 +123,14 @@ func (insert *accInsertBuilder) Prepare() *sql.Stmt { return insert.build.SimpleInsert(insert.table, insert.columns, insert.fields) } +func (insert *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err error) { + stmt := insert.Prepare() + if stmt != nil { + return stmt.Exec(args...) + } + return res, insert.build.FirstError() +} + type accCountBuilder struct { table string where string diff --git a/query_gen/main.go b/query_gen/main.go index 72be2475..91d7fd0e 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -255,8 +255,6 @@ func writeSelects(adapter qgen.Adapter) error { } func writeLeftJoins(adapter qgen.Adapter) error { - adapter.SimpleLeftJoin("getTopicRepliesOffset", "replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress, replies.likeCount, replies.actionType", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?") - adapter.SimpleLeftJoin("getTopicList", "topics", "users", "topics.tid, topics.title, topics.content, topics.createdBy, topics.is_closed, topics.sticky, topics.createdAt, topics.parentID, users.name, users.avatar", "topics.createdBy = users.uid", "", "topics.sticky DESC, topics.lastReplyAt DESC, topics.createdBy DESC", "") adapter.SimpleLeftJoin("getTopicReplies", "replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress", "replies.createdBy = users.uid", "tid = ?", "", "") @@ -286,8 +284,6 @@ func writeInserts(adapter qgen.Adapter) error { build.Insert("notifyOne").Table("activity_stream_matches").Columns("watcher, asid").Fields("?,?").Parse() - build.Insert("addEmail").Table("emails").Columns("email, uid, validated, token").Fields("?,?,?,?").Parse() - build.Insert("addForumPermsToForum").Table("forums_permissions").Columns("gid,fid,preset,permissions").Fields("?,?,?,?").Parse() build.Insert("addPlugin").Table("plugins").Columns("uname, active, installed").Fields("?,?,?").Parse() diff --git a/router_gen/main.go b/router_gen/main.go index dc134e7f..6697a5c1 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -167,18 +167,23 @@ func main() { "edge", "internetexplorer", + "androidchrome", + "mobilesafari", // Coming soon + "ucbrowser", + "googlebot", "yandex", "bing", "baidu", "duckduckgo", "discord", - "cloudflarealwayson", + "cloudflare", "uptimebot", "lynx", "blank", "malformed", "suspicious", + "zgrab", } tmplVars.AllAgentMap = make(map[string]int) @@ -220,6 +225,34 @@ var agentMapEnum = map[string]int{ {{range $index, $element := .AllAgentNames}} var reverseAgentMapEnum = map[int]string{ {{range $index, $element := .AllAgentNames}} {{$index}}: "{{$element}}",{{end}} } +var markToAgent = map[string]string{ + "OPR":"opera", + "Chrome":"chrome", + "Firefox":"firefox", + "MSIE":"internetexplorer", + //"Trident":"internetexplorer", + "Edge":"edge", + "Lynx":"lynx", // There's a rare android variant of lynx which isn't covered by this + "UCBrowser":"ucbrowser", + + "Google":"googlebot", + "Googlebot":"googlebot", + "yandex": "yandex", // from the URL + "DuckDuckBot":"duckduckgo", + "Baiduspider":"baidu", + "bingbot":"bing", + "BingPreview":"bing", + "CloudFlare":"cloudflare", // Track alwayson specifically in case there are other bots? + "Uptimebot":"uptimebot", + "Discordbot":"discord", + + "zgrab":"zgrab", +} +/*var agentRank = map[string]int{ + "opera":9, + "chrome":8, + "safari":1, +}*/ // TODO: Stop spilling these into the package scope? func init() { @@ -348,7 +381,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } if common.Dev.SuperDebug { - log.Print("before routeStatic") + log.Print("before routes.StaticFile") log.Print("Method: ", req.Method) for key, value := range req.Header { for _, vvalue := range value { @@ -366,7 +399,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { if prefix == "/static" { req.URL.Path += extraData - routeStatic(w, req) + routes.StaticFile(w, req) return } if common.Dev.SuperDebug { @@ -379,49 +412,65 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Track the user agents. Unfortunately, everyone pretends to be Mozilla, so this'll be a little less efficient than I would like. // TODO: Add a setting to disable this? // TODO: Use a more efficient detector instead of smashing every possible combination in - ua := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36")) // Noise, no one's going to be running this and it complicates implementing an efficient UA parser, particularly the more efficient right-to-left one I have in mind - switch { - case strings.Contains(ua,"Google"): - common.AgentViewCounter.Bump({{.AllAgentMap.googlebot}}) - case strings.Contains(ua,"Yandex"): - common.AgentViewCounter.Bump({{.AllAgentMap.yandex}}) - case strings.Contains(ua,"bingbot"), strings.Contains(ua,"BingPreview"): - common.AgentViewCounter.Bump({{.AllAgentMap.bing}}) - case strings.Contains(ua,"OPR"): // Pretends to be Chrome, needs to run before that - common.AgentViewCounter.Bump({{.AllAgentMap.opera}}) - case strings.Contains(ua,"Edge"): - common.AgentViewCounter.Bump({{.AllAgentMap.edge}}) - case strings.Contains(ua,"Chrome"): - common.AgentViewCounter.Bump({{.AllAgentMap.chrome}}) - case strings.Contains(ua,"Firefox"): - common.AgentViewCounter.Bump({{.AllAgentMap.firefox}}) - case strings.Contains(ua,"Safari"): - common.AgentViewCounter.Bump({{.AllAgentMap.safari}}) - case strings.Contains(ua,"MSIE"): - common.AgentViewCounter.Bump({{.AllAgentMap.internetexplorer}}) - case strings.Contains(ua,"Baiduspider"): - common.AgentViewCounter.Bump({{.AllAgentMap.baidu}}) - case strings.Contains(ua,"DuckDuckBot"): - common.AgentViewCounter.Bump({{.AllAgentMap.duckduckgo}}) - case strings.Contains(ua,"Discordbot"): - common.AgentViewCounter.Bump({{.AllAgentMap.discord}}) - case strings.Contains(ua,"Lynx"): - common.AgentViewCounter.Bump({{.AllAgentMap.lynx}}) - case strings.Contains(ua,"CloudFlare-AlwaysOnline"): - common.AgentViewCounter.Bump({{.AllAgentMap.cloudflarealwayson}}) - case strings.Contains(ua,"Uptimebot"): - common.AgentViewCounter.Bump({{.AllAgentMap.uptimebot}}) - case ua == "": + ua := strings.TrimSpace(strings.Replace(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36","",-1)) // Noise, no one's going to be running this and it would require some sort of agent ranking system to determine which identifier should be prioritised over another + if ua == "" { common.AgentViewCounter.Bump({{.AllAgentMap.blank}}) if common.Dev.DebugMode { log.Print("Blank UA: ", req.UserAgent()) router.DumpRequest(req) } - default: - common.AgentViewCounter.Bump({{.AllAgentMap.unknown}}) + } else { + // WIP UA Parser + var indices []int + var items []string + var buffer []rune + for index, item := range ua { + if (item > 64 && item < 91) || (item > 96 && item < 123) { + buffer = append(buffer, item) + } else if len(buffer) != 0 { + items = append(items, string(buffer)) + indices = append(indices, index - 1) + buffer = buffer[:0] + } + } + + // Iterate over this in reverse as the real UA tends to be on the right side + var agent string + for i := len(items) - 1; i >= 0; i-- { + fAgent, ok := markToAgent[items[i]] + if ok { + agent = fAgent + if agent != "safari" { + break + } + } + } + if common.Dev.DebugMode { - log.Print("Unknown UA: ", req.UserAgent()) - router.DumpRequest(req) + log.Print("parsed agent: ",agent) + } + + // Special handling + switch(agent) { + case "chrome": + for _, mark := range items { + if mark == "Android" { + agent = "androidchrome" + break + } + } + case "zgrab": + router.SuspiciousRequest(req) + } + + if agent == "" { + common.AgentViewCounter.Bump({{.AllAgentMap.unknown}}) + if common.Dev.DebugMode { + log.Print("Unknown UA: ", req.UserAgent()) + router.DumpRequest(req) + } + } else { + common.AgentViewCounter.Bump(agentMapEnum[agent]) } } @@ -500,8 +549,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } + // TODO: Log all bad routes for the admin to figure out where users are going wrong? lowerPath := strings.ToLower(req.URL.Path) - if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") { + if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") || strings.Contains(lowerPath,"//") || strings.Contains(lowerPath,"\\\\") { router.SuspiciousRequest(req) } common.RouteViewCounter.Bump({{.AllRouteMap.BadRoute}}) diff --git a/router_gen/routes.go b/router_gen/routes.go index 53d81d90..812db555 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -3,8 +3,8 @@ package main // TODO: How should we handle headerLite and headerVar? func routes() { addRoute(View("routeAPI", "/api/")) - addRoute(View("routeOverview", "/overview/")) - addRoute(View("routeCustomPage", "/pages/", "extraData")) + addRoute(View("routes.Overview", "/overview/")) + addRoute(View("routes.CustomPage", "/pages/", "extraData")) addRoute(View("routeForums", "/forums/" /*,"&forums"*/)) addRoute(View("routeForum", "/forum/", "extraData")) addRoute(AnonAction("routeChangeTheme", "/theme/")) @@ -65,7 +65,7 @@ func buildUserRoutes() { func buildTopicRoutes() { topicGroup := newRouteGroup("/topic/") topicGroup.Routes( - View("routeTopicID", "/topic/", "extraData"), + View("routes.ViewTopic", "/topic/", "extraData"), UploadAction("routes.CreateTopicSubmit", "/topic/create/submit/").MaxSizeVar("common.Config.MaxRequestSize"), Action("routes.EditTopicSubmit", "/topic/edit/submit/", "extraData"), Action("routes.DeleteTopicSubmit", "/topic/delete/submit/").LitBefore("req.URL.Path += extraData"), @@ -118,11 +118,11 @@ func buildAccountRoutes() { //router.HandleFunc("/accounts/list/", routeLogin) // Redirect /accounts/ and /user/ to here.. // Get a list of all of the accounts on the forum accReplyGroup := newRouteGroup("/accounts/") accReplyGroup.Routes( - View("routeLogin", "/accounts/login/"), - View("routeRegister", "/accounts/create/"), + View("routes.AccountLogin", "/accounts/login/"), + View("routes.AccountRegister", "/accounts/create/"), Action("routeLogout", "/accounts/logout/"), - AnonAction("routeLoginSubmit", "/accounts/login/submit/"), // TODO: Guard this with a token, maybe the IP hashed with a rotated key? - AnonAction("routeRegisterSubmit", "/accounts/create/submit/"), + AnonAction("routes.AccountLoginSubmit", "/accounts/login/submit/"), // TODO: Guard this with a token, maybe the IP hashed with a rotated key? + AnonAction("routes.AccountRegisterSubmit", "/accounts/create/submit/"), ) addRouteGroup(accReplyGroup) } diff --git a/routes.go b/routes.go index 017b0440..d6ed1ecc 100644 --- a/routes.go +++ b/routes.go @@ -7,9 +7,7 @@ package main import ( - "bytes" "html" - "io" "log" "net/http" "strconv" @@ -25,7 +23,6 @@ var tList []interface{} //var nList []string var successJSONBytes = []byte(`{"success":"1"}`) -var cacheControlMaxAge = "max-age=" + strconv.Itoa(common.Day) // TODO: Make this a common.Config value // HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS type HTTPSRedirect struct { @@ -48,87 +45,6 @@ func routeUploads() { func BadRoute() { } -// GET functions -func routeStatic(w http.ResponseWriter, r *http.Request) { - file, ok := common.StaticFiles.Get(r.URL.Path) - if !ok { - if common.Dev.DebugMode { - log.Printf("Failed to find '%s'", r.URL.Path) - } - w.WriteHeader(http.StatusNotFound) - return - } - h := w.Header() - - // Surely, there's a more efficient way of doing this? - t, err := time.Parse(http.TimeFormat, h.Get("If-Modified-Since")) - if err == nil && file.Info.ModTime().Before(t.Add(1*time.Second)) { - w.WriteHeader(http.StatusNotModified) - return - } - h.Set("Last-Modified", file.FormattedModTime) - h.Set("Content-Type", file.Mimetype) - h.Set("Cache-Control", cacheControlMaxAge) //Cache-Control: max-age=31536000 - h.Set("Vary", "Accept-Encoding") - if strings.Contains(h.Get("Accept-Encoding"), "gzip") { - h.Set("Content-Encoding", "gzip") - h.Set("Content-Length", strconv.FormatInt(file.GzipLength, 10)) - io.Copy(w, bytes.NewReader(file.GzipData)) // Use w.Write instead? - } else { - h.Set("Content-Length", strconv.FormatInt(file.Length, 10)) // Avoid doing a type conversion every time? - io.Copy(w, bytes.NewReader(file.Data)) - } - // Other options instead of io.Copy: io.CopyN(), w.Write(), http.ServeContent() -} - -func routeOverview(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, ferr := common.UserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.Zone = "overview" - - pi := common.Page{common.GetTitlePhrase("overview"), user, headerVars, tList, nil} - if common.PreRenderHooks["pre_render_overview"] != nil { - if common.RunPreRenderHook("pre_render_overview", w, r, &user, &pi) { - return nil - } - } - - err := common.Templates.ExecuteTemplate(w, "overview.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - -func routeCustomPage(w http.ResponseWriter, r *http.Request, user common.User, name string) common.RouteError { - headerVars, ferr := common.UserCheck(w, r, &user) - if ferr != nil { - return ferr - } - - // ! Is this safe? - if common.Templates.Lookup("page_"+name+".html") == nil { - return common.NotFound(w, r) - } - headerVars.Zone = "custom_page" - - pi := common.Page{common.GetTitlePhrase("page"), user, headerVars, tList, nil} - // TODO: Pass the page name to the pre-render hook? - if common.PreRenderHooks["pre_render_custom_page"] != nil { - if common.RunPreRenderHook("pre_render_custom_page", w, r, &user, &pi) { - return nil - } - } - - err := common.Templates.ExecuteTemplate(w, "page_"+name+".html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, ferr := common.UserCheck(w, r, &user) if ferr != nil { @@ -420,181 +336,6 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo return nil } -func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User, urlBit string) common.RouteError { - var err error - var replyList []common.ReplyUser - - page, _ := strconv.Atoi(r.FormValue("page")) - - // SEO URLs... - // TODO: Make a shared function for this - halves := strings.Split(urlBit, ".") - if len(halves) < 2 { - halves = append(halves, halves[0]) - } - - tid, err := strconv.Atoi(halves[1]) - if err != nil { - return common.PreError("The provided TopicID is not a valid number.", w, r) - } - - // Get the topic... - topic, err := common.GetTopicUser(tid) - if err == ErrNoRows { - return common.NotFound(w, r) - } else if err != nil { - return common.InternalError(err, w, r) - } - topic.ClassName = "" - //log.Printf("topic: %+v\n", topic) - - headerVars, ferr := common.ForumUserCheck(w, r, &user, topic.ParentID) - if ferr != nil { - return ferr - } - if !user.Perms.ViewTopic { - //log.Printf("user.Perms: %+v\n", user.Perms) - return common.NoPermissions(w, r, user) - } - headerVars.Zone = "view_topic" - // TODO: Only include these on pages with polls - headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") - headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") - - topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums") - topic.ContentLines = strings.Count(topic.Content, "\n") - - // We don't want users posting in locked topics... - if topic.IsClosed && !user.IsMod { - user.Perms.CreateReply = false - } - - postGroup, err := common.Groups.Get(topic.Group) - if err != nil { - return common.InternalError(err, w, r) - } - - topic.Tag = postGroup.Tag - if postGroup.IsMod || postGroup.IsAdmin { - topic.ClassName = common.Config.StaffCSS - } - topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt) - - // TODO: Make a function for this? Build a more sophisticated noavatar handling system? - if topic.Avatar != "" { - if topic.Avatar[0] == '.' { - topic.Avatar = "/uploads/avatar_" + strconv.Itoa(topic.CreatedBy) + topic.Avatar - } - } else { - topic.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(topic.CreatedBy), 1) - } - - var poll common.Poll - if topic.Poll != 0 { - pPoll, err := common.Polls.Get(topic.Poll) - if err != nil { - log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID)) - return common.InternalError(err, w, r) - } - poll = pPoll.Copy() - } - - // Calculate the offset - offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) - tpage := common.TopicPage{topic.Title, user, headerVars, replyList, topic, poll, page, lastPage} - - // Get the replies.. - rows, err := stmts.getTopicRepliesOffset.Query(topic.ID, offset, common.Config.ItemsPerPage) - if err == ErrNoRows { - return common.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - defer rows.Close() - - replyItem := common.ReplyUser{ClassName: ""} - for rows.Next() { - err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType) - if err != nil { - return common.InternalError(err, w, r) - } - - replyItem.UserLink = common.BuildProfileURL(common.NameToSlug(replyItem.CreatedByName), replyItem.CreatedBy) - replyItem.ParentID = topic.ID - replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums") - replyItem.ContentLines = strings.Count(replyItem.Content, "\n") - - postGroup, err = common.Groups.Get(replyItem.Group) - if err != nil { - return common.InternalError(err, w, r) - } - - if postGroup.IsMod || postGroup.IsAdmin { - replyItem.ClassName = common.Config.StaffCSS - } else { - replyItem.ClassName = "" - } - - // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this? - if replyItem.Avatar != "" { - if replyItem.Avatar[0] == '.' { - replyItem.Avatar = "/uploads/avatar_" + strconv.Itoa(replyItem.CreatedBy) + replyItem.Avatar - } - } else { - replyItem.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(replyItem.CreatedBy), 1) - } - - replyItem.Tag = postGroup.Tag - replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt) - - // We really shouldn't have inline HTML, we should do something about this... - if replyItem.ActionType != "" { - switch replyItem.ActionType { - case "lock": - replyItem.ActionType = "This topic has been locked by " + replyItem.CreatedByName + "" - replyItem.ActionIcon = "🔒︎" - case "unlock": - replyItem.ActionType = "This topic has been reopened by " + replyItem.CreatedByName + "" - replyItem.ActionIcon = "🔓︎" - case "stick": - replyItem.ActionType = "This topic has been pinned by " + replyItem.CreatedByName + "" - replyItem.ActionIcon = "📌︎" - case "unstick": - replyItem.ActionType = "This topic has been unpinned by " + replyItem.CreatedByName + "" - replyItem.ActionIcon = "📌︎" - case "move": - replyItem.ActionType = "This topic has been moved by " + replyItem.CreatedByName + "" - default: - replyItem.ActionType = replyItem.ActionType + " has happened" - replyItem.ActionIcon = "" - } - } - replyItem.Liked = false - - if common.Vhooks["topic_reply_row_assign"] != nil { - common.RunVhook("topic_reply_row_assign", &tpage, &replyItem) - } - replyList = append(replyList, replyItem) - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - tpage.ItemList = replyList - if common.PreRenderHooks["pre_render_view_topic"] != nil { - if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) { - return nil - } - } - err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, w) - if err != nil { - return common.InternalError(err, w, r) - } - common.TopicViewCounter.Bump(topic.ID) // TODO Move this into the router? - return nil -} - func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, ferr := common.UserCheck(w, r, &user) if ferr != nil { @@ -658,14 +399,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm } else { replyClassName = "" } - - if replyAvatar != "" { - if replyAvatar[0] == '.' { - replyAvatar = "/uploads/avatar_" + strconv.Itoa(replyCreatedBy) + replyAvatar - } - } else { - replyAvatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(replyCreatedBy), 1) - } + replyAvatar = common.BuildAvatar(replyCreatedBy, replyAvatar) if group.Tag != "" { replyTag = group.Tag @@ -703,169 +437,6 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm return nil } -func routeLogin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, ferr := common.UserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if user.Loggedin { - return common.LocalError("You're already logged in.", w, r, user) - } - pi := common.Page{common.GetTitlePhrase("login"), user, headerVars, tList, nil} - if common.PreRenderHooks["pre_render_login"] != nil { - if common.RunPreRenderHook("pre_render_login", w, r, &user, &pi) { - return nil - } - } - err := common.Templates.ExecuteTemplate(w, "login.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - -// TODO: Log failed attempted logins? -// TODO: Lock IPS out if they have too many failed attempts? -// TODO: Log unusual countries in comparison to the country a user usually logs in from? Alert the user about this? -func routeLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - if user.Loggedin { - return common.LocalError("You're already logged in.", w, r, user) - } - - username := html.EscapeString(strings.Replace(r.PostFormValue("username"), "\n", "", -1)) - uid, err := common.Auth.Authenticate(username, r.PostFormValue("password")) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - userPtr, err := common.Users.Get(uid) - if err != nil { - return common.LocalError("Bad account", w, r, user) - } - user = *userPtr - - var session string - if user.Session == "" { - session, err = common.Auth.CreateSession(uid) - if err != nil { - return common.InternalError(err, w, r) - } - } else { - session = user.Session - } - - common.Auth.SetCookies(w, uid, session) - if user.IsAdmin { - // Is this error check redundant? We already check for the error in PreRoute for the same IP - // TODO: Should we be logging this? - log.Printf("#%d has logged in with IP %s", uid, user.LastIP) - } - http.Redirect(w, r, "/", http.StatusSeeOther) - return nil -} - -func routeRegister(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerVars, ferr := common.UserCheck(w, r, &user) - if ferr != nil { - return ferr - } - if user.Loggedin { - return common.LocalError("You're already logged in.", w, r, user) - } - pi := common.Page{common.GetTitlePhrase("register"), user, headerVars, tList, nil} - if common.PreRenderHooks["pre_render_register"] != nil { - if common.RunPreRenderHook("pre_render_register", w, r, &user, &pi) { - return nil - } - } - err := common.Templates.ExecuteTemplate(w, "register.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - -func routeRegisterSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - headerLite, _ := common.SimpleUserCheck(w, r, &user) - - username := html.EscapeString(strings.Replace(r.PostFormValue("username"), "\n", "", -1)) - if username == "" { - return common.LocalError("You didn't put in a username.", w, r, user) - } - email := html.EscapeString(strings.Replace(r.PostFormValue("email"), "\n", "", -1)) - if email == "" { - return common.LocalError("You didn't put in an email.", w, r, user) - } - - password := r.PostFormValue("password") - switch password { - case "": - return common.LocalError("You didn't put in a password.", w, r, user) - case username: - return common.LocalError("You can't use your username as your password.", w, r, user) - case email: - return common.LocalError("You can't use your email as your password.", w, r, user) - } - - // ? Move this into Create()? What if we want to programatically set weak passwords for tests? - err := common.WeakPassword(password) - if err != nil { - return common.LocalError(err.Error(), w, r, user) - } - - confirmPassword := r.PostFormValue("confirm_password") - if common.Dev.DebugMode { - log.Print("Registration Attempt! Username: " + username) // TODO: Add more controls over what is logged when? - } - - // Do the two inputted passwords match..? - if password != confirmPassword { - return common.LocalError("The two passwords don't match.", w, r, user) - } - - var active bool - var group int - switch headerLite.Settings["activation_type"] { - case 1: // Activate All - active = true - group = common.Config.DefaultGroup - default: // Anything else. E.g. Admin Activation or Email Activation. - group = common.Config.ActivationGroup - } - - uid, err := common.Users.Create(username, password, email, group, active) - if err == common.ErrAccountExists { - return common.LocalError("This username isn't available. Try another.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - // Check if this user actually owns this email, if email activation is on, automatically flip their account to active when the email is validated. Validation is also useful for determining whether this user should receive any alerts, etc. via email - if common.Site.EnableEmails { - token, err := common.GenerateSafeString(80) - if err != nil { - return common.InternalError(err, w, r) - } - _, err = stmts.addEmail.Exec(email, uid, 0, token) - if err != nil { - return common.InternalError(err, w, r) - } - - if !common.SendValidationEmail(username, email, token) { - return common.LocalError("We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.", w, r, user) - } - } - - session, err := common.Auth.CreateSession(uid) - if err != nil { - return common.InternalError(err, w, r) - } - - common.Auth.SetCookies(w, uid, session) - http.Redirect(w, r, "/", http.StatusSeeOther) - return nil -} - // TODO: Set the cookie domain func routeChangeTheme(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { //headerLite, _ := SimpleUserCheck(w, r, &user) diff --git a/routes/account.go b/routes/account.go index ad65ca64..7684809d 100644 --- a/routes/account.go +++ b/routes/account.go @@ -1,14 +1,185 @@ package routes import ( + "html" + "log" "net/http" + "strings" "../common" + "../query_gen/lib" ) // A blank list to fill out that parameter in Page for routes which don't use it var tList []interface{} +func AccountLogin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, ferr := common.UserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if user.Loggedin { + return common.LocalError("You're already logged in.", w, r, user) + } + pi := common.Page{common.GetTitlePhrase("login"), user, headerVars, tList, nil} + if common.PreRenderHooks["pre_render_login"] != nil { + if common.RunPreRenderHook("pre_render_login", w, r, &user, &pi) { + return nil + } + } + err := common.Templates.ExecuteTemplate(w, "login.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} + +// TODO: Log failed attempted logins? +// TODO: Lock IPS out if they have too many failed attempts? +// TODO: Log unusual countries in comparison to the country a user usually logs in from? Alert the user about this? +func AccountLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + if user.Loggedin { + return common.LocalError("You're already logged in.", w, r, user) + } + + username := html.EscapeString(strings.Replace(r.PostFormValue("username"), "\n", "", -1)) + uid, err := common.Auth.Authenticate(username, r.PostFormValue("password")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + userPtr, err := common.Users.Get(uid) + if err != nil { + return common.LocalError("Bad account", w, r, user) + } + user = *userPtr + + var session string + if user.Session == "" { + session, err = common.Auth.CreateSession(uid) + if err != nil { + return common.InternalError(err, w, r) + } + } else { + session = user.Session + } + + common.Auth.SetCookies(w, uid, session) + if user.IsAdmin { + // Is this error check redundant? We already check for the error in PreRoute for the same IP + // TODO: Should we be logging this? + log.Printf("#%d has logged in with IP %s", uid, user.LastIP) + } + http.Redirect(w, r, "/", http.StatusSeeOther) + return nil +} + +func AccountRegister(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, ferr := common.UserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if user.Loggedin { + return common.LocalError("You're already logged in.", w, r, user) + } + pi := common.Page{common.GetTitlePhrase("register"), user, headerVars, tList, nil} + if common.PreRenderHooks["pre_render_register"] != nil { + if common.RunPreRenderHook("pre_render_register", w, r, &user, &pi) { + return nil + } + } + err := common.Templates.ExecuteTemplate(w, "register.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} + +func AccountRegisterSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerLite, _ := common.SimpleUserCheck(w, r, &user) + + username := html.EscapeString(strings.Replace(r.PostFormValue("username"), "\n", "", -1)) + if username == "" { + return common.LocalError("You didn't put in a username.", w, r, user) + } + email := html.EscapeString(strings.Replace(r.PostFormValue("email"), "\n", "", -1)) + if email == "" { + return common.LocalError("You didn't put in an email.", w, r, user) + } + + password := r.PostFormValue("password") + switch password { + case "": + return common.LocalError("You didn't put in a password.", w, r, user) + case username: + return common.LocalError("You can't use your username as your password.", w, r, user) + case email: + return common.LocalError("You can't use your email as your password.", w, r, user) + } + + // ? Move this into Create()? What if we want to programatically set weak passwords for tests? + err := common.WeakPassword(password) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + confirmPassword := r.PostFormValue("confirm_password") + if common.Dev.DebugMode { + log.Print("Registration Attempt! Username: " + username) // TODO: Add more controls over what is logged when? + } + + // Do the two inputted passwords match..? + if password != confirmPassword { + return common.LocalError("The two passwords don't match.", w, r, user) + } + + var active bool + var group int + switch headerLite.Settings["activation_type"] { + case 1: // Activate All + active = true + group = common.Config.DefaultGroup + default: // Anything else. E.g. Admin Activation or Email Activation. + group = common.Config.ActivationGroup + } + + uid, err := common.Users.Create(username, password, email, group, active) + if err == common.ErrAccountExists { + return common.LocalError("This username isn't available. Try another.", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + + // Check if this user actually owns this email, if email activation is on, automatically flip their account to active when the email is validated. Validation is also useful for determining whether this user should receive any alerts, etc. via email + if common.Site.EnableEmails { + token, err := common.GenerateSafeString(80) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: Add an EmailStore and move this there + acc := qgen.Builder.Accumulator() + _, err = acc.Insert("emails").Columns("email, uid, validated, token").Fields("?,?,?,?").Exec(email, uid, 0, token) + //_, err = stmts.addEmail.Exec(email, uid, 0, token) + if err != nil { + return common.InternalError(err, w, r) + } + + if !common.SendValidationEmail(username, email, token) { + return common.LocalError("We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.", w, r, user) + } + } + + session, err := common.Auth.CreateSession(uid) + if err != nil { + return common.InternalError(err, w, r) + } + + common.Auth.SetCookies(w, uid, session) + http.Redirect(w, r, "/", http.StatusSeeOther) + return nil +} + func AccountEditCritical(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, ferr := common.UserCheck(w, r, &user) if ferr != nil { diff --git a/routes/misc.go b/routes/misc.go new file mode 100644 index 00000000..39955f1e --- /dev/null +++ b/routes/misc.go @@ -0,0 +1,96 @@ +package routes + +import ( + "bytes" + "io" + "log" + "net/http" + "strconv" + "strings" + "time" + + "../common" +) + +var cacheControlMaxAge = "max-age=" + strconv.Itoa(common.Day) // TODO: Make this a common.Config value + +// GET functions +func StaticFile(w http.ResponseWriter, r *http.Request) { + file, ok := common.StaticFiles.Get(r.URL.Path) + if !ok { + if common.Dev.DebugMode { + log.Printf("Failed to find '%s'", r.URL.Path) + } + w.WriteHeader(http.StatusNotFound) + return + } + h := w.Header() + + // Surely, there's a more efficient way of doing this? + t, err := time.Parse(http.TimeFormat, h.Get("If-Modified-Since")) + if err == nil && file.Info.ModTime().Before(t.Add(1*time.Second)) { + w.WriteHeader(http.StatusNotModified) + return + } + h.Set("Last-Modified", file.FormattedModTime) + h.Set("Content-Type", file.Mimetype) + h.Set("Cache-Control", cacheControlMaxAge) //Cache-Control: max-age=31536000 + h.Set("Vary", "Accept-Encoding") + if strings.Contains(h.Get("Accept-Encoding"), "gzip") { + h.Set("Content-Encoding", "gzip") + h.Set("Content-Length", strconv.FormatInt(file.GzipLength, 10)) + io.Copy(w, bytes.NewReader(file.GzipData)) // Use w.Write instead? + } else { + h.Set("Content-Length", strconv.FormatInt(file.Length, 10)) // Avoid doing a type conversion every time? + io.Copy(w, bytes.NewReader(file.Data)) + } + // Other options instead of io.Copy: io.CopyN(), w.Write(), http.ServeContent() +} + +func Overview(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, ferr := common.UserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.Zone = "overview" + + pi := common.Page{common.GetTitlePhrase("overview"), user, headerVars, tList, nil} + if common.PreRenderHooks["pre_render_overview"] != nil { + if common.RunPreRenderHook("pre_render_overview", w, r, &user, &pi) { + return nil + } + } + + err := common.Templates.ExecuteTemplate(w, "overview.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} + +func CustomPage(w http.ResponseWriter, r *http.Request, user common.User, name string) common.RouteError { + headerVars, ferr := common.UserCheck(w, r, &user) + if ferr != nil { + return ferr + } + + // ! Is this safe? + if common.Templates.Lookup("page_"+name+".html") == nil { + return common.NotFound(w, r) + } + headerVars.Zone = "custom_page" + + pi := common.Page{common.GetTitlePhrase("page"), user, headerVars, tList, nil} + // TODO: Pass the page name to the pre-render hook? + if common.PreRenderHooks["pre_render_custom_page"] != nil { + if common.RunPreRenderHook("pre_render_custom_page", w, r, &user, &pi) { + return nil + } + } + + err := common.Templates.ExecuteTemplate(w, "page_"+name+".html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} diff --git a/routes/topic.go b/routes/topic.go index 368d4e5a..bb8dede3 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -15,10 +15,177 @@ import ( "strings" "../common" + "../query_gen/lib" ) var successJSONBytes = []byte(`{"success":"1"}`) +func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit string) common.RouteError { + var err error + var replyList []common.ReplyUser + page, _ := strconv.Atoi(r.FormValue("page")) + + // SEO URLs... + // TODO: Make a shared function for this + halves := strings.Split(urlBit, ".") + if len(halves) < 2 { + halves = append(halves, halves[0]) + } + + tid, err := strconv.Atoi(halves[1]) + if err != nil { + return common.PreError("The provided TopicID is not a valid number.", w, r) + } + + // Get the topic... + topic, err := common.GetTopicUser(tid) + if err == sql.ErrNoRows { + return common.NotFound(w, r) + } else if err != nil { + return common.InternalError(err, w, r) + } + topic.ClassName = "" + //log.Printf("topic: %+v\n", topic) + + headerVars, ferr := common.ForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr + } + if !user.Perms.ViewTopic { + //log.Printf("user.Perms: %+v\n", user.Perms) + return common.NoPermissions(w, r, user) + } + headerVars.Zone = "view_topic" + // TODO: Only include these on pages with polls + headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") + headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + + topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums") + topic.ContentLines = strings.Count(topic.Content, "\n") + + // We don't want users posting in locked topics... + if topic.IsClosed && !user.IsMod { + user.Perms.CreateReply = false + } + + postGroup, err := common.Groups.Get(topic.Group) + if err != nil { + return common.InternalError(err, w, r) + } + + topic.Tag = postGroup.Tag + if postGroup.IsMod || postGroup.IsAdmin { + topic.ClassName = common.Config.StaffCSS + } + topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt) + + // TODO: Make a function for this? Build a more sophisticated noavatar handling system? + topic.Avatar = common.BuildAvatar(topic.CreatedBy, topic.Avatar) + + var poll common.Poll + if topic.Poll != 0 { + pPoll, err := common.Polls.Get(topic.Poll) + if err != nil { + log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID)) + return common.InternalError(err, w, r) + } + poll = pPoll.Copy() + } + + // Calculate the offset + offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) + tpage := common.TopicPage{topic.Title, user, headerVars, replyList, topic, poll, page, lastPage} + + // Get the replies.. + // TODO: Reuse this statement rather than preparing it on the spot, maybe via a TopicList abstraction + stmt, err := qgen.Builder.SimpleLeftJoin("replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress, replies.likeCount, replies.actionType", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?") + if err != nil { + return common.InternalError(err, w, r) + } + rows, err := stmt.Query(topic.ID, offset, common.Config.ItemsPerPage) + if err == sql.ErrNoRows { + return common.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) + } else if err != nil { + return common.InternalError(err, w, r) + } + defer rows.Close() + + replyItem := common.ReplyUser{ClassName: ""} + for rows.Next() { + err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType) + if err != nil { + return common.InternalError(err, w, r) + } + + replyItem.UserLink = common.BuildProfileURL(common.NameToSlug(replyItem.CreatedByName), replyItem.CreatedBy) + replyItem.ParentID = topic.ID + replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums") + replyItem.ContentLines = strings.Count(replyItem.Content, "\n") + + postGroup, err = common.Groups.Get(replyItem.Group) + if err != nil { + return common.InternalError(err, w, r) + } + + if postGroup.IsMod || postGroup.IsAdmin { + replyItem.ClassName = common.Config.StaffCSS + } else { + replyItem.ClassName = "" + } + + // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this? + replyItem.Avatar = common.BuildAvatar(replyItem.CreatedBy, replyItem.Avatar) + replyItem.Tag = postGroup.Tag + replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt) + + // We really shouldn't have inline HTML, we should do something about this... + if replyItem.ActionType != "" { + switch replyItem.ActionType { + case "lock": + replyItem.ActionType = "This topic has been locked by " + replyItem.CreatedByName + "" + replyItem.ActionIcon = "🔒︎" + case "unlock": + replyItem.ActionType = "This topic has been reopened by " + replyItem.CreatedByName + "" + replyItem.ActionIcon = "🔓︎" + case "stick": + replyItem.ActionType = "This topic has been pinned by " + replyItem.CreatedByName + "" + replyItem.ActionIcon = "📌︎" + case "unstick": + replyItem.ActionType = "This topic has been unpinned by " + replyItem.CreatedByName + "" + replyItem.ActionIcon = "📌︎" + case "move": + replyItem.ActionType = "This topic has been moved by " + replyItem.CreatedByName + "" + default: + replyItem.ActionType = replyItem.ActionType + " has happened" + replyItem.ActionIcon = "" + } + } + replyItem.Liked = false + + if common.Vhooks["topic_reply_row_assign"] != nil { + common.RunVhook("topic_reply_row_assign", &tpage, &replyItem) + } + replyList = append(replyList, replyItem) + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + + tpage.ItemList = replyList + if common.PreRenderHooks["pre_render_view_topic"] != nil { + if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) { + return nil + } + } + err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, w) + if err != nil { + return common.InternalError(err, w, r) + } + common.TopicViewCounter.Bump(topic.ID) // TODO Move this into the router? + return nil +} + // ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation // ? - Should we allow banned users to make reports? How should we handle report abuse? // TODO: Add a permission to stop certain users from using custom avatars diff --git a/template_list.go b/template_list.go index d351bd28..3b89c199 100644 --- a/template_list.go +++ b/template_list.go @@ -126,157 +126,207 @@ var topic_20 = []byte(`' type="text" /> var topic_21 = []byte(` - -
-
+
+
+ `) +var topic_27 = []byte(` +
+ + + `) +var topic_33 = []byte(` +
+ `) +var topic_34 = []byte(` +
+ + + +
+
+
+
+
+
+
+ +
+

`) -var topic_27 = []byte(`

+var topic_43 = []byte(`

+var topic_44 = []byte(`    +var topic_45 = []byte(`" class="username real_username" rel="author">`) +var topic_46 = []byte(`   `) -var topic_31 = []byte(` - `) -var topic_39 = []byte(``) -var topic_41 = []byte(``) -var topic_44 = []byte(``) -var topic_47 = []byte(``) -var topic_50 = []byte(``) -var topic_53 = []byte(``) -var topic_56 = []byte(``) -var topic_59 = []byte(` +var topic_49 = []byte(`" class="mod_button" title="Love it" `) +var topic_50 = []byte(`aria-label="Unlike this topic"`) +var topic_51 = []byte(`aria-label="Like this topic"`) +var topic_52 = []byte(` style="color:#202020;"> + `) +var topic_55 = []byte(``) +var topic_57 = []byte(``) +var topic_60 = []byte(``) +var topic_63 = []byte(``) +var topic_66 = []byte(``) +var topic_69 = []byte(``) +var topic_72 = []byte(``) +var topic_75 = []byte(` +var topic_76 = []byte(`?session=`) +var topic_77 = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag this topic" aria-label="Flag this topic" rel="nofollow"> `) -var topic_62 = []byte(``) -var topic_64 = []byte(``) -var topic_65 = []byte(``) -var topic_66 = []byte(``) -var topic_67 = []byte(``) -var topic_68 = []byte(` +var topic_78 = []byte(``) +var topic_80 = []byte(``) +var topic_81 = []byte(``) +var topic_82 = []byte(``) +var topic_83 = []byte(``) +var topic_84 = []byte(`
`) -var topic_69 = []byte(` +var topic_85 = []byte(`
`) -var topic_70 = []byte(` +var topic_86 = []byte(` `) -var topic_71 = []byte(` +var topic_87 = []byte(`
`) -var topic_72 = []byte(` +var topic_88 = []byte(`
+var topic_89 = []byte(`" style="background-image: url(`) +var topic_90 = []byte(`), url(/static/`) +var topic_91 = []byte(`/post-avatar-bg.jpg);background-position: 0px `) +var topic_92 = []byte(`-1`) +var topic_93 = []byte(`0px;background-repeat:no-repeat, repeat-y;"> `) -var topic_78 = []byte(` +var topic_94 = []byte(`

`) -var topic_79 = []byte(`

+var topic_95 = []byte(`

   +var topic_96 = []byte(`" class="username real_username" rel="author">`) +var topic_97 = []byte(`   `) -var topic_82 = []byte(``) -var topic_87 = []byte(``) -var topic_90 = []byte(``) -var topic_93 = []byte(``) -var topic_95 = []byte(` +var topic_98 = []byte(``) +var topic_103 = []byte(``) +var topic_106 = []byte(``) +var topic_109 = []byte(``) +var topic_111 = []byte(` +var topic_112 = []byte(`?session=`) +var topic_113 = []byte(`&type=reply" class="mod_button report_item" title="Flag this reply" aria-label="Flag this reply" rel="nofollow"> `) -var topic_98 = []byte(``) -var topic_100 = []byte(``) -var topic_101 = []byte(``) -var topic_102 = []byte(``) -var topic_103 = []byte(``) -var topic_104 = []byte(` +var topic_114 = []byte(``) +var topic_116 = []byte(``) +var topic_117 = []byte(``) +var topic_118 = []byte(``) +var topic_119 = []byte(``) +var topic_120 = []byte(`
`) -var topic_105 = []byte(`
+var topic_121 = []byte(` `) -var topic_106 = []byte(` +var topic_122 = []byte(`
-
- +
+ +
- + +
+
+
+
+
+ + + +
- + + `) -var topic_109 = []byte(` - +var topic_125 = []byte(` +
`) -var topic_110 = []byte(` +var topic_126 = []byte(`
`) -var topic_111 = []byte(` +var topic_127 = []byte(` @@ -357,7 +407,7 @@ var topic_alt_21 = []byte(` var topic_alt_22 = []byte(`_form" action="/poll/vote/`) var topic_alt_23 = []byte(`?session=`) var topic_alt_24 = []byte(`" method="post"> -
+
 
@@ -568,21 +618,32 @@ var topic_alt_144 = []byte(`
`) var topic_alt_145 = []byte(`
-
- +
- + +
+
+
+
+
+ + + +
- + + `) var topic_alt_148 = []byte(` - +
`) var topic_alt_149 = []byte(` @@ -929,15 +990,15 @@ var topics_15 = []byte(`