Reduce complexity of router build method

This commit is contained in:
Jan-Lukas Else 2021-07-27 12:51:08 +02:00
parent 91e2b268c7
commit 88f26ef3c6
33 changed files with 593 additions and 435 deletions

View File

@ -22,7 +22,12 @@ import (
)
func (a *goBlog) initActivityPub() error {
if !a.cfg.ActivityPub.Enabled {
if a.isPrivate() {
// Private mode, no AP
return nil
}
if apc := a.cfg.ActivityPub; apc == nil || !apc.Enabled {
// Disabled
return nil
}
// Add hooks

View File

@ -26,7 +26,7 @@ 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 {
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled && !a.isPrivate() {
// Check if accepted media type is not HTML
if mt, _, err := ct.GetAcceptableMediaType(r, a.asCheckMediaTypes); err == nil && mt.String() != a.asCheckMediaTypes[0].String() {
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true)))

2
app.go
View File

@ -45,8 +45,6 @@ type goBlog struct {
pUpdateHooks []postHookFunc
pDeleteHooks []postHookFunc
hourlyHooks []hourlyHookFunc
// HTTP
cspDomains string
// HTTP Client
httpClient httpClient
// HTTP Routers

View File

@ -18,7 +18,7 @@ import (
const defaultBlogrollPath = "/blogroll"
func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) {
return a.getBlogrollOutlines(blog)
})
@ -42,7 +42,7 @@ func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
}
func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) {
return a.getBlogrollOutlines(blog)
})

View File

