Added the Not method to the router generator.

Refactored the router generator.

Added the NoBanned and NoSessionMismatch middlewares.
Removed some of the implicit returns.
Refactored some of the routes and their associated stores.
Added more Codebeat exclusions.
Added the AddReply method to *Topic.
Added the Like method to *Topic.
MemberOnly now emits a LoginRequired error rather than a generic NoPermissions error.

Made progress with Cosora, this time with the profile system.
This commit is contained in:
Azareal 2017-11-08 07:28:33 +00:00
parent 920c1aad0f
commit 34ca7de946
19 changed files with 224 additions and 149 deletions

View File

@ -1,6 +1,9 @@
/public/trumbowyg/*
/public/jquery-emojiarea/*
/public/font-awesome-4.7.0/*
/public/jquery-3.1.1.min.js
/public/EQCSS.min.js
/public/EQCSS.js
/schema/*
template_list.go

View File

@ -142,6 +142,18 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
err = NoBanned(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
err = NoSessionMismatch(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
switch(req.URL.Path) {
case "/report/submit/":
err = routeReportSubmit(w,req,user,extra_data)

View File

@ -238,6 +238,7 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) R
func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) RouteError {
// TODO: Reduce this to 1MB for attachments for each file?
// TODO: Reuse this code more
if r.ContentLength > int64(config.MaxRequestSize) {
size, unit := convertByteUnit(float64(config.MaxRequestSize))
return CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user)
@ -343,7 +344,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) RouteEr
return LocalError("Bad IP", w, r, user)
}
_, err = rstore.Create(tid, content, ipaddress, topic.ParentID, user.ID)
_, err = rstore.Create(topic, content, ipaddress, user.ID)
if err != nil {
return InternalError(err, w, r)
}
@ -409,18 +410,10 @@ func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) RouteErro
if !user.Perms.ViewTopic || !user.Perms.LikeItem {
return NoPermissions(w, r, user)
}
if topic.CreatedBy == user.ID {
return LocalError("You can't like your own topics", w, r, user)
}
err = stmts.hasLikedTopic.QueryRow(user.ID, tid).Scan(&tid)
if err != nil && err != ErrNoRows {
return InternalError(err, w, r)
} else if err != ErrNoRows {
return LocalError("You already liked this!", w, r, user)
}
_, err = users.Get(topic.CreatedBy)
if err != nil && err == ErrNoRows {
return LocalError("The target user doesn't exist", w, r, user)
@ -429,13 +422,10 @@ func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) RouteErro
}
score := 1
_, err = stmts.createLike.Exec(score, tid, "topics", user.ID)
if err != nil {
return InternalError(err, w, r)
}
_, err = stmts.addLikesToTopic.Exec(1, tid)
if err != nil {
err = topic.Like(score, user.ID)
if err == ErrAlreadyLiked {
return LocalError("You already liked this", w, r, user)
} else if err != nil {
return InternalError(err, w, r)
}
@ -456,11 +446,6 @@ func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) RouteErro
// Live alerts, if the poster is online and WebSockets is enabled
_ = wsHub.pushAlert(topic.CreatedBy, int(lastID), "like", "topic", user.ID, topic.CreatedBy, tid)
// Flush the topic out of the cache
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(tid)
}
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
return nil
}
@ -499,7 +484,6 @@ func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user User) Rou
if !user.Perms.ViewTopic || !user.Perms.LikeItem {
return NoPermissions(w, r, user)
}
if reply.CreatedBy == user.ID {
return LocalError("You can't like your own replies", w, r, user)
}
@ -573,26 +557,10 @@ func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user User)
}
func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemID string) RouteError {
if !user.Loggedin {
return LoginRequired(w, r, user)
}
if user.IsBanned {
return Banned(w, r, user)
}
err := r.ParseForm()
if err != nil {
return LocalError("Bad Form", w, r, user)
}
if r.FormValue("session") != user.Session {
return SecurityError(w, r, user)
}
itemID, err := strconv.Atoi(sitemID)
if err != nil {
return LocalError("Bad ID", w, r, user)
}
itemType := r.FormValue("type")
var fid = 1
@ -649,23 +617,16 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI
}
var count int
rows, err := stmts.reportExists.Query(itemType + "_" + strconv.Itoa(itemID))
err = stmts.reportExists.QueryRow(itemType + "_" + strconv.Itoa(itemID)).Scan(&count)
if err != nil && err != ErrNoRows {
return InternalError(err, w, r)
}
for rows.Next() {
err = rows.Scan(&count)
if err != nil {
return InternalError(err, w, r)
}
}
if count != 0 {
return LocalError("Someone has already reported this!", w, r, user)
}
// TODO: Repost attachments in the reports forum, so that the mods can see them
// ? - Can we do this via the TopicStore?
// ? - Can we do this via the TopicStore? Should we do a ReportStore?
res, err := stmts.createReport.Exec(title, content, parseMessage(content, 0, ""), user.ID, user.ID, itemType+"_"+strconv.Itoa(itemID))
if err != nil {
return InternalError(err, w, r)

View File

@ -750,7 +750,9 @@ func TestReplyStore(t *testing.T) {
// TODO: Test Create and Get
//Create(tid int, content string, ipaddress string, fid int, uid int) (id int, err error)
rid, err := rstore.Create(1, "Fofofo", "::1", 2, 1)
topic, err := topics.Get(1)
expectNilErr(t, err)
rid, err := rstore.Create(topic, "Fofofo", "::1", 1)
expectNilErr(t, err)
expect(t, rid == 2, fmt.Sprintf("The next reply ID should be 2 not %d", rid))

View File

@ -7,7 +7,7 @@ var rstore ReplyStore
type ReplyStore interface {
Get(id int) (*Reply, error)
Create(tid int, content string, ipaddress string, fid int, uid int) (id int, err error)
Create(topic *Topic, content string, ipaddress string, uid int) (id int, err error)
}
type SQLReplyStore struct {
@ -30,24 +30,16 @@ func (store *SQLReplyStore) Get(id int) (*Reply, error) {
}
// TODO: Write a test for this
func (store *SQLReplyStore) Create(tid int, content string, ipaddress string, fid int, uid int) (id int, err error) {
func (store *SQLReplyStore) Create(topic *Topic, content string, ipaddress string, uid int) (id int, err error) {
wcount := wordCount(content)
res, err := store.create.Exec(tid, content, parseMessage(content, fid, "forums"), ipaddress, wcount, uid)
if err != nil {
return 0, err
}
lastID, err := res.LastInsertId()
res, err := store.create.Exec(topic.ID, content, parseMessage(content, topic.ParentID, "forums"), ipaddress, wcount, uid)
if err != nil {
return 0, err
}
_, err = stmts.addRepliesToTopic.Exec(1, uid, tid)
lastID, err := res.LastInsertId()
if err != nil {
return int(lastID), err
return 0, err
}
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(tid)
}
return int(lastID), err
return int(lastID), topic.AddReply(uid)
}

45
router_gen/route_group.go Normal file
View File

@ -0,0 +1,45 @@
package main
type RouteGroup struct {
Path string
RouteList []*RouteImpl
RunBefore []Runnable
}
func newRouteGroup(path string, routes ...*RouteImpl) *RouteGroup {
return &RouteGroup{path, routes, []Runnable{}}
}
func (group *RouteGroup) Not(path ...string) *RouteSubset {
routes := make([]*RouteImpl, len(group.RouteList))
copy(routes, group.RouteList)
for i, route := range routes {
if inStringList(route.Path, path) {
routes = append(routes[:i], routes[i+1:]...)
}
}
return &RouteSubset{routes}
}
func inStringList(needle string, list []string) bool {
for _, item := range list {
if item == needle {
return true
}
}
return false
}
func (group *RouteGroup) Before(line string, literal ...bool) *RouteGroup {
var litItem bool
if len(literal) > 0 {
litItem = literal[0]
}
group.RunBefore = append(group.RunBefore, Runnable{line, litItem})
return group
}
func (group *RouteGroup) Routes(routes ...*RouteImpl) *RouteGroup {
group.RouteList = append(group.RouteList, routes...)
return group
}

View File

@ -0,0 +1,25 @@
package main
type RouteSubset struct {
RouteList []*RouteImpl
}
func (set *RouteSubset) Before(line string, literal ...bool) *RouteSubset {
var litItem bool
if len(literal) > 0 {
litItem = literal[0]
}
for _, route := range set.RouteList {
route.RunBefore = append(route.RunBefore, Runnable{line, litItem})
}
return set
}
func (set *RouteSubset) Not(path ...string) *RouteSubset {
for i, route := range set.RouteList {
if inStringList(route.Path, path) {
set.RouteList = append(set.RouteList[:i], set.RouteList[i+1:]...)
}
}
return set
}

View File

@ -7,12 +7,6 @@ type RouteImpl struct {
RunBefore []Runnable
}
type RouteGroup struct {
Path string
RouteList []*RouteImpl
RunBefore []Runnable
}
type Runnable struct {
Contents string
Literal bool
@ -31,27 +25,10 @@ func (route *RouteImpl) Before(item string, literal ...bool) *RouteImpl {
return route
}
func newRouteGroup(path string, routes ...*RouteImpl) *RouteGroup {
return &RouteGroup{path, routes, []Runnable{}}
}
func addRouteGroup(routeGroup *RouteGroup) {
routeGroups = append(routeGroups, routeGroup)
}
func (group *RouteGroup) Before(line string, literal ...bool) *RouteGroup {
var litItem bool
if len(literal) > 0 {
litItem = literal[0]
}
group.RunBefore = append(group.RunBefore, Runnable{line, litItem})
return group
}
func (group *RouteGroup) Routes(routes ...*RouteImpl) {
group.RouteList = append(group.RouteList, routes...)
}
func blankRoute() *RouteImpl {
return &RouteImpl{"", "", []string{}, []Runnable{}}
}
@ -74,9 +51,10 @@ func routes() {
addRoute(Route("routeChangeTheme", "/theme/"))
addRoute(Route("routeShowAttachment", "/attachs/", "extra_data"))
// TODO: Reduce the number of Befores. With a new method, perhaps?
reportGroup := newRouteGroup("/report/",
Route("routeReportSubmit", "/report/submit/", "extra_data"),
).Before("MemberOnly")
).Before("MemberOnly").Before("NoBanned").Before("NoSessionMismatch")
addRouteGroup(reportGroup)
topicGroup := newRouteGroup("/topics/",
@ -90,20 +68,19 @@ func routes() {
}
// TODO: Test the email token route
// TODO: Add a BeforeExcept method?
func buildUserRoutes() {
userGroup := newRouteGroup("/user/") //.Before("MemberOnly")
userGroup := newRouteGroup("/user/")
userGroup.Routes(
Route("routeProfile", "/user/").Before("req.URL.Path += extra_data", true),
Route("routeAccountOwnEditCritical", "/user/edit/critical/").Before("MemberOnly"),
Route("routeAccountOwnEditCriticalSubmit", "/user/edit/critical/submit/").Before("MemberOnly"),
Route("routeAccountOwnEditAvatar", "/user/edit/avatar/").Before("MemberOnly"),
Route("routeAccountOwnEditAvatarSubmit", "/user/edit/avatar/submit/").Before("MemberOnly"),
Route("routeAccountOwnEditUsername", "/user/edit/username/").Before("MemberOnly"),
Route("routeAccountOwnEditUsernameSubmit", "/user/edit/username/submit/").Before("MemberOnly"),
Route("routeAccountOwnEditEmail", "/user/edit/email/").Before("MemberOnly"),
Route("routeAccountOwnEditEmailTokenSubmit", "/user/edit/token/", "extra_data").Before("MemberOnly"),
)
Route("routeAccountOwnEditCritical", "/user/edit/critical/"),
Route("routeAccountOwnEditCriticalSubmit", "/user/edit/critical/submit/"),
Route("routeAccountOwnEditAvatar", "/user/edit/avatar/"),
Route("routeAccountOwnEditAvatarSubmit", "/user/edit/avatar/submit/"),
Route("routeAccountOwnEditUsername", "/user/edit/username/"),
Route("routeAccountOwnEditUsernameSubmit", "/user/edit/username/submit/"),
Route("routeAccountOwnEditEmail", "/user/edit/email/"),
Route("routeAccountOwnEditEmailTokenSubmit", "/user/edit/token/", "extra_data"),
).Not("/user/").Before("MemberOnly")
addRouteGroup(userGroup)
}

View File

@ -310,7 +310,34 @@ func SuperModOnly(w http.ResponseWriter, r *http.Request, user User) RouteError
// MemberOnly makes sure that only logged in users can access this route
func MemberOnly(w http.ResponseWriter, r *http.Request, user User) RouteError {
if !user.Loggedin {
return NoPermissions(w, r, user) // TODO: Do an error telling them to login instead?
return LoginRequired(w, r, user)
}
return nil
}
// NoBanned stops any banned users from accessing this route
func NoBanned(w http.ResponseWriter, r *http.Request, user User) RouteError {
if user.IsBanned {
return Banned(w, r, user)
}
return nil
}
func ParseForm(w http.ResponseWriter, r *http.Request, user User) RouteError {
err := r.ParseForm()
if err != nil {
return LocalError("Bad Form", w, r, user)
}
return nil
}
func NoSessionMismatch(w http.ResponseWriter, r *http.Request, user User) RouteError {
err := r.ParseForm()
if err != nil {
return LocalError("Bad Form", w, r, user)
}
if r.FormValue("session") != user.Session {
return SecurityError(w, r, user)
}
return nil
}

View File

@ -77,11 +77,8 @@ func (sBox SettingBox) ParseSetting(sname string, scontent string, stype string,
}
con1, err := strconv.Atoi(cons[0])
if err != nil {
return "Invalid contraint! The constraint field wasn't an integer!"
}
con2, err := strconv.Atoi(cons[1])
if err != nil {
con2, err2 := strconv.Atoi(cons[1])
if err != nil || err2 != nil {
return "Invalid contraint! The constraint field wasn't an integer!"
}

View File

@ -505,11 +505,11 @@ var profile_0 = []byte(`
<div class="rowitem"><h1>Profile</h1></div>
</header>-->
<div id="profile_left_pane" class="rowmenu">
<div class="rowitem avatarRow" style="padding: 0;">
<div class="rowitem avatarRow">
<img src="`)
var profile_1 = []byte(`" class="avatar" />
</div>
<div class="rowitem">`)
<div class="rowitem nameRow">`)
var profile_2 = []byte(`
<span class="profileName">`)
var profile_3 = []byte(`</span>`)

View File

@ -7,10 +7,10 @@
<div class="rowitem"><h1>Profile</h1></div>
</header>-->
<div id="profile_left_pane" class="rowmenu">
<div class="rowitem avatarRow" style="padding: 0;">
<div class="rowitem avatarRow">
<img src="{{.ProfileOwner.Avatar}}" class="avatar" />
</div>
<div class="rowitem">{{/** TODO: Stop inlining this CSS **/}}
<div class="rowitem nameRow">{{/** TODO: Stop inlining this CSS **/}}
<span class="profileName">{{.ProfileOwner.Name}}</span>{{if .ProfileOwner.Tag}}<span class="username" style="float: right;font-weight: normal;">{{.ProfileOwner.Tag}}</span>{{end}}
</div>
<div class="rowitem passive">

