From 95909420ba0efb4d307152aace38677cfb1e1da3 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Tue, 6 Oct 2020 19:07:48 +0200 Subject: [PATCH] A lot of progress --- .gitignore | 3 +- activitystreams.go | 14 +- api.go | 6 +- cache.go | 4 +- config.go | 112 ++++++++------ databaseMigrations.go | 36 +++++ database_migrations.go | 85 ----------- errors.go | 2 + example-config.yaml | 79 ++++++---- feeds.go | 35 ++--- go.mod | 20 +-- go.sum | 45 +++--- http.go | 109 ++++++++------ hugo.go | 7 +- indieauth.go | 92 ++++++++++++ micropub.go | 271 ++++++++++++++++++++++++++++++++++ posts.go | 65 +++++--- postsDb.go | 83 +++++++++-- redirects.go | 2 + render.go | 19 ++- templates/base.gohtml | 5 +- templates/error.gohtml | 2 +- templates/header.gohtml | 5 +- templates/index.gohtml | 4 +- templates/menu.gohtml | 2 +- templates/micropub.gohtml | 14 ++ templates/photos.gohtml | 14 +- templates/photosummary.gohtml | 2 +- templates/post.gohtml | 4 +- templates/redirect.gohtml | 2 +- templates/taxonomy.gohtml | 2 +- utils.go | 14 +- 32 files changed, 825 insertions(+), 334 deletions(-) create mode 100644 databaseMigrations.go delete mode 100644 database_migrations.go create mode 100644 indieauth.go create mode 100644 micropub.go create mode 100644 templates/micropub.gohtml diff --git a/.gitignore b/.gitignore index a919301..5f09b60 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /config.yaml /data /GoBlog -/tmp_assets \ No newline at end of file +/tmp_assets +/certmagic \ No newline at end of file diff --git a/activitystreams.go b/activitystreams.go index 65031c5..21652a5 100644 --- a/activitystreams.go +++ b/activitystreams.go @@ -40,6 +40,8 @@ type asAttachment struct { URL string `json:"url"` } +// TODO: Serve index + func servePostActivityStreams(w http.ResponseWriter, r *http.Request) { // Remove ".as" from path again r.URL.Path = strings.TrimSuffix(r.URL.Path, ".as") @@ -69,14 +71,14 @@ func servePostActivityStreams(w http.ResponseWriter, r *http.Request) { as.Type = "Note" } // Content - if rendered, err := renderMarkdown(post.Content); err != nil { + if rendered, err := renderMarkdown(post.Content); err == nil { + as.Content = string(rendered) + } else { http.Error(w, err.Error(), http.StatusInternalServerError) return - } else { - as.Content = string(rendered) } // Attachments - if images := post.Parameters[appConfig.Blog.ActivityStreams.ImagesParameter]; len(images) > 0 { + if images := post.Parameters[appConfig.Blogs[post.Blog].ActivityStreams.ImagesParameter]; len(images) > 0 { for _, image := range images { as.Attachment = append(as.Attachment, &asAttachment{ Type: "Image", @@ -97,10 +99,10 @@ func servePostActivityStreams(w http.ResponseWriter, r *http.Request) { } } // Reply - if replyLink := post.firstParameter(appConfig.Blog.ActivityStreams.ReplyParameter); replyLink != "" { + if replyLink := post.firstParameter(appConfig.Blogs[post.Blog].ActivityStreams.ReplyParameter); replyLink != "" { as.InReplyTo = replyLink } // Send JSON - w.Header().Add("Content-Type", contentTypeJSON) + w.Header().Add(contentType, contentTypeJSONUTF8) _ = json.NewEncoder(w).Encode(as) } diff --git a/api.go b/api.go index c1e67f8..4fef4b4 100644 --- a/api.go +++ b/api.go @@ -16,7 +16,7 @@ func apiPostCreate(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - err = post.createOrReplace() + err = post.createOrReplace(false) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -44,7 +44,7 @@ func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - err = post.createOrReplace() + err = post.createOrReplace(false) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -64,7 +64,7 @@ func apiPostRead(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - w.Header().Set("Content-Type", contentTypeJSON) + w.Header().Set(contentType, contentTypeJSONUTF8) err = json.NewEncoder(w).Encode(post) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cache.go b/cache.go index a3e4b92..26ca894 100644 --- a/cache.go +++ b/cache.go @@ -108,13 +108,13 @@ func saveCache(path string, now time.Time, header map[string][]string, body []by _ = tx.Commit() } -func purgeCache(path string) { +func purgeCache() { startWritingToDb() defer finishWritingToDb() tx, err := appDb.Begin() if err != nil { return } - _, _ = tx.Exec("delete from cache where path=?", path) + _, _ = tx.Exec("delete from cache; vacuum;") _ = tx.Commit() } diff --git a/config.go b/config.go index a72ded6..90f29ed 100644 --- a/config.go +++ b/config.go @@ -1,17 +1,21 @@ package main import ( + "errors" + "github.com/spf13/viper" ) type config struct { - Server *configServer `mapstructure:"server"` - Db *configDb `mapstructure:"database"` - Cache *configCache `mapstructure:"cache"` - Blog *configBlog `mapstructure:"blog"` - User *configUser `mapstructure:"user"` - Hooks *configHooks `mapstructure:"hooks"` - Hugo *configHugo `mapstructure:"hugo"` + Server *configServer `mapstructure:"server"` + Db *configDb `mapstructure:"database"` + Cache *configCache `mapstructure:"cache"` + DefaultBlog string `mapstructure:"defaultblog"` + Blogs map[string]*configBlog `mapstructure:"blogs"` + User *configUser `mapstructure:"user"` + Hooks *configHooks `mapstructure:"hooks"` + Hugo *configHugo `mapstructure:"hugo"` + Micropub *configMicropub `mapstructure:"micropub"` } type configServer struct { @@ -19,9 +23,9 @@ type configServer struct { Port int `mapstructure:"port"` Domain string `mapstructure:"domain"` PublicAddress string `mapstructure:"publicAddress"` - PublicHttps bool `mapstructure:"publicHttps"` + PublicHTTPS bool `mapstructure:"publicHttps"` LetsEncryptMail string `mapstructure:"letsEncryptMail"` - LocalHttps bool `mapstructure:"localHttps"` + LocalHTTPS bool `mapstructure:"localHttps"` } type configDb struct { @@ -33,32 +37,25 @@ type configCache struct { Expiration int64 `mapstructure:"expiration"` } -// exposed to templates via function "blog" type configBlog struct { - // Language of the blog, e.g. "en" or "de" - Lang string `mapstructure:"lang"` - // Title of the blog, e.g. "My blog" - Title string `mapstructure:"title"` - // Description of the blog - Description string `mapstructure:"description"` - // Number of posts per page - Pagination int `mapstructure:"pagination"` - // Sections - Sections []*section `mapstructure:"sections"` - // Taxonomies - Taxonomies []*taxonomy `mapstructure:"taxonomies"` - // Menus - Menus map[string]*menu `mapstructure:"menus"` - // Photos - Photos *photos `mapstructure:"photos"` - // ActivityStreams - ActivityStreams *activityStreams `mapstructure:"activitystreams"` + Path string `mapstructure:"path"` + Lang string `mapstructure:"lang"` + Title string `mapstructure:"title"` + Description string `mapstructure:"description"` + Pagination int `mapstructure:"pagination"` + Sections map[string]*section `mapstructure:"sections"` + Taxonomies []*taxonomy `mapstructure:"taxonomies"` + Menus map[string]*menu `mapstructure:"menus"` + Photos *photos `mapstructure:"photos"` + ActivityStreams *activityStreams `mapstructure:"activitystreams"` + DefaultSection string `mapstructure:"defaultsection"` } type section struct { - Name string `mapstructure:"name"` - Title string `mapstructure:"title"` - Description string `mapstructure:"description"` + Name string `mapstructure:"name"` + Title string `mapstructure:"title"` + Description string `mapstructure:"description"` + PathTemplate string `mapstructure:"pathtemplate"` } type taxonomy struct { @@ -110,6 +107,22 @@ type frontmatter struct { Parameter string `mapstructure:"parameter"` } +type configMicropub struct { + Enabled bool `mapstructure:"enabled"` + Path string `mapstructure:"path"` + AuthAllowed []string `mapstructure:"authAllowed"` + TokenEndpoint string `mapstructure:"tokenEndpoint"` + AuthEndpoint string `mapstructure:"authEndpoint"` + Authn string `mapstructure:"authn"` + CategoryParam string `mapstructure:"categoryParam"` + ReplyParam string `mapstructure:"replyParam"` + LikeParam string `mapstructure:"likeParam"` + BookmarkParam string `mapstructure:"bookmarkParam"` + AudioParam string `mapstructure:"audioParam"` + PhotoParam string `mapstructure:"photoParam"` + PhotoDescriptionParam string `mapstructure:"photoDescriptionParam"` +} + var appConfig = &config{} func initConfig() error { @@ -119,41 +132,42 @@ func initConfig() error { if err != nil { return err } - // Defaults viper.SetDefault("server.logging", false) viper.SetDefault("server.port", 8080) - viper.SetDefault("server.domain", "example.com") viper.SetDefault("server.publicAddress", "http://localhost:8080") viper.SetDefault("server.publicHttps", false) - viper.SetDefault("server.letsEncryptMail", "mail@example.com") viper.SetDefault("server.localHttps", false) viper.SetDefault("database.file", "data/db.sqlite") viper.SetDefault("cache.enable", true) viper.SetDefault("cache.expiration", 600) - viper.SetDefault("blog.lang", "en") - viper.SetDefault("blog.title", "My blog") - viper.SetDefault("blog.description", "This is my blog") - viper.SetDefault("blog.pagination", 10) - viper.SetDefault("blog.sections", []*section{{Name: "posts", Title: "Posts", Description: "**Posts** on this blog"}}) - viper.SetDefault("blog.taxonomies", []*taxonomy{{Name: "tags", Title: "Tags", Description: "**Tags** on this blog"}}) - viper.SetDefault("blog.menus", map[string]*menu{"main": {Items: []*menuItem{{Title: "Home", Link: "/"}, {Title: "Post", Link: "Posts"}}}}) - viper.SetDefault("blog.photos.enabled", true) - viper.SetDefault("blog.photos.parameter", "images") - viper.SetDefault("blog.photos.path", "/photos") - viper.SetDefault("blog.photos.title", "Photos") - viper.SetDefault("blog.photos.description", "Photos on this blog") - viper.SetDefault("blog.activitystreams.enabled", false) - viper.SetDefault("blog.activitystreams.replyParameter", "replylink") - viper.SetDefault("blog.activitystreams.imagesParameter", "images") viper.SetDefault("user.nick", "admin") viper.SetDefault("user.name", "Admin") viper.SetDefault("user.password", "secret") viper.SetDefault("hooks.shell", "/bin/bash") viper.SetDefault("hugo.frontmatter", []*frontmatter{{Meta: "title", Parameter: "title"}, {Meta: "tags", Parameter: "tags"}}) + viper.SetDefault("micropub.enabled", true) + viper.SetDefault("micropub.path", "/micropub") + viper.SetDefault("micropub.authAllowed", []string{}) + viper.SetDefault("micropub.tokenEndpoint", "https://tokens.indieauth.com/token") + viper.SetDefault("micropub.authEndpoint", "https://indieauth.com/auth") + viper.SetDefault("micropub.categoryParam", "tags") + viper.SetDefault("micropub.replyParam", "replylink") + viper.SetDefault("micropub.likeParam", "likelink") + viper.SetDefault("micropub.bookmarkParam", "link") + viper.SetDefault("micropub.audioParam", "audio") + viper.SetDefault("micropub.photoParam", "images") + viper.SetDefault("micropub.photoDescriptionParam", "imagealts") // Unmarshal config err = viper.Unmarshal(appConfig) if err != nil { return err } + // Check config + if len(appConfig.Blogs) == 0 { + return errors.New("no blog configured") + } + if len(appConfig.DefaultBlog) == 0 || appConfig.Blogs[appConfig.DefaultBlog] == nil { + return errors.New("no default blog or default blog not present") + } return nil } diff --git a/databaseMigrations.go b/databaseMigrations.go new file mode 100644 index 0000000..2ef3860 --- /dev/null +++ b/databaseMigrations.go @@ -0,0 +1,36 @@ +package main + +import ( + "database/sql" + + "github.com/lopezator/migrator" +) + +func migrateDb() error { + startWritingToDb() + defer finishWritingToDb() + m, err := migrator.New( + migrator.Migrations( + &migrator.Migration{ + Name: "00001", + Func: func(tx *sql.Tx) error { + _, err := tx.Exec(` + CREATE TABLE posts (path text not null primary key, content text, published text, updated text, blog text not null, section text); + CREATE TABLE post_parameters (id integer primary key autoincrement, path text not null, parameter text not null, value text); + CREATE INDEX index_pp_path on post_parameters (path); + CREATE TABLE redirects (fromPath text not null, toPath text not null, primary key (fromPath, toPath)); + CREATE TABLE cache (path text not null primary key, time integer, header blob, body blob); + `) + return err + }, + }, + ), + ) + if err != nil { + return err + } + if err := m.Migrate(appDb); err != nil { + return err + } + return nil +} diff --git a/database_migrations.go b/database_migrations.go deleted file mode 100644 index a0cfa6f..0000000 --- a/database_migrations.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "database/sql" - "github.com/lopezator/migrator" -) - -func migrateDb() error { - startWritingToDb() - defer finishWritingToDb() - m, err := migrator.New( - migrator.Migrations( - &migrator.Migration{ - Name: "00001", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("create table posts (path text not null primary key, content text, published text, updated text);") - return err - }, - }, - &migrator.Migration{ - Name: "00002", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("create table redirects (fromPath text not null, toPath text not null, primary key (fromPath, toPath));") - return err - }, - }, - &migrator.Migration{ - Name: "00003", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("create table post_parameters (path text not null, parameter text not null, value text, primary key (path, parameter));") - return err - }, - }, - &migrator.Migration{ - Name: "00004", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("create table cache (path text not null primary key, time integer, header blob, body blob);") - return err - }, - }, - &migrator.Migration{ - Name: "00005", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("create table pp_tmp(id integer primary key autoincrement, path text not null, parameter text not null, value text); insert into pp_tmp(path, parameter, value) select path, parameter, value from post_parameters; drop table post_parameters; alter table pp_tmp rename to post_parameters;") - return err - }, - }, - &migrator.Migration{ - Name: "00006", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("create index pp_path_index on post_parameters (path);") - return err - }, - }, - &migrator.Migration{ - Name: "00007", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("alter table posts add column section text; create trigger add_section after update on posts begin update posts set section = (select substr(path, 2, len) from (select path, instr(substr(path, 2),'/')-1 as len from (select new.path as path))) where path = new.path; end;") - return err - }, - }, - &migrator.Migration{ - Name: "00008", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("create index p_section_index on posts (section);") - return err - }, - }, - &migrator.Migration{ - Name: "00009", - Func: func(tx *sql.Tx) error { - _, err := tx.Exec("create trigger add_section_insert after insert on posts begin update posts set section = (select substr(path, 2, len) from (select path, instr(substr(path, 2),'/')-1 as len from (select new.path as path))) where path = new.path; end;") - return err - }, - }, - ), - ) - if err != nil { - return err - } - if err := m.Migrate(appDb); err != nil { - return err - } - return nil -} diff --git a/errors.go b/errors.go index 55a814c..ff8fec6 100644 --- a/errors.go +++ b/errors.go @@ -6,12 +6,14 @@ import ( ) type errorData struct { + Blog string Title string Message string } func serve404(w http.ResponseWriter, r *http.Request) { render(w, templateError, &errorData{ + Blog: appConfig.DefaultBlog, Title: "404 Not Found", Message: fmt.Sprintf("`%s` was not found", r.RequestURI), }) diff --git a/example-config.yaml b/example-config.yaml index 92d15b0..361a91b 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -2,7 +2,7 @@ server: logging: false port: 8080 domain: example.com - publicAdress: http://localhost:8080 + publicAddress: http://localhost:8080 publicHttps: false letsEncryptMail: mail@example.com localHttps: false @@ -11,35 +11,37 @@ database: cache: enable: true expiration: 600 -blog: - lang: en - title: My blog - description: This is my blog - sections: - - name: posts - title: Posts - description: "**Posts** on this blog" - taxonomies: - - name: tags - title: Tags - description: "**Tags** on this blog" - menus: - main: - items: - - title: Home - link: / - - title: Posts - link: /posts - photos: - enable: true - parameter: images - path: /photos - title: Photos - description: "Photos on this blog" - activitystreams: - enable: true - replyParameter: replylink - imagesParameter: images +blogs: + main: + path: / + lang: en + title: My blog + description: This is my blog + sections: + - name: posts + title: Posts + description: "**Posts** on this blog" + taxonomies: + - name: tags + title: Tags + description: "**Tags** on this blog" + menus: + main: + items: + - title: Home + link: / + - title: Posts + link: /posts + photos: + enable: true + parameter: images + path: /photos + title: Photos + description: "Photos on this blog" + activitystreams: + enable: true + replyParameter: replylink + imagesParameter: images user: nick: admin name: Admin @@ -49,4 +51,19 @@ hugo: - meta: title parameter: title - meta: tags - parameter: tags \ No newline at end of file + parameter: tags +micropub: + enabled: true + path: /micropub + authAllowed: + - example.com + tokenEndpoint: https://tokens.indieauth.com/token + authEndpoint: https://indieauth.com/auth + authn: login@example.com + categoryParam: tags + replyParam: replylink + likeParam: likelink + bookmarkParam: link + audioParam: audio + photoParam: images + photoDescriptionParam: imagealts \ No newline at end of file diff --git a/feeds.go b/feeds.go index 62dd878..27e28a4 100644 --- a/feeds.go +++ b/feeds.go @@ -1,28 +1,29 @@ package main import ( - "github.com/gorilla/feeds" "net/http" "strings" "time" + + "github.com/gorilla/feeds" ) type feedType string const ( - NONE feedType = "" - RSS feedType = "rss" - ATOM feedType = "atom" - JSON feedType = "json" + noFeed feedType = "" + rssFeed feedType = "rss" + atomFeed feedType = "atom" + jsonFeed feedType = "json" ) -func generateFeed(f feedType, w http.ResponseWriter, r *http.Request, posts []*Post, title string, description string) { +func generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Request, posts []*Post, title string, description string) { now := time.Now() if title == "" { - title = appConfig.Blog.Title + title = appConfig.Blogs[blog].Title } if description == "" { - description = appConfig.Blog.Description + description = appConfig.Blogs[blog].Description } feed := &feeds.Feed{ Title: title, @@ -43,11 +44,11 @@ func generateFeed(f feedType, w http.ResponseWriter, r *http.Request, posts []*P var feedStr string var err error switch f { - case RSS: + case rssFeed: feedStr, err = feed.ToRss() - case ATOM: + case atomFeed: feedStr, err = feed.ToAtom() - case JSON: + case jsonFeed: feedStr, err = feed.ToJSON() default: return @@ -57,12 +58,12 @@ func generateFeed(f feedType, w http.ResponseWriter, r *http.Request, posts []*P return } switch f { - case RSS: - w.Header().Add("Content-Type", "application/rss+xml; charset=utf-8") - case ATOM: - w.Header().Add("Content-Type", "application/atom+xml; charset=utf-8") - case JSON: - w.Header().Add("Content-Type", "application/feed+json; charset=utf-8") + case rssFeed: + w.Header().Add(contentType, "application/rss+xml; charset=utf-8") + case atomFeed: + w.Header().Add(contentType, "application/atom+xml; charset=utf-8") + case jsonFeed: + w.Header().Add(contentType, "application/feed+json; charset=utf-8") } w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(feedStr)) diff --git a/go.mod b/go.mod index 6d63893..c32b498 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 require ( github.com/PuerkitoBio/goquery v1.5.1 github.com/andybalholm/cascadia v1.2.0 // indirect - github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 + github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4 github.com/caddyserver/certmagic v0.12.0 github.com/go-chi/chi v4.1.2+incompatible github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect @@ -19,7 +19,7 @@ require ( github.com/lib/pq v1.8.0 // indirect github.com/lopezator/migrator v0.3.0 github.com/magiconair/properties v1.8.4 // indirect - github.com/mattn/go-sqlite3 v1.14.3 + github.com/mattn/go-sqlite3 v1.14.4 github.com/miekg/dns v1.1.31 // indirect github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect @@ -27,24 +27,24 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/smartystreets/assertions v1.2.0 // indirect github.com/snabb/sitemap v1.0.0 - github.com/spf13/afero v1.4.0 // indirect + github.com/spf13/afero v1.4.1 // indirect github.com/spf13/cast v1.3.1 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.6.1 // indirect - github.com/tdewolff/minify/v2 v2.9.5 + github.com/tdewolff/minify/v2 v2.9.7 github.com/vcraescu/go-paginator v0.0.0-20200923074551-426b20f3ae8a github.com/yuin/goldmark v1.2.1 github.com/yuin/goldmark-emoji v1.0.1 go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.16.0 // indirect - golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect + golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect - golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321 // indirect - golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d // indirect - golang.org/x/tools v0.0.0-20200924224222-8d73f17870ce // indirect + golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 // indirect + golang.org/x/sync v0.0.0-20200930132711-30421366ff76 // indirect + golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf // indirect + golang.org/x/tools v0.0.0-20201005185003-576e169c3de7 // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect - gopkg.in/ini.v1 v1.61.0 // indirect + gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 honnef.co/go/tools v0.0.1-2020.1.5 // indirect diff --git a/go.sum b/go.sum index 85529ba..874329e 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5z github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE= github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= -github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 h1:TEBmxO80TM04L8IuMWk77SGL1HomBmKTdzdJLLWznxI= -github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= +github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4 h1:OkS1BqB3CzLtGRznRyvriSY8jeaVk2CrDn2ZiRQgMUI= +github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4/go.mod h1:hMAUZFIkk4B1FouGxqlogyMyU6BwY/UiVmmbbzz9Up8= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -175,11 +175,12 @@ github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= -github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA= -github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI= +github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mholt/acmez v0.1.1 h1:KQODCqk+hBn3O7qfCRPj6L96uG65T5BSS95FKNEqtdA= github.com/mholt/acmez v0.1.1/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM= @@ -228,6 +229,7 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -244,8 +246,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8= -github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ= +github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= @@ -270,10 +272,10 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tdewolff/minify/v2 v2.9.5 h1:+fHvqLencVdv14B+zgxQGhetF9qXl/nRTN/1mcyQwpM= -github.com/tdewolff/minify/v2 v2.9.5/go.mod h1:jshtBj/uUJH6JX1fuxTLnnHOA1RVJhF5MM+leJzDKb4= -github.com/tdewolff/parse/v2 v2.5.3 h1:fnPIstKgEfxd3+wwHnH73sAYydsR0o/jYhcQ6c5PkrA= -github.com/tdewolff/parse/v2 v2.5.3/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/minify/v2 v2.9.7 h1:r8ewdcX8VYUoNj+s9WSy4FtNNNqNPevWOkb/MksAtzQ= +github.com/tdewolff/minify/v2 v2.9.7/go.mod h1:AcJ/ggtHex5N/QiafLI8rlIO3qwSlgbPNLi27VZSYz8= +github.com/tdewolff/parse/v2 v2.5.4 h1:ggaQ1SVE8wErRrZwUs49I6iQ1zL/tFlb7KtYsk2I8Yk= +github.com/tdewolff/parse/v2 v2.5.4/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -316,8 +318,8 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -362,8 +364,8 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321 h1:lleNcKRbcaC8MqgLwghIkzZ2JBQAb7QQ9MiwRt1BisA= -golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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= @@ -374,6 +376,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200930132711-30421366ff76 h1:JnxiSYT3Nm0BT2a8CyvYyM6cnrWpidecD1UuSYbhKm0= +golang.org/x/sync v0.0.0-20200930132711-30421366ff76/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -393,8 +397,9 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf h1:Bg47KQy0JhTHuf4sLiQwTMKwUMfSDwgSGatrxGR7nLM= +golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -426,8 +431,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200924224222-8d73f17870ce h1:XRr763sMfaUSNR4EsxbddvVEqYFa9picrx6ks9pJkKw= -golang.org/x/tools v0.0.0-20200924224222-8d73f17870ce/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201005185003-576e169c3de7 h1:YTAUHYgZh/ZOA35/OrjTDmFFKb6ddkBL1Zgtl9r8Di8= +golang.org/x/tools v0.0.0-20201005185003-576e169c3de7/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= @@ -463,8 +468,8 @@ gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10= -gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/http.go b/http.go index 6c0eb85..eeebbaf 100644 --- a/http.go +++ b/http.go @@ -11,8 +11,12 @@ import ( "github.com/go-chi/chi/middleware" ) -const contentTypeHTML = "text/html; charset=utf-8" -const contentTypeJSON = "application/json; charset=utf-8" +const contentType = "Content-Type" +const contentTypeHTMLUTF8 = "text/html; charset=utf-8" +const contentTypeJSONUTF8 = "application/json; charset=utf-8" +const contentTypeJSON = "application/json" +const contentTypeWWWForm = "application/x-www-form-urlencoded" +const contentTypeMultipartForm = "multipart/form-data" var d *dynamicHandler @@ -24,10 +28,10 @@ func startServer() (err error) { } d.swapHandler(h) localAddress := ":" + strconv.Itoa(appConfig.Server.Port) - if appConfig.Server.PublicHttps { + if appConfig.Server.PublicHTTPS { initPublicHTTPS() err = certmagic.HTTPS([]string{appConfig.Server.Domain}, d) - } else if appConfig.Server.LocalHttps { + } else if appConfig.Server.LocalHTTPS { err = http.ListenAndServeTLS(localAddress, "https/server.crt", "https/server.key", d) } else { err = http.ListenAndServe(localAddress, d) @@ -70,6 +74,13 @@ func buildHandler() (http.Handler, error) { apiRouter.Post("/hugo", apiPostCreateHugo) }) + // Micropub + if appConfig.Micropub.Enabled { + r.Get(appConfig.Micropub.Path, serveMicropubQuery) + r.With(checkIndieAuth).Post(appConfig.Micropub.Path, serveMicropubPost) + r.With(checkIndieAuth).Post(appConfig.Micropub.Path+micropubMediaSubPath, serveMicropubMedia) + } + // Posts allPostPaths, err := allPostPaths() if err != nil { @@ -104,56 +115,56 @@ func buildHandler() (http.Handler, error) { jsonPath := ".json" atomPath := ".atom" - // Indexes, Feeds - for _, section := range appConfig.Blog.Sections { - if section.Name != "" { - path := "/" + section.Name - r.With(cacheMiddleware, minifier.Middleware).Get(path, serveSection(path, section, NONE)) - r.With(cacheMiddleware, minifier.Middleware).Get(path+rssPath, serveSection(path, section, RSS)) - r.With(cacheMiddleware, minifier.Middleware).Get(path+jsonPath, serveSection(path, section, JSON)) - r.With(cacheMiddleware, minifier.Middleware).Get(path+atomPath, serveSection(path, section, ATOM)) - r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveSection(path, section, NONE)) - } - } + for blog, blogConfig := range appConfig.Blogs { - for _, taxonomy := range appConfig.Blog.Taxonomies { - if taxonomy.Name != "" { - r.With(cacheMiddleware, minifier.Middleware).Get("/"+taxonomy.Name, serveTaxonomy(taxonomy)) - values, err := allTaxonomyValues(taxonomy.Name) - if err != nil { - return nil, err - } - for _, tv := range values { - path := "/" + taxonomy.Name + "/" + urlize(tv) - r.With(cacheMiddleware, minifier.Middleware).Get(path, serveTaxonomyValue(path, taxonomy, tv, NONE)) - r.With(cacheMiddleware, minifier.Middleware).Get(path+rssPath, serveTaxonomyValue(path, taxonomy, tv, RSS)) - r.With(cacheMiddleware, minifier.Middleware).Get(path+jsonPath, serveTaxonomyValue(path, taxonomy, tv, JSON)) - r.With(cacheMiddleware, minifier.Middleware).Get(path+atomPath, serveTaxonomyValue(path, taxonomy, tv, ATOM)) - r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveTaxonomyValue(path, taxonomy, tv, NONE)) + blogPath := blogConfig.Path + if blogPath == "/" { + blogPath = "" + } + + // Indexes, Feeds + for _, section := range blogConfig.Sections { + if section.Name != "" { + path := blogPath + "/" + section.Name + r.With(cacheMiddleware, minifier.Middleware).Get(path, serveSection(blog, path, section, noFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(path+rssPath, serveSection(blog, path, section, rssFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(path+jsonPath, serveSection(blog, path, section, jsonFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(path+atomPath, serveSection(blog, path, section, atomFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveSection(blog, path, section, noFeed)) } } - } - if appConfig.Blog.Photos.Enabled { - r.With(cacheMiddleware, minifier.Middleware).Get(appConfig.Blog.Photos.Path, servePhotos(appConfig.Blog.Photos.Path)) - r.With(cacheMiddleware, minifier.Middleware).Get(appConfig.Blog.Photos.Path+paginationPath, servePhotos(appConfig.Blog.Photos.Path)) - } + for _, taxonomy := range blogConfig.Taxonomies { + if taxonomy.Name != "" { + path := blogPath + "/" + taxonomy.Name + r.With(cacheMiddleware, minifier.Middleware).Get(path, serveTaxonomy(blog, taxonomy)) + values, err := allTaxonomyValues(blog, taxonomy.Name) + if err != nil { + return nil, err + } + for _, tv := range values { + path = path + "/" + urlize(tv) + r.With(cacheMiddleware, minifier.Middleware).Get(path, serveTaxonomyValue(blog, path, taxonomy, tv, noFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(path+rssPath, serveTaxonomyValue(blog, path, taxonomy, tv, rssFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(path+jsonPath, serveTaxonomyValue(blog, path, taxonomy, tv, jsonFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(path+atomPath, serveTaxonomyValue(blog, path, taxonomy, tv, atomFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveTaxonomyValue(blog, path, taxonomy, tv, noFeed)) + } + } + } - // Blog - rootPath := "/" - blogPath := "/blog" - if !r.Match(chi.NewRouteContext(), http.MethodGet, rootPath) { - r.With(cacheMiddleware, minifier.Middleware).Get(rootPath, serveHome("", NONE)) - r.With(cacheMiddleware, minifier.Middleware).Get(rootPath+rssPath, serveHome("", RSS)) - r.With(cacheMiddleware, minifier.Middleware).Get(rootPath+jsonPath, serveHome("", JSON)) - r.With(cacheMiddleware, minifier.Middleware).Get(rootPath+atomPath, serveHome("", ATOM)) - r.With(cacheMiddleware, minifier.Middleware).Get(paginationPath, serveHome("", NONE)) - } else if !r.Match(chi.NewRouteContext(), http.MethodGet, blogPath) { - r.With(cacheMiddleware, minifier.Middleware).Get(blogPath, serveHome(blogPath, NONE)) - r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+rssPath, serveHome(blogPath, RSS)) - r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+jsonPath, serveHome(blogPath, JSON)) - r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+atomPath, serveHome(blogPath, ATOM)) - r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+paginationPath, serveHome(blogPath, NONE)) + // Photos + if blogConfig.Photos.Enabled { + r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+blogConfig.Photos.Path, servePhotos(blog)) + r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+blogConfig.Photos.Path+paginationPath, servePhotos(blog)) + } + + // Blog + r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path, serveHome(blog, blogPath, noFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path+rssPath, serveHome(blog, blogPath, rssFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path+jsonPath, serveHome(blog, blogPath, jsonFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path+atomPath, serveHome(blog, blogPath, atomFeed)) + r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path+paginationPath, serveHome(blog, blogPath, noFeed)) } // Sitemap diff --git a/hugo.go b/hugo.go index f11b820..7317dbd 100644 --- a/hugo.go +++ b/hugo.go @@ -2,14 +2,16 @@ package main import ( "errors" + "strconv" + "strings" + "github.com/jeremywohl/flatten" "github.com/spf13/cast" "gopkg.in/yaml.v3" - "strconv" - "strings" ) func parseHugoFile(fileContent string, path string) (*Post, error) { + // TODO: Add option to set blog, slug if path == "" { return nil, errors.New("empty path") } @@ -57,6 +59,7 @@ func parseHugoFile(fileContent string, path string) (*Post, error) { } } // Create redirects + // TODO: Move redirect creation after post creation var aliases []string for fk, value := range flat { if strings.HasPrefix(fk, "aliases") { diff --git a/indieauth.go b/indieauth.go new file mode 100644 index 0000000..f855c84 --- /dev/null +++ b/indieauth.go @@ -0,0 +1,92 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" +) + +type indieAuthTokenResponse struct { + Me string `json:"me"` + ClientID string `json:"client_id"` + Scope string `json:"scope"` + IssuedBy string `json:"issued_by"` + Error string `json:"error"` + ErrorDescription string `json:"error_description"` + StatusCode int +} + +func checkIndieAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bearerToken := r.Header.Get("Authorization") + if len(bearerToken) == 0 { + if accessToken := r.URL.Query().Get("access_token"); len(accessToken) > 0 { + bearerToken = "Bearer " + accessToken + } + } + tokenResponse, err := verifyIndieAuthAccessToken(bearerToken) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + if tokenResponse.StatusCode != http.StatusOK { + http.Error(w, "Failed to retrieve authentication information", http.StatusUnauthorized) + return + } + authorized := false + for _, allowed := range appConfig.Micropub.AuthAllowed { + if err := compareHostnames(tokenResponse.Me, allowed); err == nil { + authorized = true + break + } + } + if !authorized { + http.Error(w, "Forbidden", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + return + }) +} + +func verifyIndieAuthAccessToken(bearerToken string) (*indieAuthTokenResponse, error) { + if len(bearerToken) == 0 { + return nil, errors.New("no token") + } + req, err := http.NewRequest("GET", appConfig.Micropub.TokenEndpoint, nil) + if err != nil { + return nil, err + } + req.Header.Add(contentType, contentTypeWWWForm) + req.Header.Add("Authorization", bearerToken) + req.Header.Add("Accept", contentTypeJSON) + c := http.Client{ + Timeout: time.Duration(10 * time.Second), + } + resp, err := c.Do(req) + if err != nil { + return nil, err + } + tokenRes := indieAuthTokenResponse{StatusCode: resp.StatusCode} + err = json.NewDecoder(resp.Body).Decode(&tokenRes) + resp.Body.Close() + if err != nil { + return nil, err + } + return &tokenRes, nil +} + +func compareHostnames(a string, allowed string) error { + h1, err := url.Parse(a) + if err != nil { + return err + } + if strings.ToLower(h1.Hostname()) != strings.ToLower(allowed) { + return fmt.Errorf("hostnames do not match, %s is not %s", h1, allowed) + } + return nil +} diff --git a/micropub.go b/micropub.go new file mode 100644 index 0000000..03d22c1 --- /dev/null +++ b/micropub.go @@ -0,0 +1,271 @@ +package main + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/spf13/cast" + "gopkg.in/yaml.v3" +) + +const micropubMediaSubPath = "/media" + +type micropubConfig struct { + MediaEndpoint string `json:"media-endpoint,omitempty"` +} + +func serveMicropubQuery(w http.ResponseWriter, r *http.Request) { + if q := r.URL.Query().Get("q"); q == "config" { + w.Header().Add(contentType, contentTypeJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(µpubConfig{ + // TODO: Uncomment when media endpoint implemented + // MediaEndpoint: appConfig.Server.PublicAddress + micropubMediaPath, + }) + } else { + w.Header().Add(contentType, contentTypeJSON) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("{}")) + } +} + +func serveMicropubPost(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + var post *Post + if ct := r.Header.Get(contentType); strings.Contains(ct, contentTypeWWWForm) { + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + post, err = convertMPValueMapToPost(r.Form) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if strings.Contains(ct, contentTypeMultipartForm) { + err := r.ParseMultipartForm(1024 * 1024 * 16) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + post, err = convertMPValueMapToPost(r.MultipartForm.Value) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if strings.Contains(ct, contentTypeJSON) { + parsedMfItem := µformatItem{} + err := json.NewDecoder(r.Body).Decode(parsedMfItem) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + post, err = convertMPMfToPost(parsedMfItem) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else { + http.Error(w, "wrong content type", http.StatusBadRequest) + return + } + err := post.createOrReplace(true) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Add("Location", appConfig.Server.PublicAddress+post.Path) + w.WriteHeader(http.StatusAccepted) + return +} + +func convertMPValueMapToPost(values map[string][]string) (*Post, error) { + if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") { + return nil, errors.New("only entry type is supported so far") + } + entry := &Post{ + Parameters: map[string][]string{}, + } + if content, ok := values["content"]; ok { + entry.Content = content[0] + } + // Parameter + if name, ok := values["name"]; ok { + entry.Parameters["title"] = name + } + if category, ok := values["category"]; ok { + entry.Parameters[appConfig.Micropub.CategoryParam] = category + } else if categories, ok := values["category[]"]; ok { + entry.Parameters[appConfig.Micropub.CategoryParam] = categories + } + if inReplyTo, ok := values["in-reply-to"]; ok { + entry.Parameters[appConfig.Micropub.ReplyParam] = inReplyTo + } + if likeOf, ok := values["like-of"]; ok { + entry.Parameters[appConfig.Micropub.LikeParam] = likeOf + } + if bookmarkOf, ok := values["bookmark-of"]; ok { + entry.Parameters[appConfig.Micropub.BookmarkParam] = bookmarkOf + } + if audio, ok := values["audio"]; ok { + entry.Parameters[appConfig.Micropub.AudioParam] = audio + } else if audio, ok := values["audio[]"]; ok { + entry.Parameters[appConfig.Micropub.AudioParam] = audio + } + if photo, ok := values["photo"]; ok { + entry.Parameters[appConfig.Micropub.PhotoParam] = photo + } else if photos, ok := values["photo[]"]; ok { + entry.Parameters[appConfig.Micropub.PhotoParam] = photos + } + if photoAlt, ok := values["mp-photo-alt"]; ok { + entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = photoAlt + } else if photoAlts, ok := values["mp-photo-alt[]"]; ok { + entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = photoAlts + } + if slug, ok := values["mp-slug"]; ok { + entry.Slug = slug[0] + } + err := computeExtraPostParameters(entry) + if err != nil { + return nil, err + } + return entry, nil +} + +type microformatItem struct { + Type []string `json:"type"` + Properties *microformatProperties `json:"properties"` +} + +type microformatProperties struct { + Name []string `json:"name"` + Published []string `json:"published"` + Updated []string `json:"updated"` + Category []string `json:"category"` + Content []string `json:"content"` + URL []string `json:"url"` + InReplyTo []string `json:"in-reply-to"` + LikeOf []string `json:"like-of"` + BookmarkOf []string `json:"bookmark-of"` + MpSlug []string `json:"mp-slug"` + Photo []interface{} `json:"photo"` + Audio []string `json:"audio"` +} + +func convertMPMfToPost(mf *microformatItem) (*Post, error) { + if len(mf.Type) != 1 || mf.Type[0] != "h-entry" { + return nil, errors.New("only entry type is supported so far") + } + entry := &Post{} + // Content + if mf.Properties != nil && len(mf.Properties.Content) == 1 && len(mf.Properties.Content[0]) > 0 { + entry.Content = mf.Properties.Content[0] + } + // Parameter + if len(mf.Properties.Name) == 1 { + entry.Parameters["title"] = mf.Properties.Name + } + if len(mf.Properties.Category) > 0 { + entry.Parameters[appConfig.Micropub.CategoryParam] = mf.Properties.Category + } + if len(mf.Properties.InReplyTo) == 1 { + entry.Parameters[appConfig.Micropub.ReplyParam] = mf.Properties.InReplyTo + } + if len(mf.Properties.LikeOf) == 1 { + entry.Parameters[appConfig.Micropub.LikeParam] = mf.Properties.LikeOf + } + if len(mf.Properties.BookmarkOf) == 1 { + entry.Parameters[appConfig.Micropub.BookmarkParam] = mf.Properties.BookmarkOf + } + if len(mf.Properties.Audio) > 0 { + entry.Parameters[appConfig.Micropub.AudioParam] = mf.Properties.Audio + } + if len(mf.Properties.Photo) > 0 { + for _, photo := range mf.Properties.Photo { + if theString, justString := photo.(string); justString { + entry.Parameters[appConfig.Micropub.PhotoParam] = append(entry.Parameters[appConfig.Micropub.PhotoParam], theString) + entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = append(entry.Parameters[appConfig.Micropub.PhotoDescriptionParam], "") + } else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto { + entry.Parameters[appConfig.Micropub.PhotoParam] = append(entry.Parameters[appConfig.Micropub.PhotoParam], cast.ToString(thePhoto["value"])) + entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = append(entry.Parameters[appConfig.Micropub.PhotoDescriptionParam], cast.ToString(thePhoto["alt"])) + } + } + } + if len(mf.Properties.MpSlug) == 1 { + entry.Slug = mf.Properties.MpSlug[0] + } + err := computeExtraPostParameters(entry) + if err != nil { + return nil, err + } + return entry, nil + +} + +func computeExtraPostParameters(entry *Post) error { + // Add images not in content + images := entry.Parameters[appConfig.Micropub.PhotoParam] + imageAlts := entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] + useAlts := len(images) == len(imageAlts) + for i, image := range images { + if !strings.Contains(entry.Content, image) { + if useAlts && len(imageAlts[i]) > 0 { + entry.Content += "\n\n![" + imageAlts[i] + "](" + image + " \"" + imageAlts[i] + "\")" + } else { + entry.Content += "\n\n![](" + image + ")" + } + } + } + sep := "---\n" + if split := strings.Split(entry.Content, sep); len(split) > 2 { + // Contains frontmatter + fm := split[1] + meta := map[string]interface{}{} + err := yaml.Unmarshal([]byte(fm), &meta) + if err != nil { + return err + } + // Find section and copy frontmatter to params + for key, value := range meta { + if a, ok := value.([]interface{}); ok { + for _, ae := range a { + entry.Parameters[key] = append(entry.Parameters[key], cast.ToString(ae)) + } + } else { + entry.Parameters[key] = append(entry.Parameters[key], cast.ToString(value)) + } + } + // Remove frontmatter from content + entry.Content = strings.Replace(entry.Content, split[0]+sep+split[1]+sep, "", 1) + } + // Check settings + if blog := entry.Parameters["blog"]; len(blog) == 1 && blog[0] != "" { + entry.Blog = blog[0] + delete(entry.Parameters, "blog") + } else { + entry.Blog = appConfig.DefaultBlog + } + if path := entry.Parameters["path"]; len(path) == 1 && path[0] != "" { + entry.Path = path[0] + delete(entry.Parameters, "path") + } + if section := entry.Parameters["section"]; len(section) == 1 && section[0] != "" { + entry.Section = section[0] + delete(entry.Parameters, "section") + } + if slug := entry.Parameters["slug"]; len(slug) == 1 && slug[0] != "" { + entry.Slug = slug[0] + delete(entry.Parameters, "slug") + } + if entry.Path == "" && entry.Section == "" { + entry.Section = appConfig.Blogs[entry.Blog].DefaultSection + } + return nil +} + +func serveMicropubMedia(w http.ResponseWriter, r *http.Request) { + // TODO: Implement media server +} diff --git a/posts.go b/posts.go index 62711c7..d0968e0 100644 --- a/posts.go +++ b/posts.go @@ -5,12 +5,13 @@ import ( "database/sql" "errors" "fmt" - "github.com/go-chi/chi" - "github.com/vcraescu/go-paginator" "net/http" "reflect" "strconv" "strings" + + "github.com/go-chi/chi" + "github.com/vcraescu/go-paginator" ) var errPostNotFound = errors.New("post not found") @@ -21,6 +22,10 @@ type Post struct { Published string `json:"published"` Updated string `json:"updated"` Parameters map[string][]string `json:"parameters"` + Blog string `json:"blog"` + Section string `json:"section"` + // Not persisted + Slug string `json:"slug"` } func servePost(w http.ResponseWriter, r *http.Request) { @@ -41,6 +46,7 @@ func servePost(w http.ResponseWriter, r *http.Request) { } type indexTemplateData struct { + Blog string Title string Description string Posts []*Post @@ -78,24 +84,26 @@ func (p *postPaginationAdapter) Slice(offset, length int, data interface{}) erro return err } -func serveHome(path string, ft feedType) func(w http.ResponseWriter, r *http.Request) { +func serveHome(blog string, path string, ft feedType) func(w http.ResponseWriter, r *http.Request) { return serveIndex(&indexConfig{ + blog: blog, path: path, feed: ft, }) } -func serveSection(path string, section *section, ft feedType) func(w http.ResponseWriter, r *http.Request) { +func serveSection(blog string, path string, section *section, ft feedType) func(w http.ResponseWriter, r *http.Request) { return serveIndex(&indexConfig{ + blog: blog, path: path, section: section, feed: ft, }) } -func serveTaxonomy(tax *taxonomy) func(w http.ResponseWriter, r *http.Request) { +func serveTaxonomy(blog string, tax *taxonomy) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - allValues, err := allTaxonomyValues(tax.Name) + allValues, err := allTaxonomyValues(blog, tax.Name) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -103,15 +111,18 @@ func serveTaxonomy(tax *taxonomy) func(w http.ResponseWriter, r *http.Request) { render(w, templateTaxonomy, struct { Taxonomy *taxonomy TaxonomyValues []string + Blog string }{ Taxonomy: tax, TaxonomyValues: allValues, + Blog: blog, }) } } -func serveTaxonomyValue(path string, tax *taxonomy, value string, ft feedType) func(w http.ResponseWriter, r *http.Request) { +func serveTaxonomyValue(blog string, path string, tax *taxonomy, value string, ft feedType) func(w http.ResponseWriter, r *http.Request) { return serveIndex(&indexConfig{ + blog: blog, path: path, tax: tax, taxValue: value, @@ -119,15 +130,17 @@ func serveTaxonomyValue(path string, tax *taxonomy, value string, ft feedType) f }) } -func servePhotos(path string) func(w http.ResponseWriter, r *http.Request) { +func servePhotos(blog string) func(w http.ResponseWriter, r *http.Request) { return serveIndex(&indexConfig{ - path: path, - onlyWithParameter: appConfig.Blog.Photos.Parameter, + blog: blog, + path: appConfig.Blogs[blog].Photos.Path, + onlyWithParameter: appConfig.Blogs[blog].Photos.Path, template: templatePhotos, }) } type indexConfig struct { + blog string path string section *section tax *taxonomy @@ -141,16 +154,21 @@ func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { pageNoString := chi.URLParam(r, "page") pageNo, _ := strconv.Atoi(pageNoString) - sections := appConfig.Blog.Sections + var sections []string if ic.section != nil { - sections = []*section{ic.section} + sections = []string{ic.section.Name} + } else { + for sectionKey := range appConfig.Blogs[ic.blog].Sections { + sections = append(sections, sectionKey) + } } p := paginator.New(&postPaginationAdapter{context: r.Context(), config: &postsRequestConfig{ + blog: ic.blog, sections: sections, taxonomy: ic.tax, taxonomyValue: ic.taxValue, onlyWithParameter: ic.onlyWithParameter, - }}, appConfig.Blog.Pagination) + }}, appConfig.Blogs[ic.blog].Pagination) p.SetPage(pageNo) var posts []*Post err := p.Results(&posts) @@ -167,8 +185,8 @@ func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) { description = ic.section.Description } // Check if feed - if ic.feed != NONE { - generateFeed(ic.feed, w, r, posts, title, description) + if ic.feed != noFeed { + generateFeed(ic.blog, ic.feed, w, r, posts, title, description) return } // Navigation @@ -185,6 +203,7 @@ func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) { template = templateIndex } render(w, template, &indexTemplateData{ + Blog: ic.blog, Title: title, Description: description, Posts: posts, @@ -208,10 +227,11 @@ func getPost(context context.Context, path string) (*Post, error) { } type postsRequestConfig struct { + blog string path string limit int offset int - sections []*section + sections []string taxonomy *taxonomy taxonomyValue string onlyWithParameter string @@ -220,8 +240,11 @@ type postsRequestConfig struct { func getPosts(context context.Context, config *postsRequestConfig) (posts []*Post, err error) { paths := make(map[string]int) var rows *sql.Rows - defaultSelection := "select p.path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), coalesce(parameter, ''), coalesce(value, '') " + defaultSelection := "select p.path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), coalesce(blog, ''), coalesce(section, ''), coalesce(parameter, ''), coalesce(value, '') " postsTable := "posts" + if config.blog != "" { + postsTable = "(select * from " + postsTable + " where blog = '" + config.blog + "')" + } if config.onlyWithParameter != "" { postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = '" + config.onlyWithParameter + "' and length(coalesce(pp.value, '')) > 1)" } @@ -234,7 +257,7 @@ func getPosts(context context.Context, config *postsRequestConfig) (posts []*Pos if i > 0 { postsTable += " or" } - postsTable += " section='" + section.Name + "'" + postsTable += " section='" + section + "'" } postsTable += ")" } @@ -259,7 +282,7 @@ func getPosts(context context.Context, config *postsRequestConfig) (posts []*Pos for rows.Next() { post := &Post{} var parameterName, parameterValue string - err = rows.Scan(&post.Path, &post.Content, &post.Published, &post.Updated, ¶meterName, ¶meterValue) + err = rows.Scan(&post.Path, &post.Content, &post.Published, &post.Updated, &post.Blog, &post.Section, ¶meterName, ¶meterValue) if err != nil { return nil, err } @@ -295,9 +318,9 @@ func allPostPaths() ([]string, error) { return postPaths, nil } -func allTaxonomyValues(taxonomy string) ([]string, error) { +func allTaxonomyValues(blog string, taxonomy string) ([]string, error) { var values []string - rows, err := appDb.Query("select distinct value from post_parameters where parameter = ? and value not null and value != ''", taxonomy) + rows, err := appDb.Query("select distinct pp.value from posts p left outer join post_parameters pp on p.path = pp.path where pp.parameter = ? and length(coalesce(pp.value, '')) > 1 and blog = ?", taxonomy, blog) if err != nil { return nil, err } diff --git a/postsDb.go b/postsDb.go index dd13a27..a58ad29 100644 --- a/postsDb.go +++ b/postsDb.go @@ -1,21 +1,22 @@ package main import ( + "bytes" + "context" "errors" - "github.com/araddon/dateparse" + "fmt" "strings" + "text/template" "time" + + "github.com/araddon/dateparse" ) -func (p *Post) checkPost() error { +func (p *Post) checkPost(new bool) error { if p == nil { return errors.New("no post") } - if p.Path == "" || !strings.HasPrefix(p.Path, "/") { - return errors.New("wrong path") - } - // Fix path - p.Path = strings.TrimSuffix(p.Path, "/") + now := time.Now() // Fix content p.Content = strings.TrimSuffix(strings.TrimPrefix(p.Content, "\n"), "\n") // Fix date strings @@ -26,6 +27,9 @@ func (p *Post) checkPost() error { } p.Published = d.String() } + if p.Published == "" { + p.Published = now.String() + } if p.Updated != "" { d, err := dateparse.ParseIn(p.Updated, time.Local) if err != nil { @@ -33,11 +37,63 @@ func (p *Post) checkPost() error { } p.Updated = d.String() } + // Check blog + if p.Blog == "" { + p.Blog = appConfig.DefaultBlog + } + // Check path + p.Path = strings.TrimSuffix(p.Path, "/") + if p.Path == "" { + if p.Section == "" { + p.Section = appConfig.Blogs[p.Blog].DefaultSection + } + if p.Slug == "" { + random := generateRandomString(now, 5) + p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random) + } + pathVars := struct { + BlogPath string + Year int + Month int + Slug string + Section string + }{ + BlogPath: appConfig.Blogs[p.Blog].Path, + Year: now.Year(), + Month: int(now.Month()), + Slug: p.Slug, + Section: p.Section, + } + pathTmplString := appConfig.Blogs[p.Blog].Sections[p.Section].PathTemplate + if pathTmplString == "" { + return errors.New("path template empty") + } + pathTmpl, err := template.New("location").Parse(pathTmplString) + if err != nil { + return errors.New("failed to parse location template") + } + var pathBuffer bytes.Buffer + err = pathTmpl.Execute(&pathBuffer, pathVars) + if err != nil { + return errors.New("failed to execute location template") + } + p.Path = pathBuffer.String() + } + if p.Path != "" && !strings.HasPrefix(p.Path, "/") { + return errors.New("wrong path") + } + // Check if post with path already exists + if new { + post, _ := getPost(context.Background(), p.Path) + if post != nil { + return errors.New("path already exists") + } + } return nil } -func (p *Post) createOrReplace() error { - err := p.checkPost() +func (p *Post) createOrReplace(new bool) error { + err := p.checkPost(new) if err != nil { return err } @@ -47,7 +103,7 @@ func (p *Post) createOrReplace() error { finishWritingToDb() return err } - _, err = tx.Exec("insert or replace into posts (path, content, published, updated) values (?, ?, ?, ?)", p.Path, p.Content, p.Published, p.Updated) + _, err = tx.Exec("insert or replace into posts (path, content, published, updated, blog, section) values (?, ?, ?, ?, ?, ?)", p.Path, p.Content, p.Published, p.Updated, p.Blog, p.Section) if err != nil { _ = tx.Rollback() finishWritingToDb() @@ -77,12 +133,13 @@ func (p *Post) createOrReplace() error { return err } finishWritingToDb() - go purgeCache(p.Path) + go purgeCache() return reloadRouter() } func (p *Post) delete() error { - err := p.checkPost() + // TODO + err := p.checkPost(false) if err != nil { return err } @@ -110,6 +167,6 @@ func (p *Post) delete() error { return err } finishWritingToDb() - go purgeCache(p.Path) + go purgeCache() return reloadRouter() } diff --git a/redirects.go b/redirects.go index 35b4283..68dcd1e 100644 --- a/redirects.go +++ b/redirects.go @@ -22,8 +22,10 @@ func serveRedirect(w http.ResponseWriter, r *http.Request) { // Send redirect w.Header().Set("Location", redirect) render(w, templateRedirect, struct { + Blog string Permalink string }{ + Blog: appConfig.DefaultBlog, Permalink: redirect, }) w.WriteHeader(http.StatusFound) diff --git a/render.go b/render.go index fa2e80d..f54b631 100644 --- a/render.go +++ b/render.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "github.com/araddon/dateparse" "html/template" "log" "net/http" @@ -11,6 +10,8 @@ import ( "path/filepath" "strings" "time" + + "github.com/araddon/dateparse" ) const templatesDir = "templates" @@ -29,11 +30,17 @@ var templateFunctions template.FuncMap func initRendering() error { templateFunctions = template.FuncMap{ - "blog": func() *configBlog { - return appConfig.Blog + "blogs": func() map[string]*configBlog { + return appConfig.Blogs }, - "menu": func(id string) *menu { - return appConfig.Blog.Menus[id] + "blog": func(blog string) *configBlog { + return appConfig.Blogs[blog] + }, + "micropub": func() *configMicropub { + return appConfig.Micropub + }, + "menu": func(blog, id string) *menu { + return appConfig.Blogs[blog].Menus[id] }, "md": func(content string) template.HTML { htmlContent, err := renderMarkdown(content) @@ -104,7 +111,7 @@ func render(w http.ResponseWriter, template string, data interface{}) { http.Error(w, err.Error(), http.StatusInternalServerError) } // Set content type (needed for minification middleware - w.Header().Set("Content-Type", contentTypeHTML) + w.Header().Set(contentType, contentTypeHTMLUTF8) // Write buffered response _, _ = w.Write(buffer.Bytes()) } diff --git a/templates/base.gohtml b/templates/base.gohtml index c2f845e..5dd60f9 100644 --- a/templates/base.gohtml +++ b/templates/base.gohtml @@ -1,11 +1,14 @@ {{ define "base" }} - + {{ template "title" . }} + {{ if micropub.Enabled }} + {{ include "micropub" . }} + {{ end }} {{ include "header" . }} {{ template "main" . }} {{ end }} \ No newline at end of file diff --git a/templates/error.gohtml b/templates/error.gohtml index a655bf5..9f7698d 100644 --- a/templates/error.gohtml +++ b/templates/error.gohtml @@ -1,5 +1,5 @@ {{ define "title" }} - {{ with .Title }}{{ . }} - {{end}}{{ blog.Title }} + {{ with .Title }}{{ . }}{{end}} {{ end }} {{ define "main" }} diff --git a/templates/header.gohtml b/templates/header.gohtml index 1e355c4..24b3572 100644 --- a/templates/header.gohtml +++ b/templates/header.gohtml @@ -1,7 +1,8 @@ {{ define "header" }}
-

