package controllers import ( "bytes" "fmt" "html/template" "net/http" "strconv" "strings" "git.kirsle.net/apps/gophertype/pkg/authentication" "git.kirsle.net/apps/gophertype/pkg/glue" "git.kirsle.net/apps/gophertype/pkg/mail" "git.kirsle.net/apps/gophertype/pkg/models" "git.kirsle.net/apps/gophertype/pkg/responses" "git.kirsle.net/apps/gophertype/pkg/session" "github.com/albrow/forms" uuid "github.com/satori/go.uuid" ) func init() { glue.Register(glue.Endpoint{ Path: "/comments", Methods: []string{"GET", "POST"}, Handler: PostComment, }) glue.Register(glue.Endpoint{ Path: "/comments/subscription", Methods: []string{"GET", "POST"}, Handler: ManageSubscription, }) glue.Register(glue.Endpoint{ Path: "/comments/quick-delete", Methods: []string{"GET"}, Handler: CommentQuickDelete, }) } // RenderComments returns the partial comments HTML to embed on a page. func RenderComments(w http.ResponseWriter, r *http.Request, subject string, ids ...string) template.HTML { thread := strings.Join(ids, "-") return renderComments(w, r, subject, thread, false) } // RenderCommentsRO returns a read-only comment view. func RenderCommentsRO(w http.ResponseWriter, r *http.Request, ids ...string) template.HTML { thread := strings.Join(ids, "-") return renderComments(w, r, "", thread, true) } // RenderComment renders the HTML partial for a single comment in the thread. func RenderComment(w http.ResponseWriter, r *http.Request, com models.Comment, originURL string, editable bool) template.HTML { var ( html = bytes.NewBuffer([]byte{}) v = responses.NewTemplateVars(html, r) editToken = getEditToken(w, r) ) v.V["Comment"] = com v.V["Editable"] = editable && (com.EditToken == editToken || authentication.LoggedIn(r)) v.V["OriginURL"] = originURL responses.PartialTemplate(html, r, "_builtin/comments/entry.partial.gohtml", v) return template.HTML(html.String()) } // RenderCommentForm renders the comment entry form HTML onto a page. func RenderCommentForm(r *http.Request, com models.Comment, subject, threadID, originURL string) template.HTML { var ( html = bytes.NewBuffer([]byte{}) v = responses.NewTemplateVars(html, r) ) v.V["Comment"] = com v.V["Subject"] = subject v.V["ThreadID"] = threadID v.V["OriginURL"] = originURL responses.PartialTemplate(html, r, "_builtin/comments/form.partial.gohtml", v) return template.HTML(html.String()) } // renderComments is the internal logic for both RenderComments and RenderCommentsRO. func renderComments(w http.ResponseWriter, r *http.Request, subject string, thread string, readonly bool) template.HTML { var ( html = bytes.NewBuffer([]byte{}) v = responses.NewTemplateVars(w, r) ses = session.Get(r) ) comments, err := models.Comments.GetThread(thread) if err != nil { return template.HTML(fmt.Sprintf("[comment error: %s]", err)) } // Load their cached name and email from any previous comments the user posted. name, _ := ses.Values["c.name"].(string) email, _ := ses.Values["c.email"].(string) editToken, _ := ses.Values["c.token"].(string) // Logged in? Populate defaults from the user info. if currentUser, err := authentication.CurrentUser(r); err == nil { name = currentUser.Name email = currentUser.Email } // v.V["posts"] = posts.Posts v.V["Readonly"] = readonly v.V["Subject"] = subject v.V["ThreadID"] = thread v.V["Comments"] = comments v.V["OriginURL"] = r.URL.Path v.V["NewComment"] = models.Comment{ Name: name, Email: email, EditToken: editToken, } responses.PartialTemplate(html, r, "_builtin/comments/comments.partial.gohtml", v) return template.HTML(html.String()) } // ReadComments handles the GET /comments for viewing recently added comments // side wide. func ReadComments(w http.ResponseWriter, r *http.Request) { var ( v = responses.NewTemplateVars(w, r) query = r.URL.Query() ) // Query parameters. page, _ := strconv.Atoi(query.Get("page")) perPage, _ := strconv.Atoi(query.Get("per_page")) // Get the comments. comments, err := models.Comments.GetRecent(page, perPage) if err != nil { responses.Error(w, r, http.StatusInternalServerError, err.Error()) return } v.V["PagedComments"] = comments responses.RenderTemplate(w, r, "_builtin/comments/recent.gohtml", v) } // PostComment handles all of the top-level blog index routes // for Post, Preview, Delete comments. func PostComment(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { ReadComments(w, r) return } var ( v = responses.NewTemplateVars(w, r) editToken = getEditToken(w, r) comment = models.Comments.New() ses = session.Get(r) editing bool // true if editing an existing comment ) // Check if the user is logged in. var loggedIn bool currentUser, err := authentication.CurrentUser(r) loggedIn = err == nil // Get form parameters. form, _ := forms.Parse(r) v.V["Subject"] = form.Get("subject") v.V["ThreadID"] = form.Get("thread") v.V["OriginURL"] = form.Get("origin") // Are they editing an existing post ID? if id := form.GetInt("id"); id > 0 { // Load the comment from DB. com, err := models.Comments.Load(id) if err != nil { responses.NotFound(w, r) return } // Verify the user's EditToken matches this comment. if editToken != com.EditToken && !loggedIn { responses.Forbidden(w, r, "You do not have permission to edit that comment.") return } editing = true comment = com } comment.EditToken = editToken for { // Validate form parameters. val := form.Validator() val.Require("body") if !form.GetBool("editing") { comment.ParseForm(form) } if val.HasErrors() { v.ValidationError = val.ErrorMap() break } // Cache their name and email in their session, for future requests. ses.Values["c.email"] = form.Get("email") ses.Values["c.name"] = form.Get("name") ses.Save(r, w) switch form.Get("submit") { case "delete": v.V["deleting"] = true case "confirm-delete": // Delete the comment. err := comment.Delete() if err != nil { session.Flash(w, r, "Error deleting the comment: %s", err) } else { session.Flash(w, r, "Comment has been deleted!") } responses.Redirect(w, r, form.Get("origin")) return case "preview": v.V["preview"] = comment.HTML() case "post": // If we're logged in, tag our user ID with this post. if loggedIn && !editing { comment.UserID = currentUser.ID } // Store the OriginURL for this comment. comment.OriginURL = form.Get("origin") // Post their comment. err := comment.Save() if err != nil { session.Flash(w, r, "Error posting comment: %s", err) responses.Redirect(w, r, form.Get("origin")) return } // Notify site admins and subscribers by email. go mail.NotifyComment(v.V["Subject"].(string), v.V["OriginURL"].(string), comment) session.Flash(w, r, "Your comment has been added!") responses.Redirect(w, r, form.Get("origin")) return } break } v.V["NewComment"] = comment responses.RenderTemplate(w, r, "_builtin/comments/preview.gohtml", v) } // ManageSubscription helps users unsubscribe from comment threads. func ManageSubscription(w http.ResponseWriter, r *http.Request) { var ( v = responses.NewTemplateVars(w, r) query = r.URL.Query() qThread = query.Get("t") qEmail = strings.ToLower(strings.TrimSpace(query.Get("e"))) qAll = query.Get("all") ) // Are we unsubscribing from all? if qEmail != "" && qAll != "" { err := models.Comments.UnsubscribeFromAll(qEmail) if err != nil { session.Flash(w, r, "Error unsubscribing you: %s", err) } else { session.Flash(w, r, "Success: '%s' has been unsubscribed from ALL comment threads.", qEmail) } responses.Redirect(w, r, "/comments/subscription") return } // Is there a thread and email in the query string? if qThread != "" && qEmail != "" { err := models.Comments.UnsubscribeThread(qThread, qEmail) if err != nil { session.Flash(w, r, "Error unsubscribing you: %s", err) } else { session.Flash(w, r, "Success: '%s' has been unsubscribed from this comment thread.", qEmail) } responses.Redirect(w, r, "/comments/subscription") return } responses.RenderTemplate(w, r, "_builtin/comments/subscription.gohtml", v) } // CommentQuickDelete handles quick-delete links to remove spam comments. func CommentQuickDelete(w http.ResponseWriter, r *http.Request) { // Query parameters. var ( deleteToken = r.URL.Query().Get("d") nextURL = r.URL.Query().Get("next") ) // Look up the comment by thread and quick-delete token. comment, err := models.Comments.LoadByDeleteToken(deleteToken) if err != nil { responses.Forbidden(w, r, "Comment by Delete Token not found.") return } comment.Delete() session.Flash(w, r, "Comment has been quick-deleted!") if nextURL == "" { nextURL = "/" } responses.Redirect(w, r, nextURL) } // getEditToken gets the edit token from the user's session. func getEditToken(w http.ResponseWriter, r *http.Request) string { ses := session.Get(r) if token, ok := ses.Values["c.token"].(string); ok && len(token) > 0 { return token } token := uuid.NewV4().String() ses.Values["c.token"] = token ses.Save(r, w) return token }