From 3e4cfa88881203b373a68f7f026cc2f7624e8d45 Mon Sep 17 00:00:00 2001 From: Azareal Date: Tue, 15 Aug 2017 14:47:56 +0100 Subject: [PATCH] Added the Panel Debug page. Added the mini stats for the various parts of the Control Panel in the Control Panel Menu. Fixed a crash bug in the router. The basic arithmetic template functions now work for the interpreted templates. Tweaked the connection pool to hopefully ease the number of goroutines fighting over a database connection. The Group Manager is now paginated. panel.css is now pushed by HTTP/2 Push. Fixed an issue with the padding on /forum/ and /topics/ for Shadow. Fixed a bug in the login system forcefully overriding sessions on login. All admin logins are now logged. Refactored the client-side alert loading logic. Added SimpleCount to the Query Builder. --- database.go | 1 + errors.go | 3 + forum_store.go | 19 +++++++ gen_mysql.go | 7 +++ gen_router.go | 5 +- install/install.go | 1 + main.go | 61 +++++++++++++++++--- mysql.go | 9 +++ pages.go | 99 +++++++++++++++++++++++++++++++-- panel_routes.go | 99 ++++++++++++++++++++++----------- pgsql.go | 9 +++ public/global.js | 60 +++++++++++--------- query_gen/lib/builder.go | 15 ++++- query_gen/main.go | 2 + router.go | 2 +- router_gen/main.go | 2 +- router_gen/routes.go | 1 + routes.go | 23 ++++++++ site.go | 1 + templates/panel-debug.html | 17 ++++++ templates/panel-groups.html | 9 +++ templates/panel-inner-menu.html | 40 ++++++++++--- themes/shadow/public/main.css | 20 +++++++ user.go | 21 +++++-- user_store.go | 47 ++++++++++++++-- 25 files changed, 480 insertions(+), 93 deletions(-) create mode 100644 templates/panel-debug.html diff --git a/database.go b/database.go index 61617efc..ea501d9a 100644 --- a/database.go +++ b/database.go @@ -6,6 +6,7 @@ import "database/sql" var db *sql.DB var db_version string +var db_adapter string var ErrNoRows = sql.ErrNoRows diff --git a/errors.go b/errors.go index f2e5a335..b4b2e302 100644 --- a/errors.go +++ b/errors.go @@ -6,6 +6,9 @@ import "bytes" import "net/http" import "runtime/debug" +//var notfound_count_per_second int +//var noperms_count_per_second int + var error_internal []byte var error_notfound []byte func init_errors() error { diff --git a/forum_store.go b/forum_store.go index 7fcbe517..e192c96e 100644 --- a/forum_store.go +++ b/forum_store.go @@ -35,6 +35,8 @@ type ForumStore interface //GetChildren(parentID int, parentType string) ([]*Forum,error) //GetFirstChild(parentID int, parentType string) (*Forum,error) CreateForum(forum_name string, forum_desc string, active bool, preset string) (int, error) + + GetGlobalCount() int } type StaticForumStore struct @@ -45,6 +47,7 @@ type StaticForumStore struct get *sql.Stmt get_all *sql.Stmt + forum_count *sql.Stmt } func NewStaticForumStore() *StaticForumStore { @@ -56,9 +59,14 @@ func NewStaticForumStore() *StaticForumStore { if err != nil { log.Fatal(err) } + forum_count_stmt, err := qgen.Builder.SimpleCount("forums","name != ''","") + if err != nil { + log.Fatal(err) + } return &StaticForumStore{ get: get_stmt, get_all: get_all_stmt, + forum_count: forum_count_stmt, } } @@ -322,6 +330,17 @@ func (sfs *StaticForumStore) fill_forum_id_gap(biggerID int, smallerID int) { } } +// Return the total number of forums +// TO-DO: Get the total count of forums in the forum store minus the blanked forums rather than doing a heavy query for this? +func (sfs *StaticForumStore) GetGlobalCount() int { + var fcount int + err := sfs.forum_count.QueryRow().Scan(&fcount) + if err != nil { + LogError(err) + } + return fcount +} + // TO-DO: Work on MapForumStore // TO-DO: Work on SqlForumStore diff --git a/gen_mysql.go b/gen_mysql.go index f15a9931..1dd57bcb 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -108,6 +108,7 @@ var delete_topic_stmt *sql.Stmt var delete_profile_reply_stmt *sql.Stmt var delete_forum_perms_by_forum_stmt *sql.Stmt var report_exists_stmt *sql.Stmt +var group_count_stmt *sql.Stmt var add_forum_perms_to_forum_admins_stmt *sql.Stmt var add_forum_perms_to_forum_staff_stmt *sql.Stmt var add_forum_perms_to_forum_members_stmt *sql.Stmt @@ -724,6 +725,12 @@ func _gen_mysql() (err error) { return err } + log.Print("Preparing group_count statement.") + group_count_stmt, err = db.Prepare("SELECT COUNT(*) AS `count` FROM `users_groups`") + if err != nil { + return err + } + log.Print("Preparing add_forum_perms_to_forum_admins statement.") add_forum_perms_to_forum_admins_stmt, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) SELECT `gid`, ? AS `fid`, ? AS `preset`, ? AS `permissions` FROM `users_groups` WHERE `is_admin` = 1") if err != nil { diff --git a/gen_router.go b/gen_router.go index b327c95c..a257cb4f 100644 --- a/gen_router.go +++ b/gen_router.go @@ -50,7 +50,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // default_route(w,req) // return //} - if req.URL.Path[0] != '/' { + if len(req.URL.Path) == 0 || req.URL.Path[0] != '/' { w.WriteHeader(405) w.Write([]byte("")) return @@ -198,6 +198,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/panel/logs/mod/": route_panel_logs_mod(w,req,user) return + case "/panel/debug/": + route_panel_debug(w,req,user) + return default: route_panel(w,req,user) return diff --git a/install/install.go b/install/install.go index a0ee4972..56dec0a8 100644 --- a/install/install.go +++ b/install/install.go @@ -145,6 +145,7 @@ site.Url = "` + site_url + `" site.Port = "` + server_port + `" site.EnableSsl = false site.EnableEmails = false +site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle config.SslPrivkey = "" config.SslFullchain = "" diff --git a/main.go b/main.go index 2491fd61..296efdd0 100644 --- a/main.go +++ b/main.go @@ -210,15 +210,59 @@ func init_templates() { } compile_templates() - // Filler functions for now... - filler_func := func(in interface{}, in2 interface{})interface{} { - return 1 - } + // TO-DO: Add support for 64-bit integers + // TO-DO: Add support for floats fmap := make(map[string]interface{}) - fmap["add"] = filler_func - fmap["subtract"] = filler_func - fmap["multiply"] = filler_func - fmap["divide"] = filler_func + fmap["add"] = func(left interface{}, right interface{})interface{} { + var left_int int + var right_int int + switch left := left.(type) { + case uint, uint8, uint16, int, int32: left_int = left.(int) + } + switch right := right.(type) { + case uint, uint8, uint16, int, int32: right_int = right.(int) + } + return left_int + right_int + } + + fmap["subtract"] = func(left interface{}, right interface{})interface{} { + var left_int int + var right_int int + switch left := left.(type) { + case uint, uint8, uint16, int, int32: left_int = left.(int) + } + switch right := right.(type) { + case uint, uint8, uint16, int, int32: right_int = right.(int) + } + return left_int - right_int + } + + fmap["multiply"] = func(left interface{}, right interface{})interface{} { + var left_int int + var right_int int + switch left := left.(type) { + case uint, uint8, uint16, int, int32: left_int = left.(int) + } + switch right := right.(type) { + case uint, uint8, uint16, int, int32: right_int = right.(int) + } + return left_int * right_int + } + + fmap["divide"] = func(left interface{}, right interface{})interface{} { + var left_int int + var right_int int + switch left := left.(type) { + case uint, uint8, uint16, int, int32: left_int = left.(int) + } + switch right := right.(type) { + case uint, uint8, uint16, int, int32: right_int = right.(int) + } + if left_int == 0 || right_int == 0 { + return 0 + } + return left_int / right_int + } // The interpreted templates... if dev.DebugMode { @@ -377,6 +421,7 @@ func main(){ ///router.HandleFunc("/panel/groups/edit/perms/submit/", route_panel_groups_edit_perms_submit) ///router.HandleFunc("/panel/groups/create/", route_panel_groups_create_submit) ///router.HandleFunc("/panel/logs/mod/", route_panel_logs_mod) + ///router.HandleFunc("/panel/debug/", route_panel_debug) ///router.HandleFunc("/api/", route_api) //router.HandleFunc("/exit/", route_exit) diff --git a/mysql.go b/mysql.go index ce502aa7..37efc475 100644 --- a/mysql.go +++ b/mysql.go @@ -4,6 +4,7 @@ package main import "log" +//import "time" import "database/sql" import _ "github.com/go-sql-driver/mysql" import "./query_gen/lib" @@ -16,6 +17,10 @@ var todays_topic_count_stmt *sql.Stmt var todays_report_count_stmt *sql.Stmt var todays_newuser_count_stmt *sql.Stmt +func init() { + db_adapter = "mysql" +} + func _init_database() (err error) { var _dbpassword string if(db_config.Password != ""){ @@ -39,6 +44,10 @@ func _init_database() (err error) { // Set the number of max open connections db.SetMaxOpenConns(64) + db.SetMaxIdleConns(32) + + // Only hold connections open for five seconds to avoid accumulating a large number of stale connections + //db.SetConnMaxLifetime(5 * time.Second) // Build the generated prepared statements, we are going to slowly move the queries over to the query generator rather than writing them all by hand, this'll make it easier for us to implement database adapters for other databases like PostgreSQL, MSSQL, SQlite, etc. err = _gen_mysql() diff --git a/pages.go b/pages.go index 4d06a2e9..ddc6dec9 100644 --- a/pages.go +++ b/pages.go @@ -106,6 +106,39 @@ type CreateTopicPage struct ExtData ExtData } +type PanelStats struct +{ + Users int + Groups int + Forums int + Settings int + Themes int + Reports int +} + +type PanelPage struct +{ + Title string + CurrentUser User + Header HeaderVars + Stats PanelStats + ItemList []interface{} + Something interface{} +} + +type PanelGroupPage struct +{ + Title string + CurrentUser User + Header HeaderVars + Stats PanelStats + ItemList []GroupAdmin + PageList []int + Page int + LastPage int + ExtData ExtData +} + type GridElement struct { ID string @@ -122,25 +155,28 @@ type PanelDashboardPage struct Title string CurrentUser User Header HeaderVars + Stats PanelStats GridItems []GridElement ExtData ExtData } -type ThemesPage struct +type PanelThemesPage struct { Title string CurrentUser User Header HeaderVars + Stats PanelStats PrimaryThemes []Theme VariantThemes []Theme ExtData ExtData } -type EditGroupPage struct +type PanelEditGroupPage struct { Title string CurrentUser User Header HeaderVars + Stats PanelStats ID int Name string Tag string @@ -155,11 +191,12 @@ type GroupForumPermPreset struct Preset string } -type EditForumPage struct +type PanelEditForumPage struct { Title string CurrentUser User Header HeaderVars + Stats PanelStats ID int Name string Desc string @@ -182,11 +219,12 @@ type NameLangToggle struct Toggle bool } -type EditGroupPermsPage struct +type PanelEditGroupPermsPage struct { Title string CurrentUser User Header HeaderVars + Stats PanelStats ID int Name string LocalPerms []NameLangToggle @@ -200,15 +238,28 @@ type Log struct { DoneAt string } -type LogsPage struct +type PanelLogsPage struct { Title string CurrentUser User Header HeaderVars + Stats PanelStats Logs []Log ExtData ExtData } +type PanelDebugPage struct +{ + Title string + CurrentUser User + Header HeaderVars + Stats PanelStats + Uptime string + OpenConns int + DBAdapter string + ExtData ExtData +} + type PageSimple struct { Title string @@ -747,3 +798,41 @@ func coerce_int_bytes(data []byte) (res int, length int) { } return conv, i } + +// TO-DO: Write tests for this +func paginate(count int, per_page int, maxPages int) []int { + if count < per_page { + return []int{1} + } + + var page int + var out []int + for current := 0; current < count; current += per_page { + page++ + out = append(out,page) + if len(out) >= maxPages { + break + } + } + return out +} + +// TO-DO: Write tests for this +func page_offset(count int, page int, perPage int) (int, int, int) { + var offset int + lastPage := int(count / perPage) + 1 + if page > 1 { + offset = (perPage * page) - perPage + } else if page == -1 { + page = lastPage + offset = (perPage * page) - perPage + } else { + page = 1 + } + + // We don't want the offset to overflow the slices, if everything's in memory + if offset >= (count - 1) { + offset = 0 + } + return offset, page, lastPage +} diff --git a/panel_routes.go b/panel_routes.go index 3dfd04f7..fd9a719a 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -15,7 +15,7 @@ import ( ) func route_panel(w http.ResponseWriter, r *http.Request, user User){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -168,7 +168,7 @@ func route_panel(w http.ResponseWriter, r *http.Request, user User){ gridElements = append(gridElements, GridElement{"dash-visitorsperweek","2 visitors / week",13,"grid_stat stat_disabled","","","Coming Soon!"/*"The number of unique visitors we've had over the last 7 days"*/}) gridElements = append(gridElements, GridElement{"dash-postsperuser","5 posts / user / week",14,"grid_stat stat_disabled","","","Coming Soon!"/*"The average number of posts made by each active user over the past week"*/}) - pi := PanelDashboardPage{"Control Panel Dashboard",user,headerVars,gridElements,extData} + pi := PanelDashboardPage{"Control Panel Dashboard",user,headerVars,stats,gridElements,extData} if pre_render_hooks["pre_render_panel_dashboard"] != nil { if run_pre_render_hook("pre_render_panel_dashboard", w, r, &user, &pi) { return @@ -181,7 +181,7 @@ func route_panel(w http.ResponseWriter, r *http.Request, user User){ } func route_panel_forums(w http.ResponseWriter, r *http.Request, user User){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -206,7 +206,7 @@ func route_panel_forums(w http.ResponseWriter, r *http.Request, user User){ forumList = append(forumList,fadmin) } } - pi := Page{"Forum Manager",user,headerVars,forumList,nil} + pi := PanelPage{"Forum Manager",user,headerVars,stats,forumList,nil} if pre_render_hooks["pre_render_panel_forums"] != nil { if run_pre_render_hook("pre_render_panel_forums", w, r, &user, &pi) { return @@ -254,7 +254,7 @@ func route_panel_forums_create_submit(w http.ResponseWriter, r *http.Request, us } func route_panel_forums_delete(w http.ResponseWriter, r *http.Request, user User, sfid string){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -285,7 +285,7 @@ func route_panel_forums_delete(w http.ResponseWriter, r *http.Request, user User confirm_msg := "Are you sure you want to delete the '" + forum.Name + "' forum?" yousure := AreYouSure{"/panel/forums/delete/submit/" + strconv.Itoa(fid),confirm_msg} - pi := Page{"Delete Forum",user,headerVars,tList,yousure} + pi := PanelPage{"Delete Forum",user,headerVars,stats,tList,yousure} if pre_render_hooks["pre_render_panel_delete_forum"] != nil { if run_pre_render_hook("pre_render_panel_delete_forum", w, r, &user, &pi) { return @@ -330,7 +330,7 @@ func route_panel_forums_delete_submit(w http.ResponseWriter, r *http.Request, us } func route_panel_forums_edit(w http.ResponseWriter, r *http.Request, user User, sfid string) { - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -367,7 +367,7 @@ func route_panel_forums_edit(w http.ResponseWriter, r *http.Request, user User, gplist = append(gplist,GroupForumPermPreset{group,forum_perms_to_group_forum_preset(group.Forums[fid])}) } - pi := EditForumPage{"Forum Editor",user,headerVars,forum.ID,forum.Name,forum.Desc,forum.Active,forum.Preset,gplist,extData} + pi := PanelEditForumPage{"Forum Editor",user,headerVars,stats,forum.ID,forum.Name,forum.Desc,forum.Active,forum.Preset,gplist,extData} if pre_render_hooks["pre_render_panel_edit_forum"] != nil { if run_pre_render_hook("pre_render_panel_edit_forum", w, r, &user, &pi) { return @@ -548,7 +548,7 @@ func route_panel_forums_edit_perms_submit(w http.ResponseWriter, r *http.Request } func route_panel_settings(w http.ResponseWriter, r *http.Request, user User){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -597,7 +597,7 @@ func route_panel_settings(w http.ResponseWriter, r *http.Request, user User){ return } - pi := Page{"Setting Manager",user,headerVars,tList,settingList} + pi := PanelPage{"Setting Manager",user,headerVars,stats,tList,settingList} if pre_render_hooks["pre_render_panel_settings"] != nil { if run_pre_render_hook("pre_render_panel_settings", w, r, &user, &pi) { return @@ -610,7 +610,7 @@ func route_panel_settings(w http.ResponseWriter, r *http.Request, user User){ } func route_panel_setting(w http.ResponseWriter, r *http.Request, user User, sname string){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -653,7 +653,7 @@ func route_panel_setting(w http.ResponseWriter, r *http.Request, user User, snam } } - pi := Page{"Edit Setting",user,headerVars,itemList,setting} + pi := PanelPage{"Edit Setting",user,headerVars,stats,itemList,setting} if pre_render_hooks["pre_render_panel_setting"] != nil { if run_pre_render_hook("pre_render_panel_setting", w, r, &user, &pi) { return @@ -720,7 +720,7 @@ func route_panel_setting_edit(w http.ResponseWriter, r *http.Request, user User, } func route_panel_plugins(w http.ResponseWriter, r *http.Request, user User){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -736,7 +736,7 @@ func route_panel_plugins(w http.ResponseWriter, r *http.Request, user User){ pluginList = append(pluginList,plugin) } - pi := Page{"Plugin Manager",user,headerVars,pluginList,nil} + pi := PanelPage{"Plugin Manager",user,headerVars,stats,pluginList,nil} if pre_render_hooks["pre_render_panel_plugins"] != nil { if run_pre_render_hook("pre_render_panel_plugins", w, r, &user, &pi) { return @@ -926,7 +926,6 @@ func route_panel_plugins_install(w http.ResponseWriter, r *http.Request, user Us } } - if has_plugin { _, err = update_plugin_install_stmt.Exec(1,uname) if err != nil { @@ -960,7 +959,7 @@ func route_panel_plugins_install(w http.ResponseWriter, r *http.Request, user Us } func route_panel_users(w http.ResponseWriter, r *http.Request, user User){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -1004,7 +1003,7 @@ func route_panel_users(w http.ResponseWriter, r *http.Request, user User){ return } - pi := Page{"User Manager",user,headerVars,userList,nil} + pi := PanelPage{"User Manager",user,headerVars,stats,userList,nil} if pre_render_hooks["pre_render_panel_users"] != nil { if run_pre_render_hook("pre_render_panel_users", w, r, &user, &pi) { return @@ -1017,7 +1016,7 @@ func route_panel_users(w http.ResponseWriter, r *http.Request, user User){ } func route_panel_users_edit(w http.ResponseWriter, r *http.Request, user User, suid string){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -1058,7 +1057,7 @@ func route_panel_users_edit(w http.ResponseWriter, r *http.Request, user User, s groupList = append(groupList,group) } - pi := Page{"User Editor",user,headerVars,groupList,targetUser} + pi := PanelPage{"User Editor",user,headerVars,stats,groupList,targetUser} if pre_render_hooks["pre_render_panel_edit_user"] != nil { if run_pre_render_hook("pre_render_panel_edit_user", w, r, &user, &pi) { return @@ -1166,13 +1165,25 @@ func route_panel_users_edit_submit(w http.ResponseWriter, r *http.Request, user } func route_panel_groups(w http.ResponseWriter, r *http.Request, user User){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } - var groupList []interface{} - for _, group := range groups[1:] { + page, _ := strconv.Atoi(r.FormValue("page")) + perPage := 9 + offset, page, lastPage := page_offset(stats.Groups, page, perPage) + + // Skip the System group + offset++ + + var count int + var groupList []GroupAdmin + for _, group := range groups[offset:] { + if count == perPage { + break + } + var rank string var rank_class string var can_edit bool @@ -1197,10 +1208,12 @@ func route_panel_groups(w http.ResponseWriter, r *http.Request, user User){ can_edit = user.Perms.EditGroup && (!group.Is_Admin || user.Perms.EditGroupAdmin) && (!group.Is_Mod || user.Perms.EditGroupSuperMod) groupList = append(groupList, GroupAdmin{group.ID,group.Name,rank,rank_class,can_edit,can_delete}) + count++ } //log.Printf("groupList: %+v\n", groupList) - pi := Page{"Group Manager",user,headerVars,groupList,nil} + pageList := paginate(stats.Groups, perPage, 5) + pi := PanelGroupPage{"Group Manager",user,headerVars,stats,groupList,pageList,page,lastPage,extData} if pre_render_hooks["pre_render_panel_groups"] != nil { if run_pre_render_hook("pre_render_panel_groups", w, r, &user, &pi) { return @@ -1214,7 +1227,7 @@ func route_panel_groups(w http.ResponseWriter, r *http.Request, user User){ } func route_panel_groups_edit(w http.ResponseWriter, r *http.Request, user User, sgid string){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -1260,7 +1273,7 @@ func route_panel_groups_edit(w http.ResponseWriter, r *http.Request, user User, disable_rank := !user.Perms.EditGroupGlobalPerms || (group.ID == 6) - pi := EditGroupPage{"Group Editor",user,headerVars,group.ID,group.Name,group.Tag,rank,disable_rank,extData} + pi := PanelEditGroupPage{"Group Editor",user,headerVars,stats,group.ID,group.Name,group.Tag,rank,disable_rank,extData} if pre_render_hooks["pre_render_panel_edit_group"] != nil { if run_pre_render_hook("pre_render_panel_edit_group", w, r, &user, &pi) { return @@ -1273,7 +1286,7 @@ func route_panel_groups_edit(w http.ResponseWriter, r *http.Request, user User, } func route_panel_groups_edit_perms(w http.ResponseWriter, r *http.Request, user User, sgid string){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -1338,7 +1351,7 @@ func route_panel_groups_edit_perms(w http.ResponseWriter, r *http.Request, user globalPerms = append(globalPerms, NameLangToggle{"ViewAdminLogs",GetGlobalPermPhrase("ViewAdminLogs"),group.Perms.ViewAdminLogs}) globalPerms = append(globalPerms, NameLangToggle{"ViewIPs",GetGlobalPermPhrase("ViewIPs"),group.Perms.ViewIPs}) - pi := EditGroupPermsPage{"Group Editor",user,headerVars,group.ID,group.Name,localPerms,globalPerms,extData} + pi := PanelEditGroupPermsPage{"Group Editor",user,headerVars,stats,group.ID,group.Name,localPerms,globalPerms,extData} if pre_render_hooks["pre_render_panel_edit_group_perms"] != nil { if run_pre_render_hook("pre_render_panel_edit_group_perms", w, r, &user, &pi) { return @@ -1609,7 +1622,7 @@ func route_panel_groups_create_submit(w http.ResponseWriter, r *http.Request, us } func route_panel_themes(w http.ResponseWriter, r *http.Request, user User){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -1631,7 +1644,7 @@ func route_panel_themes(w http.ResponseWriter, r *http.Request, user User){ } - pi := ThemesPage{"Theme Manager",user,headerVars,pThemeList,vThemeList,extData} + pi := PanelThemesPage{"Theme Manager",user,headerVars,stats,pThemeList,vThemeList,extData} if pre_render_hooks["pre_render_panel_themes"] != nil { if run_pre_render_hook("pre_render_panel_themes", w, r, &user, &pi) { return @@ -1722,7 +1735,7 @@ func route_panel_themes_default(w http.ResponseWriter, r *http.Request, user Use } func route_panel_logs_mod(w http.ResponseWriter, r *http.Request, user User){ - headerVars, ok := PanelSessionCheck(w,r,&user) + headerVars, stats, ok := PanelSessionCheck(w,r,&user) if !ok { return } @@ -1813,7 +1826,7 @@ func route_panel_logs_mod(w http.ResponseWriter, r *http.Request, user User){ return } - pi := LogsPage{"Moderation Logs",user,headerVars,logs,extData} + pi := PanelLogsPage{"Moderation Logs",user,headerVars,stats,logs,extData} if pre_render_hooks["pre_render_panel_mod_log"] != nil { if run_pre_render_hook("pre_render_panel_mod_log", w, r, &user, &pi) { return @@ -1821,6 +1834,28 @@ func route_panel_logs_mod(w http.ResponseWriter, r *http.Request, user User){ } err = templates.ExecuteTemplate(w,"panel-modlogs.html",pi) if err != nil { - log.Print(err) + InternalError(err,w) + } +} + +func route_panel_debug(w http.ResponseWriter, r *http.Request, user User) { + headerVars, stats, ok := PanelSessionCheck(w,r,&user) + if !ok { + return + } + if !user.Is_Admin { + NoPermissions(w,r,user) + return + } + + uptime := "..." + db_stats := db.Stats() + open_conn_count := db_stats.OpenConnections + // Disk I/O? + + pi := PanelDebugPage{"Debug",user,headerVars,stats,uptime,open_conn_count,db_adapter,extData} + err := templates.ExecuteTemplate(w,"panel-debug.html",pi) + if err != nil { + InternalError(err,w) } } diff --git a/pgsql.go b/pgsql.go index 05300d9a..bb43de30 100644 --- a/pgsql.go +++ b/pgsql.go @@ -5,6 +5,7 @@ package main import "strings" +//import "time" import "database/sql" import _ "github.com/lib/pq" import "./query_gen/lib" @@ -18,6 +19,10 @@ var todays_topic_count_stmt *sql.Stmt var todays_report_count_stmt *sql.Stmt var todays_newuser_count_stmt *sql.Stmt +func init() { + db_adapter = "pgsql" +} + func _init_database() (err error) { // TO-DO: Investigate connect_timeout to see what it does exactly and whether it's relevant to us var _dbpassword string @@ -40,6 +45,10 @@ func _init_database() (err error) { // Set the number of max open connections. How many do we need? Might need to do some tests. db.SetMaxOpenConns(64) + db.SetMaxIdleConns(32) + + // Only hold connections open for five seconds to avoid accumulating a large number of stale connections + //db.SetConnMaxLifetime(5 * time.Second) err = _gen_pgsql() if err != nil { diff --git a/public/global.js b/public/global.js index 524d6851..4d0f67f3 100644 --- a/public/global.js +++ b/public/global.js @@ -13,14 +13,16 @@ function post_link(event) function load_alerts(menu_alerts) { - menu_alerts.find(".alert_counter").text(""); + var alertListNode = menu_alerts.getElementsByClassName("alertList")[0]; + var alertCounterNode = menu_alerts.getElementsByClassName("alert_counter")[0]; + alertCounterNode.textContent = ""; $.ajax({ type: 'get', dataType: 'json', url:'/api/?action=get&module=alerts&format=json', success: function(data) { if("errmsg" in data) { - menu_alerts.find(".alertList").html("
"+data.errmsg+"
"); + alertListNode.innerHTML = "
"+data.errmsg+"
"; return; } @@ -55,8 +57,13 @@ function load_alerts(menu_alerts) //menu_alerts.removeClass("hasAvatars"); //if(anyAvatar) menu_alerts.addClass("hasAvatars"); } - menu_alerts.find(".alertList").html(alist); - if(data.msgCount != 0) menu_alerts.find(".alert_counter").text(data.msgCount); + alertListNode.innerHTML = alist; + if(data.msgCount != 0) { + alertCounterNode.textContent = data.msgCount; + menu_alerts.classList.add("has_alerts"); + } else { + menu_alerts.classList.remove("has_alerts"); + } alertCount = data.msgCount; }, error: function(magic,theStatus,error) { @@ -69,36 +76,36 @@ function load_alerts(menu_alerts) console.log(magic.responseText); console.log(err); } - menu_alerts.find(".alertList").html("
"+errtxt+"
"); + alertListNode.innerHTML = "
"+errtxt+"
"; } }); } -$(document).ready(function(){ - function SplitN(data,ch,n) { - var out = []; - if(data.length == 0) return out; +function SplitN(data,ch,n) { + var out = []; + if(data.length == 0) return out; - var lastIndex = 0; - var j = 0; - var lastN = 1; - for(var i = 0; i < data.length; i++) { - if(data[i] == ch) { - out[j++] = data.substring(lastIndex,i); - lastIndex = i; - if(lastN == n) break; - lastN++; - } + var lastIndex = 0; + var j = 0; + var lastN = 1; + for(var i = 0; i < data.length; i++) { + if(data[i] == ch) { + out[j++] = data.substring(lastIndex,i); + lastIndex = i; + if(lastN == n) break; + lastN++; } - if(data.length > lastIndex) out[out.length - 1] += data.substring(lastIndex); - return out; } + if(data.length > lastIndex) out[out.length - 1] += data.substring(lastIndex); + return out; +} +$(document).ready(function(){ if(window["WebSocket"]) { if(window.location.protocol == "https:") conn = new WebSocket("wss://" + document.location.host + "/ws/"); else conn = new WebSocket("ws://" + document.location.host + "/ws/"); - + conn.onopen = function() { conn.send("page " + document.location.pathname + '\r'); } @@ -342,14 +349,15 @@ $(document).ready(function(){ } }); - $(".menu_alerts").ready(function(){ - load_alerts($(this)); - }); + var alert_menu_list = document.getElementsByClassName("menu_alerts"); + for(var i = 0; i < alert_menu_list.length; i++) { + load_alerts(alert_menu_list[i]); + } $(".menu_alerts").click(function(event) { event.stopPropagation(); if($(this).hasClass("selectedAlert")) return; - if(!conn) load_alerts($(this)); + if(!conn) load_alerts(this); this.className += " selectedAlert"; document.getElementById("back").className += " alertActive" }); diff --git a/query_gen/lib/builder.go b/query_gen/lib/builder.go index c07e7091..1a54a0d7 100644 --- a/query_gen/lib/builder.go +++ b/query_gen/lib/builder.go @@ -1,7 +1,7 @@ /* WIP Under Construction */ package qgen -//import "fmt" +//import "log" import "database/sql" var Builder *builder @@ -38,12 +38,21 @@ func (build *builder) SimpleSelect(table string, columns string, where string, o return build.conn.Prepare(res) } +func (build *builder) SimpleCount(table string, where string, limit string) (stmt *sql.Stmt, err error) { + res, err := build.adapter.SimpleCount("_builder", table, where, limit) + if err != nil { + return stmt, err + } + //log.Print("res",res) + return build.conn.Prepare(res) +} + func (build *builder) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) if err != nil { return stmt, err } - //fmt.Println("res",res) + //log.Print("res",res) return build.conn.Prepare(res) } @@ -52,7 +61,7 @@ func (build *builder) SimpleInnerJoin(table1 string, table2 string, columns stri if err != nil { return stmt, err } - //fmt.Println("res",res) + //log.Print("res",res) return build.conn.Prepare(res) } diff --git a/query_gen/main.go b/query_gen/main.go index f89c6b66..565dbb82 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -393,6 +393,8 @@ func write_deletes(adapter qgen.DB_Adapter) error { func write_simple_counts(adapter qgen.DB_Adapter) error { adapter.SimpleCount("report_exists","topics","data = ? AND data != '' AND parentID = 1","") + adapter.SimpleCount("group_count","users_groups","","") + return nil } diff --git a/router.go b/router.go index 36253a79..5d957d1e 100644 --- a/router.go +++ b/router.go @@ -31,7 +31,7 @@ func (router *Router) HandleFunc(pattern string, handle func(http.ResponseWriter } func (router *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if req.URL.Path[0] != '/' { + if len(req.URL.Path) == 0 || req.URL.Path[0] != '/' { w.WriteHeader(405) w.Write([]byte("")) return diff --git a/router_gen/main.go b/router_gen/main.go index e80ff296..b8fb93a5 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -127,7 +127,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // default_route(w,req) // return //} - if req.URL.Path[0] != '/' { + if len(req.URL.Path) == 0 || req.URL.Path[0] != '/' { w.WriteHeader(405) w.Write([]byte("")) return diff --git a/router_gen/routes.go b/router_gen/routes.go index 4c047fc6..c6974c7d 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -75,5 +75,6 @@ func routes() { Route{"route_panel_groups_create_submit","/panel/groups/create/","",[]string{}}, Route{"route_panel_logs_mod","/panel/logs/mod/","",[]string{}}, + Route{"route_panel_debug","/panel/debug/","",[]string{}}, ) } diff --git a/routes.go b/routes.go index 4f0dadd8..47cb0102 100644 --- a/routes.go +++ b/routes.go @@ -62,6 +62,9 @@ func route_static(w http.ResponseWriter, r *http.Request){ if strings.Contains(r.Header.Get("Accept-Encoding"),"gzip") { h.Set("Content-Encoding","gzip") h.Set("Content-Length", strconv.FormatInt(file.GzipLength, 10)) + if site.HasProxy { + h.Set("Vary","Accept-Encoding") + } io.Copy(w, bytes.NewReader(file.GzipData)) // Use w.Write instead? } else { h.Set("Content-Length", strconv.FormatInt(file.Length, 10)) // Avoid doing a type conversion every time? @@ -139,6 +142,7 @@ func route_custom_page(w http.ResponseWriter, r *http.Request, user User){ } } +// TO-DO: Paginate this func route_topics(w http.ResponseWriter, r *http.Request, user User){ headerVars, ok := SessionCheck(w,r,&user) if !ok { @@ -1726,6 +1730,9 @@ func route_login(w http.ResponseWriter, r *http.Request, user User) { templates.ExecuteTemplate(w,"login.html",pi) } +// TO-DO: Log failed attempted logins? +// TO-DO: Lock IPS out if they have too many failed attempts? +// TO-DO: Log unusual countries in comparison to the country a user usually logs in from? Alert the user about this? func route_login_submit(w http.ResponseWriter, r *http.Request, user User) { if user.Loggedin { LocalError("You're already logged in.",w,r,user) @@ -1743,6 +1750,13 @@ func route_login_submit(w http.ResponseWriter, r *http.Request, user User) { return } + userPtr, err := users.CascadeGet(uid) + if err != nil { + LocalError("Bad account",w,r,user) + return + } + user = *userPtr + var session string if user.Session == "" { session, err = auth.CreateSession(uid) @@ -1755,6 +1769,15 @@ func route_login_submit(w http.ResponseWriter, r *http.Request, user User) { } auth.SetCookies(w,uid,session) + if user.Is_Admin { + // Is this error check reundant? We already check for the error in PreRoute for the same IP + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + InternalError(err,w) + return + } + log.Print("#" + strconv.Itoa(uid) + " has logged in with IP " + host) + } http.Redirect(w,r,"/",http.StatusSeeOther) } diff --git a/site.go b/site.go index 56d39b04..5dc82273 100644 --- a/site.go +++ b/site.go @@ -15,6 +15,7 @@ type Site struct Port string EnableSsl bool EnableEmails bool + HasProxy bool } type DB_Config struct diff --git a/templates/panel-debug.html b/templates/panel-debug.html new file mode 100644 index 00000000..5646cc40 --- /dev/null +++ b/templates/panel-debug.html @@ -0,0 +1,17 @@ +{{template "header.html" . }} +{{template "panel-menu.html" . }} +
+
+ +
+
+
Uptime
+
Open DB Conns
+
Adapter
+ +
{{.Uptime}}
+
{{.OpenConns}}
+
{{.DBAdapter}}
+
+
+{{template "footer.html" . }} diff --git a/templates/panel-groups.html b/templates/panel-groups.html index 33ac7e88..67cc750b 100644 --- a/templates/panel-groups.html +++ b/templates/panel-groups.html @@ -18,6 +18,15 @@ {{end}} + {{if gt .LastPage 1}} +
+ {{if gt .Page 1}}{{end}} + {{range .PageList}} + + {{end}} + {{if ne .LastPage .Page}}{{end}} +
+ {{end}}
diff --git a/templates/panel-inner-menu.html b/templates/panel-inner-menu.html index 4c1a5dd6..db1459fd 100644 --- a/templates/panel-inner-menu.html +++ b/templates/panel-inner-menu.html @@ -2,12 +2,36 @@
Control Panel
- - - {{if .CurrentUser.Perms.ManageForums}}{{end}} - {{if .CurrentUser.Perms.EditSettings}}{{end}} - {{if .CurrentUser.Perms.ManageThemes}}{{end}} - {{if .CurrentUser.Perms.ManagePlugins}}{{end}} - - + + + {{if .CurrentUser.Perms.ManageForums}}{{end}} + {{if .CurrentUser.Perms.EditSettings}}{{end}} + {{if .CurrentUser.Perms.ManageThemes}}{{end}} + +
+
+ +
+
+ {{if .CurrentUser.Perms.ManagePlugins}}
+ Plugins +
{{end}} +
+ Logs +
+ {{if .CurrentUser.Is_Admin}}
+ Debug +
{{end}}
diff --git a/themes/shadow/public/main.css b/themes/shadow/public/main.css index d43af505..04a4671e 100644 --- a/themes/shadow/public/main.css +++ b/themes/shadow/public/main.css @@ -330,6 +330,23 @@ textarea.large { font-size: 13px; padding: 10px; } +.menu_stats { + font-size: 12px; +} + +/* Mini paginators aka panel paginators */ +.pageset { + margin-top: 4px; + clear: both; + height: 32px; +} +.pageitem { + background-color: rgb(61,61,61); + padding: 10px; + margin-right: 4px; + font-size: 13px; + float: left; +} .rowlist.bgavatars .rowitem { background-repeat: no-repeat; @@ -429,6 +446,9 @@ input, select, textarea { margin-left: 8px; width: 108px; } +.topic_list .rowitem:last-child { + margin-bottom: 10px; +} /* Profiles */ #profile_left_lane { diff --git a/user.go b/user.go index ab34f917..3c44106f 100644 --- a/user.go +++ b/user.go @@ -14,7 +14,7 @@ import ( var guest_user User = User{ID:0,Link:"#",Group:6,Perms:GuestPerms} var PreRoute func(http.ResponseWriter, *http.Request) (User,bool) = _pre_route -var PanelSessionCheck func(http.ResponseWriter, *http.Request, *User) (HeaderVars,bool) = _panel_session_check +var PanelSessionCheck func(http.ResponseWriter, *http.Request, *User) (HeaderVars,PanelStats,bool) = _panel_session_check var SimplePanelSessionCheck func(http.ResponseWriter, *http.Request, *User) bool = _simple_panel_session_check var SimpleForumSessionCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (success bool) = _simple_forum_session_check var ForumSessionCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerVars HeaderVars, success bool) = _forum_session_check @@ -208,11 +208,11 @@ func _forum_session_check(w http.ResponseWriter, r *http.Request, user *User, fi } // Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with -func _panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (headerVars HeaderVars, success bool) { +func _panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (headerVars HeaderVars, stats PanelStats, success bool) { headerVars.Site = site if !user.Is_Super_Mod { NoPermissions(w,r,*user) - return headerVars, false + return headerVars, stats, false } headerVars.Stylesheets = append(headerVars.Stylesheets,"panel.css") @@ -233,9 +233,22 @@ func _panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (h } } + err := group_count_stmt.QueryRow().Scan(&stats.Groups) + if err != nil { + InternalError(err,w) + return headerVars, stats, false + } + + stats.Users = users.GetGlobalCount() + stats.Forums = fstore.GetGlobalCount() // TO-DO: Stop it from showing the blanked forums + stats.Settings = len(settings) // TO-DO: IS this racey? + stats.Themes = len(themes) + stats.Reports = 0 // TO-DO: Do the report count. Only show open threads? + pusher, ok := w.(http.Pusher) if ok { pusher.Push("/static/main.css", nil) + pusher.Push("/static/panel.css", nil) pusher.Push("/static/global.js", nil) pusher.Push("/static/jquery-3.1.1.min.js", nil) // TO-DO: Push the theme CSS files @@ -243,7 +256,7 @@ func _panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (h // TO-DO: Push avatars? } - return headerVars, true + return headerVars, stats, true } func _simple_panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (success bool) { if !user.Is_Super_Mod { diff --git a/user_store.go b/user_store.go index e31500a2..b3b71bad 100644 --- a/user_store.go +++ b/user_store.go @@ -32,6 +32,7 @@ type UserStore interface { CreateUser(username string, password string, email string, group int, active int) (int, error) GetLength() int GetCapacity() int + GetGlobalCount() int } type MemoryUserStore struct { @@ -41,6 +42,7 @@ type MemoryUserStore struct { get *sql.Stmt register *sql.Stmt username_exists *sql.Stmt + user_count *sql.Stmt sync.RWMutex } @@ -62,12 +64,18 @@ func NewMemoryUserStore(capacity int) *MemoryUserStore { log.Fatal(err) } + user_count_stmt, err := qgen.Builder.SimpleCount("users","","") + if err != nil { + log.Fatal(err) + } + return &MemoryUserStore{ items:make(map[int]*User), capacity:capacity, get:get_stmt, register:register_stmt, username_exists:username_exists_stmt, + user_count:user_count_stmt, } } @@ -352,10 +360,21 @@ func (sus *MemoryUserStore) GetCapacity() int { return sus.capacity } +// Return the total number of users registered on the forums +func (sus *MemoryUserStore) GetGlobalCount() int { + var ucount int + err := sus.user_count.QueryRow().Scan(&ucount) + if err != nil { + LogError(err) + } + return ucount +} + type SqlUserStore struct { get *sql.Stmt register *sql.Stmt username_exists *sql.Stmt + user_count *sql.Stmt } func NewSqlUserStore() *SqlUserStore { @@ -376,10 +395,16 @@ func NewSqlUserStore() *SqlUserStore { log.Fatal(err) } + user_count_stmt, err := qgen.Builder.SimpleCount("users","","") + if err != nil { + log.Fatal(err) + } + return &SqlUserStore{ - get:get_stmt, - register:register_stmt, - username_exists:username_exists_stmt, + get: get_stmt, + register: register_stmt, + username_exists: username_exists_stmt, + user_count: user_count_stmt, } } @@ -551,6 +576,20 @@ func (sus *SqlUserStore) GetCapacity() int { return 0 } +// Return the total number of users registered on the forums func (sus *SqlUserStore) GetLength() int { - return 0 // Return the total number of users registered on the forums? + var ucount int + err := sus.user_count.QueryRow().Scan(&ucount) + if err != nil { + LogError(err) + } + return ucount +} +func (sus *SqlUserStore) GetGlobalCount() int { + var ucount int + err := sus.user_count.QueryRow().Scan(&ucount) + if err != nil { + LogError(err) + } + return ucount }