diff --git a/Dockerfile b/Dockerfile index 4e59611..e00d5cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ RUN apk add --no-cache git gcc musl-dev RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main sqlite-dev ADD *.go go.mod go.sum /app/ ENV GOFLAGS="-tags=linux,libsqlite3,sqlite_fts5" -RUN go test -cover +RUN go test -cover ./... RUN go build -ldflags '-w -s' -o GoBlog FROM alpine:3.13 diff --git a/activityPub.go b/activityPub.go index 33482c1..c2546b7 100644 --- a/activityPub.go +++ b/activityPub.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" "github.com/go-chi/chi/v5" "github.com/go-fed/httpsig" "github.com/spf13/cast" @@ -90,7 +91,7 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) { "links": []map[string]string{ { "rel": "self", - "type": contentTypeAS, + "type": contenttype.AS, "href": a.apIri(blog), }, { @@ -100,8 +101,8 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) { }, }, }) - w.Header().Set(contentType, "application/jrd+json"+charsetUtf8Suffix) - _, _ = writeMinified(w, contentTypeJSON, b) + w.Header().Set(contentType, "application/jrd+json"+contenttype.CharsetUtf8Suffix) + _, _ = a.min.Write(w, contenttype.JSON, b) } func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) { @@ -244,7 +245,7 @@ func apVerifySignature(r *http.Request) (*asPerson, string, int, error) { } func handleWellKnownHostMeta(w http.ResponseWriter, r *http.Request) { - w.Header().Set(contentType, "application/xrd+xml"+charsetUtf8Suffix) + w.Header().Set(contentType, "application/xrd+xml"+contenttype.CharsetUtf8Suffix) _, _ = w.Write([]byte(``)) } @@ -253,7 +254,7 @@ func apGetRemoteActor(iri string) (*asPerson, int, error) { if err != nil { return nil, 0, err } - req.Header.Set("Accept", contentTypeAS) + req.Header.Set("Accept", contenttype.AS) req.Header.Set(userAgent, appUserAgent) resp, err := appHttpClient.Do(req) if err != nil { @@ -308,7 +309,7 @@ func (db *database) apRemoveInbox(inbox string) error { func (a *goBlog) apPost(p *post) { n := a.toASNote(p) a.apSendToAllFollowers(p.Blog, map[string]interface{}{ - "@context": asContext, + "@context": []string{asContext}, "actor": a.apIri(a.cfg.Blogs[p.Blog]), "id": a.fullPostURL(p), "published": n.Published, @@ -324,7 +325,7 @@ func (a *goBlog) apPost(p *post) { func (a *goBlog) apUpdate(p *post) { a.apSendToAllFollowers(p.Blog, map[string]interface{}{ - "@context": asContext, + "@context": []string{asContext}, "actor": a.apIri(a.cfg.Blogs[p.Blog]), "id": a.fullPostURL(p), "published": time.Now().Format("2006-01-02T15:04:05-07:00"), @@ -335,7 +336,7 @@ func (a *goBlog) apUpdate(p *post) { func (a *goBlog) apAnnounce(p *post) { a.apSendToAllFollowers(p.Blog, map[string]interface{}{ - "@context": asContext, + "@context": []string{asContext}, "actor": a.apIri(a.cfg.Blogs[p.Blog]), "id": a.fullPostURL(p) + "#announce", "published": a.toASNote(p).Published, @@ -346,7 +347,7 @@ func (a *goBlog) apAnnounce(p *post) { func (a *goBlog) apDelete(p *post) { a.apSendToAllFollowers(p.Blog, map[string]interface{}{ - "@context": asContext, + "@context": []string{asContext}, "actor": a.apIri(a.cfg.Blogs[p.Blog]), "id": a.fullPostURL(p) + "#delete", "type": "Delete", @@ -383,7 +384,7 @@ func (a *goBlog) apAccept(blogName string, blog *configBlog, follow map[string]i // remove @context from the inner activity delete(follow, "@context") accept := map[string]interface{}{ - "@context": asContext, + "@context": []string{asContext}, "to": follow["actor"], "actor": a.apIri(blog), "object": follow, diff --git a/activityPubSending.go b/activityPubSending.go index 6a146e4..3fe4663 100644 --- a/activityPubSending.go +++ b/activityPubSending.go @@ -11,6 +11,8 @@ import ( "net/http" "net/url" "time" + + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" ) type apRequest struct { @@ -101,8 +103,8 @@ func (a *goBlog) apSendSigned(blogIri, to string, activity []byte) error { r.Header.Set("Accept-Charset", "utf-8") r.Header.Set("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") r.Header.Set(userAgent, appUserAgent) - r.Header.Set("Accept", contentTypeASUTF8) - r.Header.Set(contentType, contentTypeASUTF8) + r.Header.Set("Accept", contenttype.ASUTF8) + r.Header.Set(contentType, contenttype.ASUTF8) r.Header.Set("Host", iri.Host) // Sign request a.apPostSignMutex.Lock() diff --git a/activityStreams.go b/activityStreams.go index 847d470..5a20e01 100644 --- a/activityStreams.go +++ b/activityStreams.go @@ -8,25 +8,26 @@ import ( "fmt" "net/http" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" "github.com/araddon/dateparse" - "github.com/elnormous/contenttype" + ct "github.com/elnormous/contenttype" ) -var asContext = []string{"https://www.w3.org/ns/activitystreams"} - -var asCheckMediaTypes = []contenttype.MediaType{ - contenttype.NewMediaType(contentTypeHTML), - contenttype.NewMediaType(contentTypeAS), - contenttype.NewMediaType("application/ld+json"), -} +const asContext = "https://www.w3.org/ns/activitystreams" const asRequestKey requestContextKey = "asRequest" +var asCheckMediaTypes = []ct.MediaType{ + ct.NewMediaType(contenttype.HTML), + ct.NewMediaType(contenttype.AS), + ct.NewMediaType(contenttype.LDJSON), +} + func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled { // Check if accepted media type is not HTML - if mt, _, err := contenttype.GetAcceptableMediaType(r, asCheckMediaTypes); err == nil && mt.String() != asCheckMediaTypes[0].String() { + if mt, _, err := ct.GetAcceptableMediaType(r, asCheckMediaTypes); err == nil && mt.String() != asCheckMediaTypes[0].String() { next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true))) return } @@ -89,29 +90,29 @@ type asEndpoints struct { func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) { b, _ := json.Marshal(a.toASNote(p)) - w.Header().Set(contentType, contentTypeASUTF8) - _, _ = writeMinified(w, contentTypeAS, b) + w.Header().Set(contentType, contenttype.ASUTF8) + _, _ = a.min.Write(w, contenttype.AS, b) } func (a *goBlog) toASNote(p *post) *asNote { // Create a Note object as := &asNote{ - Context: asContext, + Context: []string{asContext}, To: []string{"https://www.w3.org/ns/activitystreams#Public"}, - MediaType: contentTypeHTML, + MediaType: contenttype.HTML, ID: a.fullPostURL(p), URL: a.fullPostURL(p), AttributedTo: a.apIri(a.cfg.Blogs[p.Blog]), } // Name and Type - if title := p.title(); title != "" { + if title := p.Title(); title != "" { as.Name = title as.Type = "Article" } else { as.Type = "Note" } // Content - as.Content = string(a.absoluteHTML(p)) + as.Content = string(a.absolutePostHTML(p)) // Attachments if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 { for _, image := range images { @@ -158,7 +159,7 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt return } asBlog := &asPerson{ - Context: asContext, + Context: []string{asContext}, Type: "Person", ID: a.apIri(b), URL: a.apIri(b), @@ -184,6 +185,6 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt } } jb, _ := json.Marshal(asBlog) - w.Header().Set(contentType, contentTypeASUTF8) - _, _ = writeMinified(w, contentTypeAS, jb) + w.Header().Set(contentType, contenttype.ASUTF8) + _, _ = a.min.Write(w, contenttype.AS, jb) } diff --git a/app.go b/app.go index c9b3c40..3b1495e 100644 --- a/app.go +++ b/app.go @@ -6,6 +6,7 @@ import ( "net/http" "sync" + "git.jlel.se/jlelse/GoBlog/pkgs/minify" shutdowner "git.jlel.se/jlelse/go-shutdowner" ts "git.jlel.se/jlelse/template-strings" "github.com/go-chi/chi/v5" @@ -27,6 +28,8 @@ type goBlog struct { assetFiles map[string]*assetFile // Blogroll blogrollCacheGroup singleflight.Group + // Blogstats + blogStatsCacheGroup singleflight.Group // Cache cache *cache // Config @@ -37,6 +40,9 @@ type goBlog struct { pPostHooks []postHookFunc pUpdateHooks []postHookFunc pDeleteHooks []postHookFunc + hourlyHooks []hourlyHookFunc + // HTTP + cspDomains string // HTTP Routers d *dynamicHandler privateMode bool @@ -53,6 +59,7 @@ type goBlog struct { setBlogMiddlewares map[string]func(http.Handler) http.Handler sectionMiddlewares map[string]func(http.Handler) http.Handler taxonomyMiddlewares map[string]func(http.Handler) http.Handler + taxValueMiddlewares map[string]func(http.Handler) http.Handler photosMiddlewares map[string]func(http.Handler) http.Handler searchMiddlewares map[string]func(http.Handler) http.Handler customPagesMiddlewares map[string]func(http.Handler) http.Handler @@ -61,6 +68,8 @@ type goBlog struct { logf *rotatelogs.RotateLogs // Markdown md, absoluteMd goldmark.Markdown + // Minify + min minify.Minifier // Regex Redirects regexRedirects []*regexRedirect // Rendering diff --git a/authentication.go b/authentication.go index 7e132d3..2496adc 100644 --- a/authentication.go +++ b/authentication.go @@ -8,6 +8,7 @@ import ( "io" "net/http" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" "github.com/pquerna/otp/totp" ) @@ -102,7 +103,7 @@ func (a *goBlog) checkLogin(w http.ResponseWriter, r *http.Request) bool { if r.Method != http.MethodPost { return false } - if r.Header.Get(contentType) != contentTypeWWWForm { + if r.Header.Get(contentType) != contenttype.WWWForm { return false } if r.FormValue("loginaction") != "login" { diff --git a/blogroll.go b/blogroll.go index eefccb8..cbdee07 100644 --- a/blogroll.go +++ b/blogroll.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" "github.com/kaorimatz/go-opml" servertiming "github.com/mitchellh/go-server-timing" "github.com/thoas/go-funk" @@ -55,14 +56,14 @@ func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) { if a.cfg.Cache != nil && a.cfg.Cache.Enable { setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration)) } - w.Header().Set(contentType, contentTypeXMLUTF8) + w.Header().Set(contentType, contenttype.XMLUTF8) var opmlBytes bytes.Buffer _ = opml.Render(&opmlBytes, &opml.OPML{ Version: "2.0", DateCreated: time.Now().UTC(), Outlines: outlines.([]*opml.Outline), }) - _, _ = writeMinified(w, contentTypeXML, opmlBytes.Bytes()) + _, _ = a.min.Write(w, contenttype.XML, opmlBytes.Bytes()) } func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) { diff --git a/blogstats.go b/blogstats.go index 992c0e7..3acfebe 100644 --- a/blogstats.go +++ b/blogstats.go @@ -5,8 +5,6 @@ import ( "encoding/json" "log" "net/http" - - "golang.org/x/sync/singleflight" ) func (a *goBlog) initBlogStats() { @@ -31,11 +29,9 @@ func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) { }) } -var blogStatsCacheGroup singleflight.Group - func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) { blog := r.Context().Value(blogContextKey).(string) - data, err, _ := blogStatsCacheGroup.Do(blog, func() (interface{}, error) { + data, err, _ := a.blogStatsCacheGroup.Do(blog, func() (interface{}, error) { return a.db.getBlogStats(blog) }) if err != nil { diff --git a/captcha.go b/captcha.go index 0c82d21..f47ad1c 100644 --- a/captcha.go +++ b/captcha.go @@ -7,6 +7,7 @@ import ( "io" "net/http" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" "github.com/dchest/captcha" ) @@ -53,7 +54,7 @@ func (a *goBlog) checkCaptcha(w http.ResponseWriter, r *http.Request) bool { if r.Method != http.MethodPost { return false } - if r.Header.Get(contentType) != contentTypeWWWForm { + if r.Header.Get(contentType) != contenttype.WWWForm { return false } if r.FormValue("captchaaction") != "captcha" { diff --git a/check.go b/check.go index 14f042e..deb0b70 100644 --- a/check.go +++ b/check.go @@ -90,7 +90,7 @@ func (a *goBlog) getExternalLinks(posts []*post, linkChan chan<- stringPair) err wg.Add(1) go func(p *post) { defer wg.Done() - links, _ := allLinksFromHTMLString(string(a.absoluteHTML(p)), a.fullPostURL(p)) + links, _ := allLinksFromHTMLString(string(a.absolutePostHTML(p)), a.fullPostURL(p)) for _, link := range links { linkChan <- stringPair{a.fullPostURL(p), link} } diff --git a/database.go b/database.go index 00a2ea5..cafbc6e 100644 --- a/database.go +++ b/database.go @@ -6,6 +6,7 @@ import ( "errors" "log" "os" + "sync" sqlite "github.com/mattn/go-sqlite3" "github.com/schollz/sqlite3dump" @@ -19,6 +20,7 @@ type database struct { stmts map[string]*sql.Stmt g singleflight.Group pc singleflight.Group + pcm sync.Mutex } func (a *goBlog) initDatabase() (err error) { @@ -38,7 +40,7 @@ func (a *goBlog) initDatabase() (err error) { } }) if a.cfg.Db.DumpFile != "" { - hourlyHooks = append(hourlyHooks, func() { + a.hourlyHooks = append(a.hourlyHooks, func() { db.dump(a.cfg.Db.DumpFile) }) db.dump(a.cfg.Db.DumpFile) diff --git a/editor.go b/editor.go index bf78f95..dfece7c 100644 --- a/editor.go +++ b/editor.go @@ -7,6 +7,8 @@ import ( "net/http" "net/http/httptest" "net/url" + + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" ) const editorPath = "/editor" @@ -71,7 +73,7 @@ func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) { a.serveError(w, r, err.Error(), http.StatusInternalServerError) return } - req.Header.Set(contentType, contentTypeJSON) + req.Header.Set(contentType, contenttype.JSON) a.editorMicropubPost(w, req, false) case "upload": a.editorMicropubPost(w, r, true) diff --git a/errors.go b/errors.go index 53f25a4..8c93f2a 100644 --- a/errors.go +++ b/errors.go @@ -4,7 +4,8 @@ import ( "fmt" "net/http" - "github.com/elnormous/contenttype" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" + ct "github.com/elnormous/contenttype" ) type errorData struct { @@ -20,12 +21,12 @@ func (a *goBlog) serveNotAllowed(w http.ResponseWriter, r *http.Request) { a.serveError(w, r, "", http.StatusMethodNotAllowed) } -var errorCheckMediaTypes = []contenttype.MediaType{ - contenttype.NewMediaType(contentTypeHTML), +var errorCheckMediaTypes = []ct.MediaType{ + ct.NewMediaType(contenttype.HTML), } func (a *goBlog) serveError(w http.ResponseWriter, r *http.Request, message string, status int) { - if mt, _, err := contenttype.GetAcceptableMediaType(r, errorCheckMediaTypes); err != nil || mt.String() != errorCheckMediaTypes[0].String() { + if mt, _, err := ct.GetAcceptableMediaType(r, errorCheckMediaTypes); err != nil || mt.String() != errorCheckMediaTypes[0].String() { // Request doesn't accept HTML http.Error(w, message, status) return diff --git a/feeds.go b/feeds.go index ae6b4e8..37ed41f 100644 --- a/feeds.go +++ b/feeds.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" "github.com/araddon/dateparse" "github.com/gorilla/feeds" ) @@ -55,11 +56,11 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r } } feed.Add(&feeds.Item{ - Title: p.title(), + Title: p.Title(), Link: &feeds.Link{Href: a.fullPostURL(p)}, - Description: a.summary(p), + Description: a.postSummary(p), Id: p.Path, - Content: string(a.absoluteHTML(p)), + Content: string(a.absolutePostHTML(p)), Created: created, Updated: updated, Enclosure: enc, @@ -69,13 +70,13 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r var feedString, feedMediaType string switch f { case rssFeed: - feedMediaType = contentTypeRSS + feedMediaType = contenttype.RSS feedString, err = feed.ToRss() case atomFeed: - feedMediaType = contentTypeATOM + feedMediaType = contenttype.ATOM feedString, err = feed.ToAtom() case jsonFeed: - feedMediaType = contentTypeJSONFeed + feedMediaType = contenttype.JSONFeed feedString, err = feed.ToJSON() default: return @@ -85,6 +86,6 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r a.serveError(w, r, err.Error(), http.StatusInternalServerError) return } - w.Header().Set(contentType, feedMediaType+charsetUtf8Suffix) - _, _ = writeMinified(w, feedMediaType, []byte(feedString)) + w.Header().Set(contentType, feedMediaType+contenttype.CharsetUtf8Suffix) + _, _ = a.min.Write(w, feedMediaType, []byte(feedString)) } diff --git a/reverseGeo.go b/geo.go similarity index 80% rename from reverseGeo.go rename to geo.go index 77d796d..66405ea 100644 --- a/reverseGeo.go +++ b/geo.go @@ -7,12 +7,16 @@ import ( "net/url" "strings" + gogeouri "git.jlel.se/jlelse/go-geouri" geojson "github.com/paulmach/go.geojson" "github.com/thoas/go-funk" ) -func (db *database) geoTitle(lat, lon float64, lang string) string { - ba, err := db.photonReverse(lat, lon, lang) +func (db *database) geoTitle(g *gogeouri.Geo, lang string) string { + if name, ok := g.Parameters["name"]; ok && len(name) > 0 && name[0] != "" { + return name[0] + } + ba, err := db.photonReverse(g.Latitude, g.Longitude, lang) if err != nil { return "" } @@ -65,3 +69,7 @@ func (db *database) photonReverse(lat, lon float64, lang string) ([]byte, error) _ = db.cachePersistently(cacheKey, ba) return ba, nil } + +func geoOSMLink(g *gogeouri.Geo) string { + return fmt.Sprintf("https://www.openstreetmap.org/?mlat=%v&mlon=%v", g.Latitude, g.Longitude) +} diff --git a/hooks.go b/hooks.go index 2b95aa7..e4cdb3e 100644 --- a/hooks.go +++ b/hooks.go @@ -78,7 +78,7 @@ func (cfg *configHooks) executeTemplateCommand(hookType string, tmpl string, dat executeHookCommand(hookType, cfg.Shell, cmd) } -var hourlyHooks = []func(){} +type hourlyHookFunc func() func (a *goBlog) startHourlyHooks() { cfg := a.cfg.Hooks @@ -88,14 +88,14 @@ func (a *goBlog) startHourlyHooks() { f := func() { executeHookCommand("hourly", cfg.Shell, c) } - hourlyHooks = append(hourlyHooks, f) + a.hourlyHooks = append(a.hourlyHooks, f) } // When there are hooks, start ticker - if len(hourlyHooks) > 0 { + if len(a.hourlyHooks) > 0 { // Wait for next full hour tr := time.AfterFunc(time.Until(time.Now().Truncate(time.Hour).Add(time.Hour)), func() { // Execute once - for _, f := range hourlyHooks { + for _, f := range a.hourlyHooks { go f() } // Start ticker and execute regularly @@ -105,7 +105,7 @@ func (a *goBlog) startHourlyHooks() { log.Println("Stopped hourly hooks") }) for range ticker.C { - for _, f := range hourlyHooks { + for _, f := range a.hourlyHooks { go f() } } diff --git a/http.go b/http.go index 54c0078..e820135 100644 --- a/http.go +++ b/http.go @@ -21,25 +21,7 @@ import ( ) const ( - contentType = "Content-Type" - - charsetUtf8Suffix = "; charset=utf-8" - - contentTypeHTML = "text/html" - contentTypeXML = "text/xml" - contentTypeJSON = "application/json" - contentTypeWWWForm = "application/x-www-form-urlencoded" - contentTypeMultipartForm = "multipart/form-data" - contentTypeAS = "application/activity+json" - contentTypeRSS = "application/rss+xml" - contentTypeATOM = "application/atom+xml" - contentTypeJSONFeed = "application/feed+json" - - contentTypeHTMLUTF8 = contentTypeHTML + charsetUtf8Suffix - contentTypeXMLUTF8 = contentTypeXML + charsetUtf8Suffix - contentTypeJSONUTF8 = contentTypeJSON + charsetUtf8Suffix - contentTypeASUTF8 = contentTypeAS + charsetUtf8Suffix - + contentType = "Content-Type" userAgent = "User-Agent" appUserAgent = "GoBlog" ) @@ -241,6 +223,7 @@ func (a *goBlog) buildStaticHandlersRouters() error { a.setBlogMiddlewares = map[string]func(http.Handler) http.Handler{} a.sectionMiddlewares = map[string]func(http.Handler) http.Handler{} a.taxonomyMiddlewares = map[string]func(http.Handler) http.Handler{} + a.taxValueMiddlewares = map[string]func(http.Handler) http.Handler{} a.photosMiddlewares = map[string]func(http.Handler) http.Handler{} a.searchMiddlewares = map[string]func(http.Handler) http.Handler{} a.customPagesMiddlewares = map[string]func(http.Handler) http.Handler{} @@ -293,10 +276,6 @@ func (a *goBlog) buildStaticHandlersRouters() error { return nil } -var ( - taxValueMiddlewares = map[string]func(http.Handler) http.Handler{} -) - func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) { r := chi.NewRouter() @@ -436,14 +415,14 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) { for _, tv := range taxValues { r.Group(func(r chi.Router) { vPath := taxPath + "/" + urlize(tv) - if _, ok := taxValueMiddlewares[vPath]; !ok { - taxValueMiddlewares[vPath] = middleware.WithValue(indexConfigKey, &indexConfig{ + if _, ok := a.taxValueMiddlewares[vPath]; !ok { + a.taxValueMiddlewares[vPath] = middleware.WithValue(indexConfigKey, &indexConfig{ path: vPath, tax: taxonomy, taxValue: tv, }) } - r.Use(taxValueMiddlewares[vPath]) + r.Use(a.taxValueMiddlewares[vPath]) r.Get(vPath, a.serveIndex) r.Get(vPath+feedPath, a.serveIndex) r.Get(vPath+paginationPath, a.serveIndex) @@ -512,10 +491,9 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) { r.Group(func(r chi.Router) { r.Use(a.privateModeHandler...) r.Use(sbm) - blogBasePath := blogConfig.getRelativePath("") - r.With(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).Get(blogBasePath, a.serveHome) - r.With(a.cache.cacheMiddleware).Get(blogBasePath+feedPath, a.serveHome) - r.With(a.cache.cacheMiddleware).Get(blogBasePath+paginationPath, a.serveHome) + r.With(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).Get(blogConfig.getRelativePath(""), a.serveHome) + r.With(a.cache.cacheMiddleware).Get(blogConfig.getRelativePath("")+feedPath, a.serveHome) + r.With(a.cache.cacheMiddleware).Get(blogConfig.getRelativePath(paginationPath), a.serveHome) }) } @@ -579,17 +557,15 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) { const blogContextKey requestContextKey = "blog" const pathContextKey requestContextKey = "httpPath" -var cspDomains = "" - func (a *goBlog) refreshCSPDomains() { - cspDomains = "" + a.cspDomains = "" if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" { if u, err := url.Parse(mp.MediaURL); err == nil { - cspDomains += " " + u.Hostname() + a.cspDomains += " " + u.Hostname() } } if len(a.cfg.Server.CSPDomains) > 0 { - cspDomains += " " + strings.Join(a.cfg.Server.CSPDomains, " ") + a.cspDomains += " " + strings.Join(a.cfg.Server.CSPDomains, " ") } } @@ -601,7 +577,7 @@ func (a *goBlog) securityHeaders(next http.Handler) http.Handler { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "SAMEORIGIN") w.Header().Set("X-Xss-Protection", "1; mode=block") - w.Header().Set("Content-Security-Policy", "default-src 'self'"+cspDomains) + w.Header().Set("Content-Security-Policy", "default-src 'self'"+a.cspDomains) if a.cfg.Server.Tor && a.torAddress != "" { w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI)) } diff --git a/indieAuthServer.go b/indieAuthServer.go index 2da8a6e..814b7f6 100644 --- a/indieAuthServer.go +++ b/indieAuthServer.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" "github.com/spf13/cast" ) @@ -137,8 +138,8 @@ func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request) { b, _ := json.Marshal(tokenResponse{ Me: a.cfg.Server.PublicAddress, }) - w.Header().Set(contentType, contentTypeJSONUTF8) - _, _ = writeMinified(w, contentTypeJSON, b) + w.Header().Set(contentType, contenttype.JSONUTF8) + _, _ = a.min.Write(w, contenttype.JSON, b) } func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) { @@ -155,8 +156,8 @@ func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) { ClientID: data.ClientID, } b, _ := json.Marshal(res) - w.Header().Set(contentType, contentTypeJSONUTF8) - _, _ = writeMinified(w, contentTypeJSON, b) + w.Header().Set(contentType, contenttype.JSONUTF8) + _, _ = a.min.Write(w, contenttype.JSON, b) return } else if r.Method == http.MethodPost { if err := r.ParseForm(); err != nil { @@ -208,8 +209,8 @@ func (a *goBlog) indieAuthToken(w http.ResponseWriter, r *http.Request) { Me: a.cfg.Server.PublicAddress, } b, _ := json.Marshal(res) - w.Header().Set(contentType, contentTypeJSONUTF8) - _, _ = writeMinified(w, contentTypeJSON, b) + w.Header().Set(contentType, contenttype.JSONUTF8) + _, _ = a.min.Write(w, contenttype.JSON, b) return } a.serveError(w, r, "", http.StatusBadRequest) diff --git a/main.go b/main.go index 070d019..5e123ab 100644 --- a/main.go +++ b/main.go @@ -10,13 +10,12 @@ import ( "github.com/pquerna/otp/totp" ) -var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") -var memprofile = flag.String("memprofile", "", "write memory profile to `file`") - func main() { var err error // Init CPU and memory profiling + cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`") + memprofile := flag.String("memprofile", "", "write memory profile to `file`") flag.Parse() if *cpuprofile != "" { f, err := os.Create(*cpuprofile) diff --git a/markdown.go b/markdown.go index beb0669..6df7fef 100644 --- a/markdown.go +++ b/markdown.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "html/template" "strings" marktag "git.jlel.se/jlelse/goldmark-mark" @@ -54,6 +55,19 @@ func (a *goBlog) renderMarkdown(source string, absoluteLinks bool) (rendered []b return buffer.Bytes(), err } +func (a *goBlog) renderMarkdownAsHTML(source string, absoluteLinks bool) (rendered template.HTML, err error) { + b, err := a.renderMarkdown(source, absoluteLinks) + if err != nil { + return "", err + } + return template.HTML(b), nil +} + +func (a *goBlog) safeRenderMarkdownAsHTML(source string) template.HTML { + h, _ := a.renderMarkdownAsHTML(source, false) + return h +} + func (a *goBlog) renderText(s string) string { h, err := a.renderMarkdown(s, false) if err != nil { diff --git a/markdown_test.go b/markdown_test.go index cc21af1..4dbd5a8 100644 --- a/markdown_test.go +++ b/markdown_test.go @@ -4,6 +4,8 @@ import ( "os" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func Test_markdown(t *testing.T) { @@ -65,6 +67,11 @@ func Test_markdown(t *testing.T) { if renderedText != "This is text" { t.Errorf("Wrong result, got \"%v\"", renderedText) } + + // Template func + + renderedText = string(app.safeRenderMarkdownAsHTML("[Relative](/relative)")) + assert.Contains(t, renderedText, `href="/relative"`) }) } diff --git a/mediaCompression.go b/mediaCompression.go index 52fbc57..8529d9d 100644 --- a/mediaCompression.go +++ b/mediaCompression.go @@ -8,6 +8,8 @@ import ( "io" "net/http" "os" + + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" ) const defaultCompressionWidth = 2000 @@ -34,7 +36,7 @@ func (a *goBlog) tinify(url string, config *configMicropubMedia) (location strin return "", err } req.SetBasicAuth("api", config.TinifyKey) - req.Header.Set(contentType, contentTypeJSON) + req.Header.Set(contentType, contenttype.JSON) resp, err := appHttpClient.Do(req) if err != nil { return "", err @@ -61,7 +63,7 @@ func (a *goBlog) tinify(url string, config *configMicropubMedia) (location strin return "", err } downloadReq.SetBasicAuth("api", config.TinifyKey) - downloadReq.Header.Set(contentType, contentTypeJSON) + downloadReq.Header.Set(contentType, contenttype.JSON) downloadResp, err := appHttpClient.Do(downloadReq) if err != nil { return "", err diff --git a/micropub.go b/micropub.go index d8d16d8..37e53eb 100644 --- a/micropub.go +++ b/micropub.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" "github.com/spf13/cast" "gopkg.in/yaml.v3" ) @@ -25,12 +26,12 @@ type micropubConfig struct { func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) { switch r.URL.Query().Get("q") { case "config": - w.Header().Set(contentType, contentTypeJSONUTF8) + w.Header().Set(contentType, contenttype.JSONUTF8) w.WriteHeader(http.StatusOK) b, _ := json.Marshal(µpubConfig{ MediaEndpoint: a.getFullAddress(micropubPath + micropubMediaSubPath), }) - _, _ = writeMinified(w, contentTypeJSON, b) + _, _ = a.min.Write(w, contenttype.JSON, b) case "source": var mf interface{} if urlString := r.URL.Query().Get("url"); urlString != "" { @@ -62,10 +63,10 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) { } mf = list } - w.Header().Set(contentType, contentTypeJSONUTF8) + w.Header().Set(contentType, contenttype.JSONUTF8) w.WriteHeader(http.StatusOK) b, _ := json.Marshal(mf) - _, _ = writeMinified(w, contentTypeJSON, b) + _, _ = a.min.Write(w, contenttype.JSON, b) case "category": allCategories := []string{} for blog := range a.cfg.Blogs { @@ -76,12 +77,12 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) { } allCategories = append(allCategories, values...) } - w.Header().Set(contentType, contentTypeJSONUTF8) + w.Header().Set(contentType, contenttype.JSONUTF8) w.WriteHeader(http.StatusOK) b, _ := json.Marshal(map[string]interface{}{ "categories": allCategories, }) - _, _ = writeMinified(w, contentTypeJSON, b) + _, _ = a.min.Write(w, contenttype.JSON, b) default: a.serve404(w, r) } @@ -120,9 +121,9 @@ func (a *goBlog) toMfItem(p *post) *microformatItem { func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var p *post - if ct := r.Header.Get(contentType); strings.Contains(ct, contentTypeWWWForm) || strings.Contains(ct, contentTypeMultipartForm) { + if ct := r.Header.Get(contentType); strings.Contains(ct, contenttype.WWWForm) || strings.Contains(ct, contenttype.MultipartForm) { var err error - if strings.Contains(ct, contentTypeMultipartForm) { + if strings.Contains(ct, contenttype.MultipartForm) { err = r.ParseMultipartForm(0) } else { err = r.ParseForm() @@ -149,7 +150,7 @@ func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) { a.serveError(w, r, err.Error(), http.StatusInternalServerError) return } - } else if strings.Contains(ct, contentTypeJSON) { + } else if strings.Contains(ct, contenttype.JSON) { parsedMfItem := µformatItem{} err := json.NewDecoder(r.Body).Decode(parsedMfItem) if err != nil { diff --git a/micropubMedia.go b/micropubMedia.go index 9869ca8..a056a75 100644 --- a/micropubMedia.go +++ b/micropubMedia.go @@ -11,6 +11,8 @@ import ( "path/filepath" "sort" "strings" + + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" ) const micropubMediaSubPath = "/media" @@ -20,7 +22,7 @@ func (a *goBlog) serveMicropubMedia(w http.ResponseWriter, r *http.Request) { a.serveError(w, r, "media scope missing", http.StatusForbidden) return } - if ct := r.Header.Get(contentType); !strings.Contains(ct, contentTypeMultipartForm) { + if ct := r.Header.Get(contentType); !strings.Contains(ct, contenttype.MultipartForm) { a.serveError(w, r, "wrong content-type", http.StatusBadRequest) return } diff --git a/minify.go b/minify.go deleted file mode 100644 index 6b51b59..0000000 --- a/minify.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "io" - "sync" - - "github.com/tdewolff/minify/v2" - mCss "github.com/tdewolff/minify/v2/css" - mHtml "github.com/tdewolff/minify/v2/html" - mJs "github.com/tdewolff/minify/v2/js" - mJson "github.com/tdewolff/minify/v2/json" - mXml "github.com/tdewolff/minify/v2/xml" -) - -var ( - initMinify sync.Once - minifier *minify.M -) - -func getMinifier() *minify.M { - initMinify.Do(func() { - minifier = minify.New() - minifier.AddFunc(contentTypeHTML, mHtml.Minify) - minifier.AddFunc("text/css", mCss.Minify) - minifier.AddFunc(contentTypeXML, mXml.Minify) - minifier.AddFunc("application/javascript", mJs.Minify) - minifier.AddFunc(contentTypeRSS, mXml.Minify) - minifier.AddFunc(contentTypeATOM, mXml.Minify) - minifier.AddFunc(contentTypeJSONFeed, mJson.Minify) - minifier.AddFunc(contentTypeAS, mJson.Minify) - }) - return minifier -} - -func writeMinified(w io.Writer, mediatype string, b []byte) (int, error) { - mw := getMinifier().Writer(mediatype, w) - defer func() { _ = mw.Close() }() - return mw.Write(b) -} diff --git a/nodeinfo.go b/nodeinfo.go index 9b34b34..0f3204c 100644 --- a/nodeinfo.go +++ b/nodeinfo.go @@ -3,6 +3,8 @@ package main import ( "encoding/json" "net/http" + + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" ) func (a *goBlog) serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) { @@ -14,8 +16,8 @@ func (a *goBlog) serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) { }, }, }) - w.Header().Set(contentType, contentTypeJSONUTF8) - _, _ = writeMinified(w, contentTypeJSON, b) + w.Header().Set(contentType, contenttype.JSONUTF8) + _, _ = a.min.Write(w, contenttype.JSON, b) } func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) { @@ -41,6 +43,6 @@ func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) { }, "metadata": map[string]interface{}{}, }) - w.Header().Set(contentType, contentTypeJSONUTF8) - _, _ = writeMinified(w, contentTypeJSON, b) + w.Header().Set(contentType, contenttype.JSONUTF8) + _, _ = a.min.Write(w, contenttype.JSON, b) } diff --git a/opensearch.go b/opensearch.go index 51329bc..feccc2a 100644 --- a/opensearch.go +++ b/opensearch.go @@ -3,6 +3,8 @@ package main import ( "fmt" "net/http" + + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" ) func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) { @@ -17,7 +19,7 @@ func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) { "", title, title, sURL, sURL) w.Header().Set(contentType, "application/opensearchdescription+xml") - _, _ = writeMinified(w, contentTypeXML, []byte(xml)) + _, _ = a.min.Write(w, contenttype.XML, []byte(xml)) } func openSearchUrl(b *configBlog) string { diff --git a/paths.go b/paths.go index 0499e85..d6bcd0b 100644 --- a/paths.go +++ b/paths.go @@ -43,3 +43,9 @@ func (cfg *configServer) getFullAddress(path string) string { } return pa + path } + +// Rendering funcs + +func (blog *configBlog) RelativePath(path string) string { + return blog.getRelativePath(path) +} diff --git a/pkgs/contenttype/contenttype.go b/pkgs/contenttype/contenttype.go new file mode 100644 index 0000000..9460410 --- /dev/null +++ b/pkgs/contenttype/contenttype.go @@ -0,0 +1,25 @@ +package contenttype + +// This package contains constants for a few content types used in GoBlog + +const ( + CharsetUtf8Suffix = "; charset=utf-8" + + AS = "application/activity+json" + ATOM = "application/atom+xml" + CSS = "text/css" + HTML = "text/html" + JS = "application/javascript" + JSON = "application/json" + JSONFeed = "application/feed+json" + LDJSON = "application/ld+json" + MultipartForm = "multipart/form-data" + RSS = "application/rss+xml" + WWWForm = "application/x-www-form-urlencoded" + XML = "text/xml" + + ASUTF8 = AS + CharsetUtf8Suffix + HTMLUTF8 = HTML + CharsetUtf8Suffix + JSONUTF8 = JSON + CharsetUtf8Suffix + XMLUTF8 = XML + CharsetUtf8Suffix +) diff --git a/pkgs/minify/minify.go b/pkgs/minify/minify.go new file mode 100644 index 0000000..6c7989d --- /dev/null +++ b/pkgs/minify/minify.go @@ -0,0 +1,45 @@ +package minify + +import ( + "io" + "sync" + + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" + "github.com/tdewolff/minify/v2" + mCss "github.com/tdewolff/minify/v2/css" + mHtml "github.com/tdewolff/minify/v2/html" + mJs "github.com/tdewolff/minify/v2/js" + mJson "github.com/tdewolff/minify/v2/json" + mXml "github.com/tdewolff/minify/v2/xml" +) + +type Minifier struct { + i sync.Once + m *minify.M +} + +func (m *Minifier) init() { + m.i.Do(func() { + m.m = minify.New() + m.m.AddFunc(contenttype.HTML, mHtml.Minify) + m.m.AddFunc(contenttype.CSS, mCss.Minify) + m.m.AddFunc(contenttype.XML, mXml.Minify) + m.m.AddFunc(contenttype.JS, mJs.Minify) + m.m.AddFunc(contenttype.RSS, mXml.Minify) + m.m.AddFunc(contenttype.ATOM, mXml.Minify) + m.m.AddFunc(contenttype.JSONFeed, mJson.Minify) + m.m.AddFunc(contenttype.AS, mJson.Minify) + }) +} + +func (m *Minifier) Get() *minify.M { + m.init() + return m.m +} + +func (m *Minifier) Write(w io.Writer, mediatype string, b []byte) (int, error) { + m.init() + mw := m.m.Writer(mediatype, w) + defer func() { _ = mw.Close() }() + return mw.Write(b) +} diff --git a/pkgs/minify/minify_test.go b/pkgs/minify/minify_test.go new file mode 100644 index 0000000..2a56c1e --- /dev/null +++ b/pkgs/minify/minify_test.go @@ -0,0 +1,12 @@ +package minify + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_minify(t *testing.T) { + var min Minifier + assert.NotNil(t, min.Get()) +} diff --git a/postsDb.go b/postsDb.go index d2afa0e..f229eaf 100644 --- a/postsDb.go +++ b/postsDb.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "strings" - "sync" "text/template" "time" @@ -122,8 +121,6 @@ type postCreationOptions struct { oldStatus postStatus } -var postCreationMutex sync.Mutex - func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error { // Check post if err := a.checkPost(p); err != nil { @@ -148,8 +145,8 @@ func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error { // Save check post to database func (db *database) savePost(p *post, o *postCreationOptions) error { // Prevent bad things - postCreationMutex.Lock() - defer postCreationMutex.Unlock() + db.pcm.Lock() + defer db.pcm.Unlock() // Check if path is already in use if o.new || (p.Path != o.oldPath) { // Post is new or post path was changed diff --git a/postsDb_test.go b/postsDb_test.go index 8efa234..cefb297 100644 --- a/postsDb_test.go +++ b/postsDb_test.go @@ -53,7 +53,7 @@ func Test_postsDb(t *testing.T) { is.Equal("en", p.Blog) is.Equal("test", p.Section) is.Equal(statusDraft, p.Status) - is.Equal("Title", p.title()) + is.Equal("Title", p.Title()) // Check number of post paths pp, err := app.db.allPostPaths(statusDraft) diff --git a/postsFuncs.go b/postsFuncs.go index cea3a9f..e70a08e 100644 --- a/postsFuncs.go +++ b/postsFuncs.go @@ -4,8 +4,11 @@ import ( "html/template" "log" "strings" + "time" + gogeouri "git.jlel.se/jlelse/go-geouri" "github.com/PuerkitoBio/goquery" + "github.com/araddon/dateparse" ) func (a *goBlog) fullPostURL(p *post) string { @@ -23,6 +26,14 @@ func (a *goBlog) shortPostURL(p *post) string { return a.getFullAddress(s) } +func postParameter(p *post, parameter string) []string { + return p.Parameters[parameter] +} + +func postHasParameter(p *post, parameter string) bool { + return len(p.Parameters[parameter]) > 0 +} + func (p *post) firstParameter(parameter string) (result string) { if pp := p.Parameters[parameter]; len(pp) > 0 { result = pp[0] @@ -30,11 +41,11 @@ func (p *post) firstParameter(parameter string) (result string) { return } -func (p *post) title() string { - return p.firstParameter("title") +func firstPostParameter(p *post, parameter string) string { + return p.firstParameter(parameter) } -func (a *goBlog) html(p *post) template.HTML { +func (a *goBlog) postHtml(p *post) template.HTML { if p.rendered != "" { return p.rendered } @@ -47,7 +58,7 @@ func (a *goBlog) html(p *post) template.HTML { return p.rendered } -func (a *goBlog) absoluteHTML(p *post) template.HTML { +func (a *goBlog) absolutePostHTML(p *post) template.HTML { if p.absoluteRendered != "" { return p.absoluteRendered } @@ -62,12 +73,12 @@ func (a *goBlog) absoluteHTML(p *post) template.HTML { const summaryDivider = "" -func (a *goBlog) summary(p *post) (summary string) { +func (a *goBlog) postSummary(p *post) (summary string) { summary = p.firstParameter("summary") if summary != "" { return } - html := string(a.html(p)) + html := string(a.postHtml(p)) if splitted := strings.Split(html, summaryDivider); len(splitted) > 1 { doc, _ := goquery.NewDocumentFromReader(strings.NewReader(splitted[0])) summary = doc.Text() @@ -78,7 +89,7 @@ func (a *goBlog) summary(p *post) (summary string) { return } -func (a *goBlog) translations(p *post) []*post { +func (a *goBlog) postTranslations(p *post) []*post { translationkey := p.firstParameter("translationkey") if translationkey == "" { return nil @@ -105,3 +116,30 @@ func (a *goBlog) translations(p *post) []*post { func (p *post) isPublishedSectionPost() bool { return p.Published != "" && p.Section != "" && p.Status == statusPublished } + +// Public because of rendering + +func (p *post) Title() string { + return p.firstParameter("title") +} + +func (p *post) GeoURI() *gogeouri.Geo { + loc := p.firstParameter("location") + if loc == "" { + return nil + } + g, _ := gogeouri.Parse(loc) + return g +} + +func (p *post) Old() bool { + pub := p.Published + if pub == "" { + return false + } + pubDate, err := dateparse.ParseLocal(pub) + if err != nil { + return false + } + return pubDate.AddDate(1, 0, 0).Before(time.Now()) +} diff --git a/render.go b/render.go index b1d9974..cd687a8 100644 --- a/render.go +++ b/render.go @@ -2,21 +2,16 @@ package main import ( "bytes" - "encoding/json" "errors" "fmt" "html/template" - "log" "net/http" - "net/url" "os" "path" "path/filepath" "strings" - "time" - gogeouri "git.jlel.se/jlelse/go-geouri" - "github.com/araddon/dateparse" + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" servertiming "github.com/mitchellh/go-server-timing" ) @@ -48,142 +43,32 @@ const ( func (a *goBlog) initRendering() error { a.templates = map[string]*template.Template{} templateFunctions := template.FuncMap{ - "menu": func(blog *configBlog, id string) *menu { - return blog.Menus[id] - }, - "user": func() *configUser { - return a.cfg.User - }, - "md": func(content string) template.HTML { - htmlContent, err := a.renderMarkdown(content, false) - if err != nil { - log.Fatal(err) - return "" - } - return template.HTML(htmlContent) - }, - "html": func(s string) template.HTML { - return template.HTML(s) - }, + "md": a.safeRenderMarkdownAsHTML, + "html": wrapStringAsHTML, // Post specific - "p": func(p *post, parameter string) string { - return p.firstParameter(parameter) - }, - "ps": func(p *post, parameter string) []string { - return p.Parameters[parameter] - }, - "hasp": func(p *post, parameter string) bool { - return len(p.Parameters[parameter]) > 0 - }, - "title": func(p *post) string { - return p.title() - }, - "content": func(p *post) template.HTML { - return a.html(p) - }, - "summary": func(p *post) string { - return a.summary(p) - }, - "translations": func(p *post) []*post { - return a.translations(p) - }, - "shorturl": func(p *post) string { - return a.shortPostURL(p) - }, + "p": firstPostParameter, + "ps": postParameter, + "hasp": postHasParameter, + "content": a.postHtml, + "summary": a.postSummary, + "translations": a.postTranslations, + "shorturl": a.shortPostURL, // Others "dateformat": dateFormat, - "isodate": func(date string) string { - return dateFormat(date, "2006-01-02") - }, - "unixtodate": func(unix int64) string { - return time.Unix(unix, 0).Local().String() - }, - "now": func() string { - return time.Now().Local().String() - }, - "dateadd": func(date string, years, months, days int) string { - d, err := dateparse.ParseLocal(date) - if err != nil { - return "" - } - return d.AddDate(years, months, days).Local().String() - }, - "datebefore": func(date string, before string) bool { - d, err := dateparse.ParseLocal(date) - if err != nil { - return false - } - b, err := dateparse.ParseLocal(before) - if err != nil { - return false - } - return d.Before(b) - }, - "asset": a.assetFileName, - "assetsri": a.assetSRI, - "string": a.ts.GetTemplateStringVariantFunc(), - "include": func(templateName string, data ...interface{}) (template.HTML, error) { - if len(data) == 0 || len(data) > 2 { - return "", errors.New("wrong argument count") - } - if rd, ok := data[0].(*renderData); ok { - if len(data) == 2 { - nrd := *rd - nrd.Data = data[1] - rd = &nrd - } - var buf bytes.Buffer - err := a.templates[templateName].ExecuteTemplate(&buf, templateName, rd) - return template.HTML(buf.String()), err - } - return "", errors.New("wrong arguments") - }, - "urlize": urlize, - "sort": sortedStrings, - "absolute": func(path string) string { - return a.getFullAddress(path) - }, - "blogrelative": func(blog *configBlog, path string) string { - return blog.getRelativePath(path) - }, - "jsonFile": func(filename string) *map[string]interface{} { - parsed := &map[string]interface{}{} - content, err := os.ReadFile(filename) - if err != nil { - return nil - } - err = json.Unmarshal(content, parsed) - if err != nil { - fmt.Println(err.Error()) - return nil - } - return parsed - }, - "mentions": func(absolute string) []*mention { - mentions, _ := a.db.getWebmentions(&webmentionsRequestConfig{ - target: absolute, - status: webmentionStatusApproved, - asc: true, - }) - return mentions - }, - "urlToString": func(u url.URL) string { - return u.String() - }, - "geouri": func(u string) *gogeouri.Geo { - g, _ := gogeouri.Parse(u) - return g - }, - "geourip": func(g *gogeouri.Geo, parameter string) (s string) { - if gp := g.Parameters[parameter]; len(gp) > 0 { - return gp[0] - } - return - }, + "isodate": isoDateFormat, + "unixtodate": unixToLocalDateString, + "now": localNowString, + "asset": a.assetFileName, + "string": a.ts.GetTemplateStringVariantFunc(), + "include": a.includeRenderedTemplate, + "urlize": urlize, + "sort": sortedStrings, + "absolute": a.getFullAddress, + "mentions": a.db.getWebmentionsByAddress, "geotitle": a.db.geoTitle, + "geolink": geoOSMLink, "opensearch": openSearchUrl, } - baseTemplate, err := template.New("base").Funcs(templateFunctions).ParseFiles(path.Join(templatesDir, templateBase+templatesExt)) if err != nil { return err @@ -212,6 +97,7 @@ type renderData struct { Canonical string TorAddress string Blog *configBlog + User *configUser Data interface{} LoggedIn bool CommentsEnabled bool @@ -223,6 +109,9 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string, // Server timing t := servertiming.FromContext(r.Context()).NewMetric("r").Start() // Check render data + if data.User == nil { + data.User = a.cfg.User + } if data.Blog == nil { if len(data.BlogString) == 0 { data.BlogString = a.cfg.DefaultBlog @@ -256,7 +145,7 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string, data.TorUsed = true } // Set content type - w.Header().Set(contentType, contentTypeHTMLUTF8) + w.Header().Set(contentType, contenttype.HTMLUTF8) // Minify and write response var tw bytes.Buffer err := a.templates[template].ExecuteTemplate(&tw, template, data) @@ -264,7 +153,7 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string, http.Error(w, err.Error(), http.StatusInternalServerError) return } - _, err = writeMinified(w, contentTypeHTML, tw.Bytes()) + _, err = a.min.Write(w, contenttype.HTML, tw.Bytes()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -272,3 +161,20 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string, // Server timing t.Stop() } + +func (a *goBlog) includeRenderedTemplate(templateName string, data ...interface{}) (template.HTML, error) { + if l := len(data); l < 1 || l > 2 { + return "", errors.New("wrong argument count") + } + if rd, ok := data[0].(*renderData); ok { + if len(data) == 2 { + nrd := *rd + nrd.Data = data[1] + rd = &nrd + } + var buf bytes.Buffer + err := a.templates[templateName].ExecuteTemplate(&buf, templateName, rd) + return template.HTML(buf.String()), err + } + return "", errors.New("wrong arguments") +} diff --git a/sessions.go b/sessions.go index 32c7a27..fb9cb86 100644 --- a/sessions.go +++ b/sessions.go @@ -27,7 +27,7 @@ func (a *goBlog) initSessions() { } } deleteExpiredSessions() - hourlyHooks = append(hourlyHooks, deleteExpiredSessions) + a.hourlyHooks = append(a.hourlyHooks, deleteExpiredSessions) a.loginSessions = &dbSessionStore{ codecs: securecookie.CodecsFromPairs(a.jwtKey()), options: &sessions.Options{ diff --git a/telegram.go b/telegram.go index 4299942..3d667db 100644 --- a/telegram.go +++ b/telegram.go @@ -16,7 +16,7 @@ const telegramBaseURL = "https://api.telegram.org/bot" func (a *goBlog) initTelegram() { a.pPostHooks = append(a.pPostHooks, func(p *post) { if tg := a.cfg.Blogs[p.Blog].Telegram; tg.enabled() && p.isPublishedSectionPost() { - if html := tg.generateHTML(p.title(), a.fullPostURL(p), a.shortPostURL(p)); html != "" { + if html := tg.generateHTML(p.Title(), a.fullPostURL(p), a.shortPostURL(p)); html != "" { if err := tg.send(html, "HTML"); err != nil { log.Printf("Failed to send post to Telegram: %v", err) } diff --git a/templateAssets.go b/templateAssets.go index 4b406a7..c107282 100644 --- a/templateAssets.go +++ b/templateAssets.go @@ -2,23 +2,21 @@ package main import ( "crypto/sha1" - "crypto/sha512" - "encoding/base64" "fmt" - "io" "mime" "net/http" "os" "path" "path/filepath" "strings" + + "git.jlel.se/jlelse/GoBlog/pkgs/contenttype" ) const assetsFolder = "templates/assets" type assetFile struct { contentType string - sri string body []byte } @@ -50,7 +48,7 @@ func (a *goBlog) compileAsset(name string) (string, error) { } ext := path.Ext(name) compiledExt := ext - m := getMinifier() + m := a.min.Get() switch ext { case ".js": content, err = m.Bytes("application/javascript", content) @@ -67,18 +65,14 @@ func (a *goBlog) compileAsset(name string) (string, error) { } // Hashes sha1Hash := sha1.New() - sha512Hash := sha512.New() - if _, err := io.MultiWriter(sha1Hash, sha512Hash).Write(content); err != nil { + if _, err := sha1Hash.Write(content); err != nil { return "", err } // File name compiledFileName := fmt.Sprintf("%x", sha1Hash.Sum(nil)) + compiledExt - // SRI - sriHash := fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil))) // Create struct a.assetFiles[compiledFileName] = &assetFile{ contentType: mime.TypeByExtension(compiledExt), - sri: sriHash, body: content, } return compiledFileName, err @@ -89,10 +83,6 @@ func (a *goBlog) assetFileName(fileName string) string { return "/" + a.assetFileNames[fileName] } -func (a *goBlog) assetSRI(fileName string) string { - return a.assetFiles[a.assetFileNames[fileName]].sri -} - func (a *goBlog) allAssetPaths() []string { var paths []string for _, name := range a.assetFileNames { @@ -109,6 +99,6 @@ func (a *goBlog) serveAsset(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Cache-Control", "public,max-age=31536000,immutable") - w.Header().Set(contentType, af.contentType+charsetUtf8Suffix) + w.Header().Set(contentType, af.contentType+contenttype.CharsetUtf8Suffix) _, _ = w.Write(af.body) } diff --git a/templates/author.gohtml b/templates/author.gohtml index 5f23fe7..722de35 100644 --- a/templates/author.gohtml +++ b/templates/author.gohtml @@ -1,5 +1,5 @@ {{ define "author" }} - {{ with user }} + {{ with .User }}
{{ with .Picture }}{{ end }} {{ if .Name }} diff --git a/templates/base.gohtml b/templates/base.gohtml index 6c89dae..ec74f38 100644 --- a/templates/base.gohtml +++ b/templates/base.gohtml @@ -15,11 +15,7 @@ - {{ with user }} - {{ range .Identities }} - - {{ end }} - {{ end }} + {{ with .User }}{{ range .Identities }}{{ end }}{{ end }} {{ $os := opensearch .Blog }} {{ if $os }} diff --git a/templates/blogroll.gohtml b/templates/blogroll.gohtml index 5f4793e..15e3f50 100644 --- a/templates/blogroll.gohtml +++ b/templates/blogroll.gohtml @@ -6,7 +6,7 @@
{{ with .Data.Title }}

{{ . }}

{{ end }} {{ with .Data.Description }}{{ md . }}{{ end }} -

{{ string .Blog.Lang "download" }}

+

{{ string .Blog.Lang "download" }}

{{ $lang := .Blog.Lang }} {{ range .Data.Outlines }} {{ $title := .Title }} @@ -16,7 +16,7 @@ {{ range .Outlines }} {{ $ct := .Title }} {{ if not $ct }}{{ $ct = .Text }}{{ end }} -
  • {{ $ct }} ({{ string $lang "feed" }})
  • +
  • {{ $ct }} ({{ string $lang "feed" }})
  • {{ end }} {{ end }} diff --git a/templates/editor.gohtml b/templates/editor.gohtml index e3ccd0f..aff3fd3 100644 --- a/templates/editor.gohtml +++ b/templates/editor.gohtml @@ -55,7 +55,7 @@ tags: diff --git a/templates/footer.gohtml b/templates/footer.gohtml index 0697025..ec685a2 100644 --- a/templates/footer.gohtml +++ b/templates/footer.gohtml @@ -1,13 +1,13 @@ {{ define "footer" }}