diff --git a/.codebeatignore b/.codebeatignore index e78cbe1d..5978505e 100644 --- a/.codebeatignore +++ b/.codebeatignore @@ -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 diff --git a/gen_router.go b/gen_router.go index d3e27aff..1e0de7be 100644 --- a/gen_router.go +++ b/gen_router.go @@ -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) diff --git a/member_routes.go b/member_routes.go index e3888699..e31777b0 100644 --- a/member_routes.go +++ b/member_routes.go @@ -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) diff --git a/misc_test.go b/misc_test.go index 2182994e..1e25b72c 100644 --- a/misc_test.go +++ b/misc_test.go @@ -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)) diff --git a/reply_store.go b/reply_store.go index 40c3ce55..e054b0f2 100644 --- a/reply_store.go +++ b/reply_store.go @@ -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) } diff --git a/router_gen/route_group.go b/router_gen/route_group.go new file mode 100644 index 00000000..330ded1f --- /dev/null +++ b/router_gen/route_group.go @@ -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 +} diff --git a/router_gen/route_subset.go b/router_gen/route_subset.go new file mode 100644 index 00000000..918bcc6c --- /dev/null +++ b/router_gen/route_subset.go @@ -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 +} diff --git a/router_gen/routes.go b/router_gen/routes.go index 45c80b5a..b96cafea 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -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) } diff --git a/routes_common.go b/routes_common.go index 0bdddee6..fc4d908a 100644 --- a/routes_common.go +++ b/routes_common.go @@ -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 } diff --git a/setting.go b/setting.go index 102c287a..d11f2848 100644 --- a/setting.go +++ b/setting.go @@ -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!" } diff --git a/template_list.go b/template_list.go index ae1ce3f7..4d87476b 100644 --- a/template_list.go +++ b/template_list.go @@ -505,11 +505,11 @@ var profile_0 = []byte(`

Profile

-->
-
+
-
`) +
`) var profile_2 = []byte(` `) var profile_3 = []byte(``) diff --git a/templates/profile.html b/templates/profile.html index 1049c878..9d05d84e 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -7,10 +7,10 @@

Profile

-->
-
+
-
{{/** TODO: Stop inlining this CSS **/}} +
{{/** TODO: Stop inlining this CSS **/}} {{.ProfileOwner.Name}}{{if .ProfileOwner.Tag}}{{.ProfileOwner.Tag}}{{end}}
diff --git a/themes/cosora/public/main.css b/themes/cosora/public/main.css index 559096e0..1b921edd 100644 --- a/themes/cosora/public/main.css +++ b/themes/cosora/public/main.css @@ -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); diff --git a/themes/shadow/public/main.css b/themes/shadow/public/main.css index 7b92d6cd..c7ee52eb 100644 --- a/themes/shadow/public/main.css +++ b/themes/shadow/public/main.css @@ -655,6 +655,7 @@ input, select, textarea { #profile_left_lane .avatarRow { overflow: hidden; max-height: 220px; + padding: 0; } #profile_left_lane .avatar { width: 100%; diff --git a/themes/tempra-conflux/public/main.css b/themes/tempra-conflux/public/main.css index 6d1d3894..507c7f38 100644 --- a/themes/tempra-conflux/public/main.css +++ b/themes/tempra-conflux/public/main.css @@ -703,6 +703,7 @@ button.username { #profile_left_lane .avatarRow { overflow: hidden; max-height: 220px; + padding: 0; } #profile_left_lane .avatar { width: 100%; diff --git a/themes/tempra-cursive/public/main.css b/themes/tempra-cursive/public/main.css index 5f27b817..be5d5557 100644 --- a/themes/tempra-cursive/public/main.css +++ b/themes/tempra-cursive/public/main.css @@ -543,6 +543,10 @@ button.username { padding-left: 136px; } +#profile_left_lane .avatarRow { + padding: 0; +} + /* Media Queries */ @media(min-width: 881px) { diff --git a/themes/tempra-simple/public/main.css b/themes/tempra-simple/public/main.css index b2921463..bc694cb3 100644 --- a/themes/tempra-simple/public/main.css +++ b/themes/tempra-simple/public/main.css @@ -755,6 +755,7 @@ button.username { #profile_left_lane .avatarRow { overflow: hidden; max-height: 220px; + padding: 0; } #profile_left_lane .avatar { width: 100%; diff --git a/topic.go b/topic.go index 65fc206d..1bd2953f 100644 --- a/topic.go +++ b/topic.go @@ -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 } diff --git a/utils.go b/utils.go index f79c4041..ff74be1c 100644 --- a/utils.go +++ b/utils.go @@ -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 }