{{ blog.Title }}

- {{ with blog.Description }}

{{ . }}

{{ end }} + {{ $blog := (blog .Blog) }} +

{{ $blog.Title }}

+ {{ with $blog.Description }}

{{ . }}

{{ end }} {{ include "menu" . }}
{{ end }} \ No newline at end of file diff --git a/templates/index.gohtml b/templates/index.gohtml index 6462faf..da07c32 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -1,8 +1,8 @@ {{ define "title" }} {{ if .Title }} - {{ .Title }} - {{ blog.Title }} + {{ .Title }} - {{ (blog .Blog).Title }} {{ else }} - {{ blog.Title }} + {{ (blog .Blog).Title }} {{ end }} - {{ with menu "main" }} + {{ with menu .Blog "main" }} {{ $first := true }} {{ range $i, $item := .Items }} {{ if ne $first true }} • {{ end }} + {{ end }} + {{ with micropub.AuthEndpoint }} + + {{ end }} + {{ with micropub.TokenEndpoint }} + + {{ end }} + {{ with micropub.Authn }} + + {{ end }} +{{ end }} \ No newline at end of file diff --git a/templates/photos.gohtml b/templates/photos.gohtml index ea15a79..9f6d156 100644 --- a/templates/photos.gohtml +++ b/templates/photos.gohtml @@ -1,16 +1,18 @@ {{ define "title" }} - {{ if blog.Photos.Title }} - {{ blog.Photos.Title }} - {{ blog.Title }} + {{ $blog := (blog .Blog) }} + {{ if $blog.Photos.Title }} + {{ $blog.Photos.Title }} - {{ $blog.Title }} {{ else }} - {{ blog.Title }} + {{ $blog.Title }} {{ end }} {{ end }} {{ define "main" }}
- {{ with blog.Photos.Title }}

