From 9a3d8425e0970eb8642e5a81fd54e361b2534c33 Mon Sep 17 00:00:00 2001 From: Azareal Date: Sun, 14 Jan 2018 12:03:20 +0000 Subject: [PATCH] Added the post counter and the associated Control Panel graph. Renamed the pre_render_panel_analytics hook to pre_render_panel_analytics_views. Added a details table to the route graphs. Moved the /topic/ and /reply/ routes to the router generator and hardened them. Made some of the route names more consistent. We now track IPs in odd situations in debug mode. Fixed a bug where people could post on non-existent profiles. Continued work on topic move. Added the postchunks table. --- common/counters.go | 67 ++- common/extend.go | 2 +- common/pages.go | 1 + common/permissions.go | 5 +- common/routes_common.go | 23 + common/topic.go | 8 + gen_router.go | 436 +++++++++++--- main.go | 22 +- member_routes.go | 58 +- mod_routes.go | 126 ++-- panel_routes.go | 484 ++++++++------- public/global.js | 43 +- query_gen/tables.go | 9 + router_gen/main.go | 4 + router_gen/route_impl.go | 19 +- router_gen/routes.go | 36 +- routes.go | 4 +- schema/mssql/query_postchunks.sql | 4 + schema/mysql/query_postchunks.sql | 4 + schema/pgsql/query_postchunks.sql | 4 + template_list.go | 551 +++++++++--------- template_topic.go | 278 +++++---- template_topic_alt.go | 286 ++++----- template_topics.go | 156 ++--- templates/create_topic.html | 2 +- templates/panel_analytics_posts.html | 62 ++ templates/panel_analytics_route_views.html | 11 + ...-views.html => panel_analytics_views.html} | 0 templates/topic.html | 18 +- templates/topic_alt.html | 18 +- templates/topics.html | 7 +- themes/cosora/public/main.css | 15 +- 32 files changed, 1695 insertions(+), 1068 deletions(-) create mode 100644 schema/mssql/query_postchunks.sql create mode 100644 schema/mysql/query_postchunks.sql create mode 100644 schema/pgsql/query_postchunks.sql create mode 100644 templates/panel_analytics_posts.html rename templates/{panel-analytics-views.html => panel_analytics_views.html} (100%) diff --git a/common/counters.go b/common/counters.go index 0c9e0745..3870e40f 100644 --- a/common/counters.go +++ b/common/counters.go @@ -8,21 +8,25 @@ import ( "../query_gen/lib" ) -var GlobalViewCounter *ChunkedViewCounter +// Global counters +var GlobalViewCounter *DefaultViewCounter var AgentViewCounter *DefaultAgentViewCounter var RouteViewCounter *DefaultRouteViewCounter +var PostCounter *DefaultPostCounter + +// Local counters var TopicViewCounter *DefaultTopicViewCounter -type ChunkedViewCounter struct { +type DefaultViewCounter struct { buckets [2]int64 currentBucket int64 insert *sql.Stmt } -func NewChunkedViewCounter() (*ChunkedViewCounter, error) { +func NewGlobalViewCounter() (*DefaultViewCounter, error) { acc := qgen.Builder.Accumulator() - counter := &ChunkedViewCounter{ + counter := &DefaultViewCounter{ currentBucket: 0, insert: acc.Insert("viewchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(), } @@ -32,7 +36,7 @@ func NewChunkedViewCounter() (*ChunkedViewCounter, error) { return counter, acc.FirstError() } -func (counter *ChunkedViewCounter) Tick() (err error) { +func (counter *DefaultViewCounter) Tick() (err error) { var oldBucket = counter.currentBucket var nextBucket int64 // 0 if counter.currentBucket == 0 { @@ -47,11 +51,11 @@ func (counter *ChunkedViewCounter) Tick() (err error) { return counter.insertChunk(previousViewChunk) } -func (counter *ChunkedViewCounter) Bump() { +func (counter *DefaultViewCounter) Bump() { atomic.AddInt64(&counter.buckets[counter.currentBucket], 1) } -func (counter *ChunkedViewCounter) insertChunk(count int64) error { +func (counter *DefaultViewCounter) insertChunk(count int64) error { if count == 0 { return nil } @@ -60,6 +64,53 @@ func (counter *ChunkedViewCounter) insertChunk(count int64) error { return err } +type DefaultPostCounter struct { + buckets [2]int64 + currentBucket int64 + + insert *sql.Stmt +} + +func NewPostCounter() (*DefaultPostCounter, error) { + acc := qgen.Builder.Accumulator() + counter := &DefaultPostCounter{ + currentBucket: 0, + insert: acc.Insert("postchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(), + } + AddScheduledFifteenMinuteTask(counter.Tick) + //AddScheduledSecondTask(counter.Tick) + AddShutdownTask(counter.Tick) + return counter, acc.FirstError() +} + +func (counter *DefaultPostCounter) Tick() (err error) { + var oldBucket = counter.currentBucket + var nextBucket int64 // 0 + if counter.currentBucket == 0 { + nextBucket = 1 + } + atomic.AddInt64(&counter.buckets[oldBucket], counter.buckets[nextBucket]) + atomic.StoreInt64(&counter.buckets[nextBucket], 0) + atomic.StoreInt64(&counter.currentBucket, nextBucket) + + var previousViewChunk = counter.buckets[oldBucket] + atomic.AddInt64(&counter.buckets[oldBucket], -previousViewChunk) + return counter.insertChunk(previousViewChunk) +} + +func (counter *DefaultPostCounter) Bump() { + atomic.AddInt64(&counter.buckets[counter.currentBucket], 1) +} + +func (counter *DefaultPostCounter) insertChunk(count int64) error { + if count == 0 { + return nil + } + debugLogf("Inserting a postchunk with a count of %d", count) + _, err := counter.insert.Exec(count) + return err +} + type RWMutexCounterBucket struct { counter int sync.RWMutex @@ -114,7 +165,7 @@ func (counter *DefaultAgentViewCounter) insertChunk(count int, agent int) error func (counter *DefaultAgentViewCounter) Bump(agent int) { // TODO: Test this check - debugLog("counter.agentBuckets[", agent, "]: ", counter.agentBuckets[agent]) + debugDetail("counter.agentBuckets[", agent, "]: ", counter.agentBuckets[agent]) if len(counter.agentBuckets) <= agent || agent < 0 { return } diff --git a/common/extend.go b/common/extend.go index ae2f0428..99fb2441 100644 --- a/common/extend.go +++ b/common/extend.go @@ -96,7 +96,7 @@ var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User "pre_render_panel_forums": nil, "pre_render_panel_delete_forum": nil, "pre_render_panel_edit_forum": nil, - "pre_render_panel_analytics": nil, + "pre_render_panel_analytics_views": nil, "pre_render_panel_analytics_routes": nil, "pre_render_panel_analytics_agents": nil, "pre_render_panel_analytics_route_views": nil, diff --git a/common/pages.go b/common/pages.go index 58c74e04..8a55499a 100644 --- a/common/pages.go +++ b/common/pages.go @@ -206,6 +206,7 @@ type PanelAnalyticsRoutePage struct { Zone string Route string PrimaryGraph PanelTimeGraph + ViewItems []PanelAnalyticsItem TimeRange string } diff --git a/common/permissions.go b/common/permissions.go index 613d249b..8b3ec5bc 100644 --- a/common/permissions.go +++ b/common/permissions.go @@ -78,8 +78,9 @@ type Perms struct { EditReply bool //EditOwnReply bool DeleteReply bool - PinTopic bool - CloseTopic bool + //DeleteOwnReply bool + PinTopic bool + CloseTopic bool //CloseOwnTopic bool //ExtData map[string]bool diff --git a/common/routes_common.go b/common/routes_common.go index 3d127a0c..fce992a3 100644 --- a/common/routes_common.go +++ b/common/routes_common.go @@ -5,6 +5,7 @@ import ( "log" "net" "net/http" + "strconv" "strings" ) @@ -346,3 +347,25 @@ func NoSessionMismatch(w http.ResponseWriter, r *http.Request, user User) RouteE func ReqIsJson(r *http.Request) bool { return r.Header.Get("Content-type") == "application/json" } + +func HandleUploadRoute(w http.ResponseWriter, r *http.Request, user User, maxFileSize int) RouteError { + // TODO: Reuse this code more + if r.ContentLength > int64(maxFileSize) { + size, unit := ConvertByteUnit(float64(maxFileSize)) + return CustomError("Your upload is too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user) + } + r.Body = http.MaxBytesReader(w, r.Body, int64(maxFileSize)) + + err := r.ParseMultipartForm(int64(Megabyte)) + if err != nil { + return LocalError("Bad Form", w, r, user) + } + return nil +} + +func NoUploadSessionMismatch(w http.ResponseWriter, r *http.Request, user User) RouteError { + if r.FormValue("session") != user.Session { + return SecurityError(w, r, user) + } + return nil +} diff --git a/common/topic.go b/common/topic.go index 6b69692e..64cca91e 100644 --- a/common/topic.go +++ b/common/topic.go @@ -112,6 +112,7 @@ type TopicStmts struct { addRepliesToTopic *sql.Stmt lock *sql.Stmt unlock *sql.Stmt + moveTo *sql.Stmt stick *sql.Stmt unstick *sql.Stmt hasLikedTopic *sql.Stmt @@ -132,6 +133,7 @@ func init() { addRepliesToTopic: acc.Update("topics").Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(), lock: acc.Update("topics").Set("is_closed = 1").Where("tid = ?").Prepare(), unlock: acc.Update("topics").Set("is_closed = 0").Where("tid = ?").Prepare(), + moveTo: acc.Update("topics").Set("parentID = ?").Where("tid = ?").Prepare(), stick: acc.Update("topics").Set("sticky = 1").Where("tid = ?").Prepare(), unstick: acc.Update("topics").Set("sticky = 0").Where("tid = ?").Prepare(), hasLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? and targetItem = ? and targetType = 'topics'").Prepare(), @@ -175,6 +177,12 @@ func (topic *Topic) Unlock() (err error) { return err } +func (topic *Topic) MoveTo(destForum int) (err error) { + _, err = topicStmts.moveTo.Exec(destForum, topic.ID) + topic.cacheRemove() + return err +} + // TODO: We might want more consistent terminology rather than using stick in some places and pin in others. If you don't understand the difference, there is none, they are one and the same. func (topic *Topic) Stick() (err error) { _, err = topicStmts.stick.Exec(topic.ID) diff --git a/gen_router.go b/gen_router.go index d81dfcb0..b3692442 100644 --- a/gen_router.go +++ b/gen_router.go @@ -56,6 +56,7 @@ var RouteMap = map[string]interface{}{ "routePanelAnalyticsAgents": routePanelAnalyticsAgents, "routePanelAnalyticsRouteViews": routePanelAnalyticsRouteViews, "routePanelAnalyticsAgentViews": routePanelAnalyticsAgentViews, + "routePanelAnalyticsPosts": routePanelAnalyticsPosts, "routePanelGroups": routePanelGroups, "routePanelGroupsEdit": routePanelGroupsEdit, "routePanelGroupsEditPerms": routePanelGroupsEditPerms, @@ -79,6 +80,20 @@ var RouteMap = map[string]interface{}{ "routeUnban": routeUnban, "routeActivate": routeActivate, "routeIps": routeIps, + "routeTopicCreateSubmit": routeTopicCreateSubmit, + "routeEditTopicSubmit": routeEditTopicSubmit, + "routeDeleteTopicSubmit": routeDeleteTopicSubmit, + "routeStickTopicSubmit": routeStickTopicSubmit, + "routeUnstickTopicSubmit": routeUnstickTopicSubmit, + "routeLockTopicSubmit": routeLockTopicSubmit, + "routeUnlockTopicSubmit": routeUnlockTopicSubmit, + "routeMoveTopicSubmit": routeMoveTopicSubmit, + "routeLikeTopicSubmit": routeLikeTopicSubmit, + "routeTopicID": routeTopicID, + "routeCreateReplySubmit": routeCreateReplySubmit, + "routeReplyEditSubmit": routeReplyEditSubmit, + "routeReplyDeleteSubmit": routeReplyDeleteSubmit, + "routeReplyLikeSubmit": routeReplyLikeSubmit, "routeDynamic": routeDynamic, "routeUploads": routeUploads, } @@ -126,31 +141,46 @@ var routeMapEnum = map[string]int{ "routePanelAnalyticsAgents": 38, "routePanelAnalyticsRouteViews": 39, "routePanelAnalyticsAgentViews": 40, - "routePanelGroups": 41, - "routePanelGroupsEdit": 42, - "routePanelGroupsEditPerms": 43, - "routePanelGroupsEditSubmit": 44, - "routePanelGroupsEditPermsSubmit": 45, - "routePanelGroupsCreateSubmit": 46, - "routePanelBackups": 47, - "routePanelLogsMod": 48, - "routePanelDebug": 49, - "routePanel": 50, - "routeAccountEditCritical": 51, - "routeAccountEditCriticalSubmit": 52, - "routeAccountEditAvatar": 53, - "routeAccountEditAvatarSubmit": 54, - "routeAccountEditUsername": 55, - "routeAccountEditUsernameSubmit": 56, - "routeAccountEditEmail": 57, - "routeAccountEditEmailTokenSubmit": 58, - "routeProfile": 59, - "routeBanSubmit": 60, - "routeUnban": 61, - "routeActivate": 62, - "routeIps": 63, - "routeDynamic": 64, - "routeUploads": 65, + "routePanelAnalyticsPosts": 41, + "routePanelGroups": 42, + "routePanelGroupsEdit": 43, + "routePanelGroupsEditPerms": 44, + "routePanelGroupsEditSubmit": 45, + "routePanelGroupsEditPermsSubmit": 46, + "routePanelGroupsCreateSubmit": 47, + "routePanelBackups": 48, + "routePanelLogsMod": 49, + "routePanelDebug": 50, + "routePanel": 51, + "routeAccountEditCritical": 52, + "routeAccountEditCriticalSubmit": 53, + "routeAccountEditAvatar": 54, + "routeAccountEditAvatarSubmit": 55, + "routeAccountEditUsername": 56, + "routeAccountEditUsernameSubmit": 57, + "routeAccountEditEmail": 58, + "routeAccountEditEmailTokenSubmit": 59, + "routeProfile": 60, + "routeBanSubmit": 61, + "routeUnban": 62, + "routeActivate": 63, + "routeIps": 64, + "routeTopicCreateSubmit": 65, + "routeEditTopicSubmit": 66, + "routeDeleteTopicSubmit": 67, + "routeStickTopicSubmit": 68, + "routeUnstickTopicSubmit": 69, + "routeLockTopicSubmit": 70, + "routeUnlockTopicSubmit": 71, + "routeMoveTopicSubmit": 72, + "routeLikeTopicSubmit": 73, + "routeTopicID": 74, + "routeCreateReplySubmit": 75, + "routeReplyEditSubmit": 76, + "routeReplyDeleteSubmit": 77, + "routeReplyLikeSubmit": 78, + "routeDynamic": 79, + "routeUploads": 80, } var reverseRouteMapEnum = map[int]string{ 0: "routeAPI", @@ -194,31 +224,46 @@ var reverseRouteMapEnum = map[int]string{ 38: "routePanelAnalyticsAgents", 39: "routePanelAnalyticsRouteViews", 40: "routePanelAnalyticsAgentViews", - 41: "routePanelGroups", - 42: "routePanelGroupsEdit", - 43: "routePanelGroupsEditPerms", - 44: "routePanelGroupsEditSubmit", - 45: "routePanelGroupsEditPermsSubmit", - 46: "routePanelGroupsCreateSubmit", - 47: "routePanelBackups", - 48: "routePanelLogsMod", - 49: "routePanelDebug", - 50: "routePanel", - 51: "routeAccountEditCritical", - 52: "routeAccountEditCriticalSubmit", - 53: "routeAccountEditAvatar", - 54: "routeAccountEditAvatarSubmit", - 55: "routeAccountEditUsername", - 56: "routeAccountEditUsernameSubmit", - 57: "routeAccountEditEmail", - 58: "routeAccountEditEmailTokenSubmit", - 59: "routeProfile", - 60: "routeBanSubmit", - 61: "routeUnban", - 62: "routeActivate", - 63: "routeIps", - 64: "routeDynamic", - 65: "routeUploads", + 41: "routePanelAnalyticsPosts", + 42: "routePanelGroups", + 43: "routePanelGroupsEdit", + 44: "routePanelGroupsEditPerms", + 45: "routePanelGroupsEditSubmit", + 46: "routePanelGroupsEditPermsSubmit", + 47: "routePanelGroupsCreateSubmit", + 48: "routePanelBackups", + 49: "routePanelLogsMod", + 50: "routePanelDebug", + 51: "routePanel", + 52: "routeAccountEditCritical", + 53: "routeAccountEditCriticalSubmit", + 54: "routeAccountEditAvatar", + 55: "routeAccountEditAvatarSubmit", + 56: "routeAccountEditUsername", + 57: "routeAccountEditUsernameSubmit", + 58: "routeAccountEditEmail", + 59: "routeAccountEditEmailTokenSubmit", + 60: "routeProfile", + 61: "routeBanSubmit", + 62: "routeUnban", + 63: "routeActivate", + 64: "routeIps", + 65: "routeTopicCreateSubmit", + 66: "routeEditTopicSubmit", + 67: "routeDeleteTopicSubmit", + 68: "routeStickTopicSubmit", + 69: "routeUnstickTopicSubmit", + 70: "routeLockTopicSubmit", + 71: "routeUnlockTopicSubmit", + 72: "routeMoveTopicSubmit", + 73: "routeLikeTopicSubmit", + 74: "routeTopicID", + 75: "routeCreateReplySubmit", + 76: "routeReplyEditSubmit", + 77: "routeReplyDeleteSubmit", + 78: "routeReplyLikeSubmit", + 79: "routeDynamic", + 80: "routeUploads", } var agentMapEnum = map[string]int{ "unknown": 0, @@ -339,6 +384,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Print("req.URL.Path: ", req.URL.Path) log.Print("extraData: ", extraData) log.Print("req.Referer(): ", req.Referer()) + log.Print("req.RemoteAddr: ", req.RemoteAddr) } if prefix == "/static" { @@ -389,6 +435,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { if common.Dev.DebugMode { log.Print("Blank UA: ", req.UserAgent()) log.Print("Method: ", req.Method) + for key, value := range req.Header { for _, vvalue := range value { log.Print("Header '" + key + "': " + vvalue + "!!") @@ -398,6 +445,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Print("req.URL.Path: ", req.URL.Path) log.Print("extraData: ", extraData) log.Print("req.Referer(): ", req.Referer()) + log.Print("req.RemoteAddr: ", req.RemoteAddr) } default: common.AgentViewCounter.Bump(0) @@ -413,6 +461,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Print("req.URL.Path: ", req.URL.Path) log.Print("extraData: ", extraData) log.Print("req.Referer(): ", req.Referer()) + log.Print("req.RemoteAddr: ", req.RemoteAddr) } } @@ -724,14 +773,23 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/panel/analytics/agent/": common.RouteViewCounter.Bump(40) err = routePanelAnalyticsAgentViews(w,req,user,extraData) - case "/panel/groups/": + case "/panel/analytics/posts/": + err = common.ParseForm(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + common.RouteViewCounter.Bump(41) + err = routePanelAnalyticsPosts(w,req,user) + case "/panel/groups/": + common.RouteViewCounter.Bump(42) err = routePanelGroups(w,req,user) case "/panel/groups/edit/": - common.RouteViewCounter.Bump(42) + common.RouteViewCounter.Bump(43) err = routePanelGroupsEdit(w,req,user,extraData) case "/panel/groups/edit/perms/": - common.RouteViewCounter.Bump(43) + common.RouteViewCounter.Bump(44) err = routePanelGroupsEditPerms(w,req,user,extraData) case "/panel/groups/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -740,7 +798,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(44) + common.RouteViewCounter.Bump(45) err = routePanelGroupsEditSubmit(w,req,user,extraData) case "/panel/groups/edit/perms/submit/": err = common.NoSessionMismatch(w,req,user) @@ -749,7 +807,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(45) + common.RouteViewCounter.Bump(46) err = routePanelGroupsEditPermsSubmit(w,req,user,extraData) case "/panel/groups/create/": err = common.NoSessionMismatch(w,req,user) @@ -758,7 +816,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(46) + common.RouteViewCounter.Bump(47) err = routePanelGroupsCreateSubmit(w,req,user) case "/panel/backups/": err = common.SuperAdminOnly(w,req,user) @@ -767,10 +825,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(47) + common.RouteViewCounter.Bump(48) err = routePanelBackups(w,req,user,extraData) case "/panel/logs/mod/": - common.RouteViewCounter.Bump(48) + common.RouteViewCounter.Bump(49) err = routePanelLogsMod(w,req,user) case "/panel/debug/": err = common.AdminOnly(w,req,user) @@ -779,10 +837,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(49) + common.RouteViewCounter.Bump(50) err = routePanelDebug(w,req,user) default: - common.RouteViewCounter.Bump(50) + common.RouteViewCounter.Bump(51) err = routePanel(w,req,user) } if err != nil { @@ -797,7 +855,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(51) + common.RouteViewCounter.Bump(52) err = routeAccountEditCritical(w,req,user) case "/user/edit/critical/submit/": err = common.NoSessionMismatch(w,req,user) @@ -812,7 +870,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(52) + common.RouteViewCounter.Bump(53) err = routeAccountEditCriticalSubmit(w,req,user) case "/user/edit/avatar/": err = common.MemberOnly(w,req,user) @@ -821,7 +879,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(53) + common.RouteViewCounter.Bump(54) err = routeAccountEditAvatar(w,req,user) case "/user/edit/avatar/submit/": err = common.MemberOnly(w,req,user) @@ -830,7 +888,18 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(54) + err = common.HandleUploadRoute(w,req,user,common.Config.MaxRequestSize) + if err != nil { + router.handleError(err,w,req,user) + return + } + err = common.NoUploadSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(55) err = routeAccountEditAvatarSubmit(w,req,user) case "/user/edit/username/": err = common.MemberOnly(w,req,user) @@ -839,7 +908,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(55) + common.RouteViewCounter.Bump(56) err = routeAccountEditUsername(w,req,user) case "/user/edit/username/submit/": err = common.NoSessionMismatch(w,req,user) @@ -854,7 +923,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(56) + common.RouteViewCounter.Bump(57) err = routeAccountEditUsernameSubmit(w,req,user) case "/user/edit/email/": err = common.MemberOnly(w,req,user) @@ -863,7 +932,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(57) + common.RouteViewCounter.Bump(58) err = routeAccountEditEmail(w,req,user) case "/user/edit/token/": err = common.NoSessionMismatch(w,req,user) @@ -878,11 +947,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(58) + common.RouteViewCounter.Bump(59) err = routeAccountEditEmailTokenSubmit(w,req,user,extraData) default: req.URL.Path += extraData - common.RouteViewCounter.Bump(59) + common.RouteViewCounter.Bump(60) err = routeProfile(w,req,user) } if err != nil { @@ -903,7 +972,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(60) + common.RouteViewCounter.Bump(61) err = routeBanSubmit(w,req,user,extraData) case "/users/unban/": err = common.NoSessionMismatch(w,req,user) @@ -918,7 +987,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(61) + common.RouteViewCounter.Bump(62) err = routeUnban(w,req,user,extraData) case "/users/activate/": err = common.NoSessionMismatch(w,req,user) @@ -933,7 +1002,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(62) + common.RouteViewCounter.Bump(63) err = routeActivate(w,req,user,extraData) case "/users/ips/": err = common.MemberOnly(w,req,user) @@ -942,12 +1011,229 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(63) + common.RouteViewCounter.Bump(64) err = routeIps(w,req,user) } if err != nil { router.handleError(err,w,req,user) } + case "/topic": + switch(req.URL.Path) { + case "/topic/create/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(65) + err = routeTopicCreateSubmit(w,req,user) + case "/topic/edit/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(66) + err = routeEditTopicSubmit(w,req,user,extraData) + case "/topic/delete/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + req.URL.Path += extraData + common.RouteViewCounter.Bump(67) + err = routeDeleteTopicSubmit(w,req,user) + case "/topic/stick/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(68) + err = routeStickTopicSubmit(w,req,user,extraData) + case "/topic/unstick/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(69) + err = routeUnstickTopicSubmit(w,req,user,extraData) + case "/topic/lock/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + req.URL.Path += extraData + common.RouteViewCounter.Bump(70) + err = routeLockTopicSubmit(w,req,user) + case "/topic/unlock/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(71) + err = routeUnlockTopicSubmit(w,req,user,extraData) + case "/topic/move/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(72) + err = routeMoveTopicSubmit(w,req,user,extraData) + case "/topic/like/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(73) + err = routeLikeTopicSubmit(w,req,user,extraData) + default: + common.RouteViewCounter.Bump(74) + err = routeTopicID(w,req,user, extraData) + } + if err != nil { + router.handleError(err,w,req,user) + } + case "/reply": + switch(req.URL.Path) { + case "/reply/create/": + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.HandleUploadRoute(w,req,user,common.Config.MaxRequestSize) + if err != nil { + router.handleError(err,w,req,user) + return + } + err = common.NoUploadSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(75) + err = routeCreateReplySubmit(w,req,user) + case "/reply/edit/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(76) + err = routeReplyEditSubmit(w,req,user,extraData) + case "/reply/delete/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(77) + err = routeReplyDeleteSubmit(w,req,user,extraData) + case "/reply/like/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + err = common.MemberOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + + common.RouteViewCounter.Bump(78) + err = routeReplyLikeSubmit(w,req,user,extraData) + } + if err != nil { + router.handleError(err,w,req,user) + } /*case "/sitemaps": // TODO: Count these views req.URL.Path += extraData err = sitemapSwitch(w,req) @@ -959,7 +1245,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { common.NotFound(w,req) return } - common.RouteViewCounter.Bump(65) + common.RouteViewCounter.Bump(80) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? router.UploadHandler(w,req) // TODO: Count these views @@ -1003,7 +1289,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { router.RUnlock() if ok { - common.RouteViewCounter.Bump(64) // TODO: Be more specific about *which* dynamic route it is + common.RouteViewCounter.Bump(79) // TODO: Be more specific about *which* dynamic route it is req.URL.Path += extraData err = handle(w,req,user) if err != nil { diff --git a/main.go b/main.go index 1cccba3d..7c23fefa 100644 --- a/main.go +++ b/main.go @@ -80,7 +80,7 @@ func afterDBInit() (err error) { if err != nil { return err } - common.GlobalViewCounter, err = common.NewChunkedViewCounter() + common.GlobalViewCounter, err = common.NewGlobalViewCounter() if err != nil { return err } @@ -92,6 +92,10 @@ func afterDBInit() (err error) { if err != nil { return err } + common.PostCounter, err = common.NewPostCounter() + if err != nil { + return err + } common.TopicViewCounter, err = common.NewDefaultTopicViewCounter() if err != nil { return err @@ -299,22 +303,6 @@ func main() { // TODO: Move these routes into the new routes list log.Print("Initialising the router") router = NewGenRouter(http.FileServer(http.Dir("./uploads"))) - router.HandleFunc("/topic/create/submit/", routeTopicCreateSubmit) - router.HandleFunc("/topic/", routeTopicID) - router.HandleFunc("/reply/create/", routeCreateReply) - //router.HandleFunc("/reply/edit/", routeReplyEdit) // No js fallback - //router.HandleFunc("/reply/delete/", routeReplyDelete) // No js confirmation page? We could have a confirmation modal for the JS case - router.HandleFunc("/reply/edit/submit/", routeReplyEditSubmit) - router.HandleFunc("/reply/delete/submit/", routeReplyDeleteSubmit) - router.HandleFunc("/reply/like/submit/", routeReplyLikeSubmit) - router.HandleFunc("/topic/edit/submit/", routeEditTopic) - router.HandleFunc("/topic/delete/submit/", routeDeleteTopic) - router.HandleFunc("/topic/stick/submit/", routeStickTopic) - router.HandleFunc("/topic/unstick/submit/", routeUnstickTopic) - router.HandleFunc("/topic/lock/submit/", routeLockTopic) - router.HandleFunc("/topic/unlock/submit/", routeUnlockTopic) - router.HandleFunc("/topic/move/submit/", routeMoveTopic) - router.HandleFunc("/topic/like/submit/", routeLikeTopic) // Accounts router.HandleFunc("/accounts/login/", routeLogin) diff --git a/member_routes.go b/member_routes.go index d710c9f1..2886fc2f 100644 --- a/member_routes.go +++ b/member_routes.go @@ -228,24 +228,12 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user common. } } + common.PostCounter.Bump() http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) return nil } -func routeCreateReply(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - // TODO: Reduce this to 1MB for attachments for each file? - // TODO: Reuse this code more - if r.ContentLength > int64(common.Config.MaxRequestSize) { - size, unit := common.ConvertByteUnit(float64(common.Config.MaxRequestSize)) - return common.CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user) - } - r.Body = http.MaxBytesReader(w, r.Body, int64(common.Config.MaxRequestSize)) - - err := r.ParseMultipartForm(int64(common.Megabyte)) - if err != nil { - return common.LocalError("Unable to parse the form", w, r, user) - } - +func routeCreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { tid, err := strconv.Atoi(r.PostFormValue("tid")) if err != nil { return common.PreError("Failed to convert the Topic ID", w, r) @@ -372,17 +360,14 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user common.User) if err != nil { return common.InternalError(err, w, r) } + + common.PostCounter.Bump() return nil } // TODO: Refactor this -func routeLikeTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - err := r.ParseForm() - if err != nil { - return common.PreError("Bad Form", w, r) - } - - tid, err := strconv.Atoi(r.URL.Path[len("/topic/like/submit/"):]) +func routeLikeTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { + tid, err := strconv.Atoi(stid) if err != nil { return common.PreError("Topic IDs can only ever be numbers.", w, r) } @@ -442,13 +427,8 @@ func routeLikeTopic(w http.ResponseWriter, r *http.Request, user common.User) co return nil } -func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - err := r.ParseForm() - if err != nil { - return common.PreError("Bad Form", w, r) - } - - rid, err := strconv.Atoi(r.URL.Path[len("/reply/like/submit/"):]) +func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user common.User, srid string) common.RouteError { + rid, err := strconv.Atoi(srid) if err != nil { return common.PreError("The provided Reply ID is not a valid number.", w, r) } @@ -524,10 +504,14 @@ func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user common if err != nil { return common.LocalError("Bad Form", w, r, user) } + uid, err := strconv.Atoi(r.PostFormValue("uid")) if err != nil { return common.LocalError("Invalid UID", w, r, user) } + if !common.Users.Exists(uid) { + return common.LocalError("The profile you're trying to post on doesn't exist.", w, r, user) + } content := common.PreparseMessage(r.PostFormValue("reply-content")) // TODO: Fully parse the post and store it in the parsed column @@ -536,10 +520,7 @@ func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user common return common.InternalError(err, w, r) } - if !common.Users.Exists(uid) { - return common.LocalError("The profile you're trying to post on doesn't exist.", w, r, user) - } - + common.PostCounter.Bump() http.Redirect(w, r, "/user/"+strconv.Itoa(uid), http.StatusSeeOther) return nil } @@ -629,6 +610,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user common.User, if err != nil && err != ErrNoRows { return common.InternalError(err, w, r) } + common.PostCounter.Bump() http.Redirect(w, r, "/topic/"+strconv.FormatInt(lastID, 10), http.StatusSeeOther) return nil @@ -719,20 +701,10 @@ func routeAccountEditAvatar(w http.ResponseWriter, r *http.Request, user common. } func routeAccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - if r.ContentLength > int64(common.Config.MaxRequestSize) { - size, unit := common.ConvertByteUnit(float64(common.Config.MaxRequestSize)) - return common.CustomError("Your avatar's too big. Avatars must be smaller than "+strconv.Itoa(int(size))+unit, http.StatusExpectationFailed, "Error", w, r, user) - } - r.Body = http.MaxBytesReader(w, r.Body, int64(common.Config.MaxRequestSize)) - headerVars, ferr := common.UserCheck(w, r, &user) if ferr != nil { return ferr } - err := r.ParseMultipartForm(int64(common.Megabyte)) - if err != nil { - return common.LocalError("Upload failed", w, r, user) - } var filename, ext string for _, fheaders := range r.MultipartForm.File { @@ -783,7 +755,7 @@ func routeAccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user c } } - err = user.ChangeAvatar("." + ext) + err := user.ChangeAvatar("." + ext) if err != nil { return common.InternalError(err, w, r) } diff --git a/mod_routes.go b/mod_routes.go index e163099a..7299d748 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -15,15 +15,10 @@ import ( // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Disable stat updates in posts handled by plugin_guilds -// TODO: Make sure this route is member only -func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - err := r.ParseForm() - if err != nil { - return common.PreError("Bad Form", w, r) - } +func routeEditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { isJs := (r.PostFormValue("js") == "1") - tid, err := strconv.Atoi(r.URL.Path[len("/topic/edit/submit/"):]) + tid, err := strconv.Atoi(stid) if err != nil { return common.PreErrorJSQ("The provided TopicID is not a valid number.", w, r, isJs) } @@ -64,8 +59,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) co // TODO: Add support for soft-deletion and add a permission for hard delete in addition to the usual // TODO: Disable stat updates in posts handled by plugin_guilds -// TODO: Make sure this route is member only -func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { +func routeDeleteTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { // TODO: Move this to some sort of middleware var tids []int var isJs = false @@ -131,8 +125,8 @@ func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user common.User) return nil } -func routeStickTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - tid, err := strconv.Atoi(r.URL.Path[len("/topic/stick/submit/"):]) +func routeStickTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { + tid, err := strconv.Atoi(stid) if err != nil { return common.PreError("The provided TopicID is not a valid number.", w, r) } @@ -170,8 +164,8 @@ func routeStickTopic(w http.ResponseWriter, r *http.Request, user common.User) c return nil } -func routeUnstickTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - tid, err := strconv.Atoi(r.URL.Path[len("/topic/unstick/submit/"):]) +func routeUnstickTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { + tid, err := strconv.Atoi(stid) if err != nil { return common.PreError("The provided TopicID is not a valid number.", w, r) } @@ -210,7 +204,7 @@ func routeUnstickTopic(w http.ResponseWriter, r *http.Request, user common.User) return nil } -func routeLockTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { +func routeLockTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { // TODO: Move this to some sort of middleware var tids []int var isJs = false @@ -272,8 +266,8 @@ func routeLockTopic(w http.ResponseWriter, r *http.Request, user common.User) co return nil } -func routeUnlockTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - tid, err := strconv.Atoi(r.URL.Path[len("/topic/unlock/submit/"):]) +func routeUnlockTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { + tid, err := strconv.Atoi(stid) if err != nil { return common.PreError("The provided TopicID is not a valid number.", w, r) } @@ -312,58 +306,70 @@ func routeUnlockTopic(w http.ResponseWriter, r *http.Request, user common.User) return nil } -func routeMoveTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - return common.NoPermissions(w, r, user) +// ! JS only route +// TODO: Figure a way to get this route to work without JS +func routeMoveTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + // Not fully implemented + return common.NoPermissionsJS(w, r, user) - tid, err := strconv.Atoi(r.URL.Path[len("/topic/move/submit/"):]) + // TODO: Move this to some sort of middleware + var tids []int + if r.Body == nil { + return common.PreErrorJS("No request body", w, r) + } + err := json.NewDecoder(r.Body).Decode(&tids) if err != nil { - return common.PreError("The provided TopicID is not a valid number.", w, r) + return common.PreErrorJS("We weren't able to parse your data", w, r) + } + if len(tids) == 0 { + return common.LocalErrorJS("You haven't provided any IDs", w, r) + } + fid := 0 + + for _, tid := range tids { + topic, err := common.Topics.Get(tid) + if err == ErrNoRows { + return common.PreErrorJS("The topic you tried to move doesn't exist.", w, r) + } else if err != nil { + return common.InternalErrorJS(err, w, r) + } + + // TODO: Add hooks to make use of headerLite + _, ferr := common.SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr + } + if !user.Perms.ViewTopic || !user.IsSuperMod { // TODO: Add a MoveTo permission + return common.NoPermissionsJS(w, r, user) + } + + err = topic.MoveTo(fid) + if err != nil { + return common.InternalErrorJS(err, w, r) + } + + err = common.ModLogs.Create("move", tid, "topic", user.LastIP, user.ID) + if err != nil { + return common.InternalErrorJS(err, w, r) + } + err = topic.CreateActionReply("move", user.LastIP, user) + if err != nil { + return common.InternalErrorJS(err, w, r) + } } - topic, err := common.Topics.Get(tid) - if err == ErrNoRows { - return common.PreError("The topic you tried to move doesn't exist.", w, r) - } else if err != nil { - return common.InternalError(err, w, r) + if len(tids) == 1 { + http.Redirect(w, r, "/topic/"+strconv.Itoa(tids[0]), http.StatusSeeOther) } - - // TODO: Add hooks to make use of headerLite - _, ferr := common.SimpleForumUserCheck(w, r, &user, topic.ParentID) - if ferr != nil { - return ferr - } - if !user.Perms.ViewTopic { // TODO: MoveTopic permission? - return common.NoPermissions(w, r, user) - } - - err = topic.Unlock() - if err != nil { - return common.InternalError(err, w, r) - } - - err = common.ModLogs.Create("move", tid, "topic", user.LastIP, user.ID) - if err != nil { - return common.InternalError(err, w, r) - } - err = topic.CreateActionReply("move", user.LastIP, user) - if err != nil { - return common.InternalError(err, w, r) - } - - http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) return nil } // TODO: Disable stat updates in posts handled by plugin_guilds // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes -func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - err := r.ParseForm() - if err != nil { - return common.PreError("Bad Form", w, r) - } +func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, srid string) common.RouteError { isJs := (r.PostFormValue("js") == "1") - rid, err := strconv.Atoi(r.URL.Path[len("/reply/edit/submit/"):]) + rid, err := strconv.Atoi(srid) if err != nil { return common.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs) } @@ -408,14 +414,10 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user common.Us // TODO: Refactor this // TODO: Disable stat updates in posts handled by plugin_guilds -func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - err := r.ParseForm() - if err != nil { - return common.PreError("Bad Form", w, r) - } +func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.User, srid string) common.RouteError { isJs := (r.PostFormValue("isJs") == "1") - rid, err := strconv.Atoi(r.URL.Path[len("/reply/delete/submit/"):]) + rid, err := strconv.Atoi(srid) if err != nil { return common.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs) } diff --git a/panel_routes.go b/panel_routes.go index 4abf9b93..ba110f3a 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -559,6 +559,52 @@ func routePanelForumsEditPermsAdvanceSubmit(w http.ResponseWriter, r *http.Reque return panelSuccessRedirect("/panel/forums/edit/perms/"+strconv.Itoa(fid)+"-"+strconv.Itoa(gid), w, r, isJs) } +type AnalyticsTimeRange struct { + Quantity int + Unit string + Slices int + SliceWidth int + Range string +} + +func panelAnalyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err error) { + timeRange.Quantity = 6 + timeRange.Unit = "hour" + timeRange.Slices = 12 + timeRange.SliceWidth = 60 * 30 + timeRange.Range = "six-hours" + + switch rawTimeRange { + case "one-month": + timeRange.Quantity = 30 + timeRange.Unit = "day" + timeRange.Slices = 30 + timeRange.SliceWidth = 60 * 60 * 24 + timeRange.Range = "one-month" + case "two-days": // Two days is experimental + timeRange.Quantity = 2 + timeRange.Unit = "day" + timeRange.Slices = 24 + timeRange.SliceWidth = 60 * 60 * 2 + timeRange.Range = "two-days" + case "one-day": + timeRange.Quantity = 1 + timeRange.Unit = "day" + timeRange.Slices = 24 + timeRange.SliceWidth = 60 * 60 + timeRange.Range = "one-day" + case "twelve-hours": + timeRange.Quantity = 12 + timeRange.Slices = 24 + timeRange.Range = "twelve-hours" + case "six-hours", "": + timeRange.Range = "six-hours" + default: + return timeRange, errors.New("Unknown time range") + } + return timeRange, nil +} + func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) if ferr != nil { @@ -567,6 +613,225 @@ func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user commo headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + var revLabelList []int64 + var labelList []int64 + var viewMap = make(map[int64]int64) + var currentTime = time.Now().Unix() + + for i := 1; i <= timeRange.Slices; i++ { + var label = currentTime - int64(i*timeRange.SliceWidth) + revLabelList = append(revLabelList, label) + viewMap[label] = 0 + } + for _, value := range revLabelList { + labelList = append(labelList, value) + } + + var viewList []int64 + log.Print("in routePanelAnalyticsViews") + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + if err != nil && err != ErrNoRows { + return common.InternalError(err, w, r) + } + defer rows.Close() + + for rows.Next() { + var count int64 + var createdAt time.Time + err := rows.Scan(&count, &createdAt) + if err != nil { + return common.InternalError(err, w, r) + } + log.Print("count: ", count) + log.Print("createdAt: ", createdAt) + + var unixCreatedAt = createdAt.Unix() + log.Print("unixCreatedAt: ", unixCreatedAt) + for _, value := range labelList { + if unixCreatedAt > value { + viewMap[value] += count + break + } + } + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + + var viewItems []common.PanelAnalyticsItem + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + log.Printf("graph: %+v\n", graph) + + pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", graph, viewItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_views", w, r, user, &pi) +} + +func routePanelAnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user common.User, route string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") + headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + + timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + var revLabelList []int64 + var labelList []int64 + var viewMap = make(map[int64]int64) + var currentTime = time.Now().Unix() + + for i := 1; i <= timeRange.Slices; i++ { + var label = currentTime - int64(i*timeRange.SliceWidth) + revLabelList = append(revLabelList, label) + viewMap[label] = 0 + } + for _, value := range revLabelList { + labelList = append(labelList, value) + } + + var viewList []int64 + log.Print("in routePanelAnalyticsRouteViews") + + acc := qgen.Builder.Accumulator() + // TODO: Validate the route is valid + rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(route) + if err != nil && err != ErrNoRows { + return common.InternalError(err, w, r) + } + defer rows.Close() + + for rows.Next() { + var count int64 + var createdAt time.Time + err := rows.Scan(&count, &createdAt) + if err != nil { + return common.InternalError(err, w, r) + } + log.Print("count: ", count) + log.Print("createdAt: ", createdAt) + + var unixCreatedAt = createdAt.Unix() + log.Print("unixCreatedAt: ", unixCreatedAt) + for _, value := range labelList { + if unixCreatedAt > value { + viewMap[value] += count + break + } + } + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + + var viewItems []common.PanelAnalyticsItem + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + log.Printf("graph: %+v\n", graph) + + pi := common.PanelAnalyticsRoutePage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", html.EscapeString(route), graph, viewItems, timeRange.Range} + return panelRenderTemplate("panel_analytics_route_views", w, r, user, &pi) +} + +func routePanelAnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user common.User, agent string) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") + headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + + timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange")) + if err != nil { + return common.LocalError(err.Error(), w, r, user) + } + + var revLabelList []int64 + var labelList []int64 + var viewMap = make(map[int64]int64) + var currentTime = time.Now().Unix() + + for i := 1; i <= timeRange.Slices; i++ { + var label = currentTime - int64(i*timeRange.SliceWidth) + revLabelList = append(revLabelList, label) + viewMap[label] = 0 + } + for _, value := range revLabelList { + labelList = append(labelList, value) + } + + var viewList []int64 + log.Print("in routePanelAnalyticsAgentViews") + + acc := qgen.Builder.Accumulator() + // TODO: Verify the agent is valid + rows, err := acc.Select("viewchunks_agents").Columns("count, createdAt").Where("browser = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(agent) + if err != nil && err != ErrNoRows { + return common.InternalError(err, w, r) + } + defer rows.Close() + + for rows.Next() { + var count int64 + var createdAt time.Time + err := rows.Scan(&count, &createdAt) + if err != nil { + return common.InternalError(err, w, r) + } + log.Print("count: ", count) + log.Print("createdAt: ", createdAt) + + var unixCreatedAt = createdAt.Unix() + log.Print("unixCreatedAt: ", unixCreatedAt) + for _, value := range labelList { + if unixCreatedAt > value { + viewMap[value] += count + break + } + } + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + + for _, value := range revLabelList { + viewList = append(viewList, viewMap[value]) + } + graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} + log.Printf("graph: %+v\n", graph) + + pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", html.EscapeString(agent), graph, timeRange.Range} + return panelRenderTemplate("panel_analytics_agent_views", w, r, user, &pi) +} + +func routePanelAnalyticsPosts(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") + headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") + var timeQuantity = 6 var timeUnit = "hour" var timeSlices = 12 @@ -617,10 +882,10 @@ func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user commo } var viewList []int64 - log.Print("in routePanelAnalyticsViews") + log.Print("in routePanelAnalyticsPosts") acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ''").DateCutoff("createdAt", timeQuantity, timeUnit).Query() + rows, err := acc.Select("postchunks").Columns("count, createdAt").DateCutoff("createdAt", timeQuantity, timeUnit).Query() if err != nil && err != ErrNoRows { return common.InternalError(err, w, r) } @@ -659,220 +924,7 @@ func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user commo log.Printf("graph: %+v\n", graph) pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", graph, viewItems, timeRange} - if common.PreRenderHooks["pre_render_panel_analytics"] != nil { - if common.RunPreRenderHook("pre_render_panel_analytics", w, r, &user, &pi) { - return nil - } - } - err = common.Templates.ExecuteTemplate(w, "panel-analytics-views.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - -func routePanelAnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user common.User, route string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") - headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") - - var timeQuantity = 6 - var timeUnit = "hour" - var timeSlices = 12 - var sliceWidth = 60 * 30 - var timeRange = "six-hours" - - switch r.FormValue("timeRange") { - case "one-month": - timeQuantity = 30 - timeUnit = "day" - timeSlices = 30 - sliceWidth = 60 * 60 * 24 - timeRange = "one-month" - case "two-days": // Two days is experimental - timeQuantity = 2 - timeUnit = "day" - timeSlices = 24 - sliceWidth = 60 * 60 * 2 - timeRange = "two-days" - case "one-day": - timeQuantity = 1 - timeUnit = "day" - timeSlices = 24 - sliceWidth = 60 * 60 - timeRange = "one-day" - case "twelve-hours": - timeQuantity = 12 - timeSlices = 24 - timeRange = "twelve-hours" - case "six-hours", "": - timeRange = "six-hours" - default: - return common.LocalError("Unknown time range", w, r, user) - } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - - for i := 1; i <= timeSlices; i++ { - var label = currentTime - int64(i*sliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } - - var viewList []int64 - log.Print("in routePanelAnalyticsRouteViews") - - acc := qgen.Builder.Accumulator() - // TODO: Validate the route is valid - rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ?").DateCutoff("createdAt", timeQuantity, timeUnit).Query(route) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - defer rows.Close() - - for rows.Next() { - var count int64 - var createdAt time.Time - err := rows.Scan(&count, &createdAt) - if err != nil { - return common.InternalError(err, w, r) - } - log.Print("count: ", count) - log.Print("createdAt: ", createdAt) - - var unixCreatedAt = createdAt.Unix() - log.Print("unixCreatedAt: ", unixCreatedAt) - for _, value := range labelList { - if unixCreatedAt > value { - viewMap[value] += count - break - } - } - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - log.Printf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsRoutePage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", html.EscapeString(route), graph, timeRange} - return panelRenderTemplate("panel_analytics_route_views", w, r, user, &pi) -} - -func routePanelAnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user common.User, agent string) common.RouteError { - headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) - if ferr != nil { - return ferr - } - headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css") - headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js") - - var timeQuantity = 6 - var timeUnit = "hour" - var timeSlices = 12 - var sliceWidth = 60 * 30 - var timeRange = "six-hours" - - switch r.FormValue("timeRange") { - case "one-month": - timeQuantity = 30 - timeUnit = "day" - timeSlices = 30 - sliceWidth = 60 * 60 * 24 - timeRange = "one-month" - case "two-days": // Two days is experimental - timeQuantity = 2 - timeUnit = "day" - timeSlices = 24 - sliceWidth = 60 * 60 * 2 - timeRange = "two-days" - case "one-day": - timeQuantity = 1 - timeUnit = "day" - timeSlices = 24 - sliceWidth = 60 * 60 - timeRange = "one-day" - case "twelve-hours": - timeQuantity = 12 - timeSlices = 24 - timeRange = "twelve-hours" - case "six-hours", "": - timeRange = "six-hours" - default: - return common.LocalError("Unknown time range", w, r, user) - } - - var revLabelList []int64 - var labelList []int64 - var viewMap = make(map[int64]int64) - var currentTime = time.Now().Unix() - - for i := 1; i <= timeSlices; i++ { - var label = currentTime - int64(i*sliceWidth) - revLabelList = append(revLabelList, label) - viewMap[label] = 0 - } - for _, value := range revLabelList { - labelList = append(labelList, value) - } - - var viewList []int64 - log.Print("in routePanelAnalyticsAgentViews") - - acc := qgen.Builder.Accumulator() - // TODO: Verify the agent is valid - rows, err := acc.Select("viewchunks_agents").Columns("count, createdAt").Where("browser = ?").DateCutoff("createdAt", timeQuantity, timeUnit).Query(agent) - if err != nil && err != ErrNoRows { - return common.InternalError(err, w, r) - } - defer rows.Close() - - for rows.Next() { - var count int64 - var createdAt time.Time - err := rows.Scan(&count, &createdAt) - if err != nil { - return common.InternalError(err, w, r) - } - log.Print("count: ", count) - log.Print("createdAt: ", createdAt) - - var unixCreatedAt = createdAt.Unix() - log.Print("unixCreatedAt: ", unixCreatedAt) - for _, value := range labelList { - if unixCreatedAt > value { - viewMap[value] += count - break - } - } - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - for _, value := range revLabelList { - viewList = append(viewList, viewMap[value]) - } - graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} - log.Printf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", html.EscapeString(agent), graph, timeRange} - return panelRenderTemplate("panel_analytics_agent_views", w, r, user, &pi) + return panelRenderTemplate("panel_analytics_posts", w, r, user, &pi) } func routePanelAnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { diff --git a/public/global.js b/public/global.js index 90f061ec..77e07a3b 100644 --- a/public/global.js +++ b/public/global.js @@ -6,6 +6,9 @@ var conn; var selectedTopics = []; var attachItemCallback = function(){} +// Topic move +var forumToMoveTo = 0; + // TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts function ajaxError(xhr,status,errstr) { console.log("The AJAX request failed"); @@ -500,6 +503,20 @@ $(document).ready(function(){ $(".mod_floater").removeClass("auto_hide"); }); }); + + let bulkActionSender = function(action, selectedTopics) { + let url = "/topic/"+action+"/submit/?session=" + session; + $.ajax({ + url: url, + type: "POST", + data: JSON.stringify(selectedTopics), + contentType: "application/json", + error: ajaxError, + success: function() { + window.location.reload(); + } + }); + }; $(".mod_floater_submit").click(function(event){ event.preventDefault(); let selectNode = this.form.querySelector(".mod_floater_options"); @@ -511,22 +528,24 @@ $(document).ready(function(){ switch(action) { case "move": console.log("move action"); + let modTopicMover = $("#mod_topic_mover"); $("#mod_topic_mover").removeClass("auto_hide"); + $("#mod_topic_mover .pane_row").click(function(){ + modTopicMover.find(".pane_row").removeClass("pane_selected"); + let fid = this.getAttribute("data-fid"); + if (fid == null) { + return; + } + this.classList.add("pane_selected"); + console.log("fid: " + fid); + let moverFid = document.getElementById("#mover_fid"); + console.log("moverFid: ", moverFid); + moverFid.value = fid; + }); return; } - let url = "/topic/"+action+"/submit/"; - //console.log("JSON.stringify(selectedTopics) ", JSON.stringify(selectedTopics)); - $.ajax({ - url: url, - type: "POST", - data: JSON.stringify(selectedTopics), - contentType: "application/json", - error: ajaxError, - success: function() { - window.location.reload(); - } - }); + bulkActionSender(action,selectedTopics); }); }); diff --git a/query_gen/tables.go b/query_gen/tables.go index 587a14f6..1d188dc4 100644 --- a/query_gen/tables.go +++ b/query_gen/tables.go @@ -386,6 +386,15 @@ func createTables(adapter qgen.Adapter) error { ) */ + qgen.Install.CreateTable("postchunks", "", "", + []qgen.DBTableColumn{ + qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, + qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, + // TODO: Add a column for the parent topic / profile? + }, + []qgen.DBTableKey{}, + ) + qgen.Install.CreateTable("sync", "", "", []qgen.DBTableColumn{ qgen.DBTableColumn{"last_update", "datetime", 0, false, false, ""}, diff --git a/router_gen/main.go b/router_gen/main.go index f0b64faa..e5c2f362 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -299,6 +299,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Print("req.URL.Path: ", req.URL.Path) log.Print("extraData: ", extraData) log.Print("req.Referer(): ", req.Referer()) + log.Print("req.RemoteAddr: ", req.RemoteAddr) } if prefix == "/static" { @@ -349,6 +350,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { if common.Dev.DebugMode { log.Print("Blank UA: ", req.UserAgent()) log.Print("Method: ", req.Method) + for key, value := range req.Header { for _, vvalue := range value { log.Print("Header '" + key + "': " + vvalue + "!!") @@ -358,6 +360,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Print("req.URL.Path: ", req.URL.Path) log.Print("extraData: ", extraData) log.Print("req.Referer(): ", req.Referer()) + log.Print("req.RemoteAddr: ", req.RemoteAddr) } default: common.AgentViewCounter.Bump({{.AllAgentMap.unknown}}) @@ -373,6 +376,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Print("req.URL.Path: ", req.URL.Path) log.Print("extraData: ", extraData) log.Print("req.Referer(): ", req.Referer()) + log.Print("req.RemoteAddr: ", req.RemoteAddr) } } diff --git a/router_gen/route_impl.go b/router_gen/route_impl.go index d89f5d28..64281f7b 100644 --- a/router_gen/route_impl.go +++ b/router_gen/route_impl.go @@ -95,10 +95,25 @@ func AnonAction(fname string, path string, args ...string) *RouteImpl { return route(fname, path, args...).Before("ParseForm") } -func UploadAction(fname string, path string, args ...string) *RouteImpl { +// Make this it's own type to force the user to manipulate methods on it to set parameters +type uploadAction struct { + Route *RouteImpl +} + +func UploadAction(fname string, path string, args ...string) *uploadAction { route := route(fname, path, args...) if !route.hasBefore("SuperModOnly", "AdminOnly") { route.Before("MemberOnly") } - return route + return &uploadAction{route} +} + +func (action *uploadAction) MaxSizeVar(varName string) *RouteImpl { + action.Route.LitBefore(`err = common.HandleUploadRoute(w,req,user,` + varName + `) + if err != nil { + router.handleError(err,w,req,user) + return + }`) + action.Route.Before("NoUploadSessionMismatch") + return action.Route } diff --git a/router_gen/routes.go b/router_gen/routes.go index 222a1e68..30973509 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -26,6 +26,8 @@ func routes() { buildPanelRoutes() buildUserRoutes() + buildTopicRoutes() + buildReplyRoutes() } // TODO: Test the email token route @@ -36,7 +38,7 @@ func buildUserRoutes() { MemberView("routeAccountEditCritical", "/user/edit/critical/"), Action("routeAccountEditCriticalSubmit", "/user/edit/critical/submit/"), // TODO: Full test this MemberView("routeAccountEditAvatar", "/user/edit/avatar/"), - UploadAction("routeAccountEditAvatarSubmit", "/user/edit/avatar/submit/"), + UploadAction("routeAccountEditAvatarSubmit", "/user/edit/avatar/submit/").MaxSizeVar("common.Config.MaxRequestSize"), MemberView("routeAccountEditUsername", "/user/edit/username/"), Action("routeAccountEditUsernameSubmit", "/user/edit/username/submit/"), // TODO: Full test this MemberView("routeAccountEditEmail", "/user/edit/email/"), @@ -55,6 +57,37 @@ func buildUserRoutes() { addRouteGroup(userGroup) } +func buildTopicRoutes() { + topicGroup := newRouteGroup("/topic/") + topicGroup.Routes( + View("routeTopicID", "/topic/", "extraData"), + Action("routeTopicCreateSubmit", "/topic/create/submit/"), + Action("routeEditTopicSubmit", "/topic/edit/submit/", "extraData"), + Action("routeDeleteTopicSubmit", "/topic/delete/submit/").LitBefore("req.URL.Path += extraData"), + Action("routeStickTopicSubmit", "/topic/stick/submit/", "extraData"), + Action("routeUnstickTopicSubmit", "/topic/unstick/submit/", "extraData"), + Action("routeLockTopicSubmit", "/topic/lock/submit/").LitBefore("req.URL.Path += extraData"), + Action("routeUnlockTopicSubmit", "/topic/unlock/submit/", "extraData"), + Action("routeMoveTopicSubmit", "/topic/move/submit/", "extraData"), + Action("routeLikeTopicSubmit", "/topic/like/submit/", "extraData"), + ) + addRouteGroup(topicGroup) +} + +func buildReplyRoutes() { + //router.HandleFunc("/reply/edit/", routeReplyEdit) // No js fallback + //router.HandleFunc("/reply/delete/", routeReplyDelete) // No js confirmation page? We could have a confirmation modal for the JS case + replyGroup := newRouteGroup("/reply/") + replyGroup.Routes( + // TODO: Reduce this to 1MB for attachments for each file? + UploadAction("routeCreateReplySubmit", "/reply/create/").MaxSizeVar("common.Config.MaxRequestSize"), // TODO: Rename the route so it's /reply/create/submit/ + Action("routeReplyEditSubmit", "/reply/edit/submit/", "extraData"), + Action("routeReplyDeleteSubmit", "/reply/delete/submit/", "extraData"), + Action("routeReplyLikeSubmit", "/reply/like/submit/", "extraData"), + ) + addRouteGroup(replyGroup) +} + func buildPanelRoutes() { panelGroup := newRouteGroup("/panel/").Before("SuperModOnly") panelGroup.Routes( @@ -96,6 +129,7 @@ func buildPanelRoutes() { View("routePanelAnalyticsAgents", "/panel/analytics/agents/"), View("routePanelAnalyticsRouteViews", "/panel/analytics/route/", "extraData"), View("routePanelAnalyticsAgentViews", "/panel/analytics/agent/", "extraData"), + View("routePanelAnalyticsPosts", "/panel/analytics/posts/").Before("ParseForm"), View("routePanelGroups", "/panel/groups/"), View("routePanelGroupsEdit", "/panel/groups/edit/", "extraData"), diff --git a/routes.go b/routes.go index a2c5bb1a..9586ccb9 100644 --- a/routes.go +++ b/routes.go @@ -438,7 +438,7 @@ 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) common.RouteError { +func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User, urlBit string) common.RouteError { var err error var page, offset int var replyList []common.ReplyUser @@ -447,7 +447,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm // SEO URLs... // TODO: Make a shared function for this - halves := strings.Split(r.URL.Path[len("/topic/"):], ".") + halves := strings.Split(urlBit, ".") if len(halves) < 2 { halves = append(halves, halves[0]) } diff --git a/schema/mssql/query_postchunks.sql b/schema/mssql/query_postchunks.sql new file mode 100644 index 00000000..68bf37ff --- /dev/null +++ b/schema/mssql/query_postchunks.sql @@ -0,0 +1,4 @@ +CREATE TABLE [postchunks] ( + [count] int DEFAULT 0 not null, + [createdAt] datetime not null +); \ No newline at end of file diff --git a/schema/mysql/query_postchunks.sql b/schema/mysql/query_postchunks.sql new file mode 100644 index 00000000..b0d3b4be --- /dev/null +++ b/schema/mysql/query_postchunks.sql @@ -0,0 +1,4 @@ +CREATE TABLE `postchunks` ( + `count` int DEFAULT 0 not null, + `createdAt` datetime not null +); \ No newline at end of file diff --git a/schema/pgsql/query_postchunks.sql b/schema/pgsql/query_postchunks.sql new file mode 100644 index 00000000..b6ee2dd3 --- /dev/null +++ b/schema/pgsql/query_postchunks.sql @@ -0,0 +1,4 @@ +CREATE TABLE `postchunks` ( + `count` int DEFAULT 0 not null, + `createdAt` timestamp not null +); \ No newline at end of file diff --git a/template_list.go b/template_list.go index 2f5e1c5c..8059405e 100644 --- a/template_list.go +++ b/template_list.go @@ -87,165 +87,176 @@ var header_21 = []byte(``) var topic_0 = []byte(`
+var topic_1 = []byte(`?session=`) +var topic_2 = []byte(`' method="post"> `) -var topic_2 = []byte(` +var topic_3 = []byte(`
`) -var topic_7 = []byte(` +var topic_6 = []byte(`?page=`) +var topic_7 = []byte(`"><`) +var topic_8 = []byte(`
+var topic_11 = []byte(`?page=`) +var topic_12 = []byte(`">>
`) -var topic_12 = []byte(` +var topic_13 = []byte(`
+var topic_14 = []byte(` topic_sticky_head`) +var topic_15 = []byte(` topic_closed_head`) +var topic_16 = []byte(`">

`) -var topic_16 = []byte(`

+var topic_17 = []byte(` `) -var topic_17 = []byte(`🔒︎`) -var topic_18 = []byte(` +var topic_18 = []byte(`🔒︎`) +var topic_19 = []byte(` +var topic_20 = []byte(`' type="text" /> `) -var topic_20 = []byte(` +var topic_21 = []byte(`
+var topic_22 = []byte(`" style="background-image: url(`) +var topic_23 = []byte(`), url(/static/`) +var topic_24 = []byte(`/post-avatar-bg.jpg);background-position: 0px `) +var topic_25 = []byte(`-1`) +var topic_26 = []byte(`0px;background-repeat:no-repeat, repeat-y;">

`) -var topic_26 = []byte(`

+var topic_27 = []byte(`

+var topic_28 = []byte(`    +var topic_29 = []byte(`" class="username real_username" rel="author">`) +var topic_30 = []byte(`   `) -var topic_30 = []byte(` +var topic_31 = []byte(` `) -var topic_37 = []byte(``) -var topic_39 = []byte(``) -var topic_41 = []byte(``) -var topic_43 = []byte(``) -var topic_45 = []byte(``) -var topic_47 = []byte(``) -var topic_49 = []byte(``) -var topic_52 = []byte(` +var topic_37 = []byte(` style="background-color:#D6FFD6;"`) +var topic_38 = []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_60 = []byte(`?session=`) +var topic_61 = []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_55 = []byte(``) -var topic_57 = []byte(``) -var topic_58 = []byte(``) -var topic_59 = []byte(``) -var topic_60 = []byte(``) -var topic_61 = []byte(` +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_62 = []byte(` +var topic_69 = []byte(`
`) -var topic_63 = []byte(` +var topic_70 = []byte(` `) -var topic_64 = []byte(` +var topic_71 = []byte(`
`) -var topic_65 = []byte(` +var topic_72 = []byte(`
+var topic_73 = []byte(`" style="background-image: url(`) +var topic_74 = []byte(`), url(/static/`) +var topic_75 = []byte(`/post-avatar-bg.jpg);background-position: 0px `) +var topic_76 = []byte(`-1`) +var topic_77 = []byte(`0px;background-repeat:no-repeat, repeat-y;"> `) -var topic_71 = []byte(` +var topic_78 = []byte(`

`) -var topic_72 = []byte(`

+var topic_79 = []byte(`

   +var topic_80 = []byte(`" class="username real_username" rel="author">`) +var topic_81 = []byte(`   `) -var topic_75 = []byte(``) -var topic_79 = []byte(``) -var topic_81 = []byte(``) -var topic_83 = []byte(``) -var topic_85 = []byte(` +var topic_82 = []byte(``) +var topic_87 = []byte(``) +var topic_90 = []byte(``) +var topic_93 = []byte(``) +var topic_95 = []byte(` +var topic_96 = []byte(`?session=`) +var topic_97 = []byte(`&type=reply" class="mod_button report_item" title="Flag this reply" aria-label="Flag this reply" rel="nofollow"> `) -var topic_88 = []byte(``) -var topic_90 = []byte(``) -var topic_91 = []byte(``) -var topic_92 = []byte(``) -var topic_93 = []byte(``) -var topic_94 = []byte(` +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_95 = []byte(`
+var topic_105 = []byte(` `) -var topic_96 = []byte(` +var topic_106 = []byte(`
-
+
+var topic_108 = []byte(`' type="hidden" />
@@ -255,16 +266,16 @@ var topic_97 = []byte(`' type="hidden" />
`) -var topic_98 = []byte(` +var topic_109 = []byte(`
`) -var topic_99 = []byte(` +var topic_110 = []byte(`
`) -var topic_100 = []byte(` +var topic_111 = []byte(`
@@ -318,21 +329,22 @@ var topic_alt_10 = []byte(`
+var topic_alt_11 = []byte(`?session=`) +var topic_alt_12 = []byte(`' method="post">
+var topic_alt_13 = []byte(` topic_sticky_head`) +var topic_alt_14 = []byte(` topic_closed_head`) +var topic_alt_15 = []byte(`">

`) -var topic_alt_15 = []byte(`

+var topic_alt_16 = []byte(` `) -var topic_alt_16 = []byte(`🔒︎`) -var topic_alt_17 = []byte(` +var topic_alt_17 = []byte(`🔒︎`) +var topic_alt_18 = []byte(` +var topic_alt_19 = []byte(`' type="text" /> `) -var topic_alt_19 = []byte(` +var topic_alt_20 = []byte(`
@@ -341,158 +353,168 @@ var topic_alt_19 = []byte(`
 
+var topic_alt_21 = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;"> 
+var topic_alt_22 = []byte(`" class="the_name" rel="author">`) +var topic_alt_23 = []byte(` `) -var topic_alt_23 = []byte(`
`) -var topic_alt_25 = []byte(`
`) -var topic_alt_27 = []byte(` +var topic_alt_24 = []byte(`
`) +var topic_alt_26 = []byte(`
`) +var topic_alt_28 = []byte(`
`) -var topic_alt_28 = []byte(`
+var topic_alt_29 = []byte(`
+var topic_alt_30 = []byte(`
`) -var topic_alt_30 = []byte(``) -var topic_alt_32 = []byte(``) -var topic_alt_34 = []byte(``) -var topic_alt_36 = []byte(``) -var topic_alt_38 = []byte(``) -var topic_alt_40 = []byte(``) -var topic_alt_42 = []byte(``) -var topic_alt_44 = []byte(``) -var topic_alt_46 = []byte(` +var topic_alt_31 = []byte(``) +var topic_alt_34 = []byte(``) +var topic_alt_36 = []byte(``) +var topic_alt_39 = []byte(``) +var topic_alt_42 = []byte(``) +var topic_alt_45 = []byte(``) +var topic_alt_48 = []byte(``) +var topic_alt_51 = []byte(``) +var topic_alt_53 = []byte(` +var topic_alt_54 = []byte(`?session=`) +var topic_alt_55 = []byte(`&type=topic" class="action_button report_item" aria-label="Report this post" data-action="report"> `) -var topic_alt_49 = []byte(` +var topic_alt_56 = []byte(`
+var topic_alt_57 = []byte(` has_likes`) +var topic_alt_58 = []byte(`"> `) -var topic_alt_52 = []byte(``) -var topic_alt_54 = []byte(` +var topic_alt_59 = []byte(``) +var topic_alt_61 = []byte(` `) -var topic_alt_55 = []byte(` +var topic_alt_62 = []byte(` `) -var topic_alt_56 = []byte(``) -var topic_alt_58 = []byte(``) -var topic_alt_59 = []byte(` +var topic_alt_63 = []byte(``) +var topic_alt_65 = []byte(``) +var topic_alt_66 = []byte(`
`) -var topic_alt_60 = []byte(` +var topic_alt_67 = []byte(`
+var topic_alt_68 = []byte(`action_item`) +var topic_alt_69 = []byte(`">
 
+var topic_alt_70 = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;"> 
+var topic_alt_71 = []byte(`" class="the_name" rel="author">`) +var topic_alt_72 = []byte(` `) -var topic_alt_66 = []byte(`
`) -var topic_alt_68 = []byte(`
`) -var topic_alt_70 = []byte(` +var topic_alt_73 = []byte(`
`) +var topic_alt_75 = []byte(`
`) +var topic_alt_77 = []byte(`
+var topic_alt_78 = []byte(`style="margin-left: 0px;"`) +var topic_alt_79 = []byte(`> `) -var topic_alt_73 = []byte(` +var topic_alt_80 = []byte(` `) -var topic_alt_74 = []byte(` +var topic_alt_81 = []byte(` `) -var topic_alt_75 = []byte(` +var topic_alt_82 = []byte(` `) -var topic_alt_76 = []byte(` +var topic_alt_83 = []byte(`
`) -var topic_alt_77 = []byte(`
+var topic_alt_84 = []byte(`
`) -var topic_alt_78 = []byte(``) -var topic_alt_80 = []byte(``) -var topic_alt_82 = []byte(``) -var topic_alt_84 = []byte(``) -var topic_alt_86 = []byte(` +var topic_alt_85 = []byte(``) +var topic_alt_88 = []byte(``) +var topic_alt_91 = []byte(``) +var topic_alt_94 = []byte(``) +var topic_alt_96 = []byte(` +var topic_alt_97 = []byte(`?session=`) +var topic_alt_98 = []byte(`&type=reply" class="action_button report_item" aria-label="Report this post" data-action="report"> `) -var topic_alt_89 = []byte(` -
- `) -var topic_alt_92 = []byte(``) -var topic_alt_94 = []byte(` - `) -var topic_alt_95 = []byte(` - `) -var topic_alt_96 = []byte(``) -var topic_alt_98 = []byte(``) var topic_alt_99 = []byte(` +
+ `) +var topic_alt_102 = []byte(``) +var topic_alt_104 = []byte(` + `) +var topic_alt_105 = []byte(` + `) +var topic_alt_106 = []byte(``) +var topic_alt_108 = []byte(``) +var topic_alt_109 = []byte(`
`) -var topic_alt_100 = []byte(` +var topic_alt_110 = []byte(`
`) -var topic_alt_101 = []byte(` +var topic_alt_111 = []byte(` `) -var topic_alt_102 = []byte(` +var topic_alt_112 = []byte(`
 
+var topic_alt_113 = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;"> 
+var topic_alt_114 = []byte(`" class="the_name" rel="author">`) +var topic_alt_115 = []byte(` `) -var topic_alt_106 = []byte(`
`) -var topic_alt_108 = []byte(`
`) -var topic_alt_110 = []byte(` +var topic_alt_116 = []byte(`
`) +var topic_alt_118 = []byte(`
`) +var topic_alt_120 = []byte(`
-
+
+var topic_alt_122 = []byte(`' type="hidden" />
@@ -502,17 +524,17 @@ var topic_alt_111 = []byte(`' type="hidden" />
`) -var topic_alt_112 = []byte(` +var topic_alt_123 = []byte(`
`) -var topic_alt_113 = []byte(` +var topic_alt_124 = []byte(`
`) -var topic_alt_114 = []byte(` +var topic_alt_125 = []byte(` @@ -821,18 +843,20 @@ var topics_8 = []byte(` `) var topics_9 = []byte(`