diff --git a/blogstats.go b/blogstats.go index 5068bf2..828cc5a 100644 --- a/blogstats.go +++ b/blogstats.go @@ -4,46 +4,45 @@ import ( "net/http" ) -func serveBlogStats(blog, statsPath string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Build query - query, params := buildPostsQuery(&postsRequestConfig{ - blog: blog, - status: statusPublished, - }) - // Count total posts - row, err := appDbQueryRow("select count(distinct path) from ("+query+")", params...) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - var totalCount int - if err = row.Scan(&totalCount); err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - // Count posts per year - rows, err := appDbQuery("select substr(published, 1, 4) as year, count(distinct path) as count from ("+query+") where published != '' group by year order by year desc", params...) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - var years, counts []int - for rows.Next() { - var year, count int - if err = rows.Scan(&year, &count); err == nil { - years = append(years, year) - counts = append(counts, count) - } - } - render(w, r, templateBlogStats, &renderData{ - BlogString: blog, - Canonical: statsPath, - Data: map[string]interface{}{ - "total": totalCount, - "years": years, - "counts": counts, - }, - }) +func serveBlogStats(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + // Build query + query, params := buildPostsQuery(&postsRequestConfig{ + blog: blog, + status: statusPublished, + }) + // Count total posts + row, err := appDbQueryRow("select count(distinct path) from ("+query+")", params...) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return } + var totalCount int + if err = row.Scan(&totalCount); err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + // Count posts per year + rows, err := appDbQuery("select substr(published, 1, 4) as year, count(distinct path) as count from ("+query+") where published != '' group by year order by year desc", params...) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + var years, counts []int + for rows.Next() { + var year, count int + if err = rows.Scan(&year, &count); err == nil { + years = append(years, year) + counts = append(counts, count) + } + } + render(w, r, templateBlogStats, &renderData{ + BlogString: blog, + Canonical: blogPath(blog) + appConfig.Blogs[blog].BlogStats.Path, + Data: map[string]interface{}{ + "total": totalCount, + "years": years, + "counts": counts, + }, + }) } diff --git a/comments.go b/comments.go index df93dcd..cda07bf 100644 --- a/comments.go +++ b/comments.go @@ -20,70 +20,67 @@ type comment struct { Comment string } -func serveComment(blog string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - id, err := strconv.Atoi(chi.URLParam(r, "id")) - if err != nil { - serveError(w, r, err.Error(), http.StatusBadRequest) - return - } - row, err := appDbQueryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id)) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - comment := &comment{} - if err = row.Scan(&comment.ID, &comment.Target, &comment.Name, &comment.Website, &comment.Comment); err == sql.ErrNoRows { - serve404(w, r) - return - } else if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("X-Robots-Tag", "noindex") - render(w, r, templateComment, &renderData{ - BlogString: blog, - Canonical: appConfig.Server.PublicAddress + appConfig.Blogs[blog].getRelativePath(fmt.Sprintf("/comment/%d", id)), - Data: comment, - }) +func serveComment(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(chi.URLParam(r, "id")) + if err != nil { + serveError(w, r, err.Error(), http.StatusBadRequest) + return } + row, err := appDbQueryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id)) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + comment := &comment{} + if err = row.Scan(&comment.ID, &comment.Target, &comment.Name, &comment.Website, &comment.Comment); err == sql.ErrNoRows { + serve404(w, r) + return + } else if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("X-Robots-Tag", "noindex") + blog := r.Context().Value(blogContextKey).(string) + render(w, r, templateComment, &renderData{ + BlogString: blog, + Canonical: appConfig.Server.PublicAddress + appConfig.Blogs[blog].getRelativePath(fmt.Sprintf("/comment/%d", id)), + Data: comment, + }) } -func createComment(blog, commentsPath string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Check target - target := checkCommentTarget(w, r) - if target == "" { - return - } - // Check and clean comment - strict := bluemonday.StrictPolicy() - comment := strings.TrimSpace(strict.Sanitize(r.FormValue("comment"))) - if comment == "" { - serveError(w, r, "Comment is empty", http.StatusBadRequest) - return - } - name := strings.TrimSpace(strict.Sanitize(r.FormValue("name"))) - if name == "" { - name = "Anonymous" - } - website := strings.TrimSpace(strict.Sanitize(r.FormValue("website"))) - // Insert - result, err := appDbExec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website)) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - if commentID, err := result.LastInsertId(); err != nil { - // Serve error - serveError(w, r, err.Error(), http.StatusInternalServerError) - } else { - commentAddress := fmt.Sprintf("%s/%d", commentsPath, commentID) - // Send webmention - _ = createWebmention(appConfig.Server.PublicAddress+commentAddress, appConfig.Server.PublicAddress+target) - // Redirect to comment - http.Redirect(w, r, commentAddress, http.StatusFound) - } +func createComment(w http.ResponseWriter, r *http.Request) { + // Check target + target := checkCommentTarget(w, r) + if target == "" { + return + } + // Check and clean comment + strict := bluemonday.StrictPolicy() + comment := strings.TrimSpace(strict.Sanitize(r.FormValue("comment"))) + if comment == "" { + serveError(w, r, "Comment is empty", http.StatusBadRequest) + return + } + name := strings.TrimSpace(strict.Sanitize(r.FormValue("name"))) + if name == "" { + name = "Anonymous" + } + website := strings.TrimSpace(strict.Sanitize(r.FormValue("website"))) + // Insert + result, err := appDbExec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website)) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + if commentID, err := result.LastInsertId(); err != nil { + // Serve error + serveError(w, r, err.Error(), http.StatusInternalServerError) + } else { + commentAddress := fmt.Sprintf("%s/%d", blogPath(r.Context().Value(blogContextKey).(string))+"/comment", commentID) + // Send webmention + _ = createWebmention(appConfig.Server.PublicAddress+commentAddress, appConfig.Server.PublicAddress+target) + // Redirect to comment + http.Redirect(w, r, commentAddress, http.StatusFound) } } diff --git a/commentsAdmin.go b/commentsAdmin.go index d97c724..7b18a7c 100644 --- a/commentsAdmin.go +++ b/commentsAdmin.go @@ -33,53 +33,53 @@ func (p *commentsPaginationAdapter) Slice(offset, length int, data interface{}) return err } -func commentsAdmin(blog, commentPath string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Adapter - pageNoString := chi.URLParam(r, "page") - pageNo, _ := strconv.Atoi(pageNoString) - p := paginator.New(&commentsPaginationAdapter{config: &commentsRequestConfig{}}, 5) - p.SetPage(pageNo) - var comments []*comment - err := p.Results(&comments) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - // Navigation - var hasPrev, hasNext bool - var prevPage, nextPage int - var prevPath, nextPath string - hasPrev, _ = p.HasPrev() - if hasPrev { - prevPage, _ = p.PrevPage() - } else { - prevPage, _ = p.Page() - } - if prevPage < 2 { - prevPath = commentPath - } else { - prevPath = fmt.Sprintf("%s/page/%d", commentPath, prevPage) - } - hasNext, _ = p.HasNext() - if hasNext { - nextPage, _ = p.NextPage() - } else { - nextPage, _ = p.Page() - } - nextPath = fmt.Sprintf("%s/page/%d", commentPath, nextPage) - // Render - render(w, r, templateCommentsAdmin, &renderData{ - BlogString: blog, - Data: map[string]interface{}{ - "Comments": comments, - "HasPrev": hasPrev, - "HasNext": hasNext, - "Prev": slashIfEmpty(prevPath), - "Next": slashIfEmpty(nextPath), - }, - }) +func commentsAdmin(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + commentsPath := r.Context().Value(pathContextKey).(string) + // Adapter + pageNoString := chi.URLParam(r, "page") + pageNo, _ := strconv.Atoi(pageNoString) + p := paginator.New(&commentsPaginationAdapter{config: &commentsRequestConfig{}}, 5) + p.SetPage(pageNo) + var comments []*comment + err := p.Results(&comments) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return } + // Navigation + var hasPrev, hasNext bool + var prevPage, nextPage int + var prevPath, nextPath string + hasPrev, _ = p.HasPrev() + if hasPrev { + prevPage, _ = p.PrevPage() + } else { + prevPage, _ = p.Page() + } + if prevPage < 2 { + prevPath = commentsPath + } else { + prevPath = fmt.Sprintf("%s/page/%d", commentsPath, prevPage) + } + hasNext, _ = p.HasNext() + if hasNext { + nextPage, _ = p.NextPage() + } else { + nextPage, _ = p.Page() + } + nextPath = fmt.Sprintf("%s/page/%d", commentsPath, nextPage) + // Render + render(w, r, templateCommentsAdmin, &renderData{ + BlogString: blog, + Data: map[string]interface{}{ + "Comments": comments, + "HasPrev": hasPrev, + "HasNext": hasNext, + "Prev": slashIfEmpty(prevPath), + "Next": slashIfEmpty(nextPath), + }, + }) } func commentsAdminDelete(w http.ResponseWriter, r *http.Request) { diff --git a/customPages.go b/customPages.go index 4b9c37c..bc41ccb 100644 --- a/customPages.go +++ b/customPages.go @@ -2,19 +2,20 @@ package main import "net/http" -func serveCustomPage(blog *configBlog, page *customPage) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if appConfig.Cache != nil && appConfig.Cache.Enable && page.Cache { - if page.CacheExpiration != 0 { - setInternalCacheExpirationHeader(w, page.CacheExpiration) - } else { - setInternalCacheExpirationHeader(w, int(appConfig.Cache.Expiration)) - } +const customPageContextKey = "custompage" + +func serveCustomPage(w http.ResponseWriter, r *http.Request) { + page := r.Context().Value(customPageContextKey).(*customPage) + if appConfig.Cache != nil && appConfig.Cache.Enable && page.Cache { + if page.CacheExpiration != 0 { + setInternalCacheExpirationHeader(w, page.CacheExpiration) + } else { + setInternalCacheExpirationHeader(w, int(appConfig.Cache.Expiration)) } - render(w, r, page.Template, &renderData{ - Blog: blog, - Canonical: appConfig.Server.PublicAddress + page.Path, - Data: page.Data, - }) } + render(w, r, page.Template, &renderData{ + BlogString: r.Context().Value(blogContextKey).(string), + Canonical: appConfig.Server.PublicAddress + page.Path, + Data: page.Data, + }) } diff --git a/editor.go b/editor.go index cda0378..61e7665 100644 --- a/editor.go +++ b/editor.go @@ -11,74 +11,72 @@ import ( const editorPath = "/editor" -func serveEditor(blog string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - render(w, r, templateEditor, &renderData{ - BlogString: blog, - Data: map[string]interface{}{ - "Drafts": loadDrafts(blog), - }, - }) - } +func serveEditor(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + render(w, r, templateEditor, &renderData{ + BlogString: blog, + Data: map[string]interface{}{ + "Drafts": loadDrafts(blog), + }, + }) } -func serveEditorPost(blog string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if action := r.FormValue("editoraction"); action != "" { - switch action { - case "loadupdate": - parsedURL, err := url.Parse(r.FormValue("url")) - if err != nil { - serveError(w, r, err.Error(), http.StatusBadRequest) - return - } - post, err := getPost(parsedURL.Path) - if err != nil { - serveError(w, r, err.Error(), http.StatusBadRequest) - return - } - mf := post.toMfItem() - render(w, r, templateEditor, &renderData{ - BlogString: blog, - Data: map[string]interface{}{ - "UpdatePostURL": parsedURL.String(), - "UpdatePostContent": mf.Properties.Content[0], - "Drafts": loadDrafts(blog), - }, - }) - case "updatepost": - urlValue := r.FormValue("url") - content := r.FormValue("content") - mf := map[string]interface{}{ - "action": actionUpdate, - "url": urlValue, - "replace": map[string][]string{ - "content": { - content, - }, - }, - } - jsonBytes, err := json.Marshal(mf) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - req, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(jsonBytes)) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - req.Header.Set(contentType, contentTypeJSON) - editorMicropubPost(w, req, false) - case "upload": - editorMicropubPost(w, r, true) - default: - serveError(w, r, "Unknown editoraction", http.StatusBadRequest) +func serveEditorPost(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + if action := r.FormValue("editoraction"); action != "" { + switch action { + case "loadupdate": + parsedURL, err := url.Parse(r.FormValue("url")) + if err != nil { + serveError(w, r, err.Error(), http.StatusBadRequest) + return } - return + post, err := getPost(parsedURL.Path) + if err != nil { + serveError(w, r, err.Error(), http.StatusBadRequest) + return + } + mf := post.toMfItem() + render(w, r, templateEditor, &renderData{ + BlogString: blog, + Data: map[string]interface{}{ + "UpdatePostURL": parsedURL.String(), + "UpdatePostContent": mf.Properties.Content[0], + "Drafts": loadDrafts(blog), + }, + }) + case "updatepost": + urlValue := r.FormValue("url") + content := r.FormValue("content") + mf := map[string]interface{}{ + "action": actionUpdate, + "url": urlValue, + "replace": map[string][]string{ + "content": { + content, + }, + }, + } + jsonBytes, err := json.Marshal(mf) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + req, err := http.NewRequest(http.MethodPost, "", bytes.NewReader(jsonBytes)) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + req.Header.Set(contentType, contentTypeJSON) + editorMicropubPost(w, req, false) + case "upload": + editorMicropubPost(w, r, true) + default: + serveError(w, r, "Unknown editoraction", http.StatusBadRequest) } - editorMicropubPost(w, r, false) + return } + editorMicropubPost(w, r, false) } func loadDrafts(blog string) []*post { diff --git a/go.mod b/go.mod index 254440d..ce633dc 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/pquerna/otp v1.3.0 github.com/smartystreets/assertions v1.2.0 // indirect github.com/snabb/sitemap v1.0.0 - github.com/spf13/afero v1.5.1 // indirect + github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.3.1 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.7.1 @@ -52,7 +52,7 @@ require ( github.com/thoas/go-funk v0.8.0 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2 - github.com/yuin/goldmark v1.3.2 + github.com/yuin/goldmark v1.3.3 github.com/yuin/goldmark-emoji v1.0.1 go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.16.0 // indirect @@ -61,7 +61,7 @@ require ( golang.org/x/mod v0.4.1 // indirect golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d // indirect + golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect golang.org/x/text v0.3.5 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index f6aa32e..5d6f7e6 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg= -github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= @@ -340,8 +340,8 @@ github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2 h1:l5j4nE6 github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2/go.mod h1:NEDNuq1asYbAeX+uy6w56MDQSFmBQz9k+N9Hy6m4r2U= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.2 h1:YjHC5TgyMmHpicTgEqDN0Q96Xo8K6tLXPnmNOHXCgs0= -github.com/yuin/goldmark v1.3.2/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.3.3 h1:37BdQwPx8VOSic8eDSWee6QL9mRpZRm9VJp/QugNrW0= +github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -453,8 +453,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d h1:jbzgAvDZn8aEnytae+4ou0J0GwFZoHR0hOrTg4qH8GA= -golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/http.go b/http.go index a467f8c..62a2817 100644 --- a/http.go +++ b/http.go @@ -9,7 +9,6 @@ import ( "strconv" "strings" "sync" - "time" "github.com/caddyserver/certmagic" "github.com/dchest/captcha" @@ -110,6 +109,8 @@ var ( privateMode = false privateModeHandler = []func(http.Handler) http.Handler{} + setBlogMiddlewares = map[string]func(http.Handler) http.Handler{} + captchaHandler http.Handler micropubRouter *chi.Mux @@ -117,10 +118,9 @@ var ( webmentionsRouter *chi.Mux notificationsRouter *chi.Mux activitypubRouter *chi.Mux - - editorRouters = map[string]*chi.Mux{} - commentRouters = map[string]*chi.Mux{} - searchRouters = map[string]*chi.Mux{} + editorRouter *chi.Mux + commentsRouter *chi.Mux + searchRouter *chi.Mux ) func buildStaticHandlersRouters() error { @@ -157,9 +157,8 @@ func buildStaticHandlersRouters() error { notificationsRouter = chi.NewRouter() notificationsRouter.Use(authMiddleware) - notificationsHandler := notificationsAdmin(notificationsPath) - notificationsRouter.Get("/", notificationsHandler) - notificationsRouter.Get(paginationPath, notificationsHandler) + notificationsRouter.Get("/", notificationsAdmin) + notificationsRouter.Get(paginationPath, notificationsAdmin) if ap := appConfig.ActivityPub; ap != nil && ap.Enabled { activitypubRouter = chi.NewRouter() @@ -167,55 +166,42 @@ func buildStaticHandlersRouters() error { activitypubRouter.Post("/{blog}/inbox", apHandleInbox) } - for blog, blogConfig := range appConfig.Blogs { - blogPath := blogPath(blogConfig) + editorRouter = chi.NewRouter() + editorRouter.Use(authMiddleware) + editorRouter.Get("/", serveEditor) + editorRouter.Post("/", serveEditorPost) - editorRouter := chi.NewRouter() - editorRouter.Use(authMiddleware) - editorRouter.Get("/", serveEditor(blog)) - editorRouter.Post("/", serveEditorPost(blog)) - editorRouters[blog] = editorRouter + commentsRouter = chi.NewRouter() + commentsRouter.Use(privateModeHandler...) + commentsRouter.With(cacheMiddleware).Get("/{id:[0-9]+}", serveComment) + commentsRouter.With(captchaMiddleware).Post("/", createComment) + commentsRouter.Group(func(r chi.Router) { + // Admin + r.Use(authMiddleware) + r.Get("/", commentsAdmin) + r.Get(paginationPath, commentsAdmin) + r.Post("/delete", commentsAdminDelete) + }) - if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled { - commentsPath := blogPath + "/comment" - commentRouter := chi.NewRouter() - commentRouter.Use(privateModeHandler...) - commentRouter.With(cacheMiddleware).Get("/{id:[0-9]+}", serveComment(blog)) - commentRouter.With(captchaMiddleware).Post("/", createComment(blog, commentsPath)) - // Admin - commentRouter.Group(func(r chi.Router) { - r.Use(authMiddleware) - handler := commentsAdmin(blog, commentsPath) - r.Get("/", handler) - r.Get(paginationPath, handler) - r.Post("/delete", commentsAdminDelete) - }) - commentRouters[blog] = commentRouter - } + searchRouter = chi.NewRouter() + searchRouter.Use(privateModeHandler...) + searchRouter.Use(cacheMiddleware) + searchRouter.Get("/", serveSearch) + searchRouter.Post("/", serveSearch) + searchResultPath := "/" + searchPlaceholder + searchRouter.Get(searchResultPath, serveSearchResult) + searchRouter.Get(searchResultPath+feedPath, serveSearchResult) + searchRouter.Get(searchResultPath+paginationPath, serveSearchResult) - if blogConfig.Search != nil && blogConfig.Search.Enabled { - searchPath := blogPath + blogConfig.Search.Path - searchRouter := chi.NewRouter() - searchRouter.Use(privateModeHandler...) - searchRouter.Use(cacheMiddleware) - handler := serveSearch(blog, searchPath) - searchRouter.Get("/", handler) - searchRouter.Post("/", handler) - searchResultPath := "/" + searchPlaceholder - resultHandler := serveSearchResults(blog, searchPath+searchResultPath) - searchRouter.Get(searchResultPath, resultHandler) - searchRouter.Get(searchResultPath+feedPath, resultHandler) - searchRouter.Get(searchResultPath+paginationPath, resultHandler) - searchRouters[blog] = searchRouter - } + for blog := range appConfig.Blogs { + sbm := middleware.WithValue(blogContextKey, blog) + setBlogMiddlewares[blog] = sbm } return nil } func buildDynamicRouter() (*chi.Mux, error) { - startTime := time.Now() - r := chi.NewRouter() // Basic middleware @@ -321,7 +307,9 @@ func buildDynamicRouter() (*chi.Mux, error) { r.With(privateModeHandler...).With(cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", redirectToLongPath) for blog, blogConfig := range appConfig.Blogs { - blogPath := blogPath(blogConfig) + blogPath := blogPath(blog) + + sbm := setBlogMiddlewares[blog] // Sections r.Group(func(r chi.Router) { @@ -330,10 +318,15 @@ func buildDynamicRouter() (*chi.Mux, error) { for _, section := range blogConfig.Sections { if section.Name != "" { secPath := blogPath + "/" + section.Name - handler := serveSection(blog, secPath, section) - r.Get(secPath, handler) - r.Get(secPath+feedPath, handler) - r.Get(secPath+paginationPath, handler) + r.Group(func(r chi.Router) { + r.Use(sbm, middleware.WithValue(indexConfigKey, &indexConfig{ + path: secPath, + section: section, + })) + r.Get(secPath, serveIndex) + r.Get(secPath+feedPath, serveIndex) + r.Get(secPath+paginationPath, serveIndex) + }) } } }) @@ -349,13 +342,19 @@ func buildDynamicRouter() (*chi.Mux, error) { r.Group(func(r chi.Router) { r.Use(privateModeHandler...) r.Use(cacheMiddleware) - r.Get(taxPath, serveTaxonomy(blog, taxonomy)) + r.With(sbm, middleware.WithValue(taxonomyContextKey, taxonomy)).Get(taxPath, serveTaxonomy) for _, tv := range taxValues { vPath := taxPath + "/" + urlize(tv) - handler := serveTaxonomyValue(blog, vPath, taxonomy, tv) - r.Get(vPath, handler) - r.Get(vPath+feedPath, handler) - r.Get(vPath+paginationPath, handler) + r.Group(func(r chi.Router) { + r.Use(sbm, middleware.WithValue(indexConfigKey, &indexConfig{ + path: vPath, + tax: taxonomy, + taxValue: tv, + })) + r.Get(vPath, serveIndex) + r.Get(vPath+feedPath, serveIndex) + r.Get(vPath+paginationPath, serveIndex) + }) } }) } @@ -367,101 +366,74 @@ func buildDynamicRouter() (*chi.Mux, error) { r.Use(privateModeHandler...) r.Use(cacheMiddleware) photoPath := blogPath + blogConfig.Photos.Path - handler := servePhotos(blog, photoPath) - r.Get(photoPath, handler) - r.Get(photoPath+feedPath, handler) - r.Get(photoPath+paginationPath, handler) + r.Use(sbm, middleware.WithValue(indexConfigKey, &indexConfig{ + path: photoPath, + parameter: blogConfig.Photos.Parameter, + title: blogConfig.Photos.Title, + description: blogConfig.Photos.Description, + summaryTemplate: templatePhotosSummary, + })) + r.Get(photoPath, serveIndex) + r.Get(photoPath+feedPath, serveIndex) + r.Get(photoPath+paginationPath, serveIndex) }) } // Search if blogConfig.Search != nil && blogConfig.Search.Enabled { - r.Mount(blogPath+blogConfig.Search.Path, searchRouters[blog]) + searchPath := blogPath + blogConfig.Search.Path + r.With(sbm, middleware.WithValue(pathContextKey, searchPath)).Mount(searchPath, searchRouter) } // Stats if blogConfig.BlogStats != nil && blogConfig.BlogStats.Enabled { statsPath := blogPath + blogConfig.BlogStats.Path - r.With(privateModeHandler...).With(cacheMiddleware).Get(statsPath, serveBlogStats(blog, statsPath)) + r.With(privateModeHandler...).With(cacheMiddleware, sbm).Get(statsPath, serveBlogStats) } - // Year / month archives - dates, err := allPublishedDates(blog) - if err != nil { - return nil, err - } + // Date archives r.Group(func(r chi.Router) { r.Use(privateModeHandler...) - r.Use(cacheMiddleware) - already := map[string]bool{} - for _, d := range dates { - // Year - yearPath := blogPath + "/" + fmt.Sprintf("%0004d", d.year) - if !already[yearPath] { - yearHandler := serveDate(blog, yearPath, d.year, 0, 0) - r.Get(yearPath, yearHandler) - r.Get(yearPath+feedPath, yearHandler) - r.Get(yearPath+paginationPath, yearHandler) - already[yearPath] = true - } - // Specific month - monthPath := yearPath + "/" + fmt.Sprintf("%02d", d.month) - if !already[monthPath] { - monthHandler := serveDate(blog, monthPath, d.year, d.month, 0) - r.Get(monthPath, monthHandler) - r.Get(monthPath+feedPath, monthHandler) - r.Get(monthPath+paginationPath, monthHandler) - already[monthPath] = true - } - // Specific day - dayPath := monthPath + "/" + fmt.Sprintf("%02d", d.day) - if !already[dayPath] { - dayHandler := serveDate(blog, monthPath, d.year, d.month, d.day) - r.Get(dayPath, dayHandler) - r.Get(dayPath+feedPath, dayHandler) - r.Get(dayPath+paginationPath, dayHandler) - already[dayPath] = true - } - // Generic month - genericMonthPath := blogPath + "/x/" + fmt.Sprintf("%02d", d.month) - if !already[genericMonthPath] { - genericMonthHandler := serveDate(blog, genericMonthPath, 0, d.month, 0) - r.Get(genericMonthPath, genericMonthHandler) - r.Get(genericMonthPath+feedPath, genericMonthHandler) - r.Get(genericMonthPath+paginationPath, genericMonthHandler) - already[genericMonthPath] = true - } - // Specific day - genericMonthDayPath := genericMonthPath + "/" + fmt.Sprintf("%02d", d.day) - if !already[genericMonthDayPath] { - genericMonthDayHandler := serveDate(blog, genericMonthDayPath, 0, d.month, d.day) - r.Get(genericMonthDayPath, genericMonthDayHandler) - r.Get(genericMonthDayPath+feedPath, genericMonthDayHandler) - r.Get(genericMonthDayPath+paginationPath, genericMonthDayHandler) - already[genericMonthDayPath] = true - } - } + r.Use(cacheMiddleware, sbm) + + yearRegex := `/{year:x|\d\d\d\d}` + monthRegex := `/{month:x|\d\d}` + dayRegex := `/{day:\d\d}` + + yearPath := blogPath + yearRegex + r.Get(yearPath, serveDate) + r.Get(yearPath+feedPath, serveDate) + r.Get(yearPath+paginationPath, serveDate) + + monthPath := yearPath + monthRegex + r.Get(monthPath, serveDate) + r.Get(monthPath+feedPath, serveDate) + r.Get(monthPath+paginationPath, serveDate) + + dayPath := monthPath + dayRegex + r.Get(dayPath, serveDate) + r.Get(dayPath+feedPath, serveDate) + r.Get(dayPath+paginationPath, serveDate) }) // Blog if !blogConfig.PostAsHome { r.Group(func(r chi.Router) { r.Use(privateModeHandler...) - r.Use(cacheMiddleware) - handler := serveHome(blog, blogPath) - r.Get(blogConfig.Path, handler) - r.Get(blogConfig.Path+feedPath, handler) - r.Get(blogPath+paginationPath, handler) + r.Use(cacheMiddleware, sbm) + r.Get(blogConfig.Path, serveHome) + r.Get(blogConfig.Path+feedPath, serveHome) + r.Get(blogPath+paginationPath, serveHome) }) } // Custom pages for _, cp := range blogConfig.CustomPages { - handler := serveCustomPage(blogConfig, cp) + scp := middleware.WithValue(customPageContextKey, cp) if cp.Cache { - r.With(privateModeHandler...).With(cacheMiddleware).Get(cp.Path, handler) + r.With(privateModeHandler...).With(cacheMiddleware, sbm, scp).Get(cp.Path, serveCustomPage) } else { - r.With(privateModeHandler...).Get(cp.Path, handler) + r.With(privateModeHandler...).With(sbm, scp).Get(cp.Path, serveCustomPage) } } @@ -471,15 +443,16 @@ func buildDynamicRouter() (*chi.Mux, error) { if randomPath == "" { randomPath = "/random" } - r.With(privateModeHandler...).Get(blogPath+randomPath, redirectToRandomPost(blog)) + r.With(privateModeHandler...).With(sbm).Get(blogPath+randomPath, redirectToRandomPost) } // Editor - r.Mount(blogPath+"/editor", editorRouters[blog]) + r.With(sbm).Mount(blogPath+"/editor", editorRouter) // Comments if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled { - r.Mount(blogPath+"/comment", commentRouters[blog]) + commentsPath := blogPath + "/comment" + r.With(sbm, middleware.WithValue(pathContextKey, commentsPath)).Mount(commentsPath, commentsRouter) } } @@ -500,19 +473,20 @@ func buildDynamicRouter() (*chi.Mux, error) { serveError(rw, r, "", http.StatusMethodNotAllowed) }) - log.Println("Building handler took", time.Since(startTime)) - return r, nil } -func blogPath(cb *configBlog) string { - blogPath := cb.Path +func blogPath(blog string) string { + blogPath := appConfig.Blogs[blog].Path if blogPath == "/" { return "" } return blogPath } +const blogContextKey requestContextKey = "blog" +const pathContextKey requestContextKey = "httpPath" + var cspDomains = "" func refreshCSPDomains() { diff --git a/notifications.go b/notifications.go index df28191..9a1162f 100644 --- a/notifications.go +++ b/notifications.go @@ -112,50 +112,48 @@ func (p *notificationsPaginationAdapter) Slice(offset, length int, data interfac return err } -func notificationsAdmin(notificationPath string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - // Adapter - pageNoString := chi.URLParam(r, "page") - pageNo, _ := strconv.Atoi(pageNoString) - p := paginator.New(¬ificationsPaginationAdapter{config: ¬ificationsRequestConfig{}}, 10) - p.SetPage(pageNo) - var notifications []*notification - err := p.Results(¬ifications) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - // Navigation - var hasPrev, hasNext bool - var prevPage, nextPage int - var prevPath, nextPath string - hasPrev, _ = p.HasPrev() - if hasPrev { - prevPage, _ = p.PrevPage() - } else { - prevPage, _ = p.Page() - } - if prevPage < 2 { - prevPath = notificationPath - } else { - prevPath = fmt.Sprintf("%s/page/%d", notificationPath, prevPage) - } - hasNext, _ = p.HasNext() - if hasNext { - nextPage, _ = p.NextPage() - } else { - nextPage, _ = p.Page() - } - nextPath = fmt.Sprintf("%s/page/%d", notificationPath, nextPage) - // Render - render(w, r, templateNotificationsAdmin, &renderData{ - Data: map[string]interface{}{ - "Notifications": notifications, - "HasPrev": hasPrev, - "HasNext": hasNext, - "Prev": slashIfEmpty(prevPath), - "Next": slashIfEmpty(nextPath), - }, - }) +func notificationsAdmin(w http.ResponseWriter, r *http.Request) { + // Adapter + pageNoString := chi.URLParam(r, "page") + pageNo, _ := strconv.Atoi(pageNoString) + p := paginator.New(¬ificationsPaginationAdapter{config: ¬ificationsRequestConfig{}}, 10) + p.SetPage(pageNo) + var notifications []*notification + err := p.Results(¬ifications) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return } + // Navigation + var hasPrev, hasNext bool + var prevPage, nextPage int + var prevPath, nextPath string + hasPrev, _ = p.HasPrev() + if hasPrev { + prevPage, _ = p.PrevPage() + } else { + prevPage, _ = p.Page() + } + if prevPage < 2 { + prevPath = notificationsPath + } else { + prevPath = fmt.Sprintf("%s/page/%d", notificationsPath, prevPage) + } + hasNext, _ = p.HasNext() + if hasNext { + nextPage, _ = p.NextPage() + } else { + nextPage, _ = p.Page() + } + nextPath = fmt.Sprintf("%s/page/%d", notificationsPath, nextPage) + // Render + render(w, r, templateNotificationsAdmin, &renderData{ + Data: map[string]interface{}{ + "Notifications": notifications, + "HasPrev": hasPrev, + "HasNext": hasNext, + "Prev": slashIfEmpty(prevPath), + "Next": slashIfEmpty(nextPath), + }, + }) } diff --git a/posts.go b/posts.go index 4be304a..661c548 100644 --- a/posts.go +++ b/posts.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "html/template" @@ -71,15 +72,13 @@ func servePost(w http.ResponseWriter, r *http.Request) { }) } -func redirectToRandomPost(blog string) func(http.ResponseWriter, *http.Request) { - return func(rw http.ResponseWriter, r *http.Request) { - randomPath, err := getRandomPostPath(blog) - if err != nil { - serveError(rw, r, err.Error(), http.StatusInternalServerError) - return - } - http.Redirect(rw, r, randomPath, http.StatusFound) +func redirectToRandomPost(rw http.ResponseWriter, r *http.Request) { + randomPath, err := getRandomPostPath(r.Context().Value(blogContextKey).(string)) + if err != nil { + serveError(rw, r, err.Error(), http.StatusInternalServerError) + return } + http.Redirect(rw, r, randomPath, http.StatusFound) } type postPaginationAdapter struct { @@ -105,77 +104,60 @@ func (p *postPaginationAdapter) Slice(offset, length int, data interface{}) erro return err } -func serveHome(blog string, path string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest { - appConfig.Blogs[blog].serveActivityStreams(blog, w, r) - return - } - serveIndex(&indexConfig{ - blog: blog, - path: path, - })(w, r) +func serveHome(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest { + appConfig.Blogs[blog].serveActivityStreams(blog, w, r) + return } + serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{ + path: blogPath(blog), + }))) } -func serveSection(blog string, path string, section *section) func(w http.ResponseWriter, r *http.Request) { - return serveIndex(&indexConfig{ - blog: blog, - path: path, - section: section, - }) -} - -func serveTaxonomyValue(blog string, path string, tax *taxonomy, value string) func(w http.ResponseWriter, r *http.Request) { - return serveIndex(&indexConfig{ - blog: blog, - path: path, - tax: tax, - taxValue: value, - }) -} - -func servePhotos(blog string, path string) func(w http.ResponseWriter, r *http.Request) { - return serveIndex(&indexConfig{ - blog: blog, - path: path, - parameter: appConfig.Blogs[blog].Photos.Parameter, - title: appConfig.Blogs[blog].Photos.Title, - description: appConfig.Blogs[blog].Photos.Description, - summaryTemplate: templatePhotosSummary, - }) -} - -func serveSearchResults(blog string, path string) func(w http.ResponseWriter, r *http.Request) { - return serveIndex(&indexConfig{ - blog: blog, - path: path, - }) -} - -func serveDate(blog string, path string, year, month, day int) func(w http.ResponseWriter, r *http.Request) { - var title strings.Builder +func serveDate(w http.ResponseWriter, r *http.Request) { + var year, month, day int + if ys := chi.URLParam(r, "year"); ys != "" && ys != "x" { + year, _ = strconv.Atoi(ys) + } + if ms := chi.URLParam(r, "month"); ms != "" && ms != "x" { + month, _ = strconv.Atoi(ms) + } + if ds := chi.URLParam(r, "day"); ds != "" { + day, _ = strconv.Atoi(ds) + } + if year == 0 && month == 0 && day == 0 { + serve404(w, r) + return + } + var title, dPath strings.Builder + dPath.WriteString(blogPath(r.Context().Value(blogContextKey).(string)) + "/") if year != 0 { - title.WriteString(fmt.Sprintf("%0004d", year)) + ys := fmt.Sprintf("%0004d", year) + title.WriteString(ys) + dPath.WriteString(ys) } else { title.WriteString("XXXX") + dPath.WriteString("x") } if month != 0 { title.WriteString(fmt.Sprintf("-%02d", month)) + dPath.WriteString(fmt.Sprintf("/%02d", month)) } else if day != 0 { title.WriteString("-XX") + dPath.WriteString("/x") } if day != 0 { title.WriteString(fmt.Sprintf("-%02d", day)) + dPath.WriteString(fmt.Sprintf("/%02d", day)) } - return serveIndex(&indexConfig{ - blog: blog, - path: path, + serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{ + path: dPath.String(), year: year, month: month, day: day, title: title.String(), - }) + }))) } type indexConfig struct { @@ -191,106 +173,111 @@ type indexConfig struct { summaryTemplate string } -func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - search := chi.URLParam(r, "search") - if search != "" { - search = searchDecode(search) - } - pageNoString := chi.URLParam(r, "page") - pageNo, _ := strconv.Atoi(pageNoString) - var sections []string - if ic.section != nil { - sections = []string{ic.section.Name} - } else { - for sectionKey := range appConfig.Blogs[ic.blog].Sections { - sections = append(sections, sectionKey) - } - } - p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{ - blog: ic.blog, - sections: sections, - taxonomy: ic.tax, - taxonomyValue: ic.taxValue, - parameter: ic.parameter, - search: search, - publishedYear: ic.year, - publishedMonth: ic.month, - publishedDay: ic.day, - status: statusPublished, - }}, appConfig.Blogs[ic.blog].Pagination) - p.SetPage(pageNo) - var posts []*post - t := servertiming.FromContext(r.Context()).NewMetric("gp").Start() - err := p.Results(&posts) - t.Stop() - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - // Meta - title := ic.title - description := ic.description - if ic.tax != nil { - title = fmt.Sprintf("%s: %s", ic.tax.Title, ic.taxValue) - } else if ic.section != nil { - title = ic.section.Title - description = ic.section.Description - } else if search != "" { - title = fmt.Sprintf("%s: %s", appConfig.Blogs[ic.blog].Search.Title, search) - } - // Clean title - title = bluemonday.StrictPolicy().Sanitize(title) - // Check if feed - if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed { - generateFeed(ic.blog, ft, w, r, posts, title, description) - return - } - // Path - path := ic.path - if strings.Contains(path, searchPlaceholder) { - path = strings.ReplaceAll(path, searchPlaceholder, searchEncode(search)) - } - // Navigation - var hasPrev, hasNext bool - var prevPage, nextPage int - var prevPath, nextPath string - hasPrev, _ = p.HasPrev() - if hasPrev { - prevPage, _ = p.PrevPage() - } else { - prevPage, _ = p.Page() - } - if prevPage < 2 { - prevPath = path - } else { - prevPath = fmt.Sprintf("%s/page/%d", path, prevPage) - } - hasNext, _ = p.HasNext() - if hasNext { - nextPage, _ = p.NextPage() - } else { - nextPage, _ = p.Page() - } - nextPath = fmt.Sprintf("%s/page/%d", path, nextPage) - summaryTemplate := ic.summaryTemplate - if summaryTemplate == "" { - summaryTemplate = templateSummary - } - render(w, r, templateIndex, &renderData{ - BlogString: ic.blog, - Canonical: appConfig.Server.PublicAddress + path, - Data: map[string]interface{}{ - "Title": title, - "Description": description, - "Posts": posts, - "HasPrev": hasPrev, - "HasNext": hasNext, - "First": slashIfEmpty(path), - "Prev": slashIfEmpty(prevPath), - "Next": slashIfEmpty(nextPath), - "SummaryTemplate": summaryTemplate, - }, - }) +const indexConfigKey requestContextKey = "indexConfig" + +func serveIndex(w http.ResponseWriter, r *http.Request) { + ic := r.Context().Value(indexConfigKey).(*indexConfig) + blog := ic.blog + if blog == "" { + blog, _ = r.Context().Value(blogContextKey).(string) } + search := chi.URLParam(r, "search") + if search != "" { + search = searchDecode(search) + } + pageNoString := chi.URLParam(r, "page") + pageNo, _ := strconv.Atoi(pageNoString) + var sections []string + if ic.section != nil { + sections = []string{ic.section.Name} + } else { + for sectionKey := range appConfig.Blogs[blog].Sections { + sections = append(sections, sectionKey) + } + } + p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{ + blog: blog, + sections: sections, + taxonomy: ic.tax, + taxonomyValue: ic.taxValue, + parameter: ic.parameter, + search: search, + publishedYear: ic.year, + publishedMonth: ic.month, + publishedDay: ic.day, + status: statusPublished, + }}, appConfig.Blogs[blog].Pagination) + p.SetPage(pageNo) + var posts []*post + t := servertiming.FromContext(r.Context()).NewMetric("gp").Start() + err := p.Results(&posts) + t.Stop() + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return + } + // Meta + title := ic.title + description := ic.description + if ic.tax != nil { + title = fmt.Sprintf("%s: %s", ic.tax.Title, ic.taxValue) + } else if ic.section != nil { + title = ic.section.Title + description = ic.section.Description + } else if search != "" { + title = fmt.Sprintf("%s: %s", appConfig.Blogs[blog].Search.Title, search) + } + // Clean title + title = bluemonday.StrictPolicy().Sanitize(title) + // Check if feed + if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed { + generateFeed(blog, ft, w, r, posts, title, description) + return + } + // Path + path := ic.path + if strings.Contains(path, searchPlaceholder) { + path = strings.ReplaceAll(path, searchPlaceholder, searchEncode(search)) + } + // Navigation + var hasPrev, hasNext bool + var prevPage, nextPage int + var prevPath, nextPath string + hasPrev, _ = p.HasPrev() + if hasPrev { + prevPage, _ = p.PrevPage() + } else { + prevPage, _ = p.Page() + } + if prevPage < 2 { + prevPath = path + } else { + prevPath = fmt.Sprintf("%s/page/%d", path, prevPage) + } + hasNext, _ = p.HasNext() + if hasNext { + nextPage, _ = p.NextPage() + } else { + nextPage, _ = p.Page() + } + nextPath = fmt.Sprintf("%s/page/%d", path, nextPage) + summaryTemplate := ic.summaryTemplate + if summaryTemplate == "" { + summaryTemplate = templateSummary + } + render(w, r, templateIndex, &renderData{ + BlogString: blog, + Canonical: appConfig.Server.PublicAddress + path, + Data: map[string]interface{}{ + "Title": title, + "Description": description, + "Posts": posts, + "HasPrev": hasPrev, + "HasNext": hasNext, + "First": slashIfEmpty(path), + "Prev": slashIfEmpty(prevPath), + "Next": slashIfEmpty(nextPath), + "SummaryTemplate": summaryTemplate, + }, + }) } diff --git a/search.go b/search.go index 76cce3d..a03057a 100644 --- a/search.go +++ b/search.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/base64" "net/http" "net/url" @@ -10,22 +11,28 @@ import ( const searchPlaceholder = "{search}" -func serveSearch(blog string, servePath string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - serveError(w, r, err.Error(), http.StatusBadRequest) - return - } - if q := r.Form.Get("q"); q != "" { - http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound) - return - } - render(w, r, templateSearch, &renderData{ - BlogString: blog, - Canonical: appConfig.Server.PublicAddress + servePath, - }) +func serveSearch(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + servePath := r.Context().Value(pathContextKey).(string) + err := r.ParseForm() + if err != nil { + serveError(w, r, err.Error(), http.StatusBadRequest) + return } + if q := r.Form.Get("q"); q != "" { + http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound) + return + } + render(w, r, templateSearch, &renderData{ + BlogString: blog, + Canonical: appConfig.Server.PublicAddress + servePath, + }) +} + +func serveSearchResult(w http.ResponseWriter, r *http.Request) { + serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{ + path: r.Context().Value(pathContextKey).(string) + "/" + searchPlaceholder, + }))) } func searchEncode(search string) string { diff --git a/taxonomies.go b/taxonomies.go index ce5c91e..4e2cddd 100644 --- a/taxonomies.go +++ b/taxonomies.go @@ -2,20 +2,22 @@ package main import "net/http" -func serveTaxonomy(blog string, tax *taxonomy) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - allValues, err := allTaxonomyValues(blog, tax.Name) - if err != nil { - serveError(w, r, err.Error(), http.StatusInternalServerError) - return - } - render(w, r, templateTaxonomy, &renderData{ - BlogString: blog, - Canonical: appConfig.Server.PublicAddress + r.URL.Path, - Data: map[string]interface{}{ - "Taxonomy": tax, - "ValueGroups": groupStrings(allValues), - }, - }) +const taxonomyContextKey = "taxonomy" + +func serveTaxonomy(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + tax := r.Context().Value(taxonomyContextKey).(*taxonomy) + allValues, err := allTaxonomyValues(blog, tax.Name) + if err != nil { + serveError(w, r, err.Error(), http.StatusInternalServerError) + return } + render(w, r, templateTaxonomy, &renderData{ + BlogString: blog, + Canonical: appConfig.Server.PublicAddress + r.URL.Path, + Data: map[string]interface{}{ + "Taxonomy": tax, + "ValueGroups": groupStrings(allValues), + }, + }) } diff --git a/tor.go b/tor.go index 820f39c..b50ed76 100644 --- a/tor.go +++ b/tor.go @@ -41,7 +41,7 @@ func startOnionService(h http.Handler) error { return err } pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: x509Encoded}) - os.WriteFile(torKeyPath, pemEncoded, os.ModePerm) + _ = os.WriteFile(torKeyPath, pemEncoded, os.ModePerm) } else { d, _ := os.ReadFile(torKeyPath) block, _ := pem.Decode(d) @@ -53,7 +53,7 @@ func startOnionService(h http.Handler) error { } // Start tor with default config (can set start conf's DebugWriter to os.Stdout for debug logs) log.Println("Starting and registering onion service, please wait a couple of minutes...") - t, err := tor.Start(nil, &tor.StartConf{ + t, err := tor.Start(context.Background(), &tor.StartConf{ TempDataDirBase: os.TempDir(), }) if err != nil {