017bce9c09
More progress on poll posts. Improved the poll UI on the other themes. De-duplicated some avatar logic. Added Exec() to accInsertBuilder. Moved routeOverview to routes.Overview Moved routeCustomPage to routes.CustomPage Moved routeTopicID to routes.ViewTopic Moved routeLogin to routes.AccountLogin Moved routeRegister to routes.AccountRegister Moved routeLoginSubmit to routes.AccountLoginSubmit Moved routeRegisterSubmit to routes.AccountRegisterSubmit We now track the Android Chrome user agent. We now track the UCBrowser user agent. We now track the zgrab user agent. Fixed a few cases where Googlebot wasn't tracked properly. Moved routeStatic to routes.StaticFile
539 lines
16 KiB
Go
539 lines
16 KiB
Go
/*
|
|
*
|
|
* Gosora Route Handlers
|
|
* Copyright Azareal 2016 - 2018
|
|
*
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"html"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"./common"
|
|
"./query_gen/lib"
|
|
)
|
|
|
|
// A blank list to fill out that parameter in Page for routes which don't use it
|
|
var tList []interface{}
|
|
|
|
//var nList []string
|
|
var successJSONBytes = []byte(`{"success":"1"}`)
|
|
|
|
// HTTPSRedirect is a connection handler which redirects all HTTP requests to HTTPS
|
|
type HTTPSRedirect struct {
|
|
}
|
|
|
|
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Connection", "close")
|
|
dest := "https://" + req.Host + req.URL.Path
|
|
if len(req.URL.RawQuery) > 0 {
|
|
dest += "?" + req.URL.RawQuery
|
|
}
|
|
http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
// Temporary stubs for view tracking
|
|
func routeDynamic() {
|
|
}
|
|
func routeUploads() {
|
|
}
|
|
func BadRoute() {
|
|
}
|
|
|
|
func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
|
|
headerVars, ferr := common.UserCheck(w, r, &user)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
headerVars.Zone = "topics"
|
|
headerVars.MetaDesc = headerVars.Settings["meta_desc"].(string)
|
|
|
|
group, err := common.Groups.Get(user.Group)
|
|
if err != nil {
|
|
log.Printf("Group #%d doesn't exist despite being used by common.User #%d", user.Group, user.ID)
|
|
return common.LocalError("Something weird happened", w, r, user)
|
|
}
|
|
|
|
// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
|
|
var canSee []int
|
|
if user.IsSuperAdmin {
|
|
canSee, err = common.Forums.GetAllVisibleIDs()
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
} else {
|
|
canSee = group.CanSee
|
|
}
|
|
|
|
// We need a list of the visible forums for Quick Topic
|
|
// ? - Would it be useful, if we could post in social groups from /topics/?
|
|
var forumList []common.Forum
|
|
for _, fid := range canSee {
|
|
forum := common.Forums.DirtyGet(fid)
|
|
if forum.Name != "" && forum.Active && (forum.ParentType == "" || forum.ParentType == "forum") {
|
|
fcopy := forum.Copy()
|
|
// TODO: Add a hook here for plugin_guilds
|
|
forumList = append(forumList, fcopy)
|
|
}
|
|
}
|
|
|
|
// ? - Should we be showing plugin_guilds posts on /topics/?
|
|
argList, qlist := common.ForumListToArgQ(forumList)
|
|
|
|
// ! Need an inline error not a page level error
|
|
if qlist == "" {
|
|
return common.NotFound(w, r)
|
|
}
|
|
|
|
topicCount, err := common.ArgQToTopicCount(argList, qlist)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
// Get the current page
|
|
page, _ := strconv.Atoi(r.FormValue("page"))
|
|
offset, page, lastPage := common.PageOffset(topicCount, page, common.Config.ItemsPerPage)
|
|
|
|
var topicList []*common.TopicsRow
|
|
stmt, err := qgen.Builder.SimpleSelect("topics", "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, postCount, likeCount", "parentID IN("+qlist+")", "sticky DESC, lastReplyAt DESC, createdBy DESC", "?,?")
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
defer stmt.Close()
|
|
|
|
argList = append(argList, offset)
|
|
argList = append(argList, common.Config.ItemsPerPage)
|
|
|
|
rows, err := stmt.Query(argList...)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var reqUserList = make(map[int]bool)
|
|
for rows.Next() {
|
|
topicItem := common.TopicsRow{ID: 0}
|
|
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.PostCount, &topicItem.LikeCount)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
|
|
|
|
forum := common.Forums.DirtyGet(topicItem.ParentID)
|
|
topicItem.ForumName = forum.Name
|
|
topicItem.ForumLink = forum.Link
|
|
|
|
//topicItem.CreatedAt = common.RelativeTime(topicItem.CreatedAt)
|
|
topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
|
|
|
|
if common.Vhooks["topics_topic_row_assign"] != nil {
|
|
common.RunVhook("topics_topic_row_assign", &topicItem, &forum)
|
|
}
|
|
topicList = append(topicList, &topicItem)
|
|
reqUserList[topicItem.CreatedBy] = true
|
|
reqUserList[topicItem.LastReplyBy] = true
|
|
}
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
// Convert the user ID map to a slice, then bulk load the users
|
|
var idSlice = make([]int, len(reqUserList))
|
|
var i int
|
|
for userID := range reqUserList {
|
|
idSlice[i] = userID
|
|
i++
|
|
}
|
|
|
|
// TODO: What if a user is deleted via the Control Panel?
|
|
userList, err := common.Users.BulkGetMap(idSlice)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
// Second pass to the add the user data
|
|
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
|
|
for _, topicItem := range topicList {
|
|
topicItem.Creator = userList[topicItem.CreatedBy]
|
|
topicItem.LastUser = userList[topicItem.LastReplyBy]
|
|
}
|
|
|
|
pageList := common.Paginate(topicCount, common.Config.ItemsPerPage, 5)
|
|
pi := common.TopicsPage{common.GetTitlePhrase("topics"), user, headerVars, topicList, forumList, common.Config.DefaultForum, pageList, page, lastPage}
|
|
if common.PreRenderHooks["pre_render_topic_list"] != nil {
|
|
if common.RunPreRenderHook("pre_render_topic_list", w, r, &user, &pi) {
|
|
return nil
|
|
}
|
|
}
|
|
err = common.RunThemeTemplate(headerVars.Theme.Name, "topics", pi, w)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError {
|
|
page, _ := strconv.Atoi(r.FormValue("page"))
|
|
|
|
// SEO URLs...
|
|
halves := strings.Split(sfid, ".")
|
|
if len(halves) < 2 {
|
|
halves = append(halves, halves[0])
|
|
}
|
|
fid, err := strconv.Atoi(halves[1])
|
|
if err != nil {
|
|
return common.PreError("The provided ForumID is not a valid number.", w, r)
|
|
}
|
|
|
|
headerVars, ferr := common.ForumUserCheck(w, r, &user, fid)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
if !user.Perms.ViewTopic {
|
|
return common.NoPermissions(w, r, user)
|
|
}
|
|
|
|
// TODO: Fix this double-check
|
|
forum, err := common.Forums.Get(fid)
|
|
if err == ErrNoRows {
|
|
return common.NotFound(w, r)
|
|
} else if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
headerVars.Zone = "view_forum"
|
|
|
|
// TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete
|
|
offset, page, lastPage := common.PageOffset(forum.TopicCount, page, common.Config.ItemsPerPage)
|
|
|
|
// TODO: Move this to *Forum
|
|
rows, err := stmts.getForumTopicsOffset.Query(fid, offset, common.Config.ItemsPerPage)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
defer rows.Close()
|
|
|
|
// TODO: Use something other than TopicsRow as we don't need to store the forum name and link on each and every topic item?
|
|
var topicList []*common.TopicsRow
|
|
var reqUserList = make(map[int]bool)
|
|
for rows.Next() {
|
|
var topicItem = common.TopicsRow{ID: 0}
|
|
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.PostCount, &topicItem.LikeCount)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
|
|
topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
|
|
|
|
if common.Vhooks["forum_trow_assign"] != nil {
|
|
common.RunVhook("forum_trow_assign", &topicItem, &forum)
|
|
}
|
|
topicList = append(topicList, &topicItem)
|
|
reqUserList[topicItem.CreatedBy] = true
|
|
reqUserList[topicItem.LastReplyBy] = true
|
|
}
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
// Convert the user ID map to a slice, then bulk load the users
|
|
var idSlice = make([]int, len(reqUserList))
|
|
var i int
|
|
for userID := range reqUserList {
|
|
idSlice[i] = userID
|
|
i++
|
|
}
|
|
|
|
// TODO: What if a user is deleted via the Control Panel?
|
|
userList, err := common.Users.BulkGetMap(idSlice)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
// Second pass to the add the user data
|
|
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
|
|
for _, topicItem := range topicList {
|
|
topicItem.Creator = userList[topicItem.CreatedBy]
|
|
topicItem.LastUser = userList[topicItem.LastReplyBy]
|
|
}
|
|
|
|
pi := common.ForumPage{forum.Name, user, headerVars, topicList, forum, page, lastPage}
|
|
if common.PreRenderHooks["pre_render_forum"] != nil {
|
|
if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) {
|
|
return nil
|
|
}
|
|
}
|
|
err = common.RunThemeTemplate(headerVars.Theme.Name, "forum", pi, w)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func routeForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
|
|
headerVars, ferr := common.UserCheck(w, r, &user)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
headerVars.Zone = "forums"
|
|
headerVars.MetaDesc = headerVars.Settings["meta_desc"].(string)
|
|
|
|
var err error
|
|
var forumList []common.Forum
|
|
var canSee []int
|
|
if user.IsSuperAdmin {
|
|
canSee, err = common.Forums.GetAllVisibleIDs()
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
} else {
|
|
group, err := common.Groups.Get(user.Group)
|
|
if err != nil {
|
|
log.Printf("Group #%d doesn't exist despite being used by common.User #%d", user.Group, user.ID)
|
|
return common.LocalError("Something weird happened", w, r, user)
|
|
}
|
|
canSee = group.CanSee
|
|
}
|
|
|
|
for _, fid := range canSee {
|
|
// Avoid data races by copying the struct into something we can freely mold without worrying about breaking something somewhere else
|
|
var forum = common.Forums.DirtyGet(fid).Copy()
|
|
if forum.ParentID == 0 && forum.Name != "" && forum.Active {
|
|
if forum.LastTopicID != 0 {
|
|
if forum.LastTopic.ID != 0 && forum.LastReplyer.ID != 0 {
|
|
forum.LastTopicTime = common.RelativeTime(forum.LastTopic.LastReplyAt)
|
|
} else {
|
|
forum.LastTopicTime = ""
|
|
}
|
|
} else {
|
|
forum.LastTopicTime = ""
|
|
}
|
|
if common.Hooks["forums_frow_assign"] != nil {
|
|
common.RunHook("forums_frow_assign", &forum)
|
|
}
|
|
forumList = append(forumList, forum)
|
|
}
|
|
}
|
|
|
|
pi := common.ForumsPage{common.GetTitlePhrase("forums"), user, headerVars, forumList}
|
|
if common.PreRenderHooks["pre_render_forum_list"] != nil {
|
|
if common.RunPreRenderHook("pre_render_forum_list", w, r, &user, &pi) {
|
|
return nil
|
|
}
|
|
}
|
|
err = common.RunThemeTemplate(headerVars.Theme.Name, "forums", pi, w)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
|
|
headerVars, ferr := common.UserCheck(w, r, &user)
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
|
|
var err error
|
|
var replyCreatedAt time.Time
|
|
var replyContent, replyCreatedByName, replyRelativeCreatedAt, replyAvatar, replyTag, replyClassName string
|
|
var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int
|
|
var replyList []common.ReplyUser
|
|
|
|
// SEO URLs...
|
|
// TODO: Do a 301 if it's the wrong username? Do a canonical too?
|
|
halves := strings.Split(r.URL.Path[len("/user/"):], ".")
|
|
if len(halves) < 2 {
|
|
halves = append(halves, halves[0])
|
|
}
|
|
|
|
pid, err := strconv.Atoi(halves[1])
|
|
if err != nil {
|
|
return common.LocalError("The provided UserID is not a valid number.", w, r, user)
|
|
}
|
|
|
|
var puser *common.User
|
|
if pid == user.ID {
|
|
user.IsMod = true
|
|
puser = &user
|
|
} else {
|
|
// Fetch the user data
|
|
// TODO: Add a shared function for checking for ErrNoRows and internal erroring if it's not that case?
|
|
puser, err = common.Users.Get(pid)
|
|
if err == ErrNoRows {
|
|
return common.NotFound(w, r)
|
|
} else if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
}
|
|
|
|
// Get the replies..
|
|
rows, err := stmts.getProfileReplies.Query(puser.ID)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
err := rows.Scan(&rid, &replyContent, &replyCreatedBy, &replyCreatedAt, &replyLastEdit, &replyLastEditBy, &replyAvatar, &replyCreatedByName, &replyGroup)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
group, err := common.Groups.Get(replyGroup)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
replyLines = strings.Count(replyContent, "\n")
|
|
if group.IsMod || group.IsAdmin {
|
|
replyClassName = common.Config.StaffCSS
|
|
} else {
|
|
replyClassName = ""
|
|
}
|
|
replyAvatar = common.BuildAvatar(replyCreatedBy, replyAvatar)
|
|
|
|
if group.Tag != "" {
|
|
replyTag = group.Tag
|
|
} else if puser.ID == replyCreatedBy {
|
|
replyTag = "Profile Owner"
|
|
} else {
|
|
replyTag = ""
|
|
}
|
|
|
|
replyLiked := false
|
|
replyLikeCount := 0
|
|
replyRelativeCreatedAt = common.RelativeTime(replyCreatedAt)
|
|
|
|
// TODO: Add a hook here
|
|
|
|
replyList = append(replyList, common.ReplyUser{rid, puser.ID, replyContent, common.ParseMessage(replyContent, 0, ""), replyCreatedBy, common.BuildProfileURL(common.NameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyRelativeCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
|
|
}
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
|
|
// TODO: Add a phrase for this title
|
|
ppage := common.ProfilePage{puser.Name + "'s Profile", user, headerVars, replyList, *puser}
|
|
if common.PreRenderHooks["pre_render_profile"] != nil {
|
|
if common.RunPreRenderHook("pre_render_profile", w, r, &user, &ppage) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
err = common.RunThemeTemplate(headerVars.Theme.Name, "profile", ppage, w)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TODO: Set the cookie domain
|
|
func routeChangeTheme(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
|
|
//headerLite, _ := SimpleUserCheck(w, r, &user)
|
|
// TODO: Rename isJs to something else, just in case we rewrite the JS side in WebAssembly?
|
|
isJs := (r.PostFormValue("isJs") == "1")
|
|
newTheme := html.EscapeString(r.PostFormValue("newTheme"))
|
|
|
|
theme, ok := common.Themes[newTheme]
|
|
if !ok || theme.HideFromThemes {
|
|
return common.LocalErrorJSQ("That theme doesn't exist", w, r, user, isJs)
|
|
}
|
|
|
|
cookie := http.Cookie{Name: "current_theme", Value: newTheme, Path: "/", MaxAge: common.Year}
|
|
http.SetCookie(w, &cookie)
|
|
|
|
if !isJs {
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
} else {
|
|
_, _ = w.Write(successJSONBytes)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TODO: Refactor this
|
|
var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}]}`)
|
|
|
|
// TODO: Refactor this endpoint
|
|
func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
|
|
// TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
return common.PreErrorJS("Bad Form", w, r)
|
|
}
|
|
|
|
action := r.FormValue("action")
|
|
if action != "get" && action != "set" {
|
|
return common.PreErrorJS("Invalid Action", w, r)
|
|
}
|
|
|
|
module := r.FormValue("module")
|
|
switch module {
|
|
case "dismiss-alert":
|
|
asid, err := strconv.Atoi(r.FormValue("asid"))
|
|
if err != nil {
|
|
return common.PreErrorJS("Invalid asid", w, r)
|
|
}
|
|
_, err = stmts.deleteActivityStreamMatch.Exec(user.ID, asid)
|
|
if err != nil {
|
|
return common.InternalError(err, w, r)
|
|
}
|
|
case "alerts": // A feed of events tailored for a specific user
|
|
if !user.Loggedin {
|
|
w.Write(phraseLoginAlerts)
|
|
return nil
|
|
}
|
|
|
|
var msglist, event, elementType string
|
|
var asid, actorID, targetUserID, elementID int
|
|
var msgCount int
|
|
|
|
err = stmts.getActivityCountByWatcher.QueryRow(user.ID).Scan(&msgCount)
|
|
if err == ErrNoRows {
|
|
return common.PreErrorJS("Couldn't find the parent topic", w, r)
|
|
} else if err != nil {
|
|
return common.InternalErrorJS(err, w, r)
|
|
}
|
|
|
|
rows, err := stmts.getActivityFeedByWatcher.Query(user.ID)
|
|
if err != nil {
|
|
return common.InternalErrorJS(err, w, r)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
err = rows.Scan(&asid, &actorID, &targetUserID, &event, &elementType, &elementID)
|
|
if err != nil {
|
|
return common.InternalErrorJS(err, w, r)
|
|
}
|
|
res, err := buildAlert(asid, event, elementType, actorID, targetUserID, elementID, user)
|
|
if err != nil {
|
|
return common.LocalErrorJS(err.Error(), w, r)
|
|
}
|
|
msglist += res + ","
|
|
}
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return common.InternalErrorJS(err, w, r)
|
|
}
|
|
|
|
if len(msglist) != 0 {
|
|
msglist = msglist[0 : len(msglist)-1]
|
|
}
|
|
_, _ = w.Write([]byte(`{"msgs":[` + msglist + `],"msgCount":` + strconv.Itoa(msgCount) + `}`))
|
|
default:
|
|
return common.PreErrorJS("Invalid Module", w, r)
|
|
}
|
|
return nil
|
|
}
|