From 8f2f47e8aaa5844933b3fec70364eea653b44aeb Mon Sep 17 00:00:00 2001 From: Azareal Date: Mon, 21 Jan 2019 22:27:59 +1000 Subject: [PATCH] Added the In-Progress Widget Manager UI. Added the IsoCode field to phrase files. Rewrote a good portion of the widget system logic. Added some tests for the widget system. Added the Online Users widget. Added a few sealed incomplete widgets like the Search & Filter Widget. Added the AllUsers method to WsHubImpl for Online Users. Please don't abuse it. Added the optional *DBTableKey field to AddColumn. Added the panel_analytics_time_range template to reduce the amount of duplication. Failed registrations now show up in red in the registration logs for Nox. Failed logins now show up in red in the login logs for Nox. Added basic h2 CSS to the other themes. Added .show_on_block_edit and .hide_on_block_edit to the other themes. Updated contributing. Updated a bunch of dates to 2019. Replaced tblKey{} with nil where possible. Switched out some &s for &s to reduce the number of possible bugs. Fixed a bug with selector messages where the inspector would get really jittery due to unnecessary DOM updates. Moved header.Zone and associated fields to the bottom of ViewTopic to reduce the chances of problems arising. Added the ZoneData field to *Header. Added IDs to the items in the forum list template. Split the fetchPhrases function into the initPhrases and fetchPhrases functions in init.js Added .colstack_sub_head. Fixed the CSS in the menu list. Removed an inline style from the simple topic like and unlike buttons. Removed an inline style from the simple topic IP button. Simplified the LoginRequired error handler. Fixed a typo in the comment prior to DatabaseError() Reduce the number of false leaves for WebSocket page transitions. Added the error zone. De-duped the logic in WsHubImpl.getUsers. Fixed a potential widget security issue. Added twenty new phrases. Added the wid column to the widgets table. You will need to run the patcher / updater for this commit. --- CONTRIBUTING.md | 4 +- cmd/query_gen/tables.go | 75 ++- common/common.go | 2 +- common/errors.go | 9 +- common/forum_store.go | 2 +- common/module_ottojs.go | 2 +- common/pages.go | 11 + common/phrases/phrases.go | 4 +- common/reply.go | 2 +- common/tasks.go | 2 +- common/template_init.go | 6 + common/templates/templates.go | 1 + common/topic.go | 2 +- common/utils.go | 2 +- common/websockets.go | 64 +- common/widget.go | 148 +++++ common/widget_search_and_filter.go | 41 ++ common/widget_store.go | 39 ++ common/widget_wol.go | 55 ++ common/widget_wol_context.go | 15 + common/widgets.go | 289 +++++---- common/ws_hub.go | 34 +- gen_router.go | 561 ++++++++++-------- gen_tables.go | 23 +- install/mysql.go | 2 +- install/pgsql.go | 2 +- langs/english.json | 23 + main.go | 2 + misc_test.go | 60 ++ patcher/patches.go | 16 +- pgsql.go | 2 +- public/global.js | 12 +- public/init.js | 18 +- public/widgets.js | 66 +++ query_gen/builder.go | 4 +- query_gen/mssql.go | 3 +- query_gen/mysql.go | 14 +- query_gen/pgsql.go | 2 +- query_gen/querygen.go | 2 +- query_gen/utils.go | 2 +- router_gen/routes.go | 6 + routes.go | 2 +- routes/panel/themes.go | 155 +++++ routes/topic.go | 4 +- schema/mssql/query_widgets.sql | 4 +- schema/mysql/query_widgets.sql | 4 +- schema/pgsql/query_widgets.sql | 4 +- templates/account_logins.html | 2 +- templates/forums.html | 2 +- templates/header.html | 6 +- templates/panel_analytics_agent_views.html | 9 +- templates/panel_analytics_agents.html | 9 +- templates/panel_analytics_forum_views.html | 9 +- templates/panel_analytics_forums.html | 9 +- templates/panel_analytics_lang_views.html | 9 +- templates/panel_analytics_langs.html | 9 +- templates/panel_analytics_posts.html | 9 +- templates/panel_analytics_referrer_views.html | 9 +- templates/panel_analytics_referrers.html | 9 +- templates/panel_analytics_route_views.html | 9 +- templates/panel_analytics_routes.html | 9 +- templates/panel_analytics_system_views.html | 9 +- templates/panel_analytics_systems.html | 9 +- templates/panel_analytics_time_range.html | 8 + templates/panel_analytics_topics.html | 9 +- templates/panel_analytics_views.html | 9 +- templates/panel_inner_menu.html | 2 +- templates/panel_reglogs.html | 2 +- templates/panel_themes_menus.html | 2 +- templates/panel_themes_widgets.html | 62 ++ templates/panel_themes_widgets_widget.html | 55 ++ templates/topic_alt_posts.html | 2 +- templates/topic_posts.html | 6 +- templates/topics_topic.html | 4 +- templates/widget_online.html | 12 + templates/widget_search_and_filter.html | 7 + themes/cosora/public/main.css | 15 +- themes/cosora/public/panel.css | 23 + themes/nox/public/acc_panel_common.css | 3 + themes/nox/public/main.css | 45 +- themes/nox/public/panel.css | 42 ++ themes/shadow/public/main.css | 16 +- themes/shadow/public/panel.css | 16 + themes/tempra-simple/public/main.css | 12 +- themes/tempra-simple/public/panel.css | 16 + 85 files changed, 1660 insertions(+), 637 deletions(-) create mode 100644 common/widget.go create mode 100644 common/widget_search_and_filter.go create mode 100644 common/widget_store.go create mode 100644 common/widget_wol.go create mode 100644 common/widget_wol_context.go create mode 100644 public/widgets.js create mode 100644 templates/panel_analytics_time_range.html create mode 100644 templates/panel_themes_widgets.html create mode 100644 templates/panel_themes_widgets_widget.html create mode 100644 templates/widget_online.html create mode 100644 templates/widget_search_and_filter.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e838550..eb8eb1ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,9 @@ Always use strict mode. Don't worry about ES5, we're targetting modern browsers. If we decide to backport code to older browsers, then we'll transpile the files. -Please don't use await. It incurs too much of a cognitive overhead as to where and when you can use it. +Please don't use await. It incurs too much of a cognitive overhead as to where and when you can use it. We can't use it everywhere quite yet, which means that we really should be using it nowhere. + +Please don't abuse `const` just to shave off a few nanoseconds. Even in the Go server where I care about performance the most, I don't use const everywhere, only in about five spots in thirty thousand lines and I don't use it for performance at all there. To keep consistency with Go code, variables must be camelCase. diff --git a/cmd/query_gen/tables.go b/cmd/query_gen/tables.go index 123f4472..397e57f6 100644 --- a/cmd/query_gen/tables.go +++ b/cmd/query_gen/tables.go @@ -114,8 +114,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"issued_by","int",0,false,false,""}, tblColumn{"issued_at","createdAt",0,false,false,""}, tblColumn{"expires_at","datetime",0,false,false,""}, - }, - []tblKey{}, + }, nil, )*/ qgen.Install.CreateTable("users_groups_scheduler", "", "", @@ -150,8 +149,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key tblColumn{"validated", "boolean", 0, false, false, "0"}, tblColumn{"token", "varchar", 200, false, false, "''"}, - }, - []tblKey{}, + }, nil, ) // TODO: Allow for patterns in domains, if the bots try to shake things up there? @@ -167,6 +165,19 @@ func createTables(adapter qgen.Adapter) error { ) */ + // TODO: Implement password resets + /*qgen.Install.CreateTable("password_resets", "", "", + []tblColumn{ + tblColumn{"email", "varchar", 200, false, false, ""}, + tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"validated", "varchar", 200, false, false, ""}, // Token given once the one-use token is consumed, used to prevent multiple people consuming the same one-use token + tblColumn{"token", "varchar", 200, false, false, ""}, + }, + []tblKey{ + tblKey{"email", "unique"}, + }, + )*/ + qgen.Install.CreateTable("forums", mysqlPre, mysqlCol, []tblColumn{ tblColumn{"fid", "int", 0, false, true, ""}, @@ -305,8 +316,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"pollID", "int", 0, false, false, ""}, tblColumn{"option", "int", 0, false, false, "0"}, tblColumn{"votes", "int", 0, false, false, "0"}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("polls_votes", mysqlPre, mysqlCol, @@ -316,8 +326,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"option", "int", 0, false, false, "0"}, tblColumn{"castAt", "createdAt", 0, false, false, ""}, tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("users_replies", mysqlPre, mysqlCol, @@ -345,16 +354,14 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"sentBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key tblColumn{"createdAt", "createdAt", 0, false, false, ""}, tblColumn{"recalc", "tinyint", 0, false, false, "0"}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("activity_stream_matches", "", "", []tblColumn{ tblColumn{"watcher", "int", 0, false, false, ""}, // TODO: Make this a foreign key tblColumn{"asid", "int", 0, false, false, ""}, // TODO: Make this a foreign key - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("activity_stream", "", "", @@ -377,8 +384,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"targetID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ tblColumn{"targetType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ tblColumn{"level", "int", 0, false, false, "0"}, /* 0: Mentions (aka the global default for any post), 1: Replies To You, 2: All Replies*/ - }, - []tblKey{}, + }, nil, ) /* Due to MySQL's design, we have to drop the unique keys for table settings, plugins, and themes down from 200 to 180 or it will error */ @@ -428,6 +434,7 @@ func createTables(adapter qgen.Adapter) error { qgen.Install.CreateTable("widgets", "", "", []tblColumn{ + tblColumn{"wid", "int", 0, false, true, ""}, tblColumn{"position", "int", 0, false, false, ""}, tblColumn{"side", "varchar", 100, false, false, ""}, tblColumn{"type", "varchar", 100, false, false, ""}, @@ -435,7 +442,9 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"location", "varchar", 100, false, false, ""}, tblColumn{"data", "text", 0, false, false, "''"}, }, - []tblKey{}, + []tblKey{ + tblKey{"wid", "primary"}, + }, ) qgen.Install.CreateTable("menus", "", "", @@ -523,8 +532,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"ipaddress", "varchar", 200, false, false, ""}, tblColumn{"actorID", "int", 0, false, false, ""}, // TODO: Make this a foreign key tblColumn{"doneAt", "datetime", 0, false, false, ""}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("administration_logs", "", "", @@ -535,8 +543,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"ipaddress", "varchar", 200, false, false, ""}, tblColumn{"actorID", "int", 0, false, false, ""}, // TODO: Make this a foreign key tblColumn{"doneAt", "datetime", 0, false, false, ""}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("viewchunks", "", "", @@ -544,8 +551,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"count", "int", 0, false, false, "0"}, tblColumn{"createdAt", "datetime", 0, false, false, ""}, tblColumn{"route", "varchar", 200, false, false, ""}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("viewchunks_agents", "", "", @@ -554,8 +560,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"createdAt", "datetime", 0, false, false, ""}, tblColumn{"browser", "varchar", 200, false, false, ""}, // googlebot, firefox, opera, etc. //tblColumn{"version","varchar",0,false,false,""}, // the version of the browser or bot - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("viewchunks_systems", "", "", @@ -563,8 +568,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"count", "int", 0, false, false, "0"}, tblColumn{"createdAt", "datetime", 0, false, false, ""}, tblColumn{"system", "varchar", 200, false, false, ""}, // windows, android, unknown, etc. - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("viewchunks_langs", "", "", @@ -572,8 +576,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"count", "int", 0, false, false, "0"}, tblColumn{"createdAt", "datetime", 0, false, false, ""}, tblColumn{"lang", "varchar", 200, false, false, ""}, // en, ru, etc. - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("viewchunks_referrers", "", "", @@ -581,8 +584,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"count", "int", 0, false, false, "0"}, tblColumn{"createdAt", "datetime", 0, false, false, ""}, tblColumn{"domain", "varchar", 200, false, false, ""}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("viewchunks_forums", "", "", @@ -590,8 +592,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"count", "int", 0, false, false, "0"}, tblColumn{"createdAt", "datetime", 0, false, false, ""}, tblColumn{"forum", "int", 0, false, false, ""}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("topicchunks", "", "", @@ -599,8 +600,7 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"count", "int", 0, false, false, "0"}, tblColumn{"createdAt", "datetime", 0, false, false, ""}, // TODO: Add a column for the parent forum? - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("postchunks", "", "", @@ -608,22 +608,19 @@ func createTables(adapter qgen.Adapter) error { tblColumn{"count", "int", 0, false, false, "0"}, tblColumn{"createdAt", "datetime", 0, false, false, ""}, // TODO: Add a column for the parent topic / profile? - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("sync", "", "", []tblColumn{ tblColumn{"last_update", "datetime", 0, false, false, ""}, - }, - []tblKey{}, + }, nil, ) qgen.Install.CreateTable("updates", "", "", []tblColumn{ tblColumn{"dbVersion", "int", 0, false, false, "0"}, - }, - []tblKey{}, + }, nil, ) return nil diff --git a/common/common.go b/common/common.go index e4055280..3ebc81ab 100644 --- a/common/common.go +++ b/common/common.go @@ -1,7 +1,7 @@ /* * * Gosora Common Resources -* Copyright Azareal 2018 - 2019 +* Copyright Azareal 2018 - 2020 * */ package common // import "github.com/Azareal/Gosora/common" diff --git a/common/errors.go b/common/errors.go index 07977c6e..90dda12a 100644 --- a/common/errors.go +++ b/common/errors.go @@ -128,6 +128,7 @@ func LogWarning(err error, extra ...string) { func errorHeader(w http.ResponseWriter, user User, title string) *Header { header := DefaultHeader(w, user) header.Title = title + header.Zone = "error" return header } @@ -160,7 +161,7 @@ func InternalErrorJS(err error, w http.ResponseWriter, r *http.Request) RouteErr return HandledRouteError() } -// When the task system detects if the database is down, some database errors might lip by this +// When the task system detects if the database is down, some database errors might slip by this func DatabaseError(w http.ResponseWriter, r *http.Request) RouteError { w.WriteHeader(500) pi := ErrorPage{errorHeader(w, GuestUser, phrases.GetErrorPhrase("internal_error_title")), phrases.GetErrorPhrase("internal_error_body")} @@ -285,10 +286,7 @@ func LoginRequiredJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bo // ? - Where is this used? Should we use it more? // LoginRequired is an error shown to the end-user when they try to access an area which requires them to login func LoginRequired(w http.ResponseWriter, r *http.Request, user User) RouteError { - w.WriteHeader(401) - pi := ErrorPage{errorHeader(w, user, phrases.GetErrorPhrase("no_permissions_title")), phrases.GetErrorPhrase("login_required_body")} - handleErrorTemplate(w, r, pi) - return HandledRouteError() + return CustomError(phrases.GetErrorPhrase("login_required_body"), 401, phrases.GetErrorPhrase("no_permissions_title"), w, r, nil, user) } // nolint @@ -343,6 +341,7 @@ func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWri header = DefaultHeader(w, user) } header.Title = errtitle + header.Zone = "error" w.WriteHeader(errcode) pi := ErrorPage{header, errmsg} handleErrorTemplate(w, r, pi) diff --git a/common/forum_store.go b/common/forum_store.go index fd87b4a7..88661ecb 100644 --- a/common/forum_store.go +++ b/common/forum_store.go @@ -1,7 +1,7 @@ /* * * Gosora Forum Store -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package common diff --git a/common/module_ottojs.go b/common/module_ottojs.go index ed6c9f3e..bd0e4718 100644 --- a/common/module_ottojs.go +++ b/common/module_ottojs.go @@ -1,7 +1,7 @@ /* * * OttoJS Plugin Module -* Copyright Azareal 2016 - 2018 +* Copyright Azareal 2016 - 2019 * */ package common diff --git a/common/pages.go b/common/pages.go index 5ba0ae10..cddd3a97 100644 --- a/common/pages.go +++ b/common/pages.go @@ -28,6 +28,8 @@ type Header struct { CurrentUser User // TODO: Deprecate CurrentUser on the page structs and use a pointer here Hooks *HookTable Zone string + ZoneID int + ZoneData interface{} Path string MetaDesc string StartedAt time.Time @@ -326,6 +328,12 @@ type PanelMenuListPage struct { ItemList []PanelMenuListItem } +type PanelWidgetListPage struct { + *BasePanelPage + Docks map[string][]WidgetEdit + BlankWidget WidgetEdit +} + type PanelMenuPage struct { *BasePanelPage MenuID int @@ -463,3 +471,6 @@ type AreYouSure struct { func DefaultHeader(w http.ResponseWriter, user User) *Header { return &Header{Site: Site, Theme: Themes[fallbackTheme], CurrentUser: user, Writer: w} } +func SimpleDefaultHeader(w http.ResponseWriter) *Header { + return &Header{Site: Site, Theme: Themes[fallbackTheme], CurrentUser: GuestUser, Writer: w} +} diff --git a/common/phrases/phrases.go b/common/phrases/phrases.go index 134fd42c..aaa2e79b 100644 --- a/common/phrases/phrases.go +++ b/common/phrases/phrases.go @@ -37,7 +37,9 @@ type LevelPhrases struct { // ! For the sake of thread safety, you must never modify a *LanguagePack directly, but to create a copy of it and overwrite the entry in the sync.Map type LanguagePack struct { - Name string + Name string + IsoCode string + // Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent. Levels LevelPhrases GlobalPerms map[string]string diff --git a/common/reply.go b/common/reply.go index fdf3558c..a718d0e1 100644 --- a/common/reply.go +++ b/common/reply.go @@ -1,7 +1,7 @@ /* * * Reply Resources File -* Copyright Azareal 2016 - 2018 +* Copyright Azareal 2016 - 2019 * */ package common diff --git a/common/tasks.go b/common/tasks.go index 97e39c96..5416a60e 100644 --- a/common/tasks.go +++ b/common/tasks.go @@ -1,7 +1,7 @@ /* * * Gosora Task System -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package common diff --git a/common/template_init.go b/common/template_init.go index 2786c060..cb9b76e3 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -470,6 +470,11 @@ func CompileJSTemplates() error { if err != nil { return err } + /*widget := &Widget{ID: 0} + panelWidgetsWidgetTmpl, err := c.Compile("panel_themes_widgets_widget.html", "templates/", "*common.Widget", widget, varList) + if err != nil { + return err + }*/ var dirPrefix = "./tmpl_client/" var wg sync.WaitGroup @@ -492,6 +497,7 @@ func CompileJSTemplates() error { writeTemplate("topics_topic", topicListItemTmpl) writeTemplate("topic_posts", topicPostsTmpl) writeTemplate("topic_alt_posts", topicAltPostsTmpl) + //writeTemplate("panel_themes_widgets_widget", panelWidgetsWidgetTmpl) writeTemplateList(c, &wg, dirPrefix) return nil } diff --git a/common/templates/templates.go b/common/templates/templates.go index d8801008..0805fe7b 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -94,6 +94,7 @@ func NewCTemplateSet() *CTemplateSet { "reltime": true, "scope": true, "dyntmpl": true, + "index": true, }, } } diff --git a/common/topic.go b/common/topic.go index 75a29410..70410c0d 100644 --- a/common/topic.go +++ b/common/topic.go @@ -1,7 +1,7 @@ /* * * Gosora Topic File -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package common diff --git a/common/utils.go b/common/utils.go index f4e1ccc9..8a6c2032 100644 --- a/common/utils.go +++ b/common/utils.go @@ -1,7 +1,7 @@ /* * * Utility Functions And Stuff -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package common diff --git a/common/websockets.go b/common/websockets.go index 686cdc54..a961a647 100644 --- a/common/websockets.go +++ b/common/websockets.go @@ -15,13 +15,14 @@ import ( "net/http" "runtime" "strconv" + "strings" "sync" "time" + "github.com/Azareal/Gosora/common/phrases" "github.com/Azareal/gopsutil/cpu" "github.com/Azareal/gopsutil/mem" "github.com/gorilla/websocket" - "github.com/Azareal/Gosora/common/phrases" ) // TODO: Disable WebSockets on high load? Add a Control Panel interface for disabling it? @@ -97,6 +98,16 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr return nil } +// TODO: Copied from routes package for use in wsPageResponse, find a more elegant solution. +func ParseSEOURL(urlBit string) (slug string, id int, err error) { + halves := strings.Split(urlBit, ".") + if len(halves) < 2 { + halves = append(halves, halves[0]) + } + tid, err := strconv.Atoi(halves[1]) + return halves[0], tid, err +} + // TODO: Use a map instead of a switch to make this more modular? func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) { if page == "/" { @@ -104,14 +115,47 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) { } DebugLog("Entering page " + page) - switch page { + switch { // Live Topic List is an experimental feature // TODO: Optimise this to reduce the amount of contention - case "/topics/": + case page == "/topics/": topicListMutex.Lock() topicListWatchers[wsUser] = true topicListMutex.Unlock() - case "/panel/": + case strings.HasPrefix(page, "/topic/"): + //fmt.Println("entering topic prefix websockets zone") + _, tid, err := ParseSEOURL(page) + if err != nil { + return + } + topic, err := Topics.Get(tid) + if err != nil { + return + } + var usercpy *User = BlankUser() + *usercpy = *wsUser.User + usercpy.Init() + + if !Forums.Exists(topic.ParentID) { + return + } + + /*skip, rerr := header.Hooks.VhookSkippable("ws_topic_check_pre_perms", w, r, usercpy, &fid, &header) + if skip || rerr != nil { + return + }*/ + + fperms, err := FPStore.Get(topic.ParentID, usercpy.Group) + if err == ErrNoRows { + fperms = BlankForumPerms() + } else if err != nil { + return + } + cascadeForumPerms(fperms, usercpy) + if !usercpy.Perms.ViewTopic { + return + } + case page == "/panel/": if !wsUser.User.IsSuperMod { return } @@ -138,15 +182,19 @@ func wsLeavePage(wsUser *WSUser, conn *websocket.Conn, page string) { page = Config.DefaultPath } - DebugLog("Leaving page " + page) - switch page { - case "/topics/": + if page != "" { + DebugLog("Leaving page " + page) + } + switch { + case page == "/topics/": wsUser.FinalizePage("/topics/", func() { topicListMutex.Lock() delete(topicListWatchers, wsUser) topicListMutex.Unlock() }) - case "/panel/": + case strings.HasPrefix(page, "/topic/"): + //fmt.Println("leaving topic prefix websockets zone") + case page == "/panel/": adminStatsMutex.Lock() delete(adminStatsWatchers, conn) adminStatsMutex.Unlock() diff --git a/common/widget.go b/common/widget.go new file mode 100644 index 00000000..6c332df5 --- /dev/null +++ b/common/widget.go @@ -0,0 +1,148 @@ +package common + +import ( + "database/sql" + "encoding/json" + "strings" + "sync/atomic" + + "github.com/Azareal/Gosora/query_gen" +) + +type WidgetStmts struct { + //getList *sql.Stmt + getDockList *sql.Stmt + delete *sql.Stmt + create *sql.Stmt + update *sql.Stmt +} + +var widgetStmts WidgetStmts + +func init() { + DbInits.Add(func(acc *qgen.Accumulator) error { + widgetStmts = WidgetStmts{ + //getList: acc.Select("widgets").Columns("wid, position, side, type, active, location, data").Orderby("position ASC").Prepare(), + getDockList: acc.Select("widgets").Columns("wid, position, type, active, location, data").Where("side = ?").Orderby("position ASC").Prepare(), + delete: acc.Delete("widgets").Where("wid = ?").Prepare(), + create: acc.Insert("widgets").Columns("position, side, type, active, location, data").Fields("?,?,?,?,?,?").Prepare(), + update: acc.Update("widgets").Set("position = ?, side = ?, type = ?, active = ?, location = ?, data = ?").Where("wid = ?").Prepare(), + } + return acc.FirstError() + }) +} + +// TODO: Shrink this struct for common uses in the templates? Would that really make things go faster? +type Widget struct { + ID int + Enabled bool + Location string // Coming Soon: overview, topics, topic / topic_view, forums, forum, global + Position int + RawBody string + Body string + Side string + Type string + + Literal bool + TickMask atomic.Value + InitFunc func(widget *Widget, schedule *WidgetScheduler) error + ShutdownFunc func(widget *Widget) error + BuildFunc func(widget *Widget, hvars interface{}) (string, error) + TickFunc func(widget *Widget) error +} + +func (widget *Widget) Delete() error { + _, err := widgetStmts.delete.Exec(widget.ID) + if err != nil { + return err + } + + // Reload the dock + // TODO: Better synchronisation + Widgets.delete(widget.ID) + widgets, err := getDockWidgets(widget.Side) + if err != nil { + return err + } + setDock(widget.Side, widgets) + return nil +} + +func (widget *Widget) Copy() (owidget *Widget) { + owidget = &Widget{} + *owidget = *widget + return owidget +} + +// TODO: Test this +// TODO: Add support for zone:id. Perhaps, carry a ZoneID property around in *Header? It might allow some weirdness like frontend[5] which matches any zone with an ID of 5 but it would be a tad faster than verifying each zone, although it might be problematic if users end up relying on this behaviour for areas which don't pass IDs to the widgets system but *probably* should +func (widget *Widget) Allowed(zone string) bool { + for _, loc := range strings.Split(widget.Location, "|") { + if loc == "global" || loc == zone { + return true + } else if len(loc) > 0 && loc[0] == '!' { + loc = loc[1:] + if loc != "global" && loc != zone { + return true + } + } + } + return false +} + +// TODO: Refactor +func (widget *Widget) Build(hvars interface{}) (string, error) { + if widget.Literal { + return widget.Body, nil + } + if widget.BuildFunc != nil { + return widget.BuildFunc(widget, hvars) + } + + var header = hvars.(*Header) + err := header.Theme.RunTmpl(widget.Body, hvars, header.Writer) + return "", err +} + +type WidgetEdit struct { + *Widget + Data map[string]string +} + +func (widget *WidgetEdit) Create() error { + data, err := json.Marshal(widget.Data) + if err != nil { + return err + } + _, err = widgetStmts.create.Exec(widget.Position, widget.Side, widget.Type, widget.Enabled, widget.Location, data) + if err != nil { + return err + } + + // Reload the dock + widgets, err := getDockWidgets(widget.Side) + if err != nil { + return err + } + setDock(widget.Side, widgets) + return nil +} + +func (widget *WidgetEdit) Commit() error { + data, err := json.Marshal(widget.Data) + if err != nil { + return err + } + _, err = widgetStmts.update.Exec(widget.Position, widget.Side, widget.Type, widget.Enabled, widget.Location, data, widget.ID) + if err != nil { + return err + } + + // Reload the dock + widgets, err := getDockWidgets(widget.Side) + if err != nil { + return err + } + setDock(widget.Side, widgets) + return nil +} diff --git a/common/widget_search_and_filter.go b/common/widget_search_and_filter.go new file mode 100644 index 00000000..69a23aa3 --- /dev/null +++ b/common/widget_search_and_filter.go @@ -0,0 +1,41 @@ +package common + +import "errors" + +// TODO: Move this into it's own package to make neater and tidier +type searchAndFilter struct { + *Header + Forums []*Forum +} + +func widgetSearchAndFilter(widget *Widget, hvars interface{}) (out string, err error) { + header := hvars.(*Header) + user := header.CurrentUser + + var forums []*Forum + var canSee []int + if user.IsSuperAdmin { + canSee, err = Forums.GetAllVisibleIDs() + if err != nil { + return "", err + } + } else { + group, err := Groups.Get(user.Group) + if err != nil { + // TODO: Revisit this + return "", errors.New("Something weird happened") + } + canSee = group.CanSee + } + + for _, fid := range canSee { + forum := Forums.DirtyGet(fid) + if forum.ParentID == 0 && forum.Name != "" && forum.Active { + forums = append(forums, forum) + } + } + + saf := &searchAndFilter{header, forums} + err = saf.Header.Theme.RunTmpl("widget_search_and_filter", saf, saf.Header.Writer) + return "", err +} \ No newline at end of file diff --git a/common/widget_store.go b/common/widget_store.go new file mode 100644 index 00000000..36fd3900 --- /dev/null +++ b/common/widget_store.go @@ -0,0 +1,39 @@ +package common + +import ( + "database/sql" + "sync" +) + +var Widgets *DefaultWidgetStore + +type DefaultWidgetStore struct { + widgets map[int]*Widget + sync.RWMutex +} + +func NewDefaultWidgetStore() *DefaultWidgetStore { + return &DefaultWidgetStore{widgets: make(map[int]*Widget)} +} + +func (widgets *DefaultWidgetStore) Get(id int) (*Widget, error) { + widgets.RLock() + defer widgets.RUnlock() + widget, ok := widgets.widgets[id] + if !ok { + return widget, sql.ErrNoRows + } + return widget, nil +} + +func (widgets *DefaultWidgetStore) set(widget *Widget) { + widgets.Lock() + defer widgets.Unlock() + widgets.widgets[widget.ID] = widget +} + +func (widgets *DefaultWidgetStore) delete(id int) { + widgets.Lock() + defer widgets.Unlock() + delete(widgets.widgets, id) +} diff --git a/common/widget_wol.go b/common/widget_wol.go new file mode 100644 index 00000000..b5a6271e --- /dev/null +++ b/common/widget_wol.go @@ -0,0 +1,55 @@ +package common + +import ( + "bytes" + "net/http/httptest" + + "github.com/Azareal/Gosora/common/phrases" +) + +type wolUsers struct { + *Header + Name string + Users []*User + UserCount int +} + +func wolInit(widget *Widget, schedule *WidgetScheduler) error { + schedule.Add(widget) + return nil +} + +func wolBuild(widget *Widget, hvars interface{}) (string, error) { + ucount := WsHub.UserCount() + // We don't want a ridiculously long list, so we'll show the number if it's too high and only show staff individually + var users []*User + if ucount < 30 { + users = WsHub.AllUsers() + } + wol := &wolUsers{hvars.(*Header), phrases.GetTmplPhrase("widget.online_name"), users, ucount} + err := wol.Header.Theme.RunTmpl("widget_online", wol, wol.Header.Writer) + return "", err +} + +func wolRender(widget *Widget, hvars interface{}) (string, error) { + iTickMask := widget.TickMask.Load() + if iTickMask != nil { + tickMask := iTickMask.(*Widget) + if tickMask != nil { + return tickMask.Body, nil + } + } + return wolBuild(widget, hvars) +} + +func wolTick(widget *Widget) error { + w := httptest.NewRecorder() + _, err := wolBuild(widget, SimpleDefaultHeader(w)) + if err != nil { + return err + } + buf := new(bytes.Buffer) + buf.ReadFrom(w.Result().Body) + widget.TickMask.Store(buf.String()) + return nil +} diff --git a/common/widget_wol_context.go b/common/widget_wol_context.go new file mode 100644 index 00000000..4d886da3 --- /dev/null +++ b/common/widget_wol_context.go @@ -0,0 +1,15 @@ +package common + +import "github.com/Azareal/Gosora/common/phrases" + +func wolContextRender(widget *Widget, hvars interface{}) (string, error) { + ucount := WsHub.UserCount() + // We don't want a ridiculously long list, so we'll show the number if it's too high and only show staff individually + var users []*User + if ucount < 30 { + users = WsHub.AllUsers() + } + wol := &wolUsers{hvars.(*Header), phrases.GetTmplPhrase("widget.online_name"), users, ucount} + err := wol.Header.Theme.RunTmpl("widget_online", wol, wol.Header.Writer) + return "", err +} diff --git a/common/widgets.go b/common/widgets.go index de90f155..7f91205f 100644 --- a/common/widgets.go +++ b/common/widgets.go @@ -1,37 +1,32 @@ -/* Copyright Azareal 2017 - 2018 */ +/* Copyright Azareal 2017 - 2019 */ package common import ( "bytes" - "database/sql" "encoding/json" + "fmt" "html/template" "strings" "sync" - - "github.com/Azareal/Gosora/query_gen" + "sync/atomic" ) +// TODO: Clean this file up var Docks WidgetDocks var widgetUpdateMutex sync.RWMutex +type WidgetDock struct { + Items []*Widget + Scheduler *WidgetScheduler +} + type WidgetDocks struct { LeftOfNav []*Widget RightOfNav []*Widget - LeftSidebar []*Widget - RightSidebar []*Widget + LeftSidebar WidgetDock + RightSidebar WidgetDock //PanelLeft []Menus - Footer []*Widget -} - -type Widget struct { - Enabled bool - Location string // Coming Soon: overview, topics, topic / topic_view, forums, forum, global - Position int - Body string - Side string - Type string - Literal bool + Footer WidgetDock } type WidgetMenu struct { @@ -50,21 +45,6 @@ type NameTextPair struct { Text template.HTML } -type WidgetStmts struct { - getWidgets *sql.Stmt -} - -var widgetStmts WidgetStmts - -func init() { - DbInits.Add(func(acc *qgen.Accumulator) error { - widgetStmts = WidgetStmts{ - getWidgets: acc.Select("widgets").Columns("position, side, type, active, location, data").Orderby("position ASC").Prepare(), - } - return acc.FirstError() - }) -} - func preparseWidget(widget *Widget, wdata string) (err error) { prebuildWidget := func(name string, data interface{}) (string, error) { var b bytes.Buffer @@ -73,25 +53,30 @@ func preparseWidget(widget *Widget, wdata string) (err error) { } sbytes := []byte(wdata) + widget.Literal = true + // TODO: Split these hard-coded items out of this file and into the files for the individual widget types switch widget.Type { - case "simple": + case "simple", "about": var tmp NameTextPair err = json.Unmarshal(sbytes, &tmp) if err != nil { return err } - widget.Body, err = prebuildWidget("widget_simple", tmp) - case "about": - var tmp NameTextPair - err = json.Unmarshal(sbytes, &tmp) - if err != nil { - return err - } - widget.Body, err = prebuildWidget("widget_about", tmp) + widget.Body, err = prebuildWidget("widget_"+widget.Type, tmp) + case "search_and_filter": + widget.Literal = false + widget.BuildFunc = widgetSearchAndFilter + case "wol": + widget.Literal = false + widget.InitFunc = wolInit + widget.BuildFunc = wolRender + widget.TickFunc = wolTick + case "wol_context": + widget.Literal = false + widget.BuildFunc = wolContextRender default: widget.Body = wdata } - widget.Literal = true // TODO: Test this // TODO: Should we toss this through a proper parser rather than crudely replacing it? @@ -115,6 +100,37 @@ func preparseWidget(widget *Widget, wdata string) (err error) { return err } +func GetDockList() []string { + return []string{ + "leftOfNav", + "rightOfNav", + "rightSidebar", + "footer", + } +} + +func GetDock(dock string) []*Widget { + switch dock { + case "leftOfNav": + return Docks.LeftOfNav + case "rightOfNav": + return Docks.RightOfNav + case "rightSidebar": + return Docks.RightSidebar.Items + case "footer": + return Docks.Footer.Items + } + return nil +} + +func HasDock(dock string) bool { + switch dock { + case "leftOfNav", "rightOfNav", "rightSidebar", "footer": + return true + } + return false +} + func BuildWidget(dock string, header *Header) (sbody string) { var widgets []*Widget if !header.Theme.HasDock(dock) { @@ -143,9 +159,9 @@ func BuildWidget(dock string, header *Header) (sbody string) { } return "" case "rightSidebar": - widgets = Docks.RightSidebar + widgets = Docks.RightSidebar.Items case "footer": - widgets = Docks.Footer + widgets = Docks.Footer.Items } for _, widget := range widgets { @@ -163,93 +179,138 @@ func BuildWidget(dock string, header *Header) (sbody string) { return sbody } -// TODO: Test this -// TODO: Add support for zone:id. Perhaps, carry a ZoneID property around in *Header? It might allow some weirdness like frontend[5] which matches any zone with an ID of 5 but it would be a tad faster than verifying each zone, although it might be problematic if users end up relying on this behaviour for areas which don't pass IDs to the widgets system but *probably* should -func (widget *Widget) Allowed(zone string) bool { - for _, loc := range strings.Split(widget.Location, "|") { - if loc == "global" || loc == zone { - return true - } else if len(loc) > 0 && loc[0] == '!' { - loc = loc[1:] - if loc != "global" && loc != zone { - return true - } +func getDockWidgets(dock string) (widgets []*Widget, err error) { + rows, err := widgetStmts.getDockList.Query(dock) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var widget = &Widget{Position: 0, Side: dock} + err = rows.Scan(&widget.ID, &widget.Position, &widget.Type, &widget.Enabled, &widget.Location, &widget.RawBody) + if err != nil { + return nil, err } - } - return false -} -// TODO: Refactor -func (widget *Widget) Build(hvars interface{}) (string, error) { - if widget.Literal { - return widget.Body, nil + err = preparseWidget(widget, widget.RawBody) + if err != nil { + return nil, err + } + Widgets.set(widget) + widgets = append(widgets, widget) } - - var header = hvars.(*Header) - err := header.Theme.RunTmpl(widget.Body, hvars, header.Writer) - return "", err + return widgets, rows.Err() } // TODO: Make a store for this? func InitWidgets() error { - rows, err := widgetStmts.getWidgets.Query() + leftOfNavWidgets, err := getDockWidgets("leftOfNav") if err != nil { return err } - defer rows.Close() - - var data string - var leftOfNavWidgets []*Widget - var rightOfNavWidgets []*Widget - var leftSidebarWidgets []*Widget - var rightSidebarWidgets []*Widget - var footerWidgets []*Widget - - for rows.Next() { - var widget = &Widget{Position: 0} - err = rows.Scan(&widget.Position, &widget.Side, &widget.Type, &widget.Enabled, &widget.Location, &data) - if err != nil { - return err - } - - err = preparseWidget(widget, data) - if err != nil { - return err - } - - switch widget.Side { - case "leftOfNav": - leftOfNavWidgets = append(leftOfNavWidgets, widget) - case "rightOfNav": - rightOfNavWidgets = append(rightOfNavWidgets, widget) - case "left": - leftSidebarWidgets = append(leftSidebarWidgets, widget) - case "right": - rightSidebarWidgets = append(rightSidebarWidgets, widget) - case "footer": - footerWidgets = append(footerWidgets, widget) - } + rightOfNavWidgets, err := getDockWidgets("rightOfNav") + if err != nil { + return err } - err = rows.Err() + leftSidebarWidgets, err := getDockWidgets("leftSidebar") + if err != nil { + return err + } + rightSidebarWidgets, err := getDockWidgets("rightSidebar") + if err != nil { + return err + } + footerWidgets, err := getDockWidgets("footer") if err != nil { return err } // TODO: Let themes set default values for widget docks, and let them lock in particular places with their stuff, e.g. leftOfNav and rightOfNav - widgetUpdateMutex.Lock() - Docks.LeftOfNav = leftOfNavWidgets - Docks.RightOfNav = rightOfNavWidgets - Docks.LeftSidebar = leftSidebarWidgets - Docks.RightSidebar = rightSidebarWidgets - Docks.Footer = footerWidgets - widgetUpdateMutex.Unlock() - - DebugLog("Docks.LeftOfNav", Docks.LeftOfNav) - DebugLog("Docks.RightOfNav", Docks.RightOfNav) - DebugLog("Docks.LeftSidebar", Docks.LeftSidebar) - DebugLog("Docks.RightSidebar", Docks.RightSidebar) - DebugLog("Docks.Footer", Docks.Footer) + setDock("leftOfNav", leftOfNavWidgets) + setDock("rightOfNav", rightOfNavWidgets) + setDock("leftSidebar", leftSidebarWidgets) + setDock("rightSidebar", rightSidebarWidgets) + setDock("footer", footerWidgets) + AddScheduledSecondTask(Docks.LeftSidebar.Scheduler.Tick) + AddScheduledSecondTask(Docks.RightSidebar.Scheduler.Tick) + AddScheduledSecondTask(Docks.Footer.Scheduler.Tick) return nil } + +func releaseWidgets(widgets []*Widget) { + for _, widget := range widgets { + if widget.ShutdownFunc != nil { + widget.ShutdownFunc(widget) + } + } +} + +// TODO: Use atomics +func setDock(dock string, widgets []*Widget) { + var dockHandle = func(dockWidgets []*Widget) { + widgetUpdateMutex.Lock() + DebugLog(dock, widgets) + releaseWidgets(dockWidgets) + } + var dockHandle2 = func(dockWidgets WidgetDock) WidgetDock { + dockHandle(dockWidgets.Items) + if dockWidgets.Scheduler == nil { + dockWidgets.Scheduler = &WidgetScheduler{} + } + for _, widget := range widgets { + if widget.InitFunc != nil { + widget.InitFunc(widget, dockWidgets.Scheduler) + } + } + dockWidgets.Scheduler.Store() + return WidgetDock{widgets, dockWidgets.Scheduler} + } + switch dock { + case "leftOfNav": + dockHandle(Docks.LeftOfNav) + Docks.LeftOfNav = widgets + case "rightOfNav": + dockHandle(Docks.RightOfNav) + Docks.RightOfNav = widgets + case "leftSidebar": + Docks.LeftSidebar = dockHandle2(Docks.LeftSidebar) + case "rightSidebar": + Docks.RightSidebar = dockHandle2(Docks.RightSidebar) + case "footer": + Docks.Footer = dockHandle2(Docks.Footer) + default: + fmt.Printf("bad dock '%s'\n", dock) + return + } + widgetUpdateMutex.Unlock() +} + +type WidgetScheduler struct { + widgets []*Widget + store atomic.Value +} + +func (schedule *WidgetScheduler) Add(widget *Widget) { + schedule.widgets = append(schedule.widgets, widget) +} + +func (schedule *WidgetScheduler) Store() { + schedule.store.Store(schedule.widgets) +} + +func (schedule *WidgetScheduler) Tick() error { + widgets := schedule.store.Load().([]*Widget) + for _, widget := range widgets { + if widget.TickFunc == nil { + continue + } + err := widget.TickFunc(widget.Copy()) + if err != nil { + return err + } + } + return nil +} diff --git a/common/ws_hub.go b/common/ws_hub.go index a20c9a2a..7d009292 100644 --- a/common/ws_hub.go +++ b/common/ws_hub.go @@ -251,24 +251,36 @@ func (hub *WsHubImpl) getUsers(uids []int) (wsUsers []*WSUser, err error) { if len(uids) == 0 { return nil, errWsNouser } - hub.evenUserLock.RLock() - // We don't want to keep a lock on this for too long, so we'll accept some nil pointers - for _, uid := range uids { - wsUsers = append(wsUsers, hub.evenOnlineUsers[uid]) + var appender = func(lock *sync.RWMutex, users map[int]*WSUser) { + lock.RLock() + defer lock.RUnlock() + // We don't want to keep a lock on this for too long, so we'll accept some nil pointers + for _, uid := range uids { + wsUsers = append(wsUsers, users[uid]) + } } - hub.evenUserLock.RUnlock() - hub.oddUserLock.RLock() - // We don't want to keep a lock on this for too long, so we'll accept some nil pointers - for _, uid := range uids { - wsUsers = append(wsUsers, hub.oddOnlineUsers[uid]) - } - hub.oddUserLock.RUnlock() + appender(&hub.evenUserLock, hub.evenOnlineUsers) + appender(&hub.oddUserLock, hub.oddOnlineUsers) if len(wsUsers) == 0 { return nil, errWsNouser } return wsUsers, nil } +// For Widget WOL, please avoid using this as it might wind up being really long and slow without the right safeguards +func (hub *WsHubImpl) AllUsers() (users []*User) { + var appender = func(lock *sync.RWMutex, userMap map[int]*WSUser) { + lock.RLock() + defer lock.RUnlock() + for _, user := range userMap { + users = append(users, user.User) + } + } + appender(&hub.evenUserLock, hub.evenOnlineUsers) + appender(&hub.oddUserLock, hub.oddOnlineUsers) + return users +} + func (hub *WsHubImpl) removeUser(uid int) { if uid%2 == 0 { hub.evenUserLock.Lock() diff --git a/gen_router.go b/gen_router.go index 354e77ab..4d6634f7 100644 --- a/gen_router.go +++ b/gen_router.go @@ -69,6 +69,10 @@ var RouteMap = map[string]interface{}{ "panel.ThemesMenuItemCreateSubmit": panel.ThemesMenuItemCreateSubmit, "panel.ThemesMenuItemDeleteSubmit": panel.ThemesMenuItemDeleteSubmit, "panel.ThemesMenuItemOrderSubmit": panel.ThemesMenuItemOrderSubmit, + "panel.ThemesWidgets": panel.ThemesWidgets, + "panel.ThemesWidgetsEditSubmit": panel.ThemesWidgetsEditSubmit, + "panel.ThemesWidgetsCreateSubmit": panel.ThemesWidgetsCreateSubmit, + "panel.ThemesWidgetsDeleteSubmit": panel.ThemesWidgetsDeleteSubmit, "panel.Plugins": panel.Plugins, "panel.PluginsActivate": panel.PluginsActivate, "panel.PluginsDeactivate": panel.PluginsDeactivate, @@ -206,93 +210,97 @@ var routeMapEnum = map[string]int{ "panel.ThemesMenuItemCreateSubmit": 43, "panel.ThemesMenuItemDeleteSubmit": 44, "panel.ThemesMenuItemOrderSubmit": 45, - "panel.Plugins": 46, - "panel.PluginsActivate": 47, - "panel.PluginsDeactivate": 48, - "panel.PluginsInstall": 49, - "panel.Users": 50, - "panel.UsersEdit": 51, - "panel.UsersEditSubmit": 52, - "panel.AnalyticsViews": 53, - "panel.AnalyticsRoutes": 54, - "panel.AnalyticsAgents": 55, - "panel.AnalyticsSystems": 56, - "panel.AnalyticsLanguages": 57, - "panel.AnalyticsReferrers": 58, - "panel.AnalyticsRouteViews": 59, - "panel.AnalyticsAgentViews": 60, - "panel.AnalyticsForumViews": 61, - "panel.AnalyticsSystemViews": 62, - "panel.AnalyticsLanguageViews": 63, - "panel.AnalyticsReferrerViews": 64, - "panel.AnalyticsPosts": 65, - "panel.AnalyticsTopics": 66, - "panel.AnalyticsForums": 67, - "panel.Groups": 68, - "panel.GroupsEdit": 69, - "panel.GroupsEditPerms": 70, - "panel.GroupsEditSubmit": 71, - "panel.GroupsEditPermsSubmit": 72, - "panel.GroupsCreateSubmit": 73, - "panel.Backups": 74, - "panel.LogsRegs": 75, - "panel.LogsMod": 76, - "panel.Debug": 77, - "panel.Dashboard": 78, - "routes.AccountEdit": 79, - "routes.AccountEditPassword": 80, - "routes.AccountEditPasswordSubmit": 81, - "routes.AccountEditAvatarSubmit": 82, - "routes.AccountEditUsernameSubmit": 83, - "routes.AccountEditMFA": 84, - "routes.AccountEditMFASetup": 85, - "routes.AccountEditMFASetupSubmit": 86, - "routes.AccountEditMFADisableSubmit": 87, - "routes.AccountEditEmail": 88, - "routes.AccountEditEmailTokenSubmit": 89, - "routes.AccountLogins": 90, - "routes.LevelList": 91, - "routes.ViewProfile": 92, - "routes.BanUserSubmit": 93, - "routes.UnbanUser": 94, - "routes.ActivateUser": 95, - "routes.IPSearch": 96, - "routes.CreateTopicSubmit": 97, - "routes.EditTopicSubmit": 98, - "routes.DeleteTopicSubmit": 99, - "routes.StickTopicSubmit": 100, - "routes.UnstickTopicSubmit": 101, - "routes.LockTopicSubmit": 102, - "routes.UnlockTopicSubmit": 103, - "routes.MoveTopicSubmit": 104, - "routes.LikeTopicSubmit": 105, - "routes.AddAttachToTopicSubmit": 106, - "routes.RemoveAttachFromTopicSubmit": 107, - "routes.ViewTopic": 108, - "routes.CreateReplySubmit": 109, - "routes.ReplyEditSubmit": 110, - "routes.ReplyDeleteSubmit": 111, - "routes.ReplyLikeSubmit": 112, - "routes.AddAttachToReplySubmit": 113, - "routes.RemoveAttachFromReplySubmit": 114, - "routes.ProfileReplyCreateSubmit": 115, - "routes.ProfileReplyEditSubmit": 116, - "routes.ProfileReplyDeleteSubmit": 117, - "routes.PollVote": 118, - "routes.PollResults": 119, - "routes.AccountLogin": 120, - "routes.AccountRegister": 121, - "routes.AccountLogout": 122, - "routes.AccountLoginSubmit": 123, - "routes.AccountLoginMFAVerify": 124, - "routes.AccountLoginMFAVerifySubmit": 125, - "routes.AccountRegisterSubmit": 126, - "routes.DynamicRoute": 127, - "routes.UploadedFile": 128, - "routes.StaticFile": 129, - "routes.RobotsTxt": 130, - "routes.SitemapXml": 131, - "routes.BadRoute": 132, + "panel.ThemesWidgets": 46, + "panel.ThemesWidgetsEditSubmit": 47, + "panel.ThemesWidgetsCreateSubmit": 48, + "panel.ThemesWidgetsDeleteSubmit": 49, + "panel.Plugins": 50, + "panel.PluginsActivate": 51, + "panel.PluginsDeactivate": 52, + "panel.PluginsInstall": 53, + "panel.Users": 54, + "panel.UsersEdit": 55, + "panel.UsersEditSubmit": 56, + "panel.AnalyticsViews": 57, + "panel.AnalyticsRoutes": 58, + "panel.AnalyticsAgents": 59, + "panel.AnalyticsSystems": 60, + "panel.AnalyticsLanguages": 61, + "panel.AnalyticsReferrers": 62, + "panel.AnalyticsRouteViews": 63, + "panel.AnalyticsAgentViews": 64, + "panel.AnalyticsForumViews": 65, + "panel.AnalyticsSystemViews": 66, + "panel.AnalyticsLanguageViews": 67, + "panel.AnalyticsReferrerViews": 68, + "panel.AnalyticsPosts": 69, + "panel.AnalyticsTopics": 70, + "panel.AnalyticsForums": 71, + "panel.Groups": 72, + "panel.GroupsEdit": 73, + "panel.GroupsEditPerms": 74, + "panel.GroupsEditSubmit": 75, + "panel.GroupsEditPermsSubmit": 76, + "panel.GroupsCreateSubmit": 77, + "panel.Backups": 78, + "panel.LogsRegs": 79, + "panel.LogsMod": 80, + "panel.Debug": 81, + "panel.Dashboard": 82, + "routes.AccountEdit": 83, + "routes.AccountEditPassword": 84, + "routes.AccountEditPasswordSubmit": 85, + "routes.AccountEditAvatarSubmit": 86, + "routes.AccountEditUsernameSubmit": 87, + "routes.AccountEditMFA": 88, + "routes.AccountEditMFASetup": 89, + "routes.AccountEditMFASetupSubmit": 90, + "routes.AccountEditMFADisableSubmit": 91, + "routes.AccountEditEmail": 92, + "routes.AccountEditEmailTokenSubmit": 93, + "routes.AccountLogins": 94, + "routes.LevelList": 95, + "routes.ViewProfile": 96, + "routes.BanUserSubmit": 97, + "routes.UnbanUser": 98, + "routes.ActivateUser": 99, + "routes.IPSearch": 100, + "routes.CreateTopicSubmit": 101, + "routes.EditTopicSubmit": 102, + "routes.DeleteTopicSubmit": 103, + "routes.StickTopicSubmit": 104, + "routes.UnstickTopicSubmit": 105, + "routes.LockTopicSubmit": 106, + "routes.UnlockTopicSubmit": 107, + "routes.MoveTopicSubmit": 108, + "routes.LikeTopicSubmit": 109, + "routes.AddAttachToTopicSubmit": 110, + "routes.RemoveAttachFromTopicSubmit": 111, + "routes.ViewTopic": 112, + "routes.CreateReplySubmit": 113, + "routes.ReplyEditSubmit": 114, + "routes.ReplyDeleteSubmit": 115, + "routes.ReplyLikeSubmit": 116, + "routes.AddAttachToReplySubmit": 117, + "routes.RemoveAttachFromReplySubmit": 118, + "routes.ProfileReplyCreateSubmit": 119, + "routes.ProfileReplyEditSubmit": 120, + "routes.ProfileReplyDeleteSubmit": 121, + "routes.PollVote": 122, + "routes.PollResults": 123, + "routes.AccountLogin": 124, + "routes.AccountRegister": 125, + "routes.AccountLogout": 126, + "routes.AccountLoginSubmit": 127, + "routes.AccountLoginMFAVerify": 128, + "routes.AccountLoginMFAVerifySubmit": 129, + "routes.AccountRegisterSubmit": 130, + "routes.DynamicRoute": 131, + "routes.UploadedFile": 132, + "routes.StaticFile": 133, + "routes.RobotsTxt": 134, + "routes.SitemapXml": 135, + "routes.BadRoute": 136, } var reverseRouteMapEnum = map[int]string{ 0: "routes.Overview", @@ -341,93 +349,97 @@ var reverseRouteMapEnum = map[int]string{ 43: "panel.ThemesMenuItemCreateSubmit", 44: "panel.ThemesMenuItemDeleteSubmit", 45: "panel.ThemesMenuItemOrderSubmit", - 46: "panel.Plugins", - 47: "panel.PluginsActivate", - 48: "panel.PluginsDeactivate", - 49: "panel.PluginsInstall", - 50: "panel.Users", - 51: "panel.UsersEdit", - 52: "panel.UsersEditSubmit", - 53: "panel.AnalyticsViews", - 54: "panel.AnalyticsRoutes", - 55: "panel.AnalyticsAgents", - 56: "panel.AnalyticsSystems", - 57: "panel.AnalyticsLanguages", - 58: "panel.AnalyticsReferrers", - 59: "panel.AnalyticsRouteViews", - 60: "panel.AnalyticsAgentViews", - 61: "panel.AnalyticsForumViews", - 62: "panel.AnalyticsSystemViews", - 63: "panel.AnalyticsLanguageViews", - 64: "panel.AnalyticsReferrerViews", - 65: "panel.AnalyticsPosts", - 66: "panel.AnalyticsTopics", - 67: "panel.AnalyticsForums", - 68: "panel.Groups", - 69: "panel.GroupsEdit", - 70: "panel.GroupsEditPerms", - 71: "panel.GroupsEditSubmit", - 72: "panel.GroupsEditPermsSubmit", - 73: "panel.GroupsCreateSubmit", - 74: "panel.Backups", - 75: "panel.LogsRegs", - 76: "panel.LogsMod", - 77: "panel.Debug", - 78: "panel.Dashboard", - 79: "routes.AccountEdit", - 80: "routes.AccountEditPassword", - 81: "routes.AccountEditPasswordSubmit", - 82: "routes.AccountEditAvatarSubmit", - 83: "routes.AccountEditUsernameSubmit", - 84: "routes.AccountEditMFA", - 85: "routes.AccountEditMFASetup", - 86: "routes.AccountEditMFASetupSubmit", - 87: "routes.AccountEditMFADisableSubmit", - 88: "routes.AccountEditEmail", - 89: "routes.AccountEditEmailTokenSubmit", - 90: "routes.AccountLogins", - 91: "routes.LevelList", - 92: "routes.ViewProfile", - 93: "routes.BanUserSubmit", - 94: "routes.UnbanUser", - 95: "routes.ActivateUser", - 96: "routes.IPSearch", - 97: "routes.CreateTopicSubmit", - 98: "routes.EditTopicSubmit", - 99: "routes.DeleteTopicSubmit", - 100: "routes.StickTopicSubmit", - 101: "routes.UnstickTopicSubmit", - 102: "routes.LockTopicSubmit", - 103: "routes.UnlockTopicSubmit", - 104: "routes.MoveTopicSubmit", - 105: "routes.LikeTopicSubmit", - 106: "routes.AddAttachToTopicSubmit", - 107: "routes.RemoveAttachFromTopicSubmit", - 108: "routes.ViewTopic", - 109: "routes.CreateReplySubmit", - 110: "routes.ReplyEditSubmit", - 111: "routes.ReplyDeleteSubmit", - 112: "routes.ReplyLikeSubmit", - 113: "routes.AddAttachToReplySubmit", - 114: "routes.RemoveAttachFromReplySubmit", - 115: "routes.ProfileReplyCreateSubmit", - 116: "routes.ProfileReplyEditSubmit", - 117: "routes.ProfileReplyDeleteSubmit", - 118: "routes.PollVote", - 119: "routes.PollResults", - 120: "routes.AccountLogin", - 121: "routes.AccountRegister", - 122: "routes.AccountLogout", - 123: "routes.AccountLoginSubmit", - 124: "routes.AccountLoginMFAVerify", - 125: "routes.AccountLoginMFAVerifySubmit", - 126: "routes.AccountRegisterSubmit", - 127: "routes.DynamicRoute", - 128: "routes.UploadedFile", - 129: "routes.StaticFile", - 130: "routes.RobotsTxt", - 131: "routes.SitemapXml", - 132: "routes.BadRoute", + 46: "panel.ThemesWidgets", + 47: "panel.ThemesWidgetsEditSubmit", + 48: "panel.ThemesWidgetsCreateSubmit", + 49: "panel.ThemesWidgetsDeleteSubmit", + 50: "panel.Plugins", + 51: "panel.PluginsActivate", + 52: "panel.PluginsDeactivate", + 53: "panel.PluginsInstall", + 54: "panel.Users", + 55: "panel.UsersEdit", + 56: "panel.UsersEditSubmit", + 57: "panel.AnalyticsViews", + 58: "panel.AnalyticsRoutes", + 59: "panel.AnalyticsAgents", + 60: "panel.AnalyticsSystems", + 61: "panel.AnalyticsLanguages", + 62: "panel.AnalyticsReferrers", + 63: "panel.AnalyticsRouteViews", + 64: "panel.AnalyticsAgentViews", + 65: "panel.AnalyticsForumViews", + 66: "panel.AnalyticsSystemViews", + 67: "panel.AnalyticsLanguageViews", + 68: "panel.AnalyticsReferrerViews", + 69: "panel.AnalyticsPosts", + 70: "panel.AnalyticsTopics", + 71: "panel.AnalyticsForums", + 72: "panel.Groups", + 73: "panel.GroupsEdit", + 74: "panel.GroupsEditPerms", + 75: "panel.GroupsEditSubmit", + 76: "panel.GroupsEditPermsSubmit", + 77: "panel.GroupsCreateSubmit", + 78: "panel.Backups", + 79: "panel.LogsRegs", + 80: "panel.LogsMod", + 81: "panel.Debug", + 82: "panel.Dashboard", + 83: "routes.AccountEdit", + 84: "routes.AccountEditPassword", + 85: "routes.AccountEditPasswordSubmit", + 86: "routes.AccountEditAvatarSubmit", + 87: "routes.AccountEditUsernameSubmit", + 88: "routes.AccountEditMFA", + 89: "routes.AccountEditMFASetup", + 90: "routes.AccountEditMFASetupSubmit", + 91: "routes.AccountEditMFADisableSubmit", + 92: "routes.AccountEditEmail", + 93: "routes.AccountEditEmailTokenSubmit", + 94: "routes.AccountLogins", + 95: "routes.LevelList", + 96: "routes.ViewProfile", + 97: "routes.BanUserSubmit", + 98: "routes.UnbanUser", + 99: "routes.ActivateUser", + 100: "routes.IPSearch", + 101: "routes.CreateTopicSubmit", + 102: "routes.EditTopicSubmit", + 103: "routes.DeleteTopicSubmit", + 104: "routes.StickTopicSubmit", + 105: "routes.UnstickTopicSubmit", + 106: "routes.LockTopicSubmit", + 107: "routes.UnlockTopicSubmit", + 108: "routes.MoveTopicSubmit", + 109: "routes.LikeTopicSubmit", + 110: "routes.AddAttachToTopicSubmit", + 111: "routes.RemoveAttachFromTopicSubmit", + 112: "routes.ViewTopic", + 113: "routes.CreateReplySubmit", + 114: "routes.ReplyEditSubmit", + 115: "routes.ReplyDeleteSubmit", + 116: "routes.ReplyLikeSubmit", + 117: "routes.AddAttachToReplySubmit", + 118: "routes.RemoveAttachFromReplySubmit", + 119: "routes.ProfileReplyCreateSubmit", + 120: "routes.ProfileReplyEditSubmit", + 121: "routes.ProfileReplyDeleteSubmit", + 122: "routes.PollVote", + 123: "routes.PollResults", + 124: "routes.AccountLogin", + 125: "routes.AccountRegister", + 126: "routes.AccountLogout", + 127: "routes.AccountLoginSubmit", + 128: "routes.AccountLoginMFAVerify", + 129: "routes.AccountLoginMFAVerifySubmit", + 130: "routes.AccountRegisterSubmit", + 131: "routes.DynamicRoute", + 132: "routes.UploadedFile", + 133: "routes.StaticFile", + 134: "routes.RobotsTxt", + 135: "routes.SitemapXml", + 136: "routes.BadRoute", } var osMapEnum = map[string]int{ "unknown": 0, @@ -716,7 +728,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { counters.GlobalViewCounter.Bump() if prefix == "/static" { - counters.RouteViewCounter.Bump(129) + counters.RouteViewCounter.Bump(133) req.URL.Path += extraData routes.StaticFile(w, req) return @@ -1230,8 +1242,35 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c counters.RouteViewCounter.Bump(45) err = panel.ThemesMenuItemOrderSubmit(w,req,user,extraData) - case "/panel/plugins/": + case "/panel/themes/widgets/": counters.RouteViewCounter.Bump(46) + err = panel.ThemesWidgets(w,req,user) + case "/panel/themes/widgets/edit/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + return err + } + + counters.RouteViewCounter.Bump(47) + err = panel.ThemesWidgetsEditSubmit(w,req,user,extraData) + case "/panel/themes/widgets/create/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + return err + } + + counters.RouteViewCounter.Bump(48) + err = panel.ThemesWidgetsCreateSubmit(w,req,user) + case "/panel/themes/widgets/delete/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + return err + } + + counters.RouteViewCounter.Bump(49) + err = panel.ThemesWidgetsDeleteSubmit(w,req,user,extraData) + case "/panel/plugins/": + counters.RouteViewCounter.Bump(50) err = panel.Plugins(w,req,user) case "/panel/plugins/activate/": err = common.NoSessionMismatch(w,req,user) @@ -1239,7 +1278,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(47) + counters.RouteViewCounter.Bump(51) err = panel.PluginsActivate(w,req,user,extraData) case "/panel/plugins/deactivate/": err = common.NoSessionMismatch(w,req,user) @@ -1247,7 +1286,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(48) + counters.RouteViewCounter.Bump(52) err = panel.PluginsDeactivate(w,req,user,extraData) case "/panel/plugins/install/": err = common.NoSessionMismatch(w,req,user) @@ -1255,13 +1294,13 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(49) + counters.RouteViewCounter.Bump(53) err = panel.PluginsInstall(w,req,user,extraData) case "/panel/users/": - counters.RouteViewCounter.Bump(50) + counters.RouteViewCounter.Bump(54) err = panel.Users(w,req,user) case "/panel/users/edit/": - counters.RouteViewCounter.Bump(51) + counters.RouteViewCounter.Bump(55) err = panel.UsersEdit(w,req,user,extraData) case "/panel/users/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1269,7 +1308,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(52) + counters.RouteViewCounter.Bump(56) err = panel.UsersEditSubmit(w,req,user,extraData) case "/panel/analytics/views/": err = common.ParseForm(w,req,user) @@ -1277,7 +1316,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(53) + counters.RouteViewCounter.Bump(57) err = panel.AnalyticsViews(w,req,user) case "/panel/analytics/routes/": err = common.ParseForm(w,req,user) @@ -1285,7 +1324,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(54) + counters.RouteViewCounter.Bump(58) err = panel.AnalyticsRoutes(w,req,user) case "/panel/analytics/agents/": err = common.ParseForm(w,req,user) @@ -1293,7 +1332,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(55) + counters.RouteViewCounter.Bump(59) err = panel.AnalyticsAgents(w,req,user) case "/panel/analytics/systems/": err = common.ParseForm(w,req,user) @@ -1301,7 +1340,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(56) + counters.RouteViewCounter.Bump(60) err = panel.AnalyticsSystems(w,req,user) case "/panel/analytics/langs/": err = common.ParseForm(w,req,user) @@ -1309,7 +1348,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(57) + counters.RouteViewCounter.Bump(61) err = panel.AnalyticsLanguages(w,req,user) case "/panel/analytics/referrers/": err = common.ParseForm(w,req,user) @@ -1317,25 +1356,25 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(58) + counters.RouteViewCounter.Bump(62) err = panel.AnalyticsReferrers(w,req,user) case "/panel/analytics/route/": - counters.RouteViewCounter.Bump(59) + counters.RouteViewCounter.Bump(63) err = panel.AnalyticsRouteViews(w,req,user,extraData) case "/panel/analytics/agent/": - counters.RouteViewCounter.Bump(60) + counters.RouteViewCounter.Bump(64) err = panel.AnalyticsAgentViews(w,req,user,extraData) case "/panel/analytics/forum/": - counters.RouteViewCounter.Bump(61) + counters.RouteViewCounter.Bump(65) err = panel.AnalyticsForumViews(w,req,user,extraData) case "/panel/analytics/system/": - counters.RouteViewCounter.Bump(62) + counters.RouteViewCounter.Bump(66) err = panel.AnalyticsSystemViews(w,req,user,extraData) case "/panel/analytics/lang/": - counters.RouteViewCounter.Bump(63) + counters.RouteViewCounter.Bump(67) err = panel.AnalyticsLanguageViews(w,req,user,extraData) case "/panel/analytics/referrer/": - counters.RouteViewCounter.Bump(64) + counters.RouteViewCounter.Bump(68) err = panel.AnalyticsReferrerViews(w,req,user,extraData) case "/panel/analytics/posts/": err = common.ParseForm(w,req,user) @@ -1343,7 +1382,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(65) + counters.RouteViewCounter.Bump(69) err = panel.AnalyticsPosts(w,req,user) case "/panel/analytics/topics/": err = common.ParseForm(w,req,user) @@ -1351,7 +1390,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(66) + counters.RouteViewCounter.Bump(70) err = panel.AnalyticsTopics(w,req,user) case "/panel/analytics/forums/": err = common.ParseForm(w,req,user) @@ -1359,16 +1398,16 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(67) + counters.RouteViewCounter.Bump(71) err = panel.AnalyticsForums(w,req,user) case "/panel/groups/": - counters.RouteViewCounter.Bump(68) + counters.RouteViewCounter.Bump(72) err = panel.Groups(w,req,user) case "/panel/groups/edit/": - counters.RouteViewCounter.Bump(69) + counters.RouteViewCounter.Bump(73) err = panel.GroupsEdit(w,req,user,extraData) case "/panel/groups/edit/perms/": - counters.RouteViewCounter.Bump(70) + counters.RouteViewCounter.Bump(74) err = panel.GroupsEditPerms(w,req,user,extraData) case "/panel/groups/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1376,7 +1415,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(71) + counters.RouteViewCounter.Bump(75) err = panel.GroupsEditSubmit(w,req,user,extraData) case "/panel/groups/edit/perms/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1384,7 +1423,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(72) + counters.RouteViewCounter.Bump(76) err = panel.GroupsEditPermsSubmit(w,req,user,extraData) case "/panel/groups/create/": err = common.NoSessionMismatch(w,req,user) @@ -1392,7 +1431,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(73) + counters.RouteViewCounter.Bump(77) err = panel.GroupsCreateSubmit(w,req,user) case "/panel/backups/": err = common.SuperAdminOnly(w,req,user) @@ -1406,13 +1445,13 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c w.Header().Del("Content-Type") w.Header().Del("Content-Encoding") } - counters.RouteViewCounter.Bump(74) + counters.RouteViewCounter.Bump(78) err = panel.Backups(w,req,user,extraData) case "/panel/logs/regs/": - counters.RouteViewCounter.Bump(75) + counters.RouteViewCounter.Bump(79) err = panel.LogsRegs(w,req,user) case "/panel/logs/mod/": - counters.RouteViewCounter.Bump(76) + counters.RouteViewCounter.Bump(80) err = panel.LogsMod(w,req,user) case "/panel/debug/": err = common.AdminOnly(w,req,user) @@ -1420,10 +1459,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(77) + counters.RouteViewCounter.Bump(81) err = panel.Debug(w,req,user) default: - counters.RouteViewCounter.Bump(78) + counters.RouteViewCounter.Bump(82) err = panel.Dashboard(w,req,user) } case "/user": @@ -1434,7 +1473,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(79) + counters.RouteViewCounter.Bump(83) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1446,7 +1485,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(80) + counters.RouteViewCounter.Bump(84) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1463,7 +1502,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(81) + counters.RouteViewCounter.Bump(85) err = routes.AccountEditPasswordSubmit(w,req,user) case "/user/edit/avatar/submit/": err = common.MemberOnly(w,req,user) @@ -1480,7 +1519,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(82) + counters.RouteViewCounter.Bump(86) err = routes.AccountEditAvatarSubmit(w,req,user) case "/user/edit/username/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1493,7 +1532,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(83) + counters.RouteViewCounter.Bump(87) err = routes.AccountEditUsernameSubmit(w,req,user) case "/user/edit/mfa/": err = common.MemberOnly(w,req,user) @@ -1501,7 +1540,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(84) + counters.RouteViewCounter.Bump(88) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1513,7 +1552,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(85) + counters.RouteViewCounter.Bump(89) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1530,7 +1569,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(86) + counters.RouteViewCounter.Bump(90) err = routes.AccountEditMFASetupSubmit(w,req,user) case "/user/edit/mfa/disable/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1543,7 +1582,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(87) + counters.RouteViewCounter.Bump(91) err = routes.AccountEditMFADisableSubmit(w,req,user) case "/user/edit/email/": err = common.MemberOnly(w,req,user) @@ -1551,7 +1590,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(88) + counters.RouteViewCounter.Bump(92) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1568,7 +1607,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(89) + counters.RouteViewCounter.Bump(93) err = routes.AccountEditEmailTokenSubmit(w,req,user,extraData) case "/user/edit/logins/": err = common.MemberOnly(w,req,user) @@ -1576,7 +1615,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(90) + counters.RouteViewCounter.Bump(94) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1588,7 +1627,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(91) + counters.RouteViewCounter.Bump(95) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1596,7 +1635,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c err = routes.LevelList(w,req,user,head) default: req.URL.Path += extraData - counters.RouteViewCounter.Bump(92) + counters.RouteViewCounter.Bump(96) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1616,7 +1655,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(93) + counters.RouteViewCounter.Bump(97) err = routes.BanUserSubmit(w,req,user,extraData) case "/users/unban/": err = common.NoSessionMismatch(w,req,user) @@ -1629,7 +1668,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(94) + counters.RouteViewCounter.Bump(98) err = routes.UnbanUser(w,req,user,extraData) case "/users/activate/": err = common.NoSessionMismatch(w,req,user) @@ -1642,7 +1681,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(95) + counters.RouteViewCounter.Bump(99) err = routes.ActivateUser(w,req,user,extraData) case "/users/ips/": err = common.MemberOnly(w,req,user) @@ -1650,7 +1689,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(96) + counters.RouteViewCounter.Bump(100) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1674,7 +1713,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(97) + counters.RouteViewCounter.Bump(101) err = routes.CreateTopicSubmit(w,req,user) case "/topic/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1687,7 +1726,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(98) + counters.RouteViewCounter.Bump(102) err = routes.EditTopicSubmit(w,req,user,extraData) case "/topic/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1701,7 +1740,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } req.URL.Path += extraData - counters.RouteViewCounter.Bump(99) + counters.RouteViewCounter.Bump(103) err = routes.DeleteTopicSubmit(w,req,user) case "/topic/stick/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1714,7 +1753,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(100) + counters.RouteViewCounter.Bump(104) err = routes.StickTopicSubmit(w,req,user,extraData) case "/topic/unstick/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1727,7 +1766,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(101) + counters.RouteViewCounter.Bump(105) err = routes.UnstickTopicSubmit(w,req,user,extraData) case "/topic/lock/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1741,7 +1780,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } req.URL.Path += extraData - counters.RouteViewCounter.Bump(102) + counters.RouteViewCounter.Bump(106) err = routes.LockTopicSubmit(w,req,user) case "/topic/unlock/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1754,7 +1793,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(103) + counters.RouteViewCounter.Bump(107) err = routes.UnlockTopicSubmit(w,req,user,extraData) case "/topic/move/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1767,7 +1806,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(104) + counters.RouteViewCounter.Bump(108) err = routes.MoveTopicSubmit(w,req,user,extraData) case "/topic/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1780,7 +1819,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(105) + counters.RouteViewCounter.Bump(109) err = routes.LikeTopicSubmit(w,req,user,extraData) case "/topic/attach/add/submit/": err = common.MemberOnly(w,req,user) @@ -1797,7 +1836,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(106) + counters.RouteViewCounter.Bump(110) err = routes.AddAttachToTopicSubmit(w,req,user,extraData) case "/topic/attach/remove/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1810,10 +1849,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(107) + counters.RouteViewCounter.Bump(111) err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData) default: - counters.RouteViewCounter.Bump(108) + counters.RouteViewCounter.Bump(112) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1837,7 +1876,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(109) + counters.RouteViewCounter.Bump(113) err = routes.CreateReplySubmit(w,req,user) case "/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1850,7 +1889,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(110) + counters.RouteViewCounter.Bump(114) err = routes.ReplyEditSubmit(w,req,user,extraData) case "/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1863,7 +1902,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(111) + counters.RouteViewCounter.Bump(115) err = routes.ReplyDeleteSubmit(w,req,user,extraData) case "/reply/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1876,7 +1915,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(112) + counters.RouteViewCounter.Bump(116) err = routes.ReplyLikeSubmit(w,req,user,extraData) case "/reply/attach/add/submit/": err = common.MemberOnly(w,req,user) @@ -1893,7 +1932,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(113) + counters.RouteViewCounter.Bump(117) err = routes.AddAttachToReplySubmit(w,req,user,extraData) case "/reply/attach/remove/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1906,7 +1945,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(114) + counters.RouteViewCounter.Bump(118) err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData) } case "/profile": @@ -1922,7 +1961,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(115) + counters.RouteViewCounter.Bump(119) err = routes.ProfileReplyCreateSubmit(w,req,user) case "/profile/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1935,7 +1974,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(116) + counters.RouteViewCounter.Bump(120) err = routes.ProfileReplyEditSubmit(w,req,user,extraData) case "/profile/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1948,7 +1987,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(117) + counters.RouteViewCounter.Bump(121) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) } case "/poll": @@ -1964,23 +2003,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(118) + counters.RouteViewCounter.Bump(122) err = routes.PollVote(w,req,user,extraData) case "/poll/results/": - counters.RouteViewCounter.Bump(119) + counters.RouteViewCounter.Bump(123) err = routes.PollResults(w,req,user,extraData) } case "/accounts": switch(req.URL.Path) { case "/accounts/login/": - counters.RouteViewCounter.Bump(120) + counters.RouteViewCounter.Bump(124) head, err := common.UserCheck(w,req,&user) if err != nil { return err } err = routes.AccountLogin(w,req,user,head) case "/accounts/create/": - counters.RouteViewCounter.Bump(121) + counters.RouteViewCounter.Bump(125) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1997,7 +2036,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(122) + counters.RouteViewCounter.Bump(126) err = routes.AccountLogout(w,req,user) case "/accounts/login/submit/": err = common.ParseForm(w,req,user) @@ -2005,10 +2044,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(123) + counters.RouteViewCounter.Bump(127) err = routes.AccountLoginSubmit(w,req,user) case "/accounts/mfa_verify/": - counters.RouteViewCounter.Bump(124) + counters.RouteViewCounter.Bump(128) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -2020,7 +2059,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(125) + counters.RouteViewCounter.Bump(129) err = routes.AccountLoginMFAVerifySubmit(w,req,user) case "/accounts/create/submit/": err = common.ParseForm(w,req,user) @@ -2028,7 +2067,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(126) + counters.RouteViewCounter.Bump(130) err = routes.AccountRegisterSubmit(w,req,user) } /*case "/sitemaps": // TODO: Count these views @@ -2044,7 +2083,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c w.Header().Del("Content-Type") w.Header().Del("Content-Encoding") } - counters.RouteViewCounter.Bump(128) + counters.RouteViewCounter.Bump(132) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? r.UploadHandler(w,req) // TODO: Count these views @@ -2054,10 +2093,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c // TODO: Add support for favicons and robots.txt files switch(extraData) { case "robots.txt": - counters.RouteViewCounter.Bump(130) + counters.RouteViewCounter.Bump(134) return routes.RobotsTxt(w,req) /*case "sitemap.xml": - counters.RouteViewCounter.Bump(131) + counters.RouteViewCounter.Bump(135) return routes.SitemapXml(w,req)*/ } return common.NotFound(w,req,nil) @@ -2068,7 +2107,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c r.RUnlock() if ok { - counters.RouteViewCounter.Bump(127) // TODO: Be more specific about *which* dynamic route it is + counters.RouteViewCounter.Bump(131) // TODO: Be more specific about *which* dynamic route it is req.URL.Path += extraData return handle(w,req,user) } @@ -2079,7 +2118,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } else { r.DumpRequest(req,"Bad Route") } - counters.RouteViewCounter.Bump(132) + counters.RouteViewCounter.Bump(136) return common.NotFound(w,req,nil) } return err diff --git a/gen_tables.go b/gen_tables.go index e30a6664..44475f3c 100644 --- a/gen_tables.go +++ b/gen_tables.go @@ -2,23 +2,24 @@ package main var dbTablePrimaryKeys = map[string]string{ - "users":"uid", "users_groups":"gid", - "users_groups_scheduler":"uid", - "polls":"pollID", - "registration_logs":"rlid", - "activity_stream":"asid", + "users_avatar_queue":"uid", + "word_filters":"wfid", + "menus":"mid", "login_logs":"lid", - "users_2fa_keys":"uid", + "polls":"pollID", + "activity_stream":"asid", + "pages":"pid", + "forums":"fid", "topics":"tid", "replies":"rid", "attachments":"attachID", "revisions":"reviseID", - "users_avatar_queue":"uid", - "forums":"fid", + "users_2fa_keys":"uid", + "users_groups_scheduler":"uid", "menu_items":"miid", + "registration_logs":"rlid", + "users":"uid", "users_replies":"rid", - "word_filters":"wfid", - "menus":"mid", - "pages":"pid", + "widgets":"wid", } diff --git a/install/mysql.go b/install/mysql.go index 3c139f5e..b3a1cea8 100644 --- a/install/mysql.go +++ b/install/mysql.go @@ -1,7 +1,7 @@ /* * * Gosora MySQL Interface -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package install diff --git a/install/pgsql.go b/install/pgsql.go index f9c6ea79..8d429ea1 100644 --- a/install/pgsql.go +++ b/install/pgsql.go @@ -2,7 +2,7 @@ * * Gosora PostgreSQL Interface * Under heavy development -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package install diff --git a/langs/english.json b/langs/english.json index 163465b6..d52fdc54 100644 --- a/langs/english.json +++ b/langs/english.json @@ -1,5 +1,6 @@ { "Name": "english", + "IsoCode":"en", "Levels": { "Level": "Level {0}", @@ -154,6 +155,7 @@ "panel_themes":"Theme Manager", "panel_themes_menus":"Menu Manager", "panel_themes_menus_edit":"Menu Editor", + "panel_themes_widgets":"Widget Manager", "panel_backups":"Backups", "panel_registration_logs":"Registration Logs", "panel_mod_logs":"Mod Action Logs", @@ -664,6 +666,10 @@ "footer_made_with_love":"Made with love by Azareal", "footer_theme_selector_aria":"Change the site's appearance", + "widget.online_name":"Online Users", + "widget.online_none_online":"No one is online.", + "widget.online_some_online":"There are %d users online.", + "option_yes":"Yes", "option_no":"No", @@ -895,6 +901,23 @@ "panel_themes_menus_edit_update_button":"Update", "panel_themes_menus_create_button":"Create", + "panel_themes_widgets_head":"Widgets", + "panel_themes_widgets_disabled":"disabled", + "panel_themes_widgets_new":"New Widget", + "panel_themes_widgets_type":"Type", + "panel_themes_widgets_type_about":"About", + "panel_themes_widgets_type_simple":"Simple", + "panel_themes_widgets_type_wol":"Online Users", + "panel_themes_widgets_type_wol_context":"Online User Context", + "panel_themes_widgets_type_search_and_filter":"Search & Filter", + "panel_themes_widgets_enabled":"Enabled", + "panel_themes_widgets_location":"Location", + "panel_themes_widgets_name":"Name", + "panel_themes_widgets_body":"Body", + "panel_themes_widgets_raw_body":"Body", + "panel_themes_widgets_save":"Save", + "panel_themes_widgets_delete":"Delete", + "panel_settings_head":"Settings", "panel_setting_head":"Edit Setting", "panel_setting_name":"Setting Name", diff --git a/main.go b/main.go index 23ed1768..72296bb8 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ * Copyright Azareal 2016 - 2019 * */ +// Package main contains the main initialisation logic for Gosora package main // import "github.com/Azareal/Gosora" import ( @@ -78,6 +79,7 @@ func afterDBInit() (err error) { } log.Print("Initialising the widgets") + common.Widgets = common.NewDefaultWidgetStore() err = common.InitWidgets() if err != nil { return errors.WithStack(err) diff --git a/misc_test.go b/misc_test.go index f9be1e50..c6b6514c 100644 --- a/misc_test.go +++ b/misc_test.go @@ -750,6 +750,7 @@ func TestReplyStore(t *testing.T) { expectNilErr(t, err) expect(t, topic.PostCount == 3, fmt.Sprintf("TID #1's post count should be three, not %d", topic.PostCount)) + // TODO: Expand upon this rid, err = common.Rstore.Create(topic, "hiii", "::1", 1) expectNilErr(t, err) replyTest(rid, topic.ID, 1, "hiii", "::1") @@ -1014,6 +1015,7 @@ func TestWordFilters(t *testing.T) { // TODO: Add deletion tests } +// TODO: Expand upon the valid characters which can go in URLs? func TestSlugs(t *testing.T) { var res string var msgList = &MEPairList{nil} @@ -1050,6 +1052,64 @@ func TestSlugs(t *testing.T) { } } +func TestWidgets(t *testing.T) { + _, err := common.Widgets.Get(1) + recordMustNotExist(t, err, "There shouldn't be any widgets by default") + widgets := common.Docks.RightSidebar + expect(t, len(widgets) == 0, fmt.Sprintf("RightSidebar should have 0 items, not %d", len(widgets))) + + widget := &common.Widget{Position: 0, Side: "rightSidebar", Type: "simple", Enabled: true, Location: "global"} + ewidget := &common.WidgetEdit{widget, map[string]string{"Name": "Test", "Text": "Testing"}} + err = ewidget.Create() + expectNilErr(t, err) + + // TODO: Do a test for the widget body + widget2, err := common.Widgets.Get(1) + expectNilErr(t, err) + expect(t, widget2.Position == widget.Position, "wrong position") + expect(t, widget2.Side == widget.Side, "wrong side") + expect(t, widget2.Type == widget.Type, "wrong type") + expect(t, widget2.Enabled, "not enabled") + expect(t, widget2.Location == widget.Location, "wrong location") + + widgets = common.Docks.RightSidebar + expect(t, len(widgets) == 1, fmt.Sprintf("RightSidebar should have 1 item, not %d", len(widgets))) + expect(t, widgets[0].Position == widget.Position, "wrong position") + expect(t, widgets[0].Side == widget.Side, "wrong side") + expect(t, widgets[0].Type == widget.Type, "wrong type") + expect(t, widgets[0].Enabled, "not enabled") + expect(t, widgets[0].Location == widget.Location, "wrong location") + + widget2.Enabled = false + ewidget = &common.WidgetEdit{widget2, map[string]string{"Name": "Test", "Text": "Testing"}} + err = ewidget.Commit() + expectNilErr(t, err) + + widget2, err = common.Widgets.Get(1) + expectNilErr(t, err) + expect(t, widget2.Position == widget.Position, "wrong position") + expect(t, widget2.Side == widget.Side, "wrong side") + expect(t, widget2.Type == widget.Type, "wrong type") + expect(t, !widget2.Enabled, "not enabled") + expect(t, widget2.Location == widget.Location, "wrong location") + + widgets = common.Docks.RightSidebar + expect(t, len(widgets) == 1, fmt.Sprintf("RightSidebar should have 1 item, not %d", len(widgets))) + expect(t, widgets[0].Position == widget.Position, "wrong position") + expect(t, widgets[0].Side == widget.Side, "wrong side") + expect(t, widgets[0].Type == widget.Type, "wrong type") + expect(t, !widgets[0].Enabled, "not enabled") + expect(t, widgets[0].Location == widget.Location, "wrong location") + + err = widget2.Delete() + expectNilErr(t, err) + + _, err = common.Widgets.Get(1) + recordMustNotExist(t, err, "There shouldn't be any widgets anymore") + widgets = common.Docks.RightSidebar + expect(t, len(widgets) == 0, fmt.Sprintf("RightSidebar should have 0 items, not %d", len(widgets))) +} + func TestAuth(t *testing.T) { // bcrypt likes doing stupid things, so this test will probably fail realPassword := "Madame Cassandra's Mystic Orb" diff --git a/patcher/patches.go b/patcher/patches.go index 9f82e3ba..525f1664 100644 --- a/patcher/patches.go +++ b/patcher/patches.go @@ -24,6 +24,7 @@ func init() { addPatch(10, patch10) addPatch(11, patch11) addPatch(12, patch12) + addPatch(13, patch13) } func patch0(scanner *bufio.Scanner) (err error) { @@ -392,11 +393,11 @@ var acc = qgen.NewAcc var itoa = strconv.Itoa func patch10(scanner *bufio.Scanner) error { - err := execStmt(qgen.Builder.AddColumn("topics", tblColumn{"attachCount", "int", 0, false, false, "0"})) + err := execStmt(qgen.Builder.AddColumn("topics", tblColumn{"attachCount", "int", 0, false, false, "0"}, nil)) if err != nil { return err } - err = execStmt(qgen.Builder.AddColumn("topics", tblColumn{"lastReplyID", "int", 0, false, false, "0"})) + err = execStmt(qgen.Builder.AddColumn("topics", tblColumn{"lastReplyID", "int", 0, false, false, "0"}, nil)) if err != nil { return err } @@ -432,7 +433,7 @@ func patch10(scanner *bufio.Scanner) error { } func patch11(scanner *bufio.Scanner) error { - err := execStmt(qgen.Builder.AddColumn("replies", tblColumn{"attachCount", "int", 0, false, false, "0"})) + err := execStmt(qgen.Builder.AddColumn("replies", tblColumn{"attachCount", "int", 0, false, false, "0"}, nil)) if err != nil { return err } @@ -504,3 +505,12 @@ func patch12(scanner *bufio.Scanner) error { } return nil } + +func patch13(scanner *bufio.Scanner) error { + err := execStmt(qgen.Builder.AddColumn("widgets", tblColumn{"wid", "int", 0, false, true, ""}, &tblKey{"wid", "primary"})) + if err != nil { + return err + } + + return nil +} diff --git a/pgsql.go b/pgsql.go index 6e372137..eca25b34 100644 --- a/pgsql.go +++ b/pgsql.go @@ -1,6 +1,6 @@ // +build pgsql -/* Copyright Azareal 2016 - 2018 */ +/* Copyright Azareal 2016 - 2019 */ /* Super experimental and incomplete. DON'T USE IT YET! */ package main diff --git a/public/global.js b/public/global.js index c202243b..1db61a12 100644 --- a/public/global.js +++ b/public/global.js @@ -23,8 +23,7 @@ function ajaxError(xhr,status,errstr) { console.trace(); } -function postLink(event) -{ +function postLink(event) { event.preventDefault(); let formAction = $(event.target).closest('a').attr("href"); $.ajax({ url: formAction, type: "POST", dataType: "json", error: ajaxError, data: {js: "1"} }); @@ -115,9 +114,7 @@ function loadAlerts(menuAlerts) { } alertList = []; alertMapping = {}; - for(var i in data.msgs) { - addAlert(data.msgs[i]); - } + for(var i in data.msgs) addAlert(data.msgs[i]); console.log("data.msgCount:",data.msgCount) alertCount = data.msgCount; updateAlertList(menuAlerts) @@ -271,8 +268,13 @@ function runWebSockets() { let msgblocks = SplitN(message," ",3); if(msgblocks.length < 3) continue; if(message.startsWith("set ")) { + let oldInnerHTML = document.querySelector(msgblocks[1]).innerHTML; + if(msgblocks[2]==oldInnerHTML) continue; document.querySelector(msgblocks[1]).innerHTML = msgblocks[2]; } else if(message.startsWith("set-class ")) { + // Fix to stop the inspector from getting all jittery + let oldClassName = document.querySelector(msgblocks[1]).className; + if(msgblocks[2]==oldClassName) continue; document.querySelector(msgblocks[1]).className = msgblocks[2]; } } diff --git a/public/init.js b/public/init.js index 1f99ffc7..a4f19adf 100644 --- a/public/init.js +++ b/public/init.js @@ -94,8 +94,12 @@ function DoNothingButPassBack(item) { return item; } -function fetchPhrases() { - fetch("/api/phrases/?query=status,topic_list,alerts") +function initPhrases() { + fetchPhrases("status,topic_list,alerts") +} + +function fetchPhrases(plist) { + fetch("/api/phrases/?query="+plist) .then((resp) => resp.json()) .then((data) => { console.log("loaded phrase endpoint data"); @@ -103,9 +107,7 @@ function fetchPhrases() { Object.keys(tmplInits).forEach((key) => { let phrases = []; let tmplInit = tmplInits[key]; - for(let phraseName of tmplInit) { - phrases.push(data[phraseName]); - } + for(let phraseName of tmplInit) phrases.push(data[phraseName]); console.log("Adding phrases"); console.log("key:",key); console.log("phrases:",phrases); @@ -115,9 +117,7 @@ function fetchPhrases() { let prefixes = {}; Object.keys(data).forEach((key) => { let prefix = key.split(".")[0]; - if(prefixes[prefix]===undefined) { - prefixes[prefix] = {}; - } + if(prefixes[prefix]===undefined) prefixes[prefix] = {}; prefixes[prefix][key] = data[key]; }); Object.keys(prefixes).forEach((prefix) => { @@ -146,7 +146,7 @@ function fetchPhrases() { loadScript("template_topics_topic.js", () => { console.log("Loaded template_topics_topic.js"); toLoad--; - if(toLoad===0) fetchPhrases(); + if(toLoad===0) initPhrases(); }); } else { me = {User:{ID:0,Session:""},Site:{"MaxRequestSize":0}}; diff --git a/public/widgets.js b/public/widgets.js new file mode 100644 index 00000000..3d4fa813 --- /dev/null +++ b/public/widgets.js @@ -0,0 +1,66 @@ +"use strict"; + +$(document).ready(() => { + let clickHandle = function(event){ + console.log("in clickHandle") + event.preventDefault(); + let eparent = $(this).closest(".editable_parent"); + eparent.find(".hide_on_block_edit").addClass("edit_opened"); + eparent.find(".show_on_block_edit").addClass("edit_opened"); + eparent.addClass("in_edit"); + + eparent.find(".widget_save").click(() => { + eparent.find(".hide_on_block_edit").removeClass("edit_opened"); + eparent.find(".show_on_block_edit").removeClass("edit_opened"); + eparent.removeClass("in_edit"); + }); + + eparent.find(".widget_delete").click(function(event) { + event.preventDefault(); + eparent.remove(); + let formData = new URLSearchParams(); + formData.append("session",me.User.Session); + let req = new XMLHttpRequest(); + let target = this.closest("a").getAttribute("href"); + req.open("POST",target,true); + req.send(formData); + }); + }; + + $(".widget_item a").click(clickHandle); + + let changeHandle = function(event){ + let wtype = this.options[this.selectedIndex].value; + let typeBlock = this.closest(".widget_edit").querySelector(".wtypes"); + typeBlock.className = "wtypes wtype_"+wtype; + }; + + $(".wtype_sel").change(changeHandle); + + $(".widget_new a").click(function(event){ + console.log("clicked widget_new a") + let widgetList = this.closest(".panel_widgets"); + let widgetNew = this.closest(".widget_new"); + let widgetTmpl = document.getElementById("widgetTmpl").querySelector(".widget_item"); + let node = widgetTmpl.cloneNode(true); + node.querySelector(".wside").value = this.getAttribute("data-dock"); + widgetList.insertBefore(node,widgetNew); + $(".widget_item a").unbind("click"); + $(".widget_item a").click(clickHandle); + $(".wtype_sel").unbind("change"); + $(".wtype_sel").change(changeHandle); + }); + + $(".widget_save").click(function(event){ + console.log("in .widget_save") + event.preventDefault(); + event.stopPropagation(); + let pform = this.closest("form"); + let data = new URLSearchParams(); + for (const pair of new FormData(pform)) data.append(pair[0], pair[1]); + data.append("session",me.User.Session); + var req = new XMLHttpRequest(); + req.open("POST", pform.getAttribute("action")); + req.send(data); + }); +}); \ No newline at end of file diff --git a/query_gen/builder.go b/query_gen/builder.go index 4e298b53..0be74b44 100644 --- a/query_gen/builder.go +++ b/query_gen/builder.go @@ -108,8 +108,8 @@ func (build *builder) CreateTable(table string, charset string, collation string return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys)) } -func (build *builder) AddColumn(table string, column DBTableColumn) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.AddColumn("", table, column)) +func (build *builder) AddColumn(table string, column DBTableColumn, key *DBTableKey) (stmt *sql.Stmt, err error) { + return build.prepare(build.adapter.AddColumn("", table, column, key)) } func (build *builder) AddIndex(table string, iname string, colname string) (stmt *sql.Stmt, err error) { diff --git a/query_gen/mssql.go b/query_gen/mssql.go index e336aa8e..2c9453f5 100644 --- a/query_gen/mssql.go +++ b/query_gen/mssql.go @@ -135,7 +135,8 @@ func (adapter *MssqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum } // TODO: Test this, not sure if some things work -func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { +// TODO: Add support for keys +func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn,key *DBTableKey) (string, error) { if table == "" { return "", errors.New("You need a name for this table") } diff --git a/query_gen/mysql.go b/query_gen/mysql.go index 03c2b575..387cccf8 100644 --- a/query_gen/mysql.go +++ b/query_gen/mysql.go @@ -173,13 +173,23 @@ func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum // TODO: Support AFTER column // TODO: Test to make sure everything works here -func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { +func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) { if table == "" { return "", errors.New("You need a name for this table") } column, size, end := adapter.parseColumn(column) - querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + ";" + querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + + if key != nil { + querystr += " " + key.Type + if key.Type != "unique" { + querystr += " key" + } else if key.Type == "primary" { + querystr += " first" + } + } + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator adapter.pushStatement(name, "add-column", querystr) return querystr, nil diff --git a/query_gen/pgsql.go b/query_gen/pgsql.go index a6641c1d..57c00c85 100644 --- a/query_gen/pgsql.go +++ b/query_gen/pgsql.go @@ -113,7 +113,7 @@ func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset stri } // TODO: Implement this -func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { +func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn,key *DBTableKey) (string, error) { if table == "" { return "", errors.New("You need a name for this table") } diff --git a/query_gen/querygen.go b/query_gen/querygen.go index 37fdfd67..b1f28ff3 100644 --- a/query_gen/querygen.go +++ b/query_gen/querygen.go @@ -108,7 +108,7 @@ type Adapter interface { CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) // TODO: Some way to add indices and keys // TODO: Test this - AddColumn(name string, table string, column DBTableColumn) (string, error) + AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) AddIndex(name string, table string, iname string, colname string) (string, error) SimpleInsert(name string, table string, columns string, fields string) (string, error) SimpleUpdate(up *updatePrebuilder) (string, error) diff --git a/query_gen/utils.go b/query_gen/utils.go index e6cb5197..52309279 100644 --- a/query_gen/utils.go +++ b/query_gen/utils.go @@ -2,7 +2,7 @@ * * Query Generator Library * WIP Under Construction -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package qgen diff --git a/router_gen/routes.go b/router_gen/routes.go index 30fcdbc3..cf3c17c1 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -177,6 +177,12 @@ func panelRoutes() *RouteGroup { Action("panel.ThemesMenuItemDeleteSubmit", "/panel/themes/menus/item/delete/submit/", "extraData"), Action("panel.ThemesMenuItemOrderSubmit", "/panel/themes/menus/item/order/edit/submit/", "extraData"), + View("panel.ThemesWidgets", "/panel/themes/widgets/"), + //View("panel.ThemesWidgetsEdit", "/panel/themes/widgets/edit/", "extraData"), + Action("panel.ThemesWidgetsEditSubmit", "/panel/themes/widgets/edit/submit/", "extraData"), + Action("panel.ThemesWidgetsCreateSubmit", "/panel/themes/widgets/create/submit/"), + Action("panel.ThemesWidgetsDeleteSubmit", "/panel/themes/widgets/delete/submit/", "extraData"), + View("panel.Plugins", "/panel/plugins/"), Action("panel.PluginsActivate", "/panel/plugins/activate/", "extraData"), Action("panel.PluginsDeactivate", "/panel/plugins/deactivate/", "extraData"), diff --git a/routes.go b/routes.go index 697b1958..31eea75b 100644 --- a/routes.go +++ b/routes.go @@ -1,7 +1,7 @@ /* * * Gosora Route Handlers -* Copyright Azareal 2016 - 2018 +* Copyright Azareal 2016 - 2019 * */ package main diff --git a/routes/panel/themes.go b/routes/panel/themes.go index 825aaedf..b9c7fafa 100644 --- a/routes/panel/themes.go +++ b/routes/panel/themes.go @@ -2,6 +2,9 @@ package panel import ( "database/sql" + "encoding/json" + "errors" + "fmt" "net/http" "strconv" "strings" @@ -330,3 +333,155 @@ func ThemesMenuItemOrderSubmit(w http.ResponseWriter, r *http.Request, user comm return successRedirect("/panel/themes/menus/edit/"+strconv.Itoa(mid), w, r, isJs) } + +func ThemesWidgets(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + basePage, ferr := buildBasePage(w, r, &user, "themes_widgets", "themes") + if ferr != nil { + return ferr + } + if !user.Perms.ManageThemes { + return common.NoPermissions(w, r, user) + } + basePage.Header.AddScript("widgets.js") + + var docks = make(map[string][]common.WidgetEdit) + for _, name := range common.GetDockList() { + var widgets []common.WidgetEdit + for _, widget := range common.GetDock(name) { + var data = make(map[string]string) + err := json.Unmarshal([]byte(widget.RawBody), &data) + if err != nil { + return common.InternalError(err, w, r) + } + widgets = append(widgets, common.WidgetEdit{widget, data}) + } + docks[name] = widgets + } + + pi := common.PanelWidgetListPage{basePage, docks, common.WidgetEdit{&common.Widget{ID: 0, Type: "simple"}, make(map[string]string)}} + return renderTemplate("panel_themes_widgets", w, r, user, &pi) +} + +func widgetsParseInputs(r *http.Request, widget *common.Widget) (*common.WidgetEdit, error) { + var data = make(map[string]string) + widget.Enabled = (r.FormValue("wenabled") == "1") + widget.Location = r.FormValue("wlocation") + if widget.Location == "" { + return nil, errors.New("You need to specify a location for this widget.") + } + widget.Side = r.FormValue("wside") + if !common.HasDock(widget.Side) { + return nil, errors.New("The widget dock you specified doesn't exist.") + } + + var wtype = r.FormValue("wtype") + switch wtype { + case "simple", "about": + data["Name"] = r.FormValue("wname") + if data["Name"] == "" { + return nil, errors.New("You need to specify a title for this widget.") + } + data["Text"] = r.FormValue("wtext") + if data["Text"] == "" { + return nil, errors.New("You need to fill in the body for this widget.") + } + widget.Type = wtype // ? - Are we sure we should be directly assigning user provided data even if it's validated? + case "wol", "search_and_filter": + widget.Type = wtype // ? - Are we sure we should be directly assigning user provided data even if it's validated? + default: + return nil, errors.New("Unknown widget type") + } + + return &common.WidgetEdit{widget, data}, nil +} + +// ThemesWidgetsEditSubmit is an action which is triggered when someone sends an update request for a widget +func ThemesWidgetsEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, swid string) common.RouteError { + fmt.Println("in ThemesWidgetsEditSubmit") + _, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + isJs := (r.PostFormValue("js") == "1") + if !user.Perms.ManageThemes { + return common.NoPermissionsJSQ(w, r, user, isJs) + } + + wid, err := strconv.Atoi(swid) + if err != nil { + return common.LocalErrorJSQ(phrases.GetErrorPhrase("id_must_be_integer"), w, r, user, isJs) + } + + widget, err := common.Widgets.Get(wid) + if err == sql.ErrNoRows { + return common.NotFoundJSQ(w, r, nil, isJs) + } else if err != nil { + return common.InternalErrorJSQ(err, w, r, isJs) + } + + ewidget, err := widgetsParseInputs(r, widget.Copy()) + if err != nil { + return common.LocalErrorJSQ(err.Error(), w, r, user, isJs) + } + + err = ewidget.Commit() + if err != nil { + return common.InternalErrorJSQ(err, w, r, isJs) + } + + return successRedirect("/panel/themes/widgets/", w, r, isJs) +} + +// ThemesWidgetsCreateSubmit is an action which is triggered when someone sends a create request for a widget +func ThemesWidgetsCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + fmt.Println("in ThemesWidgetsCreateSubmit") + isJs := (r.PostFormValue("js") == "1") + _, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + if !user.Perms.ManageThemes { + return common.NoPermissionsJSQ(w, r, user, isJs) + } + + ewidget, err := widgetsParseInputs(r, &common.Widget{}) + if err != nil { + return common.LocalErrorJSQ(err.Error(), w, r, user, isJs) + } + + err = ewidget.Create() + if err != nil { + return common.InternalErrorJSQ(err, w, r, isJs) + } + + return successRedirect("/panel/themes/widgets/", w, r, isJs) +} + +func ThemesWidgetsDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.User, swid string) common.RouteError { + _, ferr := common.SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr + } + isJs := (r.PostFormValue("js") == "1") + if !user.Perms.ManageThemes { + return common.NoPermissionsJSQ(w, r, user, isJs) + } + + wid, err := strconv.Atoi(swid) + if err != nil { + return common.LocalErrorJSQ(phrases.GetErrorPhrase("id_must_be_integer"), w, r, user, isJs) + } + widget, err := common.Widgets.Get(wid) + if err == sql.ErrNoRows { + return common.NotFound(w, r, nil) + } else if err != nil { + return common.InternalError(err, w, r) + } + + err = widget.Delete() + if err != nil { + return common.InternalError(err, w, r) + } + + return successRedirect("/panel/themes/widgets/", w, r, isJs) +} diff --git a/routes/topic.go b/routes/topic.go index 6a45a4da..22a5f9ee 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -64,7 +64,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header return common.NoPermissions(w, r, user) } header.Title = topic.Title - header.Zone = "view_topic" header.Path = common.BuildTopicURL(common.NameToSlug(topic.Title), topic.ID) // TODO: Cache ContentHTML when possible? @@ -253,6 +252,9 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header } } + header.Zone = "view_topic" + header.ZoneID = topic.ID + header.ZoneData = topic rerr := renderTemplate("topic", w, r, header, tpage) counters.TopicViewCounter.Bump(topic.ID) // TODO: Move this into the router? counters.ForumViewCounter.Bump(topic.ParentID) diff --git a/schema/mssql/query_widgets.sql b/schema/mssql/query_widgets.sql index 11d42d9c..63402a41 100644 --- a/schema/mssql/query_widgets.sql +++ b/schema/mssql/query_widgets.sql @@ -1,8 +1,10 @@ CREATE TABLE [widgets] ( + [wid] int not null IDENTITY, [position] int not null, [side] nvarchar (100) not null, [type] nvarchar (100) not null, [active] bit DEFAULT 0 not null, [location] nvarchar (100) not null, - [data] nvarchar (MAX) DEFAULT '' not null + [data] nvarchar (MAX) DEFAULT '' not null, + primary key([wid]) ); \ No newline at end of file diff --git a/schema/mysql/query_widgets.sql b/schema/mysql/query_widgets.sql index e2d1e980..af1d345e 100644 --- a/schema/mysql/query_widgets.sql +++ b/schema/mysql/query_widgets.sql @@ -1,8 +1,10 @@ CREATE TABLE `widgets` ( + `wid` int not null AUTO_INCREMENT, `position` int not null, `side` varchar(100) not null, `type` varchar(100) not null, `active` boolean DEFAULT 0 not null, `location` varchar(100) not null, - `data` text not null + `data` text not null, + primary key(`wid`) ); \ No newline at end of file diff --git a/schema/pgsql/query_widgets.sql b/schema/pgsql/query_widgets.sql index 9b110701..87affb37 100644 --- a/schema/pgsql/query_widgets.sql +++ b/schema/pgsql/query_widgets.sql @@ -1,8 +1,10 @@ CREATE TABLE "widgets" ( + `wid` serial not null, `position` int not null, `side` varchar (100) not null, `type` varchar (100) not null, `active` boolean DEFAULT 0 not null, `location` varchar (100) not null, - `data` text DEFAULT '' not null + `data` text DEFAULT '' not null, + primary key(`wid`) ); \ No newline at end of file diff --git a/templates/account_logins.html b/templates/account_logins.html index 25925d5b..ddcf2bfc 100644 --- a/templates/account_logins.html +++ b/templates/account_logins.html @@ -4,7 +4,7 @@
{{range .ItemList}} -
+
{{if .Success}}{{lang "account_logins_success"}}{{else}}{{lang "account_logins_failure"}}"{{end}}
{{.IPAddress}} diff --git a/templates/forums.html b/templates/forums.html index 04409d73..63a7e77c 100644 --- a/templates/forums.html +++ b/templates/forums.html @@ -5,7 +5,7 @@

