Bug fixes, refactoring and other improvements

pull/7/head
Jan-Lukas Else 2 years ago
parent 20df90bf4e
commit 67f2b1fbdb

@ -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

@ -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(`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="https://` + r.Host + `/.well-known/webfinger?resource={uri}"/></XRD>`))
}
@ -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,

@ -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()

@ -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)
}

@ -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

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

@ -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) {

@ -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 {

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

@ -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}
}

@ -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)

@ -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)

@ -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

@ -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))
}

@ -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)
}

@ -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()
}
}

@ -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))
}

@ -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)

@ -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)

@ -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 {

@ -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"`)
})
}

@ -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

@ -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(&micropubConfig{
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 := &microformatItem{}
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
if err != nil {

@ -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
}

@ -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)
}

@ -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)
}

@ -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) {
"</OpenSearchDescription>",
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 {

@ -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)
}

@ -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
)

@ -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)
}

@ -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())
}

@ -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

@ -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)

@ -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 = "<!--more-->"
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