diff --git a/app.go b/app.go index bfed9f6..9e50d97 100644 --- a/app.go +++ b/app.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/sessions" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" + "github.com/writeas/writefreely/page" ) const ( @@ -36,6 +37,35 @@ type app struct { 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() { @@ -79,6 +109,8 @@ func Serve() { app.cfg.Server.Dev = *debugPtr + initTemplates() + // Load keys log.Info("Loading encryption keys...") err = initKeys(app) @@ -112,7 +144,13 @@ func Serve() { 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) 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\"}}404

Not found.

{{end}}")), + Gone: template.Must(template.New("").Parse("{{define \"base\"}}410

Gone.

{{end}}")), + InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}500

Internal server error.

{{end}}")), + Blank: template.Must(template.New("").Parse("{{define \"base\"}}{{.Title}}

{{.Content}}

{{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("

%s

", 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"}}Page not found — {{.SiteName}}{{end}} +{{define "content"}} +
+

This page is missing.

+

Are you sure it was ever here?

+
+{{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"}}Post not found — {{.SiteName}}{{end}} +{{define "content"}} +
+

Post not found.

+ {{if and (not .SingleUser) .OpenRegistration}} +

Why not share a thought of your own?

+

Start a blog and spread your ideas on {{.SiteName}}, a simple{{if .Federation}}, federated{{end}} blogging community.

+ {{end}} +
+{{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"}}Unpublished — {{.SiteName}}{{end}} +{{define "content"}} +
+

{{if .Content}}{{.Content}}{{else}}Post was unpublished by the author.{{end}}

+

It might be back some day.

+
+{{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"}}Server error — {{.SiteName}}{{end}} +{{define "content"}} +
+

Server error. 😲 😵

+

The humans have been alerted and reminded of their many shortcomings.

+

On behalf of them, we apologize.

+

– The Write.as Bots

+
+{{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}} — {{.SiteName}}{{end}} +{{define "content"}}
{{.Content}}
{{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", "write freely", 1) + return template.HTML(s) +}