Stop blocked users making profile comments too.

Hide the Send Message option on profiles for blocked users.
Move the profile reply routes to their own file.
Remove a redundant user initialisation.

Shorten things.
This commit is contained in:
Azareal 2019-10-19 20:33:59 +10:00
parent 8720de83d8
commit bbfd3c51c7
11 changed files with 196 additions and 177 deletions

View File

@ -95,9 +95,9 @@ func (f *Forum) Update(name string, desc string, active bool, preset string) err
} }
func (f *Forum) SetPreset(preset string, gid int) error { func (f *Forum) SetPreset(preset string, gid int) error {
fperms, changed := GroupForumPresetToForumPerms(preset) fp, changed := GroupForumPresetToForumPerms(preset)
if changed { if changed {
return f.SetPerms(fperms, preset, gid) return f.SetPerms(fp, preset, gid)
} }
return nil return nil
} }

View File

@ -1,7 +1,10 @@
package common package common
import "database/sql" import (
import "github.com/Azareal/Gosora/query_gen" "database/sql"
qgen "github.com/Azareal/Gosora/query_gen"
)
var Likes LikeStore var Likes LikeStore
@ -22,7 +25,7 @@ func NewDefaultLikeStore(acc *qgen.Accumulator) (*DefaultLikeStore, error) {
// TODO: Write a test for this // TODO: Write a test for this
func (s *DefaultLikeStore) BulkExists(ids []int, sentBy int, targetType string) (eids []int, err error) { func (s *DefaultLikeStore) BulkExists(ids []int, sentBy int, targetType string) (eids []int, err error) {
rows, err := qgen.NewAcc().Select("likes").Columns("targetItem").Where("sentBy = ? AND targetType = ?").In("targetItem", ids).Query(sentBy,targetType) rows, err := qgen.NewAcc().Select("likes").Columns("targetItem").Where("sentBy = ? AND targetType = ?").In("targetItem", ids).Query(sentBy, targetType)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
} else if err != nil { } else if err != nil {
@ -32,11 +35,10 @@ func (s *DefaultLikeStore) BulkExists(ids []int, sentBy int, targetType string)
var id int var id int
for rows.Next() { for rows.Next() {
err = rows.Scan(&id) if err := rows.Scan(&id); err != nil {
if err != nil {
return nil, err return nil, err
} }
eids = append(eids,id) eids = append(eids, id)
} }
return eids, rows.Err() return eids, rows.Err()
} }

View File

@ -208,6 +208,8 @@ type ProfilePage struct {
CurrentScore int CurrentScore int
NextScore int NextScore int
Blocked bool Blocked bool
CanMessage bool
CanComment bool
} }
type CreateTopicPage struct { type CreateTopicPage struct {

View File

@ -23,19 +23,20 @@ type DefaultBlockStore struct {
} }
func NewDefaultBlockStore(acc *qgen.Accumulator) (*DefaultBlockStore, error) { func NewDefaultBlockStore(acc *qgen.Accumulator) (*DefaultBlockStore, error) {
ub := "users_blocks"
return &DefaultBlockStore{ return &DefaultBlockStore{
isBlocked: acc.Select("users_blocks").Cols("blocker").Where("blocker = ? AND blockedUser = ?").Prepare(), isBlocked: acc.Select(ub).Cols("blocker").Where("blocker = ? AND blockedUser = ?").Prepare(),
add: acc.Insert("users_blocks").Columns("blocker,blockedUser").Fields("?,?").Prepare(), add: acc.Insert(ub).Columns("blocker,blockedUser").Fields("?,?").Prepare(),
remove: acc.Delete("users_blocks").Where("blocker = ? AND blockedUser = ?").Prepare(), remove: acc.Delete(ub).Where("blocker = ? AND blockedUser = ?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
func (s *DefaultBlockStore) IsBlockedBy(blocker, blockee int) (bool, error) { func (s *DefaultBlockStore) IsBlockedBy(blocker, blockee int) (bool, error) {
err := s.isBlocked.QueryRow(blocker, blockee).Scan(&blocker) err := s.isBlocked.QueryRow(blocker, blockee).Scan(&blocker)
if err != nil && err != ErrNoRows { if err == ErrNoRows {
return false, err return false, nil
} }
return err != ErrNoRows, nil return err == nil, err
} }
func (s *DefaultBlockStore) Add(blocker, blockee int) error { func (s *DefaultBlockStore) Add(blocker, blockee int) error {

View File

@ -296,7 +296,7 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
return err return err
} }
ppage := ProfilePage{htitle("User 526"), replyList, user, 0, 0, false} // TODO: Use the score from user to generate the currentScore and nextScore ppage := ProfilePage{htitle("User 526"), replyList, user, 0, 0, false,false,false} // TODO: Use the score from user to generate the currentScore and nextScore
t.Add("profile", "c.ProfilePage", ppage) t.Add("profile", "c.ProfilePage", ppage)
var topicsList []*TopicsRow var topicsList []*TopicsRow

View File

@ -242,9 +242,8 @@ func init() {
// Flush the topic out of the cache // Flush the topic out of the cache
// ? - We do a CacheRemove() here instead of mutating the pointer to avoid creating a race condition // ? - We do a CacheRemove() here instead of mutating the pointer to avoid creating a race condition
func (t *Topic) cacheRemove() { func (t *Topic) cacheRemove() {
tcache := Topics.GetCache() if tc := Topics.GetCache(); tc != nil {
if tcache != nil { tc.Remove(t.ID)
tcache.Remove(t.ID)
} }
TopicListThaw.Thaw() TopicListThaw.Thaw()
} }
@ -565,11 +564,10 @@ func (t *TopicUser) Replies(offset int, pFrag int, user *User) (rlist []*ReplyUs
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if err := reply.Init(); err != nil {
err = reply.Init()
if err != nil {
return nil, "", err return nil, "", err
} }
reply.ContentHtml = ParseMessage(reply.Content, t.ParentID, "forums") reply.ContentHtml = ParseMessage(reply.Content, t.ParentID, "forums")
// TODO: This doesn't work properly so pick the first one instead? // TODO: This doesn't work properly so pick the first one instead?

View File

@ -180,9 +180,8 @@ func (u *User) Init() {
// TODO: Refactor this idiom into something shorter, maybe with a NullUserCache when one isn't set? // TODO: Refactor this idiom into something shorter, maybe with a NullUserCache when one isn't set?
func (u *User) CacheRemove() { func (u *User) CacheRemove() {
ucache := Users.GetCache() if uc := Users.GetCache(); uc != nil {
if ucache != nil { uc.Remove(u.ID)
ucache.Remove(u.ID)
} }
TopicListThaw.Thaw() TopicListThaw.Thaw()
} }
@ -336,9 +335,8 @@ func (u *User) ChangeGroup(group int) (err error) {
// ! Only updates the database not the *User for safety reasons // ! Only updates the database not the *User for safety reasons
func (u *User) UpdateIP(host string) error { func (u *User) UpdateIP(host string) error {
_, err := userStmts.updateLastIP.Exec(host, u.ID) _, err := userStmts.updateLastIP.Exec(host, u.ID)
ucache := Users.GetCache() if uc := Users.GetCache(); uc != nil {
if ucache != nil { uc.Remove(u.ID)
ucache.Remove(u.ID)
} }
return err return err
} }

View File

@ -59,7 +59,6 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user c.User, header *c.
} else if err != nil { } else if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
puser.Init()
} }
header.Title = phrases.GetTitlePhrasef("profile", puser.Name) header.Title = phrases.GetTitlePhrasef("profile", puser.Name)
header.Path = c.BuildProfileURL(c.NameToSlug(puser.Name), puser.ID) header.Path = c.BuildProfileURL(c.NameToSlug(puser.Name), puser.ID)
@ -86,13 +85,10 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user c.User, header *c.
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
if group.Tag != "" { if group.Tag != "" {
ru.Tag = group.Tag ru.Tag = group.Tag
} else if puser.ID == ru.CreatedBy { } else if puser.ID == ru.CreatedBy {
ru.Tag = phrases.GetTmplPhrase("profile_owner_tag") ru.Tag = phrases.GetTmplPhrase("profile_owner_tag")
} else {
ru.Tag = ""
} }
// TODO: Add a hook here // TODO: Add a hook here
@ -111,6 +107,13 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user c.User, header *c.
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
ppage := c.ProfilePage{header, replyList, *puser, currentScore, nextScore, blocked} blockedInv, err := c.UserBlocks.IsBlockedBy(puser.ID, user.ID)
if err != nil {
return c.InternalError(err, w, r)
}
canMessage := (!blockedInv && user.Perms.UseConvos) || user.IsSuperMod
canComment := !blockedInv && user.Perms.ViewTopic && user.Perms.CreateReply
ppage := c.ProfilePage{header, replyList, *puser, currentScore, nextScore, blocked, canMessage, canComment}
return renderTemplate("profile", w, r, header, ppage) return renderTemplate("profile", w, r, header, ppage)
} }

131
routes/profile_reply.go Normal file
View File

@ -0,0 +1,131 @@
package routes
import (
"database/sql"
"net/http"
"strconv"
c "github.com/Azareal/Gosora/common"
"github.com/Azareal/Gosora/common/counters"
)
func ProfileReplyCreateSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
if !user.Perms.ViewTopic || !user.Perms.CreateReply {
return c.NoPermissions(w, r, user)
}
uid, err := strconv.Atoi(r.PostFormValue("uid"))
if err != nil {
return c.LocalError("Invalid UID", w, r, user)
}
profileOwner, err := c.Users.Get(uid)
if err == sql.ErrNoRows {
return c.LocalError("The profile you're trying to post on doesn't exist.", w, r, user)
} else if err != nil {
return c.InternalError(err, w, r)
}
blocked, err := c.UserBlocks.IsBlockedBy(profileOwner.ID, user.ID)
if err != nil {
return c.InternalError(err, w, r)
}
// Supermods can bypass blocks so they can tell people off when they do something stupid or have to convey important information
if blocked && !user.IsSuperMod {
return c.LocalError("You don't have permission to send messages to one of these users.", w, r, user)
}
content := c.PreparseMessage(r.PostFormValue("content"))
if len(content) == 0 {
return c.LocalError("You can't make a blank post", w, r, user)
}
// TODO: Fully parse the post and store it in the parsed column
_, err = c.Prstore.Create(profileOwner.ID, content, user.ID, user.LastIP)
if err != nil {
return c.InternalError(err, w, r)
}
// ! Be careful about leaking per-route permission state with &user
alert := c.Alert{ActorID: user.ID, TargetUserID: profileOwner.ID, Event: "reply", ElementType: "user", ElementID: profileOwner.ID, Actor: &user}
err = c.AddActivityAndNotifyTarget(alert)
if err != nil {
return c.InternalError(err, w, r)
}
counters.PostCounter.Bump()
http.Redirect(w, r, "/user/"+strconv.Itoa(uid), http.StatusSeeOther)
return nil
}
func ProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid)
if err != nil {
return c.LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, js)
}
reply, err := c.Prstore.Get(rid)
if err == sql.ErrNoRows {
return c.PreErrorJSQ("The target reply doesn't exist.", w, r, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
creator, err := c.Users.Get(reply.CreatedBy)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
// ? Does the admin understand that this group perm affects this?
if user.ID != creator.ID && !user.Perms.EditReply {
return c.NoPermissionsJSQ(w, r, user, js)
}
// TODO: Stop blocked users from modifying profile replies?
err = reply.SetBody(r.PostFormValue("edit_item"))
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
if !js {
http.Redirect(w, r, "/user/"+strconv.Itoa(creator.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
} else {
w.Write(successJSONBytes)
}
return nil
}
func ProfileReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid)
if err != nil {
return c.LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, js)
}
reply, err := c.Prstore.Get(rid)
if err == sql.ErrNoRows {
return c.PreErrorJSQ("The target reply doesn't exist.", w, r, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
creator, err := c.Users.Get(reply.CreatedBy)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
if user.ID != creator.ID && !user.Perms.DeleteReply {
return c.NoPermissionsJSQ(w, r, user, js)
}
err = reply.Delete()
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
//log.Printf("The profile post '%d' was deleted by c.User #%d", reply.ID, user.ID)
if !js {
//http.Redirect(w,r, "/user/" + strconv.Itoa(creator.ID), http.StatusSeeOther)
} else {
w.Write(successJSONBytes)
}
return nil
}

View File

@ -92,26 +92,27 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro
//c.DebugDetail("key: ", key) //c.DebugDetail("key: ", key)
//c.DebugDetailf("values: %+v\n", values) //c.DebugDetailf("values: %+v\n", values)
for _, value := range values { for _, value := range values {
if strings.HasPrefix(key, "pollinputitem[") { if !strings.HasPrefix(key, "pollinputitem[") {
halves := strings.Split(key, "[") continue
if len(halves) != 2 { }
return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js) halves := strings.Split(key, "[")
} if len(halves) != 2 {
halves[1] = strings.TrimSuffix(halves[1], "]") return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
}
halves[1] = strings.TrimSuffix(halves[1], "]")
index, err := strconv.Atoi(halves[1]) index, err := strconv.Atoi(halves[1])
if err != nil { if err != nil {
return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js) return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
} }
// If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack // If there are duplicates, then something has gone horribly wrong, so let's ignore them, this'll likely happen during an attack
_, exists := pollInputItems[index] _, exists := pollInputItems[index]
// TODO: Should we use SanitiseBody instead to keep the newlines? // TODO: Should we use SanitiseBody instead to keep the newlines?
if !exists && len(c.SanitiseSingleLine(value)) != 0 { if !exists && len(c.SanitiseSingleLine(value)) != 0 {
pollInputItems[index] = c.SanitiseSingleLine(value) pollInputItems[index] = c.SanitiseSingleLine(value)
if len(pollInputItems) >= maxPollOptions { if len(pollInputItems) >= maxPollOptions {
break break
}
} }
} }
} }
@ -140,8 +141,7 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
wcount := c.WordCount(content) err = user.IncreasePostStats(c.WordCount(content), false)
err = user.IncreasePostStats(wcount, false)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -164,14 +164,12 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro
var rids []int var rids []int
for rows.Next() { for rows.Next() {
var rid int var rid int
err := rows.Scan(&rid) if err := rows.Scan(&rid); err != nil {
if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
rids = append(rids, rid) rids = append(rids, rid)
} }
err = rows.Err() if err := rows.Err(); err != nil {
if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
if len(rids) == 0 { if len(rids) == 0 {
@ -312,9 +310,7 @@ func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, user, js)
} }
} }
if err := reply.Delete(); err != nil {
err = reply.Delete()
if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -333,8 +329,7 @@ func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid
// ? - What happens if an error fires after a redirect...? // ? - What happens if an error fires after a redirect...?
replyCreator, err := c.Users.Get(reply.CreatedBy) replyCreator, err := c.Users.Get(reply.CreatedBy)
if err == nil { if err == nil {
wcount := c.WordCount(reply.Content) err = replyCreator.DecreasePostStats(c.WordCount(reply.Content), false)
err = replyCreator.DecreasePostStats(wcount, false)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -463,117 +458,6 @@ func RemoveAttachFromReplySubmit(w http.ResponseWriter, r *http.Request, user c.
return nil return nil
} }
// TODO: Move the profile reply routes to their own file?
func ProfileReplyCreateSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
if !user.Perms.ViewTopic || !user.Perms.CreateReply {
return c.NoPermissions(w, r, user)
}
uid, err := strconv.Atoi(r.PostFormValue("uid"))
if err != nil {
return c.LocalError("Invalid UID", w, r, user)
}
profileOwner, err := c.Users.Get(uid)
if err == sql.ErrNoRows {
return c.LocalError("The profile you're trying to post on doesn't exist.", w, r, user)
} else if err != nil {
return c.InternalError(err, w, r)
}
content := c.PreparseMessage(r.PostFormValue("content"))
if len(content) == 0 {
return c.LocalError("You can't make a blank post", w, r, user)
}
// TODO: Fully parse the post and store it in the parsed column
_, err = c.Prstore.Create(profileOwner.ID, content, user.ID, user.LastIP)
if err != nil {
return c.InternalError(err, w, r)
}
// ! Be careful about leaking per-route permission state with &user
alert := c.Alert{ActorID: user.ID, TargetUserID: profileOwner.ID, Event: "reply", ElementType: "user", ElementID: profileOwner.ID, Actor: &user}
err = c.AddActivityAndNotifyTarget(alert)
if err != nil {
return c.InternalError(err, w, r)
}
counters.PostCounter.Bump()
http.Redirect(w, r, "/user/"+strconv.Itoa(uid), http.StatusSeeOther)
return nil
}
func ProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid)
if err != nil {
return c.LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, js)
}
reply, err := c.Prstore.Get(rid)
if err == sql.ErrNoRows {
return c.PreErrorJSQ("The target reply doesn't exist.", w, r, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
creator, err := c.Users.Get(reply.CreatedBy)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
// ? Does the admin understand that this group perm affects this?
if user.ID != creator.ID && !user.Perms.EditReply {
return c.NoPermissionsJSQ(w, r, user, js)
}
err = reply.SetBody(r.PostFormValue("edit_item"))
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
if !js {
http.Redirect(w, r, "/user/"+strconv.Itoa(creator.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther)
} else {
w.Write(successJSONBytes)
}
return nil
}
func ProfileReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid)
if err != nil {
return c.LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, js)
}
reply, err := c.Prstore.Get(rid)
if err == sql.ErrNoRows {
return c.PreErrorJSQ("The target reply doesn't exist.", w, r, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
creator, err := c.Users.Get(reply.CreatedBy)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
if user.ID != creator.ID && !user.Perms.DeleteReply {
return c.NoPermissionsJSQ(w, r, user, js)
}
err = reply.Delete()
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
//log.Printf("The profile post '%d' was deleted by c.User #%d", reply.ID, user.ID)
if !js {
//http.Redirect(w,r, "/user/" + strconv.Itoa(creator.ID), http.StatusSeeOther)
} else {
w.Write(successJSONBytes)
}
return nil
}
func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError { func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user c.User, srid string) c.RouteError {
js := r.PostFormValue("js") == "1" js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid) rid, err := strconv.Atoi(srid)

View File

@ -27,13 +27,13 @@
{{if not .CurrentUser.Loggedin}}<div class="rowitem passive"> {{if not .CurrentUser.Loggedin}}<div class="rowitem passive">
<a class="profile_menu_item">{{lang "profile_login_for_options"}}</a> <a class="profile_menu_item">{{lang "profile_login_for_options"}}</a>
</div>{{else}} </div>{{else}}
<div class="rowitem passive"> {{if .CanMessage}}<div class="rowitem passive">
<a href="/user/convos/create/" class="profile_menu_item">{{lang "profile_send_message"}}</a> <a href="/user/convos/create/" class="profile_menu_item">{{lang "profile_send_message"}}</a>
</div> </div>{{end}}
<!--<div class="rowitem passive"> <!--<div class="rowitem passive">
<a class="profile_menu_item">{{lang "profile_add_friend"}}</a> <a class="profile_menu_item">{{lang "profile_add_friend"}}</a>
</div>--> </div>-->
{{if (.CurrentUser.IsSuperMod) and not (.ProfileOwner.IsSuperMod) }}<div class="rowitem passive"> {{if (.CurrentUser.IsSuperMod) and not (.ProfileOwner.IsSuperMod)}}<div class="rowitem passive">
{{if .ProfileOwner.IsBanned}}<a href="/users/unban/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}" class="profile_menu_item">{{lang "profile_unban"}}</a> {{if .ProfileOwner.IsBanned}}<a href="/users/unban/{{.ProfileOwner.ID}}?s={{.CurrentUser.Session}}" class="profile_menu_item">{{lang "profile_unban"}}</a>
{{else}}<a href="#ban_user" class="profile_menu_item">{{lang "profile_ban"}}</a>{{end}} {{else}}<a href="#ban_user" class="profile_menu_item">{{lang "profile_ban"}}</a>{{end}}
</div>{{end}} </div>{{end}}
@ -97,7 +97,7 @@
<div id="profile_comments" class="colstack_item hash_hide">{{template "profile_comments_row.html" . }}</div> <div id="profile_comments" class="colstack_item hash_hide">{{template "profile_comments_row.html" . }}</div>
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Loggedin}}
{{if not .CurrentUser.IsBanned}} {{if .CanComment}}
<form id="profile_comments_form" class="hash_hide" action="/profile/reply/create/?s={{.CurrentUser.Session}}" method="post"> <form id="profile_comments_form" class="hash_hide" action="/profile/reply/create/?s={{.CurrentUser.Session}}" method="post">
<input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" /> <input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" />
<div class="colstack_item topic_reply_form" style="border-top:none;"> <div class="colstack_item topic_reply_form" style="border-top:none;">