This is a simple reference sheet for Markdown syntax. The de facto place to find Markdown syntax is at
+https://daringfireball.net/projects/markdown/syntax
+but the examples here are more nicely presented.
+
+
This page just serves as a cheat sheet for Markdown syntax and their results. For descriptive paragraphs
+explaining the syntax, see the page linked above.
+
+
This website uses GitHub Flavored Markdown, an
+extension of Markdown that supports fenced code blocks, tables, and other features.
A paragraph is defined as a group of lines of text separated from other groups
+by at least one blank line. A hard return inside a paragraph doesn't get rendered
+in the output.
+
+
Headers
+
+
There are two methods to declare a header in Markdown: "underline" it by
+writing === or --- on the line directly below the
+heading (for <h1> and <h2>, respectively),
+or by prefixing the heading with # symbols. Usually the latter
+option is the easiest, and you can get more levels of headers this way.
+
+
+
+
+
Markdown Syntax
+
Output
+
+
+
+
+
+
+ This is an H1
+ =============
+
+ This is an H2
+ -------------
+
+
+
+
This is an H1
+
This is an H2
+
+
+
+
+
+ # This is an H1
+ ## This is an H2
+ #### This is an H4
+
+
+
+
This is an H1
+
This is an H2
+
This is an H4
+
+
+
+
+
+
Blockquotes
+
+
Prefix a line of text with > to "quote" it -- like in
+"e-mail syntax."
+
+
You may have multiple layers of quotes by using multiple >
+symbols.
+
+
+
+
+
Markdown Syntax
+
Output
+
+
+
+
+
+
+ > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
+ > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
+ > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
+ >
+ > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
+ > id sem consectetuer libero luctus adipiscing.
+
+
+
+
+
This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
+ consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
+ Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
+
+
Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
+ id sem consectetuer libero luctus adipiscing.
+
+
+
+
+
+
+ > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
+ consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
+ Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
+
+ > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
+ id sem consectetuer libero luctus adipiscing.
+
+
+
+
+
This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
+ consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
+ Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
+
+
Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
+ id sem consectetuer libero luctus adipiscing.
+
+
+
+
+
+
+ > This is the first level of quoting.
+ >
+ >> This is nested blockquote.
+ >>> A third level.
+ >
+ > Back to the first level.
+
+
+
+
+ This is the first level of quoting.
+
+ This is nested blockquote.
+
+ A third level.
+
+
+ Back to the first level.
+
+
+
+
+
+
+ > ## This is a header.
+ >
+ > 1. This is the first list item.
+ > 2. This is the second list item.
+ >
+ >Here's some example code:
+ >
+ > return shell_exec("echo $input | $markdown_script");
+
+
+
+ 1. This is a list item with two paragraphs. Lorem ipsum dolor
+ sit amet, consectetuer adipiscing elit. Aliquam hendrerit
+ mi posuere lectus.
+
+ Vestibulum enim wisi, viverra nec, fringilla in, laoreet
+ vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
+ sit amet velit.
+
+ 2. Suspendisse id sem consectetuer libero luctus adipiscing.
+
+
+
+
+
This is a list item with two paragraphs. Lorem ipsum dolor
+ sit amet, consectetuer adipiscing elit. Aliquam hendrerit
+ mi posuere lectus.
+
+ Vestibulum enim wisi, viverra nec, fringilla in, laoreet
+ vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
+ sit amet velit.
+
+
Suspendisse id sem consectetuer libero luctus adipiscing.
+
+
+
+
+
+
+
+
Code Blocks
+
+The typical Markdown way to write a code block is to indent each line of a paragraph with at
+least 4 spaces or 1 tab character. The Rophako CMS also uses GitHub-style code blocks, where
+you can use three backticks before and after the code block and then you don't need to indent
+each line of the code (makes copying/pasting easier!)
+
+Like GitHub-flavored Markdown, with a fenced code block you can also specify a programming
+language to get syntax highlighting for the code.
+
+
+
+
+
Markdown Syntax
+
Output
+
+
+
+
+
+
+ This is a normal paragraph.
+
+ This is a code block.
+
+
+
+ This is a normal paragraph.
+
+
This is a code block
+
+
+
+
+
+ This is a normal paragraph.
+
+ ```
+ This is a GitHub style "fenced code block".
+ ```
+
+
+Use backslash characters to escape any other special characters in the Markdown syntax. For example,
+\* to insert a literal asterisk so that it doesn't get mistaken for e.g. emphasized text,
+a list item, etc.
+
+Markdown provides backslash escapes for the following characters:
+
+
\ backslash
+` backtick
+* asterisk
+_ underscore
+{} curly braces
+[] square brackets
+() parenthesis
+# hash mark
++ plus sign
+- minus sign (hyphen)
+. dot
+! exclamation mark
+
+{{ end }}
From 60ccaf7b35d5f92bad47e0d79b05a8487172edf8 Mon Sep 17 00:00:00 2001
From: Noah Petherbridge
Date: Sat, 10 Feb 2018 11:14:42 -0800
Subject: [PATCH 04/10] Eviscerate the last of the middleware into sub-packages
---
core/admin.go | 7 +-
core/auth.go | 17 +++-
core/blog.go | 22 ++---
core/comments.go | 20 ++--
core/constants.go | 8 ++
core/core.go | 17 ++--
core/initial-setup.go | 4 +-
core/internal/middleware/auth/auth.go | 67 +++++++++++++
core/internal/middleware/csrf.go | 55 +++++++++++
core/internal/responses/responses.go | 66 -------------
core/internal/sessions/sessions.go | 56 +++++++++++
core/internal/types/context.go | 11 +++
core/middleware.go | 133 --------------------------
core/pages.go | 2 +-
core/responses.go | 23 ++---
core/templates.go | 16 ++--
16 files changed, 266 insertions(+), 258 deletions(-)
create mode 100644 core/internal/middleware/auth/auth.go
create mode 100644 core/internal/middleware/csrf.go
delete mode 100644 core/internal/responses/responses.go
create mode 100644 core/internal/sessions/sessions.go
create mode 100644 core/internal/types/context.go
delete mode 100644 core/middleware.go
diff --git a/core/admin.go b/core/admin.go
index 8b142db..91173ea 100644
--- a/core/admin.go
+++ b/core/admin.go
@@ -11,6 +11,7 @@ import (
"github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/forms"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/render"
"github.com/urfave/negroni"
@@ -24,7 +25,7 @@ func (b *Blog) AdminRoutes(r *mux.Router) {
adminRouter.HandleFunc("/editor", b.EditorHandler)
// r.HandleFunc("/admin", b.AdminHandler)
r.PathPrefix("/admin").Handler(negroni.New(
- negroni.HandlerFunc(b.LoginRequired),
+ negroni.HandlerFunc(auth.LoginRequired(b.MustLogin)),
negroni.Wrap(adminRouter),
))
}
@@ -48,8 +49,8 @@ func (b *Blog) EditorHandler(w http.ResponseWriter, r *http.Request) {
var (
fp string
fromCore = r.FormValue("from") == "core"
- saving = r.FormValue("action") == "save"
- deleting = r.FormValue("action") == "delete"
+ saving = r.FormValue("action") == ActionSave
+ deleting = r.FormValue("action") == ActionDelete
body = []byte{}
)
diff --git a/core/auth.go b/core/auth.go
index d601480..97428fa 100644
--- a/core/auth.go
+++ b/core/auth.go
@@ -7,7 +7,9 @@ import (
"github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/forms"
"github.com/kirsle/blog/core/internal/log"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/users"
+ "github.com/kirsle/blog/core/internal/sessions"
)
// AuthRoutes attaches the auth routes to the app.
@@ -17,9 +19,16 @@ func (b *Blog) AuthRoutes(r *mux.Router) {
r.HandleFunc("/account", b.AccountHandler)
}
+// MustLogin handles errors from the LoginRequired middleware by redirecting
+// the user to the login page.
+func (b *Blog) MustLogin(w http.ResponseWriter, r *http.Request) {
+ log.Info("MustLogin for %s", r.URL.Path)
+ b.Redirect(w, "/login?next="+r.URL.Path)
+}
+
// Login logs the browser in as the given user.
func (b *Blog) Login(w http.ResponseWriter, r *http.Request, u *users.User) error {
- session, err := b.store.Get(r, "session") // TODO session name
+ session, err := sessions.Store.Get(r, "session") // TODO session name
if err != nil {
return err
}
@@ -78,7 +87,7 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
// LogoutHandler logs the user out and redirects to the home page.
func (b *Blog) LogoutHandler(w http.ResponseWriter, r *http.Request) {
- session, _ := b.store.Get(r, "session")
+ session, _ := sessions.Store.Get(r, "session")
delete(session.Values, "logged-in")
delete(session.Values, "user-id")
session.Save(r, w)
@@ -87,11 +96,11 @@ func (b *Blog) LogoutHandler(w http.ResponseWriter, r *http.Request) {
// AccountHandler shows the account settings page.
func (b *Blog) AccountHandler(w http.ResponseWriter, r *http.Request) {
- if !b.LoggedIn(r) {
+ if !auth.LoggedIn(r) {
b.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!")
return
}
- currentUser, err := b.CurrentUser(r)
+ currentUser, err := auth.CurrentUser(r)
if err != nil {
b.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!!")
return
diff --git a/core/blog.go b/core/blog.go
index 8028596..b5ceb85 100644
--- a/core/blog.go
+++ b/core/blog.go
@@ -15,6 +15,7 @@ import (
"github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/markdown"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/comments"
"github.com/kirsle/blog/core/internal/models/posts"
"github.com/kirsle/blog/core/internal/models/settings"
@@ -76,19 +77,10 @@ func (b *Blog) BlogRoutes(r *mux.Router) {
loginRouter.HandleFunc("/blog/private", b.PrivatePosts)
r.PathPrefix("/blog").Handler(
negroni.New(
- negroni.HandlerFunc(b.LoginRequired),
+ negroni.HandlerFunc(auth.LoginRequired(b.MustLogin)),
negroni.Wrap(loginRouter),
),
)
-
- adminRouter := mux.NewRouter().PathPrefix("/admin").Subrouter().StrictSlash(false)
- r.HandleFunc("/admin", b.AdminHandler) // so as to not be "/admin/"
- adminRouter.HandleFunc("/settings", b.SettingsHandler)
- adminRouter.PathPrefix("/").HandlerFunc(b.PageHandler)
- r.PathPrefix("/admin").Handler(negroni.New(
- negroni.HandlerFunc(b.LoginRequired),
- negroni.Wrap(adminRouter),
- ))
}
// RSSHandler renders an RSS feed from the blog.
@@ -214,7 +206,7 @@ func (b *Blog) RecentPosts(r *http.Request, tag, privacy string) []posts.Post {
}
} else {
// Exclude certain posts in generic index views.
- if (post.Privacy == PRIVATE || post.Privacy == UNLISTED) && !b.LoggedIn(r) {
+ if (post.Privacy == PRIVATE || post.Privacy == UNLISTED) && !auth.LoggedIn(r) {
continue
} else if post.Privacy == DRAFT {
continue
@@ -370,7 +362,7 @@ func (b *Blog) BlogArchive(w http.ResponseWriter, r *http.Request) {
byMonth := map[string]*Archive{}
for _, post := range idx.Posts {
// Exclude certain posts
- if (post.Privacy == PRIVATE || post.Privacy == UNLISTED) && !b.LoggedIn(r) {
+ if (post.Privacy == PRIVATE || post.Privacy == UNLISTED) && !auth.LoggedIn(r) {
continue
} else if post.Privacy == DRAFT {
continue
@@ -416,8 +408,8 @@ func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string)
// Handle post privacy.
if post.Privacy == PRIVATE || post.Privacy == DRAFT {
- if !b.LoggedIn(r) {
- b.NotFound(w, r)
+ if !auth.LoggedIn(r) {
+ b.NotFound(w, r, "That post is not public.")
return nil
}
}
@@ -517,7 +509,7 @@ func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
if err := post.Validate(); err != nil {
v.Error = err
} else {
- author, _ := b.CurrentUser(r)
+ author, _ := auth.CurrentUser(r)
post.AuthorID = author.ID
post.Updated = time.Now().UTC()
diff --git a/core/comments.go b/core/comments.go
index 40066e8..9e613d4 100644
--- a/core/comments.go
+++ b/core/comments.go
@@ -10,12 +10,14 @@ import (
"github.com/google/uuid"
"github.com/gorilla/mux"
- "github.com/gorilla/sessions"
+ gorilla "github.com/gorilla/sessions"
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/markdown"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/comments"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/sessions"
)
// CommentRoutes attaches the comment routes to the app.
@@ -37,7 +39,7 @@ type CommentMeta struct {
}
// 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 {
+func (b *Blog) RenderComments(session *gorilla.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.
@@ -142,7 +144,7 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
return
}
v := NewVars()
- currentUser, _ := b.CurrentUser(r)
+ currentUser, _ := auth.CurrentUser(r)
editToken := b.GetEditToken(w, r)
submit := r.FormValue("submit")
@@ -193,21 +195,21 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
}
// Cache their name and email in their session.
- session := b.Session(r)
+ session := sessions.Get(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":
+ case ActionPreview, ActionDelete:
if !c.Editing && currentUser.IsAuthenticated {
c.Name = currentUser.Name
c.Email = currentUser.Email
c.LoadAvatar()
}
c.HTML = template.HTML(markdown.RenderMarkdown(c.Body))
- case "post":
+ case ActionPost:
if err := c.Validate(); err != nil {
v.Error = err
} else {
@@ -251,7 +253,7 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
v.Data["Thread"] = t
v.Data["Comment"] = c
v.Data["Editing"] = c.Editing
- v.Data["Deleting"] = submit == "delete"
+ v.Data["Deleting"] = submit == ActionDelete
b.RenderTemplate(w, r, "comments/index.gohtml", v)
}
@@ -295,7 +297,7 @@ 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)
+ b.BadRequest(w, r, "Bad Request")
return
}
@@ -315,7 +317,7 @@ func (b *Blog) QuickDeleteHandler(w http.ResponseWriter, r *http.Request) {
// 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)
+ session := sessions.Get(r)
if token, ok := session.Values["c.token"].(string); ok && len(token) > 0 {
return token
}
diff --git a/core/constants.go b/core/constants.go
index ba1bcb0..a2bd76e 100644
--- a/core/constants.go
+++ b/core/constants.go
@@ -19,3 +19,11 @@ const (
MARKDOWN ContentType = "markdown"
HTML ContentType = "html"
)
+
+// Common form actions.
+const (
+ ActionSave = "save"
+ ActionDelete = "delete"
+ ActionPreview = "preview"
+ ActionPost = "post"
+)
diff --git a/core/core.go b/core/core.go
index 60da7e7..3324f16 100644
--- a/core/core.go
+++ b/core/core.go
@@ -7,14 +7,16 @@ import (
"path/filepath"
"github.com/gorilla/mux"
- "github.com/gorilla/sessions"
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/markdown"
+ "github.com/kirsle/blog/core/internal/middleware"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/comments"
"github.com/kirsle/blog/core/internal/models/posts"
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/sessions"
"github.com/kirsle/blog/jsondb"
"github.com/kirsle/blog/jsondb/caches"
"github.com/kirsle/blog/jsondb/caches/null"
@@ -36,9 +38,8 @@ type Blog struct {
Cache caches.Cacher
// Web app objects.
- n *negroni.Negroni // Negroni middleware manager
- r *mux.Router // Router
- store sessions.Store
+ n *negroni.Negroni // Negroni middleware manager
+ r *mux.Router // Router
}
// New initializes the Blog application.
@@ -73,7 +74,7 @@ func (b *Blog) Configure() {
render.DocumentRoot = &b.DocumentRoot
// Initialize the session cookie store.
- b.store = sessions.NewCookieStore([]byte(config.Security.SecretKey))
+ sessions.SetSecretKey([]byte(config.Security.SecretKey))
users.HashCost = config.Security.HashCost
// Initialize the rest of the models.
@@ -120,9 +121,9 @@ func (b *Blog) SetupHTTP() {
n := negroni.New(
negroni.NewRecovery(),
negroni.NewLogger(),
- negroni.HandlerFunc(b.SessionLoader),
- negroni.HandlerFunc(b.CSRFMiddleware),
- negroni.HandlerFunc(b.AuthMiddleware),
+ negroni.HandlerFunc(sessions.Middleware),
+ negroni.HandlerFunc(middleware.CSRF(b.Forbidden)),
+ negroni.HandlerFunc(auth.Middleware),
)
n.UseHandler(r)
diff --git a/core/initial-setup.go b/core/initial-setup.go
index a99e656..6857365 100644
--- a/core/initial-setup.go
+++ b/core/initial-setup.go
@@ -3,12 +3,12 @@ package core
import (
"net/http"
- "github.com/gorilla/sessions"
"github.com/kirsle/blog/core/internal/forms"
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/sessions"
)
// SetupHandler is the initial blog setup route.
@@ -41,7 +41,7 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
s.Save()
// Re-initialize the cookie store with the new secret key.
- b.store = sessions.NewCookieStore([]byte(s.Security.SecretKey))
+ sessions.SetSecretKey([]byte(s.Security.SecretKey))
log.Info("Creating admin account %s", form.Username)
user := &users.User{
diff --git a/core/internal/middleware/auth/auth.go b/core/internal/middleware/auth/auth.go
new file mode 100644
index 0000000..be3d87f
--- /dev/null
+++ b/core/internal/middleware/auth/auth.go
@@ -0,0 +1,67 @@
+package auth
+
+import (
+ "context"
+ "errors"
+ "net/http"
+
+ "github.com/kirsle/blog/core/internal/log"
+ "github.com/kirsle/blog/core/internal/models/users"
+ "github.com/kirsle/blog/core/internal/sessions"
+ "github.com/kirsle/blog/core/internal/types"
+ "github.com/urfave/negroni"
+)
+
+// CurrentUser returns the current user's object.
+func CurrentUser(r *http.Request) (*users.User, error) {
+ session := sessions.Get(r)
+ if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
+ id := session.Values["user-id"].(int)
+ u, err := users.LoadReadonly(id)
+ u.IsAuthenticated = true
+ return u, err
+ }
+
+ return &users.User{
+ Admin: false,
+ }, errors.New("not authenticated")
+}
+
+// LoggedIn returns whether the current user is logged in to an account.
+func LoggedIn(r *http.Request) bool {
+ session := sessions.Get(r)
+ if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
+ return true
+ }
+ return false
+}
+
+// LoginRequired is a middleware that requires a logged-in user.
+func LoginRequired(onError http.HandlerFunc) negroni.HandlerFunc {
+ middleware := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+ ctx := r.Context()
+ if user, ok := ctx.Value(types.UserKey).(*users.User); ok {
+ if user.ID > 0 {
+ next(w, r)
+ return
+ }
+ }
+
+ log.Info("Redirect away!")
+ onError(w, r)
+ }
+
+ return middleware
+}
+
+// Middleware loads the user's authentication state from their session cookie.
+func Middleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+ u, err := CurrentUser(r)
+ if err != nil {
+ next(w, r)
+ return
+ }
+
+ ctx := context.WithValue(r.Context(), types.UserKey, u)
+ next(w, r.WithContext(ctx))
+}
diff --git a/core/internal/middleware/csrf.go b/core/internal/middleware/csrf.go
new file mode 100644
index 0000000..37d68b3
--- /dev/null
+++ b/core/internal/middleware/csrf.go
@@ -0,0 +1,55 @@
+package middleware
+
+import (
+ "net/http"
+
+ "github.com/google/uuid"
+ gorilla "github.com/gorilla/sessions"
+ "github.com/kirsle/blog/core/internal/log"
+ "github.com/kirsle/blog/core/internal/sessions"
+ "github.com/urfave/negroni"
+)
+
+// CSRF is a middleware generator that enforces CSRF tokens on all POST requests.
+func CSRF(onError func(http.ResponseWriter, *http.Request, string)) negroni.HandlerFunc {
+ middleware := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+ if r.Method == "POST" {
+ session := sessions.Get(r)
+ token := GenerateCSRFToken(w, r, session)
+ if token != r.FormValue("_csrf") {
+ log.Error("CSRF Mismatch: expected %s, got %s", r.FormValue("_csrf"), token)
+ onError(w, r, "Failed to validate CSRF token. Please try your request again.")
+ return
+ }
+ }
+ next(w, r)
+ }
+
+ return middleware
+}
+
+// ExampleCSRF shows how to use the CSRF handler.
+func ExampleCSRF() {
+ // Your error handling for CSRF failures.
+ onError := func(w http.ResponseWriter, r *http.Request, message string) {
+ w.Write([]byte("CSRF Error: " + message))
+ }
+
+ // Attach the middleware.
+ _ = negroni.New(
+ negroni.NewRecovery(),
+ negroni.NewLogger(),
+ negroni.HandlerFunc(CSRF(onError)),
+ )
+}
+
+// GenerateCSRFToken generates a CSRF token for the user and puts it in their session.
+func GenerateCSRFToken(w http.ResponseWriter, r *http.Request, session *gorilla.Session) string {
+ token, ok := session.Values["csrf"].(string)
+ if !ok {
+ token := uuid.New()
+ session.Values["csrf"] = token.String()
+ session.Save(r, w)
+ }
+ return token
+}
diff --git a/core/internal/responses/responses.go b/core/internal/responses/responses.go
deleted file mode 100644
index 1c0e193..0000000
--- a/core/internal/responses/responses.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package responses
-
-import (
- "net/http"
-
- "github.com/kirsle/blog/core/internal/log"
- "github.com/kirsle/blog/core/internal/render"
-)
-
-// Redirect sends an HTTP redirect response.
-func Redirect(w http.ResponseWriter, location string) {
- w.Header().Set("Location", location)
- w.WriteHeader(http.StatusFound)
-}
-
-// NotFound sends a 404 response.
-func NotFound(w http.ResponseWriter, r *http.Request, message ...string) {
- if len(message) == 0 {
- message = []string{"The page you were looking for was not found."}
- }
-
- w.WriteHeader(http.StatusNotFound)
- err := render.RenderTemplate(w, r, ".errors/404", &render.Vars{
- Message: message[0],
- })
- if err != nil {
- log.Error(err.Error())
- w.Write([]byte("Unrecoverable template error for NotFound()"))
- }
-}
-
-// Forbidden sends an HTTP 403 Forbidden response.
-func Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
- w.WriteHeader(http.StatusForbidden)
- err := render.RenderTemplate(w, r, ".errors/403", &render.Vars{
- Message: message[0],
- })
- if err != nil {
- log.Error(err.Error())
- w.Write([]byte("Unrecoverable template error for Forbidden()"))
- }
-}
-
-// Error sends an HTTP 500 Internal Server Error response.
-func Error(w http.ResponseWriter, r *http.Request, message ...string) {
- w.WriteHeader(http.StatusInternalServerError)
- err := render.RenderTemplate(w, r, ".errors/500", &render.Vars{
- Message: message[0],
- })
- if err != nil {
- log.Error(err.Error())
- w.Write([]byte("Unrecoverable template error for Error()"))
- }
-}
-
-// BadRequest sends an HTTP 400 Bad Request.
-func BadRequest(w http.ResponseWriter, r *http.Request, message ...string) {
- w.WriteHeader(http.StatusBadRequest)
- err := render.RenderTemplate(w, r, ".errors/400", &render.Vars{
- Message: message[0],
- })
- if err != nil {
- log.Error(err.Error())
- w.Write([]byte("Unrecoverable template error for BadRequest()"))
- }
-}
diff --git a/core/internal/sessions/sessions.go b/core/internal/sessions/sessions.go
new file mode 100644
index 0000000..3c9ec3e
--- /dev/null
+++ b/core/internal/sessions/sessions.go
@@ -0,0 +1,56 @@
+package sessions
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/sessions"
+ "github.com/kirsle/blog/core/internal/log"
+ "github.com/kirsle/blog/core/internal/types"
+)
+
+// Store holds your cookie store information.
+var Store sessions.Store
+
+// SetSecretKey initializes a session cookie store with the secret key.
+func SetSecretKey(keyPairs ...[]byte) {
+ Store = sessions.NewCookieStore(keyPairs...)
+}
+
+// Middleware gets the Gorilla session store and makes it available on the
+// Request context.
+//
+// Middleware is the first custom middleware applied, so it takes the current
+// datetime to make available later in the request and stores it on the request
+// context.
+func Middleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
+ // Store the current datetime on the request context.
+ ctx := context.WithValue(r.Context(), types.StartTimeKey, time.Now())
+
+ // Get the Gorilla session and make it available in the request context.
+ session, _ := Store.Get(r, "session")
+ ctx = context.WithValue(ctx, types.SessionKey, session)
+
+ next(w, r.WithContext(ctx))
+}
+
+// Get returns the current request's session.
+func Get(r *http.Request) *sessions.Session {
+ if r == nil {
+ panic("Session(*http.Request) with a nil argument!?")
+ }
+
+ ctx := r.Context()
+ if session, ok := ctx.Value(types.SessionKey).(*sessions.Session); ok {
+ return session
+ }
+
+ // If the session wasn't on the request, it means I broke something.
+ log.Error(
+ "Session(): didn't find session in request context! Getting it " +
+ "from the session store instead.",
+ )
+ session, _ := Store.Get(r, "session")
+ return session
+}
diff --git a/core/internal/types/context.go b/core/internal/types/context.go
new file mode 100644
index 0000000..67bd6a7
--- /dev/null
+++ b/core/internal/types/context.go
@@ -0,0 +1,11 @@
+package types
+
+// Key is an integer enum for context.Context keys.
+type Key int
+
+// Key definitions.
+const (
+ SessionKey Key = iota // The request's cookie session object.
+ UserKey // The request's user data for logged-in users.
+ StartTimeKey // HTTP request start time.
+)
diff --git a/core/middleware.go b/core/middleware.go
deleted file mode 100644
index 5c86749..0000000
--- a/core/middleware.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package core
-
-import (
- "context"
- "errors"
- "net/http"
- "time"
-
- "github.com/google/uuid"
- "github.com/gorilla/sessions"
- "github.com/kirsle/blog/core/internal/log"
- "github.com/kirsle/blog/core/internal/models/users"
-)
-
-type key int
-
-const (
- sessionKey key = iota
- userKey
- requestTimeKey
-)
-
-// SessionLoader gets the Gorilla session store and makes it available on the
-// Request context.
-//
-// SessionLoader is the first custom middleware applied, so it takes the current
-// datetime to make available later in the request and stores it on the request
-// context.
-func (b *Blog) SessionLoader(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
- // Store the current datetime on the request context.
- ctx := context.WithValue(r.Context(), requestTimeKey, time.Now())
-
- // Get the Gorilla session and make it available in the request context.
- session, _ := b.store.Get(r, "session")
- ctx = context.WithValue(ctx, sessionKey, session)
-
- next(w, r.WithContext(ctx))
-}
-
-// Session returns the current request's session.
-func (b *Blog) Session(r *http.Request) *sessions.Session {
- if r == nil {
- panic("Session(*http.Request) with a nil argument!?")
- }
-
- ctx := r.Context()
- if session, ok := ctx.Value(sessionKey).(*sessions.Session); ok {
- return session
- }
-
- log.Error(
- "Session(): didn't find session in request context! Getting it " +
- "from the session store instead.",
- )
- session, _ := b.store.Get(r, "session")
- return session
-}
-
-// CSRFMiddleware enforces CSRF tokens on all POST requests.
-func (b *Blog) CSRFMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
- if r.Method == "POST" {
- session := b.Session(r)
- token := b.GenerateCSRFToken(w, r, session)
- if token != r.FormValue("_csrf") {
- log.Error("CSRF Mismatch: expected %s, got %s", r.FormValue("_csrf"), token)
- b.Forbidden(w, r, "Failed to validate CSRF token. Please try your request again.")
- return
- }
- }
-
- next(w, r)
-}
-
-// GenerateCSRFToken generates a CSRF token for the user and puts it in their session.
-func (b *Blog) GenerateCSRFToken(w http.ResponseWriter, r *http.Request, session *sessions.Session) string {
- token, ok := session.Values["csrf"].(string)
- if !ok {
- token := uuid.New()
- session.Values["csrf"] = token.String()
- session.Save(r, w)
- }
- return token
-}
-
-// CurrentUser returns the current user's object.
-func (b *Blog) CurrentUser(r *http.Request) (*users.User, error) {
- session := b.Session(r)
- if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
- id := session.Values["user-id"].(int)
- u, err := users.LoadReadonly(id)
- u.IsAuthenticated = true
- return u, err
- }
-
- return &users.User{
- Admin: false,
- }, errors.New("not authenticated")
-}
-
-// LoggedIn returns whether the current user is logged in to an account.
-func (b *Blog) LoggedIn(r *http.Request) bool {
- session := b.Session(r)
- if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
- return true
- }
- return false
-}
-
-// AuthMiddleware loads the user's authentication state.
-func (b *Blog) AuthMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
- u, err := b.CurrentUser(r)
- if err != nil {
- next(w, r)
- return
- }
-
- ctx := context.WithValue(r.Context(), userKey, u)
- next(w, r.WithContext(ctx))
-}
-
-// LoginRequired is a middleware that requires a logged-in user.
-func (b *Blog) LoginRequired(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
- ctx := r.Context()
- if user, ok := ctx.Value(userKey).(*users.User); ok {
- if user.ID > 0 {
- next(w, r)
- return
- }
- }
-
- log.Info("Redirect away!")
- b.Redirect(w, "/login?next="+r.URL.Path)
-}
diff --git a/core/pages.go b/core/pages.go
index 1b7444b..a6f20b2 100644
--- a/core/pages.go
+++ b/core/pages.go
@@ -23,7 +23,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
// Restrict special paths.
if strings.HasPrefix(strings.ToLower(path), "/.") {
- b.Forbidden(w, r)
+ b.Forbidden(w, r, "Forbidden")
return
}
diff --git a/core/responses.go b/core/responses.go
index 02f7720..79e85b2 100644
--- a/core/responses.go
+++ b/core/responses.go
@@ -6,11 +6,12 @@ import (
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/sessions"
)
// Flash adds a flash message to the user's session.
func (b *Blog) Flash(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
- session := b.Session(r)
+ session := sessions.Get(r)
session.AddFlash(fmt.Sprintf(message, args...))
session.Save(r, w)
}
@@ -34,14 +35,14 @@ func (b *Blog) Redirect(w http.ResponseWriter, location string) {
}
// NotFound sends a 404 response.
-func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...string) {
- if len(message) == 0 {
- message = []string{"The page you were looking for was not found."}
+func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message string) {
+ if message == "" {
+ message = "The page you were looking for was not found."
}
w.WriteHeader(http.StatusNotFound)
err := b.RenderTemplate(w, r, ".errors/404", render.Vars{
- Message: message[0],
+ Message: message,
})
if err != nil {
log.Error(err.Error())
@@ -50,10 +51,10 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...strin
}
// Forbidden sends an HTTP 403 Forbidden response.
-func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
+func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message string) {
w.WriteHeader(http.StatusForbidden)
err := b.RenderTemplate(w, r, ".errors/403", render.Vars{
- Message: message[0],
+ Message: message,
})
if err != nil {
log.Error(err.Error())
@@ -62,10 +63,10 @@ func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...stri
}
// Error sends an HTTP 500 Internal Server Error response.
-func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message ...string) {
+func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message string) {
w.WriteHeader(http.StatusInternalServerError)
err := b.RenderTemplate(w, r, ".errors/500", render.Vars{
- Message: message[0],
+ Message: message,
})
if err != nil {
log.Error(err.Error())
@@ -74,10 +75,10 @@ func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message ...string)
}
// BadRequest sends an HTTP 400 Bad Request.
-func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message ...string) {
+func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message string) {
w.WriteHeader(http.StatusBadRequest)
err := b.RenderTemplate(w, r, ".errors/400", render.Vars{
- Message: message[0],
+ Message: message,
})
if err != nil {
log.Error(err.Error())
diff --git a/core/templates.go b/core/templates.go
index 3c36eea..b8759ec 100644
--- a/core/templates.go
+++ b/core/templates.go
@@ -8,9 +8,13 @@ import (
"time"
"github.com/kirsle/blog/core/internal/forms"
+ "github.com/kirsle/blog/core/internal/middleware"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/sessions"
+ "github.com/kirsle/blog/core/internal/types"
)
// Vars is an interface to implement by the templates to pass their own custom
@@ -66,11 +70,11 @@ func (b *Blog) LoadDefaults(v render.Vars, r *http.Request) render.Vars {
v.SetupNeeded = true
}
v.Request = r
- v.RequestTime = r.Context().Value(requestTimeKey).(time.Time)
+ v.RequestTime = r.Context().Value(types.StartTimeKey).(time.Time)
v.Title = s.Site.Title
v.Path = r.URL.Path
- user, err := b.CurrentUser(r)
+ user, err := auth.CurrentUser(r)
v.CurrentUser = user
v.LoggedIn = err == nil
@@ -109,7 +113,7 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
vars = b.LoadDefaults(vars, r)
// Add any flashed messages from the endpoint controllers.
- session := b.Session(r)
+ session := sessions.Get(r)
if flashes := session.Flashes(); len(flashes) > 0 {
for _, flash := range flashes {
_ = flash
@@ -119,7 +123,7 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
}
vars.RequestDuration = time.Now().Sub(vars.RequestTime)
- vars.CSRF = b.GenerateCSRFToken(w, r, session)
+ vars.CSRF = middleware.GenerateCSRFToken(w, r, session)
vars.Editable = !strings.HasPrefix(path, "admin/")
return render.Template(w, path, render.Config{
@@ -140,8 +144,8 @@ func (b *Blog) TemplateFuncs(w http.ResponseWriter, r *http.Request, inject map[
return template.HTML("[RenderComments Error: need both http.ResponseWriter and http.Request]")
}
- session := b.Session(r)
- csrf := b.GenerateCSRFToken(w, r, session)
+ session := sessions.Get(r)
+ csrf := middleware.GenerateCSRFToken(w, r, session)
return b.RenderComments(session, csrf, r.URL.Path, subject, ids...)
},
}
From f0045ae2cff14e05b500e8fdb64383b56d1baa56 Mon Sep 17 00:00:00 2001
From: Noah Petherbridge
Date: Sat, 10 Feb 2018 11:20:27 -0800
Subject: [PATCH 05/10] Move Flash and Redirect responses to subpackage
---
core/admin.go | 13 ++++++-----
core/auth.go | 29 ++++++++++++------------
core/blog.go | 15 +++++++------
core/comments.go | 21 +++++++++---------
core/contact.go | 7 +++---
core/{responses.go => errors.go} | 27 -----------------------
core/initial-setup.go | 5 +++--
core/internal/responses/responses.go | 33 ++++++++++++++++++++++++++++
core/pages.go | 3 ++-
9 files changed, 83 insertions(+), 70 deletions(-)
rename core/{responses.go => errors.go} (63%)
create mode 100644 core/internal/responses/responses.go
diff --git a/core/admin.go b/core/admin.go
index 91173ea..8f2a748 100644
--- a/core/admin.go
+++ b/core/admin.go
@@ -14,6 +14,7 @@ import (
"github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
"github.com/urfave/negroni"
)
@@ -60,18 +61,18 @@ func (b *Blog) EditorHandler(w http.ResponseWriter, r *http.Request) {
body = []byte(r.FormValue("body"))
err := ioutil.WriteFile(fp, body, 0644)
if err != nil {
- b.Flash(w, r, "Error saving: %s", err)
+ responses.Flash(w, r, "Error saving: %s", err)
} else {
- b.FlashAndRedirect(w, r, "/admin/editor?file="+url.QueryEscape(file), "Page saved successfully!")
+ responses.FlashAndRedirect(w, r, "/admin/editor?file="+url.QueryEscape(file), "Page saved successfully!")
return
}
} else if deleting {
fp = filepath.Join(b.UserRoot, file)
err := os.Remove(fp)
if err != nil {
- b.FlashAndRedirect(w, r, "/admin/editor", "Error deleting: %s", err)
+ responses.FlashAndRedirect(w, r, "/admin/editor", "Error deleting: %s", err)
} else {
- b.FlashAndRedirect(w, r, "/admin/editor", "Page deleted!")
+ responses.FlashAndRedirect(w, r, "/admin/editor", "Page deleted!")
return
}
} else {
@@ -94,7 +95,7 @@ func (b *Blog) EditorHandler(w http.ResponseWriter, r *http.Request) {
if !os.IsNotExist(err) && !f.IsDir() {
body, err = ioutil.ReadFile(fp)
if err != nil {
- b.Flash(w, r, "Error reading %s: %s", fp, err)
+ responses.Flash(w, r, "Error reading %s: %s", fp, err)
}
}
@@ -225,7 +226,7 @@ func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
settings.Save()
b.Configure()
- b.FlashAndReload(w, r, "Settings have been saved!")
+ responses.FlashAndReload(w, r, "Settings have been saved!")
return
}
}
diff --git a/core/auth.go b/core/auth.go
index 97428fa..fd8cf92 100644
--- a/core/auth.go
+++ b/core/auth.go
@@ -9,6 +9,7 @@ import (
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/users"
+ "github.com/kirsle/blog/core/internal/responses"
"github.com/kirsle/blog/core/internal/sessions"
)
@@ -23,7 +24,7 @@ func (b *Blog) AuthRoutes(r *mux.Router) {
// the user to the login page.
func (b *Blog) MustLogin(w http.ResponseWriter, r *http.Request) {
log.Info("MustLogin for %s", r.URL.Path)
- b.Redirect(w, "/login?next="+r.URL.Path)
+ responses.Redirect(w, "/login?next="+r.URL.Path)
}
// Login logs the browser in as the given user.
@@ -67,15 +68,15 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
vars.Error = errors.New("bad username or password")
} else {
// Login OK!
- b.Flash(w, r, "Login OK!")
+ responses.Flash(w, r, "Login OK!")
b.Login(w, r, user)
// A next URL given? TODO: actually get to work
log.Info("Redirect after login to: %s", nextURL)
if len(nextURL) > 0 && nextURL[0] == '/' {
- b.Redirect(w, nextURL)
+ responses.Redirect(w, nextURL)
} else {
- b.Redirect(w, "/")
+ responses.Redirect(w, "/")
}
return
}
@@ -91,25 +92,25 @@ func (b *Blog) LogoutHandler(w http.ResponseWriter, r *http.Request) {
delete(session.Values, "logged-in")
delete(session.Values, "user-id")
session.Save(r, w)
- b.Redirect(w, "/")
+ responses.Redirect(w, "/")
}
// AccountHandler shows the account settings page.
func (b *Blog) AccountHandler(w http.ResponseWriter, r *http.Request) {
if !auth.LoggedIn(r) {
- b.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!")
+ responses.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!")
return
}
currentUser, err := auth.CurrentUser(r)
if err != nil {
- b.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!!")
+ responses.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!!")
return
}
// Load an editable copy of the user.
user, err := users.Load(currentUser.ID)
if err != nil {
- b.FlashAndRedirect(w, r, "/login?next=/account", "User ID %d not loadable?", currentUser.ID)
+ responses.FlashAndRedirect(w, r, "/login?next=/account", "User ID %d not loadable?", currentUser.ID)
return
}
@@ -129,14 +130,14 @@ func (b *Blog) AccountHandler(w http.ResponseWriter, r *http.Request) {
form.NewPassword = r.FormValue("newpassword")
form.NewPassword2 = r.FormValue("newpassword2")
if err = form.Validate(); err != nil {
- b.Flash(w, r, err.Error())
+ responses.Flash(w, r, err.Error())
} else {
var ok = true
// Validate the username is available.
if form.Username != user.Username {
if _, err = users.LoadUsername(form.Username); err == nil {
- b.Flash(w, r, "That username already exists.")
+ responses.Flash(w, r, "That username already exists.")
ok = false
}
}
@@ -145,12 +146,12 @@ func (b *Blog) AccountHandler(w http.ResponseWriter, r *http.Request) {
if len(form.OldPassword) > 0 {
// Validate their old password.
if _, err = users.CheckAuth(form.Username, form.OldPassword); err != nil {
- b.Flash(w, r, "Your old password is incorrect.")
+ responses.Flash(w, r, "Your old password is incorrect.")
ok = false
} else {
err = user.SetPassword(form.NewPassword)
if err != nil {
- b.Flash(w, r, "Change password error: %s", err)
+ responses.Flash(w, r, "Change password error: %s", err)
ok = false
}
}
@@ -163,9 +164,9 @@ func (b *Blog) AccountHandler(w http.ResponseWriter, r *http.Request) {
user.Email = form.Email
err = user.Save()
if err != nil {
- b.Flash(w, r, "Error saving user: %s", err)
+ responses.Flash(w, r, "Error saving user: %s", err)
} else {
- b.FlashAndRedirect(w, r, "/account", "Settings saved!")
+ responses.FlashAndRedirect(w, r, "/account", "Settings saved!")
return
}
}
diff --git a/core/blog.go b/core/blog.go
index b5ceb85..4708d99 100644
--- a/core/blog.go
+++ b/core/blog.go
@@ -21,6 +21,7 @@ import (
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
"github.com/urfave/negroni"
)
@@ -57,7 +58,7 @@ func (b *Blog) BlogRoutes(r *mux.Router) {
b.NotFound(w, r, "Not Found")
return
}
- b.Redirect(w, "/tagged/"+tag)
+ responses.Redirect(w, "/tagged/"+tag)
})
r.HandleFunc("/blog/entry/{fragment}", func(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
@@ -66,7 +67,7 @@ func (b *Blog) BlogRoutes(r *mux.Router) {
b.NotFound(w, r, "Not Found")
return
}
- b.Redirect(w, "/"+fragment)
+ responses.Redirect(w, "/"+fragment)
})
// Login-required routers.
@@ -517,8 +518,8 @@ func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
if err != nil {
v.Error = err
} else {
- b.Flash(w, r, "Post created!")
- b.Redirect(w, "/"+post.Fragment)
+ responses.Flash(w, r, "Post created!")
+ responses.Redirect(w, "/"+post.Fragment)
}
}
}
@@ -542,7 +543,7 @@ func (b *Blog) DeletePost(w http.ResponseWriter, r *http.Request) {
idStr = r.URL.Query().Get("id")
}
if idStr == "" {
- b.FlashAndRedirect(w, r, "/admin", "No post ID given for deletion!")
+ responses.FlashAndRedirect(w, r, "/admin", "No post ID given for deletion!")
return
}
@@ -551,14 +552,14 @@ func (b *Blog) DeletePost(w http.ResponseWriter, r *http.Request) {
if err == nil {
post, err = posts.Load(id)
if err != nil {
- b.FlashAndRedirect(w, r, "/admin", "That post ID was not found.")
+ responses.FlashAndRedirect(w, r, "/admin", "That post ID was not found.")
return
}
}
if r.Method == http.MethodPost {
post.Delete()
- b.FlashAndRedirect(w, r, "/admin", "Blog entry deleted!")
+ responses.FlashAndRedirect(w, r, "/admin", "Blog entry deleted!")
return
}
diff --git a/core/comments.go b/core/comments.go
index 9e613d4..8af2624 100644
--- a/core/comments.go
+++ b/core/comments.go
@@ -17,6 +17,7 @@ import (
"github.com/kirsle/blog/core/internal/models/comments"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
"github.com/kirsle/blog/core/internal/sessions"
)
@@ -152,7 +153,7 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
c := &comments.Comment{}
c.ParseForm(r)
if c.ThreadID == "" {
- b.FlashAndRedirect(w, r, "/", "No thread ID found in the comment form.")
+ responses.FlashAndRedirect(w, r, "/", "No thread ID found in the comment form.")
return
}
@@ -173,13 +174,13 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
id := r.FormValue("id")
c, err = t.Find(id)
if err != nil {
- b.FlashAndRedirect(w, r, "/", "That comment was not found.")
+ responses.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.")
+ responses.FlashAndRedirect(w, r, origin, "You don't have permission to edit that comment.")
return
}
@@ -190,7 +191,7 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
// Are we deleting said post?
if submit == "confirm-delete" {
t.Delete(c.ID)
- b.FlashAndRedirect(w, r, origin, "Comment deleted!")
+ responses.FlashAndRedirect(w, r, origin, "Comment deleted!")
return
}
@@ -227,7 +228,7 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
// Append their comment.
err := t.Post(c)
if err != nil {
- b.FlashAndRedirect(w, r, c.OriginURL, "Error posting comment: %s", err)
+ responses.FlashAndRedirect(w, r, c.OriginURL, "Error posting comment: %s", err)
return
}
b.NotifyComment(c)
@@ -237,14 +238,14 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
if _, err := mail.ParseAddress(c.Email); err == nil {
m := comments.LoadMailingList()
m.Subscribe(t.ID, c.Email)
- b.FlashAndRedirect(w, r, c.OriginURL,
+ responses.FlashAndRedirect(w, r, c.OriginURL,
"Comment posted, and you've been subscribed to "+
"future comments on this page.",
)
return
}
}
- b.FlashAndRedirect(w, r, c.OriginURL, "Comment posted!")
+ responses.FlashAndRedirect(w, r, c.OriginURL, "Comment posted!")
log.Info("t: %v", t.Comments)
return
}
@@ -273,7 +274,7 @@ func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
m := comments.LoadMailingList()
m.UnsubscribeAll(email)
- b.FlashAndRedirect(w, r, "/comments/subscription",
+ responses.FlashAndRedirect(w, r, "/comments/subscription",
"You have been unsubscribed from all mailing lists.",
)
return
@@ -285,7 +286,7 @@ func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
if thread != "" && email != "" {
m := comments.LoadMailingList()
m.Unsubscribe(thread, email)
- b.FlashAndRedirect(w, r, "/comments/subscription", "You have been unsubscribed successfully.")
+ responses.FlashAndRedirect(w, r, "/comments/subscription", "You have been unsubscribed successfully.")
return
}
@@ -311,7 +312,7 @@ func (b *Blog) QuickDeleteHandler(w http.ResponseWriter, r *http.Request) {
t.Delete(c.ID)
}
- b.FlashAndRedirect(w, r, "/", "Comment deleted!")
+ responses.FlashAndRedirect(w, r, "/", "Comment deleted!")
}
// GetEditToken gets or generates an edit token from the user's session, which
diff --git a/core/contact.go b/core/contact.go
index f4cb61e..11a8dcc 100644
--- a/core/contact.go
+++ b/core/contact.go
@@ -12,6 +12,7 @@ import (
"github.com/kirsle/blog/core/internal/forms"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/models/settings"
+ "github.com/kirsle/blog/core/internal/responses"
)
// ContactRoutes attaches the contact URL to the app.
@@ -38,7 +39,7 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
if r.Method == http.MethodPost {
form.ParseForm(r)
if err = form.Validate(); err != nil {
- b.Flash(w, r, err.Error())
+ responses.Flash(w, r, err.Error())
} else {
go b.SendEmail(Email{
To: cfg.Site.AdminEmail,
@@ -56,7 +57,7 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
// Log it to disk, too.
fh, err := os.OpenFile(filepath.Join(b.UserRoot, ".contact.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
- b.Flash(w, r, "Error logging the message to disk: %s", err)
+ responses.Flash(w, r, "Error logging the message to disk: %s", err)
} else {
fh.WriteString(fmt.Sprintf(
"Date: %s\nName: %s\nEmail: %s\nSubject: %s\n\n%s\n\n--------------------\n\n",
@@ -68,7 +69,7 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
))
fh.Close()
}
- b.FlashAndRedirect(w, r, "/contact", "Your message has been sent.")
+ responses.FlashAndRedirect(w, r, "/contact", "Your message has been sent.")
}
}
diff --git a/core/responses.go b/core/errors.go
similarity index 63%
rename from core/responses.go
rename to core/errors.go
index 79e85b2..3219985 100644
--- a/core/responses.go
+++ b/core/errors.go
@@ -1,39 +1,12 @@
package core
import (
- "fmt"
"net/http"
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/render"
- "github.com/kirsle/blog/core/internal/sessions"
)
-// Flash adds a flash message to the user's session.
-func (b *Blog) Flash(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
- session := sessions.Get(r)
- session.AddFlash(fmt.Sprintf(message, args...))
- session.Save(r, w)
-}
-
-// FlashAndRedirect flashes and redirects in one go.
-func (b *Blog) FlashAndRedirect(w http.ResponseWriter, r *http.Request, location, message string, args ...interface{}) {
- b.Flash(w, r, message, args...)
- b.Redirect(w, location)
-}
-
-// FlashAndReload flashes and sends a redirect to the same path.
-func (b *Blog) FlashAndReload(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
- b.Flash(w, r, message, args...)
- b.Redirect(w, r.URL.Path)
-}
-
-// Redirect sends an HTTP redirect response.
-func (b *Blog) Redirect(w http.ResponseWriter, location string) {
- w.Header().Set("Location", location)
- w.WriteHeader(http.StatusFound)
-}
-
// NotFound sends a 404 response.
func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message string) {
if message == "" {
diff --git a/core/initial-setup.go b/core/initial-setup.go
index 6857365..3ad754b 100644
--- a/core/initial-setup.go
+++ b/core/initial-setup.go
@@ -8,6 +8,7 @@ import (
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
"github.com/kirsle/blog/core/internal/sessions"
)
@@ -20,7 +21,7 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
// Reject if we're already set up.
s, _ := settings.Load()
if s.Initialized {
- b.FlashAndRedirect(w, r, "/", "This website has already been configured.")
+ responses.FlashAndRedirect(w, r, "/", "This website has already been configured.")
return
}
@@ -58,7 +59,7 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
// All set!
b.Login(w, r, user)
- b.FlashAndRedirect(w, r, "/admin", "Admin user created and logged in.")
+ responses.FlashAndRedirect(w, r, "/admin", "Admin user created and logged in.")
return
}
}
diff --git a/core/internal/responses/responses.go b/core/internal/responses/responses.go
new file mode 100644
index 0000000..8776768
--- /dev/null
+++ b/core/internal/responses/responses.go
@@ -0,0 +1,33 @@
+package responses
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/kirsle/blog/core/internal/sessions"
+)
+
+// Flash adds a flash message to the user's session.
+func Flash(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
+ session := sessions.Get(r)
+ session.AddFlash(fmt.Sprintf(message, args...))
+ session.Save(r, w)
+}
+
+// FlashAndRedirect flashes and redirects in one go.
+func FlashAndRedirect(w http.ResponseWriter, r *http.Request, location, message string, args ...interface{}) {
+ Flash(w, r, message, args...)
+ Redirect(w, location)
+}
+
+// FlashAndReload flashes and sends a redirect to the same path.
+func FlashAndReload(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
+ Flash(w, r, message, args...)
+ Redirect(w, r.URL.Path)
+}
+
+// Redirect sends an HTTP redirect response.
+func Redirect(w http.ResponseWriter, location string) {
+ w.Header().Set("Location", location)
+ w.WriteHeader(http.StatusFound)
+}
diff --git a/core/pages.go b/core/pages.go
index a6f20b2..c2ce736 100644
--- a/core/pages.go
+++ b/core/pages.go
@@ -8,6 +8,7 @@ import (
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
)
// PageHandler is the catch-all route handler, for serving static web pages.
@@ -17,7 +18,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
// Remove trailing slashes by redirecting them away.
if len(path) > 1 && path[len(path)-1] == '/' {
- b.Redirect(w, strings.TrimRight(path, "/"))
+ responses.Redirect(w, strings.TrimRight(path, "/"))
return
}
From eab7dae75b839a21c06bf6724a44562ccd0f3200 Mon Sep 17 00:00:00 2001
From: Noah Petherbridge
Date: Sat, 10 Feb 2018 13:16:20 -0800
Subject: [PATCH 06/10] Further simplify template rendering
---
core/admin.go | 2 +-
core/auth.go | 1 -
core/blog.go | 26 ++---
core/comments.go | 12 +-
core/internal/controllers/setup/setup.go | 1 +
core/internal/middleware/auth/auth.go | 3 -
core/internal/render/functions.go | 14 +++
core/internal/render/templates.go | 125 ++++++++++-----------
core/pages.go | 2 +-
core/templates.go | 135 ++---------------------
root/.layout.gohtml | 2 +-
root/blog/entry.gohtml | 2 +-
12 files changed, 105 insertions(+), 220 deletions(-)
create mode 100644 core/internal/controllers/setup/setup.go
create mode 100644 core/internal/render/functions.go
diff --git a/core/admin.go b/core/admin.go
index 8f2a748..b910951 100644
--- a/core/admin.go
+++ b/core/admin.go
@@ -33,7 +33,7 @@ func (b *Blog) AdminRoutes(r *mux.Router) {
// AdminHandler is the admin landing page.
func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) {
- b.RenderTemplate(w, r, "admin/index", render.Vars{})
+ render.Template(w, r, "admin/index", NewVars())
}
// FileTree holds information about files in the document roots.
diff --git a/core/auth.go b/core/auth.go
index fd8cf92..29efd4b 100644
--- a/core/auth.go
+++ b/core/auth.go
@@ -23,7 +23,6 @@ func (b *Blog) AuthRoutes(r *mux.Router) {
// MustLogin handles errors from the LoginRequired middleware by redirecting
// the user to the login page.
func (b *Blog) MustLogin(w http.ResponseWriter, r *http.Request) {
- log.Info("MustLogin for %s", r.URL.Path)
responses.Redirect(w, "/login?next="+r.URL.Path)
}
diff --git a/core/blog.go b/core/blog.go
index 4708d99..de2e5d3 100644
--- a/core/blog.go
+++ b/core/blog.go
@@ -44,6 +44,10 @@ type Archive struct {
// BlogRoutes attaches the blog routes to the app.
func (b *Blog) BlogRoutes(r *mux.Router) {
+ render.Funcs["RenderIndex"] = b.RenderIndex
+ render.Funcs["RenderPost"] = b.RenderPost
+ render.Funcs["RenderTags"] = b.RenderTags
+
// Public routes
r.HandleFunc("/blog", b.IndexHandler)
r.HandleFunc("/blog.rss", b.RSSHandler)
@@ -302,19 +306,14 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
// Render the blog index partial.
var output bytes.Buffer
v := render.Vars{
+ NoLayout: true,
Data: map[interface{}]interface{}{
"PreviousPage": previousPage,
"NextPage": nextPage,
"View": view,
},
}
- v = b.LoadDefaults(v, r)
- render.PartialTemplate(&output, "blog/index.partial", render.Config{
- Request: r,
- Vars: &v,
- WithLayout: false,
- Functions: b.TemplateFuncs(nil, r, nil),
- })
+ b.RenderTemplate(&output, r, "blog/index.partial", v)
return template.HTML(output.String())
}
@@ -333,19 +332,13 @@ func (b *Blog) RenderTags(r *http.Request, indexView bool) template.HTML {
var output bytes.Buffer
v := render.Vars{
+ NoLayout: true,
Data: map[interface{}]interface{}{
"IndexView": indexView,
"Tags": tags,
},
}
- v = b.LoadDefaults(v, r)
- render.PartialTemplate(&output, "blog/tags.partial", render.Config{
- Request: r,
- Vars: &v,
- WithLayout: false,
- Functions: b.TemplateFuncs(nil, nil, nil),
- })
- // b.RenderPartialTemplate(&output, r, "blog/tags.partial", v, false, nil)
+ b.RenderTemplate(&output, r, "blog/tags.partial", v)
return template.HTML(output.String())
}
@@ -455,6 +448,7 @@ func (b *Blog) RenderPost(r *http.Request, p *posts.Post, indexView bool, numCom
}
meta := render.Vars{
+ NoLayout: true,
Data: map[interface{}]interface{}{
"Post": p,
"Rendered": rendered,
@@ -465,7 +459,7 @@ func (b *Blog) RenderPost(r *http.Request, p *posts.Post, indexView bool, numCom
},
}
output := bytes.Buffer{}
- err = b.RenderPartialTemplate(&output, r, "blog/entry.partial", meta, false, nil)
+ err = b.RenderTemplate(&output, r, "blog/entry.partial", meta)
if err != nil {
return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error()))
}
diff --git a/core/comments.go b/core/comments.go
index 8af2624..9d10114 100644
--- a/core/comments.go
+++ b/core/comments.go
@@ -10,7 +10,6 @@ import (
"github.com/google/uuid"
"github.com/gorilla/mux"
- gorilla "github.com/gorilla/sessions"
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/middleware/auth"
@@ -23,6 +22,8 @@ import (
// CommentRoutes attaches the comment routes to the app.
func (b *Blog) CommentRoutes(r *mux.Router) {
+ render.Funcs["RenderComments"] = b.RenderComments
+
r.HandleFunc("/comments", b.CommentHandler)
r.HandleFunc("/comments/subscription", b.SubscriptionHandler)
r.HandleFunc("/comments/quick-delete", b.QuickDeleteHandler)
@@ -40,13 +41,16 @@ type CommentMeta struct {
}
// RenderComments renders a comment form partial and returns the HTML.
-func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject string, ids ...string) template.HTML {
+func (b *Blog) RenderComments(r *http.Request, subject string, ids ...string) template.HTML {
id := strings.Join(ids, "-")
+ session := sessions.Get(r)
+ url := r.URL.Path
// 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)
+ csrf, _ := session.Values["csrf"].(string)
// Check if the user is a logged-in admin, to make all comments editable.
var isAdmin bool
@@ -71,7 +75,7 @@ func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject
c.HTML = template.HTML(markdown.RenderMarkdown(c.Body))
c.ThreadID = thread.ID
c.OriginURL = url
- c.CSRF = csrfToken
+ c.CSRF = csrf
// Look up the author username.
if c.UserID > 0 {
@@ -120,7 +124,7 @@ func (b *Blog) RenderComments(session *gorilla.Session, csrfToken, url, subject
ID: thread.ID,
OriginURL: url,
Subject: subject,
- CSRF: csrfToken,
+ CSRF: csrf,
Thread: &thread,
NewComment: comments.Comment{
Name: name,
diff --git a/core/internal/controllers/setup/setup.go b/core/internal/controllers/setup/setup.go
new file mode 100644
index 0000000..9bca696
--- /dev/null
+++ b/core/internal/controllers/setup/setup.go
@@ -0,0 +1 @@
+package setup
diff --git a/core/internal/middleware/auth/auth.go b/core/internal/middleware/auth/auth.go
index be3d87f..dad1fd8 100644
--- a/core/internal/middleware/auth/auth.go
+++ b/core/internal/middleware/auth/auth.go
@@ -5,7 +5,6 @@ import (
"errors"
"net/http"
- "github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/sessions"
"github.com/kirsle/blog/core/internal/types"
@@ -46,8 +45,6 @@ func LoginRequired(onError http.HandlerFunc) negroni.HandlerFunc {
return
}
}
-
- log.Info("Redirect away!")
onError(w, r)
}
diff --git a/core/internal/render/functions.go b/core/internal/render/functions.go
new file mode 100644
index 0000000..9e18b3c
--- /dev/null
+++ b/core/internal/render/functions.go
@@ -0,0 +1,14 @@
+package render
+
+import (
+ "html/template"
+ "strings"
+ "time"
+)
+
+// Funcs is a global funcmap that the blog can hook its internal
+// methods onto.
+var Funcs = template.FuncMap{
+ "StringsJoin": strings.Join,
+ "Now": time.Now,
+}
diff --git a/core/internal/render/templates.go b/core/internal/render/templates.go
index 3367a92..0b80619 100644
--- a/core/internal/render/templates.go
+++ b/core/internal/render/templates.go
@@ -9,23 +9,14 @@ import (
"github.com/kirsle/blog/core/internal/forms"
"github.com/kirsle/blog/core/internal/log"
+ "github.com/kirsle/blog/core/internal/middleware"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
+ "github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users"
+ "github.com/kirsle/blog/core/internal/sessions"
+ "github.com/kirsle/blog/core/internal/types"
)
-// Config provides the settings and injectables for rendering templates.
-type Config struct {
- // Refined and raw variables for the templates.
- Vars *Vars // Normal RenderTemplate's
-
- // Wrap the template with the `.layout.gohtml`
- WithLayout bool
-
- // Inject your own functions for the Go templates.
- Functions map[string]interface{}
-
- Request *http.Request
-}
-
// Vars is an interface to implement by the templates to pass their own custom
// variables in. It auto-loads global template variables (site name, etc.)
// when the template is rendered.
@@ -34,6 +25,7 @@ type Vars struct {
SetupNeeded bool
Title string
Path string
+ TemplatePath string
LoggedIn bool
CurrentUser *users.User
CSRF string
@@ -53,18 +45,58 @@ type Vars struct {
Form forms.Form
}
-// PartialTemplate handles rendering a Go template to a writer, without
-// doing anything extra to the vars or dealing with net/http. This is ideal for
-// rendering partials, such as comment partials.
-//
-// This will wrap the template in `.layout.gohtml` by default. To render just
-// a bare template on its own, i.e. for partial templates, create a Vars struct
-// with `Vars{NoIndex: true}`
-func PartialTemplate(w io.Writer, path string, C Config) error {
- if C.Request == nil {
- panic("render.RenderPartialTemplate(): The *http.Request is nil!?")
+// loadDefaults combines template variables with default, globally available vars.
+func (v *Vars) loadDefaults(r *http.Request) {
+ // Get the site settings.
+ s, err := settings.Load()
+ if err != nil {
+ s = settings.Defaults()
}
+ if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/initial-setup") {
+ v.SetupNeeded = true
+ }
+ v.Request = r
+ v.RequestTime = r.Context().Value(types.StartTimeKey).(time.Time)
+ v.Title = s.Site.Title
+ v.Path = r.URL.Path
+
+ user, err := auth.CurrentUser(r)
+ v.CurrentUser = user
+ v.LoggedIn = err == nil
+}
+
+// Template responds with an HTML template.
+//
+// The vars will be massaged a bit to load the global defaults (such as the
+// website title and user login status), the user's session may be updated with
+// new CSRF token, and other such things. If you just want to render a template
+// without all that nonsense, use RenderPartialTemplate.
+func Template(w io.Writer, r *http.Request, path string, v Vars) error {
+ // Inject globally available variables.
+ v.loadDefaults(r)
+
+ // If this is the HTTP response, handle session-related things.
+ if rw, ok := w.(http.ResponseWriter); ok {
+ rw.Header().Set("Content-Type", "text/html; encoding=UTF-8")
+ session := sessions.Get(r)
+
+ // Flashed messages.
+ if flashes := session.Flashes(); len(flashes) > 0 {
+ for _, flash := range flashes {
+ _ = flash
+ v.Flashes = append(v.Flashes, flash.(string))
+ }
+ session.Save(r, rw)
+ }
+
+ // CSRF token for forms.
+ v.CSRF = middleware.GenerateCSRFToken(rw, r, session)
+ }
+
+ v.RequestDuration = time.Now().Sub(v.RequestTime)
+ v.Editable = !strings.HasPrefix(path, "admin/")
+
// v interface{}, withLayout bool, functions map[string]interface{}) error {
var (
layout Filepath
@@ -80,7 +112,7 @@ func PartialTemplate(w io.Writer, path string, C Config) error {
}
// Get the layout template.
- if C.WithLayout {
+ if !v.NoLayout {
templateName = "layout"
layout, err = ResolvePath(".layout")
if err != nil {
@@ -98,27 +130,12 @@ func PartialTemplate(w io.Writer, path string, C Config) error {
return err
}
- // Template functions.
- funcmap := template.FuncMap{
- "StringsJoin": strings.Join,
- "Now": time.Now,
- "TemplateName": func() string {
- return filepath.URI
- },
- }
- if C.Functions != nil {
- for name, fn := range C.Functions {
- funcmap[name] = fn
- }
- }
-
- // Useful template functions.
- t := template.New(filepath.Absolute).Funcs(funcmap)
+ t := template.New(filepath.Absolute).Funcs(Funcs)
// Parse the template files. The layout comes first because it's the wrapper
// and allows the filepath template to set the page title.
var templates []string
- if C.WithLayout {
+ if !v.NoLayout {
templates = append(templates, layout.Absolute)
}
t, err = t.ParseFiles(append(templates, commentEntry.Absolute, filepath.Absolute)...)
@@ -127,7 +144,7 @@ func PartialTemplate(w io.Writer, path string, C Config) error {
return err
}
- err = t.ExecuteTemplate(w, templateName, C.Vars)
+ err = t.ExecuteTemplate(w, templateName, v)
if err != nil {
log.Error("Template parsing error: %s", err)
return err
@@ -135,25 +152,3 @@ func PartialTemplate(w io.Writer, path string, C Config) error {
return nil
}
-
-// Template responds with an HTML template.
-//
-// The vars will be massaged a bit to load the global defaults (such as the
-// website title and user login status), the user's session may be updated with
-// new CSRF token, and other such things. If you just want to render a template
-// without all that nonsense, use RenderPartialTemplate.
-func Template(w http.ResponseWriter, path string, C Config) error {
- if C.Request == nil {
- panic("render.RenderTemplate(): The *http.Request is nil!?")
- }
-
- w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
- PartialTemplate(w, path, Config{
- Request: C.Request,
- Vars: C.Vars,
- WithLayout: true,
- Functions: C.Functions,
- })
-
- return nil
-}
diff --git a/core/pages.go b/core/pages.go
index c2ce736..7bea655 100644
--- a/core/pages.go
+++ b/core/pages.go
@@ -41,7 +41,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
// Is it a template file?
if strings.HasSuffix(filepath.URI, ".gohtml") {
- b.RenderTemplate(w, r, filepath.URI, render.Vars{})
+ b.RenderTemplate(w, r, filepath.URI, NewVars())
return
}
diff --git a/core/templates.go b/core/templates.go
index b8759ec..07d45bf 100644
--- a/core/templates.go
+++ b/core/templates.go
@@ -1,49 +1,12 @@
package core
import (
- "html/template"
"io"
"net/http"
- "strings"
- "time"
- "github.com/kirsle/blog/core/internal/forms"
- "github.com/kirsle/blog/core/internal/middleware"
- "github.com/kirsle/blog/core/internal/middleware/auth"
- "github.com/kirsle/blog/core/internal/models/settings"
- "github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
- "github.com/kirsle/blog/core/internal/sessions"
- "github.com/kirsle/blog/core/internal/types"
)
-// Vars is an interface to implement by the templates to pass their own custom
-// variables in. It auto-loads global template variables (site name, etc.)
-// when the template is rendered.
-type Vars struct {
- // Global, "constant" template variables.
- SetupNeeded bool
- Title string
- Path string
- LoggedIn bool
- CurrentUser *users.User
- CSRF string
- Editable bool // page is editable
- Request *http.Request
- RequestTime time.Time
- RequestDuration time.Duration
-
- // Configuration variables
- NoLayout bool // don't wrap in .layout.html, just render the template
-
- // Common template variables.
- Message string
- Flashes []string
- Error error
- Data map[interface{}]interface{}
- Form forms.Form
-}
-
// NewVars initializes a Vars struct with the custom Data map initialized.
// You may pass in an initial value for this map if you want.
func NewVars(data ...map[interface{}]interface{}) render.Vars {
@@ -58,102 +21,20 @@ func NewVars(data ...map[interface{}]interface{}) render.Vars {
}
}
-// LoadDefaults combines template variables with default, globally available vars.
-func (b *Blog) LoadDefaults(v render.Vars, r *http.Request) render.Vars {
- // Get the site settings.
- s, err := settings.Load()
- if err != nil {
- s = settings.Defaults()
- }
-
- if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/initial-setup") {
- v.SetupNeeded = true
- }
- v.Request = r
- v.RequestTime = r.Context().Value(types.StartTimeKey).(time.Time)
- v.Title = s.Site.Title
- v.Path = r.URL.Path
-
- user, err := auth.CurrentUser(r)
- v.CurrentUser = user
- v.LoggedIn = err == nil
-
- return v
-}
-
-// RenderPartialTemplate handles rendering a Go template to a writer, without
-// doing anything extra to the vars or dealing with net/http. This is ideal for
-// rendering partials, such as comment partials.
-//
-// This will wrap the template in `.layout.gohtml` by default. To render just
-// a bare template on its own, i.e. for partial templates, create a Vars struct
-// with `Vars{NoIndex: true}`
-func (b *Blog) RenderPartialTemplate(w io.Writer, r *http.Request, path string, v render.Vars, withLayout bool, functions map[string]interface{}) error {
- v = b.LoadDefaults(v, r)
- return render.PartialTemplate(w, path, render.Config{
- Request: r,
- Vars: &v,
- WithLayout: withLayout,
- Functions: b.TemplateFuncs(nil, nil, functions),
- })
-}
-
// RenderTemplate responds with an HTML template.
//
// The vars will be massaged a bit to load the global defaults (such as the
// website title and user login status), the user's session may be updated with
-// new CSRF token, and other such things. If you just want to render a template
-// without all that nonsense, use RenderPartialTemplate.
-func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars render.Vars) error {
+// new CSRF token, and other such things.
+//
+// For server-rendered templates given directly to the user (i.e., in controllers),
+// give it the http.ResponseWriter; for partial templates you can give it a
+// bytes.Buffer to write to instead. The subtle difference is whether or not the
+// template will have access to the request's session.
+func (b *Blog) RenderTemplate(w io.Writer, r *http.Request, path string, vars render.Vars) error {
if r == nil {
panic("core.RenderTemplate(): the *http.Request is nil!?")
}
- // Inject globally available variables.
- vars = b.LoadDefaults(vars, r)
-
- // Add any flashed messages from the endpoint controllers.
- session := sessions.Get(r)
- if flashes := session.Flashes(); len(flashes) > 0 {
- for _, flash := range flashes {
- _ = flash
- vars.Flashes = append(vars.Flashes, flash.(string))
- }
- session.Save(r, w)
- }
-
- vars.RequestDuration = time.Now().Sub(vars.RequestTime)
- vars.CSRF = middleware.GenerateCSRFToken(w, r, session)
- vars.Editable = !strings.HasPrefix(path, "admin/")
-
- return render.Template(w, path, render.Config{
- Request: r,
- Vars: &vars,
- Functions: b.TemplateFuncs(w, r, nil),
- })
-}
-
-// TemplateFuncs returns the common template function map.
-func (b *Blog) TemplateFuncs(w http.ResponseWriter, r *http.Request, inject map[string]interface{}) map[string]interface{} {
- fn := map[string]interface{}{
- "RenderIndex": b.RenderIndex,
- "RenderPost": b.RenderPost,
- "RenderTags": b.RenderTags,
- "RenderComments": func(subject string, ids ...string) template.HTML {
- if w == nil || r == nil {
- return template.HTML("[RenderComments Error: need both http.ResponseWriter and http.Request]")
- }
-
- session := sessions.Get(r)
- csrf := middleware.GenerateCSRFToken(w, r, session)
- return b.RenderComments(session, csrf, r.URL.Path, subject, ids...)
- },
- }
-
- if inject != nil {
- for k, v := range inject {
- fn[k] = v
- }
- }
- return fn
+ return render.Template(w, r, path, vars)
}
diff --git a/root/.layout.gohtml b/root/.layout.gohtml
index fa8d95c..84d1eee 100644
--- a/root/.layout.gohtml
+++ b/root/.layout.gohtml
@@ -81,7 +81,7 @@
{{ if and .CurrentUser.Admin .Editable }}