Add CreateProfileReply and AutoEmbed group permissions.

Log profile reply deletions in the moderator log.
Split the global permissions in the UI to make them easier to manage.
Experiment with showing group ID in group edit header.
Avoid loading groups multiple times for the same profile reply.

Initialise disabled IP log points to empty string rather than 0.

Add CreateProfileReply perm phrase.
Add AutoEmbed perm phrase.
Add panel_group_mod_permissions phrase.
Add panel_logs_mod_action_profile_reply_delete phrase.
This commit is contained in:
Azareal 2020-02-04 21:47:03 +10:00
parent 2178efb8f0
commit 0c1d6f0516
26 changed files with 215 additions and 135 deletions

View File

@ -118,6 +118,8 @@ func seedTables(a qgen.Adapter) error {
UploadFiles UploadFiles
UploadAvatars UploadAvatars
UseConvos UseConvos
CreateProfileReply
AutoEmbed
// CreateConvo ? // CreateConvo ?
// CreateConvoReply ? // CreateConvoReply ?
@ -142,7 +144,7 @@ func seedTables(a qgen.Adapter) error {
} }
return string(jBytes) return string(jBytes)
} }
addGroup := func(name string, perms c.Perms, mod bool, admin bool, banned bool, tag string) { addGroup := func(name string, perms c.Perms, mod, admin, banned bool, tag string) {
mi, ai, bi := "0", "0", "0" mi, ai, bi := "0", "0", "0"
if mod { if mod {
mi = "1" mi = "1"
@ -161,10 +163,10 @@ func seedTables(a qgen.Adapter) error {
perms.EditGroupAdmin = false perms.EditGroupAdmin = false
addGroup("Administrator", perms, true, true, false, "Admin") addGroup("Administrator", perms, true, true, false, "Admin")
perms = c.Perms{BanUsers: true, ActivateUsers: true, EditUser: true, EditUserEmail: false, EditUserGroup: true, ViewIPs: true, UploadFiles: true, UploadAvatars: true, UseConvos: true, ViewTopic: true, LikeItem: true, CreateTopic: true, EditTopic: true, DeleteTopic: true, CreateReply: true, EditReply: true, DeleteReply: true, PinTopic: true, CloseTopic: true, MoveTopic: true} perms = c.Perms{BanUsers: true, ActivateUsers: true, EditUser: true, EditUserEmail: false, EditUserGroup: true, ViewIPs: true, UploadFiles: true, UploadAvatars: true, UseConvos: true, CreateProfileReply: true, AutoEmbed: true, ViewTopic: true, LikeItem: true, CreateTopic: true, EditTopic: true, DeleteTopic: true, CreateReply: true, EditReply: true, DeleteReply: true, PinTopic: true, CloseTopic: true, MoveTopic: true}
addGroup("Moderator", perms, true, false, false, "Mod") addGroup("Moderator", perms, true, false, false, "Mod")
perms = c.Perms{UploadFiles: true, UploadAvatars: true, UseConvos: true, ViewTopic: true, LikeItem: true, CreateTopic: true, CreateReply: true} perms = c.Perms{UploadFiles: true, UploadAvatars: true, UseConvos: true, CreateProfileReply: true, AutoEmbed: true, ViewTopic: true, LikeItem: true, CreateTopic: true, CreateReply: true}
addGroup("Member", perms, false, false, false, "") addGroup("Member", perms, false, false, false, "")
perms = c.Perms{ViewTopic: true} perms = c.Perms{ViewTopic: true}

View File

@ -594,6 +594,7 @@ type PanelEditGroupPermsPage struct {
Name string Name string
LocalPerms []NameLangToggle LocalPerms []NameLangToggle
GlobalPerms []NameLangToggle GlobalPerms []NameLangToggle
ModPerms []NameLangToggle
} }
type GroupPromotionExtend struct { type GroupPromotionExtend struct {

View File

@ -465,19 +465,25 @@ func (ps *ParseSettings) CopyPtr() *ParseSettings {
// TODO: Write a test for this // TODO: Write a test for this
// TODO: We need a lot more hooks here. E.g. To add custom media types and handlers. // TODO: We need a lot more hooks here. E.g. To add custom media types and handlers.
// TODO: Use templates to reduce the amount of boilerplate? // TODO: Use templates to reduce the amount of boilerplate?
func ParseMessage(msg string, sectionID int, sectionType string, settings *ParseSettings /*, user User*/) string { func ParseMessage(msg string, sectionID int, sectionType string, settings *ParseSettings, user *User) string {
if settings == nil { if settings == nil {
settings = DefaultParseSettings settings = DefaultParseSettings
} }
if user == nil {
user = &GuestUser
}
// TODO: Word boundary detection for these to avoid mangling code // TODO: Word boundary detection for these to avoid mangling code
msg = strings.Replace(msg, ":)", "😀", -1) rep := func(find, replace string) {
msg = strings.Replace(msg, ":(", "😞", -1) msg = strings.Replace(msg, find, replace, -1)
msg = strings.Replace(msg, ":D", "😃", -1) }
msg = strings.Replace(msg, ":P", "😛", -1) rep(":)", "😀")
msg = strings.Replace(msg, ":O", "😲", -1) rep(":(", "😞")
msg = strings.Replace(msg, ":p", "😛", -1) rep(":D", "😃")
msg = strings.Replace(msg, ":o", "😲", -1) rep(":P", "😛")
msg = strings.Replace(msg, ";)", "😉", -1) rep(":O", "😲")
rep(":p", "😛")
rep(":o", "😲")
rep(";)", "😉")
// Word filter list. E.g. Swear words and other things the admins don't like // Word filter list. E.g. Swear words and other things the admins don't like
wordFilters, err := WordFilters.GetAll() wordFilters, err := WordFilters.GetAll()

View File

@ -40,6 +40,8 @@ var GlobalPermList = []string{
"UploadFiles", "UploadFiles",
"UploadAvatars", "UploadAvatars",
"UseConvos", "UseConvos",
"CreateProfileReply",
"AutoEmbed",
} }
// Permission Structure: ActionComponent[Subcomponent]Flag // Permission Structure: ActionComponent[Subcomponent]Flag
@ -69,6 +71,8 @@ type Perms struct {
UploadFiles bool `json:",omitempty"` UploadFiles bool `json:",omitempty"`
UploadAvatars bool `json:",omitempty"` UploadAvatars bool `json:",omitempty"`
UseConvos bool `json:",omitempty"` UseConvos bool `json:",omitempty"`
CreateProfileReply bool `json:",omitempty"`
AutoEmbed bool `json:",omitempty"`
// Forum permissions // Forum permissions
ViewTopic bool `json:",omitempty"` ViewTopic bool `json:",omitempty"`
@ -125,6 +129,8 @@ func init() {
UploadFiles: true, UploadFiles: true,
UploadAvatars: true, UploadAvatars: true,
UseConvos: true, UseConvos: true,
CreateProfileReply: true,
AutoEmbed: true,
ViewTopic: true, ViewTopic: true,
LikeItem: true, LikeItem: true,

View File

@ -65,7 +65,7 @@ func (r *ProfileReply) Delete() error {
func (r *ProfileReply) SetBody(content string) error { func (r *ProfileReply) SetBody(content string) error {
content = PreparseMessage(html.UnescapeString(content)) content = PreparseMessage(html.UnescapeString(content))
_, err := profileReplyStmts.edit.Exec(content, ParseMessage(content, 0, "", nil), r.ID) _, err := profileReplyStmts.edit.Exec(content, ParseMessage(content, 0, "", nil, nil), r.ID)
return err return err
} }

View File

@ -50,9 +50,9 @@ func (s *SQLProfileReplyStore) Exists(id int) bool {
func (s *SQLProfileReplyStore) Create(profileID int, content string, createdBy int, ip string) (id int, err error) { func (s *SQLProfileReplyStore) Create(profileID int, content string, createdBy int, ip string) (id int, err error) {
if Config.DisablePostIP { if Config.DisablePostIP {
ip = "0" ip = ""
} }
res, err := s.create.Exec(profileID, content, ParseMessage(content, 0, "", nil), createdBy, ip) res, err := s.create.Exec(profileID, content, ParseMessage(content, 0, "", nil, nil), createdBy, ip)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -199,7 +199,7 @@ func (r *Reply) SetPost(content string) error {
return err return err
} }
content = PreparseMessage(html.UnescapeString(content)) content = PreparseMessage(html.UnescapeString(content))
parsedContent := ParseMessage(content, topic.ParentID, "forums", nil) parsedContent := ParseMessage(content, topic.ParentID, "forums", nil, nil)
_, err = replyStmts.edit.Exec(content, parsedContent, r.ID) // TODO: Sniff if this changed anything to see if we hit an existing poll _, err = replyStmts.edit.Exec(content, parsedContent, r.ID) // TODO: Sniff if this changed anything to see if we hit an existing poll
_ = Rstore.GetCache().Remove(r.ID) _ = Rstore.GetCache().Remove(r.ID)
return err return err

View File

@ -99,9 +99,9 @@ func (s *SQLReplyStore) Exists(id int) bool {
// TODO: Write a test for this // TODO: Write a test for this
func (s *SQLReplyStore) Create(t *Topic, content, ip string, uid int) (rid int, err error) { func (s *SQLReplyStore) Create(t *Topic, content, ip string, uid int) (rid int, err error) {
if Config.DisablePostIP { if Config.DisablePostIP {
ip = "0" ip = ""
} }
res, err := s.create.Exec(t.ID, content, ParseMessage(content, t.ParentID, "forums", nil), ip, WordCount(content), uid) res, err := s.create.Exec(t.ID, content, ParseMessage(content, t.ParentID, "forums", nil, nil), ip, WordCount(content), uid)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -46,9 +46,9 @@ func (s *DefaultReportStore) Create(title, content string, u *User, itemType str
ip := u.GetIP() ip := u.GetIP()
if Config.DisablePostIP { if Config.DisablePostIP {
ip = "0" ip = ""
} }
res, err := s.create.Exec(title, content, ParseMessage(content, 0, "", nil), ip, u.ID, u.ID, itemType+"_"+strconv.Itoa(itemID), ReportForumID) res, err := s.create.Exec(title, content, ParseMessage(content, 0, "", nil, nil), ip, u.ID, u.ID, itemType+"_"+strconv.Itoa(itemID), ReportForumID)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -400,7 +400,7 @@ func handleAttachments(stmt *sql.Stmt, id int) error {
} }
// TODO: Only load a row per createdBy, maybe with group by? // TODO: Only load a row per createdBy, maybe with group by?
func handleTopicReplies(umap map[int]struct{}, uid int, tid int) error { func handleTopicReplies(umap map[int]struct{}, uid, tid int) error {
rows, err := userStmts.getRepliesOfTopic.Query(uid, tid) rows, err := userStmts.getRepliesOfTopic.Query(uid, tid)
if err != nil { if err != nil {
return err return err
@ -505,7 +505,7 @@ func (t *Topic) Update(name, content string) error {
} }
content = PreparseMessage(html.UnescapeString(content)) content = PreparseMessage(html.UnescapeString(content))
parsedContent := ParseMessage(content, t.ParentID, "forums", nil) parsedContent := ParseMessage(content, t.ParentID, "forums", nil, nil)
_, err := topicStmts.edit.Exec(name, content, parsedContent, t.ID) _, err := topicStmts.edit.Exec(name, content, parsedContent, t.ID)
t.cacheRemove() t.cacheRemove()
return err return err
@ -518,9 +518,9 @@ func (t *Topic) SetPoll(pollID int) error {
} }
// TODO: Have this go through the ReplyStore? // TODO: Have this go through the ReplyStore?
func (t *Topic) CreateActionReply(action string, ip string, uid int) (err error) { func (t *Topic) CreateActionReply(action, ip string, uid int) (err error) {
if Config.DisablePostIP { if Config.DisablePostIP {
ip = "0" ip = ""
} }
res, err := topicStmts.createAction.Exec(t.ID, action, ip, uid) res, err := topicStmts.createAction.Exec(t.ID, action, ip, uid)
if err != nil { if err != nil {
@ -566,13 +566,13 @@ var unlockai = "&#x1F513"
var stickai = "&#x1F4CC" var stickai = "&#x1F4CC"
var unstickai = "&#x1F4CC" + aipost var unstickai = "&#x1F4CC" + aipost
func (ru *ReplyUser) Init() error { func (ru *ReplyUser) Init() (group *Group, err error) {
ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy) ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy)
ru.ContentLines = strings.Count(ru.Content, "\n") ru.ContentLines = strings.Count(ru.Content, "\n")
postGroup, err := Groups.Get(ru.Group) postGroup, err := Groups.Get(ru.Group)
if err != nil { if err != nil {
return err return nil, err
} }
if postGroup.IsMod { if postGroup.IsMod {
ru.ClassName = Config.StaffCSS ru.ClassName = Config.StaffCSS
@ -581,9 +581,6 @@ func (ru *ReplyUser) Init() error {
// TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the c.UserStore initialise this? // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the c.UserStore initialise this?
ru.Avatar, ru.MicroAvatar = BuildAvatar(ru.CreatedBy, ru.Avatar) ru.Avatar, ru.MicroAvatar = BuildAvatar(ru.CreatedBy, ru.Avatar)
if ru.Tag == "" {
ru.Tag = postGroup.Tag
}
// We really shouldn't have inline HTML, we should do something about this... // We really shouldn't have inline HTML, we should do something about this...
if ru.ActionType != "" { if ru.ActionType != "" {
@ -604,18 +601,18 @@ func (ru *ReplyUser) Init() error {
forum, err := Forums.Get(fid) forum, err := Forums.Get(fid)
if err == nil { if err == nil {
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_move_dest", forum.Link, forum.Name, ru.UserLink, ru.CreatedByName) ru.ActionType = p.GetTmplPhrasef("topic.action_topic_move_dest", forum.Link, forum.Name, ru.UserLink, ru.CreatedByName)
return nil return postGroup, nil
} }
} }
default: default:
// TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry? // TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry?
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_default", ru.ActionType) ru.ActionType = p.GetTmplPhrasef("topic.action_topic_default", ru.ActionType)
return nil return postGroup, nil
} }
ru.ActionType = p.GetTmplPhrasef("topic.action_topic_"+action, ru.UserLink, ru.CreatedByName) ru.ActionType = p.GetTmplPhrasef("topic.action_topic_"+action, ru.UserLink, ru.CreatedByName)
} }
return nil return postGroup, nil
} }
// TODO: Factor TopicUser into a *Topic and *User, as this starting to become overly complicated x.x // TODO: Factor TopicUser into a *Topic and *User, as this starting to become overly complicated x.x
@ -648,12 +645,21 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser,
hTbl := GetHookTable() hTbl := GetHookTable()
rf := func(r *ReplyUser) error { rf := func(r *ReplyUser) error {
//log.Printf("before r: %+v\n", r) //log.Printf("before r: %+v\n", r)
err := r.Init() group, err := r.Init()
if err != nil { if err != nil {
return err return err
} }
//log.Printf("after r: %+v\n", r) //log.Printf("after r: %+v\n", r)
r.ContentHtml = ParseMessage(r.Content, t.ParentID, "forums", user.ParseSettings)
var parseSettings *ParseSettings
if !group.Perms.AutoEmbed && (user.ParseSettings == nil || !user.ParseSettings.NoEmbed) {
parseSettings = DefaultParseSettings.CopyPtr()
parseSettings.NoEmbed = true
} else {
parseSettings = user.ParseSettings
}
r.ContentHtml = ParseMessage(r.Content, t.ParentID, "forums", parseSettings, user)
// TODO: Do this more efficiently by avoiding the allocations entirely in ParseMessage, if there's nothing to do. // TODO: Do this more efficiently by avoiding the allocations entirely in ParseMessage, if there's nothing to do.
if r.ContentHtml == r.Content { if r.ContentHtml == r.Content {
r.ContentHtml = r.Content r.ContentHtml = r.Content

View File

@ -222,14 +222,14 @@ func (s *DefaultTopicStore) Create(fid int, name, content string, uid int, ip st
return 0, ErrLongTitle return 0, ErrLongTitle
} }
parsedContent := strings.TrimSpace(ParseMessage(content, fid, "forums", nil)) parsedContent := strings.TrimSpace(ParseMessage(content, fid, "forums", nil, nil))
if parsedContent == "" { if parsedContent == "" {
return 0, ErrNoBody return 0, ErrNoBody
} }
// TODO: Move this statement into the topic store // TODO: Move this statement into the topic store
if Config.DisablePostIP { if Config.DisablePostIP {
ip = "0" ip = ""
} }
res, err := s.create.Exec(fid, name, content, parsedContent, uid, ip, WordCount(content), uid) res, err := s.create.Exec(fid, name, content, parsedContent, uid, ip, WordCount(content), uid)
if err != nil { if err != nil {

View File

@ -936,7 +936,7 @@ func BenchmarkParserSerial(b *testing.B) {
f := func(name, msg string) func(b *testing.B) { f := func(name, msg string) func(b *testing.B) {
return func(b *testing.B) { return func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = c.ParseMessage(msg, 0, "", nil) _ = c.ParseMessage(msg, 0, "", nil, nil)
} }
} }
} }

View File

@ -31,6 +31,8 @@
"UploadFiles": "Can upload files", "UploadFiles": "Can upload files",
"UploadAvatars": "Can upload avatars", "UploadAvatars": "Can upload avatars",
"UseConvos":"Can use conversations", "UseConvos":"Can use conversations",
"CreateProfileReply": "Can create profile replies",
"AutoEmbed":"Automatically embed media they post",
"ViewTopic": "Can view topics", "ViewTopic": "Can view topics",
"LikeItem": "Can like items", "LikeItem": "Can like items",
@ -919,6 +921,7 @@
"panel_group_tag_placeholder":"VIP", "panel_group_tag_placeholder":"VIP",
"panel_group_update_button":"Update Group", "panel_group_update_button":"Update Group",
"panel_group_extended_permissions":"Extended Permissions", "panel_group_extended_permissions":"Extended Permissions",
"panel_group_mod_permissions":"Moderator Permissions",
"panel_group_promotions_level_prefix":"level ", "panel_group_promotions_level_prefix":"level ",
"panel_group_promotions_posts_prefix":"posts ", "panel_group_promotions_posts_prefix":"posts ",
@ -1029,6 +1032,7 @@
"panel_logs_mod_action_topic_move_dest":"<a href='%s'>%s</a> was moved to <a href='%s'>%s</a> by <a href='%s'>%s</a>", "panel_logs_mod_action_topic_move_dest":"<a href='%s'>%s</a> was moved to <a href='%s'>%s</a> by <a href='%s'>%s</a>",
"panel_logs_mod_action_topic_unknown":"Unknown action '%s' on elementType '%s' by <a href='%s'>%s</a>", "panel_logs_mod_action_topic_unknown":"Unknown action '%s' on elementType '%s' by <a href='%s'>%s</a>",
"panel_logs_mod_action_reply_delete":"A reply in <a href='%s'>%s</a> was deleted by <a href='%s'>%s</a>", "panel_logs_mod_action_reply_delete":"A reply in <a href='%s'>%s</a> was deleted by <a href='%s'>%s</a>",
"panel_logs_mod_action_profile_reply_delete":"A reply on <a href='%s'>%s</a>'s profile was deleted by <a href='%s'>%s</a>",
"panel_logs_mod_action_user_ban":"<a href='%s'>%s</a> was banned by <a href='%s'>%s</a>", "panel_logs_mod_action_user_ban":"<a href='%s'>%s</a> was banned by <a href='%s'>%s</a>",
"panel_logs_mod_action_user_unban":"<a href='%s'>%s</a> was unbanned by <a href='%s'>%s</a>", "panel_logs_mod_action_user_unban":"<a href='%s'>%s</a> was unbanned by <a href='%s'>%s</a>",
"panel_logs_mod_action_user_delete-posts":"<a href='%s'>%s</a> had their posts purged by <a href='%s'>%s</a>", "panel_logs_mod_action_user_delete-posts":"<a href='%s'>%s</a> had their posts purged by <a href='%s'>%s</a>",

View File

@ -59,17 +59,17 @@ func TestUserStore(t *testing.T) {
func userStoreTest(t *testing.T, newUserID int) { func userStoreTest(t *testing.T, newUserID int) {
ucache := c.Users.GetCache() ucache := c.Users.GetCache()
// Go doesn't have short-circuiting, so this'll allow us to do one liner tests // Go doesn't have short-circuiting, so this'll allow us to do one liner tests
isCacheLengthZero := func(ucache c.UserCache) bool { isCacheLengthZero := func(uc c.UserCache) bool {
if ucache == nil { if uc == nil {
return true return true
} }
return ucache.Length() == 0 return uc.Length() == 0
} }
cacheLength := func(ucache c.UserCache) int { cacheLength := func(uc c.UserCache) int {
if ucache == nil { if uc == nil {
return 0 return 0
} }
return ucache.Length() return uc.Length()
} }
expect(t, isCacheLengthZero(ucache), fmt.Sprintf("The initial ucache length should be zero, not %d", cacheLength(ucache))) expect(t, isCacheLengthZero(ucache), fmt.Sprintf("The initial ucache length should be zero, not %d", cacheLength(ucache)))
@ -84,7 +84,7 @@ func userStoreTest(t *testing.T, newUserID int) {
user, err := c.Users.Get(1) user, err := c.Users.Get(1)
recordMustExist(t, err, "Couldn't find UID #1") recordMustExist(t, err, "Couldn't find UID #1")
expectW := func(cond bool, expec bool, prefix string, suffix string) { expectW := func(cond, expec bool, prefix, suffix string) {
midfix := "should not be" midfix := "should not be"
if expec { if expec {
midfix = "should be" midfix = "should be"
@ -93,7 +93,7 @@ func userStoreTest(t *testing.T, newUserID int) {
} }
// TODO: Add email checks too? Do them separately? // TODO: Add email checks too? Do them separately?
expectUser := func(u *c.User, uid int, name string, group int, super bool, admin bool, mod bool, banned bool) { expectUser := func(u *c.User, uid int, name string, group int, super, admin, mod, banned bool) {
expect(t, u.ID == uid, fmt.Sprintf("u.ID should be %d. Got '%d' instead.", uid, u.ID)) expect(t, u.ID == uid, fmt.Sprintf("u.ID should be %d. Got '%d' instead.", uid, u.ID))
expect(t, u.Name == name, fmt.Sprintf("u.Name should be '%s', not '%s'", name, u.Name)) expect(t, u.Name == name, fmt.Sprintf("u.Name should be '%s', not '%s'", name, u.Name))
expectW(u.Group == group, true, u.Name, "in group"+strconv.Itoa(group)) expectW(u.Group == group, true, u.Name, "in group"+strconv.Itoa(group))
@ -258,7 +258,7 @@ func userStoreTest(t *testing.T, newUserID int) {
dummyRequest2 := httptest.NewRequest("", "/forum/"+strconv.Itoa(generalForumID), bytesBuffer) dummyRequest2 := httptest.NewRequest("", "/forum/"+strconv.Itoa(generalForumID), bytesBuffer)
var user2 *c.User var user2 *c.User
changeGroupTest := func(oldGroup int, newGroup int) { changeGroupTest := func(oldGroup, newGroup int) {
err = user.ChangeGroup(newGroup) err = user.ChangeGroup(newGroup)
expectNilErr(t, err) expectNilErr(t, err)
// ! I don't think ChangeGroup should be changing the value of user... Investigate this. // ! I don't think ChangeGroup should be changing the value of user... Investigate this.
@ -270,7 +270,7 @@ func userStoreTest(t *testing.T, newUserID int) {
*user2 = *user *user2 = *user
} }
changeGroupTest2 := func(rank string, firstShouldBe bool, secondShouldBe bool) { changeGroupTest2 := func(rank string, firstShouldBe, secondShouldBe bool) {
head, err := c.UserCheck(dummyResponseRecorder, dummyRequest1, user) head, err := c.UserCheck(dummyResponseRecorder, dummyRequest1, user)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -364,7 +364,7 @@ func expectNilErr(t *testing.T, item error) {
} }
} }
func expectIntToBeX(t *testing.T, item int, expect int, errmsg string) { func expectIntToBeX(t *testing.T, item, expect int, errmsg string) {
if item != expect { if item != expect {
debug.PrintStack() debug.PrintStack()
t.Fatalf(errmsg, item) t.Fatalf(errmsg, item)
@ -452,14 +452,14 @@ func TestTopicStore(t *testing.T) {
c.Config.DisablePostIP = false c.Config.DisablePostIP = false
topicStoreTest(t, 2, "::1") topicStoreTest(t, 2, "::1")
c.Config.DisablePostIP = true c.Config.DisablePostIP = true
topicStoreTest(t, 3, "0") topicStoreTest(t, 3, "")
c.Topics, err = c.NewDefaultTopicStore(nil) c.Topics, err = c.NewDefaultTopicStore(nil)
expectNilErr(t, err) expectNilErr(t, err)
c.Config.DisablePostIP = false c.Config.DisablePostIP = false
topicStoreTest(t, 4, "::1") topicStoreTest(t, 4, "::1")
c.Config.DisablePostIP = true c.Config.DisablePostIP = true
topicStoreTest(t, 5, "0") topicStoreTest(t, 5, "")
} }
func topicStoreTest(t *testing.T, newID int, ip string) { func topicStoreTest(t *testing.T, newID int, ip string) {
var topic *c.Topic var topic *c.Topic
@ -914,20 +914,20 @@ func TestReplyStore(t *testing.T) {
c.Config.DisablePostIP = false c.Config.DisablePostIP = false
testReplyStore(t, 2, 1, "::1") testReplyStore(t, 2, 1, "::1")
c.Config.DisablePostIP = true c.Config.DisablePostIP = true
testReplyStore(t, 5, 3, "0") testReplyStore(t, 5, 3, "")
} }
func testReplyStore(t *testing.T, newID, newPostCount int, ip string) { func testReplyStore(t *testing.T, newID, newPostCount int, ip string) {
replyTest2 := func(reply *c.Reply, err error, rid int, parentID int, createdBy int, content string, ip string) { replyTest2 := func(r *c.Reply, err error, rid, parentID, createdBy int, content, ip string) {
expectNilErr(t, err) expectNilErr(t, err)
expect(t, reply.ID == rid, fmt.Sprintf("RID #%d has the wrong ID. It should be %d not %d", rid, rid, reply.ID)) expect(t, r.ID == rid, fmt.Sprintf("RID #%d has the wrong ID. It should be %d not %d", rid, rid, r.ID))
expect(t, reply.ParentID == parentID, fmt.Sprintf("The parent topic of RID #%d should be %d not %d", rid, parentID, reply.ParentID)) expect(t, r.ParentID == parentID, fmt.Sprintf("The parent topic of RID #%d should be %d not %d", rid, parentID, r.ParentID))
expect(t, reply.CreatedBy == createdBy, fmt.Sprintf("The creator of RID #%d should be %d not %d", rid, createdBy, reply.CreatedBy)) expect(t, r.CreatedBy == createdBy, fmt.Sprintf("The creator of RID #%d should be %d not %d", rid, createdBy, r.CreatedBy))
expect(t, reply.Content == content, fmt.Sprintf("The contents of RID #%d should be '%s' not %s", rid, content, reply.Content)) expect(t, r.Content == content, fmt.Sprintf("The contents of RID #%d should be '%s' not %s", rid, content, r.Content))
expect(t, reply.IP == ip, fmt.Sprintf("The IP of RID#%d should be '%s' not %s", rid, ip, reply.IP)) expect(t, r.IP == ip, fmt.Sprintf("The IP of RID#%d should be '%s' not %s", rid, ip, r.IP))
} }
replyTest := func(rid int, parentID int, createdBy int, content string, ip string) { replyTest := func(rid, parentID, createdBy int, content, ip string) {
reply, err := c.Rstore.Get(rid) reply, err := c.Rstore.Get(rid)
replyTest2(reply, err, rid, parentID, createdBy, content, ip) replyTest2(reply, err, rid, parentID, createdBy, content, ip)
reply, err = c.Rstore.GetCache().Get(rid) reply, err = c.Rstore.GetCache().Get(rid)
@ -1015,7 +1015,7 @@ func TestProfileReplyStore(t *testing.T) {
c.Config.DisablePostIP = false c.Config.DisablePostIP = false
testProfileReplyStore(t, 1, "::1") testProfileReplyStore(t, 1, "::1")
c.Config.DisablePostIP = true c.Config.DisablePostIP = true
testProfileReplyStore(t, 2, "0") testProfileReplyStore(t, 2, "")
} }
func testProfileReplyStore(t *testing.T, newID int, ip string) { func testProfileReplyStore(t *testing.T, newID int, ip string) {
// ? - Commented this one out as strong constraints like this put an unreasonable load on the database, we only want errors if a delete which should succeed fails // ? - Commented this one out as strong constraints like this put an unreasonable load on the database, we only want errors if a delete which should succeed fails
@ -1466,22 +1466,22 @@ func TestWidgets(t *testing.T) {
expectNilErr(t, err) expectNilErr(t, err)
expect(t, wid == 1, "wid should be 1") expect(t, wid == 1, "wid should be 1")
wtest := func(w, w2 *c.Widget) {
expect(t, w.Position == w2.Position, "wrong position")
expect(t, w.Side == w2.Side, "wrong side")
expect(t, w.Type == w2.Type, "wrong type")
expect(t, w2.Enabled, "not enabled")
expect(t, w.Location == w2.Location, "wrong location")
}
// TODO: Do a test for the widget body // TODO: Do a test for the widget body
widget2, err := c.Widgets.Get(1) widget2, err := c.Widgets.Get(1)
expectNilErr(t, err) expectNilErr(t, err)
expect(t, widget2.Position == widget.Position, "wrong position") wtest(widget, widget2)
expect(t, widget2.Side == widget.Side, "wrong side")
expect(t, widget2.Type == widget.Type, "wrong type")
expect(t, widget2.Enabled, "not enabled")
expect(t, widget2.Location == widget.Location, "wrong location")
widgets = c.Docks.RightSidebar.Items widgets = c.Docks.RightSidebar.Items
expect(t, len(widgets) == 1, fmt.Sprintf("RightSidebar should have 1 item, not %d", len(widgets))) expect(t, len(widgets) == 1, fmt.Sprintf("RightSidebar should have 1 item, not %d", len(widgets)))
expect(t, widgets[0].Position == widget.Position, "wrong position") wtest(widget, widgets[0])
expect(t, widgets[0].Side == widget.Side, "wrong side")
expect(t, widgets[0].Type == widget.Type, "wrong type")
expect(t, widgets[0].Enabled, "not enabled")
expect(t, widgets[0].Location == widget.Location, "wrong location")
widget2.Enabled = false widget2.Enabled = false
ewidget = &c.WidgetEdit{widget2, map[string]string{"Name": "Test", "Text": "Testing"}} ewidget = &c.WidgetEdit{widget2, map[string]string{"Name": "Test", "Text": "Testing"}}
@ -1493,7 +1493,7 @@ func TestWidgets(t *testing.T) {
expect(t, widget2.Position == widget.Position, "wrong position") expect(t, widget2.Position == widget.Position, "wrong position")
expect(t, widget2.Side == widget.Side, "wrong side") expect(t, widget2.Side == widget.Side, "wrong side")
expect(t, widget2.Type == widget.Type, "wrong type") expect(t, widget2.Type == widget.Type, "wrong type")
expect(t, !widget2.Enabled, "not enabled") expect(t, !widget2.Enabled, "should not be enabled")
expect(t, widget2.Location == widget.Location, "wrong location") expect(t, widget2.Location == widget.Location, "wrong location")
widgets = c.Docks.RightSidebar.Items widgets = c.Docks.RightSidebar.Items
@ -1501,7 +1501,7 @@ func TestWidgets(t *testing.T) {
expect(t, widgets[0].Position == widget.Position, "wrong position") expect(t, widgets[0].Position == widget.Position, "wrong position")
expect(t, widgets[0].Side == widget.Side, "wrong side") expect(t, widgets[0].Side == widget.Side, "wrong side")
expect(t, widgets[0].Type == widget.Type, "wrong type") expect(t, widgets[0].Type == widget.Type, "wrong type")
expect(t, !widgets[0].Enabled, "not enabled") expect(t, !widgets[0].Enabled, "should not be enabled")
expect(t, widgets[0].Location == widget.Location, "wrong location") expect(t, widgets[0].Location == widget.Location, "wrong location")
err = widget2.Delete() err = widget2.Delete()

View File

@ -300,7 +300,7 @@ func TestParser(t *testing.T) {
// TODO: Fix this hack and make the results a bit more reproducible, push the tests further in the process. // TODO: Fix this hack and make the results a bit more reproducible, push the tests further in the process.
for _, item := range l.Items { for _, item := range l.Items {
if res := c.ParseMessage(item.Msg, 1, "forums", nil); res != item.Expects { if res := c.ParseMessage(item.Msg, 1, "forums", nil, nil); res != item.Expects {
if item.Name != "" { if item.Name != "" {
t.Error("Name: ", item.Name) t.Error("Name: ", item.Name)
} }
@ -321,7 +321,7 @@ func TestParser(t *testing.T) {
l.Add("//"+c.Site.URL+"\n", "<a href='https://"+c.Site.URL+"'>"+c.Site.URL+"</a><br>") l.Add("//"+c.Site.URL+"\n", "<a href='https://"+c.Site.URL+"'>"+c.Site.URL+"</a><br>")
l.Add("//"+c.Site.URL+"\n//"+c.Site.URL, "<a href='https://"+c.Site.URL+"'>"+c.Site.URL+"</a><br><a href='https://"+c.Site.URL+"'>"+c.Site.URL+"</a>") l.Add("//"+c.Site.URL+"\n//"+c.Site.URL, "<a href='https://"+c.Site.URL+"'>"+c.Site.URL+"</a><br><a href='https://"+c.Site.URL+"'>"+c.Site.URL+"</a>")
for _, item := range l.Items { for _, item := range l.Items {
if res := c.ParseMessage(item.Msg, 1, "forums", nil); res != item.Expects { if res := c.ParseMessage(item.Msg, 1, "forums", nil, nil); res != item.Expects {
if item.Name != "" { if item.Name != "" {
t.Error("Name: ", item.Name) t.Error("Name: ", item.Name)
} }
@ -345,7 +345,7 @@ func TestParser(t *testing.T) {
} }
c.WriteURL(sb, c.BuildTopicURL("", tid), "#nnid-"+strconv.Itoa(tid)) c.WriteURL(sb, c.BuildTopicURL("", tid), "#nnid-"+strconv.Itoa(tid))
}) })
res := c.ParseMessage("#nnid-1", 1, "forums", nil) res := c.ParseMessage("#nnid-1", 1, "forums", nil, nil)
expect := "<a href='/topic/1'>#nnid-1</a>" expect := "<a href='/topic/1'>#nnid-1</a>"
if res != expect { if res != expect {
t.Error("Bad output:", "'"+res+"'") t.Error("Bad output:", "'"+res+"'")
@ -363,7 +363,7 @@ func TestParser(t *testing.T) {
} }
c.WriteURL(sb, c.BuildTopicURL("", tid), "#longidnameneedtooverflowhack-"+strconv.Itoa(tid)) c.WriteURL(sb, c.BuildTopicURL("", tid), "#longidnameneedtooverflowhack-"+strconv.Itoa(tid))
}) })
res = c.ParseMessage("#longidnameneedtooverflowhack-1", 1, "forums", nil) res = c.ParseMessage("#longidnameneedtooverflowhack-1", 1, "forums", nil,nil)
expect = "<a href='/topic/1'>#longidnameneedtooverflowhack-1</a>" expect = "<a href='/topic/1'>#longidnameneedtooverflowhack-1</a>"
if res != expect { if res != expect {
t.Error("Bad output:", "'"+res+"'") t.Error("Bad output:", "'"+res+"'")

View File

@ -336,6 +336,17 @@ func GroupsEditPerms(w http.ResponseWriter, r *http.Request, user c.User, sgid s
globalPerms = append(globalPerms, c.NameLangToggle{permStr, p.GetPermPhrase(permStr), perm}) globalPerms = append(globalPerms, c.NameLangToggle{permStr, p.GetPermPhrase(permStr), perm})
} }
addPerm("UploadFiles", g.Perms.UploadFiles)
addPerm("UploadAvatars", g.Perms.UploadAvatars)
addPerm("UseConvos", g.Perms.UseConvos)
addPerm("CreateProfileReply", g.Perms.CreateProfileReply)
addPerm("AutoEmbed", g.Perms.AutoEmbed)
var modPerms []c.NameLangToggle
addPerm = func(permStr string, perm bool) {
modPerms = append(modPerms, c.NameLangToggle{permStr, p.GetPermPhrase(permStr), perm})
}
addPerm("BanUsers", g.Perms.BanUsers) addPerm("BanUsers", g.Perms.BanUsers)
addPerm("ActivateUsers", g.Perms.ActivateUsers) addPerm("ActivateUsers", g.Perms.ActivateUsers)
addPerm("EditUser", g.Perms.EditUser) addPerm("EditUser", g.Perms.EditUser)
@ -355,11 +366,8 @@ func GroupsEditPerms(w http.ResponseWriter, r *http.Request, user c.User, sgid s
addPerm("ManagePlugins", g.Perms.ManagePlugins) addPerm("ManagePlugins", g.Perms.ManagePlugins)
addPerm("ViewAdminLogs", g.Perms.ViewAdminLogs) addPerm("ViewAdminLogs", g.Perms.ViewAdminLogs)
addPerm("ViewIPs", g.Perms.ViewIPs) addPerm("ViewIPs", g.Perms.ViewIPs)
addPerm("UploadFiles", g.Perms.UploadFiles)
addPerm("UploadAvatars", g.Perms.UploadAvatars)
addPerm("UseConvos", g.Perms.UseConvos)
pi := c.PanelEditGroupPermsPage{basePage, g.ID, g.Name, localPerms, globalPerms} pi := c.PanelEditGroupPermsPage{basePage, g.ID, g.Name, localPerms, globalPerms, modPerms}
return renderTemplate("panel_group_edit_perms", w, r, basePage.Header, pi) return renderTemplate("panel_group_edit_perms", w, r, basePage.Header, pi)
} }

View File

@ -39,21 +39,21 @@ func LogsRegs(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
// TODO: Log errors when something really screwy is going on? // TODO: Log errors when something really screwy is going on?
// TODO: Base the slugs on the localised usernames? // TODO: Base the slugs on the localised usernames?
func handleUnknownUser(user *c.User, err error) *c.User { func handleUnknownUser(u *c.User, err error) *c.User {
if err != nil { if err != nil {
return &c.User{Name: p.GetTmplPhrase("user_unknown"), Link: c.BuildProfileURL("unknown", 0)} return &c.User{Name: p.GetTmplPhrase("user_unknown"), Link: c.BuildProfileURL("unknown", 0)}
} }
return user return u
} }
func handleUnknownTopic(topic *c.Topic, err error) *c.Topic { func handleUnknownTopic(t *c.Topic, err error) *c.Topic {
if err != nil { if err != nil {
return &c.Topic{Title: p.GetTmplPhrase("topic_unknown"), Link: c.BuildTopicURL("unknown", 0)} return &c.Topic{Title: p.GetTmplPhrase("topic_unknown"), Link: c.BuildTopicURL("unknown", 0)}
} }
return topic return t
} }
// TODO: Move the log building logic into /common/ and it's own abstraction // TODO: Move the log building logic into /common/ and it's own abstraction
func topicElementTypeAction(action string, elementType string, elementID int, actor *c.User, topic *c.Topic) (out string) { func topicElementTypeAction(action, elementType string, elementID int, actor *c.User, topic *c.Topic) (out string) {
if action == "delete" { if action == "delete" {
return p.GetTmplPhrasef("panel_logs_mod_action_topic_delete", elementID, actor.Link, actor.Name) return p.GetTmplPhrasef("panel_logs_mod_action_topic_delete", elementID, actor.Link, actor.Name)
} }
@ -80,7 +80,7 @@ func topicElementTypeAction(action string, elementType string, elementID int, ac
return fmt.Sprintf(out, topic.Link, topic.Title, actor.Link, actor.Name) return fmt.Sprintf(out, topic.Link, topic.Title, actor.Link, actor.Name)
} }
func modlogsElementType(action string, elementType string, elementID int, actor *c.User) (out string) { func modlogsElementType(action, elementType string, elementID int, actor *c.User) (out string) {
switch elementType { switch elementType {
case "topic": case "topic":
topic := handleUnknownTopic(c.Topics.Get(elementID)) topic := handleUnknownTopic(c.Topics.Get(elementID))
@ -93,6 +93,18 @@ func modlogsElementType(action string, elementType string, elementID int, actor
topic := handleUnknownTopic(c.TopicByReplyID(elementID)) topic := handleUnknownTopic(c.TopicByReplyID(elementID))
out = p.GetTmplPhrasef("panel_logs_mod_action_reply_delete", topic.Link, topic.Title, actor.Link, actor.Name) out = p.GetTmplPhrasef("panel_logs_mod_action_reply_delete", topic.Link, topic.Title, actor.Link, actor.Name)
} }
case "profile-reply":
if action == "delete" {
// TODO: Optimise this
var profile *c.User
profileReply, err := c.Prstore.Get(elementID)
if err != nil {
profile = &c.User{Name: p.GetTmplPhrase("user_unknown"), Link: c.BuildProfileURL("unknown", 0)}
} else {
profile = handleUnknownUser(c.Users.Get(profileReply.ParentID))
}
out = p.GetTmplPhrasef("panel_logs_mod_action_profile_reply_delete", profile.Link, profile.Name, actor.Link, actor.Name)
}
} }
if out == "" { if out == "" {
out = p.GetTmplPhrasef("panel_logs_mod_action_unknown", action, elementType, actor.Link, actor.Name) out = p.GetTmplPhrasef("panel_logs_mod_action_unknown", action, elementType, actor.Link, actor.Name)
@ -100,7 +112,7 @@ func modlogsElementType(action string, elementType string, elementID int, actor
return out return out
} }
func adminlogsElementType(action string, elementType string, elementID int, actor *c.User, extra string) (out string) { func adminlogsElementType(action, elementType string, elementID int, actor *c.User, extra string) (out string) {
switch elementType { switch elementType {
// TODO: Record more detail for this, e.g. which field/s was changed // TODO: Record more detail for this, e.g. which field/s was changed
case "user": case "user":

View File

@ -28,11 +28,10 @@ func init() {
// TODO: Remove the View part of the name? // TODO: Remove the View part of the name?
func ViewProfile(w http.ResponseWriter, r *http.Request, user c.User, header *c.Header) c.RouteError { func ViewProfile(w http.ResponseWriter, r *http.Request, user c.User, header *c.Header) c.RouteError {
var err error var reCreatedAt time.Time
var replyCreatedAt time.Time var reContent, reCreatedByName, reAvatar string
var replyContent, replyCreatedByName, replyAvatar string var rid, reCreatedBy, reLastEdit, reLastEditBy, reGroup int
var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyGroup int var reList []*c.ReplyUser
var replyList []*c.ReplyUser
// TODO: Do a 301 if it's the wrong username? Do a canonical too? // TODO: Do a 301 if it's the wrong username? Do a canonical too?
_, pid, err := ParseSEOURL(r.URL.Path[len("/user/"):]) _, pid, err := ParseSEOURL(r.URL.Path[len("/user/"):])
@ -71,28 +70,24 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user c.User, header *c.
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
err := rows.Scan(&rid, &replyContent, &replyCreatedBy, &replyCreatedAt, &replyLastEdit, &replyLastEditBy, &replyAvatar, &replyCreatedByName, &replyGroup) err := rows.Scan(&rid, &reContent, &reCreatedBy, &reCreatedAt, &reLastEdit, &reLastEditBy, &reAvatar, &reCreatedByName, &reGroup)
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
replyLiked := false reLiked := false
replyLikeCount := 0 reLikeCount := 0
ru := &c.ReplyUser{Reply: c.Reply{rid, puser.ID, replyContent, replyCreatedBy, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, 0, "", replyLiked, replyLikeCount, 0, ""}, ContentHtml: c.ParseMessage(replyContent, 0, "", user.ParseSettings), CreatedByName: replyCreatedByName, Avatar: replyAvatar, Level: 0} ru := &c.ReplyUser{Reply: c.Reply{rid, puser.ID, reContent, reCreatedBy, reGroup, reCreatedAt, reLastEdit, reLastEditBy, 0, "", reLiked, reLikeCount, 0, ""}, ContentHtml: c.ParseMessage(reContent, 0, "", user.ParseSettings, &user), CreatedByName: reCreatedByName, Avatar: reAvatar, Level: 0}
ru.Init() _, err = ru.Init()
group, err := c.Groups.Get(ru.Group)
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
if group.Tag != "" { if puser.ID == ru.CreatedBy {
ru.Tag = group.Tag
} else if puser.ID == ru.CreatedBy {
ru.Tag = phrases.GetTmplPhrase("profile.owner_tag") ru.Tag = phrases.GetTmplPhrase("profile.owner_tag")
} }
// TODO: Add a hook here // TODO: Add a hook here
replyList = append(replyList, ru) reList = append(reList, ru)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
@ -114,8 +109,8 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user c.User, header *c.
} }
} }
canMessage := (!blockedInv && user.Perms.UseConvos) || user.IsSuperMod canMessage := (!blockedInv && user.Perms.UseConvos) || user.IsSuperMod
canComment := !blockedInv && user.Perms.ViewTopic && user.Perms.CreateReply canComment := !blockedInv && user.Perms.CreateProfileReply
ppage := c.ProfilePage{header, replyList, *puser, currentScore, nextScore, blocked, canMessage, canComment} ppage := c.ProfilePage{header, reList, *puser, currentScore, nextScore, blocked, canMessage, canComment}
return renderTemplate("profile", w, r, header, ppage) return renderTemplate("profile", w, r, header, ppage)
} }

View File

@ -10,7 +10,7 @@ import (
) )
func ProfileReplyCreateSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError { func ProfileReplyCreateSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
if !user.Perms.ViewTopic || !user.Perms.CreateReply { if !user.Perms.CreateProfileReply {
return c.NoPermissions(w, r, user) return c.NoPermissions(w, r, user)
} }
uid, err := strconv.Atoi(r.PostFormValue("uid")) uid, err := strconv.Atoi(r.PostFormValue("uid"))
@ -74,6 +74,9 @@ func ProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user c.User,
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
if !user.Perms.CreateProfileReply {
return c.NoPermissionsJSQ(w, r, user, js)
}
// ? Does the admin understand that this group perm affects this? // ? Does the admin understand that this group perm affects this?
if user.ID != creator.ID && !user.Perms.EditReply { if user.ID != creator.ID && !user.Perms.EditReply {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, user, js)
@ -127,5 +130,10 @@ func ProfileReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.Use
} else { } else {
w.Write(successJSONBytes) w.Write(successJSONBytes)
} }
err = c.ModLogs.Create("delete", reply.ParentID, "profile-reply", user.GetIP(), user.ID)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
return nil return nil
} }

View File

@ -194,7 +194,7 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro
prid, _ := strconv.Atoi(r.FormValue("prid")) prid, _ := strconv.Atoi(r.FormValue("prid"))
if js && (prid == 0 || rids[0] == prid) { if js && (prid == 0 || rids[0] == prid) {
outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums", user.ParseSettings)}) outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums", user.ParseSettings, &user)})
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -267,7 +267,7 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid s
if !js { if !js {
http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther) http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
} else { } else {
outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums", user.ParseSettings)}) outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums", user.ParseSettings, &user)})
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }

View File

@ -72,14 +72,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user c.User, header *c.He
header.Title = topic.Title header.Title = topic.Title
header.Path = c.BuildTopicURL(c.NameToSlug(topic.Title), topic.ID) header.Path = c.BuildTopicURL(c.NameToSlug(topic.Title), topic.ID)
// TODO: Cache ContentHTML when possible?
topic.ContentHTML = c.ParseMessage(topic.Content, topic.ParentID, "forums", user.ParseSettings)
// TODO: Do this more efficiently by avoiding the allocations entirely in ParseMessage, if there's nothing to do.
if topic.ContentHTML == topic.Content {
topic.ContentHTML = topic.Content
}
topic.ContentLines = strings.Count(topic.Content, "\n") topic.ContentLines = strings.Count(topic.Content, "\n")
if len(topic.Content) > 200 { if len(topic.Content) > 200 {
header.OGDesc = topic.Content[:197] + "..." header.OGDesc = topic.Content[:197] + "..."
} else { } else {
@ -90,6 +83,22 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user c.User, header *c.He
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
var parseSettings *c.ParseSettings
if !postGroup.Perms.AutoEmbed && (user.ParseSettings == nil || !user.ParseSettings.NoEmbed) {
parseSettings = c.DefaultParseSettings.CopyPtr()
parseSettings.NoEmbed = true
} else {
parseSettings = user.ParseSettings
}
// TODO: Cache ContentHTML when possible?
topic.ContentHTML = c.ParseMessage(topic.Content, topic.ParentID, "forums", parseSettings, &user)
// TODO: Do this more efficiently by avoiding the allocations entirely in ParseMessage, if there's nothing to do.
if topic.ContentHTML == topic.Content {
topic.ContentHTML = topic.Content
}
topic.Tag = postGroup.Tag topic.Tag = postGroup.Tag
if postGroup.IsMod { if postGroup.IsMod {
topic.ClassName = c.Config.StaffCSS topic.ClassName = c.Config.StaffCSS
@ -604,7 +613,7 @@ func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User, stid s
if !js { if !js {
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
} else { } else {
outBytes, err := json.Marshal(JsonReply{c.ParseMessage(topic.Content, topic.ParentID, "forums", user.ParseSettings)}) outBytes, err := json.Marshal(JsonReply{c.ParseMessage(topic.Content, topic.ParentID, "forums", user.ParseSettings, &user)})
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }

View File

@ -7,9 +7,9 @@ INSERT INTO [settings] ([name],[content],[type]) VALUES ('rapid_loading','1','bo
INSERT INTO [settings] ([name],[content],[type]) VALUES ('google_site_verify','','html-attribute'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('google_site_verify','','html-attribute');
INSERT INTO [themes] ([uname],[default]) VALUES ('cosora',1); INSERT INTO [themes] ([uname],[default]) VALUES ('cosora',1);
INSERT INTO [emails] ([email],[uid],[validated]) VALUES ('admin@localhost',1,1); INSERT INTO [emails] ([email],[uid],[validated]) VALUES ('admin@localhost',1,1);
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin'); INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin');
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,0,0,'Mod'); INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,0,0,'Mod');
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Member','{"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}',0,0,0,""); INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Member','{"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}',0,0,0,"");
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Banned','{"ViewTopic":true}','{}',0,0,1,""); INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Banned','{"ViewTopic":true}','{}',0,0,1,"");
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Awaiting Activation','{"ViewTopic":true}','{}',0,0,0,""); INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Awaiting Activation','{"ViewTopic":true}','{}',0,0,0,"");
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Not Loggedin','{"ViewTopic":true}','{}',0,0,0,'Guest'); INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[is_banned],[tag]) VALUES ('Not Loggedin','{"ViewTopic":true}','{}',0,0,0,'Guest');

View File

@ -15,9 +15,9 @@ INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('rapid_loading','1','boo
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('google_site_verify','','html-attribute'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('google_site_verify','','html-attribute');
INSERT INTO `themes`(`uname`,`default`) VALUES ('cosora',1); INSERT INTO `themes`(`uname`,`default`) VALUES ('cosora',1);
INSERT INTO `emails`(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1); INSERT INTO `emails`(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1);
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin'); INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin');
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,0,0,'Mod'); INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,0,0,'Mod');
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Member','{"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}',0,0,0,""); INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Member','{"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}',0,0,0,"");
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Banned','{"ViewTopic":true}','{}',0,0,1,""); INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Banned','{"ViewTopic":true}','{}',0,0,1,"");
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Awaiting Activation','{"ViewTopic":true}','{}',0,0,0,""); INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Awaiting Activation','{"ViewTopic":true}','{}',0,0,0,"");
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Not Loggedin','{"ViewTopic":true}','{}',0,0,0,'Guest'); INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`is_banned`,`tag`) VALUES ('Not Loggedin','{"ViewTopic":true}','{}',0,0,0,'Guest');

View File

@ -7,9 +7,9 @@ INSERT INTO "settings"("name","content","type") VALUES ('rapid_loading','1','boo
INSERT INTO "settings"("name","content","type") VALUES ('google_site_verify','','html-attribute'); INSERT INTO "settings"("name","content","type") VALUES ('google_site_verify','','html-attribute');
INSERT INTO "themes"("uname","default") VALUES ('cosora',1); INSERT INTO "themes"("uname","default") VALUES ('cosora',1);
INSERT INTO "emails"("email","uid","validated") VALUES ('admin@localhost',1,1); INSERT INTO "emails"("email","uid","validated") VALUES ('admin@localhost',1,1);
INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin'); INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,0,'Admin');
INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,0,0,'Mod'); INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Moderator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,0,0,'Mod');
INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Member','{"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}',0,0,0,""); INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Member','{"UploadFiles":true,"UploadAvatars":true,"UseConvos":true,"CreateProfileReply":true,"AutoEmbed":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"CreateReply":true}','{}',0,0,0,"");
INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Banned','{"ViewTopic":true}','{}',0,0,1,""); INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Banned','{"ViewTopic":true}','{}',0,0,1,"");
INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Awaiting Activation','{"ViewTopic":true}','{}',0,0,0,""); INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Awaiting Activation','{"ViewTopic":true}','{}',0,0,0,"");
INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Not Loggedin','{"ViewTopic":true}','{}',0,0,0,'Guest'); INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","is_banned","tag") VALUES ('Not Loggedin','{"ViewTopic":true}','{}',0,0,0,'Guest');

View File

@ -4,7 +4,7 @@
<main class="colstack_right"> <main class="colstack_right">
{{template "panel_before_head.html" . }} {{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div> <div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}} - #{{.ID}}</h1></div>
</div> </div>
<div id="panel_group" class="colstack_item the_form"> <div id="panel_group" class="colstack_item the_form">
<form action="/panel/groups/edit/submit/{{.ID}}?s={{.CurrentUser.Session}}" method="post"> <form action="/panel/groups/edit/submit/{{.ID}}?s={{.CurrentUser.Session}}" method="post">

View File

@ -4,7 +4,7 @@
<main class="colstack_right"> <main class="colstack_right">
{{template "panel_before_head.html" . }} {{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}}</h1></div> <div class="rowitem"><h1>{{.Name}}{{lang "panel_group_head_suffix"}} - #{{.ID}}</h1></div>
</div> </div>
<form action="/panel/groups/edit/perms/submit/{{.ID}}?s={{.CurrentUser.Session}}" method="post"> <form action="/panel/groups/edit/perms/submit/{{.ID}}?s={{.CurrentUser.Session}}" method="post">
{{if .CurrentUser.Perms.EditGroupLocalPerms}} {{if .CurrentUser.Perms.EditGroupLocalPerms}}
@ -50,6 +50,29 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{if .CurrentUser.Perms.EditGroupGlobalPerms}}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_group_mod_permissions"}}</h1></div>
</div>
<div class="colstack_item rowlist formlist the_form panel_group_perms">
{{range .ModPerms}}
<div class="formrow">
<div class="formitem">
<a>{{.LangStr}}</a>
<div class="to_right">
<select name="perm-{{.Name}}">
<option{{if .Toggle}} selected{{end}} value=1>{{lang "option_yes"}}</option>
<option{{if not .Toggle}} selected{{end}} value=0>{{lang "option_no"}}</option>
</select>
</div>
</div>
</div>
{{end}}
<div class="formrow">
<div class="formitem"><button name="panel-button" class="formbutton form_middle_button">{{lang "panel_group_update_button"}}</button></div>
</div>
</div>
{{end}}
</form> </form>
</main> </main>
</div> </div>