diff --git a/README.md b/README.md index ebfcdfc6..477f41ce 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,8 @@ More images in the /images/ folder. Beware though, some of them are *really* out * ithub.com/denisenkom/go-mssqldb For interfacing with MSSQL. You will be able to pick this instead of MSSQL soon. +* github.com/go-ego/riot A search engine library. + # Bundled Plugins There are several plugins which are bundled with the software by default. These cover various common tasks which aren't common enough to clutter the core with or which have competing implementation methods (E.g. plugin_markdown vs plugin_bbcode for post mark-up). diff --git a/common/audit_logs.go b/common/audit_logs.go index 5daf3545..d4fe5b6e 100644 --- a/common/audit_logs.go +++ b/common/audit_logs.go @@ -6,31 +6,64 @@ import ( "../query_gen/lib" ) -type LogStmts struct { - addModLogEntry *sql.Stmt - addAdminLogEntry *sql.Stmt +var ModLogs LogStore +var AdminLogs LogStore + +type LogStore interface { + Create(action string, elementID int, elementType string, ipaddress string, actorID int) (err error) + GlobalCount() int } -var logStmts LogStmts +type SQLModLogStore struct { + create *sql.Stmt + count *sql.Stmt +} -func init() { - DbInits.Add(func(acc *qgen.Accumulator) error { - logStmts = LogStmts{ - addModLogEntry: acc.Insert("moderation_logs").Columns("action, elementID, elementType, ipaddress, actorID, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(), - addAdminLogEntry: acc.Insert("administration_logs").Columns("action, elementID, elementType, ipaddress, actorID, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(), - } - return acc.FirstError() - }) +func NewModLogStore() (*SQLModLogStore, error) { + acc := qgen.Builder.Accumulator() + return &SQLModLogStore{ + create: acc.Insert("moderation_logs").Columns("action, elementID, elementType, ipaddress, actorID, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(), + count: acc.Count("moderation_logs").Prepare(), + }, acc.FirstError() } // TODO: Make a store for this? -func AddModLog(action string, elementID int, elementType string, ipaddress string, actorID int) (err error) { - _, err = logStmts.addModLogEntry.Exec(action, elementID, elementType, ipaddress, actorID) +func (store *SQLModLogStore) Create(action string, elementID int, elementType string, ipaddress string, actorID int) (err error) { + _, err = store.create.Exec(action, elementID, elementType, ipaddress, actorID) return err } +func (store *SQLModLogStore) GlobalCount() (logCount int) { + err := store.count.QueryRow().Scan(&logCount) + if err != nil { + LogError(err) + } + return logCount +} + +type SQLAdminLogStore struct { + create *sql.Stmt + count *sql.Stmt +} + +func NewAdminLogStore() (*SQLAdminLogStore, error) { + acc := qgen.Builder.Accumulator() + return &SQLAdminLogStore{ + create: acc.Insert("administration_logs").Columns("action, elementID, elementType, ipaddress, actorID, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(), + count: acc.Count("administration_logs").Prepare(), + }, acc.FirstError() +} + // TODO: Make a store for this? -func AddAdminLog(action string, elementID string, elementType int, ipaddress string, actorID int) (err error) { - _, err = logStmts.addAdminLogEntry.Exec(action, elementID, elementType, ipaddress, actorID) +func (store *SQLAdminLogStore) Create(action string, elementID int, elementType string, ipaddress string, actorID int) (err error) { + _, err = store.create.Exec(action, elementID, elementType, ipaddress, actorID) return err } + +func (store *SQLAdminLogStore) GlobalCount() (logCount int) { + err := store.count.QueryRow().Scan(&logCount) + if err != nil { + LogError(err) + } + return logCount +} diff --git a/common/auth.go b/common/auth.go index e75a704a..09f0c626 100644 --- a/common/auth.go +++ b/common/auth.go @@ -91,9 +91,9 @@ func (auth *DefaultAuth) ForceLogout(uid int) error { } // Flush the user out of the cache - ucache, ok := Users.(UserCache) - if ok { - ucache.CacheRemove(uid) + ucache := Users.GetCache() + if ucache != nil { + ucache.Remove(uid) } return nil @@ -170,9 +170,9 @@ func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) { } // Flush the user data from the cache - ucache, ok := Users.(UserCache) - if ok { - ucache.CacheRemove(uid) + ucache := Users.GetCache() + if ucache != nil { + ucache.Remove(uid) } return session, nil } diff --git a/common/forum.go b/common/forum.go index fb660958..c7a47cf2 100644 --- a/common/forum.go +++ b/common/forum.go @@ -87,7 +87,7 @@ func (forum *Forum) Update(name string, desc string, active bool, preset string) return err } } - _ = Fstore.Reload(forum.ID) + _ = Forums.Reload(forum.ID) return nil } @@ -113,11 +113,11 @@ func (forum *Forum) setPreset(fperms *ForumPerms, preset string, gid int) (err e LogError(err) return errors.New("Unable to update the forum") } - err = Fstore.Reload(forum.ID) + err = Forums.Reload(forum.ID) if err != nil { return errors.New("Unable to reload forum") } - err = Fpstore.ReloadGroup(forum.ID, gid) + err = FPStore.ReloadGroup(forum.ID, gid) if err != nil { return errors.New("Unable to reload the forum permissions") } diff --git a/common/forum_perms.go b/common/forum_perms.go index d8ff87b7..2fd452ef 100644 --- a/common/forum_perms.go +++ b/common/forum_perms.go @@ -22,6 +22,7 @@ var LocalPermList = []string{ "CloseTopic", } +// TODO: Rename this to ForumPermSet? /* Inherit from group permissions for ones we don't have */ type ForumPerms struct { ViewTopic bool @@ -165,7 +166,7 @@ func PermmapToQuery(permmap map[string]*ForumPerms, fid int) error { if err != nil { return err } - return Fpstore.Reload(fid) + return FPStore.Reload(fid) } func ReplaceForumPermsForGroup(gid int, presetSet map[int]string, permSets map[int]*ForumPerms) error { diff --git a/common/forum_perms_store.go b/common/forum_perms_store.go index 6c0e933d..e0699608 100644 --- a/common/forum_perms_store.go +++ b/common/forum_perms_store.go @@ -9,7 +9,7 @@ import ( "../query_gen/lib" ) -var Fpstore ForumPermsStore +var FPStore ForumPermsStore type ForumPermsStore interface { Init() error @@ -41,7 +41,7 @@ func NewMemoryForumPermsStore() (*MemoryForumPermsStore, error) { func (fps *MemoryForumPermsStore) Init() error { fps.updateMutex.Lock() defer fps.updateMutex.Unlock() - fids, err := Fstore.GetAllIDs() + fids, err := Forums.GetAllIDs() if err != nil { return err } @@ -55,7 +55,6 @@ func (fps *MemoryForumPermsStore) Init() error { debugLog("Adding the forum permissions") debugDetail("forumPerms[gid][fid]") - // Temporarily store the forum perms in a map before transferring it to a much faster and thread-safe slice forumPerms = make(map[int]map[int]*ForumPerms) for rows.Next() { var gid, fid int @@ -97,7 +96,7 @@ func (fps *MemoryForumPermsStore) Reload(fid int) error { fps.updateMutex.Lock() defer fps.updateMutex.Unlock() debugLogf("Reloading the forum permissions for forum #%d", fid) - fids, err := Fstore.GetAllIDs() + fids, err := Forums.GetAllIDs() if err != nil { return err } @@ -143,7 +142,7 @@ func (fps *MemoryForumPermsStore) ReloadGroup(fid int, gid int) (err error) { if err != nil { return err } - group, err := Gstore.Get(gid) + group, err := Groups.Get(gid) if err != nil { return err } @@ -153,7 +152,7 @@ func (fps *MemoryForumPermsStore) ReloadGroup(fid int, gid int) (err error) { } func (fps *MemoryForumPermsStore) cascadePermSetToGroups(forumPerms map[int]map[int]*ForumPerms, fids []int) error { - groups, err := Gstore.GetAll() + groups, err := Groups.GetAll() if err != nil { return err } @@ -200,7 +199,7 @@ func (fps *MemoryForumPermsStore) cascadePermSetToGroup(forumPerms map[int]map[i // TODO: Add a hook here and have plugin_guilds use it func (fps *MemoryForumPermsStore) Get(fid int, gid int) (fperms *ForumPerms, err error) { - group, err := Gstore.Get(gid) + group, err := Groups.Get(gid) if err != nil { return fperms, ErrNoRows } diff --git a/common/forum_store.go b/common/forum_store.go index 0e83702f..d9177e8b 100644 --- a/common/forum_store.go +++ b/common/forum_store.go @@ -19,7 +19,7 @@ import ( var forumCreateMutex sync.Mutex var forumPerms map[int]map[int]*ForumPerms // [gid][fid]*ForumPerms // TODO: Add an abstraction around this and make it more thread-safe -var Fstore ForumStore +var Forums ForumStore // ForumStore is an interface for accessing the forums and the metadata stored on them type ForumStore interface { diff --git a/common/group.go b/common/group.go index 4423cafc..cee41f91 100644 --- a/common/group.go +++ b/common/group.go @@ -51,7 +51,7 @@ func (group *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err err return err } - Gstore.Reload(group.ID) + Groups.Reload(group.ID) return nil } diff --git a/common/group_store.go b/common/group_store.go index 5954f64a..d1efcab4 100644 --- a/common/group_store.go +++ b/common/group_store.go @@ -12,7 +12,7 @@ import ( "../query_gen/lib" ) -var Gstore GroupStore +var Groups GroupStore // ? - We could fallback onto the database when an item can't be found in the cache? type GroupStore interface { @@ -230,7 +230,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod } // Generate the forum permissions based on the presets... - fdata, err := Fstore.GetAll() + fdata, err := Forums.GetAll() if err != nil { return 0, err } @@ -279,7 +279,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod mgs.Unlock() for _, forum := range fdata { - err = Fpstore.Reload(forum.ID) + err = FPStore.Reload(forum.ID) if err != nil { return gid, err } diff --git a/common/null_topic_cache.go b/common/null_topic_cache.go new file mode 100644 index 00000000..0b14b354 --- /dev/null +++ b/common/null_topic_cache.go @@ -0,0 +1,55 @@ +package common + +type NullTopicCache struct { +} + +// NewNullTopicCache gives you a new instance of NullTopicCache +func NewNullTopicCache() *NullTopicCache { + return &NullTopicCache{} +} + +func (mts *NullTopicCache) Get(id int) (*Topic, error) { + return nil, ErrNoRows +} + +func (mts *NullTopicCache) GetUnsafe(id int) (*Topic, error) { + return nil, ErrNoRows +} + +func (mts *NullTopicCache) Set(_ *Topic) error { + return nil +} + +func (mts *NullTopicCache) Add(item *Topic) error { + _ = item + return nil +} + +// TODO: Make these length increments thread-safe. Ditto for the other DataStores +func (mts *NullTopicCache) AddUnsafe(item *Topic) error { + _ = item + return nil +} + +// TODO: Make these length decrements thread-safe. Ditto for the other DataStores +func (mts *NullTopicCache) Remove(id int) error { + return nil +} + +func (mts *NullTopicCache) RemoveUnsafe(id int) error { + return nil +} + +func (mts *NullTopicCache) Flush() { +} + +func (mts *NullTopicCache) Length() int { + return 0 +} + +func (mts *NullTopicCache) SetCapacity(_ int) { +} + +func (mts *NullTopicCache) GetCapacity() int { + return 0 +} diff --git a/common/null_user_cache.go b/common/null_user_cache.go new file mode 100644 index 00000000..30f3b5c3 --- /dev/null +++ b/common/null_user_cache.go @@ -0,0 +1,57 @@ +package common + +type NullUserCache struct { +} + +// NewNullUserCache gives you a new instance of NullUserCache +func NewNullUserCache() *NullUserCache { + return &NullUserCache{} +} + +func (mus *NullUserCache) Get(id int) (*User, error) { + return nil, ErrNoRows +} + +func (mus *NullUserCache) BulkGet(ids []int) (list []*User) { + return list +} + +func (mus *NullUserCache) GetUnsafe(id int) (*User, error) { + return nil, ErrNoRows +} + +func (mus *NullUserCache) Set(_ *User) error { + return nil +} + +func (mus *NullUserCache) Add(item *User) error { + _ = item + return nil +} + +func (mus *NullUserCache) AddUnsafe(item *User) error { + _ = item + return nil +} + +func (mus *NullUserCache) Remove(id int) error { + return nil +} + +func (mus *NullUserCache) RemoveUnsafe(id int) error { + return nil +} + +func (mus *NullUserCache) Flush() { +} + +func (mus *NullUserCache) Length() int { + return 0 +} + +func (mus *NullUserCache) SetCapacity(_ int) { +} + +func (mus *NullUserCache) GetCapacity() int { + return 0 +} diff --git a/common/pages.go b/common/pages.go index 64ec5114..492b94ac 100644 --- a/common/pages.go +++ b/common/pages.go @@ -13,8 +13,8 @@ type HeaderVars struct { Widgets PageWidgets Site *site Settings SettingMap - Themes map[string]Theme // TODO: Use a slice containing every theme instead of the main map for speed - ThemeName string + Themes map[string]Theme // TODO: Use a slice containing every theme instead of the main map for speed? + Theme Theme //TemplateName string // TODO: Use this to move template calls to the router rather than duplicating them over and over and over? ExtData ExtData } @@ -282,5 +282,5 @@ type AreYouSure struct { // This is mostly for errors.go, please create *HeaderVars on the spot instead of relying on this or the atomic store underlying it, if possible // TODO: Write a test for this func DefaultHeaderVar() *HeaderVars { - return &HeaderVars{Site: Site, ThemeName: fallbackTheme} + return &HeaderVars{Site: Site, Theme: Themes[fallbackTheme]} } diff --git a/common/parser.go b/common/parser.go index b0c77104..890d60c7 100644 --- a/common/parser.go +++ b/common/parser.go @@ -219,7 +219,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/) i += intLen topic, err := Topics.Get(tid) - if err != nil || !Fstore.Exists(topic.ParentID) { + if err != nil || !Forums.Exists(topic.ParentID) { outbytes = append(outbytes, InvalidTopic...) lastItem = i continue @@ -250,7 +250,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/) reply := BlankReply() reply.ID = rid topic, err := reply.Topic() - if err != nil || !Fstore.Exists(topic.ParentID) { + if err != nil || !Forums.Exists(topic.ParentID) { outbytes = append(outbytes, InvalidTopic...) lastItem = i continue @@ -271,7 +271,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/) fid, intLen := CoerceIntBytes(msgbytes[start:]) i += intLen - if !Fstore.Exists(fid) { + if !Forums.Exists(fid) { outbytes = append(outbytes, InvalidForum...) lastItem = i continue diff --git a/common/permissions.go b/common/permissions.go index 07d4691b..613d249b 100644 --- a/common/permissions.go +++ b/common/permissions.go @@ -189,7 +189,7 @@ func RebuildGroupPermissions(gid int) error { return err } - group, err := Gstore.Get(gid) + group, err := Groups.Get(gid) if err != nil { return err } diff --git a/common/phrases.go b/common/phrases.go index 5fb32b0d..a132fbc3 100644 --- a/common/phrases.go +++ b/common/phrases.go @@ -17,6 +17,7 @@ import ( "sync/atomic" ) +// TODO: Add a phrase store? // TODO: Let the admin edit phrases from inside the Control Panel? How should we persist these? Should we create a copy of the langpack or edit the primaries? Use the changeLangpack mutex for this? // nolint Be quiet megacheck, this *is* used var currentLangPack atomic.Value @@ -40,7 +41,9 @@ type LanguagePack struct { LocalPerms map[string]string SettingLabels map[string]string PermPresets map[string]string - Accounts map[string]string // TODO: Apply these phrases in the software proper + Accounts map[string]string // TODO: Apply these phrases in the software proper + Errors map[string]map[string]string // map[category]map[name]value + PageTitles map[string]string } // TODO: Add the ability to edit language JSON files from the Control Panel and automatically scan the files for changes @@ -114,7 +117,7 @@ func GetPhrase(name string) (string, bool) { func GetGlobalPermPhrase(name string) string { res, ok := currentLangPack.Load().(*LanguagePack).GlobalPerms[name] if !ok { - return "{name}" + return getPhrasePlaceholder() } return res } @@ -122,7 +125,7 @@ func GetGlobalPermPhrase(name string) string { func GetLocalPermPhrase(name string) string { res, ok := currentLangPack.Load().(*LanguagePack).LocalPerms[name] if !ok { - return "{name}" + return getPhrasePlaceholder() } return res } @@ -130,7 +133,7 @@ func GetLocalPermPhrase(name string) string { func GetSettingLabel(name string) string { res, ok := currentLangPack.Load().(*LanguagePack).SettingLabels[name] if !ok { - return "{name}" + return getPhrasePlaceholder() } return res } @@ -146,11 +149,32 @@ func GetAllPermPresets() map[string]string { func GetAccountPhrase(name string) string { res, ok := currentLangPack.Load().(*LanguagePack).Accounts[name] if !ok { - return "{name}" + return getPhrasePlaceholder() } return res } +// TODO: Does comma ok work with multi-dimensional maps? +func GetErrorPhrase(category string, name string) string { + res, ok := currentLangPack.Load().(*LanguagePack).Errors[category][name] + if !ok { + return getPhrasePlaceholder() + } + return res +} + +func GetTitlePhrase(name string) string { + res, ok := currentLangPack.Load().(*LanguagePack).PageTitles[name] + if !ok { + return getPhrasePlaceholder() + } + return res +} + +func getPhrasePlaceholder() string { + return "{name}" +} + // ? - Use runtime reflection for updating phrases? // TODO: Implement these func AddPhrase() { diff --git a/common/reply.go b/common/reply.go index 0c530a68..e304e354 100644 --- a/common/reply.go +++ b/common/reply.go @@ -112,9 +112,9 @@ func (reply *Reply) Delete() error { } // TODO: Move this bit to *Topic _, err = replyStmts.removeRepliesFromTopic.Exec(1, reply.ParentID) - tcache, ok := Topics.(TopicCache) - if ok { - tcache.CacheRemove(reply.ParentID) + tcache := Topics.GetCache() + if tcache != nil { + tcache.Remove(reply.ParentID) } return err } diff --git a/common/routes_common.go b/common/routes_common.go index e59ae840..88366c8a 100644 --- a/common/routes_common.go +++ b/common/routes_common.go @@ -31,8 +31,7 @@ func BuildWidgets(zone string, data interface{}, headerVars *HeaderVars, r *http } } - //log.Print("Themes[headerVars.ThemeName].Sidebars", Themes[headerVars.ThemeName].Sidebars) - if Themes[headerVars.ThemeName].Sidebars == "right" { + if Themes[headerVars.Theme.Name].Sidebars == "right" { if len(Docks.RightSidebar) != 0 { var sbody string for _, widget := range Docks.RightSidebar { @@ -48,7 +47,7 @@ func BuildWidgets(zone string, data interface{}, headerVars *HeaderVars, r *http } func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerLite *HeaderLite, rerr RouteError) { - if !Fstore.Exists(fid) { + if !Forums.Exists(fid) { return nil, PreError("The target forum doesn't exist.", w, r) } @@ -61,7 +60,7 @@ func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fi } } - fperms, err := Fpstore.Get(fid, user.Group) + fperms, err := FPStore.Get(fid, user.Group) if err != nil { // TODO: Refactor this log.Printf("Unable to get the forum perms for Group #%d for User #%d", user.Group, user.ID) @@ -76,7 +75,7 @@ func forumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) if rerr != nil { return headerVars, rerr } - if !Fstore.Exists(fid) { + if !Forums.Exists(fid) { return headerVars, NotFound(w, r) } @@ -88,14 +87,12 @@ func forumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) } } - fperms, err := Fpstore.Get(fid, user.Group) + fperms, err := FPStore.Get(fid, user.Group) if err != nil { // TODO: Refactor this log.Printf("Unable to get the forum perms for Group #%d for User #%d", user.Group, user.ID) return nil, PreError("Something weird happened", w, r) } - //log.Printf("user.Perms: %+v\n", user.Perms) - //log.Printf("fperms: %+v\n", fperms) cascadeForumPerms(fperms, user) return headerVars, rerr } @@ -125,28 +122,30 @@ func cascadeForumPerms(fperms *ForumPerms, user *User) { // Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with // TODO: Do a panel specific theme? func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerVars *HeaderVars, stats PanelStats, rerr RouteError) { - var themeName = DefaultThemeBox.Load().(string) + var theme Theme cookie, err := r.Cookie("current_theme") if err == nil { - cookie := html.EscapeString(cookie.Value) - theme, ok := Themes[cookie] + inTheme, ok := Themes[html.EscapeString(cookie.Value)] if ok && !theme.HideFromThemes { - themeName = cookie + theme = inTheme } } + if theme.Name == "" { + theme = Themes[DefaultThemeBox.Load().(string)] + } headerVars = &HeaderVars{ - Site: Site, - Settings: SettingBox.Load().(SettingMap), - Themes: Themes, - ThemeName: themeName, + Site: Site, + Settings: SettingBox.Load().(SettingMap), + Themes: Themes, + Theme: theme, } // TODO: We should probably initialise headerVars.ExtData - headerVars.Stylesheets = append(headerVars.Stylesheets, headerVars.ThemeName+"/panel.css") - if len(Themes[headerVars.ThemeName].Resources) > 0 { - rlist := Themes[headerVars.ThemeName].Resources + headerVars.Stylesheets = append(headerVars.Stylesheets, theme.Name+"/panel.css") + if len(theme.Resources) > 0 { + rlist := theme.Resources for _, resource := range rlist { if resource.Location == "global" || resource.Location == "panel" { extarr := strings.Split(resource.Name, ".") @@ -161,8 +160,8 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerV } stats.Users = Users.GlobalCount() - stats.Groups = Gstore.GlobalCount() - stats.Forums = Fstore.GlobalCount() // TODO: Stop it from showing the blanked forums + stats.Groups = Groups.GlobalCount() + stats.Forums = Forums.GlobalCount() // TODO: Stop it from showing the blanked forums stats.Settings = len(headerVars.Settings) stats.WordFilters = len(WordFilterBox.Load().(WordFilterMap)) stats.Themes = len(Themes) @@ -170,12 +169,17 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerV pusher, ok := w.(http.Pusher) if ok { - pusher.Push("/static/"+headerVars.ThemeName+"/main.css", nil) - pusher.Push("/static/"+headerVars.ThemeName+"/panel.css", nil) + pusher.Push("/static/"+theme.Name+"/main.css", nil) + pusher.Push("/static/"+theme.Name+"/panel.css", nil) pusher.Push("/static/global.js", nil) pusher.Push("/static/jquery-3.1.1.min.js", nil) - // TODO: Push the theme CSS files - // TODO: Push the theme scripts + // TODO: Test these + for _, sheet := range headerVars.Stylesheets { + pusher.Push("/static/"+sheet, nil) + } + for _, script := range headerVars.Scripts { + pusher.Push("/static/"+script, nil) + } // TODO: Push avatars? } @@ -209,30 +213,32 @@ func simpleUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header // TODO: Add the ability for admins to restrict certain themes to certain groups? func userCheck(w http.ResponseWriter, r *http.Request, user *User) (headerVars *HeaderVars, rerr RouteError) { - var themeName = DefaultThemeBox.Load().(string) + var theme Theme cookie, err := r.Cookie("current_theme") if err == nil { - cookie := html.EscapeString(cookie.Value) - theme, ok := Themes[cookie] + inTheme, ok := Themes[html.EscapeString(cookie.Value)] if ok && !theme.HideFromThemes { - themeName = cookie + theme = inTheme } } + if theme.Name == "" { + theme = Themes[DefaultThemeBox.Load().(string)] + } headerVars = &HeaderVars{ - Site: Site, - Settings: SettingBox.Load().(SettingMap), - Themes: Themes, - ThemeName: themeName, + Site: Site, + Settings: SettingBox.Load().(SettingMap), + Themes: Themes, + Theme: theme, } if user.IsBanned { headerVars.NoticeList = append(headerVars.NoticeList, "Your account has been suspended. Some of your permissions may have been revoked.") } - if len(Themes[headerVars.ThemeName].Resources) > 0 { - rlist := Themes[headerVars.ThemeName].Resources + if len(theme.Resources) > 0 { + rlist := theme.Resources for _, resource := range rlist { if resource.Location == "global" || resource.Location == "frontend" { extarr := strings.Split(resource.Name, ".") @@ -248,11 +254,16 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (headerVars * pusher, ok := w.(http.Pusher) if ok { - pusher.Push("/static/"+headerVars.ThemeName+"/main.css", nil) + pusher.Push("/static/"+theme.Name+"/main.css", nil) pusher.Push("/static/global.js", nil) pusher.Push("/static/jquery-3.1.1.min.js", nil) - // TODO: Push the theme CSS files - // TODO: Push the theme scripts + // TODO: Test these + for _, sheet := range headerVars.Stylesheets { + pusher.Push("/static/"+sheet, nil) + } + for _, script := range headerVars.Scripts { + pusher.Push("/static/"+script, nil) + } // TODO: Push avatars? } @@ -344,3 +355,7 @@ func NoSessionMismatch(w http.ResponseWriter, r *http.Request, user User) RouteE } return nil } + +func ReqIsJson(r *http.Request) bool { + return r.Header.Get("Content-type") == "application/json" +} diff --git a/common/settings.go b/common/settings.go index b7e0f3a6..75643e60 100644 --- a/common/settings.go +++ b/common/settings.go @@ -2,6 +2,7 @@ package common import ( "database/sql" + "errors" "strconv" "strings" "sync/atomic" @@ -17,6 +18,7 @@ type SettingMap map[string]interface{} type SettingStore interface { ParseSetting(sname string, scontent string, stype string, sconstraint string) string BypassGet(name string) (*Setting, error) + BypassGetAll(name string) ([]*Setting, error) } type OptionLabel struct { @@ -35,6 +37,7 @@ type Setting struct { type SettingStmts struct { getAll *sql.Stmt get *sql.Stmt + update *sql.Stmt } var settingStmts SettingStmts @@ -45,80 +48,83 @@ func init() { settingStmts = SettingStmts{ getAll: acc.Select("settings").Columns("name, content, type, constraints").Prepare(), get: acc.Select("settings").Columns("content, type, constraints").Where("name = ?").Prepare(), + update: acc.Update("settings").Set("content = ?").Where("name = ?").Prepare(), } return acc.FirstError() }) } func LoadSettings() error { - rows, err := settingStmts.getAll.Query() + var sBox = SettingMap(make(map[string]interface{})) + settings, err := sBox.BypassGetAll() if err != nil { return err } - defer rows.Close() - var sBox = SettingMap(make(map[string]interface{})) - var sname, scontent, stype, sconstraints string - for rows.Next() { - err = rows.Scan(&sname, &scontent, &stype, &sconstraints) + for _, setting := range settings { + err = sBox.ParseSetting(setting.Name, setting.Content, setting.Type, setting.Constraint) if err != nil { return err } - errmsg := sBox.ParseSetting(sname, scontent, stype, sconstraints) - if errmsg != "" { - return err - } - } - err = rows.Err() - if err != nil { - return err } SettingBox.Store(sBox) return nil } +// nolint +var ErrNotInteger = errors.New("You were supposed to enter an integer x.x") +var ErrSettingNotInteger = errors.New("Only integers are allowed in this setting x.x") +var ErrBadConstraintNotInteger = errors.New("Invalid contraint! The constraint field wasn't an integer!") +var ErrBadSettingRange = errors.New("Only integers between a certain range are allowed in this setting") + +// To avoid leaking internal state to the user +// TODO: We need to add some sort of DualError interface +func SafeSettingError(err error) bool { + return err == ErrNotInteger || err == ErrSettingNotInteger || err == ErrBadConstraintNotInteger || err == ErrBadSettingRange || err == ErrNoRows +} + // TODO: Add better support for HTML attributes (html-attribute). E.g. Meta descriptions. -func (sBox SettingMap) ParseSetting(sname string, scontent string, stype string, constraint string) string { - var err error +func (sBox SettingMap) ParseSetting(sname string, scontent string, stype string, constraint string) (err error) { var ssBox = map[string]interface{}(sBox) - if stype == "bool" { + switch stype { + case "bool": ssBox[sname] = (scontent == "1") - } else if stype == "int" { + case "int": ssBox[sname], err = strconv.Atoi(scontent) if err != nil { - return "You were supposed to enter an integer x.x\nType mismatch in " + sname + return ErrNotInteger } - } else if stype == "int64" { + case "int64": ssBox[sname], err = strconv.ParseInt(scontent, 10, 64) if err != nil { - return "You were supposed to enter an integer x.x\nType mismatch in " + sname + return ErrNotInteger } - } else if stype == "list" { + case "list": cons := strings.Split(constraint, "-") if len(cons) < 2 { - return "Invalid constraint! The second field wasn't set!" + return errors.New("Invalid constraint! The second field wasn't set!") } con1, err := strconv.Atoi(cons[0]) con2, err2 := strconv.Atoi(cons[1]) if err != nil || err2 != nil { - return "Invalid contraint! The constraint field wasn't an integer!" + return ErrBadConstraintNotInteger } value, err := strconv.Atoi(scontent) if err != nil { - return "Only integers are allowed in this setting x.x\nType mismatch in " + sname + return ErrSettingNotInteger } if value < con1 || value > con2 { - return "Only integers between a certain range are allowed in this setting" + return ErrBadSettingRange } ssBox[sname] = value - } else { + default: ssBox[sname] = scontent } - return "" + return nil } func (sBox SettingMap) BypassGet(name string) (*Setting, error) { @@ -126,3 +132,51 @@ func (sBox SettingMap) BypassGet(name string) (*Setting, error) { err := settingStmts.get.QueryRow(name).Scan(&setting.Content, &setting.Type, &setting.Constraint) return setting, err } + +func (sBox SettingMap) BypassGetAll() (settingList []*Setting, err error) { + rows, err := settingStmts.getAll.Query() + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + setting := &Setting{Name: ""} + err := rows.Scan(&setting.Name, &setting.Content, &setting.Type, &setting.Constraint) + if err != nil { + return nil, err + } + settingList = append(settingList, setting) + } + return settingList, rows.Err() +} + +func (sBox SettingMap) Update(name string, content string) error { + setting, err := sBox.BypassGet(name) + if err == ErrNoRows { + return err + } + + // TODO: Why is this here and not in a common function? + if setting.Type == "bool" { + if content == "on" || content == "1" { + content = "1" + } else { + content = "0" + } + } + + // TODO: Make this a method or function? + _, err = settingStmts.update.Exec(content, name) + if err != nil { + return err + } + + err = sBox.ParseSetting(name, content, setting.Type, setting.Constraint) + if err != nil { + return err + } + // TODO: Do a reload instead? + SettingBox.Store(sBox) + return nil +} diff --git a/common/site.go b/common/site.go index f6354f31..cb2e0813 100644 --- a/common/site.go +++ b/common/site.go @@ -99,7 +99,7 @@ func ProcessConfig() error { } func VerifyConfig() error { - if !Fstore.Exists(Config.DefaultForum) { + if !Forums.Exists(Config.DefaultForum) { return errors.New("Invalid default forum") } return nil diff --git a/common/tasks.go b/common/tasks.go index c0324466..d9692a17 100644 --- a/common/tasks.go +++ b/common/tasks.go @@ -67,7 +67,7 @@ func HandleServerSync() error { if lastUpdate.After(lastSync) { // TODO: A more granular sync - err = Fstore.LoadForums() + err = Forums.LoadForums() if err != nil { log.Print("Unable to reload the forums") return err diff --git a/common/template_init.go b/common/template_init.go index d0a15b90..b75d9582 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -96,7 +96,7 @@ func compileTemplates() error { Site: Site, Settings: SettingBox.Load().(SettingMap), Themes: Themes, - ThemeName: DefaultThemeBox.Load().(string), + Theme: Themes[DefaultThemeBox.Load().(string)], NoticeList: []string{"test"}, Stylesheets: []string{"panel"}, Scripts: []string{"whatever"}, @@ -132,7 +132,7 @@ func compileTemplates() error { // TODO: Use a dummy forum list to avoid o(n) problems var forumList []Forum - forums, err := Fstore.GetAll() + forums, err := Forums.GetAll() if err != nil { return err } diff --git a/common/templates/templates.go b/common/templates/templates.go index 1dfc5b9f..c2a56e9a 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -28,21 +28,23 @@ type VarItemReflect struct { Value reflect.Value } type CTemplateSet struct { - tlist map[string]*parse.Tree - dir string - funcMap map[string]interface{} - importMap map[string]string - Fragments map[string]int - FragmentCursor map[string]int - FragOut string - varList map[string]VarItem - localVars map[string]map[string]VarItemReflect - stats map[string]int - pVarList string - pVarPosition int - previousNode parse.NodeType - currentNode parse.NodeType - nextNode parse.NodeType + tlist map[string]*parse.Tree + dir string + funcMap map[string]interface{} + importMap map[string]string + Fragments map[string]int + FragmentCursor map[string]int + FragOut string + varList map[string]VarItem + localVars map[string]map[string]VarItemReflect + hasDispInt bool + localDispStructIndex int + stats map[string]int + pVarList string + pVarPosition int + previousNode parse.NodeType + currentNode parse.NodeType + nextNode parse.NodeType //tempVars map[string]string doImports bool minify bool @@ -102,6 +104,8 @@ func (c *CTemplateSet) Compile(name string, dir string, expects string, expectsI } c.varList = varList + c.hasDispInt = false + c.localDispStructIndex = 0 //c.pVarList = "" //c.pVarPosition = 0 c.stats = make(map[string]int) @@ -187,7 +191,6 @@ w.Write([]byte(`, " + ", -1) c.log("Output!") c.log(fout) - //log.Fatal("remove the log.Fatal line") return fout, nil } @@ -334,10 +337,8 @@ func (c *CTemplateSet) compileSubswitch(varholder string, holdreflect reflect.Va varbit += ".(" + cur.Type().Name() + ")" } - for _, id := range n.Ident { - c.log("Data Kind:", cur.Kind().String()) - c.log("Field Bit:", id) - + // ! Might not work so well for non-struct pointers + skipPointers := func(cur reflect.Value, id string) reflect.Value { if cur.Kind() == reflect.Ptr { c.log("Looping over pointer") for cur.Kind() == reflect.Ptr { @@ -346,40 +347,87 @@ func (c *CTemplateSet) compileSubswitch(varholder string, holdreflect reflect.Va c.log("Data Kind:", cur.Kind().String()) c.log("Field Bit:", id) } + return cur + } + + var assLines string + var multiline = false + for _, id := range n.Ident { + c.log("Data Kind:", cur.Kind().String()) + c.log("Field Bit:", id) + cur = skipPointers(cur, id) if !cur.IsValid() { - if c.debug { - fmt.Println("Debug Data:") - fmt.Println("Holdreflect:", holdreflect) - fmt.Println("Holdreflect.Kind():", holdreflect.Kind()) - if !c.superDebug { - fmt.Println("cur.Kind():", cur.Kind().String()) - } - fmt.Println("") + c.error("Debug Data:") + c.error("Holdreflect:", holdreflect) + c.error("Holdreflect.Kind():", holdreflect.Kind()) + if !c.superDebug { + c.error("cur.Kind():", cur.Kind().String()) + } + c.error("") + if !multiline { + panic(varholder + varbit + "^\n" + "Invalid value. Maybe, it doesn't exist?") + } else { + panic(varbit + "^\n" + "Invalid value. Maybe, it doesn't exist?") } - - panic(varholder + varbit + "^\n" + "Invalid value. Maybe, it doesn't exist?") } - cur = cur.FieldByName(id) - if cur.Kind() == reflect.Interface { - cur = cur.Elem() - // TODO: Surely, there's a better way of detecting this? - /*if cur.Kind() == reflect.String && cur.Type().Name() != "string" { - varbit = "string(" + varbit + "." + id + ")"*/ - //if cur.Kind() == reflect.String && cur.Type().Name() != "string" { - if cur.Type().PkgPath() != "main" && cur.Type().PkgPath() != "" { - c.importMap["html/template"] = "html/template" - varbit += "." + id + ".(" + strings.TrimPrefix(cur.Type().PkgPath(), "html/") + "." + cur.Type().Name() + ")" + c.log("in-loop varbit: " + varbit) + if cur.Kind() == reflect.Map { + cur = cur.MapIndex(reflect.ValueOf(id)) + varbit += "[\"" + id + "\"]" + cur = skipPointers(cur, id) + + if cur.Kind() == reflect.Struct || cur.Kind() == reflect.Interface { + // TODO: Move the newVarByte declaration to the top level or to the if level, if a dispInt is only used in a particular if statement + var dispStr, newVarByte string + if cur.Kind() == reflect.Interface { + dispStr = "Int" + if !c.hasDispInt { + newVarByte = ":" + c.hasDispInt = true + } + } + // TODO: De-dupe identical struct types rather than allocating a variable for each one + if cur.Kind() == reflect.Struct { + dispStr = "Struct" + strconv.Itoa(c.localDispStructIndex) + newVarByte = ":" + c.localDispStructIndex++ + } + varbit = "disp" + dispStr + " " + newVarByte + "= " + varholder + varbit + "\n" + varholder = "disp" + dispStr + multiline = true } else { - varbit += "." + id + ".(" + cur.Type().Name() + ")" + continue } - } else { + } + if cur.Kind() != reflect.Interface { + cur = cur.FieldByName(id) varbit += "." + id } - c.log("End Cycle") + + // TODO: Handle deeply nested pointers mixed with interfaces mixed with pointers better + if cur.Kind() == reflect.Interface { + cur = cur.Elem() + varbit += ".(" + // TODO: Surely, there's a better way of doing this? + if cur.Type().PkgPath() != "main" && cur.Type().PkgPath() != "" { + c.importMap["html/template"] = "html/template" + varbit += strings.TrimPrefix(cur.Type().PkgPath(), "html/") + "." + } + varbit += cur.Type().Name() + ")" + } + c.log("End Cycle: ", varbit) } - out = c.compileVarsub(varholder+varbit, cur) + + if multiline { + assSplit := strings.Split(varbit, "\n") + varbit = assSplit[len(assSplit)-1] + assSplit = assSplit[:len(assSplit)-1] + assLines = strings.Join(assSplit, "\n") + "\n" + } + varbit = varholder + varbit + out = c.compileVarsub(varbit, cur, assLines) for _, varItem := range c.varList { if strings.HasPrefix(out, varItem.Destination) { @@ -389,20 +437,21 @@ func (c *CTemplateSet) compileSubswitch(varholder string, holdreflect reflect.Va return out case *parse.DotNode: c.log("Dot Node:", node.String()) - return c.compileVarsub(varholder, holdreflect) + return c.compileVarsub(varholder, holdreflect, "") case *parse.NilNode: panic("Nil is not a command x.x") case *parse.VariableNode: c.log("Variable Node:", n.String()) c.log(n.Ident) varname, reflectVal := c.compileIfVarsub(n.String(), varholder, templateName, holdreflect) - return c.compileVarsub(varname, reflectVal) + return c.compileVarsub(varname, reflectVal, "") case *parse.StringNode: return n.Quoted case *parse.IdentifierNode: c.log("Identifier Node:", node) c.log("Identifier Node Args:", node.Args) - return c.compileVarsub(c.compileIdentSwitch(varholder, holdreflect, templateName, node)) + out, outval := c.compileIdentSwitch(varholder, holdreflect, templateName, node) + return c.compileVarsub(out, outval, "") default: return c.unknownNode(node) } @@ -525,7 +574,6 @@ func (c *CTemplateSet) compareJoin(varholder string, holdreflect reflect.Value, func (c *CTemplateSet) compileIdentSwitch(varholder string, holdreflect reflect.Value, templateName string, node *parse.CommandNode) (out string, val reflect.Value) { c.log("in compileIdentSwitch") - //var outbuf map[int]string ArgLoop: for pos := 0; pos < len(node.Args); pos++ { id := node.Args[pos] @@ -728,7 +776,7 @@ func (c *CTemplateSet) compileBoolsub(varname string, varholder string, template return out } -func (c *CTemplateSet) compileVarsub(varname string, val reflect.Value) string { +func (c *CTemplateSet) compileVarsub(varname string, val reflect.Value, assLines string) (out string) { c.log("in compileVarsub") for _, varItem := range c.varList { if strings.HasPrefix(varname, varItem.Destination) { @@ -747,29 +795,33 @@ func (c *CTemplateSet) compileVarsub(varname string, val reflect.Value) string { val = val.Elem() } + c.log("varname: ", varname) + c.log("assLines: ", assLines) switch val.Kind() { case reflect.Int: c.importMap["strconv"] = "strconv" - return "w.Write([]byte(strconv.Itoa(" + varname + ")))\n" + out = "w.Write([]byte(strconv.Itoa(" + varname + ")))\n" case reflect.Bool: - return "if " + varname + " {\nw.Write([]byte(\"true\"))} else {\nw.Write([]byte(\"false\"))\n}\n" + out = "if " + varname + " {\nw.Write([]byte(\"true\"))} else {\nw.Write([]byte(\"false\"))\n}\n" case reflect.String: if val.Type().Name() != "string" && !strings.HasPrefix(varname, "string(") { - return "w.Write([]byte(string(" + varname + ")))\n" + varname = "string(" + varname + ")" } - return "w.Write([]byte(" + varname + "))\n" + out = "w.Write([]byte(" + varname + "))\n" case reflect.Int64: c.importMap["strconv"] = "strconv" - return "w.Write([]byte(strconv.FormatInt(" + varname + ", 10)))" + out = "w.Write([]byte(strconv.FormatInt(" + varname + ", 10)))" default: if !val.IsValid() { - panic(varname + "^\n" + "Invalid value. Maybe, it doesn't exist?") + panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?") } fmt.Println("Unknown Variable Name:", varname) fmt.Println("Unknown Kind:", val.Kind()) fmt.Println("Unknown Type:", val.Type().Name()) panic("// I don't know what this variable's type is o.o\n") } + c.log("out: ", out) + return assLines + out } func (c *CTemplateSet) compileSubtemplate(pvarholder string, pholdreflect reflect.Value, node *parse.TemplateNode) (out string) { @@ -831,6 +883,12 @@ func (c *CTemplateSet) log(args ...interface{}) { } } +func (c *CTemplateSet) error(args ...interface{}) { + if c.debug { + fmt.Println(args...) + } +} + func (c *CTemplateSet) compileCommand(*parse.CommandNode) (out string) { panic("Uh oh! Something went wrong!") } diff --git a/common/themes.go b/common/themes.go index 01041921..ce0f5d6b 100644 --- a/common/themes.go +++ b/common/themes.go @@ -22,7 +22,7 @@ import ( "../query_gen/lib" ) -type ThemeList map[string]Theme +type ThemeList map[string]Theme // ? Use pointers instead? var Themes ThemeList = make(map[string]Theme) var DefaultThemeBox atomic.Value @@ -47,6 +47,7 @@ type Theme struct { Tag string URL string Sidebars string // Allowed Values: left, right, both, false + AboutSegment bool // ? - Should this be a theme var instead? //DisableMinifier // Is this really a good idea? I don't think themes should be fighting against the minifier Settings map[string]ThemeSetting Templates []TemplateMapping diff --git a/common/topic.go b/common/topic.go index 9bfaf3dd..10eaf02e 100644 --- a/common/topic.go +++ b/common/topic.go @@ -149,9 +149,9 @@ func init() { // Flush the topic out of the cache // ? - We do a CacheRemove() here instead of mutating the pointer to avoid creating a race condition func (topic *Topic) cacheRemove() { - tcache, ok := Topics.(TopicCache) - if ok { - tcache.CacheRemove(topic.ID) + tcache := Topics.(TopicCache) + if tcache != nil { + tcache.Remove(topic.ID) } } @@ -226,7 +226,7 @@ func (topic *Topic) Delete() error { return err } - err = Fstore.RemoveTopic(topic.ParentID) + err = Forums.RemoveTopic(topic.ParentID) if err != nil && err != ErrNoRows { return err } @@ -263,10 +263,10 @@ func (topic *Topic) Copy() Topic { // TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser func GetTopicUser(tid int) (TopicUser, error) { - tcache, tok := Topics.(TopicCache) - ucache, uok := Users.(UserCache) - if tok && uok { - topic, err := tcache.CacheGet(tid) + tcache := Topics.GetCache() + ucache := Users.GetCache() + if tcache != nil && ucache != nil { + topic, err := tcache.Get(tid) if err == nil { user, err := Users.Get(topic.CreatedBy) if err != nil { @@ -292,12 +292,12 @@ func GetTopicUser(tid int) (TopicUser, error) { err := topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.PostCount, &tu.LikeCount, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level) tu.Link = BuildTopicURL(NameToSlug(tu.Title), tu.ID) tu.UserLink = BuildProfileURL(NameToSlug(tu.CreatedByName), tu.CreatedBy) - tu.Tag = Gstore.DirtyGet(tu.Group).Tag + tu.Tag = Groups.DirtyGet(tu.Group).Tag - if tok { + if tcache != nil { theTopic := Topic{ID: tu.ID, Link: tu.Link, Title: tu.Title, Content: tu.Content, CreatedBy: tu.CreatedBy, IsClosed: tu.IsClosed, Sticky: tu.Sticky, CreatedAt: tu.CreatedAt, LastReplyAt: tu.LastReplyAt, ParentID: tu.ParentID, IPAddress: tu.IPAddress, PostCount: tu.PostCount, LikeCount: tu.LikeCount} //log.Printf("theTopic: %+v\n", theTopic) - _ = tcache.CacheAdd(&theTopic) + _ = tcache.Add(&theTopic) } return tu, err } diff --git a/common/topic_cache.go b/common/topic_cache.go new file mode 100644 index 00000000..e6694311 --- /dev/null +++ b/common/topic_cache.go @@ -0,0 +1,127 @@ +package common + +import ( + "sync" + "sync/atomic" +) + +type TopicCache interface { + Get(id int) (*Topic, error) + GetUnsafe(id int) (*Topic, error) + Set(item *Topic) error + Add(item *Topic) error + AddUnsafe(item *Topic) error + Remove(id int) error + RemoveUnsafe(id int) error + Flush() + Length() int + SetCapacity(capacity int) + GetCapacity() int +} + +type MemoryTopicCache struct { + items map[int]*Topic + length int64 // sync/atomic only lets us operate on int32s and int64s + capacity int + + sync.RWMutex +} + +// NewMemoryTopicCache gives you a new instance of MemoryTopicCache +func NewMemoryTopicCache(capacity int) *MemoryTopicCache { + return &MemoryTopicCache{ + items: make(map[int]*Topic), + capacity: capacity, + } +} + +func (mts *MemoryTopicCache) Get(id int) (*Topic, error) { + mts.RLock() + item, ok := mts.items[id] + mts.RUnlock() + if ok { + return item, nil + } + return item, ErrNoRows +} + +func (mts *MemoryTopicCache) GetUnsafe(id int) (*Topic, error) { + item, ok := mts.items[id] + if ok { + return item, nil + } + return item, ErrNoRows +} + +func (mts *MemoryTopicCache) Set(item *Topic) error { + mts.Lock() + _, ok := mts.items[item.ID] + if ok { + mts.items[item.ID] = item + } else if int(mts.length) >= mts.capacity { + mts.Unlock() + return ErrStoreCapacityOverflow + } else { + mts.items[item.ID] = item + atomic.AddInt64(&mts.length, 1) + } + mts.Unlock() + return nil +} + +func (mts *MemoryTopicCache) Add(item *Topic) error { + if int(mts.length) >= mts.capacity { + return ErrStoreCapacityOverflow + } + mts.Lock() + mts.items[item.ID] = item + mts.Unlock() + atomic.AddInt64(&mts.length, 1) + return nil +} + +// TODO: Make these length increments thread-safe. Ditto for the other DataStores +func (mts *MemoryTopicCache) AddUnsafe(item *Topic) error { + if int(mts.length) >= mts.capacity { + return ErrStoreCapacityOverflow + } + mts.items[item.ID] = item + atomic.AddInt64(&mts.length, 1) + return nil +} + +// TODO: Make these length decrements thread-safe. Ditto for the other DataStores +func (mts *MemoryTopicCache) Remove(id int) error { + mts.Lock() + delete(mts.items, id) + mts.Unlock() + atomic.AddInt64(&mts.length, -1) + return nil +} + +func (mts *MemoryTopicCache) RemoveUnsafe(id int) error { + delete(mts.items, id) + atomic.AddInt64(&mts.length, -1) + return nil +} + +func (mts *MemoryTopicCache) Flush() { + mts.Lock() + mts.items = make(map[int]*Topic) + mts.length = 0 + mts.Unlock() +} + +// ! Is this concurrent? +// Length returns the number of topics in the memory cache +func (mts *MemoryTopicCache) Length() int { + return int(mts.length) +} + +func (mts *MemoryTopicCache) SetCapacity(capacity int) { + mts.capacity = capacity +} + +func (mts *MemoryTopicCache) GetCapacity() int { + return mts.capacity +} diff --git a/common/topic_store.go b/common/topic_store.go index fad86b32..8c7628cb 100644 --- a/common/topic_store.go +++ b/common/topic_store.go @@ -10,8 +10,6 @@ import ( "database/sql" "errors" "strings" - "sync" - "sync/atomic" "../query_gen/lib" ) @@ -31,44 +29,33 @@ type TopicStore interface { Exists(id int) bool Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) AddLastTopic(item *Topic, fid int) error // unimplemented + Reload(id int) error // Too much SQL logic to move into TopicCache // TODO: Implement these two methods //Replies(tid int) ([]*Reply, error) //RepliesRange(tid int, lower int, higher int) ([]*Reply, error) GlobalCount() int + + SetCache(cache TopicCache) + GetCache() TopicCache } -type TopicCache interface { - CacheGet(id int) (*Topic, error) - CacheGetUnsafe(id int) (*Topic, error) - CacheSet(item *Topic) error - CacheAdd(item *Topic) error - CacheAddUnsafe(item *Topic) error - CacheRemove(id int) error - CacheRemoveUnsafe(id int) error - Flush() - Reload(id int) error - Length() int - SetCapacity(capacity int) - GetCapacity() int -} +type DefaultTopicStore struct { + cache TopicCache -type MemoryTopicStore struct { - items map[int]*Topic - length int64 // sync/atomic only lets us operate on int32s and int64s - capacity int get *sql.Stmt exists *sql.Stmt topicCount *sql.Stmt create *sql.Stmt - sync.RWMutex } -// NewMemoryTopicStore gives you a new instance of MemoryTopicStore -func NewMemoryTopicStore(capacity int) (*MemoryTopicStore, error) { +// NewDefaultTopicStore gives you a new instance of DefaultTopicStore +func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) { acc := qgen.Builder.Accumulator() - return &MemoryTopicStore{ - items: make(map[int]*Topic), - capacity: capacity, + if cache == nil { + cache = NewNullTopicCache() + } + return &DefaultTopicStore{ + cache: cache, get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data").Where("tid = ?").Prepare(), exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(), topicCount: acc.Count("topics").Prepare(), @@ -76,84 +63,63 @@ func NewMemoryTopicStore(capacity int) (*MemoryTopicStore, error) { }, acc.FirstError() } -func (mts *MemoryTopicStore) CacheGet(id int) (*Topic, error) { - mts.RLock() - item, ok := mts.items[id] - mts.RUnlock() - if ok { - return item, nil - } - return item, ErrNoRows -} - -func (mts *MemoryTopicStore) CacheGetUnsafe(id int) (*Topic, error) { - item, ok := mts.items[id] - if ok { - return item, nil - } - return item, ErrNoRows -} - -func (mts *MemoryTopicStore) DirtyGet(id int) *Topic { - mts.RLock() - topic, ok := mts.items[id] - mts.RUnlock() - if ok { +func (mts *DefaultTopicStore) DirtyGet(id int) *Topic { + topic, err := mts.cache.Get(id) + if err == nil { return topic } topic = &Topic{ID: id} - err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) + err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) if err == nil { topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) - _ = mts.CacheAdd(topic) + _ = mts.cache.Add(topic) return topic } return BlankTopic() } -func (mts *MemoryTopicStore) Get(id int) (*Topic, error) { - mts.RLock() - topic, ok := mts.items[id] - mts.RUnlock() - if ok { +// TODO: Log weird cache errors? +func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) { + topic, err = mts.cache.Get(id) + if err == nil { return topic, nil } topic = &Topic{ID: id} - err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) + err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) if err == nil { topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) - _ = mts.CacheAdd(topic) + _ = mts.cache.Add(topic) } return topic, err } // BypassGet will always bypass the cache and pull the topic directly from the database -func (mts *MemoryTopicStore) BypassGet(id int) (*Topic, error) { +func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) { topic := &Topic{ID: id} err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) return topic, err } -func (mts *MemoryTopicStore) Reload(id int) error { +func (mts *DefaultTopicStore) Reload(id int) error { topic := &Topic{ID: id} err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) if err == nil { topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) - _ = mts.CacheSet(topic) + _ = mts.cache.Set(topic) } else { - _ = mts.CacheRemove(id) + _ = mts.cache.Remove(id) } return err } -func (mts *MemoryTopicStore) Exists(id int) bool { +func (mts *DefaultTopicStore) Exists(id int) bool { return mts.exists.QueryRow(id).Scan(&id) == nil } -func (mts *MemoryTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) { +func (mts *DefaultTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) { topicName = strings.TrimSpace(topicName) if topicName == "" { return 0, ErrNoBody @@ -177,91 +143,17 @@ func (mts *MemoryTopicStore) Create(fid int, topicName string, content string, u return 0, err } - return int(lastID), Fstore.AddTopic(int(lastID), uid, fid) -} - -func (mts *MemoryTopicStore) CacheSet(item *Topic) error { - mts.Lock() - _, ok := mts.items[item.ID] - if ok { - mts.items[item.ID] = item - } else if int(mts.length) >= mts.capacity { - mts.Unlock() - return ErrStoreCapacityOverflow - } else { - mts.items[item.ID] = item - atomic.AddInt64(&mts.length, 1) - } - mts.Unlock() - return nil -} - -func (mts *MemoryTopicStore) CacheAdd(item *Topic) error { - if int(mts.length) >= mts.capacity { - return ErrStoreCapacityOverflow - } - mts.Lock() - mts.items[item.ID] = item - mts.Unlock() - atomic.AddInt64(&mts.length, 1) - return nil -} - -// TODO: Make these length increments thread-safe. Ditto for the other DataStores -func (mts *MemoryTopicStore) CacheAddUnsafe(item *Topic) error { - if int(mts.length) >= mts.capacity { - return ErrStoreCapacityOverflow - } - mts.items[item.ID] = item - atomic.AddInt64(&mts.length, 1) - return nil -} - -// TODO: Make these length decrements thread-safe. Ditto for the other DataStores -func (mts *MemoryTopicStore) CacheRemove(id int) error { - mts.Lock() - delete(mts.items, id) - mts.Unlock() - atomic.AddInt64(&mts.length, -1) - return nil -} - -func (mts *MemoryTopicStore) CacheRemoveUnsafe(id int) error { - delete(mts.items, id) - atomic.AddInt64(&mts.length, -1) - return nil + return int(lastID), Forums.AddTopic(int(lastID), uid, fid) } // ? - What is this? Do we need it? Should it be in the main store interface? -func (mts *MemoryTopicStore) AddLastTopic(item *Topic, fid int) error { +func (mts *DefaultTopicStore) AddLastTopic(item *Topic, fid int) error { // Coming Soon... return nil } -func (mts *MemoryTopicStore) Flush() { - mts.Lock() - mts.items = make(map[int]*Topic) - mts.length = 0 - mts.Unlock() -} - -// ! Is this concurrent? -// Length returns the number of topics in the memory cache -func (mts *MemoryTopicStore) Length() int { - return int(mts.length) -} - -func (mts *MemoryTopicStore) SetCapacity(capacity int) { - mts.capacity = capacity -} - -func (mts *MemoryTopicStore) GetCapacity() int { - return mts.capacity -} - // GlobalCount returns the total number of topics on these forums -func (mts *MemoryTopicStore) GlobalCount() int { - var tcount int +func (mts *DefaultTopicStore) GlobalCount() (tcount int) { err := mts.topicCount.QueryRow().Scan(&tcount) if err != nil { LogError(err) @@ -269,91 +161,15 @@ func (mts *MemoryTopicStore) GlobalCount() int { return tcount } -type SQLTopicStore struct { - get *sql.Stmt - exists *sql.Stmt - topicCount *sql.Stmt - create *sql.Stmt +func (mts *DefaultTopicStore) SetCache(cache TopicCache) { + mts.cache = cache } -func NewSQLTopicStore() (*SQLTopicStore, error) { - acc := qgen.Builder.Accumulator() - return &SQLTopicStore{ - get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data").Where("tid = ?").Prepare(), - exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(), - topicCount: acc.Count("topics").Prepare(), - create: acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(), - }, acc.FirstError() -} - -func (sts *SQLTopicStore) DirtyGet(id int) *Topic { - topic := &Topic{ID: id} - err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) - topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) - if err != nil { - return BlankTopic() +// TODO: We're temporarily doing this so that you can do tcache != nil in getTopicUser. Refactor it. +func (mts *DefaultTopicStore) GetCache() TopicCache { + _, ok := mts.cache.(*NullTopicCache) + if ok { + return nil } - return topic -} - -func (sts *SQLTopicStore) Get(id int) (*Topic, error) { - topic := Topic{ID: id} - err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) - topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) - return &topic, err -} - -// BypassGet is an alias of Get(), as we don't have a cache for SQLTopicStore -func (sts *SQLTopicStore) BypassGet(id int) (*Topic, error) { - topic := &Topic{ID: id} - err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data) - topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) - return topic, err -} - -func (sts *SQLTopicStore) Exists(id int) bool { - return sts.exists.QueryRow(id).Scan(&id) == nil -} - -func (sts *SQLTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) { - topicName = strings.TrimSpace(topicName) - if topicName == "" { - return 0, ErrNoBody - } - - content = strings.TrimSpace(content) - parsedContent := ParseMessage(content, fid, "forums") - if strings.TrimSpace(parsedContent) == "" { - return 0, ErrNoBody - } - - wcount := WordCount(content) - // TODO: Move this statement into the topic store - res, err := sts.create.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid) - if err != nil { - return 0, err - } - - lastID, err := res.LastInsertId() - if err != nil { - return 0, err - } - - return int(lastID), Fstore.AddTopic(int(lastID), uid, fid) -} - -// ? - What're we going to do about this? -func (sts *SQLTopicStore) AddLastTopic(item *Topic, fid int) error { - // Coming Soon... - return nil -} - -// GlobalCount returns the total number of topics on these forums -func (sts *SQLTopicStore) GlobalCount() int { - var tcount int - err := sts.topicCount.QueryRow().Scan(&tcount) - if err != nil { - LogError(err) - } - return tcount + return mts.cache } diff --git a/common/user.go b/common/user.go index 193925cc..d461f432 100644 --- a/common/user.go +++ b/common/user.go @@ -7,8 +7,6 @@ package common import ( - //"log" - //"fmt" "database/sql" "errors" "strconv" @@ -111,14 +109,15 @@ func (user *User) Init() { user.Avatar = strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(user.ID), 1) } user.Link = BuildProfileURL(NameToSlug(user.Name), user.ID) - user.Tag = Gstore.DirtyGet(user.Group).Tag + user.Tag = Groups.DirtyGet(user.Group).Tag user.InitPerms() } +// TODO: Refactor this idiom into something shorter, maybe with a NullUserCache when one isn't set? func (user *User) CacheRemove() { - ucache, ok := Users.(UserCache) - if ok { - ucache.CacheRemove(user.ID) + ucache := Users.GetCache() + if ucache != nil { + ucache.Remove(user.ID) } } @@ -337,7 +336,7 @@ func (user *User) InitPerms() { user.Group = user.TempGroup } - group := Gstore.DirtyGet(user.Group) + group := Groups.DirtyGet(user.Group) if user.IsSuperAdmin { user.Perms = AllPerms user.PluginPerms = AllPluginPerms diff --git a/common/user_cache.go b/common/user_cache.go new file mode 100644 index 00000000..f10c9aba --- /dev/null +++ b/common/user_cache.go @@ -0,0 +1,146 @@ +package common + +import ( + "sync" + "sync/atomic" +) + +type UserCache interface { + Get(id int) (*User, error) + GetUnsafe(id int) (*User, error) + BulkGet(ids []int) (list []*User) + Set(item *User) error + Add(item *User) error + AddUnsafe(item *User) error + Remove(id int) error + RemoveUnsafe(id int) error + Flush() + Length() int + SetCapacity(capacity int) + GetCapacity() int +} + +type MemoryUserCache struct { + items map[int]*User + length int64 + capacity int + + sync.RWMutex +} + +// NewMemoryUserCache gives you a new instance of MemoryUserCache +func NewMemoryUserCache(capacity int) *MemoryUserCache { + return &MemoryUserCache{ + items: make(map[int]*User), + capacity: capacity, + } +} + +func (mus *MemoryUserCache) Get(id int) (*User, error) { + mus.RLock() + item, ok := mus.items[id] + mus.RUnlock() + if ok { + return item, nil + } + return item, ErrNoRows +} + +func (mus *MemoryUserCache) BulkGet(ids []int) (list []*User) { + list = make([]*User, len(ids)) + mus.RLock() + for i, id := range ids { + list[i] = mus.items[id] + } + mus.RUnlock() + return list +} + +func (mus *MemoryUserCache) GetUnsafe(id int) (*User, error) { + item, ok := mus.items[id] + if ok { + return item, nil + } + return item, ErrNoRows +} + +func (mus *MemoryUserCache) Set(item *User) error { + mus.Lock() + user, ok := mus.items[item.ID] + if ok { + mus.Unlock() + *user = *item + } else if int(mus.length) >= mus.capacity { + mus.Unlock() + return ErrStoreCapacityOverflow + } else { + mus.items[item.ID] = item + mus.Unlock() + atomic.AddInt64(&mus.length, 1) + } + return nil +} + +func (mus *MemoryUserCache) Add(item *User) error { + if int(mus.length) >= mus.capacity { + return ErrStoreCapacityOverflow + } + mus.Lock() + mus.items[item.ID] = item + mus.length = int64(len(mus.items)) + mus.Unlock() + return nil +} + +func (mus *MemoryUserCache) AddUnsafe(item *User) error { + if int(mus.length) >= mus.capacity { + return ErrStoreCapacityOverflow + } + mus.items[item.ID] = item + mus.length = int64(len(mus.items)) + return nil +} + +func (mus *MemoryUserCache) Remove(id int) error { + mus.Lock() + _, ok := mus.items[id] + if !ok { + mus.Unlock() + return ErrNoRows + } + delete(mus.items, id) + mus.Unlock() + atomic.AddInt64(&mus.length, -1) + return nil +} + +func (mus *MemoryUserCache) RemoveUnsafe(id int) error { + _, ok := mus.items[id] + if !ok { + return ErrNoRows + } + delete(mus.items, id) + atomic.AddInt64(&mus.length, -1) + return nil +} + +func (mus *MemoryUserCache) Flush() { + mus.Lock() + mus.items = make(map[int]*User) + mus.length = 0 + mus.Unlock() +} + +// ! Is this concurrent? +// Length returns the number of users in the memory cache +func (mus *MemoryUserCache) Length() int { + return int(mus.length) +} + +func (mus *MemoryUserCache) SetCapacity(capacity int) { + mus.capacity = capacity +} + +func (mus *MemoryUserCache) GetCapacity() int { + return mus.capacity +} diff --git a/common/user_store.go b/common/user_store.go index a67c084d..513d3899 100644 --- a/common/user_store.go +++ b/common/user_store.go @@ -5,8 +5,6 @@ import ( "errors" "log" "strconv" - "sync" - "sync/atomic" "../query_gen/lib" "golang.org/x/crypto/bcrypt" @@ -25,43 +23,32 @@ type UserStore interface { BulkGetMap(ids []int) (map[int]*User, error) BypassGet(id int) (*User, error) Create(username string, password string, email string, group int, active bool) (int, error) - GlobalCount() int -} - -type UserCache interface { - CacheGet(id int) (*User, error) - CacheGetUnsafe(id int) (*User, error) - CacheSet(item *User) error - CacheAdd(item *User) error - CacheAddUnsafe(item *User) error - CacheRemove(id int) error - CacheRemoveUnsafe(id int) error - Flush() Reload(id int) error - Length() int - SetCapacity(capacity int) - GetCapacity() int + GlobalCount() int + + SetCache(cache UserCache) + GetCache() UserCache } -type MemoryUserStore struct { - items map[int]*User - length int64 - capacity int +type DefaultUserStore struct { + cache UserCache + get *sql.Stmt exists *sql.Stmt register *sql.Stmt usernameExists *sql.Stmt userCount *sql.Stmt - sync.RWMutex } -// NewMemoryUserStore gives you a new instance of MemoryUserStore -func NewMemoryUserStore(capacity int) (*MemoryUserStore, error) { +// NewDefaultUserStore gives you a new instance of DefaultUserStore +func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) { acc := qgen.Builder.Accumulator() + if cache == nil { + cache = NewNullUserCache() + } // TODO: Add an admin version of registerStmt with more flexibility? - return &MemoryUserStore{ - items: make(map[int]*User), - capacity: capacity, + return &DefaultUserStore{ + cache: cache, get: acc.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", ""), exists: acc.SimpleSelect("users", "uid", "uid = ?", "", ""), register: acc.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()"), @@ -70,75 +57,43 @@ func NewMemoryUserStore(capacity int) (*MemoryUserStore, error) { }, acc.FirstError() } -func (mus *MemoryUserStore) CacheGet(id int) (*User, error) { - mus.RLock() - item, ok := mus.items[id] - mus.RUnlock() - if ok { - return item, nil - } - return item, ErrNoRows -} - -func (mus *MemoryUserStore) CacheGetUnsafe(id int) (*User, error) { - item, ok := mus.items[id] - if ok { - return item, nil - } - return item, ErrNoRows -} - -func (mus *MemoryUserStore) DirtyGet(id int) *User { - mus.RLock() - user, ok := mus.items[id] - mus.RUnlock() - if ok { +func (mus *DefaultUserStore) DirtyGet(id int) *User { + user, err := mus.cache.Get(id) + if err == nil { return user } user = &User{ID: id, Loggedin: true} - err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) + err = mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) user.Init() if err == nil { - mus.CacheSet(user) + mus.cache.Set(user) return user } return BlankUser() } -func (mus *MemoryUserStore) Get(id int) (*User, error) { - mus.RLock() - user, ok := mus.items[id] - mus.RUnlock() - if ok { +// TODO: Log weird cache errors? Not just here but in every *Cache? +func (mus *DefaultUserStore) Get(id int) (*User, error) { + user, err := mus.cache.Get(id) + if err == nil { return user, nil } user = &User{ID: id, Loggedin: true} - err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) + err = mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) user.Init() if err == nil { - mus.CacheSet(user) + mus.cache.Set(user) } return user, err } -// WARNING: We did a little hack to make this as thin and quick as possible to reduce lock contention, use the * Cascade* methods instead for normal use -func (mus *MemoryUserStore) bulkGet(ids []int) (list []*User) { - list = make([]*User, len(ids)) - mus.RLock() - for i, id := range ids { - list[i] = mus.items[id] - } - mus.RUnlock() - return list -} - // TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts? // TODO: ID of 0 should always error? -func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) { +func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) { var idCount = len(ids) list = make(map[int]*User) if idCount == 0 { @@ -146,7 +101,7 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error } var stillHere []int - sliceList := mus.bulkGet(ids) + sliceList := mus.cache.BulkGet(ids) for i, sliceItem := range sliceList { if sliceItem != nil { list[sliceItem.ID] = sliceItem @@ -184,7 +139,7 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error } user.Init() - mus.CacheSet(user) + mus.cache.Set(user) list[user.ID] = user } @@ -216,7 +171,7 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error return list, err } -func (mus *MemoryUserStore) BypassGet(id int) (*User, error) { +func (mus *DefaultUserStore) BypassGet(id int) (*User, error) { user := &User{ID: id, Loggedin: true} err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) @@ -224,20 +179,20 @@ func (mus *MemoryUserStore) BypassGet(id int) (*User, error) { return user, err } -func (mus *MemoryUserStore) Reload(id int) error { +func (mus *DefaultUserStore) Reload(id int) error { user := &User{ID: id, Loggedin: true} err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) if err != nil { - mus.CacheRemove(id) + mus.cache.Remove(id) return err } user.Init() - _ = mus.CacheSet(user) + _ = mus.cache.Set(user) return nil } -func (mus *MemoryUserStore) Exists(id int) bool { +func (mus *DefaultUserStore) Exists(id int) bool { err := mus.exists.QueryRow(id).Scan(&id) if err != nil && err != ErrNoRows { LogError(err) @@ -245,210 +200,8 @@ func (mus *MemoryUserStore) Exists(id int) bool { return err != ErrNoRows } -func (mus *MemoryUserStore) CacheSet(item *User) error { - mus.Lock() - user, ok := mus.items[item.ID] - if ok { - mus.Unlock() - *user = *item - } else if int(mus.length) >= mus.capacity { - mus.Unlock() - return ErrStoreCapacityOverflow - } else { - mus.items[item.ID] = item - mus.Unlock() - atomic.AddInt64(&mus.length, 1) - } - return nil -} - -func (mus *MemoryUserStore) CacheAdd(item *User) error { - if int(mus.length) >= mus.capacity { - return ErrStoreCapacityOverflow - } - mus.Lock() - mus.items[item.ID] = item - mus.length = int64(len(mus.items)) - mus.Unlock() - return nil -} - -func (mus *MemoryUserStore) CacheAddUnsafe(item *User) error { - if int(mus.length) >= mus.capacity { - return ErrStoreCapacityOverflow - } - mus.items[item.ID] = item - mus.length = int64(len(mus.items)) - return nil -} - -func (mus *MemoryUserStore) CacheRemove(id int) error { - mus.Lock() - _, ok := mus.items[id] - if !ok { - mus.Unlock() - return ErrNoRows - } - delete(mus.items, id) - mus.Unlock() - atomic.AddInt64(&mus.length, -1) - return nil -} - -func (mus *MemoryUserStore) CacheRemoveUnsafe(id int) error { - _, ok := mus.items[id] - if !ok { - return ErrNoRows - } - delete(mus.items, id) - atomic.AddInt64(&mus.length, -1) - return nil -} - // TODO: Change active to a bool? -func (mus *MemoryUserStore) Create(username string, password string, email string, group int, active bool) (int, error) { - // Is this username already taken..? - err := mus.usernameExists.QueryRow(username).Scan(&username) - if err != ErrNoRows { - return 0, ErrAccountExists - } - - salt, err := GenerateSafeString(SaltLength) - if err != nil { - return 0, err - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password+salt), bcrypt.DefaultCost) - if err != nil { - return 0, err - } - - res, err := mus.register.Exec(username, email, string(hashedPassword), salt, group, active) - if err != nil { - return 0, err - } - - lastID, err := res.LastInsertId() - return int(lastID), err -} - -func (mus *MemoryUserStore) Flush() { - mus.Lock() - mus.items = make(map[int]*User) - mus.length = 0 - mus.Unlock() -} - -// ! Is this concurrent? -// Length returns the number of users in the memory cache -func (mus *MemoryUserStore) Length() int { - return int(mus.length) -} - -func (mus *MemoryUserStore) SetCapacity(capacity int) { - mus.capacity = capacity -} - -func (mus *MemoryUserStore) GetCapacity() int { - return mus.capacity -} - -// GlobalCount returns the total number of users registered on the forums -func (mus *MemoryUserStore) GlobalCount() (ucount int) { - err := mus.userCount.QueryRow().Scan(&ucount) - if err != nil { - LogError(err) - } - return ucount -} - -type SQLUserStore struct { - get *sql.Stmt - exists *sql.Stmt - register *sql.Stmt - usernameExists *sql.Stmt - userCount *sql.Stmt -} - -func NewSQLUserStore() (*SQLUserStore, error) { - acc := qgen.Builder.Accumulator() - // TODO: Add an admin version of registerStmt with more flexibility? - return &SQLUserStore{ - get: acc.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", ""), - exists: acc.SimpleSelect("users", "uid", "uid = ?", "", ""), - register: acc.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()"), - usernameExists: acc.SimpleSelect("users", "name", "name = ?", "", ""), - userCount: acc.SimpleCount("users", "", ""), - }, acc.FirstError() -} - -func (mus *SQLUserStore) DirtyGet(id int) *User { - user := &User{ID: id, Loggedin: true} - err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) - - user.Init() - if err != nil { - return BlankUser() - } - return user -} - -func (mus *SQLUserStore) Get(id int) (*User, error) { - user := &User{ID: id, Loggedin: true} - err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) - - user.Init() - return user, err -} - -// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts? -func (mus *SQLUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) { - var qlist string - var uidList []interface{} - for _, id := range ids { - uidList = append(uidList, strconv.Itoa(id)) - qlist += "?," - } - qlist = qlist[0 : len(qlist)-1] - - acc := qgen.Builder.Accumulator() - rows, err := acc.Select("users").Columns("uid, name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group").Where("uid IN(" + qlist + ")").Query(uidList...) - if err != nil { - return nil, err - } - - list = make(map[int]*User) - for rows.Next() { - user := &User{Loggedin: true} - err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) - if err != nil { - return nil, err - } - - user.Init() - list[user.ID] = user - } - - return list, nil -} - -func (mus *SQLUserStore) BypassGet(id int) (*User, error) { - user := &User{ID: id, Loggedin: true} - err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) - - user.Init() - return user, err -} - -func (mus *SQLUserStore) Exists(id int) bool { - err := mus.exists.QueryRow(id).Scan(&id) - if err != nil && err != ErrNoRows { - LogError(err) - } - return err != ErrNoRows -} - -func (mus *SQLUserStore) Create(username string, password string, email string, group int, active bool) (int, error) { +func (mus *DefaultUserStore) Create(username string, password string, email string, group int, active bool) (int, error) { // Is this username already taken..? err := mus.usernameExists.QueryRow(username).Scan(&username) if err != ErrNoRows { @@ -475,7 +228,7 @@ func (mus *SQLUserStore) Create(username string, password string, email string, } // GlobalCount returns the total number of users registered on the forums -func (mus *SQLUserStore) GlobalCount() (ucount int) { +func (mus *DefaultUserStore) GlobalCount() (ucount int) { err := mus.userCount.QueryRow().Scan(&ucount) if err != nil { LogError(err) @@ -483,54 +236,15 @@ func (mus *SQLUserStore) GlobalCount() (ucount int) { return ucount } -// TODO: MockUserStore - -// NullUserStore is here for tests because Go doesn't have short-circuiting -type NullUserStore struct { +func (mus *DefaultUserStore) SetCache(cache UserCache) { + mus.cache = cache } -func (nus *NullUserStore) CacheGet(_ int) (*User, error) { - return nil, ErrNoRows -} - -func (nus *NullUserStore) CacheGetUnsafe(_ int) (*User, error) { - return nil, ErrNoRows -} - -func (nus *NullUserStore) CacheSet(_ *User) error { - return ErrStoreCapacityOverflow -} - -func (nus *NullUserStore) CacheAdd(_ *User) error { - return ErrStoreCapacityOverflow -} - -func (nus *NullUserStore) CacheAddUnsafe(_ *User) error { - return ErrStoreCapacityOverflow -} - -func (nus *NullUserStore) CacheRemove(_ int) error { - return ErrNoRows -} - -func (nus *NullUserStore) CacheRemoveUnsafe(_ int) error { - return ErrNoRows -} - -func (nus *NullUserStore) Flush() { -} - -func (nus *NullUserStore) Reload(_ int) error { - return ErrNoRows -} - -func (nus *NullUserStore) Length() int { - return 0 -} - -func (nus *NullUserStore) SetCapacity(_ int) { -} - -func (nus *NullUserStore) GetCapacity() int { - return 0 +// TODO: We're temporarily doing this so that you can do ucache != nil in getTopicUser. Refactor it. +func (mus *DefaultUserStore) GetCache() UserCache { + _, ok := mus.cache.(*NullUserCache) + if ok { + return nil + } + return mus.cache } diff --git a/common/word_filters.go b/common/word_filters.go index 73598f47..c0a440f8 100644 --- a/common/word_filters.go +++ b/common/word_filters.go @@ -33,26 +33,37 @@ func init() { } func LoadWordFilters() error { - rows, err := filterStmts.getWordFilters.Query() + var wordFilters = WordFilterMap(make(map[int]WordFilter)) + filters, err := wordFilters.BypassGetAll() if err != nil { return err } + + for _, filter := range filters { + wordFilters[filter.ID] = filter + } + + WordFilterBox.Store(wordFilters) + return nil +} + +// TODO: Return pointers to word filters intead to save memory? +func (wBox WordFilterMap) BypassGetAll() (filters []WordFilter, err error) { + rows, err := filterStmts.getWordFilters.Query() + if err != nil { + return nil, err + } defer rows.Close() - var wordFilters = WordFilterMap(make(map[int]WordFilter)) - var wfid int - var find string - var replacement string - for rows.Next() { - err := rows.Scan(&wfid, &find, &replacement) + filter := WordFilter{ID: 0} + err := rows.Scan(&filter.ID, &filter.Find, &filter.Replacement) if err != nil { - return err + return filters, err } - wordFilters[wfid] = WordFilter{ID: wfid, Find: find, Replacement: replacement} + filters = append(filters, filter) } - WordFilterBox.Store(wordFilters) - return rows.Err() + return filters, rows.Err() } func AddWordFilter(id int, find string, replacement string) { diff --git a/database.go b/database.go index 0f37fc30..c29d1883 100644 --- a/database.go +++ b/database.go @@ -35,47 +35,50 @@ func InitDatabase() (err error) { } log.Print("Loading the usergroups.") - common.Gstore, err = common.NewMemoryGroupStore() + common.Groups, err = common.NewMemoryGroupStore() if err != nil { return err } - err2 := common.Gstore.LoadGroups() + err2 := common.Groups.LoadGroups() if err2 != nil { return err2 } // We have to put this here, otherwise LoadForums() won't be able to get the last poster data when building it's forums log.Print("Initialising the user and topic stores") + + var ucache common.UserCache + var tcache common.TopicCache if common.Config.CacheTopicUser == common.CACHE_STATIC { - common.Users, err = common.NewMemoryUserStore(common.Config.UserCacheCapacity) - common.Topics, err2 = common.NewMemoryTopicStore(common.Config.TopicCacheCapacity) - } else { - common.Users, err = common.NewSQLUserStore() - common.Topics, err2 = common.NewSQLTopicStore() + ucache = common.NewMemoryUserCache(common.Config.UserCacheCapacity) + tcache = common.NewMemoryTopicCache(common.Config.TopicCacheCapacity) } + + common.Users, err = common.NewDefaultUserStore(ucache) if err != nil { return err } - if err2 != nil { + common.Topics, err = common.NewDefaultTopicStore(tcache) + if err != nil { return err2 } log.Print("Loading the forums.") - common.Fstore, err = common.NewMemoryForumStore() + common.Forums, err = common.NewMemoryForumStore() if err != nil { return err } - err = common.Fstore.LoadForums() + err = common.Forums.LoadForums() if err != nil { return err } log.Print("Loading the forum permissions.") - common.Fpstore, err = common.NewMemoryForumPermsStore() + common.FPStore, err = common.NewMemoryForumPermsStore() if err != nil { return err } - err = common.Fpstore.Init() + err = common.FPStore.Init() if err != nil { return err } diff --git a/extend/guilds/lib/guild_store.go b/extend/guilds/lib/guild_store.go new file mode 100644 index 00000000..018555f8 --- /dev/null +++ b/extend/guilds/lib/guild_store.go @@ -0,0 +1,44 @@ +package guilds + +import "database/sql" +import "../../../query_gen/lib" + +var Gstore GuildStore + +type GuildStore interface { + Get(guildID int) (guild *Guild, err error) + Create(name string, desc string, active bool, privacy int, uid int, fid int) (int, error) +} + +type SQLGuildStore struct { + get *sql.Stmt + create *sql.Stmt +} + +func NewSQLGuildStore() (*SQLGuildStore, error) { + acc := qgen.Builder.Accumulator() + return &SQLGuildStore{ + get: acc.Select("guilds").Columns("name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime").Where("guildID = ?").Prepare(), + create: acc.Insert("guilds").Columns("name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime").Fields("?,?,?,?,1,?,1,?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), + }, acc.FirstError() +} + +func (store *SQLGuildStore) Close() { + _ = store.get.Close() + _ = store.create.Close() +} + +func (store *SQLGuildStore) Get(guildID int) (guild *Guild, err error) { + guild = &Guild{ID: guildID} + err = store.get.QueryRow(guildID).Scan(&guild.Name, &guild.Desc, &guild.Active, &guild.Privacy, &guild.Joinable, &guild.Owner, &guild.MemberCount, &guild.MainForumID, &guild.Backdrop, &guild.CreatedAt, &guild.LastUpdateTime) + return guild, err +} + +func (store *SQLGuildStore) Create(name string, desc string, active bool, privacy int, uid int, fid int) (int, error) { + res, err := store.create.Exec(name, desc, active, privacy, uid, fid) + if err != nil { + return 0, err + } + lastID, err := res.LastInsertId() + return int(lastID), err +} diff --git a/extend/guilds/lib/guilds.go b/extend/guilds/lib/guilds.go index 09a6e796..96e27c14 100644 --- a/extend/guilds/lib/guilds.go +++ b/extend/guilds/lib/guilds.go @@ -21,8 +21,6 @@ var ListStmt *sql.Stmt var MemberListStmt *sql.Stmt var MemberListJoinStmt *sql.Stmt var GetMemberStmt *sql.Stmt -var GetGuildStmt *sql.Stmt -var CreateGuildStmt *sql.Stmt var AttachForumStmt *sql.Stmt var UnattachForumStmt *sql.Stmt var AddMemberStmt *sql.Stmt @@ -105,8 +103,8 @@ func PrebuildTmplList(user common.User, headerVars *common.HeaderVars) common.CT CreatedAt: "date", LastUpdateTime: "date", MainForumID: 1, - MainForum: common.Fstore.DirtyGet(1), - Forums: []*common.Forum{common.Fstore.DirtyGet(1)}, + MainForum: common.Forums.DirtyGet(1), + Forums: []*common.Forum{common.Forums.DirtyGet(1)}, }, } listPage := ListPage{"Guild List", user, headerVars, guildList} @@ -127,9 +125,9 @@ func CommonAreaWidgets(headerVars *common.HeaderVars) { return } - if common.Themes[headerVars.ThemeName].Sidebars == "left" { + if common.Themes[headerVars.Theme.Name].Sidebars == "left" { headerVars.Widgets.LeftSidebar = template.HTML(string(b.Bytes())) - } else if common.Themes[headerVars.ThemeName].Sidebars == "right" || common.Themes[headerVars.ThemeName].Sidebars == "both" { + } else if common.Themes[headerVars.Theme.Name].Sidebars == "right" || common.Themes[headerVars.Theme.Name].Sidebars == "both" { headerVars.Widgets.RightSidebar = template.HTML(string(b.Bytes())) } } @@ -151,9 +149,9 @@ func GuildWidgets(headerVars *common.HeaderVars, guildItem *Guild) (success bool return false } - if themes[headerVars.ThemeName].Sidebars == "left" { + if themes[headerVars.Theme.Name].Sidebars == "left" { headerVars.Widgets.LeftSidebar = template.HTML(string(b.Bytes())) - } else if themes[headerVars.ThemeName].Sidebars == "right" || themes[headerVars.ThemeName].Sidebars == "both" { + } else if themes[headerVars.Theme.Name].Sidebars == "right" || themes[headerVars.Theme.Name].Sidebars == "both" { headerVars.Widgets.RightSidebar = template.HTML(string(b.Bytes())) } else { return false @@ -194,19 +192,13 @@ func RouteGuildList(w http.ResponseWriter, r *http.Request, user common.User) co } pi := ListPage{"Guild List", user, headerVars, guildList} - err = common.RunThemeTemplate(headerVars.ThemeName, "guilds_guild_list", pi, w) + err = common.RunThemeTemplate(headerVars.Theme.Name, "guilds_guild_list", pi, w) if err != nil { return common.InternalError(err, w, r) } return nil } -func GetGuild(guildID int) (guildItem *Guild, err error) { - guildItem = &Guild{ID: guildID} - err = GetGuildStmt.QueryRow(guildID).Scan(&guildItem.Name, &guildItem.Desc, &guildItem.Active, &guildItem.Privacy, &guildItem.Joinable, &guildItem.Owner, &guildItem.MemberCount, &guildItem.MainForumID, &guildItem.Backdrop, &guildItem.CreatedAt, &guildItem.LastUpdateTime) - return guildItem, err -} - func MiddleViewGuild(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { // SEO URLs... halves := strings.Split(r.URL.Path[len("/guild/"):], ".") @@ -218,7 +210,7 @@ func MiddleViewGuild(w http.ResponseWriter, r *http.Request, user common.User) c return common.PreError("Not a valid guild ID", w, r) } - guildItem, err := GetGuild(guildID) + guildItem, err := Gstore.Get(guildID) if err != nil { return common.LocalError("Bad guild", w, r, user) } @@ -277,32 +269,28 @@ func RouteCreateGuildSubmit(w http.ResponseWriter, r *http.Request, user common. } // Create the backing forum - fid, err := common.Fstore.Create(guildName, "", true, "") + fid, err := common.Forums.Create(guildName, "", true, "") if err != nil { return common.InternalError(err, w, r) } - res, err := CreateGuildStmt.Exec(guildName, guildDesc, guildActive, guildPrivacy, user.ID, fid) - if err != nil { - return common.InternalError(err, w, r) - } - lastID, err := res.LastInsertId() + gid, err := Gstore.Create(guildName, guildDesc, guildActive, guildPrivacy, user.ID, fid) if err != nil { return common.InternalError(err, w, r) } // Add the main backing forum to the forum list - err = AttachForum(int(lastID), fid) + err = AttachForum(gid, fid) if err != nil { return common.InternalError(err, w, r) } - _, err = AddMemberStmt.Exec(lastID, user.ID, 2) + _, err = AddMemberStmt.Exec(gid, user.ID, 2) if err != nil { return common.InternalError(err, w, r) } - http.Redirect(w, r, BuildGuildURL(common.NameToSlug(guildName), int(lastID)), http.StatusSeeOther) + http.Redirect(w, r, BuildGuildURL(common.NameToSlug(guildName), gid), http.StatusSeeOther) return nil } @@ -322,9 +310,7 @@ func RouteMemberList(w http.ResponseWriter, r *http.Request, user common.User) c return common.PreError("Not a valid group ID", w, r) } - var guildItem = &Guild{ID: guildID} - var mainForum int // Unused - err = GetGuildStmt.QueryRow(guildID).Scan(&guildItem.Name, &guildItem.Desc, &guildItem.Active, &guildItem.Privacy, &guildItem.Joinable, &guildItem.Owner, &guildItem.MemberCount, &mainForum, &guildItem.Backdrop, &guildItem.CreatedAt, &guildItem.LastUpdateTime) + guildItem, err := Gstore.Get(guildID) if err != nil { return common.LocalError("Bad group", w, r, user) } @@ -380,7 +366,7 @@ func RouteMemberList(w http.ResponseWriter, r *http.Request, user common.User) c return nil } } - err = common.RunThemeTemplate(headerVars.ThemeName, "guilds_member_list", pi, w) + err = common.RunThemeTemplate(headerVars.Theme.Name, "guilds_member_list", pi, w) if err != nil { return common.InternalError(err, w, r) } @@ -439,7 +425,7 @@ func TrowAssign(args ...interface{}) interface{} { // TODO: It would be nice, if you could select one of the boards in the group from that drop-down rather than just the one you got linked from func TopicCreatePreLoop(args ...interface{}) interface{} { var fid = args[2].(int) - if common.Fstore.DirtyGet(fid).ParentType == "guild" { + if common.Forums.DirtyGet(fid).ParentType == "guild" { var strictmode = args[5].(*bool) *strictmode = true } @@ -452,14 +438,14 @@ func TopicCreatePreLoop(args ...interface{}) interface{} { func ForumCheck(args ...interface{}) (skip bool, rerr common.RouteError) { var r = args[1].(*http.Request) var fid = args[3].(*int) - var forum = common.Fstore.DirtyGet(*fid) + var forum = common.Forums.DirtyGet(*fid) if forum.ParentType == "guild" { var err error var w = args[0].(http.ResponseWriter) guildItem, ok := r.Context().Value("guilds_current_group").(*Guild) if !ok { - guildItem, err = GetGuild(forum.ParentID) + guildItem, err = Gstore.Get(forum.ParentID) if err != nil { return true, common.InternalError(errors.New("Unable to find the parent group for a forum"), w, r) } diff --git a/gen_mssql.go b/gen_mssql.go index afba5918..9ab0b781 100644 --- a/gen_mssql.go +++ b/gen_mssql.go @@ -10,7 +10,6 @@ import "./common" // nolint type Stmts struct { getPassword *sql.Stmt - getSettings *sql.Stmt isPluginActive *sql.Stmt getUsersOffset *sql.Stmt isThemeDefault *sql.Stmt @@ -45,7 +44,6 @@ type Stmts struct { createWordFilter *sql.Stmt editReply *sql.Stmt editProfileReply *sql.Stmt - updateSetting *sql.Stmt updatePlugin *sql.Stmt updatePluginInstall *sql.Stmt updateTheme *sql.Stmt @@ -61,7 +59,6 @@ type Stmts struct { deleteActivityStreamMatch *sql.Stmt deleteWordFilter *sql.Stmt reportExists *sql.Stmt - modlogCount *sql.Stmt notifyWatchers *sql.Stmt getActivityFeedByWatcher *sql.Stmt @@ -90,13 +87,6 @@ func _gen_mssql() (err error) { return err } - log.Print("Preparing getSettings statement.") - stmts.getSettings, err = db.Prepare("SELECT [name],[content],[type] FROM [settings]") - if err != nil { - log.Print("Bad Query: ","SELECT [name],[content],[type] FROM [settings]") - return err - } - log.Print("Preparing isPluginActive statement.") stmts.isPluginActive, err = db.Prepare("SELECT [active] FROM [plugins] WHERE [uname] = ?1") if err != nil { @@ -335,13 +325,6 @@ func _gen_mssql() (err error) { return err } - log.Print("Preparing updateSetting statement.") - stmts.updateSetting, err = db.Prepare("UPDATE [settings] SET [content] = ? WHERE [name] = ?") - if err != nil { - log.Print("Bad Query: ","UPDATE [settings] SET [content] = ? WHERE [name] = ?") - return err - } - log.Print("Preparing updatePlugin statement.") stmts.updatePlugin, err = db.Prepare("UPDATE [plugins] SET [active] = ? WHERE [uname] = ?") if err != nil { @@ -447,13 +430,6 @@ func _gen_mssql() (err error) { return err } - log.Print("Preparing modlogCount statement.") - stmts.modlogCount, err = db.Prepare("SELECT COUNT(*) AS [count] FROM [moderation_logs]") - if err != nil { - log.Print("Bad Query: ","SELECT COUNT(*) AS [count] FROM [moderation_logs]") - return err - } - log.Print("Preparing notifyWatchers statement.") stmts.notifyWatchers, err = db.Prepare("INSERT INTO [activity_stream_matches] ([watcher],[asid]) SELECT [activity_subscriptions].[user],[activity_stream].[asid] FROM [activity_stream] INNER JOIN [activity_subscriptions] ON [activity_subscriptions].[targetType] = [activity_stream].[elementType] AND [activity_subscriptions].[targetID] = [activity_stream].[elementID] AND [activity_subscriptions].[user] != [activity_stream].[actor] WHERE [asid] = ?1") if err != nil { diff --git a/gen_mysql.go b/gen_mysql.go index 4c5df0d6..b771e4fc 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -12,7 +12,6 @@ import "./common" // nolint type Stmts struct { getPassword *sql.Stmt - getSettings *sql.Stmt isPluginActive *sql.Stmt getUsersOffset *sql.Stmt isThemeDefault *sql.Stmt @@ -47,7 +46,6 @@ type Stmts struct { createWordFilter *sql.Stmt editReply *sql.Stmt editProfileReply *sql.Stmt - updateSetting *sql.Stmt updatePlugin *sql.Stmt updatePluginInstall *sql.Stmt updateTheme *sql.Stmt @@ -63,7 +61,6 @@ type Stmts struct { deleteActivityStreamMatch *sql.Stmt deleteWordFilter *sql.Stmt reportExists *sql.Stmt - modlogCount *sql.Stmt notifyWatchers *sql.Stmt getActivityFeedByWatcher *sql.Stmt @@ -91,12 +88,6 @@ func _gen_mysql() (err error) { return err } - log.Print("Preparing getSettings statement.") - stmts.getSettings, err = db.Prepare("SELECT `name`,`content`,`type` FROM `settings`") - if err != nil { - return err - } - log.Print("Preparing isPluginActive statement.") stmts.isPluginActive, err = db.Prepare("SELECT `active` FROM `plugins` WHERE `uname` = ?") if err != nil { @@ -301,12 +292,6 @@ func _gen_mysql() (err error) { return err } - log.Print("Preparing updateSetting statement.") - stmts.updateSetting, err = db.Prepare("UPDATE `settings` SET `content` = ? WHERE `name` = ?") - if err != nil { - return err - } - log.Print("Preparing updatePlugin statement.") stmts.updatePlugin, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?") if err != nil { @@ -397,12 +382,6 @@ func _gen_mysql() (err error) { return err } - log.Print("Preparing modlogCount statement.") - stmts.modlogCount, err = db.Prepare("SELECT COUNT(*) AS `count` FROM `moderation_logs`") - if err != nil { - return err - } - log.Print("Preparing notifyWatchers statement.") stmts.notifyWatchers, err = db.Prepare("INSERT INTO `activity_stream_matches`(`watcher`,`asid`) SELECT `activity_subscriptions`.`user`, `activity_stream`.`asid` FROM `activity_stream` INNER JOIN `activity_subscriptions` ON `activity_subscriptions`.`targetType` = `activity_stream`.`elementType` AND `activity_subscriptions`.`targetID` = `activity_stream`.`elementID` AND `activity_subscriptions`.`user` != `activity_stream`.`actor` WHERE `asid` = ?") if err != nil { diff --git a/gen_pgsql.go b/gen_pgsql.go index 362d6621..c7dc2749 100644 --- a/gen_pgsql.go +++ b/gen_pgsql.go @@ -11,7 +11,6 @@ import "./common" type Stmts struct { editReply *sql.Stmt editProfileReply *sql.Stmt - updateSetting *sql.Stmt updatePlugin *sql.Stmt updatePluginInstall *sql.Stmt updateTheme *sql.Stmt @@ -55,12 +54,6 @@ func _gen_pgsql() (err error) { return err } - log.Print("Preparing updateSetting statement.") - stmts.updateSetting, err = db.Prepare("UPDATE `settings` SET `content` = ? WHERE `name` = ?") - if err != nil { - return err - } - log.Print("Preparing updatePlugin statement.") stmts.updatePlugin, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?") if err != nil { diff --git a/gen_router.go b/gen_router.go index ba87de48..ad7e7fe5 100644 --- a/gen_router.go +++ b/gen_router.go @@ -246,7 +246,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/panel/settings/": err = routePanelSettings(w,req,user) case "/panel/settings/edit/": - err = routePanelSetting(w,req,user,extra_data) + err = routePanelSettingEdit(w,req,user,extra_data) case "/panel/settings/edit/submit/": err = common.NoSessionMismatch(w,req,user) if err != nil { @@ -254,7 +254,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - err = routePanelSettingEdit(w,req,user,extra_data) + err = routePanelSettingEditSubmit(w,req,user,extra_data) case "/panel/settings/word-filters/": err = routePanelWordFilters(w,req,user) case "/panel/settings/word-filters/create/": diff --git a/general_test.go b/general_test.go index 4824d21d..769076d7 100644 --- a/general_test.go +++ b/general_test.go @@ -15,7 +15,6 @@ import ( "./common" "./install/install" "./query_gen/lib" - //"runtime/pprof" //"github.com/husobee/vestigo" ) @@ -77,44 +76,7 @@ func gloinit() (err error) { return err } - common.Rstore, err = common.NewSQLReplyStore() - if err != nil { - return err - } - common.Prstore, err = common.NewSQLProfileReplyStore() - if err != nil { - return err - } - - dbProd = db - //db_test, err = sql.Open("testdb","") - //if err != nil { - // return err - //} - - err = common.InitTemplates() - if err != nil { - return err - } - dbProd.SetMaxOpenConns(64) - - err = common.InitPhrases() - if err != nil { - log.Fatal(err) - } - - log.Print("Loading the static files.") - err = common.StaticFiles.Init() - if err != nil { - return err - } - - common.Auth, err = common.NewDefaultAuth() - if err != nil { - return err - } - - err = common.LoadWordFilters() + err = afterDBInit() if err != nil { return err } diff --git a/install/install/mssql.go b/install/install/mssql.go index 672983c6..03aec43e 100644 --- a/install/install/mssql.go +++ b/install/install/mssql.go @@ -1,7 +1,6 @@ /* * * Gosora MSSQL Interface -* Under heavy development * Copyright Azareal 2017 - 2018 * */ @@ -80,14 +79,9 @@ func (ins *MssqlInstaller) InitDatabase() (err error) { // TODO: Create the database, if it doesn't exist // Ready the query builder - qgen.Builder.SetConn(db) - err = qgen.Builder.SetAdapter("mssql") - if err != nil { - return err - } ins.db = db - - return nil + qgen.Builder.SetConn(db) + return qgen.Builder.SetAdapter("mssql") } func (ins *MssqlInstaller) TableDefs() (err error) { @@ -126,7 +120,6 @@ func (ins *MssqlInstaller) TableDefs() (err error) { return err } } - //fmt.Println("Finished creating the tables") return nil } @@ -150,8 +143,6 @@ func (ins *MssqlInstaller) InitialData() (err error) { return err } } - - //fmt.Println("Finished inserting the database data") return nil } diff --git a/langs/english.json b/langs/english.json index d8a19939..3bd4f2e9 100644 --- a/langs/english.json +++ b/langs/english.json @@ -55,5 +55,36 @@ "Accounts": { "VerifyEmailSubject": "Validate Your Email @ {{name}}", "VerifyEmailBody": "Dear {{username}}, following your registration on our forums, we ask you to validate your email, so that we can confirm that this email actually belongs to you.\n\nClick on the following link to do so. {{schema}}://{{url}}/user/edit/token/{{token}}\n\nIf you haven't created an account here, then please feel free to ignore this email.\nWe're sorry for the inconvenience this may have caused." + }, + "Errors": { + "NoPerms": { + } + }, + "PageTitles": { + "overview":"Overview", + "page":"Page", + "topics":"All Topics", + "forums":"Forum List", + "login":"Login", + "register":"Registration", + "ip-search":"IP Search", + + "panel-dashboard":"Control Panel Dashboard", + "panel-forums":"Forum Manager", + "panel-delete-forum":"Delete Forum", + "panel-edit-forum":"Forum Editor", + "panel-settings":"Setting Manager", + "panel-edit-setting":"Edit Setting", + "panel-word-filters":"Word Filter Manager", + "panel-edit-word-filter":"Edit Word Filter", + "panel-plugins":"Plugin Manager", + "panel-users":"User Manager", + "panel-edit-user":"User Editor", + "panel-groups":"Group Manager", + "panel-edit-group":"Group Editor", + "panel-themes":"Theme Manager", + "panel-backups":"Backups", + "panel-mod-logs":"Moderation Logs", + "panel-debug":"Debug" } } \ No newline at end of file diff --git a/main.go b/main.go index 51afa429..a2f033a2 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,61 @@ type Globs struct { stmts *Stmts } +func afterDBInit() (err error) { + common.Rstore, err = common.NewSQLReplyStore() + if err != nil { + return err + } + common.Prstore, err = common.NewSQLProfileReplyStore() + if err != nil { + return err + } + + err = common.InitTemplates() + if err != nil { + return err + } + err = common.InitPhrases() + if err != nil { + return err + } + + log.Print("Loading the static files.") + err = common.StaticFiles.Init() + if err != nil { + return err + } + + log.Print("Initialising the widgets") + err = common.InitWidgets() + if err != nil { + return err + } + + log.Print("Initialising the authentication system") + common.Auth, err = common.NewDefaultAuth() + if err != nil { + return err + } + + err = common.LoadWordFilters() + if err != nil { + return err + } + + common.ModLogs, err = common.NewModLogStore() + if err != nil { + return err + } + + common.AdminLogs, err = common.NewAdminLogStore() + if err != nil { + return err + } + + return nil +} + // TODO: Split this function up func main() { // TODO: Recover from panics @@ -90,44 +145,7 @@ func main() { log.Fatal(err) } - common.Rstore, err = common.NewSQLReplyStore() - if err != nil { - log.Fatal(err) - } - common.Prstore, err = common.NewSQLProfileReplyStore() - if err != nil { - log.Fatal(err) - } - - err = common.InitTemplates() - if err != nil { - log.Fatal(err) - } - - err = common.InitPhrases() - if err != nil { - log.Fatal(err) - } - - log.Print("Loading the static files.") - err = common.StaticFiles.Init() - if err != nil { - log.Fatal(err) - } - - log.Print("Initialising the widgets") - err = common.InitWidgets() - if err != nil { - log.Fatal(err) - } - - log.Print("Initialising the authentication system") - common.Auth, err = common.NewDefaultAuth() - if err != nil { - log.Fatal(err) - } - - err = common.LoadWordFilters() + err = afterDBInit() if err != nil { log.Fatal(err) } diff --git a/member_routes.go b/member_routes.go index bb156d41..7c1d67e4 100644 --- a/member_routes.go +++ b/member_routes.go @@ -55,12 +55,12 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user common.User, var forumList []common.Forum var canSee []int if user.IsSuperAdmin { - canSee, err = common.Fstore.GetAllVisibleIDs() + canSee, err = common.Forums.GetAllVisibleIDs() if err != nil { return common.InternalError(err, w, r) } } else { - group, err := common.Gstore.Get(user.Group) + group, err := common.Groups.Get(user.Group) if err != nil { // TODO: Refactor this common.LocalError("Something weird happened behind the scenes", w, r, user) @@ -78,7 +78,7 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user common.User, } // Do a bulk forum fetch, just in case it's the SqlForumStore? - forum := common.Fstore.DirtyGet(ffid) + forum := common.Forums.DirtyGet(ffid) if forum.Name != "" && forum.Active { fcopy := forum.Copy() if common.Hooks["topic_create_frow_assign"] != nil { @@ -98,7 +98,7 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user common.User, } } - err = common.RunThemeTemplate(headerVars.ThemeName, "create-topic", ctpage, w) + err = common.RunThemeTemplate(headerVars.Theme.Name, "create-topic", ctpage, w) if err != nil { return common.InternalError(err, w, r) } @@ -340,7 +340,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user common.User) return common.InternalError(err, w, r) } - err = common.Fstore.UpdateLastTopic(tid, user.ID, topic.ParentID) + err = common.Forums.UpdateLastTopic(tid, user.ID, topic.ParentID) if err != nil && err != ErrNoRows { return common.InternalError(err, w, r) } @@ -623,7 +623,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user common.User, return common.InternalError(err, w, r) } - err = common.Fstore.AddTopic(int(lastID), user.ID, fid) + err = common.Forums.AddTopic(int(lastID), user.ID, fid) if err != nil && err != ErrNoRows { return common.InternalError(err, w, r) } diff --git a/migrations/filler.txt b/migrations/filler.txt new file mode 100644 index 00000000..20e14b1e --- /dev/null +++ b/migrations/filler.txt @@ -0,0 +1 @@ +This file is here so that Git will include this folder in the repository. \ No newline at end of file diff --git a/misc_test.go b/misc_test.go index 3cb833ea..e1bcfa91 100644 --- a/misc_test.go +++ b/misc_test.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http/httptest" "runtime/debug" - "strconv" "testing" "time" @@ -40,29 +39,39 @@ func TestUserStore(t *testing.T) { } var err error - common.Users, err = common.NewMemoryUserStore(common.Config.UserCacheCapacity) + ucache := common.NewMemoryUserCache(common.Config.UserCacheCapacity) + common.Users, err = common.NewDefaultUserStore(ucache) expectNilErr(t, err) - common.Users.(common.UserCache).Flush() + ucache.Flush() userStoreTest(t, 2) - common.Users, err = common.NewSQLUserStore() + common.Users, err = common.NewDefaultUserStore(nil) expectNilErr(t, err) userStoreTest(t, 3) } func userStoreTest(t *testing.T, newUserID int) { - ucache, hasCache := common.Users.(common.UserCache) + ucache := common.Users.GetCache() // Go doesn't have short-circuiting, so this'll allow us to do one liner tests - if !hasCache { - ucache = &common.NullUserStore{} + isCacheLengthZero := func(ucache common.UserCache) bool { + if ucache == nil { + return true + } + return ucache.Length() == 0 } - expect(t, (!hasCache || ucache.Length() == 0), fmt.Sprintf("The initial ucache length should be zero, not %d", ucache.Length())) + cacheLength := func(ucache common.UserCache) int { + if ucache == nil { + return 0 + } + return ucache.Length() + } + expect(t, isCacheLengthZero(ucache), fmt.Sprintf("The initial ucache length should be zero, not %d", cacheLength(ucache))) _, err := common.Users.Get(-1) recordMustNotExist(t, err, "UID #-1 shouldn't exist") - expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", ucache.Length())) + expect(t, isCacheLengthZero(ucache), fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", cacheLength(ucache))) _, err = common.Users.Get(0) recordMustNotExist(t, err, "UID #0 shouldn't exist") - expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", ucache.Length())) + expect(t, isCacheLengthZero(ucache), fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", cacheLength(ucache))) user, err := common.Users.Get(1) recordMustExist(t, err, "Couldn't find UID #1") @@ -79,24 +88,20 @@ func userStoreTest(t *testing.T, newUserID int) { _, err = common.Users.Get(newUserID) recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't exist", newUserID)) - if hasCache { + if ucache != nil { expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d") - _, err = ucache.CacheGet(-1) + _, err = ucache.Get(-1) recordMustNotExist(t, err, "UID #-1 shouldn't exist, even in the cache") - _, err = ucache.CacheGet(0) + _, err = ucache.Get(0) recordMustNotExist(t, err, "UID #0 shouldn't exist, even in the cache") - user, err = ucache.CacheGet(1) + user, err = ucache.Get(1) recordMustExist(t, err, "Couldn't find UID #1 in the cache") - if user.ID != 1 { - t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.") - } - if user.Name != "Admin" { - t.Error("user.Name should be 'Admin', not '" + user.Name + "'") - } + expect(t, user.ID == 1, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID)) + expect(t, user.Name == "Admin", fmt.Sprintf("user.Name should be 'Admin', not '%s'", user.Name)) - _, err = ucache.CacheGet(newUserID) + _, err = ucache.Get(newUserID) recordMustNotExist(t, err, "UID #%d shouldn't exist, even in the cache", newUserID) ucache.Flush() @@ -106,22 +111,12 @@ func userStoreTest(t *testing.T, newUserID int) { // TODO: Lock onto the specific error type. Is this even possible without sacrificing the detailed information in the error message? var userList map[int]*common.User userList, _ = common.Users.BulkGetMap([]int{-1}) - if len(userList) > 0 { - t.Error("There shouldn't be any results for UID #-1") - } - - if hasCache { - expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") - } + expect(t, len(userList) == 0, fmt.Sprintf("The userList length should be 0, not %d", len(userList))) + expect(t, isCacheLengthZero(ucache), fmt.Sprintf("User cache length should be 0, not %d", cacheLength(ucache))) userList, _ = common.Users.BulkGetMap([]int{0}) - if len(userList) > 0 { - t.Error("There shouldn't be any results for UID #0") - } - - if hasCache { - expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") - } + expect(t, len(userList) == 0, fmt.Sprintf("The userList length should be 0, not %d", len(userList))) + expect(t, isCacheLengthZero(ucache), fmt.Sprintf("User cache length should be 0, not %d", cacheLength(ucache))) userList, _ = common.Users.BulkGetMap([]int{1}) if len(userList) == 0 { @@ -135,18 +130,14 @@ func userStoreTest(t *testing.T, newUserID int) { t.Error("We couldn't find UID #1 in the returned map") t.Error("userList", userList) } - if user.ID != 1 { - t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.") - } + expect(t, user.ID == 1, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID)) - if hasCache { + if ucache != nil { expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d") - user, err = ucache.CacheGet(1) + user, err = ucache.Get(1) recordMustExist(t, err, "Couldn't find UID #1 in the cache") - if user.ID != 1 { - t.Errorf("user.ID does not match the requested UID. Got '%d' instead.", user.ID) - } + expect(t, user.ID == 1, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID)) ucache.Flush() } @@ -155,7 +146,7 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, common.Users.Exists(1), "UID #1 should exist") expect(t, !common.Users.Exists(newUserID), fmt.Sprintf("UID #%d shouldn't exist", newUserID)) - expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("User cache length should be 0, not %d", ucache.Length())) + expect(t, isCacheLengthZero(ucache), fmt.Sprintf("User cache length should be 0, not %d", cacheLength(ucache))) expectIntToBeX(t, common.Users.GlobalCount(), 1, "The number of users should be one, not %d") var awaitingActivation = 5 @@ -166,9 +157,7 @@ func userStoreTest(t *testing.T, newUserID int) { user, err = common.Users.Get(newUserID) recordMustExist(t, err, "Couldn't find UID #%d", newUserID) - if user.ID != newUserID { - t.Errorf("The UID of the user record should be %d", newUserID) - } + expect(t, user.ID == newUserID, fmt.Sprintf("The UID of the user record should be %d", newUserID)) expect(t, user.Name == "Sam", "The user should be named Sam") expect(t, !user.IsSuperAdmin, "Sam should not be a super admin") @@ -178,9 +167,9 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, !user.IsBanned, "Sam should not be banned") expectIntToBeX(t, user.Group, 5, "Sam should be in group 5") - if hasCache { + if ucache != nil { expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d") - user, err = ucache.CacheGet(newUserID) + user, err = ucache.Get(newUserID) recordMustExist(t, err, "Couldn't find UID #%d in the cache", newUserID) expect(t, user.ID == newUserID, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID)) } @@ -190,9 +179,9 @@ func userStoreTest(t *testing.T, newUserID int) { expectIntToBeX(t, user.Group, 5, "Sam should still be in group 5 in this copy") // ? - What if we change the caching mechanism so it isn't hard purged and reloaded? We'll deal with that when we come to it, but for now, this is a sign of a cache bug - if hasCache { + if ucache != nil { expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") - _, err = ucache.CacheGet(newUserID) + _, err = ucache.Get(newUserID) recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID) } @@ -216,18 +205,16 @@ func userStoreTest(t *testing.T, newUserID int) { expectNilErr(t, err) expect(t, user.Group == common.Config.DefaultGroup, fmt.Sprintf("Sam should be in group %d, not %d", common.Config.DefaultGroup, user.Group)) - if hasCache { + if ucache != nil { expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") - _, err = ucache.CacheGet(2) + _, err = ucache.Get(2) recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID) } user, err = common.Users.Get(newUserID) recordMustExist(t, err, "Couldn't find UID #%d", newUserID) - if user.ID != newUserID { - t.Errorf("The UID of the user record should be %d", newUserID) - } + expect(t, user.ID == newUserID, fmt.Sprintf("The UID of the user record should be %d", newUserID)) expect(t, !user.IsSuperAdmin, "Sam should not be a super admin") expect(t, !user.IsAdmin, "Sam should not be an admin") expect(t, !user.IsSuperMod, "Sam should not be a super mod") @@ -242,9 +229,9 @@ func userStoreTest(t *testing.T, newUserID int) { expectNilErr(t, err) expectIntToBeX(t, user.Group, common.BanGroup, "Sam should still be in the ban group in this copy") - if hasCache { + if ucache != nil { expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") - _, err = ucache.CacheGet(newUserID) + _, err = ucache.Get(newUserID) recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID) } @@ -414,9 +401,9 @@ func userStoreTest(t *testing.T, newUserID int) { expectNilErr(t, err) expect(t, !common.Users.Exists(newUserID), fmt.Sprintf("UID #%d should no longer exist", newUserID)) - if hasCache { + if ucache != nil { expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d") - _, err = ucache.CacheGet(newUserID) + _, err = ucache.Get(newUserID) recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID) } @@ -505,10 +492,11 @@ func TestTopicStore(t *testing.T) { } var err error - common.Topics, err = common.NewMemoryTopicStore(common.Config.TopicCacheCapacity) + tcache := common.NewMemoryTopicCache(common.Config.TopicCacheCapacity) + common.Topics, err = common.NewDefaultTopicStore(tcache) expectNilErr(t, err) topicStoreTest(t) - common.Topics, err = common.NewSQLTopicStore() + common.Topics, err = common.NewDefaultTopicStore(nil) expectNilErr(t, err) topicStoreTest(t) } @@ -526,25 +514,19 @@ func topicStoreTest(t *testing.T) { recordMustExist(t, err, "Couldn't find TID #1") if topic.ID != 1 { - t.Error("topic.ID does not match the requested TID. Got '" + strconv.Itoa(topic.ID) + "' instead.") + t.Error("topic.ID does not match the requested TID. Got '%d' instead.", topic.ID) } // TODO: Add BulkGetMap() to the TopicStore ok := common.Topics.Exists(-1) - if ok { - t.Error("TID #-1 shouldn't exist") - } + expect(t, !ok, "TID #-1 shouldn't exist") ok = common.Topics.Exists(0) - if ok { - t.Error("TID #0 shouldn't exist") - } + expect(t, !ok, "TID #0 shouldn't exist") ok = common.Topics.Exists(1) - if !ok { - t.Error("TID #1 should exist") - } + expect(t, ok, "TID #1 should exist") count := common.Topics.GlobalCount() if count <= 0 { @@ -563,17 +545,17 @@ func TestForumStore(t *testing.T) { common.InitPlugins() } - _, err := common.Fstore.Get(-1) + _, err := common.Forums.Get(-1) recordMustNotExist(t, err, "FID #-1 shouldn't exist") - _, err = common.Fstore.Get(0) + _, err = common.Forums.Get(0) recordMustNotExist(t, err, "FID #0 shouldn't exist") - forum, err := common.Fstore.Get(1) + forum, err := common.Forums.Get(1) recordMustExist(t, err, "Couldn't find FID #1") if forum.ID != 1 { - t.Error("forum.ID doesn't not match the requested FID. Got '" + strconv.Itoa(forum.ID) + "' instead.'") + t.Error("forum.ID doesn't not match the requested FID. Got '%d' instead.'", forum.ID) } // TODO: Check the preset and forum permissions expect(t, forum.Name == "Reports", fmt.Sprintf("FID #0 is named '%s' and not 'Reports'", forum.Name)) @@ -581,7 +563,7 @@ func TestForumStore(t *testing.T) { var expectDesc = "All the reports go here" expect(t, forum.Desc == expectDesc, fmt.Sprintf("The forum description should be '%s' not '%s'", expectDesc, forum.Desc)) - forum, err = common.Fstore.Get(2) + forum, err = common.Forums.Get(2) recordMustExist(t, err, "Couldn't find FID #1") expect(t, forum.ID == 2, fmt.Sprintf("The FID should be 2 not %d", forum.ID)) @@ -590,11 +572,11 @@ func TestForumStore(t *testing.T) { expectDesc = "A place for general discussions which don't fit elsewhere" expect(t, forum.Desc == expectDesc, fmt.Sprintf("The forum description should be '%s' not '%s'", expectDesc, forum.Desc)) - ok := common.Fstore.Exists(-1) + ok := common.Forums.Exists(-1) expect(t, !ok, "FID #-1 shouldn't exist") - ok = common.Fstore.Exists(0) + ok = common.Forums.Exists(0) expect(t, !ok, "FID #0 shouldn't exist") - ok = common.Fstore.Exists(1) + ok = common.Forums.Exists(1) expect(t, ok, "FID #1 should exist") // TODO: Test forum creation @@ -621,43 +603,38 @@ func TestGroupStore(t *testing.T) { common.InitPlugins() } - _, err := common.Gstore.Get(-1) + _, err := common.Groups.Get(-1) recordMustNotExist(t, err, "GID #-1 shouldn't exist") // TODO: Refactor the group store to remove GID #0 - group, err := common.Gstore.Get(0) + group, err := common.Groups.Get(0) recordMustExist(t, err, "Couldn't find GID #0") - if group.ID != 0 { - t.Errorf("group.ID doesn't not match the requested GID. Got '%d' instead.", group.ID) - } + expect(t, group.ID == 0, fmt.Sprintf("group.ID doesn't not match the requested GID. Got '%d' instead.", group.ID)) expect(t, group.Name == "Unknown", fmt.Sprintf("GID #0 is named '%s' and not 'Unknown'", group.Name)) - group, err = common.Gstore.Get(1) + group, err = common.Groups.Get(1) recordMustExist(t, err, "Couldn't find GID #1") + expect(t, group.ID == 1, fmt.Sprintf("group.ID doesn't not match the requested GID. Got '%d' instead.'", group.ID)) - if group.ID != 1 { - t.Errorf("group.ID doesn't not match the requested GID. Got '%d' instead.'", group.ID) - } - - ok := common.Gstore.Exists(-1) + ok := common.Groups.Exists(-1) expect(t, !ok, "GID #-1 shouldn't exist") // 0 aka Unknown, for system posts and other oddities - ok = common.Gstore.Exists(0) + ok = common.Groups.Exists(0) expect(t, ok, "GID #0 should exist") - ok = common.Gstore.Exists(1) + ok = common.Groups.Exists(1) expect(t, ok, "GID #1 should exist") var isAdmin = true var isMod = true var isBanned = false - gid, err := common.Gstore.Create("Testing", "Test", isAdmin, isMod, isBanned) + gid, err := common.Groups.Create("Testing", "Test", isAdmin, isMod, isBanned) expectNilErr(t, err) - expect(t, common.Gstore.Exists(gid), "The group we just made doesn't exist") + expect(t, common.Groups.Exists(gid), "The group we just made doesn't exist") - group, err = common.Gstore.Get(gid) + group, err = common.Groups.Get(gid) expectNilErr(t, err) expect(t, group.ID == gid, "The group ID should match the requested ID") expect(t, group.IsAdmin, "This should be an admin group") @@ -667,11 +644,11 @@ func TestGroupStore(t *testing.T) { isAdmin = false isMod = true isBanned = true - gid, err = common.Gstore.Create("Testing 2", "Test", isAdmin, isMod, isBanned) + gid, err = common.Groups.Create("Testing 2", "Test", isAdmin, isMod, isBanned) expectNilErr(t, err) - expect(t, common.Gstore.Exists(gid), "The group we just made doesn't exist") + expect(t, common.Groups.Exists(gid), "The group we just made doesn't exist") - group, err = common.Gstore.Get(gid) + group, err = common.Groups.Get(gid) expectNilErr(t, err) expect(t, group.ID == gid, "The group ID should match the requested ID") expect(t, !group.IsAdmin, "This should not be an admin group") @@ -682,7 +659,7 @@ func TestGroupStore(t *testing.T) { err = group.ChangeRank(false, false, true) expectNilErr(t, err) - group, err = common.Gstore.Get(gid) + group, err = common.Groups.Get(gid) expectNilErr(t, err) expect(t, group.ID == gid, "The group ID should match the requested ID") expect(t, !group.IsAdmin, "This shouldn't be an admin group") @@ -692,7 +669,7 @@ func TestGroupStore(t *testing.T) { err = group.ChangeRank(true, true, true) expectNilErr(t, err) - group, err = common.Gstore.Get(gid) + group, err = common.Groups.Get(gid) expectNilErr(t, err) expect(t, group.ID == gid, "The group ID should match the requested ID") expect(t, group.IsAdmin, "This should be an admin group") @@ -702,7 +679,7 @@ func TestGroupStore(t *testing.T) { err = group.ChangeRank(false, true, true) expectNilErr(t, err) - group, err = common.Gstore.Get(gid) + group, err = common.Groups.Get(gid) expectNilErr(t, err) expect(t, group.ID == gid, "The group ID should match the requested ID") expect(t, !group.IsAdmin, "This shouldn't be an admin group") @@ -710,9 +687,9 @@ func TestGroupStore(t *testing.T) { expect(t, !group.IsBanned, "This shouldn't be a ban group") // Make sure the data is static - common.Gstore.Reload(gid) + common.Groups.Reload(gid) - group, err = common.Gstore.Get(gid) + group, err = common.Groups.Get(gid) expectNilErr(t, err) expect(t, group.ID == gid, "The group ID should match the requested ID") expect(t, !group.IsAdmin, "This shouldn't be an admin group") diff --git a/mod_routes.go b/mod_routes.go index 87c6f8d6..073f854c 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -15,6 +15,7 @@ import ( // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Disable stat updates in posts handled by plugin_guilds +// TODO: Make sure this route is member only func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { err := r.ParseForm() if err != nil { @@ -50,7 +51,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) co return common.InternalErrorJSQ(err, w, r, isJs) } - err = common.Fstore.UpdateLastTopic(topic.ID, user.ID, topic.ParentID) + err = common.Forums.UpdateLastTopic(topic.ID, user.ID, topic.ParentID) if err != nil && err != ErrNoRows { return common.InternalErrorJSQ(err, w, r, isJs) } @@ -65,11 +66,12 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) co // TODO: Add support for soft-deletion and add a permission for hard delete in addition to the usual // TODO: Disable stat updates in posts handled by plugin_guilds +// TODO: Make sure this route is member only func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { // TODO: Move this to some sort of middleware var tids []int var isJs = false - if r.Header.Get("Content-type") == "application/json" { + if common.ReqIsJson(r) { if r.Body == nil { return common.PreErrorJS("No request body", w, r) } @@ -114,7 +116,7 @@ func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user common.User) return common.InternalErrorJSQ(err, w, r, isJs) } - err = common.AddModLog("delete", tid, "topic", user.LastIP, user.ID) + err = common.ModLogs.Create("delete", tid, "topic", user.LastIP, user.ID) if err != nil { return common.InternalErrorJSQ(err, w, r, isJs) } @@ -158,7 +160,7 @@ func routeStickTopic(w http.ResponseWriter, r *http.Request, user common.User) c return common.InternalError(err, w, r) } - err = common.AddModLog("stick", tid, "topic", user.LastIP, user.ID) + err = common.ModLogs.Create("stick", tid, "topic", user.LastIP, user.ID) if err != nil { return common.InternalError(err, w, r) } @@ -197,7 +199,7 @@ func routeUnstickTopic(w http.ResponseWriter, r *http.Request, user common.User) return common.InternalError(err, w, r) } - err = common.AddModLog("unstick", tid, "topic", user.LastIP, user.ID) + err = common.ModLogs.Create("unstick", tid, "topic", user.LastIP, user.ID) if err != nil { return common.InternalError(err, w, r) } @@ -214,7 +216,7 @@ func routeLockTopic(w http.ResponseWriter, r *http.Request, user common.User) co // TODO: Move this to some sort of middleware var tids []int var isJs = false - if r.Header.Get("Content-type") == "application/json" { + if common.ReqIsJson(r) { if r.Body == nil { return common.PreErrorJS("No request body", w, r) } @@ -256,7 +258,7 @@ func routeLockTopic(w http.ResponseWriter, r *http.Request, user common.User) co return common.InternalErrorJSQ(err, w, r, isJs) } - err = common.AddModLog("lock", tid, "topic", user.LastIP, user.ID) + err = common.ModLogs.Create("lock", tid, "topic", user.LastIP, user.ID) if err != nil { return common.InternalErrorJSQ(err, w, r, isJs) } @@ -299,7 +301,7 @@ func routeUnlockTopic(w http.ResponseWriter, r *http.Request, user common.User) return common.InternalError(err, w, r) } - err = common.AddModLog("unlock", tid, "topic", user.LastIP, user.ID) + err = common.ModLogs.Create("unlock", tid, "topic", user.LastIP, user.ID) if err != nil { return common.InternalError(err, w, r) } @@ -425,7 +427,7 @@ func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user common. return common.InternalErrorJSQ(err, w, r, isJs) } - err = common.AddModLog("delete", reply.ParentID, "reply", user.LastIP, user.ID) + err = common.ModLogs.Create("delete", reply.ParentID, "reply", user.LastIP, user.ID) if err != nil { return common.InternalErrorJSQ(err, w, r, isJs) } @@ -588,7 +590,7 @@ func routeIps(w http.ResponseWriter, r *http.Request, user common.User) common.R return common.InternalError(err, w, r) } - pi := common.IPSearchPage{"IP Search", user, headerVars, userList, ip} + pi := common.IPSearchPage{common.GetTitlePhrase("ip-search"), user, headerVars, userList, ip} if common.PreRenderHooks["pre_render_ips"] != nil { if common.RunPreRenderHook("pre_render_ips", w, r, &user, &pi) { return nil @@ -610,9 +612,9 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User) co if err != nil { return common.LocalError("The provided common.User ID is not a valid number.", w, r, user) } - /*if uid == -2 { - return common.LocalError("Stop trying to ban Merlin! Ban admin! Bad! No!",w,r,user) - }*/ + if uid == -2 { + return common.LocalError("Why don't you like Merlin?", w, r, user) + } targetUser, err := common.Users.Get(uid) if err == ErrNoRows { @@ -622,7 +624,7 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User) co } // TODO: Is there a difference between IsMod and IsSuperMod? Should we delete the redundant one? - if targetUser.IsSuperAdmin || targetUser.IsAdmin || targetUser.IsMod { + if targetUser.IsMod { return common.LocalError("You may not ban another staff member.", w, r, user) } if uid == user.ID { @@ -665,7 +667,7 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User) co return common.InternalError(err, w, r) } - err = common.AddModLog("ban", uid, "user", user.LastIP, user.ID) + err = common.ModLogs.Create("ban", uid, "user", user.LastIP, user.ID) if err != nil { return common.InternalError(err, w, r) } @@ -704,7 +706,7 @@ func routeUnban(w http.ResponseWriter, r *http.Request, user common.User) common return common.InternalError(err, w, r) } - err = common.AddModLog("unban", uid, "user", user.LastIP, user.ID) + err = common.ModLogs.Create("unban", uid, "user", user.LastIP, user.ID) if err != nil { return common.InternalError(err, w, r) } @@ -738,7 +740,7 @@ func routeActivate(w http.ResponseWriter, r *http.Request, user common.User) com return common.InternalError(err, w, r) } - err = common.AddModLog("activate", targetUser.ID, "user", user.LastIP, user.ID) + err = common.ModLogs.Create("activate", targetUser.ID, "user", user.LastIP, user.ID) if err != nil { return common.InternalError(err, w, r) } diff --git a/panel_routes.go b/panel_routes.go index 557596b5..cbcca578 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -28,6 +28,16 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common var cpustr = "Unknown" var cpuColour string + lessThanSwitch := func(number int, lowerBound int, midBound int) string { + switch { + case number < lowerBound: + return "stat_green" + case number < midBound: + return "stat_orange" + } + return "stat_red" + } + var ramstr, ramColour string memres, err := mem.VirtualMemory() if err != nil { @@ -37,7 +47,6 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common usedCount := common.ConvertByteInUnit(float64(memres.Total-memres.Available), totalUnit) // Round totals with .9s up, it's how most people see it anyway. Floats are notoriously imprecise, so do it off 0.85 - //log.Print("pre used_count",used_count) var totstr string if (totalCount - float64(int(totalCount))) > 0.85 { usedCount += 1.0 - (totalCount - float64(int(totalCount))) @@ -45,7 +54,6 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common } else { totstr = fmt.Sprintf("%.1f", totalCount) } - //log.Print("post used_count",used_count) if usedCount > totalCount { usedCount = totalCount @@ -53,31 +61,27 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common ramstr = fmt.Sprintf("%.1f", usedCount) + " / " + totstr + totalUnit ramperc := ((memres.Total - memres.Available) * 100) / memres.Total - //log.Print("ramperc",ramperc) - if ramperc < 50 { - ramColour = "stat_green" - } else if ramperc < 75 { - ramColour = "stat_orange" - } else { - ramColour = "stat_red" - } + ramColour = lessThanSwitch(int(ramperc), 50, 75) } + greaterThanSwitch := func(number int, lowerBound int, midBound int) string { + switch { + case number > midBound: + return "stat_green" + case number > lowerBound: + return "stat_orange" + } + return "stat_red" + } + + // TODO: Add a stat store for this? var postCount int err = stmts.todaysPostCount.QueryRow().Scan(&postCount) if err != nil && err != ErrNoRows { return common.InternalError(err, w, r) } var postInterval = "day" - - var postColour string - if postCount > 25 { - postColour = "stat_green" - } else if postCount > 5 { - postColour = "stat_orange" - } else { - postColour = "stat_red" - } + var postColour = greaterThanSwitch(postCount, 5, 25) var topicCount int err = stmts.todaysTopicCount.QueryRow().Scan(&topicCount) @@ -85,15 +89,7 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common return common.InternalError(err, w, r) } var topicInterval = "day" - - var topicColour string - if topicCount > 8 { - topicColour = "stat_green" - } else if topicCount > 0 { - topicColour = "stat_orange" - } else { - topicColour = "stat_red" - } + var topicColour = greaterThanSwitch(topicCount, 0, 8) var reportCount int err = stmts.todaysReportCount.QueryRow().Scan(&reportCount) @@ -120,32 +116,9 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common gonline := wsHub.guestCount() totonline := uonline + gonline - var onlineColour string - if totonline > 10 { - onlineColour = "stat_green" - } else if totonline > 3 { - onlineColour = "stat_orange" - } else { - onlineColour = "stat_red" - } - - var onlineGuestsColour string - if gonline > 10 { - onlineGuestsColour = "stat_green" - } else if gonline > 1 { - onlineGuestsColour = "stat_orange" - } else { - onlineGuestsColour = "stat_red" - } - - var onlineUsersColour string - if uonline > 5 { - onlineUsersColour = "stat_green" - } else if uonline > 1 { - onlineUsersColour = "stat_orange" - } else { - onlineUsersColour = "stat_red" - } + var onlineColour = greaterThanSwitch(totonline, 3, 10) + var onlineGuestsColour = greaterThanSwitch(gonline, 1, 10) + var onlineUsersColour = greaterThanSwitch(uonline, 1, 5) totonline, totunit := common.ConvertFriendlyUnit(totonline) uonline, uunit := common.ConvertFriendlyUnit(uonline) @@ -168,7 +141,7 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common gridElements = append(gridElements, common.GridElement{"dash-visitorsperweek", "2 visitors / week", 13, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The number of unique visitors we've had over the last 7 days"*/}) gridElements = append(gridElements, common.GridElement{"dash-postsperuser", "5 posts / user / week", 14, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The average number of posts made by each active user over the past week"*/}) - pi := common.PanelDashboardPage{"Control Panel Dashboard", user, headerVars, stats, gridElements} + pi := common.PanelDashboardPage{common.GetTitlePhrase("panel-dashboard"), user, headerVars, stats, gridElements} if common.PreRenderHooks["pre_render_panel_dashboard"] != nil { if common.RunPreRenderHook("pre_render_panel_dashboard", w, r, &user, &pi) { return nil @@ -192,7 +165,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user common.User) // TODO: Paginate this? var forumList []interface{} - forums, err := common.Fstore.GetAll() + forums, err := common.Forums.GetAll() if err != nil { return common.InternalError(err, w, r) } @@ -207,7 +180,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user common.User) forumList = append(forumList, fadmin) } } - pi := common.PanelPage{"Forum Manager", user, headerVars, stats, forumList, nil} + pi := common.PanelPage{common.GetTitlePhrase("panel-forums"), user, headerVars, stats, forumList, nil} if common.PreRenderHooks["pre_render_panel_forums"] != nil { if common.RunPreRenderHook("pre_render_panel_forums", w, r, &user, &pi) { return nil @@ -217,6 +190,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user common.User) if err != nil { return common.InternalError(err, w, r) } + return nil } @@ -235,7 +209,7 @@ func routePanelForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user c factive := r.PostFormValue("forum-name") active := (factive == "on" || factive == "1") - _, err := common.Fstore.Create(fname, fdesc, active, fpreset) + _, err := common.Forums.Create(fname, fdesc, active, fpreset) if err != nil { return common.InternalError(err, w, r) } @@ -259,17 +233,18 @@ func routePanelForumsDelete(w http.ResponseWriter, r *http.Request, user common. return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) } - forum, err := common.Fstore.Get(fid) + forum, err := common.Forums.Get(fid) if err == ErrNoRows { return common.LocalError("The forum you're trying to delete doesn't exist.", w, r, user) } else if err != nil { return common.InternalError(err, w, r) } + // TODO: Make this a phrase confirmMsg := "Are you sure you want to delete the '" + forum.Name + "' forum?" yousure := common.AreYouSure{"/panel/forums/delete/submit/" + strconv.Itoa(fid), confirmMsg} - pi := common.PanelPage{"Delete Forum", user, headerVars, stats, tList, yousure} + pi := common.PanelPage{common.GetTitlePhrase("panel-delete-forum"), user, headerVars, stats, tList, yousure} if common.PreRenderHooks["pre_render_panel_delete_forum"] != nil { if common.RunPreRenderHook("pre_render_panel_delete_forum", w, r, &user, &pi) { return nil @@ -296,7 +271,7 @@ func routePanelForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user c return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) } - err = common.Fstore.Delete(fid) + err = common.Forums.Delete(fid) if err == ErrNoRows { return common.LocalError("The forum you're trying to delete doesn't exist.", w, r, user) } else if err != nil { @@ -321,7 +296,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.Us return common.LocalError("The provided Forum ID is not a valid number.", w, r, user) } - forum, err := common.Fstore.Get(fid) + forum, err := common.Forums.Get(fid) if err == ErrNoRows { return common.LocalError("The forum you're trying to edit doesn't exist.", w, r, user) } else if err != nil { @@ -332,7 +307,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.Us forum.Preset = "custom" } - glist, err := common.Gstore.GetAll() + glist, err := common.Groups.GetAll() if err != nil { return common.InternalError(err, w, r) } @@ -345,7 +320,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.Us gplist = append(gplist, common.GroupForumPermPreset{group, common.ForumPermsToGroupForumPreset(group.Forums[fid])}) } - pi := common.PanelEditForumPage{"Forum Editor", user, headerVars, stats, forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist} + pi := common.PanelEditForumPage{common.GetTitlePhrase("panel-edit-forum"), user, headerVars, stats, forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist} if common.PreRenderHooks["pre_render_panel_edit_forum"] != nil { if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) { return nil @@ -355,6 +330,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.Us if err != nil { return common.InternalError(err, w, r) } + return nil } @@ -373,7 +349,7 @@ func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user com return common.LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) } - forum, err := common.Fstore.Get(fid) + forum, err := common.Forums.Get(fid) if err == ErrNoRows { return common.LocalErrorJSQ("The forum you're trying to edit doesn't exist.", w, r, user, isJs) } else if err != nil { @@ -425,7 +401,7 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use return common.LocalErrorJSQ("Invalid Group ID", w, r, user, isJs) } - forum, err := common.Fstore.Get(fid) + forum, err := common.Forums.Get(fid) if err == ErrNoRows { return common.LocalErrorJSQ("This forum doesn't exist", w, r, user, isJs) } else if err != nil { @@ -454,46 +430,35 @@ func routePanelSettings(w http.ResponseWriter, r *http.Request, user common.User if !user.Perms.EditSettings { return common.NoPermissions(w, r, user) } - var settingList = make(map[string]interface{}) - rows, err := stmts.getSettings.Query() + + settings, err := headerVars.Settings.BypassGetAll() if err != nil { return common.InternalError(err, w, r) } - defer rows.Close() - // nolint need the type so people viewing this file understand what it returns without visiting setting.go + // nolint need the type so people viewing this file understand what it returns without visiting phrases.go var settingLabels map[string]string = common.GetAllSettingLabels() - var sname, scontent, stype string - for rows.Next() { - err := rows.Scan(&sname, &scontent, &stype) - if err != nil { - return common.InternalError(err, w, r) - } - - if stype == "list" { - llist := settingLabels[sname] + for _, setting := range settings { + if setting.Type == "list" { + llist := settingLabels[setting.Name] labels := strings.Split(llist, ",") - conv, err := strconv.Atoi(scontent) + conv, err := strconv.Atoi(setting.Content) if err != nil { - return common.LocalError("The setting '"+sname+"' can't be converted to an integer", w, r, user) + return common.LocalError("The setting '"+setting.Name+"' can't be converted to an integer", w, r, user) } - scontent = labels[conv-1] - } else if stype == "bool" { - if scontent == "1" { - scontent = "Yes" + setting.Content = labels[conv-1] + } else if setting.Type == "bool" { + if setting.Content == "1" { + setting.Content = "Yes" } else { - scontent = "No" + setting.Content = "No" } } - settingList[sname] = scontent - } - err = rows.Err() - if err != nil { - return common.InternalError(err, w, r) + settingList[setting.Name] = setting.Content } - pi := common.PanelPage{"Setting Manager", user, headerVars, stats, tList, settingList} + pi := common.PanelPage{common.GetTitlePhrase("panel-settings"), user, headerVars, stats, tList, settingList} if common.PreRenderHooks["pre_render_panel_settings"] != nil { if common.RunPreRenderHook("pre_render_panel_settings", w, r, &user, &pi) { return nil @@ -506,7 +471,7 @@ func routePanelSettings(w http.ResponseWriter, r *http.Request, user common.User return nil } -func routePanelSetting(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { +func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) if ferr != nil { return ferr @@ -539,7 +504,7 @@ func routePanelSetting(w http.ResponseWriter, r *http.Request, user common.User, } } - pi := common.PanelPage{"Edit Setting", user, headerVars, stats, itemList, setting} + pi := common.PanelPage{common.GetTitlePhrase("panel-edit-setting"), user, headerVars, stats, itemList, setting} if common.PreRenderHooks["pre_render_panel_setting"] != nil { if common.RunPreRenderHook("pre_render_panel_setting", w, r, &user, &pi) { return nil @@ -552,7 +517,7 @@ func routePanelSetting(w http.ResponseWriter, r *http.Request, user common.User, return nil } -func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { +func routePanelSettingEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { headerLite, ferr := common.SimplePanelUserCheck(w, r, &user) if ferr != nil { return ferr @@ -562,34 +527,14 @@ func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user common.U } scontent := r.PostFormValue("setting-value") - setting, err := headerLite.Settings.BypassGet(sname) - if err == ErrNoRows { - return common.LocalError("The setting you want to edit doesn't exist.", w, r, user) - } else if err != nil { - return common.InternalError(err, w, r) - } - - if setting.Type == "bool" { - if scontent == "on" || scontent == "1" { - scontent = "1" - } else { - scontent = "0" - } - } - - // TODO: Make this a method or function? - _, err = stmts.updateSetting.Exec(scontent, sname) + err := headerLite.Settings.Update(sname, scontent) if err != nil { + if common.SafeSettingError(err) { + return common.LocalError(err.Error(), w, r, user) + } return common.InternalError(err, w, r) } - errmsg := headerLite.Settings.ParseSetting(sname, scontent, setting.Type, setting.Constraint) - if errmsg != "" { - return common.LocalError(errmsg, w, r, user) - } - // TODO: Do a reload instead? - common.SettingBox.Store(headerLite.Settings) - http.Redirect(w, r, "/panel/settings/", http.StatusSeeOther) return nil } @@ -604,7 +549,7 @@ func routePanelWordFilters(w http.ResponseWriter, r *http.Request, user common.U } var filterList = common.WordFilterBox.Load().(common.WordFilterMap) - pi := common.PanelPage{"Word Filter Manager", user, headerVars, stats, tList, filterList} + pi := common.PanelPage{common.GetTitlePhrase("panel-word-filters"), user, headerVars, stats, tList, filterList} if common.PreRenderHooks["pre_render_panel_word_filters"] != nil { if common.RunPreRenderHook("pre_render_panel_word_filters", w, r, &user, &pi) { return nil @@ -665,7 +610,7 @@ func routePanelWordFiltersEdit(w http.ResponseWriter, r *http.Request, user comm _ = wfid - pi := common.PanelPage{"Edit Word Filter", user, headerVars, stats, tList, nil} + pi := common.PanelPage{common.GetTitlePhrase("panel-edit-word-filter"), user, headerVars, stats, tList, nil} if common.PreRenderHooks["pre_render_panel_word_filters_edit"] != nil { if common.RunPreRenderHook("pre_render_panel_word_filters_edit", w, r, &user, &pi) { return nil @@ -758,7 +703,7 @@ func routePanelPlugins(w http.ResponseWriter, r *http.Request, user common.User) pluginList = append(pluginList, plugin) } - pi := common.PanelPage{"Plugin Manager", user, headerVars, stats, pluginList, nil} + pi := common.PanelPage{common.GetTitlePhrase("panel-plugins"), user, headerVars, stats, pluginList, nil} if common.PreRenderHooks["pre_render_panel_plugins"] != nil { if common.RunPreRenderHook("pre_render_panel_plugins", w, r, &user, &pi) { return nil @@ -966,8 +911,8 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user common.User) c puser.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(puser.ID), 1) } - if common.Gstore.DirtyGet(puser.Group).Tag != "" { - puser.Tag = common.Gstore.DirtyGet(puser.Group).Tag + if common.Groups.DirtyGet(puser.Group).Tag != "" { + puser.Tag = common.Groups.DirtyGet(puser.Group).Tag } else { puser.Tag = "" } @@ -979,7 +924,7 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user common.User) c } pageList := common.Paginate(stats.Users, perPage, 5) - pi := common.PanelUserPage{"User Manager", user, headerVars, stats, userList, pageList, page, lastPage} + pi := common.PanelUserPage{common.GetTitlePhrase("panel-users"), user, headerVars, stats, userList, pageList, page, lastPage} if common.PreRenderHooks["pre_render_panel_users"] != nil { if common.RunPreRenderHook("pre_render_panel_users", w, r, &user, &pi) { return nil @@ -1018,7 +963,7 @@ func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user common.Use } // ? - Should we stop admins from deleting all the groups? Maybe, protect the group they're currently using? - groups, err := common.Gstore.GetRange(1, 0) // ? - 0 = Go to the end + groups, err := common.Groups.GetRange(1, 0) // ? - 0 = Go to the end if err != nil { return common.InternalError(err, w, r) } @@ -1034,7 +979,7 @@ func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user common.Use groupList = append(groupList, group) } - pi := common.PanelPage{"User Editor", user, headerVars, stats, groupList, targetUser} + pi := common.PanelPage{common.GetTitlePhrase("panel-edit-user"), user, headerVars, stats, groupList, targetUser} if common.PreRenderHooks["pre_render_panel_edit_user"] != nil { if common.RunPreRenderHook("pre_render_panel_edit_user", w, r, &user, &pi) { return nil @@ -1095,7 +1040,7 @@ func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user comm return common.LocalError("You need to provide a whole number for the group ID", w, r, user) } - group, err := common.Gstore.Get(newgroup) + group, err := common.Groups.Get(newgroup) if err == ErrNoRows { return common.LocalError("The group you're trying to place this user in doesn't exist.", w, r, user) } else if err != nil { @@ -1109,6 +1054,7 @@ func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user comm return common.LocalError("You need the EditUserGroupSuperMod permission to assign someone to a super mod group.", w, r, user) } + // TODO: Move this query into common _, err = stmts.updateUser.Exec(newname, newemail, newgroup, targetUser.ID) if err != nil { return common.InternalError(err, w, r) @@ -1138,7 +1084,7 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user common.User) var count int var groupList []common.GroupAdmin - groups, _ := common.Gstore.GetRange(offset, 0) + groups, _ := common.Groups.GetRange(offset, 0) for _, group := range groups { if count == perPage { break @@ -1149,6 +1095,7 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user common.User) var canEdit bool var canDelete = false + // TODO: Use a switch for this if group.IsAdmin { rank = "Admin" rankClass = "admin" @@ -1173,7 +1120,7 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user common.User) //log.Printf("groupList: %+v\n", groupList) pageList := common.Paginate(stats.Groups, perPage, 5) - pi := common.PanelGroupPage{"Group Manager", user, headerVars, stats, groupList, pageList, page, lastPage} + pi := common.PanelGroupPage{common.GetTitlePhrase("panel-groups"), user, headerVars, stats, groupList, pageList, page, lastPage} if common.PreRenderHooks["pre_render_panel_groups"] != nil { if common.RunPreRenderHook("pre_render_panel_groups", w, r, &user, &pi) { return nil @@ -1201,7 +1148,7 @@ func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user common.Us return common.LocalError("You need to provide a whole number for the group ID", w, r, user) } - group, err := common.Gstore.Get(gid) + group, err := common.Groups.Get(gid) if err == ErrNoRows { //log.Print("aaaaa monsters") return common.NotFound(w, r) @@ -1232,7 +1179,7 @@ func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user common.Us disableRank := !user.Perms.EditGroupGlobalPerms || (group.ID == 6) - pi := common.PanelEditGroupPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, group.Tag, rank, disableRank} + pi := common.PanelEditGroupPage{common.GetTitlePhrase("panel-edit-group"), user, headerVars, stats, group.ID, group.Name, group.Tag, rank, disableRank} if common.PreRenderHooks["pre_render_panel_edit_group"] != nil { if common.RunPreRenderHook("pre_render_panel_edit_group", w, r, &user, &pi) { return nil @@ -1259,7 +1206,7 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user comm return common.LocalError("The Group ID is not a valid integer.", w, r, user) } - group, err := common.Gstore.Get(gid) + group, err := common.Groups.Get(gid) if err == ErrNoRows { //log.Print("aaaaa monsters") return common.NotFound(w, r) @@ -1310,7 +1257,7 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user comm globalPerms = append(globalPerms, common.NameLangToggle{"ViewIPs", common.GetGlobalPermPhrase("ViewIPs"), group.Perms.ViewIPs}) globalPerms = append(globalPerms, common.NameLangToggle{"UploadFiles", common.GetGlobalPermPhrase("UploadFiles"), group.Perms.UploadFiles}) - pi := common.PanelEditGroupPermsPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, localPerms, globalPerms} + pi := common.PanelEditGroupPermsPage{common.GetTitlePhrase("panel-edit-group"), user, headerVars, stats, group.ID, group.Name, localPerms, globalPerms} if common.PreRenderHooks["pre_render_panel_edit_group_perms"] != nil { if common.RunPreRenderHook("pre_render_panel_edit_group_perms", w, r, &user, &pi) { return nil @@ -1337,7 +1284,7 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user com return common.LocalError("You need to provide a whole number for the group ID", w, r, user) } - group, err := common.Gstore.Get(gid) + group, err := common.Groups.Get(gid) if err == ErrNoRows { //log.Print("aaaaa monsters") return common.NotFound(w, r) @@ -1360,6 +1307,7 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user com rank := r.FormValue("group-type") var originalRank string + // TODO: Use a switch for this if group.IsAdmin { originalRank = "Admin" } else if group.IsMod { @@ -1407,7 +1355,7 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user com if err != nil { return common.InternalError(err, w, r) } - common.Gstore.Reload(gid) + common.Groups.Reload(gid) http.Redirect(w, r, "/panel/groups/edit/"+strconv.Itoa(gid), http.StatusSeeOther) return nil @@ -1427,7 +1375,7 @@ func routePanelGroupsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use return common.LocalError("The Group ID is not a valid integer.", w, r, user) } - group, err := common.Gstore.Get(gid) + group, err := common.Groups.Get(gid) if err == ErrNoRows { //log.Print("aaaaa monsters o.o") return common.NotFound(w, r) @@ -1508,7 +1456,7 @@ func routePanelGroupsCreateSubmit(w http.ResponseWriter, r *http.Request, user c } } - gid, err := common.Gstore.Create(groupName, groupTag, isAdmin, isMod, isBanned) + gid, err := common.Groups.Create(groupName, groupTag, isAdmin, isMod, isBanned) if err != nil { return common.InternalError(err, w, r) } @@ -1538,7 +1486,7 @@ func routePanelThemes(w http.ResponseWriter, r *http.Request, user common.User) } - pi := common.PanelThemesPage{"Theme Manager", user, headerVars, stats, pThemeList, vThemeList} + pi := common.PanelThemesPage{common.GetTitlePhrase("panel-themes"), user, headerVars, stats, pThemeList, vThemeList} if common.PreRenderHooks["pre_render_panel_themes"] != nil { if common.RunPreRenderHook("pre_render_panel_themes", w, r, &user, &pi) { return nil @@ -1658,7 +1606,7 @@ func routePanelBackups(w http.ResponseWriter, r *http.Request, user common.User, backupList = append(backupList, common.BackupItem{backupFile.Name(), backupFile.ModTime()}) } - pi := common.PanelBackupPage{"Backups", user, headerVars, stats, backupList} + pi := common.PanelBackupPage{common.GetTitlePhrase("panel-backups"), user, headerVars, stats, backupList} err = common.Templates.ExecuteTemplate(w, "panel-backups.html", pi) if err != nil { return common.InternalError(err, w, r) @@ -1672,12 +1620,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User) return ferr } - var logCount int - err := stmts.modlogCount.QueryRow().Scan(&logCount) - if err != nil { - return common.InternalError(err, w, r) - } - + logCount := common.ModLogs.GlobalCount() page, _ := strconv.Atoi(r.FormValue("page")) perPage := 10 offset, page, lastPage := common.PageOffset(logCount, page, perPage) @@ -1688,6 +1631,21 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User) } defer rows.Close() + // TODO: Log errors when something really screwy is going on? + handleUnknownUser := func(user *common.User, err error) *common.User { + if err != nil { + return &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)} + } + return user + } + + handleUnknownTopic := func(topic *common.Topic, err error) *common.Topic { + if err != nil { + return &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)} + } + return topic + } + var logs []common.LogItem var action, elementType, ipaddress, doneAt string var elementID, actorID int @@ -1697,68 +1655,41 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User) return common.InternalError(err, w, r) } - actor, err := common.Users.Get(actorID) - if err != nil { - actor = &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } + actor := handleUnknownUser(common.Users.Get(actorID)) switch action { case "lock": - topic, err := common.Topics.Get(elementID) - if err != nil { - topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - action = "" + topic.Title + " was locked by " + actor.Name + "" + topic := handleUnknownTopic(common.Topics.Get(elementID)) + action = fmt.Sprintf("%s was locked by %s", topic.Link, topic.Title, actor.Link, actor.Name) case "unlock": - topic, err := common.Topics.Get(elementID) - if err != nil { - topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - action = "" + topic.Title + " was reopened by " + actor.Name + "" + topic := handleUnknownTopic(common.Topics.Get(elementID)) + action = fmt.Sprintf("%s was reopened by %s", topic.Link, topic.Title, actor.Link, actor.Name) case "stick": - topic, err := common.Topics.Get(elementID) - if err != nil { - topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - action = "" + topic.Title + " was pinned by " + actor.Name + "" + topic := handleUnknownTopic(common.Topics.Get(elementID)) + action = fmt.Sprintf("%s was pinned by %s", topic.Link, topic.Title, actor.Link, actor.Name) case "unstick": - topic, err := common.Topics.Get(elementID) - if err != nil { - topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - action = "" + topic.Title + " was unpinned by " + actor.Name + "" + topic := handleUnknownTopic(common.Topics.Get(elementID)) + action = fmt.Sprintf("%s was unpinned by %s", topic.Link, topic.Title, actor.Link, actor.Name) case "delete": if elementType == "topic" { - action = "Topic #" + strconv.Itoa(elementID) + " was deleted by " + actor.Name + "" + action = fmt.Sprintf("Topic #%d was deleted by %s", elementID, actor.Link, actor.Name) } else { reply := common.BlankReply() reply.ID = elementID - topic, err := reply.Topic() - if err != nil { - topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - action = "A reply in " + topic.Title + " was deleted by " + actor.Name + "" + topic := handleUnknownTopic(reply.Topic()) + action = fmt.Sprintf("A reply in %s was deleted by %s", topic.Link, topic.Title, actor.Link, actor.Name) } case "ban": - targetUser, err := common.Users.Get(elementID) - if err != nil { - targetUser = &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - action = "" + targetUser.Name + " was banned by " + actor.Name + "" + targetUser := handleUnknownUser(common.Users.Get(elementID)) + action = fmt.Sprintf("%s was banned by %s", targetUser.Link, targetUser.Name, actor.Link, actor.Name) case "unban": - targetUser, err := common.Users.Get(elementID) - if err != nil { - targetUser = &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - action = "" + targetUser.Name + " was unbanned by " + actor.Name + "" + targetUser := handleUnknownUser(common.Users.Get(elementID)) + action = fmt.Sprintf("%s was unbanned by %s", targetUser.Link, targetUser.Name, actor.Link, actor.Name) case "activate": - targetUser, err := common.Users.Get(elementID) - if err != nil { - targetUser = &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)} - } - action = "" + targetUser.Name + " was activated by " + actor.Name + "" + targetUser := handleUnknownUser(common.Users.Get(elementID)) + action = fmt.Sprintf("%s was activated by %s", targetUser.Link, targetUser.Name, actor.Link, actor.Name) default: - action = "Unknown action '" + action + "' by " + actor.Name + "" + action = fmt.Sprintf("Unknown action '%s' by %s", action, actor.Link, actor.Name) } logs = append(logs, common.LogItem{Action: template.HTML(action), IPAddress: ipaddress, DoneAt: doneAt}) } @@ -1768,7 +1699,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User) } pageList := common.Paginate(logCount, perPage, 5) - pi := common.PanelLogsPage{"Moderation Logs", user, headerVars, stats, logs, pageList, page, lastPage} + pi := common.PanelLogsPage{common.GetTitlePhrase("panel-mod-logs"), user, headerVars, stats, logs, pageList, page, lastPage} if common.PreRenderHooks["pre_render_panel_mod_log"] != nil { if common.RunPreRenderHook("pre_render_panel_mod_log", w, r, &user, &pi) { return nil @@ -1792,7 +1723,7 @@ func routePanelDebug(w http.ResponseWriter, r *http.Request, user common.User) c openConnCount := dbStats.OpenConnections // Disk I/O? - pi := common.PanelDebugPage{"Debug", user, headerVars, stats, uptime, openConnCount, dbAdapter} + pi := common.PanelDebugPage{common.GetTitlePhrase("panel-debug"), user, headerVars, stats, uptime, openConnCount, dbAdapter} err := common.Templates.ExecuteTemplate(w, "panel-debug.html", pi) if err != nil { return common.InternalError(err, w, r) diff --git a/plugin_guilds.go b/plugin_guilds.go index 876c22ef..bf1d38f1 100644 --- a/plugin_guilds.go +++ b/plugin_guilds.go @@ -33,20 +33,21 @@ func initGuilds() (err error) { router.HandleFunc("/guild/create/submit/", guilds.RouteCreateGuildSubmit) router.HandleFunc("/guild/members/", guilds.RouteMemberList) + guilds.Gstore, err = guilds.NewSQLGuildStore() + if err != nil { + return err + } + acc := qgen.Builder.Accumulator() guilds.ListStmt = acc.Select("guilds").Columns("guildID, name, desc, active, privacy, joinable, owner, memberCount, createdAt, lastUpdateTime").Prepare() - guilds.GetGuildStmt = acc.Select("guilds").Columns("name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime").Where("guildID = ?").Prepare() - guilds.MemberListStmt = acc.Select("guilds_members").Columns("guildID, uid, rank, posts, joinedAt").Prepare() guilds.MemberListJoinStmt = acc.SimpleLeftJoin("guilds_members", "users", "users.uid, guilds_members.rank, guilds_members.posts, guilds_members.joinedAt, users.name, users.avatar", "guilds_members.uid = users.uid", "guilds_members.guildID = ?", "guilds_members.rank DESC, guilds_members.joinedat ASC", "") guilds.GetMemberStmt = acc.Select("guilds_members").Columns("rank, posts, joinedAt").Where("guildID = ? AND uid = ?").Prepare() - guilds.CreateGuildStmt = acc.Insert("guilds").Columns("name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime").Fields("?,?,?,?,1,?,1,?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare() - guilds.AttachForumStmt = acc.Update("forums").Set("parentID = ?, parentType = 'guild'").Where("fid = ?").Prepare() guilds.UnattachForumStmt = acc.Update("forums").Set("parentID = 0, parentType = ''").Where("fid = ?").Prepare() @@ -72,8 +73,6 @@ func deactivateGuilds() { _ = guilds.MemberListStmt.Close() _ = guilds.MemberListJoinStmt.Close() _ = guilds.GetMemberStmt.Close() - _ = guilds.GetGuildStmt.Close() - _ = guilds.CreateGuildStmt.Close() _ = guilds.AttachForumStmt.Close() _ = guilds.UnattachForumStmt.Close() _ = guilds.AddMemberStmt.Close() diff --git a/query_gen/lib/install.go b/query_gen/lib/install.go index 99aa5b49..f7219cd1 100644 --- a/query_gen/lib/install.go +++ b/query_gen/lib/install.go @@ -1,4 +1,3 @@ -/* WIP Under Construction */ package qgen var Install *installer @@ -13,11 +12,21 @@ type DB_Install_Instruction struct { Type string } +// TODO: Add methods to this to construct it OO-like +type DB_Install_Table struct { + Name string + Charset string + Collation string + Columns []DBTableColumn + Keys []DBTableKey +} + // A set of wrappers around the generator methods, so we can use this in the installer // TODO: Re-implement the query generation, query builder and installer adapters as layers on-top of a query text adapter type installer struct { adapter Adapter instructions []DB_Install_Instruction + tables []*DB_Install_Table // TODO: Use this in Record() in the next commit to allow us to auto-migrate settings rather than manually patching them in on upgrade plugins []QueryPlugin } @@ -26,8 +35,7 @@ func (install *installer) SetAdapter(name string) error { if err != nil { return err } - install.adapter = adap - install.instructions = []DB_Install_Instruction{} + install.SetAdapterInstance(adap) return nil } @@ -36,49 +44,54 @@ func (install *installer) SetAdapterInstance(adapter Adapter) { install.instructions = []DB_Install_Instruction{} } -func (install *installer) RegisterPlugin(plugin QueryPlugin) { - install.plugins = append(install.plugins, plugin) +func (install *installer) AddPlugins(plugins ...QueryPlugin) { + install.plugins = append(install.plugins, plugins...) } func (install *installer) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) error { - for _, plugin := range install.plugins { - err := plugin.Hook("CreateTableStart", table, charset, collation, columns, keys) - if err != nil { - return err - } + tableStruct := &DB_Install_Table{table, charset, collation, columns, keys} + err := install.RunHook("CreateTableStart", tableStruct) + if err != nil { + return err } res, err := install.adapter.CreateTable("_installer", table, charset, collation, columns, keys) if err != nil { return err } - for _, plugin := range install.plugins { - err := plugin.Hook("CreateTableAfter", table, charset, collation, columns, keys, res) - if err != nil { - return err - } + err = install.RunHook("CreateTableAfter", tableStruct) + if err != nil { + return err } install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "create-table"}) + install.tables = append(install.tables, tableStruct) return nil } +// TODO: Let plugins manipulate the parameters like in CreateTable func (install *installer) SimpleInsert(table string, columns string, fields string) error { - for _, plugin := range install.plugins { - err := plugin.Hook("SimpleInsertStart", table, columns, fields) - if err != nil { - return err - } + err := install.RunHook("SimpleInsertStart", table, columns, fields) + if err != nil { + return err } res, err := install.adapter.SimpleInsert("_installer", table, columns, fields) if err != nil { return err } + err = install.RunHook("SimpleInsertAfter", table, columns, fields, res) + if err != nil { + return err + } + install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "insert"}) + return nil +} + +func (install *installer) RunHook(name string, args ...interface{}) error { for _, plugin := range install.plugins { - err := plugin.Hook("SimpleInsertAfter", table, columns, fields, res) + err := plugin.Hook(name, args...) if err != nil { return err } } - install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "insert"}) return nil } diff --git a/query_gen/main.go b/query_gen/main.go index 1d79e07b..7a5d1ad2 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -26,7 +26,7 @@ func main() { for _, adapter := range qgen.Registry { log.Printf("Building the queries for the %s adapter", adapter.GetName()) qgen.Install.SetAdapterInstance(adapter) - qgen.Install.RegisterPlugin(NewPrimaryKeySpitter()) // TODO: Do we really need to fill the spitter for every adapter? + qgen.Install.AddPlugins(NewPrimaryKeySpitter()) // TODO: Do we really need to fill the spitter for every adapter? err := writeStatements(adapter) if err != nil { @@ -75,16 +75,6 @@ func writeStatements(adapter qgen.Adapter) error { return err } - /*err = writeReplaces(adapter) - if err != nil { - return err - } - - err = writeUpserts(adapter) - if err != nil { - return err - }*/ - err = writeUpdates(adapter) if err != nil { return err @@ -125,6 +115,8 @@ func seedTables(adapter qgen.Adapter) error { qgen.Install.SimpleInsert("settings", "name, content, type, constraints", "'activation_type','1','list','1-3'") qgen.Install.SimpleInsert("settings", "name, content, type", "'bigpost_min_words','250','int'") qgen.Install.SimpleInsert("settings", "name, content, type", "'megapost_min_words','1000','int'") + qgen.Install.SimpleInsert("settings", "name, content, type", "'about_segment_title','','text'") + qgen.Install.SimpleInsert("settings", "name, content, type", "'about_segment_body','','text'") qgen.Install.SimpleInsert("themes", "uname, default", "'tempra-simple',1") qgen.Install.SimpleInsert("emails", "email, uid, validated", "'admin@localhost',1,1") // ? - Use a different default email or let the admin input it during installation? @@ -168,6 +160,7 @@ func seedTables(adapter qgen.Adapter) error { CloseTopic */ + // TODO: Set the permissions on a struct and then serialize the struct and insert that instead of writing raw JSON qgen.Install.SimpleInsert("users_groups", "name, permissions, plugin_perms, is_mod, is_admin, tag", `'Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,"Admin"`) qgen.Install.SimpleInsert("users_groups", "name, permissions, plugin_perms, is_mod, tag", `'Moderator','{"BanUsers":true,"ActivateUsers":false,"EditUser":true,"EditUserEmail":false,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,"Mod"`) @@ -226,8 +219,6 @@ func writeSelects(adapter qgen.Adapter) error { build.Select("getPassword").Table("users").Columns("password, salt").Where("uid = ?").Parse() - build.Select("getSettings").Table("settings").Columns("name, content, type").Parse() - build.Select("isPluginActive").Table("plugins").Columns("active").Where("uname = ?").Parse() //build.Select("isPluginInstalled").Table("plugins").Columns("installed").Where("uname = ?").Parse() @@ -314,25 +305,6 @@ func writeInserts(adapter qgen.Adapter) error { return nil } -func writeReplaces(adapter qgen.Adapter) (err error) { - return nil -} - -// ! Upserts are broken atm -/*func writeUpserts(adapter qgen.Adapter) (err error) { - _, err = adapter.SimpleUpsert("addForumPermsToGroup", "forums_permissions", "gid, fid, preset, permissions", "?,?,?,?", "gid = ? AND fid = ?") - if err != nil { - return err - } - - _, err = adapter.SimpleUpsert("replaceScheduleGroup", "users_groups_scheduler", "uid, set_group, issued_by, issued_at, revert_at, temporary", "?,?,?,UTC_TIMESTAMP(),?,?", "uid = ?") - if err != nil { - return err - } - - return nil -}*/ - func writeUpdates(adapter qgen.Adapter) error { build := adapter.Builder() @@ -340,8 +312,6 @@ func writeUpdates(adapter qgen.Adapter) error { build.Update("editProfileReply").Table("users_replies").Set("content = ?, parsed_content = ?").Where("rid = ?").Parse() - build.Update("updateSetting").Table("settings").Set("content = ?").Where("name = ?").Parse() - build.Update("updatePlugin").Table("plugins").Set("active = ?").Where("uname = ?").Parse() build.Update("updatePluginInstall").Table("plugins").Set("installed = ?").Where("uname = ?").Parse() @@ -385,8 +355,6 @@ func writeDeletes(adapter qgen.Adapter) error { func writeSimpleCounts(adapter qgen.Adapter) error { adapter.SimpleCount("reportExists", "topics", "data = ? AND data != '' AND parentID = 1", "") - adapter.SimpleCount("modlogCount", "moderation_logs", "", "") - return nil } diff --git a/query_gen/spitter.go b/query_gen/spitter.go index 60e1ab75..8989c361 100644 --- a/query_gen/spitter.go +++ b/query_gen/spitter.go @@ -14,7 +14,8 @@ func NewPrimaryKeySpitter() *PrimaryKeySpitter { func (spit *PrimaryKeySpitter) Hook(name string, args ...interface{}) error { if name == "CreateTableStart" { var found string - for _, key := range args[4].([]qgen.DBTableKey) { + var table = args[0].(*qgen.DB_Install_Table) + for _, key := range table.Keys { if key.Type == "primary" { expl := strings.Split(key.Columns, ",") if len(expl) > 1 { @@ -23,7 +24,7 @@ func (spit *PrimaryKeySpitter) Hook(name string, args ...interface{}) error { found = key.Columns } if found != "" { - table := args[0].(string) + table := table.Name spit.keys[table] = found } } diff --git a/router_gen/routes.go b/router_gen/routes.go index be5b766f..5a93ebbc 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -68,8 +68,8 @@ func buildPanelRoutes() { Action("routePanelForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extra_data"), View("routePanelSettings", "/panel/settings/"), - View("routePanelSetting", "/panel/settings/edit/", "extra_data"), - Action("routePanelSettingEdit", "/panel/settings/edit/submit/", "extra_data"), + View("routePanelSettingEdit", "/panel/settings/edit/", "extra_data"), + Action("routePanelSettingEditSubmit", "/panel/settings/edit/submit/", "extra_data"), View("routePanelWordFilters", "/panel/settings/word-filters/"), Action("routePanelWordFiltersCreate", "/panel/settings/word-filters/create/"), diff --git a/routes.go b/routes.go index 7d740253..47099f80 100644 --- a/routes.go +++ b/routes.go @@ -46,7 +46,7 @@ func routeStatic(w http.ResponseWriter, r *http.Request) { file, ok := common.StaticFiles[r.URL.Path] if !ok { if common.Dev.DebugMode { - log.Print("Failed to find '" + r.URL.Path + "'") + log.Printf("Failed to find '%s'", r.URL.Path) } w.WriteHeader(http.StatusNotFound) return @@ -103,7 +103,7 @@ func routeOverview(w http.ResponseWriter, r *http.Request, user common.User) com } common.BuildWidgets("overview", nil, headerVars, r) - pi := common.Page{"Overview", user, headerVars, tList, nil} + pi := common.Page{common.GetTitlePhrase("overview"), user, headerVars, tList, nil} if common.PreRenderHooks["pre_render_overview"] != nil { if common.RunPreRenderHook("pre_render_overview", w, r, &user, &pi) { return nil @@ -129,7 +129,7 @@ func routeCustomPage(w http.ResponseWriter, r *http.Request, user common.User) c } common.BuildWidgets("custom_page", name, headerVars, r) - pi := common.Page{"Page", user, headerVars, tList, nil} + pi := common.Page{common.GetTitlePhrase("page"), user, headerVars, tList, nil} if common.PreRenderHooks["pre_render_custom_page"] != nil { if common.RunPreRenderHook("pre_render_custom_page", w, r, &user, &pi) { return nil @@ -152,7 +152,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo // TODO: Add a function for the qlist stuff var qlist string - group, err := common.Gstore.Get(user.Group) + group, err := common.Groups.Get(user.Group) if err != nil { log.Printf("Group #%d doesn't exist despite being used by common.User #%d", user.Group, user.ID) return common.LocalError("Something weird happened", w, r, user) @@ -161,7 +161,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo // TODO: Make CanSee a method on *Group with a canSee field? var canSee []int if user.IsSuperAdmin { - canSee, err = common.Fstore.GetAllVisibleIDs() + canSee, err = common.Forums.GetAllVisibleIDs() if err != nil { return common.InternalError(err, w, r) } @@ -174,7 +174,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo var argList []interface{} for _, fid := range canSee { - forum := common.Fstore.DirtyGet(fid) + forum := common.Forums.DirtyGet(fid) if forum.Name != "" && forum.Active { if forum.ParentType == "" || forum.ParentType == "forum" { // Optimise Quick Topic away for guests @@ -251,7 +251,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) - forum := common.Fstore.DirtyGet(topicItem.ParentID) + forum := common.Forums.DirtyGet(topicItem.ParentID) topicItem.ForumName = forum.Name topicItem.ForumLink = forum.Link @@ -291,13 +291,13 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo topicItem.LastUser = userList[topicItem.LastReplyBy] } - pi := common.TopicsPage{"All Topics", user, headerVars, topicList, forumList, common.Config.DefaultForum} + pi := common.TopicsPage{common.GetTitlePhrase("topics"), user, headerVars, topicList, forumList, common.Config.DefaultForum} if common.PreRenderHooks["pre_render_topic_list"] != nil { if common.RunPreRenderHook("pre_render_topic_list", w, r, &user, &pi) { return nil } } - err = common.RunThemeTemplate(headerVars.ThemeName, "topics", pi, w) + err = common.RunThemeTemplate(headerVars.Theme.Name, "topics", pi, w) if err != nil { return common.InternalError(err, w, r) } @@ -327,7 +327,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid s } // TODO: Fix this double-check - forum, err := common.Fstore.Get(fid) + forum, err := common.Forums.Get(fid) if err == ErrNoRows { return common.NotFound(w, r) } else if err != nil { @@ -408,7 +408,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid s return nil } } - err = common.RunThemeTemplate(headerVars.ThemeName, "forum", pi, w) + err = common.RunThemeTemplate(headerVars.Theme.Name, "forum", pi, w) if err != nil { return common.InternalError(err, w, r) } @@ -426,13 +426,12 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo var forumList []common.Forum var canSee []int if user.IsSuperAdmin { - canSee, err = common.Fstore.GetAllVisibleIDs() + canSee, err = common.Forums.GetAllVisibleIDs() if err != nil { return common.InternalError(err, w, r) } - //log.Print("canSee ", canSee) } else { - group, err := common.Gstore.Get(user.Group) + group, err := common.Groups.Get(user.Group) if err != nil { log.Printf("Group #%d doesn't exist despite being used by common.User #%d", user.Group, user.ID) return common.LocalError("Something weird happened", w, r, user) @@ -442,7 +441,7 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo for _, fid := range canSee { // Avoid data races by copying the struct into something we can freely mold without worrying about breaking something somewhere else - var forum = common.Fstore.DirtyGet(fid).Copy() + var forum = common.Forums.DirtyGet(fid).Copy() if forum.ParentID == 0 && forum.Name != "" && forum.Active { if forum.LastTopicID != 0 { if forum.LastTopic.ID != 0 && forum.LastReplyer.ID != 0 { @@ -460,13 +459,13 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo } } - pi := common.ForumsPage{"Forum List", user, headerVars, forumList} + pi := common.ForumsPage{common.GetTitlePhrase("forums"), user, headerVars, forumList} if common.PreRenderHooks["pre_render_forum_list"] != nil { if common.RunPreRenderHook("pre_render_forum_list", w, r, &user, &pi) { return nil } } - err = common.RunThemeTemplate(headerVars.ThemeName, "forums", pi, w) + err = common.RunThemeTemplate(headerVars.Theme.Name, "forums", pi, w) if err != nil { return common.InternalError(err, w, r) } @@ -481,6 +480,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm page, _ = strconv.Atoi(r.FormValue("page")) // SEO URLs... + // TODO: Make a shared function for this halves := strings.Split(r.URL.Path[len("/topic/"):], ".") if len(halves) < 2 { halves = append(halves, halves[0]) @@ -520,7 +520,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm user.Perms.CreateReply = false } - postGroup, err := common.Gstore.Get(topic.Group) + postGroup, err := common.Groups.Get(topic.Group) if err != nil { return common.InternalError(err, w, r) } @@ -574,7 +574,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums") replyItem.ContentLines = strings.Count(replyItem.Content, "\n") - postGroup, err = common.Gstore.Get(replyItem.Group) + postGroup, err = common.Groups.Get(replyItem.Group) if err != nil { return common.InternalError(err, w, r) } @@ -635,7 +635,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm return nil } } - err = common.RunThemeTemplate(headerVars.ThemeName, "topic", tpage, w) + err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, w) if err != nil { return common.InternalError(err, w, r) } @@ -671,6 +671,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm puser = &user } else { // Fetch the user data + // TODO: Add a shared function for checking for ErrNoRows and internal erroring if it's not that case? puser, err = common.Users.Get(pid) if err == ErrNoRows { return common.NotFound(w, r) @@ -692,7 +693,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm return common.InternalError(err, w, r) } - group, err := common.Gstore.Get(replyGroup) + group, err := common.Groups.Get(replyGroup) if err != nil { return common.InternalError(err, w, r) } @@ -733,6 +734,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm return common.InternalError(err, w, r) } + // TODO: Add a phrase for this title ppage := common.ProfilePage{puser.Name + "'s Profile", user, headerVars, replyList, *puser} if common.PreRenderHooks["pre_render_profile"] != nil { if common.RunPreRenderHook("pre_render_profile", w, r, &user, &ppage) { @@ -740,7 +742,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm } } - err = common.RunThemeTemplate(headerVars.ThemeName, "profile", ppage, w) + err = common.RunThemeTemplate(headerVars.Theme.Name, "profile", ppage, w) if err != nil { return common.InternalError(err, w, r) } @@ -755,7 +757,7 @@ func routeLogin(w http.ResponseWriter, r *http.Request, user common.User) common if user.Loggedin { return common.LocalError("You're already logged in.", w, r, user) } - pi := common.Page{"Login", user, headerVars, tList, nil} + pi := common.Page{common.GetTitlePhrase("login"), user, headerVars, tList, nil} if common.PreRenderHooks["pre_render_login"] != nil { if common.RunPreRenderHook("pre_render_login", w, r, &user, &pi) { return nil @@ -803,7 +805,7 @@ func routeLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.Auth.SetCookies(w, uid, session) if user.IsAdmin { - // Is this error check reundant? We already check for the error in PreRoute for the same IP + // Is this error check redundant? We already check for the error in PreRoute for the same IP // TODO: Should we be logging this? log.Printf("#%d has logged in with IP %s", uid, user.LastIP) } @@ -819,7 +821,7 @@ func routeRegister(w http.ResponseWriter, r *http.Request, user common.User) com if user.Loggedin { return common.LocalError("You're already logged in.", w, r, user) } - pi := common.Page{"Registration", user, headerVars, tList, nil} + pi := common.Page{common.GetTitlePhrase("register"), user, headerVars, tList, nil} if common.PreRenderHooks["pre_render_register"] != nil { if common.RunPreRenderHook("pre_render_register", w, r, &user, &pi) { return nil @@ -937,10 +939,12 @@ func routeChangeTheme(w http.ResponseWriter, r *http.Request, user common.User) return nil } -// TODO: We don't need support XML here to support sitemaps, we could handle those elsewhere +// TODO: Refactor this var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}]}`) +// TODO: Refactor this endpoint func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + // TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats w.Header().Set("Content-Type", "application/json") err := r.ParseForm() if err != nil { @@ -1008,11 +1012,6 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R msglist = msglist[0 : len(msglist)-1] } _, _ = w.Write([]byte(`{"msgs":[` + msglist + `],"msgCount":` + strconv.Itoa(msgCount) + `}`)) - //log.Print(`{"msgs":[` + msglist + `],"msgCount":` + strconv.Itoa(msgCount) + `}`) - //case "topics": - //case "forums": - //case "users": - //case "pages": default: return common.PreErrorJS("Invalid Module", w, r) } diff --git a/schema/mssql/inserts.sql b/schema/mssql/inserts.sql index 4b794aec..12818f2c 100644 --- a/schema/mssql/inserts.sql +++ b/schema/mssql/inserts.sql @@ -3,6 +3,8 @@ INSERT INTO [settings] ([name],[content],[type]) VALUES ('url_tags','1','bool'); INSERT INTO [settings] ([name],[content],[type],[constraints]) VALUES ('activation_type','1','list','1-3'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('bigpost_min_words','250','int'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1000','int'); +INSERT INTO [settings] ([name],[content],[type]) VALUES ('about_segment_title','','text'); +INSERT INTO [settings] ([name],[content],[type]) VALUES ('about_segment_body','','text'); INSERT INTO [themes] ([uname],[default]) VALUES ('tempra-simple',1); INSERT INTO [emails] ([email],[uid],[validated]) VALUES ('admin@localhost',1,1); INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[tag]) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,'Admin'); diff --git a/schema/mysql/inserts.sql b/schema/mysql/inserts.sql index 6a48d6e6..8060ff0a 100644 --- a/schema/mysql/inserts.sql +++ b/schema/mysql/inserts.sql @@ -3,6 +3,8 @@ INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('url_tags','1','bool'); INSERT INTO `settings`(`name`,`content`,`type`,`constraints`) VALUES ('activation_type','1','list','1-3'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('bigpost_min_words','250','int'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int'); +INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('about_segment_title','','text'); +INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('about_segment_body','','text'); INSERT INTO `themes`(`uname`,`default`) VALUES ('tempra-simple',1); INSERT INTO `emails`(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1); INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,'Admin'); diff --git a/schema/pgsql/inserts.sql b/schema/pgsql/inserts.sql index 136d3909..27b5c310 100644 --- a/schema/pgsql/inserts.sql +++ b/schema/pgsql/inserts.sql @@ -27,3 +27,5 @@ ; ; ; +; +; diff --git a/template_forum.go b/template_forum.go index cb9ebe26..bda03045 100644 --- a/template_forum.go +++ b/template_forum.go @@ -22,7 +22,7 @@ w.Write([]byte(tmpl_forum_vars.Title)) w.Write(header_1) w.Write([]byte(tmpl_forum_vars.Header.Site.Name)) w.Write(header_2) -w.Write([]byte(tmpl_forum_vars.Header.ThemeName)) +w.Write([]byte(tmpl_forum_vars.Header.Theme.Name)) w.Write(header_3) if len(tmpl_forum_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_forum_vars.Header.Stylesheets { @@ -203,28 +203,37 @@ w.Write(forum_57) w.Write(forum_58) } w.Write(forum_59) +if tmpl_forum_vars.Header.Theme.AboutSegment { w.Write(footer_0) +dispInt := tmpl_forum_vars.Header.Settings["about_segment_title"] +w.Write([]byte(dispInt.(string))) +w.Write(footer_1) +dispInt = tmpl_forum_vars.Header.Settings["about_segment_body"] +w.Write([]byte(dispInt.(string))) +w.Write(footer_2) +} +w.Write(footer_3) if len(tmpl_forum_vars.Header.Themes) != 0 { for _, item := range tmpl_forum_vars.Header.Themes { if !item.HideFromThemes { -w.Write(footer_1) -w.Write([]byte(item.Name)) -w.Write(footer_2) -if tmpl_forum_vars.Header.ThemeName == item.Name { -w.Write(footer_3) -} w.Write(footer_4) -w.Write([]byte(item.FriendlyName)) +w.Write([]byte(item.Name)) w.Write(footer_5) -} -} -} +if tmpl_forum_vars.Header.Theme.Name == item.Name { w.Write(footer_6) -if tmpl_forum_vars.Header.Widgets.RightSidebar != "" { +} w.Write(footer_7) -w.Write([]byte(string(tmpl_forum_vars.Header.Widgets.RightSidebar))) +w.Write([]byte(item.FriendlyName)) w.Write(footer_8) } +} +} w.Write(footer_9) +if tmpl_forum_vars.Header.Widgets.RightSidebar != "" { +w.Write(footer_10) +w.Write([]byte(string(tmpl_forum_vars.Header.Widgets.RightSidebar))) +w.Write(footer_11) +} +w.Write(footer_12) return nil } diff --git a/template_forums.go b/template_forums.go index 619dd13a..9d849a5a 100644 --- a/template_forums.go +++ b/template_forums.go @@ -21,7 +21,7 @@ w.Write([]byte(tmpl_forums_vars.Title)) w.Write(header_1) w.Write([]byte(tmpl_forums_vars.Header.Site.Name)) w.Write(header_2) -w.Write([]byte(tmpl_forums_vars.Header.ThemeName)) +w.Write([]byte(tmpl_forums_vars.Header.Theme.Name)) w.Write(header_3) if len(tmpl_forums_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_forums_vars.Header.Stylesheets { @@ -118,28 +118,37 @@ w.Write(forums_18) w.Write(forums_19) } w.Write(forums_20) +if tmpl_forums_vars.Header.Theme.AboutSegment { w.Write(footer_0) +dispInt := tmpl_forums_vars.Header.Settings["about_segment_title"] +w.Write([]byte(dispInt.(string))) +w.Write(footer_1) +dispInt = tmpl_forums_vars.Header.Settings["about_segment_body"] +w.Write([]byte(dispInt.(string))) +w.Write(footer_2) +} +w.Write(footer_3) if len(tmpl_forums_vars.Header.Themes) != 0 { for _, item := range tmpl_forums_vars.Header.Themes { if !item.HideFromThemes { -w.Write(footer_1) -w.Write([]byte(item.Name)) -w.Write(footer_2) -if tmpl_forums_vars.Header.ThemeName == item.Name { -w.Write(footer_3) -} w.Write(footer_4) -w.Write([]byte(item.FriendlyName)) +w.Write([]byte(item.Name)) w.Write(footer_5) -} -} -} +if tmpl_forums_vars.Header.Theme.Name == item.Name { w.Write(footer_6) -if tmpl_forums_vars.Header.Widgets.RightSidebar != "" { +} w.Write(footer_7) -w.Write([]byte(string(tmpl_forums_vars.Header.Widgets.RightSidebar))) +w.Write([]byte(item.FriendlyName)) w.Write(footer_8) } +} +} w.Write(footer_9) +if tmpl_forums_vars.Header.Widgets.RightSidebar != "" { +w.Write(footer_10) +w.Write([]byte(string(tmpl_forums_vars.Header.Widgets.RightSidebar))) +w.Write(footer_11) +} +w.Write(footer_12) return nil } diff --git a/template_guilds_guild_list.go b/template_guilds_guild_list.go index bbaa3790..b22e4d5e 100644 --- a/template_guilds_guild_list.go +++ b/template_guilds_guild_list.go @@ -20,7 +20,7 @@ w.Write([]byte(tmpl_guilds_guild_list_vars.Title)) w.Write(header_1) w.Write([]byte(tmpl_guilds_guild_list_vars.Header.Site.Name)) w.Write(header_2) -w.Write([]byte(tmpl_guilds_guild_list_vars.Header.ThemeName)) +w.Write([]byte(tmpl_guilds_guild_list_vars.Header.Theme.Name)) w.Write(header_3) if len(tmpl_guilds_guild_list_vars.Header.Stylesheets) != 0 { for _, item := range tmpl_guilds_guild_list_vars.Header.Stylesheets { @@ -91,28 +91,37 @@ w.Write(guilds_guild_list_6) w.Write(guilds_guild_list_7) } w.Write(guilds_guild_list_8) +if tmpl_guilds_guild_list_vars.Header.Theme.AboutSegment { w.Write(footer_0) +dispInt := tmpl_guilds_guild_list_vars.Header.Settings["about_segment_title"] +w.Write([]byte(dispInt.(string))) +w.Write(footer_1) +dispInt = tmpl_guilds_guild_list_vars.Header.Settings["about_segment_body"] +w.Write([]byte(dispInt.(string))) +w.Write(footer_2) +} +w.Write(footer_3) if len(tmpl_guilds_guild_list_vars.Header.Themes) != 0 { for _, item := range tmpl_guilds_guild_list_vars.Header.Themes { if !item.HideFromThemes { -w.Write(footer_1) -w.Write([]byte(item.Name)) -w.Write(footer_2) -if tmpl_guilds_guild_list_vars.Header.ThemeName == item.Name { -w.Write(footer_3) -} w.Write(footer_4) -w.Write([]byte(item.FriendlyName)) +w.Write([]byte(item.Name)) w.Write(footer_5) -} -} -} +if tmpl_guilds_guild_list_vars.Header.Theme.Name == item.Name { w.Write(footer_6) -if tmpl_guilds_guild_list_vars.Header.Widgets.RightSidebar != "" { +} w.Write(footer_7) -w.Write([]byte(string(tmpl_guilds_guild_list_vars.Header.Widgets.RightSidebar))) +w.Write([]byte(item.FriendlyName)) w.Write(footer_8) } +} +} w.Write(footer_9) +if tmpl_guilds_guild_list_vars.Header.Widgets.RightSidebar != "" { +w.Write(footer_10) +w.Write([]byte(string(tmpl_guilds_guild_list_vars.Header.Widgets.RightSidebar))) +w.Write(footer_11) +} +w.Write(footer_12) return nil } diff --git a/template_list.go b/template_list.go index dad04b44..ff7c49e8 100644 --- a/template_list.go +++ b/template_list.go @@ -264,27 +264,36 @@ var topic_100 = []byte(` `) -var footer_0 = []byte(`
`) -var footer_7 = []byte(``) -var footer_9 = []byte(` +var footer_10 = []byte(``) +var footer_12 = []byte(` @@ -498,7 +507,7 @@ var topic_alt_104 = []byte(` `) var profile_0 = []byte(` -