/* * * Gosora Route Handlers * Copyright Azareal 2016 - 2018 * */ package main import ( "log" //"fmt" "bytes" "html" "io" "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"}`) var cacheControlMaxAge = "max-age=" + strconv.Itoa(common.Day) // TODO: Make this a common.Config value // 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) { dest := "https://" + req.Host + req.URL.Path if len(req.URL.RawQuery) > 0 { dest += "?" + req.URL.RawQuery } http.Redirect(w, req, dest, http.StatusTemporaryRedirect) } // GET functions func routeStatic(w http.ResponseWriter, r *http.Request) { file, ok := common.StaticFiles.Get(r.URL.Path) if !ok { if common.Dev.DebugMode { log.Printf("Failed to find '%s'", r.URL.Path) } w.WriteHeader(http.StatusNotFound) return } h := w.Header() // Surely, there's a more efficient way of doing this? t, err := time.Parse(http.TimeFormat, h.Get("If-Modified-Since")) if err == nil && file.Info.ModTime().Before(t.Add(1*time.Second)) { w.WriteHeader(http.StatusNotModified) return } h.Set("Last-Modified", file.FormattedModTime) h.Set("Content-Type", file.Mimetype) h.Set("Cache-Control", cacheControlMaxAge) //Cache-Control: max-age=31536000 h.Set("Vary", "Accept-Encoding") if strings.Contains(h.Get("Accept-Encoding"), "gzip") { h.Set("Content-Encoding", "gzip") h.Set("Content-Length", strconv.FormatInt(file.GzipLength, 10)) io.Copy(w, bytes.NewReader(file.GzipData)) // Use w.Write instead? } else { h.Set("Content-Length", strconv.FormatInt(file.Length, 10)) // Avoid doing a type conversion every time? io.Copy(w, bytes.NewReader(file.Data)) } // Other options instead of io.Copy: io.CopyN(), w.Write(), http.ServeContent() } // TODO: Make this a static file somehow? Is it possible for us to put this file somewhere else? // TODO: Add a sitemap // TODO: Add an API so that plugins can register disallowed areas. E.g. /guilds/join for plugin_guilds func routeRobotsTxt(w http.ResponseWriter, r *http.Request) common.RouteError { _, _ = w.Write([]byte(`User-agent: * Disallow: /panel/ Disallow: /topics/create/ Disallow: /user/edit/ Disallow: /accounts/ `)) return nil } func routeOverview(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 = "overview" pi := common.Page{common.GetTitlePhrase("overview"), user, headerVars, tList, nil} if common.PreRenderHooks["pre_render_overview"] != nil { if common.RunPreRenderHook("pre_render_overview", w, r, &user, &pi) { return nil } } err := common.Templates.ExecuteTemplate(w, "overview.html", pi) if err != nil { return common.InternalError(err, w, r) } return nil } func routeCustomPage(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, ferr := common.UserCheck(w, r, &user) if ferr != nil { return ferr } name := r.URL.Path[len("/pages/"):] if common.Templates.Lookup("page_"+name) == nil { return common.NotFound(w, r) } headerVars.Zone = "custom_page" pi := common.Page{common.GetTitlePhrase("page"), user, headerVars, tList, nil} if common.PreRenderHooks["pre_render_custom_page"] != nil { if common.RunPreRenderHook("pre_render_custom_page", w, r, &user, &pi) { return nil } } err := common.Templates.ExecuteTemplate(w, "page_"+name, pi) if err != nil { return common.InternalError(err, w, r) } return nil } 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" // TODO: Add a function for the qlist stuff var qlist 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? 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 var forumList []common.Forum var argList []interface{} for _, fid := range canSee { forum := common.Forums.DirtyGet(fid) if forum.Name != "" && forum.Active { if forum.ParentType == "" || forum.ParentType == "forum" { // Optimise Quick Topic away for guests if user.Loggedin { fcopy := forum.Copy() // TODO: Add a hook here for plugin_guilds forumList = append(forumList, fcopy) } } // ? - Should we be showing plugin_guilds posts on /topics/? // ? - Would it be useful, if we could post in social groups from /topics/? argList = append(argList, strconv.Itoa(fid)) qlist += "?," } } // ! Need an inline error not a page level error if qlist == "" { return common.NotFound(w, r) } qlist = qlist[0 : len(qlist)-1] topicCountStmt, err := qgen.Builder.SimpleCount("topics", "parentID IN("+qlist+")", "") if err != nil { return common.InternalError(err, w, r) } defer topicCountStmt.Close() var topicCount int err = topicCountStmt.QueryRow(argList...).Scan(&topicCount) if err != nil { return common.InternalError(err, w, r) } // Get the current page page, _ := strconv.Atoi(r.FormValue("page")) // Calculate the offset var offset int lastPage := (topicCount / common.Config.ItemsPerPage) + 1 if page > 1 { offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage } else if page == -1 { page = lastPage offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage } else { page = 1 } 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] } pi := common.TopicsPage{common.GetTitlePhrase("topics"), user, headerVars, topicList, forumList, common.Config.DefaultForum} 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" // Calculate the offset var offset int // TODO: Does forum.TopicCount take the deleted items into consideration for guests? lastPage := (forum.TopicCount / common.Config.ItemsPerPage) + 1 if page > 1 { offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage } else if page == -1 { page = lastPage offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage } else { page = 1 } // 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_view_forum"] != nil { if common.RunPreRenderHook("pre_render_view_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" 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 routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { var err error var page, offset int var replyList []common.ReplyUser page, _ = strconv.Atoi(r.FormValue("page")) // SEO URLs... // TODO: Make a shared function for this halves := strings.Split(r.URL.Path[len("/topic/"):], ".") if len(halves) < 2 { halves = append(halves, halves[0]) } tid, err := strconv.Atoi(halves[1]) if err != nil { return common.PreError("The provided TopicID is not a valid number.", w, r) } // Get the topic... topic, err := common.GetTopicUser(tid) if err == ErrNoRows { return common.NotFound(w, r) } else if err != nil { return common.InternalError(err, w, r) } topic.ClassName = "" //log.Printf("topic: %+v\n", topic) headerVars, ferr := common.ForumUserCheck(w, r, &user, topic.ParentID) if ferr != nil { return ferr } if !user.Perms.ViewTopic { //log.Printf("user.Perms: %+v\n", user.Perms) return common.NoPermissions(w, r, user) } headerVars.Zone = "view_topic" topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums") topic.ContentLines = strings.Count(topic.Content, "\n") // We don't want users posting in locked topics... if topic.IsClosed && !user.IsMod { user.Perms.CreateReply = false } postGroup, err := common.Groups.Get(topic.Group) if err != nil { return common.InternalError(err, w, r) } topic.Tag = postGroup.Tag if postGroup.IsMod || postGroup.IsAdmin { topic.ClassName = common.Config.StaffCSS } topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt) // TODO: Make a function for this? Build a more sophisticated noavatar handling system? if topic.Avatar != "" { if topic.Avatar[0] == '.' { topic.Avatar = "/uploads/avatar_" + strconv.Itoa(topic.CreatedBy) + topic.Avatar } } else { topic.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(topic.CreatedBy), 1) } // Calculate the offset lastPage := (topic.PostCount / common.Config.ItemsPerPage) + 1 if page > 1 { offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage } else if page == -1 { page = lastPage offset = (common.Config.ItemsPerPage * page) - common.Config.ItemsPerPage } else { page = 1 } tpage := common.TopicPage{topic.Title, user, headerVars, replyList, topic, page, lastPage} // Get the replies.. rows, err := stmts.getTopicRepliesOffset.Query(topic.ID, offset, common.Config.ItemsPerPage) if err == ErrNoRows { return common.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) } else if err != nil { return common.InternalError(err, w, r) } defer rows.Close() replyItem := common.ReplyUser{ClassName: ""} for rows.Next() { err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType) if err != nil { return common.InternalError(err, w, r) } replyItem.UserLink = common.BuildProfileURL(common.NameToSlug(replyItem.CreatedByName), replyItem.CreatedBy) replyItem.ParentID = topic.ID replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums") replyItem.ContentLines = strings.Count(replyItem.Content, "\n") postGroup, err = common.Groups.Get(replyItem.Group) if err != nil { return common.InternalError(err, w, r) } if postGroup.IsMod || postGroup.IsAdmin { replyItem.ClassName = common.Config.StaffCSS } else { replyItem.ClassName = "" } // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this? if replyItem.Avatar != "" { if replyItem.Avatar[0] == '.' { replyItem.Avatar = "/uploads/avatar_" + strconv.Itoa(replyItem.CreatedBy) + replyItem.Avatar } } else { replyItem.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(replyItem.CreatedBy), 1) } replyItem.Tag = postGroup.Tag replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt) // We really shouldn't have inline HTML, we should do something about this... if replyItem.ActionType != "" { switch replyItem.ActionType { case "lock": replyItem.ActionType = "This topic has been locked by " + replyItem.CreatedByName + "" replyItem.ActionIcon = "🔒︎" case "unlock": replyItem.ActionType = "This topic has been reopened by " + replyItem.CreatedByName + "" replyItem.ActionIcon = "🔓︎" case "stick": replyItem.ActionType = "This topic has been pinned by " + replyItem.CreatedByName + "" replyItem.ActionIcon = "📌︎" case "unstick": replyItem.ActionType = "This topic has been unpinned by " + replyItem.CreatedByName + "" replyItem.ActionIcon = "📌︎" default: replyItem.ActionType = replyItem.ActionType + " has happened" replyItem.ActionIcon = "" } } replyItem.Liked = false if common.Vhooks["topic_reply_row_assign"] != nil { common.RunVhook("topic_reply_row_assign", &tpage, &replyItem) } replyList = append(replyList, replyItem) } err = rows.Err() if err != nil { return common.InternalError(err, w, r) } tpage.ItemList = replyList if common.PreRenderHooks["pre_render_view_topic"] != nil { if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) { return nil } } err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, 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... 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 common.User ID 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 = "" } if replyAvatar != "" { if replyAvatar[0] == '.' { replyAvatar = "/uploads/avatar_" + strconv.Itoa(replyCreatedBy) + replyAvatar } } else { replyAvatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(replyCreatedBy), 1) } 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 } func routeLogin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, ferr := common.UserCheck(w, r, &user) if ferr != nil { return ferr } if user.Loggedin { return common.LocalError("You're already logged in.", w, r, user) } pi := common.Page{common.GetTitlePhrase("login"), user, headerVars, tList, nil} if common.PreRenderHooks["pre_render_login"] != nil { if common.RunPreRenderHook("pre_render_login", w, r, &user, &pi) { return nil } } err := common.Templates.ExecuteTemplate(w, "login.html", pi) if err != nil { return common.InternalError(err, w, r) } return nil } // TODO: Log failed attempted logins? // TODO: Lock IPS out if they have too many failed attempts? // TODO: Log unusual countries in comparison to the country a user usually logs in from? Alert the user about this? func routeLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { if user.Loggedin { return common.LocalError("You're already logged in.", w, r, user) } err := r.ParseForm() if err != nil { return common.LocalError("Bad Form", w, r, user) } uid, err := common.Auth.Authenticate(html.EscapeString(r.PostFormValue("username")), r.PostFormValue("password")) if err != nil { return common.LocalError(err.Error(), w, r, user) } userPtr, err := common.Users.Get(uid) if err != nil { return common.LocalError("Bad account", w, r, user) } user = *userPtr var session string if user.Session == "" { session, err = common.Auth.CreateSession(uid) if err != nil { return common.InternalError(err, w, r) } } else { session = user.Session } common.Auth.SetCookies(w, uid, session) if user.IsAdmin { // Is this error check redundant? We already check for the error in PreRoute for the same IP // TODO: Should we be logging this? log.Printf("#%d has logged in with IP %s", uid, user.LastIP) } http.Redirect(w, r, "/", http.StatusSeeOther) return nil } func routeRegister(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, ferr := common.UserCheck(w, r, &user) if ferr != nil { return ferr } if user.Loggedin { return common.LocalError("You're already logged in.", w, r, user) } pi := common.Page{common.GetTitlePhrase("register"), user, headerVars, tList, nil} if common.PreRenderHooks["pre_render_register"] != nil { if common.RunPreRenderHook("pre_render_register", w, r, &user, &pi) { return nil } } err := common.Templates.ExecuteTemplate(w, "register.html", pi) if err != nil { return common.InternalError(err, w, r) } return nil } func routeRegisterSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerLite, _ := common.SimpleUserCheck(w, r, &user) err := r.ParseForm() if err != nil { return common.LocalError("Bad Form", w, r, user) } username := html.EscapeString(r.PostFormValue("username")) if username == "" { return common.LocalError("You didn't put in a username.", w, r, user) } email := html.EscapeString(r.PostFormValue("email")) if email == "" { return common.LocalError("You didn't put in an email.", w, r, user) } password := r.PostFormValue("password") switch password { case "": return common.LocalError("You didn't put in a password.", w, r, user) case username: return common.LocalError("You can't use your username as your password.", w, r, user) case email: return common.LocalError("You can't use your email as your password.", w, r, user) } err = common.WeakPassword(password) if err != nil { return common.LocalError(err.Error(), w, r, user) } confirmPassword := r.PostFormValue("confirm_password") log.Print("Registration Attempt! common.Username: " + username) // TODO: Add more controls over what is logged when? // Do the two inputted passwords match..? if password != confirmPassword { return common.LocalError("The two passwords don't match.", w, r, user) } var active bool var group int switch headerLite.Settings["activation_type"] { case 1: // Activate All active = true group = common.Config.DefaultGroup default: // Anything else. E.g. Admin Activation or Email Activation. group = common.Config.ActivationGroup } uid, err := common.Users.Create(username, password, email, group, active) if err == common.ErrAccountExists { return common.LocalError("This username isn't available. Try another.", w, r, user) } else if err != nil { return common.InternalError(err, w, r) } // Check if this user actually owns this email, if email activation is on, automatically flip their account to active when the email is validated. Validation is also useful for determining whether this user should receive any alerts, etc. via email if common.Site.EnableEmails { token, err := common.GenerateSafeString(80) if err != nil { return common.InternalError(err, w, r) } _, err = stmts.addEmail.Exec(email, uid, 0, token) if err != nil { return common.InternalError(err, w, r) } if !common.SendValidationEmail(username, email, token) { return common.LocalError("We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.", w, r, user) } } session, err := common.Auth.CreateSession(uid) if err != nil { return common.InternalError(err, w, r) } common.Auth.SetCookies(w, uid, session) http.Redirect(w, r, "/", http.StatusSeeOther) 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 }