@ -7,7 +7,10 @@ import (
"net/http"
)
const defaultBlogStatsPath = "/statistics"
const (
defaultBlogStatsPath = "/statistics"
blogStatsTablePath = ".table.html"
)
func (a *goBlog) initBlogStats() {
f := func(p *post) {
@ -19,20 +22,20 @@ func (a *goBlog) initBlogStats() {
}
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
bc := a.cfg.Blogs[blog]
canonical := bc.getRelativePath(defaultIfEmpty(bc.BlogStats.Path, defaultBlogStatsPath))
a.render(w, r, templateBlogStats, &renderData{
BlogString: blog,
Canonical: a.getFullAddress(canonical),
Data: map[string]interface{}{
"TableUrl": canonical + ".table.html",
"TableUrl": canonical + blogStatsTablePath,
},
})
}
func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
data, err, _ := a.blogStatsCacheGroup.Do(blog, func() (interface{}, error) {
return a.db.getBlogStats(blog)
})

View File

@ -39,7 +39,7 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
a.render(w, r, templateComment, &renderData{
BlogString: blog,
Canonical: a.getFullAddress(a.cfg.Blogs[blog].getRelativePath(fmt.Sprintf("/comment/%d", id))),
@ -75,7 +75,7 @@ func (a *goBlog) createComment(w http.ResponseWriter, r *http.Request) {
// Serve error
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
} else {
commentAddress := fmt.Sprintf("%s/%d", a.getRelativePath(r.Context().Value(blogContextKey).(string), "/comment"), commentID)
commentAddress := fmt.Sprintf("%s/%d", a.getRelativePath(r.Context().Value(blogKey).(string), "/comment"), commentID)
// Send webmention
_ = a.createWebmention(a.getFullAddress(commentAddress), a.getFullAddress(target))
// Redirect to comment

View File

@ -35,8 +35,8 @@ func (p *commentsPaginationAdapter) Slice(offset, length int, data interface{})
}
func (a *goBlog) commentsAdmin(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
commentsPath := r.Context().Value(pathContextKey).(string)
blog := r.Context().Value(blogKey).(string)
commentsPath := r.Context().Value(pathKey).(string)
// Adapter
pageNoString := chi.URLParam(r, "page")
pageNo, _ := strconv.Atoi(pageNoString)

View File

@ -290,8 +290,6 @@ func (a *goBlog) initConfig() error {
viper.SetDefault("micropub.locationParam", "location")
viper.SetDefault("activityPub.keyPath", "data/private.pem")
viper.SetDefault("activityPub.tagsTaxonomies", []string{"tags"})
viper.SetDefault("webmention.disableSending", false)
viper.SetDefault("webmention.disableReceiving", false)
// Unmarshal config
a.cfg = &config{}
err = viper.Unmarshal(a.cfg)
@ -329,9 +327,6 @@ func (a *goBlog) initConfig() error {
}
a.cfg.Micropub.MediaStorage.MediaURL = strings.TrimSuffix(a.cfg.Micropub.MediaStorage.MediaURL, "/")
}
if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled {
a.cfg.ActivityPub = &configActivityPub{Enabled: false}
}
if wm := a.cfg.Webmention; wm != nil && wm.DisableReceiving {
// Disable comments for all blogs
for _, b := range a.cfg.Blogs {

View File

@ -16,7 +16,7 @@ import (
const defaultContactPath = "/contact"
func (a *goBlog) serveContactForm(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
cc := a.cfg.Blogs[blog].Contact
a.render(w, r, templateContact, &renderData{
BlogString: blog,
@ -62,7 +62,7 @@ func (a *goBlog) sendContactSubmission(w http.ResponseWriter, r *http.Request) {
}
_, _ = message.WriteString(formMessage)
// Send submission
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
if cc := a.cfg.Blogs[blog].Contact; cc != nil && cc.SMTPHost != "" && cc.EmailFrom != "" && cc.EmailTo != "" {
// Build email
var email bytes.Buffer

View File

@ -7,7 +7,7 @@ const customPageContextKey = "custompage"
func (a *goBlog) serveCustomPage(w http.ResponseWriter, r *http.Request) {
page := r.Context().Value(customPageContextKey).(*configCustomPage)
a.render(w, r, page.Template, &renderData{
BlogString: r.Context().Value(blogContextKey).(string),
BlogString: r.Context().Value(blogKey).(string),
Canonical: a.getFullAddress(page.Path),
Data: page.Data,
})

View File

@ -14,7 +14,7 @@ import (
const editorPath = "/editor"
func (a *goBlog) serveEditor(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
a.render(w, r, templateEditor, &renderData{
BlogString: blog,
Data: map[string]interface{}{},
@ -22,7 +22,7 @@ func (a *goBlog) serveEditor(w http.ResponseWriter, r *http.Request) {
}
func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
if action := r.FormValue("editoraction"); action != "" {
switch action {
case "loaddelete":

View File

@ -8,7 +8,7 @@ import (
)
func (a *goBlog) serveEditorFiles(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
// Get files
files, err := a.mediaFiles()
if err != nil {
@ -69,5 +69,5 @@ func (a *goBlog) serveEditorFilesDelete(w http.ResponseWriter, r *http.Request)
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, a.getRelativePath(r.Context().Value(blogContextKey).(string), "/editor/files"), http.StatusFound)
http.Redirect(w, r, a.getRelativePath(r.Context().Value(blogKey).(string), "/editor/files"), http.StatusFound)
}

View File

@ -41,16 +41,14 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
},
}
for _, p := range posts {
created, _ := dateparse.ParseLocal(p.Published)
updated, _ := dateparse.ParseLocal(p.Updated)
feed.Add(&feeds.Item{
Title: p.Title(),
Link: &feeds.Link{Href: a.fullPostURL(p)},
Description: a.postSummary(p),
Id: p.Path,
Content: string(a.postHtml(p, true)),
Created: created,
Updated: updated,
Created: timeNoErr(dateparse.ParseLocal(p.Published)),
Updated: timeNoErr(dateparse.ParseLocal(p.Updated)),
})
}
var err error

View File

@ -17,7 +17,7 @@ import (
const defaultGeoMapPath = "/map"
func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
bc := a.cfg.Blogs[blog]
allPostsWithLocation, err := a.db.getPosts(&postsRequestConfig{

2
go.mod
View File

@ -53,7 +53,7 @@ require (
// master
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/net v0.0.0-20210716203947-853a461950ff
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect

4
go.sum
View File

@ -465,8 +465,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210716203947-853a461950ff h1:j2EK/QoxYNBsXI4R7fQkkRUk8y6wnOBI+6hgPdP/6Ds=
golang.org/x/net v0.0.0-20210716203947-853a461950ff/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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=

393
http.go
View File

@ -8,9 +8,7 @@ import (
"log"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/dchest/captcha"
@ -26,16 +24,18 @@ const (
contentType = "Content-Type"
userAgent = "User-Agent"
appUserAgent = "GoBlog"
blogKey contextKey = "blog"
pathKey contextKey = "httpPath"
)
func (a *goBlog) startServer() (err error) {
log.Println("Start server(s)...")
// Load router
router, err := a.buildRouter()
a.d, err = a.buildRouter()
if err != nil {
return err
}
a.d = fixHTTPHandler(router)
// Set basic middlewares
h := alice.New()
if a.cfg.Server.Logging {
@ -126,28 +126,21 @@ const (
feedPath = ".{feed:rss|json|atom}"
)
func (a *goBlog) buildRouter() (*chi.Mux, error) {
r := chi.NewRouter()
// Private mode
privateMode := false
var privateModeHandler []func(http.Handler) http.Handler
if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled {
privateMode = true
privateModeHandler = append(privateModeHandler, a.authMiddleware)
}
func (a *goBlog) buildRouter() (http.Handler, error) {
r := chi.NewMux()
// Basic middleware
r.Use(fixHTTPHandler)
r.Use(a.redirectShortDomain)
r.Use(middleware.RedirectSlashes)
r.Use(middleware.CleanPath)
r.Use(middleware.GetHead)
if !a.cfg.Cache.Enable {
if cache := a.cfg.Cache; cache != nil && !cache.Enable {
r.Use(middleware.NoCache)
}
// No Index Header
if privateMode {
if a.isPrivate() {
r.Use(noIndexHeader)
}
@ -155,340 +148,60 @@ func (a *goBlog) buildRouter() (*chi.Mux, error) {
r.Use(a.checkIsLogin)
r.Use(a.checkIsCaptcha)
// Logout
r.With(a.authMiddleware).Get("/login", serveLogin)
r.With(a.authMiddleware).Get("/logout", a.serveLogout)
// Login
r.Group(a.loginRouter)
// Micropub
r.Route(micropubPath, func(r chi.Router) {
r.Use(a.checkIndieAuth)
r.Get("/", a.serveMicropubQuery)
r.Post("/", a.serveMicropubPost)
r.Post(micropubMediaSubPath, a.serveMicropubMedia)
})
r.Route(micropubPath, a.micropubRouter)
// IndieAuth
r.Route("/indieauth", func(r chi.Router) {
r.Get("/", a.indieAuthRequest)
r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept)
r.Post("/", a.indieAuthVerification)
r.Get("/token", a.indieAuthToken)
r.Post("/token", a.indieAuthToken)
})
r.Route("/indieauth", a.indieAuthRouter)
// ActivityPub and stuff
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
r.Route("/activitypub", func(r chi.Router) {
r.Post("/inbox/{blog}", a.apHandleInbox)
r.Post("/{blog}/inbox", a.apHandleInbox)
})
r.Group(func(r chi.Router) {
r.Use(cacheLoggedIn, a.cacheMiddleware)
r.Get("/.well-known/webfinger", a.apHandleWebfinger)
r.Get("/.well-known/host-meta", handleWellKnownHostMeta)
r.Get("/.well-known/nodeinfo", a.serveNodeInfoDiscover)
r.Get("/nodeinfo", a.serveNodeInfo)
})
}
r.Group(a.activityPubRouter)
// Webmentions
if wm := a.cfg.Webmention; wm != nil && !wm.DisableReceiving {
r.Route(webmentionPath, func(r chi.Router) {
r.Post("/", a.handleWebmention)
r.Group(func(r chi.Router) {
// Authenticated routes
r.Use(a.authMiddleware)
r.Get("/", a.webmentionAdmin)
r.Get(paginationPath, a.webmentionAdmin)
r.Post("/delete", a.webmentionAdminDelete)
r.Post("/approve", a.webmentionAdminApprove)
r.Post("/reverify", a.webmentionAdminReverify)
})
})
}
r.Route(webmentionPath, a.webmentionsRouter)
// Notifications
r.Route(notificationsPath, func(r chi.Router) {
r.Use(a.authMiddleware)
r.Get("/", a.notificationsAdmin)
r.Get(paginationPath, a.notificationsAdmin)
r.Post("/delete", a.notificationsAdminDelete)
})
r.Route(notificationsPath, a.notificationsRouter)
// Assets
for _, path := range a.allAssetPaths() {
r.Get(path, a.serveAsset)
}
r.Group(a.assetsRouter)
// Static files
for _, path := range allStaticPaths() {
r.With(privateModeHandler...).Get(path, a.serveStaticFile)
}
r.Group(a.staticFilesRouter)
// Media files
r.With(privateModeHandler...).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile)
r.With(a.privateModeHandler).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile)
// Captcha
r.Handle("/captcha/*", captcha.Server(500, 250))
// Short paths
r.With(privateModeHandler...).With(cacheLoggedIn, a.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath)
r.With(a.privateModeHandler, cacheLoggedIn, a.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath)
// Blogs
for blog, blogConfig := range a.cfg.Blogs {
sbm := middleware.WithValue(blogContextKey, blog)
// Sections
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(a.cacheMiddleware, sbm)
for _, section := range blogConfig.Sections {
if section.Name != "" {
r.Group(func(r chi.Router) {
secPath := blogConfig.getRelativePath(section.Name)
r.Use(middleware.WithValue(indexConfigKey, &indexConfig{
path: secPath,
section: section,
}))
r.Get(secPath, a.serveIndex)
r.Get(secPath+feedPath, a.serveIndex)
r.Get(secPath+paginationPath, a.serveIndex)
})
}
}
})
// Taxonomies
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(a.cacheMiddleware, sbm)
for _, taxonomy := range blogConfig.Taxonomies {
if taxonomy.Name != "" {
r.Group(func(r chi.Router) {
r.Use(middleware.WithValue(taxonomyContextKey, taxonomy))
taxBasePath := blogConfig.getRelativePath(taxonomy.Name)
r.Get(taxBasePath, a.serveTaxonomy)
taxValPath := taxBasePath + "/{taxValue}"
r.Get(taxValPath, a.serveTaxonomyValue)
r.Get(taxValPath+feedPath, a.serveTaxonomyValue)
r.Get(taxValPath+paginationPath, a.serveTaxonomyValue)
})
}
}
})
// Photos
if pc := blogConfig.Photos; pc != nil && pc.Enabled {
r.Group(func(r chi.Router) {
photoPath := blogConfig.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath))
r.Use(privateModeHandler...)
r.Use(a.cacheMiddleware, sbm, middleware.WithValue(indexConfigKey, &indexConfig{
path: photoPath,
parameter: pc.Parameter,
title: pc.Title,
description: pc.Description,
summaryTemplate: templatePhotosSummary,
}))
r.Get(photoPath, a.serveIndex)
r.Get(photoPath+feedPath, a.serveIndex)
r.Get(photoPath+paginationPath, a.serveIndex)
})
}
// Search
if bsc := blogConfig.Search; bsc != nil && bsc.Enabled {
searchPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath))
r.Route(searchPath, func(r chi.Router) {
r.Use(sbm, middleware.WithValue(
pathContextKey,
searchPath,
))
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(a.cacheMiddleware)
r.Get("/", a.serveSearch)
r.Post("/", a.serveSearch)
searchResultPath := "/" + searchPlaceholder
r.Get(searchResultPath, a.serveSearchResult)
r.Get(searchResultPath+feedPath, a.serveSearchResult)
r.Get(searchResultPath+paginationPath, a.serveSearchResult)
})
r.With(a.cacheMiddleware).Get("/opensearch.xml", a.serveOpenSearch)
})
}
// Stats
if bsc := blogConfig.BlogStats; bsc != nil && bsc.Enabled {
statsPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath))
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.With(a.cacheMiddleware, sbm).Get(statsPath, a.serveBlogStats)
r.With(cacheLoggedIn, a.cacheMiddleware, sbm).Get(statsPath+".table.html", a.serveBlogStatsTable)
})
}
// Date archives
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(a.cacheMiddleware, sbm)
yearRegex := `/{year:x|\d\d\d\d}`
monthRegex := `/{month:x|\d\d}`
dayRegex := `/{day:\d\d}`
yearPath := blogConfig.getRelativePath(yearRegex)
r.Get(yearPath, a.serveDate)
r.Get(yearPath+feedPath, a.serveDate)
r.Get(yearPath+paginationPath, a.serveDate)
monthPath := yearPath + monthRegex
r.Get(monthPath, a.serveDate)
r.Get(monthPath+feedPath, a.serveDate)
r.Get(monthPath+paginationPath, a.serveDate)
dayPath := monthPath + dayRegex
r.Get(dayPath, a.serveDate)
r.Get(dayPath+feedPath, a.serveDate)
r.Get(dayPath+paginationPath, a.serveDate)
})
// Blog
if !blogConfig.PostAsHome {
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(sbm)
r.With(a.checkActivityStreamsRequest, a.cacheMiddleware).Get(blogConfig.getRelativePath(""), a.serveHome)
r.With(a.cacheMiddleware).Get(blogConfig.getRelativePath("")+feedPath, a.serveHome)
r.With(a.cacheMiddleware).Get(blogConfig.getRelativePath(paginationPath), a.serveHome)
})
}
// Custom pages
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(sbm)
for _, cp := range blogConfig.CustomPages {
r.Group(func(r chi.Router) {
scp := middleware.WithValue(customPageContextKey, cp)
if cp.Cache {
ce := cp.CacheExpiration
if ce == 0 {
ce = a.defaultCacheExpiration()
}
r.With(
a.cacheMiddleware,
middleware.WithValue(cacheExpirationKey, ce),
scp,
).Get(cp.Path, a.serveCustomPage)
} else {
r.With(scp).Get(cp.Path, a.serveCustomPage)
}
})
}
})
// Random post
if rp := blogConfig.RandomPost; rp != nil && rp.Enabled {
r.With(privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost)
}
// Editor
r.Route(blogConfig.getRelativePath("/editor"), func(r chi.Router) {
r.Use(sbm, a.authMiddleware)
r.Get("/", a.serveEditor)
r.Post("/", a.serveEditorPost)
r.Get("/files", a.serveEditorFiles)
r.Post("/files/view", a.serveEditorFilesView)
r.Post("/files/delete", a.serveEditorFilesDelete)
r.Get("/drafts", a.serveDrafts)
r.Get("/drafts"+feedPath, a.serveDrafts)
r.Get("/drafts"+paginationPath, a.serveDrafts)
r.Get("/private", a.servePrivate)
r.Get("/private"+feedPath, a.servePrivate)
r.Get("/private"+paginationPath, a.servePrivate)
r.Get("/unlisted", a.serveUnlisted)
r.Get("/unlisted"+feedPath, a.serveUnlisted)
r.Get("/unlisted"+paginationPath, a.serveUnlisted)
})
// Comments
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
commentsPath := blogConfig.getRelativePath("/comment")
r.Route(commentsPath, func(r chi.Router) {
r.Use(sbm, middleware.WithValue(pathContextKey, commentsPath))
r.Use(privateModeHandler...)
r.With(a.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment)
r.With(a.captchaMiddleware).Post("/", a.createComment)
r.Group(func(r chi.Router) {
// Admin
r.Use(a.authMiddleware)
r.Get("/", a.commentsAdmin)
r.Get(paginationPath, a.commentsAdmin)
r.Post("/delete", a.commentsAdminDelete)
})
})
}
// Blogroll
if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled {
brPath := blogConfig.getRelativePath(defaultIfEmpty(brConfig.Path, defaultBlogrollPath))
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(middleware.WithValue(cacheExpirationKey, a.defaultCacheExpiration()))
r.Use(a.cacheMiddleware, sbm)
r.Get(brPath, a.serveBlogroll)
r.Get(brPath+".opml", a.serveBlogrollExport)
})
}
// Geo map
if mc := blogConfig.Map; mc != nil && mc.Enabled {
mapPath := blogConfig.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath))
r.Route(mapPath, func(r chi.Router) {
r.Use(privateModeHandler...)
r.Group(func(r chi.Router) {
r.With(a.cacheMiddleware, sbm).Get("/", a.serveGeoMap)
r.With(cacheLoggedIn, a.cacheMiddleware).HandleFunc("/leaflet/*", a.serveLeaflet(mapPath+"/"))
})
r.Get("/tiles/{z}/{x}/{y}.png", a.proxyTiles(mapPath+"/tiles"))
})
}
// Contact
if cc := blogConfig.Contact; cc != nil && cc.Enabled {
contactPath := blogConfig.getRelativePath(defaultIfEmpty(cc.Path, defaultContactPath))
r.Route(contactPath, func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(a.cacheMiddleware, sbm)
r.Get("/", a.serveContactForm)
r.With(a.captchaMiddleware).Post("/", a.sendContactSubmission)
})
}
r.Group(a.blogRouter(blog, blogConfig))
}
// Sitemap
r.With(privateModeHandler...).With(cacheLoggedIn, a.cacheMiddleware).Get(sitemapPath, a.serveSitemap)
r.With(a.privateModeHandler, cacheLoggedIn, a.cacheMiddleware).Get(sitemapPath, a.serveSitemap)
// Robots.txt - doesn't need cache, because it's too simple
if !privateMode {
r.Get("/robots.txt", a.serveRobotsTXT)
} else {
r.Get("/robots.txt", servePrivateRobotsTXT)
}
// Robots.txt
r.With(cacheLoggedIn, a.cacheMiddleware).Get(robotsTXTPath, a.serveRobotsTXT)
r.NotFound(a.servePostsAliasesRedirects(privateModeHandler...))
r.NotFound(a.servePostsAliasesRedirects())
r.MethodNotAllowed(a.serveNotAllowed)
return r, nil
}
func (a *goBlog) servePostsAliasesRedirects(pmh ...func(http.Handler) http.Handler) http.HandlerFunc {
func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
// Private mode
alicePrivate := alice.New()
for _, h := range pmh {
alicePrivate = alicePrivate.Append(h)
}
alicePrivate := alice.New(a.privateModeHandler)
// Return handler func
return func(w http.ResponseWriter, r *http.Request) {
// Only allow GET requests
@ -550,53 +263,3 @@ func (a *goBlog) servePostsAliasesRedirects(pmh ...func(http.Handler) http.Handl
alice.New(a.cacheMiddleware, a.checkRegexRedirects).ThenFunc(a.serve404).ServeHTTP(w, r)
}
}
const blogContextKey contextKey = "blog"
const pathContextKey contextKey = "httpPath"
func (a *goBlog) refreshCSPDomains() {
var cspBuilder strings.Builder
if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" {
if u, err := url.Parse(mp.MediaURL); err == nil {
cspBuilder.WriteByte(' ')
cspBuilder.WriteString(u.Hostname())
}
}
if len(a.cfg.Server.CSPDomains) > 0 {
cspBuilder.WriteByte(' ')
cspBuilder.WriteString(strings.Join(a.cfg.Server.CSPDomains, " "))
}
a.cspDomains = cspBuilder.String()
}
const cspHeader = "Content-Security-Policy"
func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
a.refreshCSPDomains()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Strict-Transport-Security", "max-age=31536000;")
w.Header().Set("Referrer-Policy", "no-referrer")
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(cspHeader, "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))
}
next.ServeHTTP(w, r)
})
}
func noIndexHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Robots-Tag", "noindex")
next.ServeHTTP(w, r)
})
}
func fixHTTPHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.RawPath = ""
next.ServeHTTP(w, r)
})
}

