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 {
fperms, changed := GroupForumPresetToForumPerms(preset)
fp, changed := GroupForumPresetToForumPerms(preset)
if changed {
return f.SetPerms(fperms, preset, gid)
return f.SetPerms(fp, preset, gid)
}
return nil
}

View File

@ -1,7 +1,10 @@
package common
import "database/sql"
import "github.com/Azareal/Gosora/query_gen"
import (
"database/sql"
qgen "github.com/Azareal/Gosora/query_gen"
)
var Likes LikeStore
@ -22,7 +25,7 @@ func NewDefaultLikeStore(acc *qgen.Accumulator) (*DefaultLikeStore, error) {
// TODO: Write a test for this
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 {
return nil, nil
} else if err != nil {
@ -32,11 +35,10 @@ func (s *DefaultLikeStore) BulkExists(ids []int, sentBy int, targetType string)
var id int
for rows.Next() {
err = rows.Scan(&id)
if err != nil {
if err := rows.Scan(&id); err != nil {
return nil, err
}
eids = append(eids,id)
eids = append(eids, id)
}
return eids, rows.Err()
}
@ -49,4 +51,4 @@ func (s *DefaultLikeStore) Count() (count int) {
LogError(err)
}
return count
}
}

View File

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

View File

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

View File

@ -242,9 +242,8 @@ func init() {
// Flush the topic out of the cache
// ? - We do a CacheRemove() here instead of mutating the pointer to avoid creating a race condition
func (t *Topic) cacheRemove() {
tcache := Topics.GetCache()
if tcache != nil {
tcache.Remove(t.ID)
if tc := Topics.GetCache(); tc != nil {
tc.Remove(t.ID)
}
TopicListThaw.Thaw()
}
@ -565,11 +564,10 @@ func (t *TopicUser) Replies(offset int, pFrag int, user *User) (rlist []*ReplyUs
if err != nil {
return nil, "", err
}
err = reply.Init()
if err != nil {
if err := reply.Init(); err != nil {
return nil, "", err
}
reply.ContentHtml = ParseMessage(reply.Content, t.ParentID, "forums")
// 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?
func (u *User) CacheRemove() {
ucache := Users.GetCache()
if ucache != nil {
ucache.Remove(u.ID)
if uc := Users.GetCache(); uc != nil {
uc.Remove(u.ID)
}
TopicListThaw.Thaw()
}
@ -336,9 +335,8 @@ func (u *User) ChangeGroup(group int) (err error) {
// ! Only updates the database not the *User for safety reasons
func (u *User) UpdateIP(host string) error {
_, err := userStmts.updateLastIP.Exec(host, u.ID)
ucache := Users.GetCache()
if ucache != nil {
ucache.Remove(u.ID)
if uc := Users.GetCache(); uc != nil {
uc.Remove(u.ID)
}
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 {
return c.InternalError(err, w, r)
}
puser.Init()
}
header.Title = phrases.GetTitlePhrasef("profile", puser.Name)
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 {
return c.InternalError(err, w, r)
}
if group.Tag != "" {
ru.Tag = group.Tag
} else if puser.ID == ru.CreatedBy {
ru.Tag = phrases.GetTmplPhrase("profile_owner_tag")
} else {
ru.Tag = ""
}
// 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)
}
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)
}

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.DebugDetailf("values: %+v\n", values)
for _, value := range values {
if strings.HasPrefix(key, "pollinputitem[") {
halves := strings.Split(key, "[")
if len(halves) != 2 {
return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
}
halves[1] = strings.TrimSuffix(halves[1], "]")
if !strings.HasPrefix(key, "pollinputitem[") {
continue
}
halves := strings.Split(key, "[")
if len(halves) != 2 {
return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
}
halves[1] = strings.TrimSuffix(halves[1], "]")
index, err := strconv.Atoi(halves[1])
if err != nil {
return c.LocalErrorJSQ("Malformed pollinputitem", w, r, user, js)
}
index, err := strconv.Atoi(halves[1])
if err != nil {
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
_, exists := pollInputItems[index]
// TODO: Should we use SanitiseBody instead to keep the newlines?
if !exists && len(c.SanitiseSingleLine(value)) != 0 {
pollInputItems[index] = c.SanitiseSingleLine(value)
if len(pollInputItems) >= maxPollOptions {
break
}
// 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]
// TODO: Should we use SanitiseBody instead to keep the newlines?
if !exists && len(c.SanitiseSingleLine(value)) != 0 {
pollInputItems[index] = c.SanitiseSingleLine(value)
if len(pollInputItems) >= maxPollOptions {
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)
}
wcount := c.WordCount(content)
err = user.IncreasePostStats(wcount, false)
err = user.IncreasePostStats(c.WordCount(content), false)
if err != nil {
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
for rows.Next() {
var rid int
err := rows.Scan(&rid)
if err != nil {
if err := rows.Scan(&rid); err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
rids = append(rids, rid)
}
err = rows.Err()
if err != nil {
if err := rows.Err(); err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
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)
}
}
err = reply.Delete()
if err != nil {
if err := reply.Delete(); err != nil {
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...?
replyCreator, err := c.Users.Get(reply.CreatedBy)
if err == nil {
wcount := c.WordCount(reply.Content)
err = replyCreator.DecreasePostStats(wcount, false)
err = replyCreator.DecreasePostStats(c.WordCount(reply.Content), false)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
@ -463,117 +458,6 @@ func RemoveAttachFromReplySubmit(w http.ResponseWriter, r *http.Request, user c.
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 {
js := r.PostFormValue("js") == "1"
rid, err := strconv.Atoi(srid)

View File

@ -27,13 +27,13 @@
{{if not .CurrentUser.Loggedin}}<div class="rowitem passive">
<a class="profile_menu_item">{{lang "profile_login_for_options"}}</a>
</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>
</div>
</div>{{end}}
<!--<div class="rowitem passive">
<a class="profile_menu_item">{{lang "profile_add_friend"}}</a>
</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>
{{else}}<a href="#ban_user" class="profile_menu_item">{{lang "profile_ban"}}</a>{{end}}
</div>{{end}}
@ -97,7 +97,7 @@
<div id="profile_comments" class="colstack_item hash_hide">{{template "profile_comments_row.html" . }}</div>
{{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">
<input name="uid" value='{{.ProfileOwner.ID}}' type="hidden" />
<div class="colstack_item topic_reply_form" style="border-top:none;">