{{ . }}

{{ end }} - {{ with blog.Photos.Description }}{{ md . }}{{ end }} - {{ if (or blog.Photos.Title blog.Photos.Description) }} + {{ $blog := (blog .Blog) }} + {{ with $blog.Photos.Title }}

{{ . }}

{{ end }} + {{ with $blog.Photos.Description }}{{ md . }}{{ end }} + {{ if (or $blog.Photos.Title $blog.Photos.Description) }}
{{ end }} {{ range $i, $post := .Posts }} diff --git a/templates/photosummary.gohtml b/templates/photosummary.gohtml index 772a82c..dee12b4 100644 --- a/templates/photosummary.gohtml +++ b/templates/photosummary.gohtml @@ -2,7 +2,7 @@
{{ with p . "title" }}

{{ . }}

{{ end }} {{ with .Published }}

{{ dateformat . "02. Jan 2006" }}

{{ end }} - {{ range $i, $photo := ( ps . blog.Photos.Parameter ) }} + {{ range $i, $photo := ( ps . (blog .Blog).Photos.Parameter ) }} {{ md ( printf "![](%s)" $photo ) }} {{ end }}

{{ summary . }}

diff --git a/templates/post.gohtml b/templates/post.gohtml index d43834c..5f2176c 100644 --- a/templates/post.gohtml +++ b/templates/post.gohtml @@ -1,5 +1,5 @@ {{ define "title" }} - {{ with p . "title" }}{{ . }} - {{end}}{{ blog.Title }} + {{ with p . "title" }}{{ . }} - {{end}}{{ (blog .Blog).Title }} {{ end }} {{ define "main" }} @@ -14,7 +14,7 @@
{{ md . }}
{{ end }}
- {{ $taxonomies := blog.Taxonomies }} + {{ $taxonomies := (blog .Blog).Taxonomies }} {{ $post := . }} {{ range $i, $tax := $taxonomies }} {{ $tvs := ps $post $tax.Name }} diff --git a/templates/redirect.gohtml b/templates/redirect.gohtml index d5564dc..c8c7c39 100644 --- a/templates/redirect.gohtml +++ b/templates/redirect.gohtml @@ -1,6 +1,6 @@ {{ define "redirect" }} - + {{ .Permalink }} diff --git a/templates/taxonomy.gohtml b/templates/taxonomy.gohtml index ea5d1eb..6c055e4 100644 --- a/templates/taxonomy.gohtml +++ b/templates/taxonomy.gohtml @@ -1,5 +1,5 @@ {{ define "title" }} - {{ .Taxonomy.Title }} - {{ blog.Title }} + {{ .Taxonomy.Title }} - {{ (blog .Blog).Title }} {{ end }} {{ define "main" }} diff --git a/utils.go b/utils.go index b82620d..6eaee94 100644 --- a/utils.go +++ b/utils.go @@ -1,8 +1,10 @@ package main import ( + "math/rand" "sort" "strings" + "time" ) func urlize(str string) string { @@ -20,7 +22,7 @@ func urlize(str string) string { func firstSentences(value string, count int) string { for i := range value { if value[i] == '.' || value[i] == '!' || value[i] == '?' { - count -= 1 + count-- if count == 0 && i < len(value) { return value[0 : i+1] } @@ -35,3 +37,13 @@ func sortedStrings(s []string) []string { }) return s } + +func generateRandomString(now time.Time, n int) string { + rand.Seed(now.UnixNano()) + letters := []rune("abcdefghijklmnopqrstuvwxyz") + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +}