diff --git a/.codebeatignore b/.codebeatignore index b6fccf76..459e3a7e 100644 --- a/.codebeatignore +++ b/.codebeatignore @@ -1,3 +1,4 @@ +/public/chartist/** /public/trumbowyg/** /public/jquery-emojiarea/** /public/font-awesome-4.7.0/** @@ -17,3 +18,4 @@ template_profile.go gen_mysql.go gen_mssql.go gen_pgsql.go +gen_router.go diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..4cc0ec54 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,17 @@ +exclude_patterns: +- "gen_*" +- "schema/*" +- "public/chartist/*" +- "public/trumbowyg/*" +- "public/jquery-emojiarea/*" +- "public/font-awesome-4.7.0/*" +- "public/jquery-3.1.1.min.js" +- "public/EQCSS.min.js" +- "public/EQCSS.js" +- "template_list.go" +- "template_forum.go" +- "template_forums.go" +- "template_topic.go" +- "template_topic_alt.go" +- "template_topics.go" +- "template_profile.go" \ No newline at end of file diff --git a/common/extend.go b/common/extend.go index 99fb2441..562bb8fb 100644 --- a/common/extend.go +++ b/common/extend.go @@ -75,7 +75,7 @@ var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User "pre_render": nil, "pre_render_forum_list": nil, - "pre_render_view_forum": nil, + "pre_render_forum": nil, "pre_render_topic_list": nil, "pre_render_view_topic": nil, "pre_render_profile": nil, @@ -90,7 +90,7 @@ var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User "pre_render_login": nil, "pre_render_register": nil, "pre_render_ban": nil, - "pre_render_ips": nil, + "pre_render_ip_search": nil, "pre_render_panel_dashboard": nil, "pre_render_panel_forums": nil, diff --git a/common/ip_search.go b/common/ip_search.go new file mode 100644 index 00000000..c81dfec5 --- /dev/null +++ b/common/ip_search.go @@ -0,0 +1,76 @@ +package common + +import ( + "database/sql" + + "../query_gen/lib" +) + +var IPSearch IPSearcher + +type IPSearcher interface { + Lookup(ip string) (uids []int, err error) +} + +type DefaultIPSearcher struct { + searchUsers *sql.Stmt + searchReplies *sql.Stmt + searchTopics *sql.Stmt +} + +// NewDefaultIPSearcher gives you a new instance of DefaultIPSearcher +func NewDefaultIPSearcher() (*DefaultIPSearcher, error) { + acc := qgen.Builder.Accumulator() + return &DefaultIPSearcher{ + searchUsers: acc.Select("users").Columns("uid").Where("last_ip = ?").Prepare(), + searchTopics: acc.Select("users").Columns("uid").InQ("uid", acc.Select("topics").Columns("createdBy").Where("ipaddress = ?")).Prepare(), + searchReplies: acc.Select("users").Columns("uid").InQ("uid", acc.Select("replies").Columns("createdBy").Where("ipaddress = ?")).Prepare(), + }, acc.FirstError() +} + +func (searcher *DefaultIPSearcher) Lookup(ip string) (uids []int, err error) { + var uid int + var reqUserList = make(map[int]bool) + + var runQuery = func(stmt *sql.Stmt) error { + rows, err := stmt.Query(ip) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + err := rows.Scan(&uid) + if err != nil { + return err + } + reqUserList[uid] = true + } + return rows.Err() + } + + err = runQuery(searcher.searchUsers) + if err != nil { + return uids, err + } + + err = runQuery(searcher.searchTopics) + if err != nil { + return uids, err + } + + err = runQuery(searcher.searchReplies) + if err != nil { + return uids, err + } + + // Convert the user ID map to a slice, then bulk load the users + uids = make([]int, len(reqUserList)) + var i int + for userID := range reqUserList { + uids[i] = userID + i++ + } + + return uids, nil +} diff --git a/gen_mssql.go b/gen_mssql.go index eb618180..a3a9ec1e 100644 --- a/gen_mssql.go +++ b/gen_mssql.go @@ -64,9 +64,6 @@ type Stmts struct { todaysTopicCount *sql.Stmt todaysReportCount *sql.Stmt todaysNewUserCount *sql.Stmt - findUsersByIPUsers *sql.Stmt - findUsersByIPTopics *sql.Stmt - findUsersByIPReplies *sql.Stmt Mocks bool } diff --git a/gen_mysql.go b/gen_mysql.go index 5297a126..55e07e0d 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -66,9 +66,6 @@ type Stmts struct { todaysTopicCount *sql.Stmt todaysReportCount *sql.Stmt todaysNewUserCount *sql.Stmt - findUsersByIPUsers *sql.Stmt - findUsersByIPTopics *sql.Stmt - findUsersByIPReplies *sql.Stmt Mocks bool } diff --git a/gen_pgsql.go b/gen_pgsql.go index 0db9f8ac..2e42523c 100644 --- a/gen_pgsql.go +++ b/gen_pgsql.go @@ -28,9 +28,6 @@ type Stmts struct { todaysTopicCount *sql.Stmt todaysReportCount *sql.Stmt todaysNewUserCount *sql.Stmt - findUsersByIPUsers *sql.Stmt - findUsersByIPTopics *sql.Stmt - findUsersByIPReplies *sql.Stmt Mocks bool } diff --git a/gen_router.go b/gen_router.go index 59aeb0dd..536e9e2e 100644 --- a/gen_router.go +++ b/gen_router.go @@ -25,7 +25,7 @@ var RouteMap = map[string]interface{}{ "routeShowAttachment": routeShowAttachment, "routeWebsockets": routeWebsockets, "routeReportSubmit": routeReportSubmit, - "routeTopicCreate": routeTopicCreate, + "routes.CreateTopic": routes.CreateTopic, "routeTopics": routeTopics, "routePanelForums": routePanelForums, "routePanelForumsCreateSubmit": routePanelForumsCreateSubmit, @@ -79,11 +79,11 @@ var RouteMap = map[string]interface{}{ "routeAccountEditEmail": routeAccountEditEmail, "routeAccountEditEmailTokenSubmit": routeAccountEditEmailTokenSubmit, "routeProfile": routeProfile, - "routeBanSubmit": routeBanSubmit, - "routeUnban": routeUnban, - "routeActivate": routeActivate, - "routeIps": routeIps, - "routeTopicCreateSubmit": routeTopicCreateSubmit, + "routes.BanUserSubmit": routes.BanUserSubmit, + "routes.UnbanUser": routes.UnbanUser, + "routes.ActivateUser": routes.ActivateUser, + "routes.IPSearch": routes.IPSearch, + "routeCreateTopicSubmit": routeCreateTopicSubmit, "routes.EditTopicSubmit": routes.EditTopicSubmit, "routes.DeleteTopicSubmit": routes.DeleteTopicSubmit, "routes.StickTopicSubmit": routes.StickTopicSubmit, @@ -120,7 +120,7 @@ var routeMapEnum = map[string]int{ "routeShowAttachment": 6, "routeWebsockets": 7, "routeReportSubmit": 8, - "routeTopicCreate": 9, + "routes.CreateTopic": 9, "routeTopics": 10, "routePanelForums": 11, "routePanelForumsCreateSubmit": 12, @@ -174,11 +174,11 @@ var routeMapEnum = map[string]int{ "routeAccountEditEmail": 60, "routeAccountEditEmailTokenSubmit": 61, "routeProfile": 62, - "routeBanSubmit": 63, - "routeUnban": 64, - "routeActivate": 65, - "routeIps": 66, - "routeTopicCreateSubmit": 67, + "routes.BanUserSubmit": 63, + "routes.UnbanUser": 64, + "routes.ActivateUser": 65, + "routes.IPSearch": 66, + "routeCreateTopicSubmit": 67, "routes.EditTopicSubmit": 68, "routes.DeleteTopicSubmit": 69, "routes.StickTopicSubmit": 70, @@ -213,7 +213,7 @@ var reverseRouteMapEnum = map[int]string{ 6: "routeShowAttachment", 7: "routeWebsockets", 8: "routeReportSubmit", - 9: "routeTopicCreate", + 9: "routes.CreateTopic", 10: "routeTopics", 11: "routePanelForums", 12: "routePanelForumsCreateSubmit", @@ -267,11 +267,11 @@ var reverseRouteMapEnum = map[int]string{ 60: "routeAccountEditEmail", 61: "routeAccountEditEmailTokenSubmit", 62: "routeProfile", - 63: "routeBanSubmit", - 64: "routeUnban", - 65: "routeActivate", - 66: "routeIps", - 67: "routeTopicCreateSubmit", + 63: "routes.BanUserSubmit", + 64: "routes.UnbanUser", + 65: "routes.ActivateUser", + 66: "routes.IPSearch", + 67: "routeCreateTopicSubmit", 68: "routes.EditTopicSubmit", 69: "routes.DeleteTopicSubmit", 70: "routes.StickTopicSubmit", @@ -657,7 +657,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } common.RouteViewCounter.Bump(9) - err = routeTopicCreate(w,req,user,extraData) + err = routes.CreateTopic(w,req,user,extraData) default: common.RouteViewCounter.Bump(10) err = routeTopics(w,req,user) @@ -1083,7 +1083,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } common.RouteViewCounter.Bump(63) - err = routeBanSubmit(w,req,user,extraData) + err = routes.BanUserSubmit(w,req,user,extraData) case "/users/unban/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -1098,7 +1098,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } common.RouteViewCounter.Bump(64) - err = routeUnban(w,req,user,extraData) + err = routes.UnbanUser(w,req,user,extraData) case "/users/activate/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -1113,7 +1113,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } common.RouteViewCounter.Bump(65) - err = routeActivate(w,req,user,extraData) + err = routes.ActivateUser(w,req,user,extraData) case "/users/ips/": err = common.MemberOnly(w,req,user) if err != nil { @@ -1122,7 +1122,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } common.RouteViewCounter.Bump(66) - err = routeIps(w,req,user) + err = routes.IPSearch(w,req,user) } if err != nil { router.handleError(err,w,req,user) @@ -1143,7 +1143,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } common.RouteViewCounter.Bump(67) - err = routeTopicCreateSubmit(w,req,user) + err = routeCreateTopicSubmit(w,req,user) case "/topic/edit/submit/": err = common.NoSessionMismatch(w,req,user) if err != nil { diff --git a/main.go b/main.go index e10f199d..e2a5c135 100644 --- a/main.go +++ b/main.go @@ -80,6 +80,11 @@ func afterDBInit() (err error) { if err != nil { return err } + common.IPSearch, err = common.NewDefaultIPSearcher() + if err != nil { + return err + } + common.GlobalViewCounter, err = common.NewGlobalViewCounter() if err != nil { return err diff --git a/member_routes.go b/member_routes.go index 027f9d8f..30ab8243 100644 --- a/member_routes.go +++ b/member_routes.go @@ -16,110 +16,7 @@ import ( "./common" ) -// Experimenting -/*func memberRenderTemplate(tmplName string, themeName string, w http.ResponseWriter, r *http.Request, user common.User, pi interface{}) common.RouteError { - if common.PreRenderHooks["pre_render_"+tmplName] != nil { - if common.RunPreRenderHook("pre_render_"+tmplName, w, r, &user, pi) { - return nil - } - } - - err := common.RunThemeTemplate(themeName, tmplName, pi, w) - if err != nil { - return common.InternalError(err, w, r) - } - 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 -// ? - Log username changes and put restrictions on this? -func routeTopicCreate(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { - var fid int - var err error - if sfid != "" { - fid, err = strconv.Atoi(sfid) - if err != nil { - return common.LocalError("You didn't provide a valid number for the forum ID.", w, r, user) - } - } - if fid == 0 { - fid = common.Config.DefaultForum - } - - headerVars, ferr := common.ForumUserCheck(w, r, &user, fid) - if ferr != nil { - return ferr - } - if !user.Perms.ViewTopic || !user.Perms.CreateTopic { - return common.NoPermissions(w, r, user) - } - headerVars.Zone = "create_topic" - - // Lock this to the forum being linked? - // Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile) - var strictmode bool - if common.Vhooks["topic_create_pre_loop"] != nil { - common.RunVhook("topic_create_pre_loop", w, r, fid, &headerVars, &user, &strictmode) - } - - // TODO: Re-add support for plugin_guilds - var forumList []common.Forum - var canSee []int - if user.IsSuperAdmin { - canSee, err = common.Forums.GetAllVisibleIDs() - if err != nil { - return common.InternalError(err, w, r) - } - } else { - group, err := common.Groups.Get(user.Group) - if err != nil { - // TODO: Refactor this - common.LocalError("Something weird happened behind the scenes", w, r, user) - log.Printf("Group #%d doesn't exist, but it's set on common.User #%d", user.Group, user.ID) - return nil - } - canSee = group.CanSee - } - - // TODO: plugin_superadmin needs to be able to override this loop. Skip flag on topic_create_pre_loop? - for _, ffid := range canSee { - // TODO: Surely, there's a better way of doing this. I've added it in for now to support plugin_guilds, but we really need to clean this up - if strictmode && ffid != fid { - continue - } - - // Do a bulk forum fetch, just in case it's the SqlForumStore? - forum := common.Forums.DirtyGet(ffid) - if forum.Name != "" && forum.Active { - fcopy := forum.Copy() - if common.Hooks["topic_create_frow_assign"] != nil { - // TODO: Add the skip feature to all the other row based hooks? - if common.RunHook("topic_create_frow_assign", &fcopy).(bool) { - continue - } - } - forumList = append(forumList, fcopy) - } - } - - ctpage := common.CreateTopicPage{"Create Topic", user, headerVars, forumList, fid} - if common.PreRenderHooks["pre_render_create_topic"] != nil { - if common.RunPreRenderHook("pre_render_create_topic", w, r, &user, &ctpage) { - return nil - } - } - - err = common.RunThemeTemplate(headerVars.Theme.Name, "create_topic", ctpage, w) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - -// POST functions. Authorised users only. -func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { +func routeCreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { // TODO: Reduce this to 1MB for attachments for each file? if r.ContentLength > int64(common.Config.MaxRequestSize) { size, unit := common.ConvertByteUnit(float64(common.Config.MaxRequestSize)) diff --git a/mssql.go b/mssql.go index 74f49379..08e62100 100644 --- a/mssql.go +++ b/mssql.go @@ -75,56 +75,41 @@ func initMSSQL() (err error) { } // TODO: Is there a less noisy way of doing this for tests? - log.Print("Preparing get_activity_feed_by_watcher statement.") + log.Print("Preparing getActivityFeedByWatcher statement.") getActivityFeedByWatcherStmt, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID FROM [activity_stream_matches] INNER JOIN [activity_stream] ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE [watcher] = ? ORDER BY activity_stream.asid ASC OFFSET 0 ROWS FETCH NEXT 8 ROWS ONLY") if err != nil { return err } - log.Print("Preparing get_activity_count_by_watcher statement.") + log.Print("Preparing getActivityCountByWatcher statement.") getActivityCountByWatcherStmt, err = db.Prepare("SELECT count(*) FROM [activity_stream_matches] INNER JOIN [activity_stream] ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE [watcher] = ?") if err != nil { return err } - log.Print("Preparing todays_post_count statement.") + log.Print("Preparing todaysPostCount statement.") todaysPostCountStmt, err = db.Prepare("select count(*) from replies where createdAt >= DATEADD(DAY, -1, GETUTCDATE())") if err != nil { return err } - log.Print("Preparing todays_topic_count statement.") + log.Print("Preparing todaysTopicCount statement.") todaysTopicCountStmt, err = db.Prepare("select count(*) from topics where createdAt >= DATEADD(DAY, -1, GETUTCDATE())") if err != nil { return err } - log.Print("Preparing todays_report_count statement.") + log.Print("Preparing todaysReportCount statement.") todaysReportCountStmt, err = db.Prepare("select count(*) from topics where createdAt >= DATEADD(DAY, -1, GETUTCDATE()) and parentID = 1") if err != nil { return err } - log.Print("Preparing todays_newuser_count statement.") + log.Print("Preparing todaysNewUserCount statement.") todaysNewUserCountStmt, err = db.Prepare("select count(*) from users where createdAt >= DATEADD(DAY, -1, GETUTCDATE())") if err != nil { return err } - // ? - Why is this a custom query? Are we planning a union or something? - log.Print("Preparing find_users_by_ip_users statement.") - findUsersByIPUsersStmt, err = db.Prepare("select uid from users where last_ip = ?") - if err != nil { - return err - } - - log.Print("Preparing find_users_by_ip_topics statement.") - findUsersByIPTopicsStmt, err = db.Prepare("select uid from users where uid in(select createdBy from topics where ipaddress = ?)") - if err != nil { - return err - } - - log.Print("Preparing find_users_by_ip_replies statement.") - findUsersByIPRepliesStmt, err = db.Prepare("select uid from users where uid in(select createdBy from replies where ipaddress = ?)") - return err + return nil } diff --git a/mysql.go b/mysql.go index 24146119..68ad040e 100644 --- a/mysql.go +++ b/mysql.go @@ -69,55 +69,41 @@ func initMySQL() (err error) { } // TODO: Is there a less noisy way of doing this for tests? - log.Print("Preparing get_activity_feed_by_watcher statement.") + log.Print("Preparing getActivityFeedByWatcher statement.") stmts.getActivityFeedByWatcher, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID FROM `activity_stream_matches` INNER JOIN `activity_stream` ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE `watcher` = ? ORDER BY activity_stream.asid ASC LIMIT 8") if err != nil { return err } - log.Print("Preparing get_activity_count_by_watcher statement.") + log.Print("Preparing getActivityCountByWatcher statement.") stmts.getActivityCountByWatcher, err = db.Prepare("SELECT count(*) FROM `activity_stream_matches` INNER JOIN `activity_stream` ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE `watcher` = ?") if err != nil { return err } - log.Print("Preparing todays_post_count statement.") + log.Print("Preparing todaysPostCount statement.") stmts.todaysPostCount, err = db.Prepare("select count(*) from replies where createdAt BETWEEN (utc_timestamp() - interval 1 day) and utc_timestamp()") if err != nil { return err } - log.Print("Preparing todays_topic_count statement.") + log.Print("Preparing todaysTopicCount statement.") stmts.todaysTopicCount, err = db.Prepare("select count(*) from topics where createdAt BETWEEN (utc_timestamp() - interval 1 day) and utc_timestamp()") if err != nil { return err } - log.Print("Preparing todays_report_count statement.") + log.Print("Preparing todaysReportCount statement.") stmts.todaysReportCount, err = db.Prepare("select count(*) from topics where createdAt BETWEEN (utc_timestamp() - interval 1 day) and utc_timestamp() and parentID = 1") if err != nil { return err } - log.Print("Preparing todays_newuser_count statement.") + log.Print("Preparing todaysNewUserCount statement.") stmts.todaysNewUserCount, err = db.Prepare("select count(*) from users where createdAt BETWEEN (utc_timestamp() - interval 1 day) and utc_timestamp()") if err != nil { return err } - log.Print("Preparing find_users_by_ip_users statement.") - stmts.findUsersByIPUsers, err = db.Prepare("select uid from users where last_ip = ?") - if err != nil { - return err - } - - log.Print("Preparing find_users_by_ip_topics statement.") - stmts.findUsersByIPTopics, err = db.Prepare("select uid from users where uid in(select createdBy from topics where ipaddress = ?)") - if err != nil { - return err - } - - log.Print("Preparing find_users_by_ip_replies statement.") - stmts.findUsersByIPReplies, err = db.Prepare("select uid from users where uid in(select createdBy from replies where ipaddress = ?)") - return err + return nil } diff --git a/plugin_guilds.go b/plugin_guilds.go index bf1d38f1..31840b26 100644 --- a/plugin_guilds.go +++ b/plugin_guilds.go @@ -22,7 +22,7 @@ func initGuilds() (err error) { common.Plugins["guilds"].AddHook("intercept_build_widgets", guilds.Widgets) common.Plugins["guilds"].AddHook("trow_assign", guilds.TrowAssign) common.Plugins["guilds"].AddHook("topic_create_pre_loop", guilds.TopicCreatePreLoop) - common.Plugins["guilds"].AddHook("pre_render_view_forum", guilds.PreRenderViewForum) + common.Plugins["guilds"].AddHook("pre_render_forum", guilds.PreRenderViewForum) common.Plugins["guilds"].AddHook("simple_forum_check_pre_perms", guilds.ForumCheck) common.Plugins["guilds"].AddHook("forum_check_pre_perms", guilds.ForumCheck) // TODO: Auto-grant this perm to admins upon installation? @@ -61,7 +61,7 @@ func deactivateGuilds() { common.Plugins["guilds"].RemoveHook("intercept_build_widgets", guilds.Widgets) common.Plugins["guilds"].RemoveHook("trow_assign", guilds.TrowAssign) common.Plugins["guilds"].RemoveHook("topic_create_pre_loop", guilds.TopicCreatePreLoop) - common.Plugins["guilds"].RemoveHook("pre_render_view_forum", guilds.PreRenderViewForum) + common.Plugins["guilds"].RemoveHook("pre_render_forum", guilds.PreRenderViewForum) common.Plugins["guilds"].RemoveHook("simple_forum_check_pre_perms", guilds.ForumCheck) common.Plugins["guilds"].RemoveHook("forum_check_pre_perms", guilds.ForumCheck) common.DeregisterPluginPerm("CreateGuild") diff --git a/query_gen/lib/acc_builders.go b/query_gen/lib/acc_builders.go index b5e1a367..10aa5703 100644 --- a/query_gen/lib/acc_builders.go +++ b/query_gen/lib/acc_builders.go @@ -47,6 +47,8 @@ type accSelectBuilder struct { orderby string limit string dateCutoff *dateCutoff // We might want to do this in a slightly less hacky way + inChain *accSelectBuilder + inColumn string build *Accumulator } @@ -61,6 +63,12 @@ func (selectItem *accSelectBuilder) Where(where string) *accSelectBuilder { return selectItem } +func (selectItem *accSelectBuilder) InQ(column string, subBuilder *accSelectBuilder) *accSelectBuilder { + selectItem.inChain = subBuilder + selectItem.inColumn = column + return selectItem +} + func (selectItem *accSelectBuilder) DateCutoff(column string, quantity int, unit string) *accSelectBuilder { selectItem.dateCutoff = &dateCutoff{column, quantity, unit} return selectItem @@ -78,7 +86,7 @@ func (selectItem *accSelectBuilder) Limit(limit string) *accSelectBuilder { func (selectItem *accSelectBuilder) Prepare() *sql.Stmt { // TODO: Phase out the procedural API and use the adapter's OO API? The OO API might need a bit more work before we do that and it needs to be rolled out to MSSQL. - if selectItem.dateCutoff != nil { + if selectItem.dateCutoff != nil || selectItem.inChain != nil { selectBuilder := selectItem.build.GetAdapter().Builder().Select().FromAcc(selectItem) return selectItem.build.prepare(selectItem.build.GetAdapter().ComplexSelect(selectBuilder)) } diff --git a/query_gen/lib/accumulator.go b/query_gen/lib/accumulator.go index 2c37858f..9479d8b2 100644 --- a/query_gen/lib/accumulator.go +++ b/query_gen/lib/accumulator.go @@ -198,7 +198,7 @@ func (build *Accumulator) Update(table string) *accUpdateBuilder { } func (build *Accumulator) Select(table string) *accSelectBuilder { - return &accSelectBuilder{table, "", "", "", "", nil, build} + return &accSelectBuilder{table, "", "", "", "", nil, nil, "", build} } func (build *Accumulator) Insert(table string) *accInsertBuilder { diff --git a/query_gen/lib/micro_builders.go b/query_gen/lib/micro_builders.go index 530467ca..50640253 100644 --- a/query_gen/lib/micro_builders.go +++ b/query_gen/lib/micro_builders.go @@ -12,7 +12,7 @@ type prebuilder struct { func (build *prebuilder) Select(nlist ...string) *selectPrebuilder { name := optString(nlist, "_builder") - return &selectPrebuilder{name, "", "", "", "", "", nil, build.adapter} + return &selectPrebuilder{name, "", "", "", "", "", nil, nil, "", build.adapter} } func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder { @@ -96,6 +96,8 @@ type selectPrebuilder struct { orderby string limit string dateCutoff *dateCutoff + inChain *selectPrebuilder + inColumn string // for inChain build Adapter } @@ -115,6 +117,11 @@ func (selectItem *selectPrebuilder) Where(where string) *selectPrebuilder { return selectItem } +func (selectItem *selectPrebuilder) InQ(subBuilder *selectPrebuilder) *selectPrebuilder { + selectItem.inChain = subBuilder + return selectItem +} + func (selectItem *selectPrebuilder) Orderby(orderby string) *selectPrebuilder { selectItem.orderby = orderby return selectItem @@ -130,9 +137,14 @@ func (selectItem *selectPrebuilder) FromAcc(accBuilder *accSelectBuilder) *selec selectItem.table = accBuilder.table selectItem.columns = accBuilder.columns selectItem.where = accBuilder.where - selectItem.dateCutoff = accBuilder.dateCutoff selectItem.orderby = accBuilder.orderby selectItem.limit = accBuilder.limit + + selectItem.dateCutoff = accBuilder.dateCutoff + if accBuilder.inChain != nil { + selectItem.inChain = &selectPrebuilder{"__builder", accBuilder.inChain.table, accBuilder.inChain.columns, accBuilder.inChain.where, accBuilder.inChain.orderby, accBuilder.inChain.limit, accBuilder.inChain.dateCutoff, nil, "", selectItem.build} + selectItem.inColumn = accBuilder.inColumn + } return selectItem } diff --git a/query_gen/lib/mssql.go b/query_gen/lib/mssql.go index 9c151295..1ad29a0c 100644 --- a/query_gen/lib/mssql.go +++ b/query_gen/lib/mssql.go @@ -1114,9 +1114,6 @@ type Stmts struct { todaysTopicCount *sql.Stmt todaysReportCount *sql.Stmt todaysNewUserCount *sql.Stmt - findUsersByIPUsers *sql.Stmt - findUsersByIPTopics *sql.Stmt - findUsersByIPReplies *sql.Stmt Mocks bool } diff --git a/query_gen/lib/mysql.go b/query_gen/lib/mysql.go index 257269e6..70213def 100644 --- a/query_gen/lib/mysql.go +++ b/query_gen/lib/mysql.go @@ -2,9 +2,11 @@ package qgen //import "fmt" -import "strings" -import "strconv" -import "errors" +import ( + "errors" + "strconv" + "strings" +) func init() { Registry = append(Registry, @@ -399,7 +401,7 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str return querystr, nil } -func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (string, error) { +func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (out string, err error) { if preBuilder.name == "" { return "", errors.New("You need a name for this statement") } @@ -418,9 +420,19 @@ func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (string } querystr = querystr[0 : len(querystr)-1] - whereStr, err := adapter.buildFlexiWhere(preBuilder.where, preBuilder.dateCutoff) - if err != nil { - return querystr, err + var whereStr string + // TODO: Let callers have a Where() and a InQ() + if preBuilder.inChain != nil { + whereStr, err = adapter.ComplexSelect(preBuilder.inChain) + if err != nil { + return querystr, err + } + whereStr = " WHERE `" + preBuilder.inColumn + "` IN(" + whereStr + ")" + } else { + whereStr, err = adapter.buildFlexiWhere(preBuilder.where, preBuilder.dateCutoff) + if err != nil { + return querystr, err + } } querystr += " FROM `" + preBuilder.table + "`" + whereStr + adapter.buildOrderby(preBuilder.orderby) + adapter.buildLimit(preBuilder.limit) @@ -664,9 +676,6 @@ type Stmts struct { todaysTopicCount *sql.Stmt todaysReportCount *sql.Stmt todaysNewUserCount *sql.Stmt - findUsersByIPUsers *sql.Stmt - findUsersByIPTopics *sql.Stmt - findUsersByIPReplies *sql.Stmt Mocks bool } diff --git a/query_gen/lib/pgsql.go b/query_gen/lib/pgsql.go index 8403e3e9..799186ba 100644 --- a/query_gen/lib/pgsql.go +++ b/query_gen/lib/pgsql.go @@ -375,9 +375,6 @@ type Stmts struct { todaysTopicCount *sql.Stmt todaysReportCount *sql.Stmt todaysNewUserCount *sql.Stmt - findUsersByIPUsers *sql.Stmt - findUsersByIPTopics *sql.Stmt - findUsersByIPReplies *sql.Stmt Mocks bool } diff --git a/router_gen/routes.go b/router_gen/routes.go index 7a3a76da..49149be2 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -20,7 +20,7 @@ func routes() { topicGroup := newRouteGroup("/topics/", View("routeTopics", "/topics/"), - MemberView("routeTopicCreate", "/topics/create/", "extraData"), + MemberView("routes.CreateTopic", "/topics/create/", "extraData"), ) addRouteGroup(topicGroup) @@ -53,10 +53,10 @@ func buildUserRoutes() { // TODO: Auto test and manual test these routes userGroup = newRouteGroup("/users/") userGroup.Routes( - Action("routeBanSubmit", "/users/ban/submit/", "extraData"), - Action("routeUnban", "/users/unban/", "extraData"), - Action("routeActivate", "/users/activate/", "extraData"), - MemberView("routeIps", "/users/ips/"), // TODO: .Perms("ViewIPs")? + Action("routes.BanUserSubmit", "/users/ban/submit/", "extraData"), + Action("routes.UnbanUser", "/users/unban/", "extraData"), + Action("routes.ActivateUser", "/users/activate/", "extraData"), + MemberView("routes.IPSearch", "/users/ips/"), // TODO: .Perms("ViewIPs")? ) addRouteGroup(userGroup) } @@ -65,7 +65,7 @@ func buildTopicRoutes() { topicGroup := newRouteGroup("/topic/") topicGroup.Routes( View("routeTopicID", "/topic/", "extraData"), - Action("routeTopicCreateSubmit", "/topic/create/submit/"), + Action("routeCreateTopicSubmit", "/topic/create/submit/"), Action("routes.EditTopicSubmit", "/topic/edit/submit/", "extraData"), Action("routes.DeleteTopicSubmit", "/topic/delete/submit/").LitBefore("req.URL.Path += extraData"), Action("routes.StickTopicSubmit", "/topic/stick/submit/", "extraData"), diff --git a/routes.go b/routes.go index 355783ee..9ad89a8d 100644 --- a/routes.go +++ b/routes.go @@ -291,18 +291,8 @@ func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid s } headerVars.Zone = "view_forum" - // Calculate the offset - var offset int - // TODO: Does forum.TopicCount take the deleted items into consideration for guests? - lastPage := (forum.TopicCount / common.Config.ItemsPerPage) + 1 - if page > 1 { - offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage - } else if page == -1 { - page = lastPage - offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage - } else { - page = 1 - } + // TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete + offset, page, lastPage := common.PageOffset(forum.TopicCount, page, common.Config.ItemsPerPage) // TODO: Move this to *Forum rows, err := stmts.getForumTopicsOffset.Query(fid, offset, common.Config.ItemsPerPage) @@ -358,8 +348,8 @@ func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid s } pi := common.ForumPage{forum.Name, user, headerVars, topicList, forum, page, lastPage} - if common.PreRenderHooks["pre_render_view_forum"] != nil { - if common.RunPreRenderHook("pre_render_view_forum", w, r, &user, &pi) { + if common.PreRenderHooks["pre_render_forum"] != nil { + if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) { return nil } } @@ -430,10 +420,9 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo 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 - page, _ = strconv.Atoi(r.FormValue("page")) + page, _ := strconv.Atoi(r.FormValue("page")) // SEO URLs... // TODO: Make a shared function for this @@ -496,16 +485,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User, urlB } // Calculate the offset - lastPage := (topic.PostCount / common.Config.ItemsPerPage) + 1 - if page > 1 { - offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage - } else if page == -1 { - page = lastPage - offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage - } else { - page = 1 - } - + offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) tpage := common.TopicPage{topic.Title, user, headerVars, replyList, topic, page, lastPage} // Get the replies.. @@ -918,7 +898,6 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R if err != nil { return common.PreErrorJS("Invalid asid", w, r) } - _, err = stmts.deleteActivityStreamMatch.Exec(user.ID, asid) if err != nil { return common.InternalError(err, w, r) @@ -957,7 +936,6 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R } msglist += res + "," } - err = rows.Err() if err != nil { return common.InternalErrorJS(err, w, r) diff --git a/routes/moderate.go b/routes/moderate.go new file mode 100644 index 00000000..db850b86 --- /dev/null +++ b/routes/moderate.go @@ -0,0 +1,44 @@ +package routes + +import ( + "html" + "net/http" + + "../common" +) + +func IPSearch(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + headerVars, ferr := common.UserCheck(w, r, &user) + if ferr != nil { + return ferr + } + // TODO: How should we handle the permissions if we extend this into an alt detector of sorts? + if !user.Perms.ViewIPs { + return common.NoPermissions(w, r, user) + } + + // TODO: Reject IP Addresses with illegal characters + var ip = html.EscapeString(r.FormValue("ip")) + uids, err := common.IPSearch.Lookup(ip) + if err != nil { + return common.InternalError(err, w, r) + } + + // TODO: What if a user is deleted via the Control Panel? We'll cross that bridge when we come to it, although we might lean towards blanking the account and removing the related data rather than purging it + userList, err := common.Users.BulkGetMap(uids) + if err != nil { + return common.InternalError(err, w, r) + } + + pi := common.IPSearchPage{common.GetTitlePhrase("ip-search"), user, headerVars, userList, ip} + if common.PreRenderHooks["pre_render_ip_search"] != nil { + if common.RunPreRenderHook("pre_render_ip_search", w, r, &user, &pi) { + return nil + } + } + err = common.Templates.ExecuteTemplate(w, "ip-search.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} diff --git a/routes/panel/filler.txt b/routes/panel/filler.txt new file mode 100644 index 00000000..20e14b1e --- /dev/null +++ b/routes/panel/filler.txt @@ -0,0 +1 @@ +This file is here so that Git will include this folder in the repository. \ No newline at end of file diff --git a/routes/topic.go b/routes/topic.go index 723eb5c0..8de77a0b 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -12,6 +12,93 @@ import ( var successJSONBytes = []byte(`{"success":"1"}`) +// ? - 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 +// ? - Log username changes and put restrictions on this? +func CreateTopic(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { + var fid int + var err error + if sfid != "" { + fid, err = strconv.Atoi(sfid) + if err != nil { + return common.LocalError("You didn't provide a valid number for the forum ID.", w, r, user) + } + } + if fid == 0 { + fid = common.Config.DefaultForum + } + + headerVars, ferr := common.ForumUserCheck(w, r, &user, fid) + if ferr != nil { + return ferr + } + if !user.Perms.ViewTopic || !user.Perms.CreateTopic { + return common.NoPermissions(w, r, user) + } + headerVars.Zone = "create_topic" + + // Lock this to the forum being linked? + // Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile) + var strictmode bool + if common.Vhooks["topic_create_pre_loop"] != nil { + common.RunVhook("topic_create_pre_loop", w, r, fid, &headerVars, &user, &strictmode) + } + + // TODO: Re-add support for plugin_guilds + var forumList []common.Forum + var canSee []int + if user.IsSuperAdmin { + canSee, err = common.Forums.GetAllVisibleIDs() + if err != nil { + return common.InternalError(err, w, r) + } + } else { + group, err := common.Groups.Get(user.Group) + if err != nil { + // TODO: Refactor this + common.LocalError("Something weird happened behind the scenes", w, r, user) + log.Printf("Group #%d doesn't exist, but it's set on common.User #%d", user.Group, user.ID) + return nil + } + canSee = group.CanSee + } + + // TODO: plugin_superadmin needs to be able to override this loop. Skip flag on topic_create_pre_loop? + for _, ffid := range canSee { + // TODO: Surely, there's a better way of doing this. I've added it in for now to support plugin_guilds, but we really need to clean this up + if strictmode && ffid != fid { + continue + } + + // Do a bulk forum fetch, just in case it's the SqlForumStore? + forum := common.Forums.DirtyGet(ffid) + if forum.Name != "" && forum.Active { + fcopy := forum.Copy() + if common.Hooks["topic_create_frow_assign"] != nil { + // TODO: Add the skip feature to all the other row based hooks? + if common.RunHook("topic_create_frow_assign", &fcopy).(bool) { + continue + } + } + forumList = append(forumList, fcopy) + } + } + + ctpage := common.CreateTopicPage{"Create Topic", user, headerVars, forumList, fid} + if common.PreRenderHooks["pre_render_create_topic"] != nil { + if common.RunPreRenderHook("pre_render_create_topic", w, r, &user, &ctpage) { + return nil + } + } + + err = common.RunThemeTemplate(headerVars.Theme.Name, "create_topic", ctpage, w) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} + // 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 func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { @@ -149,11 +236,7 @@ func StickTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, return common.InternalError(err, w, r) } - err = common.ModLogs.Create("stick", tid, "topic", user.LastIP, user.ID) - if err != nil { - return common.InternalError(err, w, r) - } - err = topic.CreateActionReply("stick", user.LastIP, user) + err = addTopicAction("stick", topic, user) if err != nil { return common.InternalError(err, w, r) } @@ -188,11 +271,7 @@ func UnstickTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User return common.InternalError(err, w, r) } - err = common.ModLogs.Create("unstick", tid, "topic", user.LastIP, user.ID) - if err != nil { - return common.InternalError(err, w, r) - } - err = topic.CreateActionReply("unstick", user.LastIP, user) + err = addTopicAction("unstick", topic, user) if err != nil { return common.InternalError(err, w, r) } @@ -247,11 +326,7 @@ func LockTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User) c return common.InternalErrorJSQ(err, w, r, isJs) } - err = common.ModLogs.Create("lock", tid, "topic", user.LastIP, user.ID) - if err != nil { - return common.InternalErrorJSQ(err, w, r, isJs) - } - err = topic.CreateActionReply("lock", user.LastIP, user) + err = addTopicAction("lock", topic, user) if err != nil { return common.InternalErrorJSQ(err, w, r, isJs) } @@ -290,11 +365,7 @@ func UnlockTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, return common.InternalError(err, w, r) } - err = common.ModLogs.Create("unlock", tid, "topic", user.LastIP, user.ID) - if err != nil { - return common.InternalError(err, w, r) - } - err = topic.CreateActionReply("unlock", user.LastIP, user) + err = addTopicAction("unlock", topic, user) if err != nil { return common.InternalError(err, w, r) } @@ -354,11 +425,7 @@ func MoveTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, s } // TODO: Log more data so we can list the destination forum in the action post? - 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) + err = addTopicAction("move", topic, user) if err != nil { return common.InternalErrorJS(err, w, r) } @@ -369,3 +436,11 @@ func MoveTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, s } return nil } + +func addTopicAction(action string, topic *common.Topic, user common.User) error { + err := common.ModLogs.Create(action, topic.ID, "topic", user.LastIP, user.ID) + if err != nil { + return err + } + return topic.CreateActionReply(action, user.LastIP, user) +} diff --git a/mod_routes.go b/routes/user.go similarity index 62% rename from mod_routes.go rename to routes/user.go index 9d5b9b00..845f3242 100644 --- a/mod_routes.go +++ b/routes/user.go @@ -1,108 +1,15 @@ -package main +package routes import ( + "database/sql" "net/http" "strconv" "time" - "./common" + "../common" ) -func routeIps(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.Perms.ViewIPs { - return common.NoPermissions(w, r, user) - } - - var ip = r.FormValue("ip") - var uid int - var reqUserList = make(map[int]bool) - - rows, err := stmts.findUsersByIPUsers.Query(ip) - if err != nil { - return common.InternalError(err, w, r) - } - defer rows.Close() - - for rows.Next() { - err := rows.Scan(&uid) - if err != nil { - return common.InternalError(err, w, r) - } - reqUserList[uid] = true - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - rows2, err := stmts.findUsersByIPTopics.Query(ip) - if err != nil { - return common.InternalError(err, w, r) - } - defer rows2.Close() - - for rows2.Next() { - err := rows2.Scan(&uid) - if err != nil { - return common.InternalError(err, w, r) - } - reqUserList[uid] = true - } - err = rows2.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - rows3, err := stmts.findUsersByIPReplies.Query(ip) - if err != nil { - return common.InternalError(err, w, r) - } - defer rows3.Close() - - for rows3.Next() { - err := rows3.Scan(&uid) - if err != nil { - return common.InternalError(err, w, r) - } - reqUserList[uid] = true - } - err = rows3.Err() - if err != nil { - return common.InternalError(err, w, r) - } - - // Convert the user ID map to a slice, then bulk load the users - var idSlice = make([]int, len(reqUserList)) - var i int - for userID := range reqUserList { - idSlice[i] = userID - i++ - } - - // TODO: What if a user is deleted via the Control Panel? - userList, err := common.Users.BulkGetMap(idSlice) - if err != nil { - return common.InternalError(err, w, r) - } - - pi := common.IPSearchPage{common.GetTitlePhrase("ip-search"), user, headerVars, userList, ip} - if common.PreRenderHooks["pre_render_ips"] != nil { - if common.RunPreRenderHook("pre_render_ips", w, r, &user, &pi) { - return nil - } - } - err = common.Templates.ExecuteTemplate(w, "ip-search.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil -} - -func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError { +func BanUserSubmit(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError { if !user.Perms.BanUsers { return common.NoPermissions(w, r, user) } @@ -116,7 +23,7 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User, su } targetUser, err := common.Users.Get(uid) - if err == ErrNoRows { + if err == sql.ErrNoRows { return common.LocalError("The user you're trying to ban no longer exists.", w, r, user) } else if err != nil { return common.InternalError(err, w, r) @@ -160,7 +67,7 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User, su } err = targetUser.Ban(duration, user.ID) - if err == ErrNoRows { + if err == sql.ErrNoRows { return common.LocalError("The user you're trying to ban no longer exists.", w, r, user) } else if err != nil { return common.InternalError(err, w, r) @@ -175,7 +82,7 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User, su return nil } -func routeUnban(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError { +func UnbanUser(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError { if !user.Perms.BanUsers { return common.NoPermissions(w, r, user) } @@ -186,7 +93,7 @@ func routeUnban(w http.ResponseWriter, r *http.Request, user common.User, suid s } targetUser, err := common.Users.Get(uid) - if err == ErrNoRows { + if err == sql.ErrNoRows { return common.LocalError("The user you're trying to unban no longer exists.", w, r, user) } else if err != nil { return common.InternalError(err, w, r) @@ -199,7 +106,7 @@ func routeUnban(w http.ResponseWriter, r *http.Request, user common.User, suid s err = targetUser.Unban() if err == common.ErrNoTempGroup { return common.LocalError("The user you're trying to unban is not banned", w, r, user) - } else if err == ErrNoRows { + } else if err == sql.ErrNoRows { return common.LocalError("The user you're trying to unban no longer exists.", w, r, user) } else if err != nil { return common.InternalError(err, w, r) @@ -214,7 +121,7 @@ func routeUnban(w http.ResponseWriter, r *http.Request, user common.User, suid s return nil } -func routeActivate(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError { +func ActivateUser(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError { if !user.Perms.ActivateUsers { return common.NoPermissions(w, r, user) } @@ -225,7 +132,7 @@ func routeActivate(w http.ResponseWriter, r *http.Request, user common.User, sui } targetUser, err := common.Users.Get(uid) - if err == ErrNoRows { + if err == sql.ErrNoRows { return common.LocalError("The account you're trying to activate no longer exists.", w, r, user) } else if err != nil { return common.InternalError(err, w, r)