gosora/routes.go
Azareal 017bce9c09 Replaced the user agent parser with a faster and more flexible one.
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
2018-02-03 05:47:14 +00:00

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
}