View File

@ -703,11 +703,14 @@ select, input, textarea {
display: flex;
}
#profile_left_lane {
margin-left: 8px;
margin-right: 16px;
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);
}
#profile_left_pane {
flex-direction: column;
padding-bottom: 18px;
}
#profile_left_pane .avatarRow {
padding: 24px;
@ -715,6 +718,16 @@ select, input, textarea {
#profile_left_pane .avatar {
border-radius: 80px;
}
#profile_left_pane .nameRow {
display: flex;
flex-direction: column;
margin-left: auto;
margin-right: auto;
}
#profile_right_lane {
width: 100%;
margin-right: 12px;
}
#profile_right_lane .colstack_item {
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);

View File

@ -655,6 +655,7 @@ input, select, textarea {
#profile_left_lane .avatarRow {
overflow: hidden;
max-height: 220px;
padding: 0;
}
#profile_left_lane .avatar {
width: 100%;

View File

@ -703,6 +703,7 @@ button.username {
#profile_left_lane .avatarRow {
overflow: hidden;
max-height: 220px;
padding: 0;
}
#profile_left_lane .avatar {
width: 100%;

View File

@ -543,6 +543,10 @@ button.username {
padding-left: 136px;
}
#profile_left_lane .avatarRow {
padding: 0;
}
/* Media Queries */
@media(min-width: 881px) {

View File

@ -755,6 +755,7 @@ button.username {
#profile_left_lane .avatarRow {
overflow: hidden;
max-height: 220px;
padding: 0;
}
#profile_left_lane .avatar {
width: 100%;

View File

@ -14,6 +14,9 @@ import (
"time"
)
// This is also in reply.go
//var ErrAlreadyLiked = errors.New("This item was already liked by this user")
// ? - Add a TopicMeta struct for *Forums?
type Topic struct {
@ -102,51 +105,70 @@ type TopicsRow struct {
ForumLink string
}
func (topic *Topic) Lock() (err error) {
_, err = stmts.lockTopic.Exec(topic.ID)
// Flush the topic out of the cache
// ? - We do a CacheRemove() here instead of mutating the pointer to avoid creating a race condition
func (topic *Topic) cacheRemove() {
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
}
}
// TODO: Write a test for this
func (topic *Topic) AddReply(uid int) (err error) {
_, err = stmts.addRepliesToTopic.Exec(1, uid, topic.ID)
topic.cacheRemove()
return err
}
func (topic *Topic) Lock() (err error) {
_, err = stmts.lockTopic.Exec(topic.ID)
topic.cacheRemove()
return err
}
func (topic *Topic) Unlock() (err error) {
_, err = stmts.unlockTopic.Exec(topic.ID)
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
}
topic.cacheRemove()
return err
}
// TODO: We might want more consistent terminology rather than using stick in some places and pin in others. If you don't understand the difference, there is none, they are one and the same.
// ? - We do a CacheDelete() here instead of mutating the pointer to avoid creating a race condition
func (topic *Topic) Stick() (err error) {
_, err = stmts.stickTopic.Exec(topic.ID)
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
}
topic.cacheRemove()
return err
}
func (topic *Topic) Unstick() (err error) {
_, err = stmts.unstickTopic.Exec(topic.ID)
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
topic.cacheRemove()
return err
}
// TODO: Test this
// TODO: Use a transaction for this
func (topic *Topic) Like(score int, uid int) (err error) {
var tid int // Unused
err = stmts.hasLikedTopic.QueryRow(uid, topic.ID).Scan(&tid)
if err != nil && err != ErrNoRows {
return err
} else if err != ErrNoRows {
return ErrAlreadyLiked
}
_, err = stmts.createLike.Exec(score, tid, "topics", uid)
if err != nil {
return err
}
_, err = stmts.addLikesToTopic.Exec(1, tid)
topic.cacheRemove()
return err
}
// TODO: Implement this
func (topic *Topic) AddLike(uid int) error {
return nil
}
// TODO: Implement this
func (topic *Topic) RemoveLike(uid int) error {
func (topic *Topic) Unlike(uid int) error {
return nil
}
@ -169,22 +191,16 @@ func (topic *Topic) Delete() error {
}
_, err = stmts.deleteTopic.Exec(topic.ID)
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
}
topic.cacheRemove()
return err
}
func (topic *Topic) Update(name string, content string) error {
content = preparseMessage(content)
parsed_content := parseMessage(html.EscapeString(content), topic.ParentID, "forums")
_, err := stmts.editTopic.Exec(name, content, parsed_content, topic.ID)
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
}
_, err := stmts.editTopic.Exec(name, content, parsed_content, topic.ID)
topic.cacheRemove()
return err
}
@ -194,10 +210,7 @@ func (topic *Topic) CreateActionReply(action string, ipaddress string, user User
return err
}
_, err = stmts.addRepliesToTopic.Exec(1, user.ID, topic.ID)
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
}
topic.cacheRemove()
// ? - Update the last topic cache for the parent forum?
return err
}

View File

@ -199,7 +199,8 @@ func nameToSlug(name string) (slug string) {
return slug
}
func SendEmail(email string, subject string, msg string) (res bool) {
// TODO: Refactor this
func SendEmail(email string, subject string, msg string) bool {
// This hook is useful for plugin_sendmail or for testing tools. Possibly to hook it into some sort of mail server?
if vhooks["email_send_intercept"] != nil {
return vhooks["email_send_intercept"](email, subject, msg).(bool)
@ -208,42 +209,42 @@ func SendEmail(email string, subject string, msg string) (res bool) {
con, err := smtp.Dial(config.SMTPServer + ":" + config.SMTPPort)
if err != nil {
return
return false
}
if config.SMTPUsername != "" {
auth := smtp.PlainAuth("", config.SMTPUsername, config.SMTPPassword, config.SMTPServer)
err = con.Auth(auth)
if err != nil {
return
return false
}
}
err = con.Mail(site.Email)
if err != nil {
return
return false
}
err = con.Rcpt(email)
if err != nil {
return
return false
}
emailData, err := con.Data()
if err != nil {
return
return false
}
_, err = fmt.Fprintf(emailData, body)
if err != nil {
return
return false
}
err = emailData.Close()
if err != nil {
return
return false
}
err = con.Quit()
if err != nil {
return
return false
}
return true
}