{{lang "forums_head"}}

- {{range .ItemList}}
{{.Name}}
diff --git a/templates/header.html b/templates/header.html index 1c6fe5b0..af4a6914 100644 --- a/templates/header.html +++ b/templates/header.html @@ -4,14 +4,12 @@ {{.Title}} | {{.Header.Site.Name}} {{range .Header.Stylesheets}} - - {{end}} + {{end}} {{range .Header.Scripts}} - - {{end}} + {{end}} {{if .Header.MetaDesc}}{{end}} diff --git a/templates/panel_analytics_agent_views.html b/templates/panel_analytics_agent_views.html index f256cd59..b1c33117 100644 --- a/templates/panel_analytics_agent_views.html +++ b/templates/panel_analytics_agent_views.html @@ -6,14 +6,7 @@
{{.FriendlyAgent}}{{lang "panel_statistics_views_head_suffix"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_agents.html b/templates/panel_analytics_agents.html index 9194ef07..0975f416 100644 --- a/templates/panel_analytics_agents.html +++ b/templates/panel_analytics_agents.html @@ -6,14 +6,7 @@
{{lang "panel_statistics_user_agents_head"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_forum_views.html b/templates/panel_analytics_forum_views.html index fc27f224..f262849b 100644 --- a/templates/panel_analytics_forum_views.html +++ b/templates/panel_analytics_forum_views.html @@ -6,14 +6,7 @@
{{.FriendlyAgent}}{{lang "panel_statistics_views_head_suffix"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_forums.html b/templates/panel_analytics_forums.html index c5da95fc..c525cc08 100644 --- a/templates/panel_analytics_forums.html +++ b/templates/panel_analytics_forums.html @@ -6,14 +6,7 @@
{{lang "panel_statistics_forums_head"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_lang_views.html b/templates/panel_analytics_lang_views.html index 343aadec..ea98e48d 100644 --- a/templates/panel_analytics_lang_views.html +++ b/templates/panel_analytics_lang_views.html @@ -6,14 +6,7 @@
{{.FriendlyAgent}}{{lang "panel_statistics_views_head_suffix"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_langs.html b/templates/panel_analytics_langs.html index f98f7165..bf221840 100644 --- a/templates/panel_analytics_langs.html +++ b/templates/panel_analytics_langs.html @@ -6,14 +6,7 @@
{{lang "panel_statistics_languages_head"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_posts.html b/templates/panel_analytics_posts.html index 2c04734f..dbc4bd19 100644 --- a/templates/panel_analytics_posts.html +++ b/templates/panel_analytics_posts.html @@ -6,14 +6,7 @@
{{lang "panel_statistics_post_counts_head"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_referrer_views.html b/templates/panel_analytics_referrer_views.html index fad6fdca..a74c3f22 100644 --- a/templates/panel_analytics_referrer_views.html +++ b/templates/panel_analytics_referrer_views.html @@ -6,14 +6,7 @@
{{.Agent}}{{lang "panel_statistics_views_head_suffix"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_referrers.html b/templates/panel_analytics_referrers.html index aee2e402..bc10af6e 100644 --- a/templates/panel_analytics_referrers.html +++ b/templates/panel_analytics_referrers.html @@ -6,14 +6,7 @@
{{lang "panel_statistics_referrers_head"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_route_views.html b/templates/panel_analytics_route_views.html index 21c9b3c5..4387629b 100644 --- a/templates/panel_analytics_route_views.html +++ b/templates/panel_analytics_route_views.html @@ -6,14 +6,7 @@
{{.Route}}{{lang "panel_statistics_views_head_suffix"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_routes.html b/templates/panel_analytics_routes.html index 43491aaf..09af23b6 100644 --- a/templates/panel_analytics_routes.html +++ b/templates/panel_analytics_routes.html @@ -6,14 +6,7 @@
{{lang "panel_statistics_routes_head"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_system_views.html b/templates/panel_analytics_system_views.html index 23679039..b7dfc6f7 100644 --- a/templates/panel_analytics_system_views.html +++ b/templates/panel_analytics_system_views.html @@ -6,14 +6,7 @@
{{.FriendlyAgent}}{{lang "panel_statistics_views_head_suffix"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_systems.html b/templates/panel_analytics_systems.html index 7c3ba309..54b6b574 100644 --- a/templates/panel_analytics_systems.html +++ b/templates/panel_analytics_systems.html @@ -6,14 +6,7 @@
{{lang "panel_statistics_operating_systems_head"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_time_range.html b/templates/panel_analytics_time_range.html new file mode 100644 index 00000000..170494f2 --- /dev/null +++ b/templates/panel_analytics_time_range.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/templates/panel_analytics_topics.html b/templates/panel_analytics_topics.html index d6fcb162..3a06e73d 100644 --- a/templates/panel_analytics_topics.html +++ b/templates/panel_analytics_topics.html @@ -6,14 +6,7 @@
{{lang "panel_statistics_topic_counts_head"}} - + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_analytics_views.html b/templates/panel_analytics_views.html index 2e9a8f9e..4c56150c 100644 --- a/templates/panel_analytics_views.html +++ b/templates/panel_analytics_views.html @@ -6,14 +6,7 @@

{{lang "panel_statistics_requests_head"}}

- + {{template "panel_analytics_time_range.html" . }}
diff --git a/templates/panel_inner_menu.html b/templates/panel_inner_menu.html index bbb06b71..bdc5d943 100644 --- a/templates/panel_inner_menu.html +++ b/templates/panel_inner_menu.html @@ -26,7 +26,7 @@
{{if eq .Zone "themes"}} - + {{end}} {{end}}
diff --git a/templates/panel_reglogs.html b/templates/panel_reglogs.html index be86c2a9..6a1d2bcf 100644 --- a/templates/panel_reglogs.html +++ b/templates/panel_reglogs.html @@ -7,7 +7,7 @@
{{range .Logs}} -
+
{{if not .Success}}{{lang "panel_logs_registration_attempt"}}: {{end}}{{.Username}} ({{lang "panel_logs_registration_email"}}: {{.Email}}){{if .ParsedReason}} ({{lang "panel_logs_registration_reason"}}: {{.ParsedReason}}){{end}} {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}} diff --git a/templates/panel_themes_menus.html b/templates/panel_themes_menus.html index 5c402f1d..65ac2c0f 100644 --- a/templates/panel_themes_menus.html +++ b/templates/panel_themes_menus.html @@ -5,7 +5,7 @@

{{lang "panel_themes_menus_head"}}

-
+
{{range .ItemList}}
{{if .Name}}{{.Name}} - {{end}}#{{.ID}} diff --git a/templates/panel_themes_widgets.html b/templates/panel_themes_widgets.html new file mode 100644 index 00000000..43f90613 --- /dev/null +++ b/templates/panel_themes_widgets.html @@ -0,0 +1,62 @@ +{{/** +type Widget struct { + Enabled bool + Location string // Coming Soon: overview, topics, topic / topic_view, forums, forum, global + Position int + Body string + Side string + Type string + Literal bool +} +**/}} +{{template "header.html" . }} +
+{{template "panel_menu.html" . }} +
+
+

{{lang "panel_themes_widgets_head"}}

+
+ {{range $name, $dock := .Docks}} + {{if $dock}} +
+

{{$name}}

+
+
+ {{range $widget := $dock}} +
+ +
+
+ + {{template "panel_themes_widgets_widget.html" $widget }} +
+
+
+ {{end}} + +
+ {{end}} + {{end}} +
+
+ +
+
+ + + {{template "panel_themes_widgets_widget.html" .BlankWidget }} +
+
+
+
+
+
+{{template "footer.html" . }} \ No newline at end of file diff --git a/templates/panel_themes_widgets_widget.html b/templates/panel_themes_widgets_widget.html new file mode 100644 index 00000000..1419ebd0 --- /dev/null +++ b/templates/panel_themes_widgets_widget.html @@ -0,0 +1,55 @@ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ + + + +
+
\ No newline at end of file diff --git a/templates/topic_alt_posts.html b/templates/topic_alt_posts.html index e954cc19..c8e206b4 100644 --- a/templates/topic_alt_posts.html +++ b/templates/topic_alt_posts.html @@ -36,7 +36,7 @@ {{end}} {{if $.CurrentUser.Perms.DeleteReply}}{{end}} {{if $.CurrentUser.Perms.ViewIPs}}{{end}} - + {{end}}
diff --git a/templates/topic_posts.html b/templates/topic_posts.html index 0616ffb2..d127375c 100644 --- a/templates/topic_posts.html +++ b/templates/topic_posts.html @@ -13,15 +13,15 @@    - {{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}{{else}}{{end}}{{end}} + {{if $.CurrentUser.Perms.LikeItem}}{{if .Liked}}{{else}}{{end}}{{end}} {{if not $.Topic.IsClosed or $.CurrentUser.Perms.CloseTopic}} {{if $.CurrentUser.Perms.EditReply}}{{end}} {{end}} {{if $.CurrentUser.Perms.DeleteReply}}{{end}} - {{if $.CurrentUser.Perms.ViewIPs}}{{end}} - + {{if $.CurrentUser.Perms.ViewIPs}}{{end}} + diff --git a/templates/topics_topic.html b/templates/topics_topic.html index 989464dc..892ba08d 100644 --- a/templates/topics_topic.html +++ b/templates/topics_topic.html @@ -1,7 +1,7 @@
- {{.Creator.Name}}'s Avatar + {{.Creator.Name}}'s Avatar {{.Title}} {{if .ForumName}}{{.ForumName}}{{end}}
{{.Creator.Name}} @@ -24,7 +24,7 @@
- {{.LastUser.Name}}'s Avatar + {{.LastUser.Name}}'s Avatar {{.LastUser.Name}}
{{reltime .LastReplyAt}} diff --git a/templates/widget_online.html b/templates/widget_online.html new file mode 100644 index 00000000..9ec56375 --- /dev/null +++ b/templates/widget_online.html @@ -0,0 +1,12 @@ +
+

{{.Name}}

+
+
+ {{if lt .UserCount 30}} + {{range .Users}}
+ {{.Name}}'s Avatar + {{.Name}} +
+ {{else}}
{{lang "widget.online_none_online"}}
{{end}} + {{else}}
{{langf "widget.online_some_online" .UserCount}}
{{end}} +
\ No newline at end of file diff --git a/templates/widget_search_and_filter.html b/templates/widget_search_and_filter.html new file mode 100644 index 00000000..a78531cb --- /dev/null +++ b/templates/widget_search_and_filter.html @@ -0,0 +1,7 @@ + +
+{{range .Forums}}
{{.Name}}
+{{end}} +
\ No newline at end of file diff --git a/themes/cosora/public/main.css b/themes/cosora/public/main.css index 5422d268..7e1c8324 100644 --- a/themes/cosora/public/main.css +++ b/themes/cosora/public/main.css @@ -236,12 +236,16 @@ ul { .rowhead:not(:first-child), .opthead:not(:first-child), .colstack_head:not(:first-child) { margin-top: 8px; } -.rowhead h1, .opthead h1, .colstack_head h1 { +.rowhead h1, .opthead h1, .colstack_head h1, +.rowhead h2, .opthead h2, .colstack_head h2 { font-size: 19px; font-weight: normal; color: var(--lightened-primary-text-color); display: inline-block; } +.rowhead h2, .opthead h2, .colstack_head h2 { + font-size: 17px; +} .colstack_head a h1 { color: var(--primary-link-color); } @@ -251,7 +255,7 @@ ul { .colstack_head h1 { font-size: 18px; } -h1, h3 { +h1, h2, h3 { -webkit-margin-before: 0; -webkit-margin-after: 0; margin-block-start: 0; @@ -534,7 +538,12 @@ input[type=checkbox]:checked + label .sel { .pollinput:not(:only-child):not(:first-child) { margin-bottom: 5px; } -.auto_hide, .show_on_edit:not(.edit_opened), .hide_on_edit.edit_opened, .link_select:not(.link_opened) { +.auto_hide, +.show_on_edit:not(.edit_opened), +.hide_on_edit.edit_opened, +.show_on_block_edit:not(.edit_opened), +.hide_on_block_edit.edit_opened, +.link_select:not(.link_opened) { display: none; } diff --git a/themes/cosora/public/panel.css b/themes/cosora/public/panel.css index 0b5a014d..9d21e1ca 100644 --- a/themes/cosora/public/panel.css +++ b/themes/cosora/public/panel.css @@ -309,6 +309,29 @@ } */ +.widget_normal { + display: flex; + width: 100%; +} +#widgetTmpl { + display: none; +} +.widget_disabled { + display: none; +} +.bg_red .widget_disabled { + display: inline; +} +.wtypes .formrow { + display: none; +} +.wtype_about .w_about, .wtype_simple .w_simple, .wtype_wol .w_wol, .wtype_default .w_default { + display: block; +} +.panel_widgets { + margin-bottom: 18px; +} + .pageset { margin-left: 16px; } diff --git a/themes/nox/public/acc_panel_common.css b/themes/nox/public/acc_panel_common.css index f9f5c7c4..94aed3c0 100644 --- a/themes/nox/public/acc_panel_common.css +++ b/themes/nox/public/acc_panel_common.css @@ -42,6 +42,9 @@ .to_right { margin-left: auto; } +.bg_red { + background-color: rgb(88,68,68) !important; +} @media (max-width: 420px) { .colstack { diff --git a/themes/nox/public/main.css b/themes/nox/public/main.css index f47b34b6..f49ab36c 100644 --- a/themes/nox/public/main.css +++ b/themes/nox/public/main.css @@ -170,11 +170,29 @@ li a { .sidebar .rowblock:not(.topic_list):not(.rowhead):not(.opthead) .rowitem { margin-left: 12px; } +.sidebar .search { + margin-left: 12px; +} +.widget_search:first-child { + margin-top: 36px; +} +.widget_search input { + width: 100%; + height: 30px; + margin-left: 0px; +} +.filter_list { + margin-top: 5px; +} .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem { border-radius: 3px; background-color: #444444; padding: 16px; } +.filter_item { + margin-bottom: 5px; + padding: 4px; +} .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem:not(:last-child) { margin-bottom: 8px; } @@ -261,6 +279,10 @@ h1, h2, h3, h4, h5 { .sidebar .rowhead h1 { font-size: 20px; } +.sidebar .rowhead:not(:first-child) h1 { + margin-top: 12px; + font-size: 19px; +} h2 { font-size: 18px; @@ -335,7 +357,14 @@ h2 { .more_topic_block_active { display: block; } -.hide_ajax_topic, .auto_hide, .show_on_edit:not(.edit_opened), .hide_on_edit.edit_opened, .link_select:not(.link_opened) { + +.hide_ajax_topic, +.auto_hide, +.show_on_edit:not(.edit_opened), +.hide_on_edit.edit_opened, +.show_on_block_edit:not(.edit_opened), +.hide_on_block_edit.edit_opened, +.link_select:not(.link_opened) { display: none !important; } @@ -866,7 +895,7 @@ input[type=checkbox]:checked + label .sel { display: flex; } -.rowlist.bgavatars, .micro_grid { +.rowlist.bgavatars:not(.not_grid), .micro_grid { display: grid; /*grid-gap: 16px; grid-row-gap: 8px;*/ @@ -888,6 +917,9 @@ input[type=checkbox]:checked + label .sel { margin-bottom: 10px; padding: 16px; } +.rowlist.not_grid .rowitem { + flex-direction: row; +} .rowlist.bgavatars .bgsub, .rowlist.bgavatars .rowTitle { margin-left: auto; margin-right: auto; @@ -901,6 +933,15 @@ input[type=checkbox]:checked + label .sel { font-size: 18px; margin-top: 4px; } +.rowlist.bgavatars.not_grid .bgsub { + height: 36px; + width: 36px; + margin-left: 8px; + margin-right: 12px; +} +.rowlist.bgavatars.not_grid .rowTitle { + margin-left: 0px; +} .ip_search_block { margin-bottom: 12px; diff --git a/themes/nox/public/panel.css b/themes/nox/public/panel.css index bb350d6d..c8c76f12 100644 --- a/themes/nox/public/panel.css +++ b/themes/nox/public/panel.css @@ -36,12 +36,21 @@ .colstack_right .colstack_head { margin-bottom: 5px; } +.colstack_right .colstack_head + .colstack_head:not(:first-child) { + margin-top: 5px; +} .colstack_right .colstack_head h1 { font-size: 21px; } .colstack_right .colstack_item.the_form, .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem { background-color: #444444; } +.colstack_right .colstack_head.colstack_sub_head:not(:first-child) { + margin-top: 6px; +} +.colstack_head + .colstack_head.colstack_sub_head:not(:first-child) { + margin-top: 2px; +} .rowitem { display: flex; } @@ -258,6 +267,39 @@ button, .formbutton, .panel_right_button:not(.has_inner_button), #panel_users .p margin-bottom: auto; } +.widget_normal { + display: flex; + width: 100%; +} +.bg_red.in_edit.widget_item { + background-color: #444444 !important; +} +.widget_item .form_button_row .rowitem { + display: flex; +} +.widget_edit .form_button_row .formitem a { + display: inline; +} +.colstack_right .colstack_item.the_form, .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem.widget_new { + padding-top: 12px; + padding-bottom: 12px; +} +#widgetTmpl { + display: none; +} +.widget_disabled { + display: none; +} +.bg_red .widget_disabled { + display: inline; +} +.wtypes .formrow { + display: none; +} +.wtype_about .w_about, .wtype_simple .w_simple, .wtype_wol .w_wol, .wtype_default .w_default { + display: block; +} + #panel_debug .grid_stat:not(.grid_stat_head) { margin-bottom: 5px; } diff --git a/themes/shadow/public/main.css b/themes/shadow/public/main.css index 07fcb1d2..b6ed0285 100644 --- a/themes/shadow/public/main.css +++ b/themes/shadow/public/main.css @@ -179,16 +179,18 @@ a { margin-top: 8px; padding: 12px; } -.rowitem h1 { +.rowitem h1, .rowitem h2 { font-size: 16px; - font-weight: normal; + display: inline; +} +h1, h2, h3 { -webkit-margin-before: 0; -webkit-margin-after: 0; margin-block-start: 0; margin-block-end: 0; margin-top: 0px; margin-bottom: 0px; - display: inline; + font-weight: normal; } .rowsmall { font-size: 12px; @@ -208,7 +210,13 @@ a { float: left; width: calc(70% - 24px); } -.colstack_left:empty, .colstack_right:empty, .show_on_edit:not(.edit_opened), .hide_on_edit.edit_opened, .link_select:not(.link_opened) { +.colstack_left:empty, +.colstack_right:empty, +.show_on_edit:not(.edit_opened), +.hide_on_edit.edit_opened, +.show_on_block_edit:not(.edit_opened), +.hide_on_block_edit.edit_opened, +.link_select:not(.link_opened) { display: none; } diff --git a/themes/shadow/public/panel.css b/themes/shadow/public/panel.css index b37931a5..954b8959 100644 --- a/themes/shadow/public/panel.css +++ b/themes/shadow/public/panel.css @@ -103,6 +103,22 @@ stroke: hsl(359,98%,43%) !important; } +#widgetTmpl { + display: none; +} +.widget_disabled { + display: none; +} +.bg_red .widget_disabled { + display: inline; +} +.wtypes .formrow { + display: none; +} +.wtype_about .w_about, .wtype_simple .w_simple, .wtype_wol .w_wol, .wtype_default .w_default { + display: block; +} + .pageset { margin-left: 0px; margin-bottom: 0px; diff --git a/themes/tempra-simple/public/main.css b/themes/tempra-simple/public/main.css index 43fa8a37..eebb61c5 100644 --- a/themes/tempra-simple/public/main.css +++ b/themes/tempra-simple/public/main.css @@ -249,7 +249,8 @@ li a { margin-top: -3px; margin-bottom: -2px; } -.rowhead h1, .colstack_head h1 { +.rowhead h1, .colstack_head h1, +.rowhead h2, .colstack_head h2 { -webkit-margin-before: 0; -webkit-margin-after: 0; margin-block-start: 0; @@ -743,7 +744,14 @@ button.username { .mention { font-weight: bold; } -.show_on_edit:not(.edit_opened), .hide_on_edit.edit_opened, .auto_hide, .hide_on_big, .show_on_mobile, .link_select:not(.link_opened) { +.show_on_edit:not(.edit_opened), +.hide_on_edit.edit_opened, +.show_on_block_edit:not(.edit_opened), +.hide_on_block_edit.edit_opened, +.auto_hide, +.hide_on_big, +.show_on_mobile, +.link_select:not(.link_opened) { display: none; } diff --git a/themes/tempra-simple/public/panel.css b/themes/tempra-simple/public/panel.css index 7d03acdf..602643e1 100644 --- a/themes/tempra-simple/public/panel.css +++ b/themes/tempra-simple/public/panel.css @@ -162,4 +162,20 @@ background-color: white; border: 1px solid hsl(0,0%,85%); +} + +#widgetTmpl { + display: none; +} +.widget_disabled { + display: none; +} +.bg_red .widget_disabled { + display: inline; +} +.wtypes .formrow { + display: none; +} +.wtype_about .w_about, .wtype_simple .w_simple, .wtype_wol .w_wol, .wtype_default .w_default { + display: block; } \ No newline at end of file