diff --git a/.golangci.yml b/.golangci.yml index f02cc00..fd5b089 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -36,4 +36,6 @@ linters-settings: checks: ["all"] gostatichcheck: go: "1.17" - checks: ["all"] \ No newline at end of file + checks: ["all"] + dupl: + threshold: 125 \ No newline at end of file diff --git a/activityPub.go b/activityPub.go index 8be529a..02be17e 100644 --- a/activityPub.go +++ b/activityPub.go @@ -164,54 +164,44 @@ func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) { case "Follow": a.apAccept(blogName, blog, activity) case "Undo": - { - if object, ok := activity["object"].(map[string]interface{}); ok { - ot := cast.ToString(object["type"]) - actor := cast.ToString(object["actor"]) - if ot == "Follow" && actor == activityActor { - _ = a.db.apRemoveFollower(blogName, activityActor) - } + if object, ok := activity["object"].(map[string]interface{}); ok { + ot := cast.ToString(object["type"]) + actor := cast.ToString(object["actor"]) + if ot == "Follow" && actor == activityActor { + _ = a.db.apRemoveFollower(blogName, activityActor) } } case "Create": - { - if object, ok := activity["object"].(map[string]interface{}); ok { - baseUrl := cast.ToString(object["id"]) - if ou := cast.ToString(object["url"]); ou != "" { - baseUrl = ou - } - if r := cast.ToString(object["inReplyTo"]); r != "" && baseUrl != "" && strings.HasPrefix(r, blogIri) { - // It's an ActivityPub reply; save reply as webmention - _ = a.createWebmention(baseUrl, r) - } else if content := cast.ToString(object["content"]); content != "" && baseUrl != "" { - // May be a mention; find links to blog and save them as webmentions - if links, err := allLinksFromHTMLString(content, baseUrl); err == nil { - for _, link := range links { - if strings.HasPrefix(link, a.cfg.Server.PublicAddress) { - _ = a.createWebmention(baseUrl, link) - } + if object, ok := activity["object"].(map[string]interface{}); ok { + baseUrl := cast.ToString(object["id"]) + if ou := cast.ToString(object["url"]); ou != "" { + baseUrl = ou + } + if r := cast.ToString(object["inReplyTo"]); r != "" && baseUrl != "" && strings.HasPrefix(r, blogIri) { + // It's an ActivityPub reply; save reply as webmention + _ = a.createWebmention(baseUrl, r) + } else if content := cast.ToString(object["content"]); content != "" && baseUrl != "" { + // May be a mention; find links to blog and save them as webmentions + if links, err := allLinksFromHTMLString(content, baseUrl); err == nil { + for _, link := range links { + if strings.HasPrefix(link, a.cfg.Server.PublicAddress) { + _ = a.createWebmention(baseUrl, link) } } } } } case "Delete", "Block": - { - if o := cast.ToString(activity["object"]); o == activityActor { - _ = a.db.apRemoveFollower(blogName, activityActor) - } + if o := cast.ToString(activity["object"]); o == activityActor { + _ = a.db.apRemoveFollower(blogName, activityActor) } case "Like": - { - if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) { - a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, o)) - } + if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) { + a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, o)) } case "Announce": - { - if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) { - a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, o)) - } + if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) { + a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, o)) } } // Return 200 diff --git a/blogstats_test.go b/blogstats_test.go index 974a914..cb7754d 100644 --- a/blogstats_test.go +++ b/blogstats_test.go @@ -112,14 +112,12 @@ func Test_blogStats(t *testing.T) { // Test HTML t.Run("Test stats table", func(t *testing.T) { - h := http.HandlerFunc(app.serveBlogStatsTable) - req := httptest.NewRequest(http.MethodGet, "/abc", nil) req = req.WithContext(context.WithValue(req.Context(), blogKey, "en")) rec := httptest.NewRecorder() - h(rec, req) + app.serveBlogStatsTable(rec, req) res := rec.Result() resBody, _ := io.ReadAll(res.Body) @@ -132,14 +130,12 @@ func Test_blogStats(t *testing.T) { }) t.Run("Test stats page", func(t *testing.T) { - h := http.HandlerFunc(app.serveBlogStats) - req := httptest.NewRequest(http.MethodGet, "/abc", nil) req = req.WithContext(context.WithValue(req.Context(), blogKey, "en")) rec := httptest.NewRecorder() - h(rec, req) + app.serveBlogStats(rec, req) res := rec.Result() resBody, _ := io.ReadAll(res.Body) diff --git a/cache.go b/cache.go index c4efc3c..cd60535 100644 --- a/cache.go +++ b/cache.go @@ -68,12 +68,10 @@ func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler { if cli, ok := r.Context().Value(cacheLoggedInKey).(bool); ok && cli { // Continue caching, but remove login setLoggedIn(r, false) - } else { - if a.isLoggedIn(r) { - // Don't cache logged in requests - next.ServeHTTP(w, r) - return - } + } else if a.isLoggedIn(r) { + // Don't cache logged in requests + next.ServeHTTP(w, r) + return } // Search and serve cache key := cacheKey(r) diff --git a/commentsAdmin.go b/commentsAdmin.go index d72aa42..d5b202f 100644 --- a/commentsAdmin.go +++ b/commentsAdmin.go @@ -37,10 +37,8 @@ func (p *commentsPaginationAdapter) Slice(offset, length int, data interface{}) func (a *goBlog) commentsAdmin(w http.ResponseWriter, r *http.Request) { commentsPath := r.Context().Value(pathKey).(string) // Adapter - pageNoString := chi.URLParam(r, "page") - pageNo, _ := strconv.Atoi(pageNoString) p := paginator.New(&commentsPaginationAdapter{config: &commentsRequestConfig{}, db: a.db}, 5) - p.SetPage(pageNo) + p.SetPage(stringToInt(chi.URLParam(r, "page"))) var comments []*comment err := p.Results(&comments) if err != nil { diff --git a/garbagecollector.go b/garbagecollector.go index 049530e..042185c 100644 --- a/garbagecollector.go +++ b/garbagecollector.go @@ -17,14 +17,14 @@ func initGC() { } func doGC() { - var old, new runtime.MemStats - runtime.ReadMemStats(&old) + var before, after runtime.MemStats + runtime.ReadMemStats(&before) runtime.GC() - runtime.ReadMemStats(&new) + runtime.ReadMemStats(&after) log.Println(fmt.Sprintf( "\nAlloc: %d MiB -> %d MiB\nSys: %d MiB -> %d MiB\nNumGC: %d", - old.Alloc/1024/1024, new.Alloc/1024/1024, - old.Sys/1024/1024, new.Sys/1024/1024, - new.NumGC, + before.Alloc/1024/1024, after.Alloc/1024/1024, + before.Sys/1024/1024, after.Sys/1024/1024, + after.NumGC, )) } diff --git a/go.mod b/go.mod index 26056d9..fbc7c51 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 golang.org/x/crypto v0.0.0-20220214200702-86341886e292 - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd + golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b diff --git a/go.sum b/go.sum index 79456d2..fd2173f 100644 --- a/go.sum +++ b/go.sum @@ -562,8 +562,9 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/indexnow.go b/indexnow.go index 15e4b3e..705d08d 100644 --- a/indexnow.go +++ b/indexnow.go @@ -41,7 +41,7 @@ func (a *goBlog) indexNowEnabled() bool { return true } -func (a *goBlog) serveIndexNow(w http.ResponseWriter, r *http.Request) { +func (a *goBlog) serveIndexNow(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write(a.indexNowKey()) } diff --git a/micropub.go b/micropub.go index 71de05b..2243370 100644 --- a/micropub.go +++ b/micropub.go @@ -8,35 +8,29 @@ import ( "net/http" "net/url" "regexp" - "strconv" "strings" "time" "github.com/spf13/cast" "github.com/thoas/go-funk" + "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" "gopkg.in/yaml.v3" ) const micropubPath = "/micropub" -type micropubConfig struct { - MediaEndpoint string `json:"media-endpoint,omitempty"` -} - func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) { - switch r.URL.Query().Get("q") { + var result interface{} + switch query := r.URL.Query(); query.Get("q") { case "config": - w.Header().Set(contentType, contenttype.JSONUTF8) - mw := a.min.Writer(contenttype.JSON, w) - defer mw.Close() - _ = json.NewEncoder(mw).Encode(µpubConfig{ - MediaEndpoint: a.getFullAddress(micropubPath + micropubMediaSubPath), - }) + type micropubConfig struct { + MediaEndpoint string `json:"media-endpoint"` + } + result = micropubConfig{MediaEndpoint: a.getFullAddress(micropubPath + micropubMediaSubPath)} case "source": - var mf interface{} - if urlString := r.URL.Query().Get("url"); urlString != "" { - u, err := url.Parse(r.URL.Query().Get("url")) + if urlString := query.Get("url"); urlString != "" { + u, err := url.Parse(query.Get("url")) if err != nil { a.serveError(w, r, err.Error(), http.StatusBadRequest) return @@ -46,13 +40,11 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) { a.serveError(w, r, err.Error(), http.StatusBadRequest) return } - mf = a.postToMfItem(p) + result = a.postToMfItem(p) } else { - limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) - offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) posts, err := a.getPosts(&postsRequestConfig{ - limit: limit, - offset: offset, + limit: stringToInt(query.Get("limit")), + offset: stringToInt(query.Get("offset")), }) if err != nil { a.serveError(w, r, err.Error(), http.StatusInternalServerError) @@ -62,12 +54,8 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) { for _, p := range posts { list["items"] = append(list["items"], a.postToMfItem(p)) } - mf = list + result = list } - w.Header().Set(contentType, contenttype.JSONUTF8) - mw := a.min.Writer(contenttype.JSON, w) - defer mw.Close() - _ = json.NewEncoder(mw).Encode(mf) case "category": allCategories := []string{} for blog := range a.cfg.Blogs { @@ -78,22 +66,28 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) { } allCategories = append(allCategories, values...) } - w.Header().Set(contentType, contenttype.JSONUTF8) - mw := a.min.Writer(contenttype.JSON, w) - defer mw.Close() - _ = json.NewEncoder(mw).Encode(map[string]interface{}{ - "categories": allCategories, - }) + result = map[string]interface{}{"categories": allCategories} default: a.serve404(w, r) + return } + buf := bufferpool.Get() + defer bufferpool.Put(buf) + mw := a.min.Writer(contenttype.JSON, buf) + err := json.NewEncoder(mw).Encode(result) + _ = mw.Close() + if err != nil { + a.serveError(w, r, "Failed to encode json", http.StatusInternalServerError) + return + } + w.Header().Set(contentType, contenttype.JSONUTF8) + _, _ = buf.WriteTo(w) } func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() switch mt, _, _ := mime.ParseMediaType(r.Header.Get(contentType)); mt { case contenttype.WWWForm, contenttype.MultipartForm: - _ = r.ParseForm() _ = r.ParseMultipartForm(0) if r.Form == nil { a.serveError(w, r, "Failed to parse form", http.StatusBadRequest) @@ -442,9 +436,16 @@ func (a *goBlog) micropubCreatePostFromJson(w http.ResponseWriter, r *http.Reque a.micropubCreate(w, r, p) } +func (a *goBlog) micropubCheckScope(w http.ResponseWriter, r *http.Request, required string) bool { + if !strings.Contains(r.Context().Value(indieAuthScope).(string), required) { + a.serveError(w, r, required+" scope missing", http.StatusForbidden) + return false + } + return true +} + func (a *goBlog) micropubCreate(w http.ResponseWriter, r *http.Request, p *post) { - if !strings.Contains(r.Context().Value(indieAuthScope).(string), "create") { - a.serveError(w, r, "create scope missing", http.StatusForbidden) + if !a.micropubCheckScope(w, r, "create") { return } if err := a.computeExtraPostParameters(p); err != nil { @@ -459,8 +460,7 @@ func (a *goBlog) micropubCreate(w http.ResponseWriter, r *http.Request, p *post) } func (a *goBlog) micropubDelete(w http.ResponseWriter, r *http.Request, u string) { - if !strings.Contains(r.Context().Value(indieAuthScope).(string), "delete") { - a.serveError(w, r, "delete scope missing", http.StatusForbidden) + if !a.micropubCheckScope(w, r, "delete") { return } uu, err := url.Parse(u) @@ -476,8 +476,7 @@ func (a *goBlog) micropubDelete(w http.ResponseWriter, r *http.Request, u string } func (a *goBlog) micropubUndelete(w http.ResponseWriter, r *http.Request, u string) { - if !strings.Contains(r.Context().Value(indieAuthScope).(string), "undelete") { - a.serveError(w, r, "undelete scope missing", http.StatusForbidden) + if !a.micropubCheckScope(w, r, "undelete") { return } uu, err := url.Parse(u) @@ -493,8 +492,7 @@ func (a *goBlog) micropubUndelete(w http.ResponseWriter, r *http.Request, u stri } func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string, mf *microformatItem) { - if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") { - a.serveError(w, r, "update scope missing", http.StatusForbidden) + if !a.micropubCheckScope(w, r, "update") { return } uu, err := url.Parse(u) diff --git a/micropubMedia.go b/micropubMedia.go index 130c614..f4548f4 100644 --- a/micropubMedia.go +++ b/micropubMedia.go @@ -17,8 +17,7 @@ const micropubMediaSubPath = "/media" func (a *goBlog) serveMicropubMedia(w http.ResponseWriter, r *http.Request) { // Check scope - if !strings.Contains(r.Context().Value(indieAuthScope).(string), "media") { - a.serveError(w, r, "media scope missing", http.StatusForbidden) + if !a.micropubCheckScope(w, r, "media") { return } // Check if request is multipart diff --git a/micropub_test.go b/micropub_test.go new file mode 100644 index 0000000..a52dfde --- /dev/null +++ b/micropub_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_micropubQuery(t *testing.T) { + + app := &goBlog{ + cfg: createDefaultTestConfig(t), + } + _ = app.initConfig() + _ = app.initDatabase(false) + defer app.db.close() + app.initComponents(false) + + // Create a test post with tags + err := app.createPost(&post{ + Path: "/test/post", + Content: "Test post", + Parameters: map[string][]string{ + "tags": {"test", "test2"}, + }, + }) + require.NoError(t, err) + + type testCase struct { + query string + want string + wantStatus int + } + + testCases := []testCase{ + { + query: "config", + want: "{\"media-endpoint\":\"http://localhost:8080/micropub/media\"}\n", + wantStatus: http.StatusOK, + }, + { + query: "source&url=http://localhost:8080/test/post", + want: "{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"]}}\n", + wantStatus: http.StatusOK, + }, + { + query: "source", + want: "{\"items\":[{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"]}}]}\n", + wantStatus: http.StatusOK, + }, + { + query: "category", + want: "{\"categories\":[\"test\",\"test2\"]}\n", + wantStatus: http.StatusOK, + }, + { + query: "somethingelse", + wantStatus: http.StatusNotFound, + }, + } + + for _, tc := range testCases { + req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/micropub?q="+tc.query, nil) + rec := httptest.NewRecorder() + + app.serveMicropubQuery(rec, req) + rec.Flush() + + assert.Equal(t, tc.wantStatus, rec.Code) + if tc.want != "" { + assert.Equal(t, tc.want, rec.Body.String()) + } + } + +} diff --git a/notifications.go b/notifications.go index 7436cb8..58a9673 100644 --- a/notifications.go +++ b/notifications.go @@ -122,10 +122,8 @@ func (p *notificationsPaginationAdapter) Slice(offset, length int, data interfac func (a *goBlog) notificationsAdmin(w http.ResponseWriter, r *http.Request) { // Adapter - pageNoString := chi.URLParam(r, "page") - pageNo, _ := strconv.Atoi(pageNoString) p := paginator.New(¬ificationsPaginationAdapter{config: ¬ificationsRequestConfig{}, db: a.db}, 10) - p.SetPage(pageNo) + p.SetPage(stringToInt(chi.URLParam(r, "page"))) var notifications []*notification err := p.Results(¬ifications) if err != nil { diff --git a/posts.go b/posts.go index f89e0c4..22d70a2 100644 --- a/posts.go +++ b/posts.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "reflect" - "strconv" "strings" "time" @@ -202,13 +201,13 @@ func (a *goBlog) serveDeleted(w http.ResponseWriter, r *http.Request) { func (a *goBlog) 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) + year = stringToInt(ys) } if ms := chi.URLParam(r, "month"); ms != "" && ms != "x" { - month, _ = strconv.Atoi(ms) + month = stringToInt(ms) } if ds := chi.URLParam(r, "day"); ds != "" { - day, _ = strconv.Atoi(ds) + day = stringToInt(ds) } if year == 0 && month == 0 && day == 0 { a.serve404(w, r) @@ -270,8 +269,6 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) { // Decode and sanitize search search = cleanHTMLText(searchDecode(search)) } - pageNoString := chi.URLParam(r, "page") - pageNo, _ := strconv.Atoi(pageNoString) var sections []string if ic.section != nil { sections = []string{ic.section.Name} @@ -300,7 +297,7 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) { statusse: statusse, priorityOrder: true, }, a: a}, bc.Pagination) - p.SetPage(pageNo) + p.SetPage(stringToInt(chi.URLParam(r, "page"))) var posts []*post err := p.Results(&posts) if err != nil { diff --git a/templateAssets.go b/templateAssets.go index f7c9293..e9b6b4f 100644 --- a/templateAssets.go +++ b/templateAssets.go @@ -122,9 +122,5 @@ func (a *goBlog) initChromaCSS() error { if err != nil { return err } - err = a.compileAsset(chromaPath, buf) - if err != nil { - return err - } - return nil + return a.compileAsset(chromaPath, buf) } diff --git a/ui.go b/ui.go index ae8d2fa..267f4ce 100644 --- a/ui.go +++ b/ui.go @@ -471,21 +471,13 @@ func (a *goBlog) renderBlogStatsTable(hb *htmlBuilder, rd *renderData) { hb.writeElementOpen("th", "class", "tar") hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts")) hb.writeElementClose("th") - // Chars - hb.writeElementOpen("th", "class", "tar") - hb.write("~") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "chars")) - hb.writeElementClose("th") - // Words - hb.writeElementOpen("th", "class", "tar") - hb.write("~") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "words")) - hb.writeElementClose("th") - // Words/post - hb.writeElementOpen("th", "class", "tar") - hb.write("~") - hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "wordsperpost")) - hb.writeElementClose("th") + // Chars, Words, Words/Post + for _, s := range []string{"chars", "words", "wordsperpost"} { + hb.writeElementOpen("th", "class", "tar") + hb.write("~") + hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, s)) + hb.writeElementClose("th") + } hb.writeElementClose("thead") // Table body hb.writeElementOpen("tbody") diff --git a/utils.go b/utils.go index 50e03fe..f331150 100644 --- a/utils.go +++ b/utils.go @@ -12,6 +12,7 @@ import ( "path" "path/filepath" "sort" + "strconv" "strings" "sync" "time" @@ -211,9 +212,7 @@ func urlHasExt(rawUrl string, allowed ...string) (ext string, has bool) { return "", false } ext = ext[1:] - allowed = funk.Map(allowed, func(str string) string { - return strings.ToLower(str) - }).([]string) + allowed = funk.Map(allowed, strings.ToLower).([]string) return ext, funk.ContainsString(allowed, strings.ToLower(ext)) } @@ -375,3 +374,8 @@ func matchTimeDiffLocale(lang string) tdl.Locale { timeDiffLocaleMap[lang] = locale return locale } + +func stringToInt(s string) int { + i, _ := strconv.Atoi(s) + return i +} diff --git a/webmentionAdmin.go b/webmentionAdmin.go index e3a2dce..36b5115 100644 --- a/webmentionAdmin.go +++ b/webmentionAdmin.go @@ -38,7 +38,6 @@ func (p *webmentionPaginationAdapter) Slice(offset, length int, data interface{} } func (a *goBlog) webmentionAdmin(w http.ResponseWriter, r *http.Request) { - pageNo, _ := strconv.Atoi(chi.URLParam(r, "page")) var status webmentionStatus = "" switch webmentionStatus(r.URL.Query().Get("status")) { case webmentionStatusVerified: @@ -51,7 +50,7 @@ func (a *goBlog) webmentionAdmin(w http.ResponseWriter, r *http.Request) { status: status, sourcelike: sourcelike, }, db: a.db}, 5) - p.SetPage(pageNo) + p.SetPage(stringToInt(chi.URLParam(r, "page"))) var mentions []*mention err := p.Results(&mentions) if err != nil {