From d0363f3eb136d9cf2abbf2fa7c2843a9b0e98dde Mon Sep 17 00:00:00 2001 From: Azareal Date: Thu, 2 Nov 2017 13:35:19 +0000 Subject: [PATCH] Refactored the GroupStore to make it more thread-safe. Refactored the store initialisers to better propogate errors upwards. Moved the user initialisation logic to a method on *User. Added the Reload and GlobalCount methods to the GroupStore. Added the CacheSet method to the GroupCache. Renamed plugin_socialgroups to plugin_guilds 3/3 --- database.go | 30 +++- errors.go | 21 +-- extend/guilds/plugin.json | 7 + forum_store.go | 12 +- group.go | 27 ++-- group_store.go | 216 +++++++++++++++++++++-------- misc_test.go | 14 +- panel_routes.go | 6 +- permissions.go | 113 ++++++--------- plugin_guilds.go | 2 +- pluginlangs.go | 4 + templates/guilds_create_guild.html | 8 +- templates/guilds_guild_list.html | 6 +- templates/guilds_member_list.html | 19 +-- templates/guilds_view_guild.html | 12 +- topic_store.go | 21 ++- user.go | 14 ++ user_store.go | 110 +++------------ 18 files changed, 347 insertions(+), 295 deletions(-) create mode 100644 extend/guilds/plugin.json diff --git a/database.go b/database.go index 206ab9d2..c1e6f95f 100644 --- a/database.go +++ b/database.go @@ -21,7 +21,10 @@ func initDatabase() (err error) { } log.Print("Loading the usergroups.") - gstore = NewMemoryGroupStore() + gstore, err = NewMemoryGroupStore() + if err != nil { + return err + } err = gstore.LoadGroups() if err != nil { return err @@ -30,15 +33,30 @@ func initDatabase() (err error) { // 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") if config.CacheTopicUser == CACHE_STATIC { - users = NewMemoryUserStore(config.UserCacheCapacity) - topics = NewMemoryTopicStore(config.TopicCacheCapacity) + users, err = NewMemoryUserStore(config.UserCacheCapacity) + if err != nil { + return err + } + topics, err = NewMemoryTopicStore(config.TopicCacheCapacity) + if err != nil { + return err + } } else { - users = NewSQLUserStore() - topics = NewSQLTopicStore() + users, err = NewSQLUserStore() + if err != nil { + return err + } + topics, err = NewSQLTopicStore() + if err != nil { + return err + } } log.Print("Loading the forums.") - fstore = NewMemoryForumStore() + fstore, err = NewMemoryForumStore() + if err != nil { + return err + } err = fstore.LoadForums() if err != nil { return err diff --git a/errors.go b/errors.go index 6a0778cb..1c902db7 100644 --- a/errors.go +++ b/errors.go @@ -37,9 +37,8 @@ func (err *RouteErrorImpl) Type() string { // System errors may contain sensitive information we don't want the user to see if err.system { return "system" - } else { - return "user" } + return "user" } func (err *RouteErrorImpl) Error() string { @@ -101,9 +100,8 @@ func InternalError(err error, w http.ResponseWriter, r *http.Request) RouteError func InternalErrorJSQ(err error, w http.ResponseWriter, r *http.Request, isJs bool) RouteError { if !isJs { return InternalError(err, w, r) - } else { - return InternalErrorJS(err, w, r) } + return InternalErrorJS(err, w, r) } // InternalErrorJS is the JSON version of InternalError on routes we know will only be requested via JSON. E.g. An API. @@ -160,9 +158,8 @@ func PreErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteErro func PreErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, isJs bool) RouteError { if !isJs { return PreError(errmsg, w, r) - } else { - return PreErrorJS(errmsg, w, r) } + return PreErrorJS(errmsg, w, r) } // LocalError is an error shown to the end-user when something goes wrong and it's not the software's fault @@ -184,9 +181,8 @@ func LocalError(errmsg string, w http.ResponseWriter, r *http.Request, user User func LocalErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { if !isJs { return LocalError(errmsg, w, r, user) - } else { - return LocalErrorJS(errmsg, w, r) } + return LocalErrorJS(errmsg, w, r) } func LocalErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError { @@ -216,9 +212,8 @@ func NoPermissions(w http.ResponseWriter, r *http.Request, user User) RouteError func NoPermissionsJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { if !isJs { return NoPermissions(w, r, user) - } else { - return NoPermissionsJS(w, r, user) } + return NoPermissionsJS(w, r, user) } func NoPermissionsJS(w http.ResponseWriter, r *http.Request, user User) RouteError { @@ -248,9 +243,8 @@ func Banned(w http.ResponseWriter, r *http.Request, user User) RouteError { func BannedJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { if !isJs { return Banned(w, r, user) - } else { - return BannedJS(w, r, user) } + return BannedJS(w, r, user) } func BannedJS(w http.ResponseWriter, r *http.Request, user User) RouteError { @@ -331,9 +325,8 @@ func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWri func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { if !isJs { return CustomError(errmsg, errcode, errtitle, w, r, user) - } else { - return CustomErrorJS(errmsg, errcode, errtitle, w, r, user) } + return CustomErrorJS(errmsg, errcode, errtitle, w, r, user) } // CustomErrorJS is the pure JSON version of CustomError diff --git a/extend/guilds/plugin.json b/extend/guilds/plugin.json new file mode 100644 index 00000000..b35e87f6 --- /dev/null +++ b/extend/guilds/plugin.json @@ -0,0 +1,7 @@ +{ + "UName":"guilds", + "Name":"Guilds", + "Author":"Azareal", + "URL":"https://github.com/Azareal/Gosora", + "Skip":true +} \ No newline at end of file diff --git a/forum_store.go b/forum_store.go index 847e8745..f718571d 100644 --- a/forum_store.go +++ b/forum_store.go @@ -66,30 +66,30 @@ type MemoryForumStore struct { } // NewMemoryForumStore gives you a new instance of MemoryForumStore -func NewMemoryForumStore() *MemoryForumStore { +func NewMemoryForumStore() (*MemoryForumStore, error) { getStmt, err := qgen.Builder.SimpleSelect("forums", "name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID", "fid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } getAllStmt, err := qgen.Builder.SimpleSelect("forums", "fid, name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID", "", "fid ASC", "") if err != nil { - log.Fatal(err) + return nil, err } // TODO: Do a proper delete deleteStmt, err := qgen.Builder.SimpleUpdate("forums", "name= '', active = 0", "fid = ?") if err != nil { - log.Fatal(err) + return nil, err } forumCountStmt, err := qgen.Builder.SimpleCount("forums", "name != ''", "") if err != nil { - log.Fatal(err) + return nil, err } return &MemoryForumStore{ get: getStmt, getAll: getAllStmt, delete: deleteStmt, getForumCount: forumCountStmt, - } + }, nil } // TODO: Add support for subforums diff --git a/group.go b/group.go index 23ac043a..a138e5d8 100644 --- a/group.go +++ b/group.go @@ -11,7 +11,7 @@ type GroupAdmin struct { CanDelete bool } -// ! Fix the data races +// ! Fix the data races in the fperms type Group struct { ID int Name string @@ -27,26 +27,31 @@ type Group struct { CanSee []int // The IDs of the forums this group can see } -// TODO: Reload the group from the database rather than modifying it via it's pointer func (group *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err error) { _, err = updateGroupRankStmt.Exec(isAdmin, isMod, isBanned, group.ID) if err != nil { return err } - group.IsAdmin = isAdmin - group.IsMod = isMod - if isAdmin || isMod { - group.IsBanned = false - } else { - group.IsBanned = isBanned - } - + gstore.Reload(group.ID) return nil } -// ! Ahem, don't listen to the comment below. It's not concurrency safe right now. // Copy gives you a non-pointer concurrency safe copy of the group func (group *Group) Copy() Group { return *group } + +// TODO: Replace this sorting mechanism with something a lot more efficient +// ? - Use sort.Slice instead? +type SortGroup []*Group + +func (sg SortGroup) Len() int { + return len(sg) +} +func (sg SortGroup) Swap(i, j int) { + sg[i], sg[j] = sg[j], sg[i] +} +func (sg SortGroup) Less(i, j int) bool { + return sg[i].ID < sg[j].ID +} diff --git a/group_store.go b/group_store.go index 7ad8d8a3..848668f9 100644 --- a/group_store.go +++ b/group_store.go @@ -2,16 +2,16 @@ package main import ( + "database/sql" "encoding/json" "errors" "log" + "sort" "sync" "./query_gen/lib" ) -var groupCreateMutex sync.Mutex -var groupUpdateMutex sync.Mutex var gstore GroupStore // ? - We could fallback onto the database when an item can't be found in the cache? @@ -24,23 +24,40 @@ type GroupStore interface { Create(name string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error) GetAll() ([]*Group, error) GetRange(lower int, higher int) ([]*Group, error) + Reload(id int) error // ? - Should we move this to GroupCache? It might require us to do some unnecessary casting though + GlobalCount() int } type GroupCache interface { + CacheSet(group *Group) error Length() int } type MemoryGroupStore struct { - groups []*Group // TODO: Use a sync.Map instead of a slice - groupCapCount int + groups map[int]*Group // TODO: Use a sync.Map instead of a map? + groupCount int + get *sql.Stmt + + sync.RWMutex } -func NewMemoryGroupStore() *MemoryGroupStore { - return &MemoryGroupStore{} +func NewMemoryGroupStore() (*MemoryGroupStore, error) { + getGroupStmt, err := qgen.Builder.SimpleSelect("users_groups", "name, permissions, plugin_perms, is_mod, is_admin, is_banned, tag", "gid = ?", "", "") + if err != nil { + return nil, err + } + + return &MemoryGroupStore{ + groups: make(map[int]*Group), + groupCount: 0, + get: getGroupStmt, + }, nil } func (mgs *MemoryGroupStore) LoadGroups() error { - mgs.groups = []*Group{&Group{ID: 0, Name: "Unknown"}} + mgs.Lock() + defer mgs.Unlock() + mgs.groups[0] = &Group{ID: 0, Name: "Unknown"} rows, err := getGroupsStmt.Query() if err != nil { @@ -50,79 +67,136 @@ func (mgs *MemoryGroupStore) LoadGroups() error { i := 1 for ; rows.Next(); i++ { - group := Group{ID: 0} + group := &Group{ID: 0} err := rows.Scan(&group.ID, &group.Name, &group.PermissionsText, &group.PluginPermsText, &group.IsMod, &group.IsAdmin, &group.IsBanned, &group.Tag) if err != nil { return err } - err = json.Unmarshal(group.PermissionsText, &group.Perms) + err = mgs.initGroup(group) if err != nil { return err } - if dev.DebugMode { - log.Print(group.Name + ": ") - log.Printf("%+v\n", group.Perms) - } - - err = json.Unmarshal(group.PluginPermsText, &group.PluginPerms) - if err != nil { - return err - } - if dev.DebugMode { - log.Print(group.Name + ": ") - log.Printf("%+v\n", group.PluginPerms) - } - //group.Perms.ExtData = make(map[string]bool) - // TODO: Can we optimise the bit where this cascades down to the user now? - if group.IsAdmin || group.IsMod { - group.IsBanned = false - } - mgs.groups = append(mgs.groups, &group) + mgs.groups[group.ID] = group } err = rows.Err() if err != nil { return err } - mgs.groupCapCount = i + mgs.groupCount = i if dev.DebugMode { log.Print("Binding the Not Loggedin Group") } - GuestPerms = mgs.groups[6].Perms + GuestPerms = mgs.dirtyGetUnsafe(6).Perms return nil } -func (mgs *MemoryGroupStore) DirtyGet(gid int) *Group { - if !mgs.Exists(gid) { +// TODO: Hit the database when the item isn't in memory +func (mgs *MemoryGroupStore) dirtyGetUnsafe(gid int) *Group { + group, ok := mgs.groups[gid] + if !ok { return &blankGroup } - return mgs.groups[gid] + return group } +// TODO: Hit the database when the item isn't in memory +func (mgs *MemoryGroupStore) DirtyGet(gid int) *Group { + mgs.RLock() + group, ok := mgs.groups[gid] + mgs.RUnlock() + if !ok { + return &blankGroup + } + return group +} + +// TODO: Hit the database when the item isn't in memory func (mgs *MemoryGroupStore) Get(gid int) (*Group, error) { - if !mgs.Exists(gid) { + mgs.RLock() + group, ok := mgs.groups[gid] + mgs.RUnlock() + if !ok { return nil, ErrNoRows } - return mgs.groups[gid], nil + return group, nil } +// TODO: Hit the database when the item isn't in memory func (mgs *MemoryGroupStore) GetCopy(gid int) (Group, error) { - if !mgs.Exists(gid) { + mgs.RLock() + group, ok := mgs.groups[gid] + mgs.RUnlock() + if !ok { return blankGroup, ErrNoRows } - return *mgs.groups[gid], nil + return *group, nil } +func (mgs *MemoryGroupStore) Reload(id int) error { + var group = &Group{ID: id} + err := mgs.get.QueryRow(id).Scan(&group.Name, &group.PermissionsText, &group.PluginPermsText, &group.IsMod, &group.IsAdmin, &group.IsBanned, &group.Tag) + if err != nil { + return err + } + + err = mgs.initGroup(group) + if err != nil { + LogError(err) + } + mgs.CacheSet(group) + + err = rebuildGroupPermissions(id) + if err != nil { + LogError(err) + } + return nil +} + +func (mgs *MemoryGroupStore) initGroup(group *Group) error { + err := json.Unmarshal(group.PermissionsText, &group.Perms) + if err != nil { + return err + } + if dev.DebugMode { + log.Print(group.Name + ": ") + log.Printf("%+v\n", group.Perms) + } + + err = json.Unmarshal(group.PluginPermsText, &group.PluginPerms) + if err != nil { + return err + } + if dev.DebugMode { + log.Print(group.Name + ": ") + log.Printf("%+v\n", group.PluginPerms) + } + //group.Perms.ExtData = make(map[string]bool) + // TODO: Can we optimise the bit where this cascades down to the user now? + if group.IsAdmin || group.IsMod { + group.IsBanned = false + } + return nil +} + +func (mgs *MemoryGroupStore) CacheSet(group *Group) error { + mgs.Lock() + mgs.groups[group.ID] = group + mgs.Unlock() + return nil +} + +// TODO: Hit the database when the item isn't in memory func (mgs *MemoryGroupStore) Exists(gid int) bool { - return (gid <= mgs.groupCapCount) && (gid >= 0) && mgs.groups[gid].Name != "" + mgs.RLock() + group, ok := mgs.groups[gid] + mgs.RUnlock() + return ok && group.Name != "" } // ? Allow two groups with the same name? -func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error) { - groupCreateMutex.Lock() - defer groupCreateMutex.Unlock() - +func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod bool, isBanned bool) (gid int, err error) { var permstr = "{}" tx, err := db.Begin() if err != nil { @@ -143,7 +217,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod if err != nil { return 0, err } - var gid = int(gid64) + gid = int(gid64) var perms = BlankPerms var blankForums []ForumPerms @@ -199,8 +273,10 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod isBanned = false } - mgs.groups = append(mgs.groups, &Group{gid, name, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankForums, blankIntList}) - mgs.groupCapCount++ + mgs.Lock() + mgs.groups[gid] = &Group{gid, name, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankForums, blankIntList} + mgs.groupCount++ + mgs.Unlock() for _, forum := range fdata { err = rebuildForumPermissions(forum.ID) @@ -212,34 +288,62 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod return gid, nil } -// ! NOT CONCURRENT -func (mgs *MemoryGroupStore) GetAll() ([]*Group, error) { +func (mgs *MemoryGroupStore) GetAll() (results []*Group, err error) { + var i int + mgs.RLock() + results = make([]*Group, len(mgs.groups)) + for _, group := range mgs.groups { + results[i] = group + i++ + } + mgs.RUnlock() + sort.Sort(SortGroup(results)) + return results, nil +} + +func (mgs *MemoryGroupStore) GetAllMap() (map[int]*Group, error) { + mgs.RLock() + defer mgs.RUnlock() return mgs.groups, nil } -// ? - It's currently faster to use GetAll(), but we'll be dropping the guarantee that the slices are ordered soon // ? - Set the lower and higher numbers to 0 to remove the bounds -// ? - Currently uses slicing for efficiency, so it might behave a little weirdly +// TODO: Might be a little slow right now, maybe we can cache the groups in a slice or break the map up into chunks func (mgs *MemoryGroupStore) GetRange(lower int, higher int) (groups []*Group, err error) { if lower == 0 && higher == 0 { return mgs.GetAll() - } else if lower == 0 { + } + + if lower == 0 { if higher < 0 { return nil, errors.New("higher may not be lower than 0") } - if higher > len(mgs.groups) { - higher = len(mgs.groups) - } - groups = mgs.groups[:higher] } else if higher == 0 { if lower < 0 { return nil, errors.New("lower may not be lower than 0") } - groups = mgs.groups[lower:] } + + mgs.RLock() + for gid, group := range mgs.groups { + if gid >= lower && (gid <= higher || higher == 0) { + groups = append(groups, group) + } + } + mgs.RUnlock() + sort.Sort(SortGroup(groups)) + return groups, nil } func (mgs *MemoryGroupStore) Length() int { - return len(mgs.groups) + mgs.RLock() + defer mgs.RUnlock() + return mgs.groupCount +} + +func (mgs *MemoryGroupStore) GlobalCount() int { + mgs.RLock() + defer mgs.RUnlock() + return mgs.groupCount } diff --git a/misc_test.go b/misc_test.go index f264c6b4..41e11a37 100644 --- a/misc_test.go +++ b/misc_test.go @@ -37,10 +37,13 @@ func TestUserStore(t *testing.T) { initPlugins() } - users = NewMemoryUserStore(config.UserCacheCapacity) + var err error + users, err = NewMemoryUserStore(config.UserCacheCapacity) + expectNilErr(t, err) users.(UserCache).Flush() userStoreTest(t, 2) - users = NewSQLUserStore() + users, err = NewSQLUserStore() + expectNilErr(t, err) userStoreTest(t, 3) } func userStoreTest(t *testing.T, newUserID int) { @@ -453,9 +456,12 @@ func TestTopicStore(t *testing.T) { initPlugins() } - topics = NewMemoryTopicStore(config.TopicCacheCapacity) + var err error + topics, err = NewMemoryTopicStore(config.TopicCacheCapacity) + expectNilErr(t, err) topicStoreTest(t) - topics = NewSQLTopicStore() + topics, err = NewSQLTopicStore() + expectNilErr(t, err) topicStoreTest(t) } func topicStoreTest(t *testing.T) { diff --git a/panel_routes.go b/panel_routes.go index 5c6373b2..0477cf8c 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -1485,8 +1485,6 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use originalRank = "Member" } - groupUpdateMutex.Lock() - defer groupUpdateMutex.Unlock() if rank != originalRank { if !user.Perms.EditGroupGlobalPerms { return LocalError("You need the EditGroupGlobalPerms permission to change the group type.", w, r, user) @@ -1517,12 +1515,12 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use } } + // TODO: Move this to *Group _, err = updateGroupStmt.Exec(gname, gtag, gid) if err != nil { return InternalError(err, w, r) } - group.Name = gname - group.Tag = gtag + gstore.Reload(gid) http.Redirect(w, r, "/panel/groups/edit/"+strconv.Itoa(gid), http.StatusSeeOther) return nil diff --git a/permissions.go b/permissions.go index 159194e2..b738fe7d 100644 --- a/permissions.go +++ b/permissions.go @@ -4,7 +4,6 @@ import ( "database/sql" "encoding/json" "log" - "strconv" "sync" "./query_gen/lib" @@ -434,47 +433,9 @@ func rebuildForumPermissions(fid int) error { forumPerms[gid][fid] = pperms } - groups, err := gstore.GetAll() - if err != nil { - return err - } - - for _, group := range groups { - if dev.DebugMode { - log.Print("Updating the forum permissions for Group #" + strconv.Itoa(group.ID)) - } - group.Forums = []ForumPerms{BlankForumPerms} - group.CanSee = []int{} - - for _, ffid := range fids { - forumPerm, ok := forumPerms[group.ID][ffid] - if ok { - //log.Print("Overriding permissions for forum #" + strconv.Itoa(fid)) - group.Forums = append(group.Forums, forumPerm) - } else { - //log.Print("Inheriting from default for forum #" + strconv.Itoa(fid)) - forumPerm = BlankForumPerms - group.Forums = append(group.Forums, forumPerm) - } - if forumPerm.Overrides { - if forumPerm.ViewTopic { - group.CanSee = append(group.CanSee, ffid) - } - } else if group.Perms.ViewTopic { - group.CanSee = append(group.CanSee, ffid) - } - } - if dev.SuperDebug { - log.Printf("group.CanSee %+v\n", group.CanSee) - log.Printf("group.Forums %+v\n", group.Forums) - log.Print("len(group.CanSee)", len(group.CanSee)) - log.Print("len(group.Forums)", len(group.Forums)) // This counts blank aka 0 - } - } - return nil + return cascadePermSetToGroups(forumPerms, fids) } -// ? - We could have buildForumPermissions and rebuildForumPermissions call a third function containing common logic? func buildForumPermissions() error { fids, err := fstore.GetAllIDs() if err != nil { @@ -496,6 +457,7 @@ func buildForumPermissions() error { log.Print("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() { @@ -529,6 +491,10 @@ func buildForumPermissions() error { forumPerms[gid][fid] = pperms } + return cascadePermSetToGroups(forumPerms, fids) +} + +func cascadePermSetToGroups(forumPerms map[int]map[int]ForumPerms, fids []int) error { groups, err := gstore.GetAll() if err != nil { return err @@ -536,49 +502,54 @@ func buildForumPermissions() error { for _, group := range groups { if dev.DebugMode { - log.Print("Adding the forum permissions for Group #" + strconv.Itoa(group.ID) + " - " + group.Name) + log.Printf("Updating the forum permissions for Group #%d", group.ID) } group.Forums = []ForumPerms{BlankForumPerms} group.CanSee = []int{} - for _, fid := range fids { - if dev.SuperDebug { - log.Printf("Forum #%+v\n", fid) - } - forumPerm, ok := forumPerms[group.ID][fid] - if ok { - // Override group perms - //log.Print("Overriding permissions for forum #" + strconv.Itoa(fid)) - group.Forums = append(group.Forums, forumPerm) - } else { - // Inherit from Group - //log.Print("Inheriting from default for forum #" + strconv.Itoa(fid)) - forumPerm = BlankForumPerms - group.Forums = append(group.Forums, forumPerm) - } - if forumPerm.Overrides { - if forumPerm.ViewTopic { - group.CanSee = append(group.CanSee, fid) - } - } else if group.Perms.ViewTopic { - group.CanSee = append(group.CanSee, fid) - } + cascadePermSetToGroup(forumPerms, group, fids) - if dev.SuperDebug { - log.Print("group.ID: ", group.ID) - log.Printf("forumPerm: %+v\n", forumPerm) - log.Print("group.CanSee: ", group.CanSee) - } - } if dev.SuperDebug { log.Printf("group.CanSee %+v\n", group.CanSee) log.Printf("group.Forums %+v\n", group.Forums) - log.Print("len(group.CanSee)", len(group.CanSee)) - log.Print("len(group.Forums)", len(group.Forums)) // This counts blank aka 0 + log.Print("len(group.CanSee): ", len(group.CanSee)) + log.Print("len(group.Forums): ", len(group.Forums)) // This counts blank aka 0 } } return nil } +func cascadePermSetToGroup(forumPerms map[int]map[int]ForumPerms, group *Group, fids []int) { + for _, fid := range fids { + if dev.SuperDebug { + log.Printf("Forum #%+v\n", fid) + } + forumPerm, ok := forumPerms[group.ID][fid] + if ok { + // Override group perms + //log.Print("Overriding permissions for forum #" + strconv.Itoa(fid)) + group.Forums = append(group.Forums, forumPerm) + } else { + // Inherit from Group + //log.Print("Inheriting from default for forum #" + strconv.Itoa(fid)) + forumPerm = BlankForumPerms + group.Forums = append(group.Forums, forumPerm) + } + if forumPerm.Overrides { + if forumPerm.ViewTopic { + group.CanSee = append(group.CanSee, fid) + } + } else if group.Perms.ViewTopic { + group.CanSee = append(group.CanSee, fid) + } + + if dev.SuperDebug { + log.Print("group.ID: ", group.ID) + log.Printf("forumPerm: %+v\n", forumPerm) + log.Print("group.CanSee: ", group.CanSee) + } + } +} + func forumPermsToGroupForumPreset(fperms ForumPerms) string { if !fperms.Overrides { return "default" diff --git a/plugin_guilds.go b/plugin_guilds.go index d76aff40..83d244f7 100644 --- a/plugin_guilds.go +++ b/plugin_guilds.go @@ -67,7 +67,7 @@ type GuildListPage struct { Title string CurrentUser User Header *HeaderVars - GroupList []*Guild + GuildList []*Guild } type GuildMemberListPage struct { diff --git a/pluginlangs.go b/pluginlangs.go index ac61c2fd..1b1eea12 100644 --- a/pluginlangs.go +++ b/pluginlangs.go @@ -18,6 +18,7 @@ type PluginMeta struct { Settings string Tag string + Skip bool // Skip this folder? Main string // The main file Hooks map[string]string // Hooks mapped to functions } @@ -62,6 +63,9 @@ func InitPluginLangs() error { if err != nil { return err } + if plugin.Skip { + continue + } if plugin.UName == "" { return errors.New("The UName field must not be blank on plugin '" + pluginItem + "'") diff --git a/templates/guilds_create_guild.html b/templates/guilds_create_guild.html index 00ae83ad..371a68d1 100644 --- a/templates/guilds_create_guild.html +++ b/templates/guilds_create_guild.html @@ -2,12 +2,12 @@
-

Create Group

+

Create Guild

-
+
@@ -25,7 +25,7 @@
-
+
diff --git a/templates/guilds_guild_list.html b/templates/guilds_guild_list.html index bba3acc5..aeb7342f 100644 --- a/templates/guilds_guild_list.html +++ b/templates/guilds_guild_list.html @@ -1,10 +1,10 @@ {{template "header.html" . }}
- {{range .GroupList}}
+ {{range .GuildList}}
{{.Name}}
{{.Desc}} @@ -15,7 +15,7 @@
- {{else}}
There aren't any visible groups.
{{end}} + {{else}}
There aren't any visible guilds.
{{end}}
{{template "footer.html" . }} diff --git a/templates/guilds_member_list.html b/templates/guilds_member_list.html index c15a928a..957f19b6 100644 --- a/templates/guilds_member_list.html +++ b/templates/guilds_member_list.html @@ -1,19 +1,20 @@ {{template "header.html" . }} -{{/** TODO: Move this into a CSS file **/}} -{{template "socialgroups_css.html" . }} +{{/** TODO: Move this into a per-theme CSS file **/}} +{{template "guilds_css.html" . }} +{{/** TODO: Add next / prev bits **/}} {{/** TODO: Port the page template functions to the template interpreter **/}} -{{if gt .Page 1}}{{end}} -{{if ne .LastPage .Page}} -{{end}} +{{if gt .Page 1}}{{end}} +{{if ne .LastPage .Page}} +{{end}} @@ -25,7 +26,7 @@ {{.User.Name}} - {{/** Use this for badges instead of rank? Both? Group Titles? **/}} + {{/** Use this for badges instead of rank? Both? Guild Titles? **/}}
{{.PostCount}} posts
diff --git a/templates/guilds_view_guild.html b/templates/guilds_view_guild.html index 8cb8e1ef..c91f4e26 100644 --- a/templates/guilds_view_guild.html +++ b/templates/guilds_view_guild.html @@ -3,18 +3,18 @@ {{template "socialgroups_css.html" . }} {{/** TODO: Port the page template functions to the template interpreter **/}} -{{if gt .Page 1}}{{end}} -{{if ne .LastPage .Page}} -{{end}} +{{if gt .Page 1}}{{end}} +{{if ne .LastPage .Page}} +{{end}} diff --git a/topic_store.go b/topic_store.go index 295b0299..ecb3df9e 100644 --- a/topic_store.go +++ b/topic_store.go @@ -9,7 +9,6 @@ package main import ( "database/sql" "errors" - "log" "strings" "sync" "sync/atomic" @@ -63,18 +62,18 @@ type MemoryTopicStore struct { } // NewMemoryTopicStore gives you a new instance of MemoryTopicStore -func NewMemoryTopicStore(capacity int) *MemoryTopicStore { +func NewMemoryTopicStore(capacity int) (*MemoryTopicStore, error) { getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } existsStmt, err := qgen.Builder.SimpleSelect("topics", "tid", "tid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } topicCountStmt, err := qgen.Builder.SimpleCount("topics", "", "") if err != nil { - log.Fatal(err) + return nil, err } return &MemoryTopicStore{ items: make(map[int]*Topic), @@ -82,7 +81,7 @@ func NewMemoryTopicStore(capacity int) *MemoryTopicStore { get: getStmt, exists: existsStmt, topicCount: topicCountStmt, - } + }, nil } func (mts *MemoryTopicStore) CacheGet(id int) (*Topic, error) { @@ -267,24 +266,24 @@ type SQLTopicStore struct { topicCount *sql.Stmt } -func NewSQLTopicStore() *SQLTopicStore { +func NewSQLTopicStore() (*SQLTopicStore, error) { getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } existsStmt, err := qgen.Builder.SimpleSelect("topics", "tid", "tid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } topicCountStmt, err := qgen.Builder.SimpleCount("topics", "", "") if err != nil { - log.Fatal(err) + return nil, err } return &SQLTopicStore{ get: getStmt, exists: existsStmt, topicCount: topicCountStmt, - } + }, nil } func (sts *SQLTopicStore) Get(id int) (*Topic, error) { diff --git a/user.go b/user.go index b01f7b6f..53ee3fa2 100644 --- a/user.go +++ b/user.go @@ -12,6 +12,7 @@ import ( "database/sql" "errors" "strconv" + "strings" "time" "./query_gen/lib" @@ -65,6 +66,19 @@ type Email struct { Token string } +func (user *User) Init() { + if user.Avatar != "" { + if user.Avatar[0] == '.' { + user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar + } + } else { + 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.initPerms() +} + func (user *User) Ban(duration time.Duration, issuedBy int) error { return user.ScheduleGroupUpdate(banGroup, issuedBy, duration) } diff --git a/user_store.go b/user_store.go index d4972371..3f3a69f3 100644 --- a/user_store.go +++ b/user_store.go @@ -5,7 +5,6 @@ import ( "errors" "log" "strconv" - "strings" "sync" "sync/atomic" @@ -56,32 +55,32 @@ type MemoryUserStore struct { } // NewMemoryUserStore gives you a new instance of MemoryUserStore -func NewMemoryUserStore(capacity int) *MemoryUserStore { +func NewMemoryUserStore(capacity int) (*MemoryUserStore, error) { getStmt, err := qgen.Builder.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } existsStmt, err := qgen.Builder.SimpleSelect("users", "uid", "uid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } // Add an admin version of register_stmt with more flexibility? // create_account_stmt, err = db.Prepare("INSERT INTO registerStmt, err := qgen.Builder.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()") if err != nil { - log.Fatal(err) + return nil, err } usernameExistsStmt, err := qgen.Builder.SimpleSelect("users", "name", "name = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } userCountStmt, err := qgen.Builder.SimpleCount("users", "", "") if err != nil { - log.Fatal(err) + return nil, err } return &MemoryUserStore{ @@ -92,7 +91,7 @@ func NewMemoryUserStore(capacity int) *MemoryUserStore { register: registerStmt, usernameExists: usernameExistsStmt, userCount: userCountStmt, - } + }, nil } func (mus *MemoryUserStore) CacheGet(id int) (*User, error) { @@ -124,17 +123,7 @@ func (mus *MemoryUserStore) 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) - // TODO: Add an init method to User rather than writing this same bit of code over and over - if user.Avatar != "" { - if user.Avatar[0] == '.' { - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar - } - } else { - user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1) - } - user.Link = buildProfileURL(nameToSlug(user.Name), id) - user.Tag = gstore.DirtyGet(user.Group).Tag - user.initPerms() + user.Init() if err == nil { mus.CacheSet(user) } @@ -203,18 +192,8 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error return nil, err } - // TODO: Add an init method to User rather than writing this same bit of code over and over // Initialise the user - if user.Avatar != "" { - if user.Avatar[0] == '.' { - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar - } - } else { - 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.initPerms() + user.Init() // Add it to the cache... _ = mus.CacheSet(user) @@ -255,17 +234,7 @@ func (mus *MemoryUserStore) 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) - // TODO: Add an init method to User rather than writing this same bit of code over and over - if user.Avatar != "" { - if user.Avatar[0] == '.' { - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar - } - } else { - user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1) - } - user.Link = buildProfileURL(nameToSlug(user.Name), id) - user.Tag = gstore.DirtyGet(user.Group).Tag - user.initPerms() + user.Init() return user, err } @@ -277,17 +246,7 @@ func (mus *MemoryUserStore) Reload(id int) error { return err } - // TODO: Add an init method to User rather than writing this same bit of code over and over - if user.Avatar != "" { - if user.Avatar[0] == '.' { - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar - } - } else { - user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1) - } - user.Link = buildProfileURL(nameToSlug(user.Name), id) - user.Tag = gstore.DirtyGet(user.Group).Tag - user.initPerms() + user.Init() _ = mus.CacheSet(user) return nil } @@ -425,32 +384,32 @@ type SQLUserStore struct { userCount *sql.Stmt } -func NewSQLUserStore() *SQLUserStore { +func NewSQLUserStore() (*SQLUserStore, error) { getStmt, err := qgen.Builder.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } existsStmt, err := qgen.Builder.SimpleSelect("users", "uid", "uid = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } // Add an admin version of register_stmt with more flexibility? // create_account_stmt, err = db.Prepare("INSERT INTO registerStmt, err := qgen.Builder.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()") if err != nil { - log.Fatal(err) + return nil, err } usernameExistsStmt, err := qgen.Builder.SimpleSelect("users", "name", "name = ?", "", "") if err != nil { - log.Fatal(err) + return nil, err } userCountStmt, err := qgen.Builder.SimpleCount("users", "", "") if err != nil { - log.Fatal(err) + return nil, err } return &SQLUserStore{ @@ -459,23 +418,14 @@ func NewSQLUserStore() *SQLUserStore { register: registerStmt, usernameExists: usernameExistsStmt, userCount: userCountStmt, - } + }, nil } 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) - if user.Avatar != "" { - if user.Avatar[0] == '.' { - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar - } - } else { - user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1) - } - user.Link = buildProfileURL(nameToSlug(user.Name), id) - user.Tag = gstore.DirtyGet(user.Group).Tag - user.initPerms() + user.Init() return user, err } @@ -508,16 +458,7 @@ func (mus *SQLUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) { } // Initialise the user - if user.Avatar != "" { - if user.Avatar[0] == '.' { - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar - } - } else { - 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.initPerms() + user.Init() // Add it to the list to be returned list[user.ID] = user @@ -530,16 +471,7 @@ 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) - if user.Avatar != "" { - if user.Avatar[0] == '.' { - user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar - } - } else { - user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1) - } - user.Link = buildProfileURL(nameToSlug(user.Name), id) - user.Tag = gstore.DirtyGet(user.Group).Tag - user.initPerms() + user.Init() return user, err }