Page MenuHomeMusing Studio

No OneTemporary

diff --git a/app.go b/app.go
index bfed9f6..9e50d97 100644
--- a/app.go
+++ b/app.go
@@ -1,146 +1,184 @@
package writefreely
import (
"database/sql"
"flag"
"fmt"
_ "github.com/go-sql-driver/mysql"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
+ "github.com/writeas/writefreely/page"
)
const (
staticDir = "static/"
serverSoftware = "Write Freely"
softwareURL = "https://writefreely.org"
softwareVer = "0.1"
)
var (
debugging bool
)
type app struct {
router *mux.Router
db *datastore
cfg *config.Config
keys *keychain
sessionStore *sessions.CookieStore
}
+func pageForReq(app *app, r *http.Request) page.StaticPage {
+ p := page.StaticPage{
+ AppCfg: app.cfg.App,
+ Path: r.URL.Path,
+ Version: "v" + softwareVer,
+ }
+
+ // Add user information, if given
+ var u *User
+ accessToken := r.FormValue("t")
+ if accessToken != "" {
+ userID := app.db.GetUserID(accessToken)
+ if userID != -1 {
+ var err error
+ u, err = app.db.GetUserByID(userID)
+ if err == nil {
+ p.Username = u.Username
+ }
+ }
+ } else {
+ u = getUserSession(app, r)
+ if u != nil {
+ p.Username = u.Username
+ }
+ }
+
+ return p
+}
+
var shttp = http.NewServeMux()
func Serve() {
debugPtr := flag.Bool("debug", false, "Enables debug logging.")
createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits")
doConfig := flag.Bool("config", false, "Run the configuration process")
flag.Parse()
debugging = *debugPtr
if *createConfig {
log.Info("Creating configuration...")
c := config.New()
log.Info("Saving configuration...")
err := config.Save(c)
if err != nil {
log.Error("Unable to save configuration: %v", err)
os.Exit(1)
}
os.Exit(0)
} else if *doConfig {
err := config.Configure()
if err != nil {
log.Error("Unable to configure: %v", err)
os.Exit(1)
}
os.Exit(0)
}
log.Info("Initializing...")
log.Info("Loading configuration...")
cfg, err := config.Load()
if err != nil {
log.Error("Unable to load configuration: %v", err)
os.Exit(1)
}
app := &app{
cfg: cfg,
}
app.cfg.Server.Dev = *debugPtr
+ initTemplates()
+
// Load keys
log.Info("Loading encryption keys...")
err = initKeys(app)
if err != nil {
log.Error("\n%s\n", err)
}
// Initialize modules
app.sessionStore = initSession(app)
// Check database configuration
if app.cfg.Database.User == "" || app.cfg.Database.Password == "" {
log.Error("Database user or password not set.")
os.Exit(1)
}
if app.cfg.Database.Host == "" {
app.cfg.Database.Host = "localhost"
}
if app.cfg.Database.Database == "" {
app.cfg.Database.Database = "writeas"
}
log.Info("Connecting to database...")
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database))
if err != nil {
log.Error("\n%s\n", err)
os.Exit(1)
}
app.db = &datastore{db}
defer shutdown(app)
app.db.SetMaxOpenConns(50)
r := mux.NewRouter()
- handler := NewHandler(app.sessionStore)
+ handler := NewHandler(app)
+ handler.SetErrorPages(&ErrorPages{
+ NotFound: pages["404-general.tmpl"],
+ Gone: pages["410.tmpl"],
+ InternalServerError: pages["500.tmpl"],
+ Blank: pages["blank.tmpl"],
+ })
// Handle app routes
initRoutes(handler, r, app.cfg, app.db)
// Handle static files
fs := http.FileServer(http.Dir(staticDir))
shttp.Handle("/", fs)
r.PathPrefix("/").Handler(fs)
// Handle shutdown
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
log.Info("Shutting down...")
shutdown(app)
log.Info("Done.")
os.Exit(0)
}()
// Start web application server
http.Handle("/", r)
log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port)
log.Info("---")
http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil)
}
func shutdown(app *app) {
log.Info("Closing database connection...")
app.db.Close()
}
diff --git a/handle.go b/handle.go
new file mode 100644
index 0000000..cc74bd1
--- /dev/null
+++ b/handle.go
@@ -0,0 +1,584 @@
+package writefreely
+
+import (
+ "fmt"
+ "html/template"
+ "net/http"
+ "net/url"
+ "runtime/debug"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gorilla/sessions"
+ "github.com/writeas/impart"
+ "github.com/writeas/web-core/log"
+ "github.com/writeas/writefreely/page"
+)
+
+type UserLevel int
+
+const (
+ UserLevelNone UserLevel = iota // user or not -- ignored
+ UserLevelOptional // user or not -- object fetched if user
+ UserLevelNoneRequired // non-user (required)
+ UserLevelUser // user (required)
+)
+
+type (
+ handlerFunc func(app *app, w http.ResponseWriter, r *http.Request) error
+ userHandlerFunc func(app *app, u *User, w http.ResponseWriter, r *http.Request) error
+ dataHandlerFunc func(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error)
+ authFunc func(app *app, r *http.Request) (*User, error)
+)
+
+type Handler struct {
+ errors *ErrorPages
+ sessionStore *sessions.CookieStore
+ app *app
+}
+
+// ErrorPages hold template HTML error pages for displaying errors to the user.
+// In each, there should be a defined template named "base".
+type ErrorPages struct {
+ NotFound *template.Template
+ Gone *template.Template
+ InternalServerError *template.Template
+ Blank *template.Template
+}
+
+// NewHandler returns a new Handler instance, using the given StaticPage data,
+// and saving alias to the application's CookieStore.
+func NewHandler(app *app) *Handler {
+ h := &Handler{
+ errors: &ErrorPages{
+ NotFound: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>404</title></head><body><p>Not found.</p></body></html>{{end}}")),
+ Gone: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>410</title></head><body><p>Gone.</p></body></html>{{end}}")),
+ InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>500</title></head><body><p>Internal server error.</p></body></html>{{end}}")),
+ Blank: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>{{.Title}}</title></head><body><p>{{.Content}}</p></body></html>{{end}}")),
+ },
+ sessionStore: app.sessionStore,
+ app: app,
+ }
+
+ return h
+}
+
+// SetErrorPages sets the given set of ErrorPages as templates for any errors
+// that come up.
+func (h *Handler) SetErrorPages(e *ErrorPages) {
+ h.errors = e
+}
+
+// User handles requests made in the web application by the authenticated user.
+// This provides user-friendly HTML pages and actions that work in the browser.
+func (h *Handler) User(f userHandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleHTTPError(w, r, func() error {
+ var status int
+ start := time.Now()
+
+ defer func() {
+ if e := recover(); e != nil {
+ log.Error("%s: %s", e, debug.Stack())
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ status = http.StatusInternalServerError
+ }
+
+ log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
+ }()
+
+ u := getUserSession(h.app, r)
+ if u == nil {
+ err := ErrNotLoggedIn
+ status = err.Status
+ return err
+ }
+
+ err := f(h.app, u, w, r)
+ if err == nil {
+ status = http.StatusOK
+ } else if err, ok := err.(impart.HTTPError); ok {
+ status = err.Status
+ } else {
+ status = http.StatusInternalServerError
+ }
+
+ return err
+ }())
+ }
+}
+
+// UserAPI handles requests made in the API by the authenticated user.
+// This provides user-friendly HTML pages and actions that work in the browser.
+func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
+ return h.UserAll(false, f, func(app *app, r *http.Request) (*User, error) {
+ // Authorize user from Authorization header
+ t := r.Header.Get("Authorization")
+ if t == "" {
+ return nil, ErrNoAccessToken
+ }
+ u := &User{ID: app.db.GetUserID(t)}
+ if u.ID == -1 {
+ return nil, ErrBadAccessToken
+ }
+
+ return u, nil
+ })
+}
+
+func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ handleFunc := func() error {
+ var status int
+ start := time.Now()
+
+ defer func() {
+ if e := recover(); e != nil {
+ log.Error("%s: %s", e, debug.Stack())
+ impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
+ status = 500
+ }
+
+ log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
+ }()
+
+ u, err := a(h.app, r)
+ if err != nil {
+ if err, ok := err.(impart.HTTPError); ok {
+ status = err.Status
+ } else {
+ status = 500
+ }
+ return err
+ }
+
+ err = f(h.app, u, w, r)
+ if err == nil {
+ status = 200
+ } else if err, ok := err.(impart.HTTPError); ok {
+ status = err.Status
+ } else {
+ status = 500
+ }
+
+ return err
+ }
+
+ if web {
+ h.handleHTTPError(w, r, handleFunc())
+ } else {
+ h.handleError(w, r, handleFunc())
+ }
+ }
+}
+
+func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc {
+ return func(app *app, w http.ResponseWriter, r *http.Request) error {
+ err := f(app, w, r)
+ if err != nil {
+ if ie, ok := err.(impart.HTTPError); ok {
+ // Override default redirect with returned error's, if it's a
+ // redirect error.
+ if ie.Status == http.StatusFound {
+ return ie
+ }
+ }
+ return impart.HTTPError{http.StatusFound, loc}
+ }
+ return nil
+ }
+}
+
+func (h *Handler) Page(n string) http.HandlerFunc {
+ return h.Web(func(app *app, w http.ResponseWriter, r *http.Request) error {
+ t, ok := pages[n]
+ if !ok {
+ return impart.HTTPError{http.StatusNotFound, "Page not found."}
+ }
+
+ sp := pageForReq(app, r)
+
+ err := t.ExecuteTemplate(w, "base", sp)
+ if err != nil {
+ log.Error("Unable to render page: %v", err)
+ }
+ return err
+ }, UserLevelOptional)
+}
+
+func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // TODO: factor out this logic shared with Web()
+ h.handleHTTPError(w, r, func() error {
+ var status int
+ start := time.Now()
+
+ defer func() {
+ if e := recover(); e != nil {
+ u := getUserSession(h.app, r)
+ username := "None"
+ if u != nil {
+ username = u.Username
+ }
+ log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ status = 500
+ }
+
+ log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
+ }()
+
+ var session *sessions.Session
+ var err error
+ if ul != UserLevelNone {
+ session, err = h.sessionStore.Get(r, cookieName)
+ if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
+ // Cookie is required, but we can ignore this error
+ log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
+ }
+
+ _, gotUser := session.Values[cookieUserVal].(*User)
+ if ul == UserLevelNoneRequired && gotUser {
+ to := correctPageFromLoginAttempt(r)
+ log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
+ err := impart.HTTPError{http.StatusFound, to}
+ status = err.Status
+ return err
+ } else if ul == UserLevelUser && !gotUser {
+ log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
+ err := ErrNotLoggedIn
+ status = err.Status
+ return err
+ }
+ }
+
+ // TODO: pass User object to function
+ err = f(h.app, w, r)
+ if err == nil {
+ status = 200
+ } else if httpErr, ok := err.(impart.HTTPError); ok {
+ status = httpErr.Status
+ if status < 300 || status > 399 {
+ addSessionFlash(h.app, w, r, httpErr.Message, session)
+ return impart.HTTPError{http.StatusFound, r.Referer()}
+ }
+ } else {
+ e := fmt.Sprintf("[Web handler] 500: %v", err)
+ if !strings.HasSuffix(e, "write: broken pipe") {
+ log.Error(e)
+ } else {
+ log.Error(e)
+ }
+ log.Info("Web handler internal error render")
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ status = 500
+ }
+
+ return err
+ }())
+ }
+}
+
+// Web handles requests made in the web application. This provides user-
+// friendly HTML pages and actions that work in the browser.
+func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleHTTPError(w, r, func() error {
+ var status int
+ start := time.Now()
+
+ defer func() {
+ if e := recover(); e != nil {
+ u := getUserSession(h.app, r)
+ username := "None"
+ if u != nil {
+ username = u.Username
+ }
+ log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
+ log.Info("Web deferred internal error render")
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ status = 500
+ }
+
+ log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
+ }()
+
+ if ul != UserLevelNone {
+ session, err := h.sessionStore.Get(r, cookieName)
+ if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
+ // Cookie is required, but we can ignore this error
+ log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
+ }
+
+ _, gotUser := session.Values[cookieUserVal].(*User)
+ if ul == UserLevelNoneRequired && gotUser {
+ to := correctPageFromLoginAttempt(r)
+ log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
+ err := impart.HTTPError{http.StatusFound, to}
+ status = err.Status
+ return err
+ } else if ul == UserLevelUser && !gotUser {
+ log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
+ err := ErrNotLoggedIn
+ status = err.Status
+ return err
+ }
+ }
+
+ // TODO: pass User object to function
+ err := f(h.app, w, r)
+ if err == nil {
+ status = 200
+ } else if httpErr, ok := err.(impart.HTTPError); ok {
+ status = httpErr.Status
+ } else {
+ e := fmt.Sprintf("[Web handler] 500: %v", err)
+ log.Error(e)
+ log.Info("Web internal error render")
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ status = 500
+ }
+
+ return err
+ }())
+ }
+}
+
+func (h *Handler) All(f handlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleError(w, r, func() error {
+ // TODO: return correct "success" status
+ status := 200
+ start := time.Now()
+
+ defer func() {
+ if e := recover(); e != nil {
+ log.Error("%s:\n%s", e, debug.Stack())
+ impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
+ status = 500
+ }
+
+ log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
+ }()
+
+ // TODO: do any needed authentication
+
+ err := f(h.app, w, r)
+ if err != nil {
+ if err, ok := err.(impart.HTTPError); ok {
+ status = err.Status
+ } else {
+ status = 500
+ }
+ }
+
+ return err
+ }())
+ }
+}
+
+func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleHTTPError(w, r, func() error {
+ var status int
+ start := time.Now()
+ defer func() {
+ if e := recover(); e != nil {
+ log.Error("%s: %s", e, debug.Stack())
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ status = 500
+ }
+
+ log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
+ }()
+
+ data, filename, err := f(h.app, w, r)
+ if err != nil {
+ if err, ok := err.(impart.HTTPError); ok {
+ status = err.Status
+ } else {
+ status = 500
+ }
+ return err
+ }
+
+ ext := ".json"
+ ct := "application/json"
+ if strings.HasSuffix(r.URL.Path, ".csv") {
+ ext = ".csv"
+ ct = "text/csv"
+ } else if strings.HasSuffix(r.URL.Path, ".zip") {
+ ext = ".zip"
+ ct = "application/zip"
+ }
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s%s", filename, ext))
+ w.Header().Set("Content-Type", ct)
+ w.Header().Set("Content-Length", strconv.Itoa(len(data)))
+ fmt.Fprint(w, string(data))
+
+ status = 200
+ return nil
+ }())
+ }
+}
+
+func (h *Handler) Redirect(url string, ul UserLevel) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleHTTPError(w, r, func() error {
+ start := time.Now()
+
+ var status int
+ if ul != UserLevelNone {
+ session, err := h.sessionStore.Get(r, cookieName)
+ if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
+ // Cookie is required, but we can ignore this error
+ log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
+ }
+
+ _, gotUser := session.Values[cookieUserVal].(*User)
+ if ul == UserLevelNoneRequired && gotUser {
+ to := correctPageFromLoginAttempt(r)
+ log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
+ err := impart.HTTPError{http.StatusFound, to}
+ status = err.Status
+ return err
+ } else if ul == UserLevelUser && !gotUser {
+ log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
+ err := ErrNotLoggedIn
+ status = err.Status
+ return err
+ }
+ }
+
+ status = sendRedirect(w, http.StatusFound, url)
+
+ log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
+
+ return nil
+ }())
+ }
+}
+
+func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err error) {
+ if err == nil {
+ return
+ }
+
+ if err, ok := err.(impart.HTTPError); ok {
+ if err.Status >= 300 && err.Status < 400 {
+ sendRedirect(w, err.Status, err.Message)
+ return
+ } else if err.Status == http.StatusUnauthorized {
+ q := ""
+ if r.URL.RawQuery != "" {
+ q = url.QueryEscape("?" + r.URL.RawQuery)
+ }
+ sendRedirect(w, http.StatusFound, "/login?to="+r.URL.Path+q)
+ return
+ } else if err.Status == http.StatusGone {
+ p := &struct {
+ page.StaticPage
+ Content *template.HTML
+ }{
+ StaticPage: pageForReq(h.app, r),
+ }
+ if err.Message != "" {
+ co := template.HTML(err.Message)
+ p.Content = &co
+ }
+ h.errors.Gone.ExecuteTemplate(w, "base", p)
+ return
+ } else if err.Status == http.StatusNotFound {
+ h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ return
+ } else if err.Status == http.StatusInternalServerError {
+ log.Info("handleHTTPErorr internal error render")
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ return
+ } else if err.Status == http.StatusAccepted {
+ impart.WriteSuccess(w, "", err.Status)
+ return
+ } else {
+ p := &struct {
+ page.StaticPage
+ Title string
+ Content template.HTML
+ }{
+ pageForReq(h.app, r),
+ fmt.Sprintf("Uh oh (%d)", err.Status),
+ template.HTML(fmt.Sprintf("<p style=\"text-align: center\" class=\"introduction\">%s</p>", err.Message)),
+ }
+ h.errors.Blank.ExecuteTemplate(w, "base", p)
+ return
+ }
+ impart.WriteError(w, err)
+ return
+ }
+
+ impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
+}
+
+func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) {
+ if err == nil {
+ return
+ }
+
+ if err, ok := err.(impart.HTTPError); ok {
+ if err.Status >= 300 && err.Status < 400 {
+ sendRedirect(w, err.Status, err.Message)
+ return
+ }
+
+ // if strings.Contains(r.Header.Get("Accept"), "text/html") {
+ impart.WriteError(w, err)
+ // }
+ return
+ }
+
+ if IsJSON(r.Header.Get("Content-Type")) {
+ impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
+ return
+ }
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+}
+
+func correctPageFromLoginAttempt(r *http.Request) string {
+ to := r.FormValue("to")
+ if to == "" {
+ to = "/"
+ } else if !strings.HasPrefix(to, "/") {
+ to = "/" + to
+ }
+ return to
+}
+
+func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleHTTPError(w, r, func() error {
+ status := 200
+ start := time.Now()
+
+ defer func() {
+ if e := recover(); e != nil {
+ log.Error("Handler.LogHandlerFunc\n\n%s: %s", e, debug.Stack())
+ h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
+ status = 500
+ }
+
+ // TODO: log actual status code returned
+ log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
+ }()
+
+ f(w, r)
+
+ return nil
+ }())
+ }
+}
+
+func sendRedirect(w http.ResponseWriter, code int, location string) int {
+ w.Header().Set("Location", location)
+ w.WriteHeader(code)
+ return code
+}
diff --git a/page/page.go b/page/page.go
new file mode 100644
index 0000000..673424e
--- /dev/null
+++ b/page/page.go
@@ -0,0 +1,29 @@
+// package page provides mechanisms and data for generating a WriteFreely page.
+package page
+
+import (
+ "github.com/writeas/writefreely/config"
+ "strings"
+)
+
+type StaticPage struct {
+ // App configuration
+ config.AppCfg
+ Version string
+ HeaderNav bool
+
+ // Request values
+ Path string
+ Username string
+ Values map[string]string
+ Flashes []string
+}
+
+// SanitizeHost alters the StaticPage to contain a real hostname. This is
+// especially important for the Tor hidden service, as it can be served over
+// proxies, messing up the apparent hostname.
+func (sp *StaticPage) SanitizeHost(cfg *config.Config) {
+ if cfg.Server.HiddenHost != "" && strings.HasPrefix(sp.Host, cfg.Server.HiddenHost) {
+ sp.Host = cfg.Server.HiddenHost
+ }
+}
diff --git a/pages/404-general.tmpl b/pages/404-general.tmpl
new file mode 100644
index 0000000..dfc4653
--- /dev/null
+++ b/pages/404-general.tmpl
@@ -0,0 +1,7 @@
+{{define "head"}}<title>Page not found &mdash; {{.SiteName}}</title>{{end}}
+{{define "content"}}
+ <div class="error-page">
+ <p class="msg">This page is missing.</p>
+ <p>Are you sure it was ever here?</p>
+ </div>
+{{end}}
diff --git a/pages/404.tmpl b/pages/404.tmpl
new file mode 100644
index 0000000..b103e27
--- /dev/null
+++ b/pages/404.tmpl
@@ -0,0 +1,10 @@
+{{define "head"}}<title>Post not found &mdash; {{.SiteName}}</title>{{end}}
+{{define "content"}}
+ <div class="error-page" style="max-width:30em">
+ <p class="msg">Post not found.</p>
+ {{if and (not .SingleUser) .OpenRegistration}}
+ <p class="commentary" style="margin-top:2.5em">Why not share a thought of your own?</p>
+ <p><a href="/">Start a blog</a> and spread your ideas on <strong>{{.SiteName}}</strong>, a simple{{if .Federation}}, federated{{end}} blogging community.</p>
+ {{end}}
+ </div>
+{{end}}
diff --git a/pages/410.tmpl b/pages/410.tmpl
new file mode 100644
index 0000000..5dfd4a4
--- /dev/null
+++ b/pages/410.tmpl
@@ -0,0 +1,7 @@
+{{define "head"}}<title>Unpublished &mdash; {{.SiteName}}</title>{{end}}
+{{define "content"}}
+ <div class="error-page">
+ <p class="msg">{{if .Content}}{{.Content}}{{else}}Post was unpublished by the author.{{end}}</p>
+ <p class="commentary">It might be back some day.</p>
+ </div>
+{{end}}
diff --git a/pages/500.tmpl b/pages/500.tmpl
new file mode 100644
index 0000000..999d80a
--- /dev/null
+++ b/pages/500.tmpl
@@ -0,0 +1,9 @@
+{{define "head"}}<title>Server error &mdash; {{.SiteName}}</title>{{end}}
+{{define "content"}}
+ <div class="error-page">
+ <p class="msg">Server error. &#x1F632; &#x1F635;</p>
+ <p>The humans have been alerted and reminded of their many shortcomings.</p>
+ <p style="margin-top:3em">On behalf of them, we apologize.</p>
+ <p>&ndash; The Write.as Bots</p>
+ </div>
+{{end}}
diff --git a/pages/blank.tmpl b/pages/blank.tmpl
new file mode 100644
index 0000000..b45cd47
--- /dev/null
+++ b/pages/blank.tmpl
@@ -0,0 +1,2 @@
+{{define "head"}}<title>{{.Title}} &mdash; {{.SiteName}}</title>{{end}}
+{{define "content"}}<div class="content-container">{{.Content}}</div>{{end}}
diff --git a/templates.go b/templates.go
new file mode 100644
index 0000000..d283f67
--- /dev/null
+++ b/templates.go
@@ -0,0 +1,185 @@
+package writefreely
+
+import (
+ "fmt"
+ "github.com/dustin/go-humanize"
+ "github.com/writeas/web-core/l10n"
+ "github.com/writeas/web-core/log"
+ "html/template"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+var (
+ templates = map[string]*template.Template{}
+ pages = map[string]*template.Template{}
+ userPages = map[string]*template.Template{}
+ funcMap = template.FuncMap{
+ "largeNumFmt": largeNumFmt,
+ "pluralize": pluralize,
+ "isRTL": isRTL,
+ "isLTR": isLTR,
+ "localstr": localStr,
+ "localhtml": localHTML,
+ "tolower": strings.ToLower,
+ }
+)
+
+const (
+ templatesDir = "templates/"
+ pagesDir = "pages/"
+)
+
+func showUserPage(w http.ResponseWriter, name string, obj interface{}) {
+ if obj == nil {
+ log.Error("showUserPage: data is nil!")
+ return
+ }
+ if err := userPages["user/"+name+".tmpl"].ExecuteTemplate(w, name, obj); err != nil {
+ log.Error("Error parsing %s: %v", name, err)
+ }
+}
+
+func initTemplate(name string) {
+ if debugging {
+ log.Info(" %s%s.tmpl", templatesDir, name)
+ }
+
+ if name == "collection" || name == "collection-tags" {
+ // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
+ templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(
+ templatesDir+name+".tmpl",
+ templatesDir+"include/posts.tmpl",
+ templatesDir+"include/footer.tmpl",
+ templatesDir+"base.tmpl",
+ ))
+ } else {
+ templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(
+ templatesDir+name+".tmpl",
+ templatesDir+"include/footer.tmpl",
+ templatesDir+"base.tmpl",
+ ))
+ }
+}
+
+func initPage(path, key string) {
+ if debugging {
+ log.Info(" %s", key)
+ }
+
+ pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(
+ path,
+ templatesDir+"include/footer.tmpl",
+ templatesDir+"base.tmpl",
+ ))
+}
+
+func initUserPage(path, key string) {
+ if debugging {
+ log.Info(" %s", key)
+ }
+
+ userPages[key] = template.Must(template.New(key).Funcs(funcMap).ParseFiles(
+ path,
+ templatesDir+"user/include/header.tmpl",
+ templatesDir+"user/include/footer.tmpl",
+ ))
+}
+
+func initTemplates() error {
+ log.Info("Loading templates...")
+ tmplFiles, err := ioutil.ReadDir(templatesDir)
+ if err != nil {
+ return err
+ }
+
+ for _, f := range tmplFiles {
+ if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
+ parts := strings.Split(f.Name(), ".")
+ key := parts[0]
+ initTemplate(key)
+ }
+ }
+
+ log.Info("Loading pages...")
+ // Initialize all static pages that use the base template
+ filepath.Walk(pagesDir, func(path string, i os.FileInfo, err error) error {
+ if !i.IsDir() && !strings.HasPrefix(i.Name(), ".") {
+ parts := strings.Split(path, "/")
+ key := i.Name()
+ if len(parts) > 2 {
+ key = fmt.Sprintf("%s/%s", parts[1], i.Name())
+ }
+ initPage(path, key)
+ }
+
+ return nil
+ })
+
+ log.Info("Loading user pages...")
+ // Initialize all user pages that use base templates
+ filepath.Walk(templatesDir+"/user/", func(path string, f os.FileInfo, err error) error {
+ if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") {
+ parts := strings.Split(path, "/")
+ key := f.Name()
+ if len(parts) > 2 {
+ key = fmt.Sprintf("%s/%s", parts[1], f.Name())
+ }
+ initUserPage(path, key)
+ }
+
+ return nil
+ })
+
+ return nil
+}
+
+// renderPage retrieves the given template and renders it to the given io.Writer.
+// If something goes wrong, the error is logged and returned.
+func renderPage(w io.Writer, tmpl string, data interface{}) error {
+ err := pages[tmpl].ExecuteTemplate(w, "base", data)
+ if err != nil {
+ log.Error("%v", err)
+ }
+ return err
+}
+
+func largeNumFmt(n int64) string {
+ return humanize.Comma(n)
+}
+
+func pluralize(singular, plural string, n int64) string {
+ if n == 1 {
+ return singular
+ }
+ return plural
+}
+
+func isRTL(d string) bool {
+ return d == "rtl"
+}
+
+func isLTR(d string) bool {
+ return d == "ltr" || d == "auto"
+}
+
+func localStr(term, lang string) string {
+ s := l10n.Strings(lang)[term]
+ if s == "" {
+ s = l10n.Strings("")[term]
+ }
+ return s
+}
+
+func localHTML(term, lang string) template.HTML {
+ s := l10n.Strings(lang)[term]
+ if s == "" {
+ s = l10n.Strings("")[term]
+ }
+ s = strings.Replace(s, "write.as", "<a href=\"https://writefreely.org\">write freely</a>", 1)
+ return template.HTML(s)
+}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Feb 6, 11:49 PM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3620412

Event Timeline