51
httpMiddlewares.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"net/http"
"net/url"
"strings"
)
func noIndexHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Robots-Tag", "noindex")
next.ServeHTTP(w, r)
})
}
func fixHTTPHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.RawPath = ""
next.ServeHTTP(w, r)
})
}
func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
// Build CSP domains list
var cspBuilder strings.Builder
if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" {
if u, err := url.Parse(mp.MediaURL); err == nil {
cspBuilder.WriteByte(' ')
cspBuilder.WriteString(u.Hostname())
}
}
if len(a.cfg.Server.CSPDomains) > 0 {
cspBuilder.WriteByte(' ')
cspBuilder.WriteString(strings.Join(a.cfg.Server.CSPDomains, " "))
}
cspDomains := cspBuilder.String()
// Return handler
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Strict-Transport-Security", "max-age=31536000;")
w.Header().Set("Referrer-Policy", "no-referrer")
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)
if a.cfg.Server.Tor && a.torAddress != "" {
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
}
next.ServeHTTP(w, r)
})
}

414
httpRouters.go Normal file
View File

@ -0,0 +1,414 @@
package main
import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
// Login
func (a *goBlog) loginRouter(r chi.Router) {
r.Use(a.authMiddleware)
r.Get("/login", serveLogin)
r.Get("/logout", a.serveLogout)
}
// Micropub
func (a *goBlog) micropubRouter(r chi.Router) {
r.Use(a.checkIndieAuth)
r.Get("/", a.serveMicropubQuery)
r.Post("/", a.serveMicropubPost)
r.Post(micropubMediaSubPath, a.serveMicropubMedia)
}
// IndieAuth
func (a *goBlog) indieAuthRouter(r chi.Router) {
r.Get("/", a.indieAuthRequest)
r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept)
r.Post("/", a.indieAuthVerification)
r.Get("/token", a.indieAuthToken)
r.Post("/token", a.indieAuthToken)
}
// ActivityPub
func (a *goBlog) activityPubRouter(r chi.Router) {
if a.isPrivate() {
// Private mode, no ActivityPub
return
}
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
r.Route("/activitypub", func(r chi.Router) {
r.Post("/inbox/{blog}", a.apHandleInbox)
r.Post("/{blog}/inbox", a.apHandleInbox)
})
r.Group(func(r chi.Router) {
r.Use(cacheLoggedIn, a.cacheMiddleware)
r.Get("/.well-known/webfinger", a.apHandleWebfinger)
r.Get("/.well-known/host-meta", handleWellKnownHostMeta)
r.Get("/.well-known/nodeinfo", a.serveNodeInfoDiscover)
r.Get("/nodeinfo", a.serveNodeInfo)
})
}
}
// Webmentions
func (a *goBlog) webmentionsRouter(r chi.Router) {
if wm := a.cfg.Webmention; wm != nil && !wm.DisableReceiving {
// Endpoint
r.Post("/", a.handleWebmention)
// Authenticated routes
r.Group(func(r chi.Router) {
r.Use(a.authMiddleware)
r.Get("/", a.webmentionAdmin)
r.Get(paginationPath, a.webmentionAdmin)
r.Post("/delete", a.webmentionAdminDelete)
r.Post("/approve", a.webmentionAdminApprove)
r.Post("/reverify", a.webmentionAdminReverify)
})
}
}
// Notifications
func (a *goBlog) notificationsRouter(r chi.Router) {
r.Use(a.authMiddleware)
r.Get("/", a.notificationsAdmin)
r.Get(paginationPath, a.notificationsAdmin)
r.Post("/delete", a.notificationsAdminDelete)
}
// Assets
func (a *goBlog) assetsRouter(r chi.Router) {
for _, path := range a.allAssetPaths() {
r.Get(path, a.serveAsset)
}
}
// Static files
func (a *goBlog) staticFilesRouter(r chi.Router) {
r.Use(a.privateModeHandler)
for _, path := range allStaticPaths() {
r.Get(path, a.serveStaticFile)
}
}
// Blog
func (a *goBlog) blogRouter(blog string, conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
// Set blog
r.Use(middleware.WithValue(blogKey, blog))
// Home
r.Group(a.blogHomeRouter(conf))
// Sections
r.Group(a.blogSectionsRouter(conf))
// Taxonomies
r.Group(a.blogTaxonomiesRouter(conf))
// Dates
r.Group(a.blogDatesRouter(conf))
// Photos
r.Group(a.blogPhotosRouter(conf))
// Search
r.Group(a.blogSearchRouter(conf))
// Custom pages
r.Group(a.blogCustomPagesRouter(conf))
// Random post
r.Group(a.blogRandomRouter(conf))
// Editor
r.Route(conf.getRelativePath(editorPath), a.blogEditorRouter(conf))
// Comments
r.Group(a.blogCommentsRouter(conf))
// Stats
r.Group(a.blogStatsRouter(conf))
// Blogroll
r.Group(a.blogBlogrollRouter(conf))
// Geo map
r.Group(a.blogGeoMapRouter(conf))
// Contact
r.Group(a.blogContactRouter(conf))
}
}
// Blog - Home
func (a *goBlog) blogHomeRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if !conf.PostAsHome {
r.Use(a.privateModeHandler)
r.With(a.checkActivityStreamsRequest, a.cacheMiddleware).Get(conf.getRelativePath(""), a.serveHome)
r.With(a.cacheMiddleware).Get(conf.getRelativePath("")+feedPath, a.serveHome)
r.With(a.cacheMiddleware).Get(conf.getRelativePath(paginationPath), a.serveHome)
}
}
}
// Blog - Sections
func (a *goBlog) blogSectionsRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
r.Use(
a.privateModeHandler,
a.cacheMiddleware,
)
for _, section := range conf.Sections {
if section.Name != "" {
r.Group(func(r chi.Router) {
secPath := conf.getRelativePath(section.Name)
r.Use(middleware.WithValue(indexConfigKey, &indexConfig{
path: secPath,
section: section,
}))
r.Get(secPath, a.serveIndex)
r.Get(secPath+feedPath, a.serveIndex)
r.Get(secPath+paginationPath, a.serveIndex)
})
}
}
}
}
// Blog - Taxonomies
func (a *goBlog) blogTaxonomiesRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
r.Use(
a.privateModeHandler,
a.cacheMiddleware,
)
for _, taxonomy := range conf.Taxonomies {
if taxonomy.Name != "" {
r.Group(func(r chi.Router) {
r.Use(middleware.WithValue(taxonomyContextKey, taxonomy))
taxBasePath := conf.getRelativePath(taxonomy.Name)
r.Get(taxBasePath, a.serveTaxonomy)
taxValPath := taxBasePath + "/{taxValue}"
r.Get(taxValPath, a.serveTaxonomyValue)
r.Get(taxValPath+feedPath, a.serveTaxonomyValue)
r.Get(taxValPath+paginationPath, a.serveTaxonomyValue)
})
}
}
}
}
// Blog - Dates
func (a *goBlog) blogDatesRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
r.Use(
a.privateModeHandler,
a.cacheMiddleware,
)
yearPath := conf.getRelativePath(`/{year:x|\d\d\d\d}`)
r.Get(yearPath, a.serveDate)
r.Get(yearPath+feedPath, a.serveDate)
r.Get(yearPath+paginationPath, a.serveDate)
monthPath := yearPath + `/{month:x|\d\d}`
r.Get(monthPath, a.serveDate)
r.Get(monthPath+feedPath, a.serveDate)
r.Get(monthPath+paginationPath, a.serveDate)
dayPath := monthPath + `/{day:\d\d}`
r.Get(dayPath, a.serveDate)
r.Get(dayPath+feedPath, a.serveDate)
r.Get(dayPath+paginationPath, a.serveDate)
}
}
// Blog - Photos
func (a *goBlog) blogPhotosRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if pc := conf.Photos; pc != nil && pc.Enabled {
photoPath := conf.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath))
r.Use(
a.privateModeHandler,
a.cacheMiddleware,
middleware.WithValue(indexConfigKey, &indexConfig{
path: photoPath,
parameter: pc.Parameter,
title: pc.Title,
description: pc.Description,
summaryTemplate: templatePhotosSummary,
}),
)
r.Get(photoPath, a.serveIndex)
r.Get(photoPath+feedPath, a.serveIndex)
r.Get(photoPath+paginationPath, a.serveIndex)
}
}
}
// Blog - Search
func (a *goBlog) blogSearchRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if bsc := conf.Search; bsc != nil && bsc.Enabled {
searchPath := conf.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath))
r.Route(searchPath, func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(
a.privateModeHandler,
a.cacheMiddleware,
middleware.WithValue(pathKey, searchPath),
)
r.Get("/", a.serveSearch)
r.Post("/", a.serveSearch)
searchResultPath := "/" + searchPlaceholder
r.Get(searchResultPath, a.serveSearchResult)
r.Get(searchResultPath+feedPath, a.serveSearchResult)
r.Get(searchResultPath+paginationPath, a.serveSearchResult)
})
r.With(
// No private mode, to allow using OpenSearch in browser
a.cacheMiddleware,
middleware.WithValue(pathKey, searchPath),
).Get("/opensearch.xml", a.serveOpenSearch)
})
}
}
}
// Blog - Custom pages
func (a *goBlog) blogCustomPagesRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
r.Use(a.privateModeHandler)
for _, cp := range conf.CustomPages {
r.Group(func(r chi.Router) {
r.Use(middleware.WithValue(customPageContextKey, cp))
if cp.Cache {
ce := cp.CacheExpiration
if ce == 0 {
ce = a.defaultCacheExpiration()
}
r.Use(
a.cacheMiddleware,
middleware.WithValue(cacheExpirationKey, ce),
)
}
r.Get(cp.Path, a.serveCustomPage)
})
}
}
}
// Blog - Random
func (a *goBlog) blogRandomRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if rp := conf.RandomPost; rp != nil && rp.Enabled {
r.With(a.privateModeHandler).Get(conf.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost)
}
}
}
// Blog - Editor
func (a *goBlog) blogEditorRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
r.Use(a.authMiddleware)
r.Get("/", a.serveEditor)
r.Post("/", a.serveEditorPost)
r.Get("/files", a.serveEditorFiles)
r.Post("/files/view", a.serveEditorFilesView)
r.Post("/files/delete", a.serveEditorFilesDelete)
r.Get("/drafts", a.serveDrafts)
r.Get("/drafts"+feedPath, a.serveDrafts)
r.Get("/drafts"+paginationPath, a.serveDrafts)
r.Get("/private", a.servePrivate)
r.Get("/private"+feedPath, a.servePrivate)
r.Get("/private"+paginationPath, a.servePrivate)
r.Get("/unlisted", a.serveUnlisted)
r.Get("/unlisted"+feedPath, a.serveUnlisted)
r.Get("/unlisted"+paginationPath, a.serveUnlisted)
}
}
// Blog - Comments
func (a *goBlog) blogCommentsRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if commentsConfig := conf.Comments; commentsConfig != nil && commentsConfig.Enabled {
commentsPath := conf.getRelativePath("/comment")
r.Route(commentsPath, func(r chi.Router) {
r.Use(
a.privateModeHandler,
middleware.WithValue(pathKey, commentsPath),
)
r.With(a.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment)
r.With(a.captchaMiddleware).Post("/", a.createComment)
r.Group(func(r chi.Router) {
// Admin
r.Use(a.authMiddleware)
r.Get("/", a.commentsAdmin)
r.Get(paginationPath, a.commentsAdmin)
r.Post("/delete", a.commentsAdminDelete)
})
})
}
}
}
// Blog - Stats
func (a *goBlog) blogStatsRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if bsc := conf.BlogStats; bsc != nil && bsc.Enabled {
statsPath := conf.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath))
r.Use(a.privateModeHandler)
r.With(a.cacheMiddleware).Get(statsPath, a.serveBlogStats)
r.With(cacheLoggedIn, a.cacheMiddleware).Get(statsPath+blogStatsTablePath, a.serveBlogStatsTable)
}
}
}
// Blog - Blogroll
func (a *goBlog) blogBlogrollRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if brConfig := conf.Blogroll; brConfig != nil && brConfig.Enabled {
brPath := conf.getRelativePath(defaultIfEmpty(brConfig.Path, defaultBlogrollPath))
r.Use(
a.privateModeHandler,
middleware.WithValue(cacheExpirationKey, a.defaultCacheExpiration()),
a.cacheMiddleware,
)
r.Get(brPath, a.serveBlogroll)
r.Get(brPath+".opml", a.serveBlogrollExport)
}
}
}
// Blog - Geo Map
func (a *goBlog) blogGeoMapRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if mc := conf.Map; mc != nil && mc.Enabled {
mapPath := conf.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath))
r.Route(mapPath, func(r chi.Router) {
r.Use(a.privateModeHandler)
r.Group(func(r chi.Router) {
r.With(a.cacheMiddleware).Get("/", a.serveGeoMap)
r.With(cacheLoggedIn, a.cacheMiddleware).HandleFunc("/leaflet/*", a.serveLeaflet(mapPath+"/"))
})
r.Get("/tiles/{z}/{x}/{y}.png", a.proxyTiles(mapPath+"/tiles"))
})
}
}
}
// Blog - Contact
func (a *goBlog) blogContactRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
if cc := conf.Contact; cc != nil && cc.Enabled {
contactPath := conf.getRelativePath(defaultIfEmpty(cc.Path, defaultContactPath))
r.Route(contactPath, func(r chi.Router) {
r.Use(a.privateModeHandler, a.cacheMiddleware)
r.Get("/", a.serveContactForm)
r.With(a.captchaMiddleware).Post("/", a.sendContactSubmission)
})
}
}
}

View File

@ -57,7 +57,7 @@ func (a *goBlog) serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
return
}
// Try to compress file (only when not in private mode)
if pm := a.cfg.PrivateMode; pm == nil || !pm.Enabled {
if !a.isPrivate() {
compressedLocation, compressionErr := a.compressMediaFile(location)
if compressionErr != nil {
a.serveError(w, r, "failed to compress file: "+compressionErr.Error(), http.StatusInternalServerError)

View File

@ -8,7 +8,7 @@ import (
)
func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
b := a.cfg.Blogs[blog]
title := b.Title
sURL := a.getFullAddress(b.getRelativePath(defaultIfEmpty(b.Search.Path, defaultSearchPath)))

View File

@ -78,7 +78,7 @@ func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
}
func (a *goBlog) redirectToRandomPost(rw http.ResponseWriter, r *http.Request) {
randomPath, err := a.getRandomPostPath(r.Context().Value(blogContextKey).(string))
randomPath, err := a.getRandomPostPath(r.Context().Value(blogKey).(string))
if err != nil {
a.serveError(rw, r, err.Error(), http.StatusInternalServerError)
return
@ -111,7 +111,7 @@ func (p *postPaginationAdapter) Slice(offset, length int, data interface{}) erro
}
func (a *goBlog) serveHome(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
a.serveActivityStreams(blog, w, r)
return
@ -122,7 +122,7 @@ func (a *goBlog) serveHome(w http.ResponseWriter, r *http.Request) {
}
func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: a.getRelativePath(blog, "/editor/drafts"),
title: a.ts.GetTemplateStringVariant(a.cfg.Blogs[blog].Lang, "drafts"),
@ -131,7 +131,7 @@ func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) {
}
func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: a.getRelativePath(blog, "/editor/private"),
title: a.ts.GetTemplateStringVariant(a.cfg.Blogs[blog].Lang, "privateposts"),
@ -140,7 +140,7 @@ func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) {
}
func (a *goBlog) serveUnlisted(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: a.getRelativePath(blog, "/editor/unlisted"),
title: a.ts.GetTemplateStringVariant(a.cfg.Blogs[blog].Lang, "unlistedposts"),
@ -184,7 +184,7 @@ func (a *goBlog) serveDate(w http.ResponseWriter, r *http.Request) {
dPath.WriteString(fmt.Sprintf("/%02d", day))
}
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: a.getRelativePath(r.Context().Value(blogContextKey).(string), dPath.String()),
path: a.getRelativePath(r.Context().Value(blogKey).(string), dPath.String()),
year: year,
month: month,
day: day,
@ -214,7 +214,7 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
ic := r.Context().Value(indexConfigKey).(*indexConfig)
blog := ic.blog
if blog == "" {
blog, _ = r.Context().Value(blogContextKey).(string)
blog, _ = r.Context().Value(blogKey).(string)
}
search := chi.URLParam(r, "search")
if search != "" {

View File

@ -79,7 +79,7 @@ func (a *goBlog) checkPost(p *post) (err error) {
random := generateRandomString(5)
p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random)
}
published, _ := dateparse.ParseLocal(p.Published)
published := timeNoErr(dateparse.ParseLocal(p.Published))
pathTmplString := a.cfg.Blogs[p.Blog].Sections[p.Section].PathTemplate
if pathTmplString == "" {
return errors.New("path template empty")

24
privateMode.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"net/http"
"github.com/justinas/alice"
)
func (a *goBlog) isPrivate() bool {
if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled {
return true
}
return false
}
func (a *goBlog) privateModeHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if a.isPrivate() {
alice.New(a.authMiddleware).Then(next).ServeHTTP(w, r)
} else {
next.ServeHTTP(w, r)
}
})
}

View File

@ -5,10 +5,12 @@ import (
"net/http"
)
const robotsTXTPath = "/robots.txt"
func (a *goBlog) serveRobotsTXT(w http.ResponseWriter, r *http.Request) {
if a.isPrivate() {
_, _ = w.Write([]byte("User-agent: *\nDisallow: /"))
return
}
_, _ = w.Write([]byte(fmt.Sprintf("User-agent: *\nSitemap: %v", a.getFullAddress(sitemapPath))))
}
func servePrivateRobotsTXT(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("User-agent: *\nDisallow: /"))
}

View File

@ -9,11 +9,6 @@ import (
func Test_robotsTXT(t *testing.T) {
h := http.HandlerFunc(servePrivateRobotsTXT)
assert.HTTPStatusCode(t, h, http.MethodGet, "", nil, 200)
txt := assert.HTTPBody(h, http.MethodGet, "", nil)
assert.Equal(t, "User-agent: *\nDisallow: /", txt)
app := &goBlog{
cfg: &config{
Server: &configServer{
@ -22,9 +17,18 @@ func Test_robotsTXT(t *testing.T) {
},
}
h := http.HandlerFunc(app.serveRobotsTXT)
assert.HTTPStatusCode(t, h, http.MethodGet, "", nil, 200)
txt := assert.HTTPBody(h, http.MethodGet, "", nil)
assert.Equal(t, "User-agent: *\nSitemap: https://example.com/sitemap.xml", txt)
app.cfg.PrivateMode = &configPrivateMode{
Enabled: true,
}
h = http.HandlerFunc(app.serveRobotsTXT)
assert.HTTPStatusCode(t, h, http.MethodGet, "", nil, 200)
txt = assert.HTTPBody(h, http.MethodGet, "", nil)
assert.Equal(t, "User-agent: *\nSitemap: https://example.com/sitemap.xml", txt)
assert.Equal(t, "User-agent: *\nDisallow: /", txt)
}

View File

@ -15,8 +15,8 @@ const defaultSearchPath = "/search"
const searchPlaceholder = "{search}"
func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
servePath := r.Context().Value(pathContextKey).(string)
blog := r.Context().Value(blogKey).(string)
servePath := r.Context().Value(pathKey).(string)
err := r.ParseForm()
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
@ -37,7 +37,7 @@ func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) serveSearchResult(w http.ResponseWriter, r *http.Request) {
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: r.Context().Value(pathContextKey).(string) + "/" + searchPlaceholder,
path: r.Context().Value(pathKey).(string) + "/" + searchPlaceholder,
})))
}

