2017-11-26 23:53:10 +00:00
|
|
|
package core
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2017-11-27 02:52:14 +00:00
|
|
|
"errors"
|
2017-11-26 23:53:10 +00:00
|
|
|
"html/template"
|
|
|
|
"net/http"
|
2017-11-27 02:52:14 +00:00
|
|
|
"net/mail"
|
2017-11-26 23:53:10 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/gorilla/sessions"
|
|
|
|
"github.com/kirsle/blog/core/models/comments"
|
|
|
|
"github.com/kirsle/blog/core/models/users"
|
|
|
|
)
|
|
|
|
|
|
|
|
// CommentRoutes attaches the comment routes to the app.
|
|
|
|
func (b *Blog) CommentRoutes(r *mux.Router) {
|
|
|
|
r.HandleFunc("/comments", b.CommentHandler)
|
2017-11-27 02:52:14 +00:00
|
|
|
r.HandleFunc("/comments/subscription", b.SubscriptionHandler)
|
|
|
|
r.HandleFunc("/comments/quick-delete", b.QuickDeleteHandler)
|
2017-11-26 23:53:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CommentMeta is the template variables for comment threads.
|
|
|
|
type CommentMeta struct {
|
2017-11-27 02:52:14 +00:00
|
|
|
NewComment comments.Comment
|
|
|
|
ID string
|
|
|
|
OriginURL string // URL where original comment thread appeared
|
|
|
|
Subject string // email subject
|
|
|
|
Thread *comments.Thread
|
|
|
|
Authors map[int]*users.User
|
|
|
|
CSRF string
|
2017-11-26 23:53:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// RenderComments renders a comment form partial and returns the HTML.
|
|
|
|
func (b *Blog) RenderComments(session *sessions.Session, csrfToken, url, subject string, ids ...string) template.HTML {
|
|
|
|
id := strings.Join(ids, "-")
|
|
|
|
|
|
|
|
// Load their cached name and email if they posted a comment before.
|
|
|
|
name, _ := session.Values["c.name"].(string)
|
|
|
|
email, _ := session.Values["c.email"].(string)
|
|
|
|
editToken, _ := session.Values["c.token"].(string)
|
|
|
|
|
|
|
|
// Check if the user is a logged-in admin, to make all comments editable.
|
|
|
|
var isAdmin bool
|
|
|
|
var isAuthenticated bool
|
|
|
|
if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
|
|
|
|
isAuthenticated = true
|
|
|
|
if userID, ok := session.Values["user-id"].(int); ok {
|
|
|
|
if user, err := users.Load(userID); err == nil {
|
|
|
|
isAdmin = user.Admin
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
thread, err := comments.Load(id)
|
|
|
|
if err != nil {
|
|
|
|
thread = comments.New(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Render all the comments in the thread.
|
|
|
|
userMap := map[int]*users.User{}
|
|
|
|
for _, c := range thread.Comments {
|
|
|
|
c.HTML = template.HTML(b.RenderMarkdown(c.Body))
|
|
|
|
c.ThreadID = thread.ID
|
|
|
|
c.OriginURL = url
|
|
|
|
c.CSRF = csrfToken
|
|
|
|
|
|
|
|
// Look up the author username.
|
|
|
|
if c.UserID > 0 {
|
|
|
|
if _, ok := userMap[c.UserID]; !ok {
|
2017-12-23 22:48:47 +00:00
|
|
|
if user, err2 := users.Load(c.UserID); err2 == nil {
|
2017-11-26 23:53:10 +00:00
|
|
|
userMap[c.UserID] = user
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if user, ok := userMap[c.UserID]; ok {
|
|
|
|
c.Name = user.Name
|
|
|
|
c.Username = user.Username
|
|
|
|
c.Email = user.Email
|
|
|
|
c.LoadAvatar()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Is it editable?
|
|
|
|
if isAdmin || (len(c.EditToken) > 0 && c.EditToken == editToken) {
|
|
|
|
c.Editable = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the template snippet.
|
|
|
|
filepath, err := b.ResolvePath("comments/comments.partial")
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err.Error())
|
|
|
|
return template.HTML("[error: missing comments/comments.partial]")
|
|
|
|
}
|
|
|
|
|
|
|
|
// And the comment view partial.
|
|
|
|
entryPartial, err := b.ResolvePath("comments/entry.partial")
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err.Error())
|
|
|
|
return template.HTML("[error: missing comments/entry.partial]")
|
|
|
|
}
|
|
|
|
|
|
|
|
t := template.New("comments.partial.gohtml")
|
|
|
|
t, err = t.ParseFiles(entryPartial.Absolute, filepath.Absolute)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Failed to parse comments.partial: %s", err.Error())
|
|
|
|
return template.HTML("[error parsing template in comments/comments.partial]")
|
|
|
|
}
|
|
|
|
|
|
|
|
v := CommentMeta{
|
2017-11-27 02:52:14 +00:00
|
|
|
ID: thread.ID,
|
|
|
|
OriginURL: url,
|
|
|
|
Subject: subject,
|
|
|
|
CSRF: csrfToken,
|
|
|
|
Thread: &thread,
|
|
|
|
NewComment: comments.Comment{
|
|
|
|
Name: name,
|
|
|
|
Email: email,
|
|
|
|
IsAuthenticated: isAuthenticated,
|
|
|
|
},
|
2017-11-26 23:53:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
output := bytes.Buffer{}
|
|
|
|
err = t.Execute(&output, v)
|
|
|
|
if err != nil {
|
2017-11-27 02:52:14 +00:00
|
|
|
return template.HTML(err.Error())
|
2017-11-26 23:53:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return template.HTML(output.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
// CommentHandler handles the /comments URI for previewing and posting.
|
|
|
|
func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
b.BadRequest(w, r, "That method is not allowed.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
v := NewVars()
|
|
|
|
currentUser, _ := b.CurrentUser(r)
|
|
|
|
editToken := b.GetEditToken(w, r)
|
|
|
|
submit := r.FormValue("submit")
|
|
|
|
|
|
|
|
// Load the comment data from the form.
|
|
|
|
c := &comments.Comment{}
|
|
|
|
c.ParseForm(r)
|
|
|
|
if c.ThreadID == "" {
|
|
|
|
b.FlashAndRedirect(w, r, "/", "No thread ID found in the comment form.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Look up the thread.
|
|
|
|
t, err := comments.Load(c.ThreadID)
|
|
|
|
if err != nil {
|
|
|
|
t = comments.New(c.ThreadID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Origin URL to redirect them to at the end.
|
|
|
|
origin := "/"
|
|
|
|
if c.OriginURL != "" {
|
|
|
|
origin = c.OriginURL
|
|
|
|
}
|
|
|
|
|
|
|
|
// Are we editing a post?
|
|
|
|
if r.FormValue("editing") == "true" {
|
|
|
|
id := r.FormValue("id")
|
|
|
|
c, err = t.Find(id)
|
|
|
|
if err != nil {
|
|
|
|
b.FlashAndRedirect(w, r, "/", "That comment was not found.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify they have the matching edit token. Admin users are allowed.
|
|
|
|
if c.EditToken != editToken && !currentUser.Admin {
|
|
|
|
b.FlashAndRedirect(w, r, origin, "You don't have permission to edit that comment.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the extra form data into the comment struct.
|
|
|
|
c.ParseForm(r)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Are we deleting said post?
|
|
|
|
if submit == "confirm-delete" {
|
|
|
|
t.Delete(c.ID)
|
|
|
|
b.FlashAndRedirect(w, r, origin, "Comment deleted!")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cache their name and email in their session.
|
|
|
|
session := b.Session(r)
|
|
|
|
session.Values["c.name"] = c.Name
|
|
|
|
session.Values["c.email"] = c.Email
|
|
|
|
session.Save(r, w)
|
|
|
|
|
|
|
|
// Previewing, deleting, or posting?
|
|
|
|
switch submit {
|
|
|
|
case "preview", "delete":
|
|
|
|
if !c.Editing && currentUser.IsAuthenticated {
|
|
|
|
c.Name = currentUser.Name
|
|
|
|
c.Email = currentUser.Email
|
|
|
|
c.LoadAvatar()
|
|
|
|
}
|
|
|
|
c.HTML = template.HTML(b.RenderMarkdown(c.Body))
|
|
|
|
case "post":
|
|
|
|
if err := c.Validate(); err != nil {
|
|
|
|
v.Error = err
|
|
|
|
} else {
|
|
|
|
// Store our edit token, if we don't have one. For example, admins
|
|
|
|
// can edit others' comments but should not replace their edit token.
|
|
|
|
if c.EditToken == "" {
|
|
|
|
c.EditToken = editToken
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we're logged in, tag our user ID with this post.
|
|
|
|
if !c.Editing && c.UserID == 0 && currentUser.IsAuthenticated {
|
|
|
|
c.UserID = currentUser.ID
|
|
|
|
}
|
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
// Append their comment.
|
2017-12-23 22:48:47 +00:00
|
|
|
err := t.Post(c)
|
|
|
|
if err != nil {
|
|
|
|
b.FlashAndRedirect(w, r, c.OriginURL, "Error posting comment: %s", err)
|
|
|
|
return
|
|
|
|
}
|
2017-11-27 02:52:14 +00:00
|
|
|
b.NotifyComment(c)
|
|
|
|
|
|
|
|
// Are they subscribing to future comments?
|
|
|
|
if c.Subscribe && len(c.Email) > 0 {
|
|
|
|
if _, err := mail.ParseAddress(c.Email); err == nil {
|
|
|
|
m := comments.LoadMailingList()
|
|
|
|
m.Subscribe(t.ID, c.Email)
|
|
|
|
b.FlashAndRedirect(w, r, c.OriginURL,
|
|
|
|
"Comment posted, and you've been subscribed to "+
|
|
|
|
"future comments on this page.",
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2017-11-26 23:53:10 +00:00
|
|
|
b.FlashAndRedirect(w, r, c.OriginURL, "Comment posted!")
|
2017-12-23 22:48:47 +00:00
|
|
|
log.Info("t: %v", t.Comments)
|
2017-11-27 02:52:14 +00:00
|
|
|
return
|
2017-11-26 23:53:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
v.Data["Thread"] = t
|
|
|
|
v.Data["Comment"] = c
|
|
|
|
v.Data["Editing"] = c.Editing
|
|
|
|
v.Data["Deleting"] = submit == "delete"
|
|
|
|
|
|
|
|
b.RenderTemplate(w, r, "comments/index.gohtml", v)
|
|
|
|
}
|
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
// SubscriptionHandler to opt out of subscriptions.
|
|
|
|
func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
v := NewVars()
|
2017-11-26 23:53:10 +00:00
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
// POST to unsubscribe from all threads.
|
|
|
|
if r.Method == http.MethodPost {
|
|
|
|
email := r.FormValue("email")
|
|
|
|
if email == "" {
|
|
|
|
v.Error = errors.New("email address is required to unsubscribe from comment threads")
|
|
|
|
} else if _, err := mail.ParseAddress(email); err != nil {
|
|
|
|
v.Error = errors.New("invalid email address")
|
|
|
|
}
|
2017-11-26 23:53:10 +00:00
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
m := comments.LoadMailingList()
|
|
|
|
m.UnsubscribeAll(email)
|
|
|
|
b.FlashAndRedirect(w, r, "/comments/subscription",
|
|
|
|
"You have been unsubscribed from all mailing lists.",
|
|
|
|
)
|
2017-11-26 23:53:10 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
// GET to unsubscribe from a single thread.
|
|
|
|
thread := r.URL.Query().Get("t")
|
|
|
|
email := r.URL.Query().Get("e")
|
|
|
|
if thread != "" && email != "" {
|
|
|
|
m := comments.LoadMailingList()
|
|
|
|
m.Unsubscribe(thread, email)
|
|
|
|
b.FlashAndRedirect(w, r, "/comments/subscription", "You have been unsubscribed successfully.")
|
2017-11-26 23:53:10 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
b.RenderTemplate(w, r, "comments/subscription.gohtml", v)
|
|
|
|
}
|
2017-11-26 23:53:10 +00:00
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
// QuickDeleteHandler allows the admin to quickly delete spam without logging in.
|
|
|
|
func (b *Blog) QuickDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
thread := r.URL.Query().Get("t")
|
|
|
|
token := r.URL.Query().Get("d")
|
|
|
|
if thread == "" || token == "" {
|
|
|
|
b.BadRequest(w, r)
|
|
|
|
return
|
|
|
|
}
|
2017-11-26 23:53:10 +00:00
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
t, err := comments.Load(thread)
|
|
|
|
if err != nil {
|
|
|
|
b.BadRequest(w, r, "Comment thread does not exist.")
|
|
|
|
return
|
|
|
|
}
|
2017-11-26 23:53:10 +00:00
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
if c, err := t.FindByDeleteToken(token); err == nil {
|
|
|
|
t.Delete(c.ID)
|
|
|
|
}
|
2017-11-26 23:53:10 +00:00
|
|
|
|
2017-11-27 02:52:14 +00:00
|
|
|
b.FlashAndRedirect(w, r, "/", "Comment deleted!")
|
2017-11-26 23:53:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetEditToken gets or generates an edit token from the user's session, which
|
|
|
|
// allows a user to edit their comment for a short while after they post it.
|
|
|
|
func (b *Blog) GetEditToken(w http.ResponseWriter, r *http.Request) string {
|
|
|
|
session := b.Session(r)
|
|
|
|
if token, ok := session.Values["c.token"].(string); ok && len(token) > 0 {
|
|
|
|
return token
|
|
|
|
}
|
|
|
|
|
|
|
|
token := uuid.New().String()
|
|
|
|
session.Values["c.token"] = token
|
|
|
|
session.Save(r, w)
|
|
|
|
return token
|
|
|
|
}
|