View File

@ -114,15 +114,12 @@ func (s *dbSessionStore) load(session *sessions.Session) (err error) {
if err = row.Scan(&data, &createdStr, &modifiedStr, &expiresStr); err != nil {
return err
}
created, _ := dateparse.ParseLocal(createdStr)
modified, _ := dateparse.ParseLocal(modifiedStr)
expires, _ := dateparse.ParseLocal(expiresStr)
if err = securecookie.DecodeMulti(session.Name(), data, &session.Values, s.codecs...); err != nil {
return err
}
session.Values[sessionCreatedOn] = created
session.Values[sessionModifiedOn] = modified
session.Values[sessionExpiresOn] = expires
session.Values[sessionCreatedOn] = timeNoErr(dateparse.ParseLocal(createdStr))
session.Values[sessionModifiedOn] = timeNoErr(dateparse.ParseLocal(modifiedStr))
session.Values[sessionExpiresOn] = timeNoErr(dateparse.ParseLocal(expiresStr))
return nil
}

View File

@ -143,16 +143,16 @@ func (a *goBlog) serveSitemap(w http.ResponseWriter, r *http.Request) {
})
}
}
// Posts
// Published posts
if posts, err := a.db.getPosts(&postsRequestConfig{status: statusPublished, withoutParameters: true}); err == nil {
for _, p := range posts {
item := &sitemap.URL{Loc: a.fullPostURL(p)}
var lastMod time.Time
if p.Updated != "" {
lastMod, _ = dateparse.ParseLocal(p.Updated)
lastMod = timeNoErr(dateparse.ParseLocal(p.Updated))
}
if p.Published != "" && lastMod.IsZero() {
lastMod, _ = dateparse.ParseLocal(p.Published)
lastMod = timeNoErr(dateparse.ParseLocal(p.Published))
}
if !lastMod.IsZero() {
item.LastMod = &lastMod

View File

@ -13,7 +13,7 @@ import (
const taxonomyContextKey = "taxonomy"
func (a *goBlog) serveTaxonomy(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
tax := r.Context().Value(taxonomyContextKey).(*configTaxonomy)
allValues, err := a.db.allTaxonomyValues(blog, tax.Name)
if err != nil {
@ -31,7 +31,7 @@ func (a *goBlog) serveTaxonomy(w http.ResponseWriter, r *http.Request) {
}
func (a *goBlog) serveTaxonomyValue(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
blog := r.Context().Value(blogKey).(string)
tax := r.Context().Value(taxonomyContextKey).(*configTaxonomy)
taxValueParam := chi.URLParam(r, "taxValue")
if taxValueParam == "" {

View File

@ -3,7 +3,7 @@
{{ $blog := .Blog }}
{{ range $i, $tax := $blog.Taxonomies }}
{{ $tvs := sort (ps $post $tax.Name) }}
{{ if gt (len $tvs) 0 }}
{{ if $tvs }}
<p><b>{{ $tax.Title }}</b>:
{{ range $j, $tv := $tvs }}
<a class="p-category" rel="tag" href="{{ $blog.RelativePath ( printf "/%s/%s" $tax.Name (urlize $tv) ) }}">{{ $tv }}</a>

View File

@ -256,3 +256,7 @@ func containsStrings(s string, subStrings ...string) bool {
}
return false
}
func timeNoErr(t time.Time, _ error) time.Time {
return t
}

View File

@ -49,7 +49,7 @@ func (a *goBlog) sendWebmentions(p *post) error {
continue
}
// External mention
if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled {
if a.isPrivate() {
// Private mode, don't send external mentions
continue
}