Various refactorings

This commit is contained in:
Jan-Lukas Else 2021-06-23 19:20:50 +02:00
parent 8db544150d
commit 6bc70f0a0e
11 changed files with 262 additions and 212 deletions

View File

@ -82,22 +82,23 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, "Resource not found", http.StatusNotFound) a.serveError(w, r, "Resource not found", http.StatusNotFound)
return return
} }
apIri := a.apIri(blog)
b, _ := json.Marshal(map[string]interface{}{ b, _ := json.Marshal(map[string]interface{}{
"subject": a.webfingerAccts[a.apIri(blog)], "subject": a.webfingerAccts[apIri],
"aliases": []string{ "aliases": []string{
a.webfingerAccts[a.apIri(blog)], a.webfingerAccts[apIri],
a.apIri(blog), apIri,
}, },
"links": []map[string]string{ "links": []map[string]string{
{ {
"rel": "self", "rel": "self",
"type": contenttype.AS, "type": contenttype.AS,
"href": a.apIri(blog), "href": apIri,
}, },
{ {
"rel": "http://webfinger.net/rel/profile-page", "rel": "http://webfinger.net/rel/profile-page",
"type": "text/html", "type": "text/html",
"href": a.apIri(blog), "href": apIri,
}, },
}, },
}) })
@ -107,8 +108,8 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) { func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
blogName := chi.URLParam(r, "blog") blogName := chi.URLParam(r, "blog")
blog := a.cfg.Blogs[blogName] blog, ok := a.cfg.Blogs[blogName]
if blog == nil { if !ok || blog == nil {
a.serveError(w, r, "Inbox not found", http.StatusNotFound) a.serveError(w, r, "Inbox not found", http.StatusNotFound)
return return
} }
@ -159,31 +160,28 @@ func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
case "Undo": case "Undo":
{ {
if object, ok := activity["object"].(map[string]interface{}); ok { if object, ok := activity["object"].(map[string]interface{}); ok {
if objectType, ok := object["type"].(string); ok && objectType == "Follow" { ot := cast.ToString(object["type"])
if iri, ok := object["actor"].(string); ok && iri == activityActor { actor := cast.ToString(object["actor"])
_ = a.db.apRemoveFollower(blogName, activityActor) if ot == "Follow" && actor == activityActor {
} _ = a.db.apRemoveFollower(blogName, activityActor)
} }
} }
} }
case "Create": case "Create":
{ {
if object, ok := activity["object"].(map[string]interface{}); ok { if object, ok := activity["object"].(map[string]interface{}); ok {
inReplyTo := cast.ToString(object["inReplyTo"]) baseUrl := cast.ToString(object["id"])
objectId := cast.ToString(object["id"]) if ou := cast.ToString(object["url"]); ou != "" {
objectUrl := cast.ToString(object["url"]) baseUrl = ou
baseUrl := objectId
if objectUrl != "" {
baseUrl = objectUrl
} }
if inReplyTo != "" && objectId != "" && strings.Contains(inReplyTo, blogIri) { if r := cast.ToString(object["inReplyTo"]); r != "" && baseUrl != "" && strings.HasPrefix(r, blogIri) {
// It's an ActivityPub reply; save reply as webmention // It's an ActivityPub reply; save reply as webmention
_ = a.createWebmention(baseUrl, inReplyTo) _ = a.createWebmention(baseUrl, r)
} else if content, hasContent := object["content"].(string); hasContent && objectId != "" { } else if content := cast.ToString(object["content"]); content != "" && baseUrl != "" {
// May be a mention; find links to blog and save them as webmentions // May be a mention; find links to blog and save them as webmentions
if links, err := allLinksFromHTMLString(content, baseUrl); err == nil { if links, err := allLinksFromHTMLString(content, baseUrl); err == nil {
for _, link := range links { for _, link := range links {
if strings.Contains(link, blogIri) { if strings.HasPrefix(link, blogIri) {
_ = a.createWebmention(baseUrl, link) _ = a.createWebmention(baseUrl, link)
} }
} }
@ -191,25 +189,22 @@ func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
} }
} }
} }
case "Delete": case "Delete", "Block":
case "Block":
{ {
if object, ok := activity["object"].(string); ok && len(object) > 0 && object == activityActor { if o := cast.ToString(activity["object"]); o == activityActor {
_ = a.db.apRemoveFollower(blogName, activityActor) _ = a.db.apRemoveFollower(blogName, activityActor)
} }
} }
case "Like": case "Like":
{ {
likeObject, likeObjectOk := activity["object"].(string) if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) {
if likeObjectOk && len(likeObject) > 0 && strings.Contains(likeObject, blogIri) { a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, o))
a.sendNotification(fmt.Sprintf("%s liked %s", activityActor, likeObject))
} }
} }
case "Announce": case "Announce":
{ {
announceObject, announceObjectOk := activity["object"].(string) if o := cast.ToString(activity["object"]); o != "" && strings.HasPrefix(o, blogIri) {
if announceObjectOk && len(announceObject) > 0 && strings.Contains(announceObject, blogIri) { a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, o))
a.sendNotification(fmt.Sprintf("%s announced %s", activityActor, announceObject))
} }
} }
} }
@ -230,11 +225,11 @@ func (a *goBlog) apVerifySignature(r *http.Request) (*asPerson, string, int, err
return nil, keyID, statusCode, err return nil, keyID, statusCode, err
} }
if actor.PublicKey == nil || actor.PublicKey.PublicKeyPem == "" { if actor.PublicKey == nil || actor.PublicKey.PublicKeyPem == "" {
return nil, keyID, 0, errors.New("Actor has no public key") return nil, keyID, 0, errors.New("actor has no public key")
} }
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem)) block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
if block == nil { if block == nil {
return nil, keyID, 0, errors.New("Public key invalid") return nil, keyID, 0, errors.New("public key invalid")
} }
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil { if err != nil {

View File

@ -32,7 +32,7 @@ func Test_captchaMiddleware(t *testing.T) {
_ = app.initRendering() _ = app.initRendering()
h := app.captchaMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { h := app.captchaMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("ABC Test")) _, _ = rw.Write([]byte("ABC Test"))
})) }))
t.Run("Default", func(t *testing.T) { t.Run("Default", func(t *testing.T) {
@ -58,7 +58,7 @@ func Test_captchaMiddleware(t *testing.T) {
session, _ := app.captchaSessions.Get(req, "c") session, _ := app.captchaSessions.Get(req, "c")
session.Values["captcha"] = true session.Values["captcha"] = true
session.Save(req, rec1) _ = session.Save(req, rec1)
for _, cookie := range rec1.Result().Cookies() { for _, cookie := range rec1.Result().Cookies() {
req.AddCookie(cookie) req.AddCookie(cookie)

View File

@ -50,7 +50,7 @@ func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
BlogString: blog, BlogString: blog,
Data: map[string]interface{}{ Data: map[string]interface{}{
"UpdatePostURL": parsedURL.String(), "UpdatePostURL": parsedURL.String(),
"UpdatePostContent": a.toMfItem(post).Properties.Content[0], "UpdatePostContent": a.postToMfItem(post).Properties.Content[0],
"Drafts": a.db.getDrafts(blog), "Drafts": a.db.getDrafts(blog),
}, },
}) })
@ -77,6 +77,14 @@ func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
a.editorMicropubPost(w, req, false) a.editorMicropubPost(w, req, false)
case "upload": case "upload":
a.editorMicropubPost(w, r, true) a.editorMicropubPost(w, r, true)
case "viewdraft":
parsedURL, err := url.Parse(r.FormValue("url"))
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
http.Redirect(w, r, parsedURL.Path, http.StatusFound)
return
default: default:
a.serveError(w, r, "Unknown editoraction", http.StatusBadRequest) a.serveError(w, r, "Unknown editoraction", http.StatusBadRequest)
} }

View File

@ -36,7 +36,7 @@ func (c *fakeHttpClient) setHandler(handler http.Handler) {
func (c *fakeHttpClient) setFakeResponse(statusCode int, body string) { func (c *fakeHttpClient) setFakeResponse(statusCode int, body string) {
c.setHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { c.setHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(statusCode) rw.WriteHeader(statusCode)
rw.Write([]byte(body)) _, _ = rw.Write([]byte(body))
})) }))
} }

View File

@ -32,7 +32,7 @@ func Test_compress(t *testing.T) {
assert.Equal(t, "https://www.cloudflare.com/cdn-cgi/image/f=jpeg,q=75,metadata=none,fit=scale-down,w=2000,h=3000/https://example.com/original.jpg", r.URL.String()) assert.Equal(t, "https://www.cloudflare.com/cdn-cgi/image/f=jpeg,q=75,metadata=none,fit=scale-down,w=2000,h=3000/https://example.com/original.jpg", r.URL.String())
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte(fakeFileContent)) _, _ = rw.Write([]byte(fakeFileContent))
})) }))
cf := &cloudflare{} cf := &cloudflare{}
@ -59,7 +59,7 @@ func Test_compress(t *testing.T) {
assert.Equal(t, "https://example.com/original.jpg", requestJson["url"]) assert.Equal(t, "https://example.com/original.jpg", requestJson["url"])
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte(fakeFileContent)) _, _ = rw.Write([]byte(fakeFileContent))
})) }))
cf := &shortpixel{"testkey"} cf := &shortpixel{"testkey"}

View File

@ -40,10 +40,13 @@ type mediaStorage interface {
type localMediaStorage struct { type localMediaStorage struct {
mediaURL string // optional mediaURL string // optional
path string // required
} }
func (a *goBlog) initLocalMediaStorage() mediaStorage { func (a *goBlog) initLocalMediaStorage() mediaStorage {
ms := &localMediaStorage{} ms := &localMediaStorage{
path: mediaFilePath,
}
if config := a.cfg.Micropub.MediaStorage; config != nil && config.MediaURL != "" { if config := a.cfg.Micropub.MediaStorage; config != nil && config.MediaURL != "" {
ms.mediaURL = config.MediaURL ms.mediaURL = config.MediaURL
} }
@ -51,10 +54,10 @@ func (a *goBlog) initLocalMediaStorage() mediaStorage {
} }
func (l *localMediaStorage) save(filename string, file io.Reader) (location string, err error) { func (l *localMediaStorage) save(filename string, file io.Reader) (location string, err error) {
if err = os.MkdirAll(mediaFilePath, 0644); err != nil { if err = os.MkdirAll(l.path, 0644); err != nil {
return "", err return "", err
} }
newFile, err := os.Create(filepath.Join(mediaFilePath, filename)) newFile, err := os.Create(filepath.Join(l.path, filename))
if err != nil { if err != nil {
return "", err return "", err
} }

30
mediaStorage_test.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_localMediaStorage(t *testing.T) {
testDir := t.TempDir()
l := &localMediaStorage{
mediaURL: "https://example.com",
path: testDir,
}
testFileContent := "This is a test"
loc, err := l.save("test.txt", strings.NewReader(testFileContent))
require.Nil(t, err)
assert.Equal(t, "https://example.com/test.txt", loc)
file, err := os.ReadFile(filepath.Join(testDir, "test.txt"))
require.Nil(t, err)
assert.Equal(t, testFileContent, string(file))
}

View File

@ -3,7 +3,8 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "io"
"mime"
"net/http" "net/http"
"net/url" "net/url"
"reflect" "reflect"
@ -45,7 +46,7 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, err.Error(), http.StatusBadRequest) a.serveError(w, r, err.Error(), http.StatusBadRequest)
return return
} }
mf = a.toMfItem(p) mf = a.postToMfItem(p)
} else { } else {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
@ -59,7 +60,7 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
} }
list := map[string][]*microformatItem{} list := map[string][]*microformatItem{}
for _, p := range posts { for _, p := range posts {
list["items"] = append(list["items"], a.toMfItem(p)) list["items"] = append(list["items"], a.postToMfItem(p))
} }
mf = list mf = list
} }
@ -88,138 +89,73 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
} }
} }
func (a *goBlog) toMfItem(p *post) *microformatItem {
params := p.Parameters
params["path"] = []string{p.Path}
params["section"] = []string{p.Section}
params["blog"] = []string{p.Blog}
params["published"] = []string{p.Published}
params["updated"] = []string{p.Updated}
params["status"] = []string{string(p.Status)}
pb, _ := yaml.Marshal(p.Parameters)
content := fmt.Sprintf("---\n%s---\n%s", string(pb), p.Content)
return &microformatItem{
Type: []string{"h-entry"},
Properties: &microformatProperties{
Name: p.Parameters["title"],
Published: []string{p.Published},
Updated: []string{p.Updated},
PostStatus: []string{string(p.Status)},
Category: p.Parameters[a.cfg.Micropub.CategoryParam],
Content: []string{content},
URL: []string{a.fullPostURL(p)},
InReplyTo: p.Parameters[a.cfg.Micropub.ReplyParam],
LikeOf: p.Parameters[a.cfg.Micropub.LikeParam],
BookmarkOf: p.Parameters[a.cfg.Micropub.BookmarkParam],
MpSlug: []string{p.Slug},
Audio: p.Parameters[a.cfg.Micropub.AudioParam],
// TODO: Photos
},
}
}
func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) { func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
var p *post switch mt, _, _ := mime.ParseMediaType(r.Header.Get(contentType)); mt {
if ct := r.Header.Get(contentType); strings.Contains(ct, contenttype.WWWForm) || strings.Contains(ct, contenttype.MultipartForm) { case contenttype.WWWForm, contenttype.MultipartForm:
var err error _ = r.ParseForm()
if strings.Contains(ct, contenttype.MultipartForm) { _ = r.ParseMultipartForm(0)
err = r.ParseMultipartForm(0) if r.Form == nil {
} else { a.serveError(w, r, "Failed to parse form", http.StatusBadRequest)
err = r.ParseForm()
}
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return return
} }
if action := micropubAction(r.Form.Get("action")); action != "" { if action := micropubAction(r.Form.Get("action")); action != "" {
u, err := url.Parse(r.Form.Get("url")) switch action {
if err != nil { case actionDelete:
a.serveError(w, r, err.Error(), http.StatusBadRequest) a.micropubDelete(w, r, r.Form.Get("url"))
return default:
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
} }
if action == actionDelete {
a.micropubDelete(w, r, u)
return
}
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
return return
} }
p, err = a.convertMPValueMapToPost(r.Form) a.micropubCreatePostFromForm(w, r)
if err != nil { case contenttype.JSON:
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
} else if strings.Contains(ct, contenttype.JSON) {
parsedMfItem := &microformatItem{} parsedMfItem := &microformatItem{}
err := json.NewDecoder(r.Body).Decode(parsedMfItem) b, _ := io.ReadAll(io.LimitReader(r.Body, 10000000)) // 10 MB
if err != nil { if err := json.Unmarshal(b, parsedMfItem); err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError) a.serveError(w, r, err.Error(), http.StatusBadRequest)
return return
} }
if parsedMfItem.Action != "" { if parsedMfItem.Action != "" {
u, err := url.Parse(parsedMfItem.URL) switch parsedMfItem.Action {
if err != nil { case actionDelete:
a.serveError(w, r, err.Error(), http.StatusBadRequest) a.micropubDelete(w, r, parsedMfItem.URL)
return case actionUpdate:
a.micropubUpdate(w, r, parsedMfItem.URL, parsedMfItem)
default:
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
} }
if parsedMfItem.Action == actionDelete {
a.micropubDelete(w, r, u)
return
}
if parsedMfItem.Action == actionUpdate {
a.micropubUpdate(w, r, u, parsedMfItem)
return
}
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
return return
} }
p, err = a.convertMPMfToPost(parsedMfItem) a.micropubCreatePostFromJson(w, r, parsedMfItem)
if err != nil { default:
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
} else {
a.serveError(w, r, "wrong content type", http.StatusBadRequest) a.serveError(w, r, "wrong content type", http.StatusBadRequest)
return
} }
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "create") {
a.serveError(w, r, "create scope missing", http.StatusForbidden)
return
}
err := a.createPost(p)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, a.fullPostURL(p), http.StatusAccepted)
} }
func (a *goBlog) convertMPValueMapToPost(values map[string][]string) (*post, error) { func (a *goBlog) micropubParseValuePostParamsValueMap(entry *post, values map[string][]string) error {
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") { if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
return nil, errors.New("only entry type is supported so far") return errors.New("only entry type is supported so far")
} }
delete(values, "h") delete(values, "h")
entry := &post{ entry.Parameters = map[string][]string{}
Parameters: map[string][]string{}, if content, ok := values["content"]; ok && len(content) > 0 {
}
if content, ok := values["content"]; ok {
entry.Content = content[0] entry.Content = content[0]
delete(values, "content") delete(values, "content")
} }
if published, ok := values["published"]; ok { if published, ok := values["published"]; ok && len(published) > 0 {
entry.Published = published[0] entry.Published = published[0]
delete(values, "published") delete(values, "published")
} }
if updated, ok := values["updated"]; ok { if updated, ok := values["updated"]; ok && len(updated) > 0 {
entry.Updated = updated[0] entry.Updated = updated[0]
delete(values, "updated") delete(values, "updated")
} }
if status, ok := values["post-status"]; ok { if status, ok := values["post-status"]; ok && len(status) > 0 {
entry.Status = postStatus(status[0]) entry.Status = postStatus(status[0])
delete(values, "post-status") delete(values, "post-status")
} }
if slug, ok := values["mp-slug"]; ok { if slug, ok := values["mp-slug"]; ok && len(slug) > 0 {
entry.Slug = slug[0] entry.Slug = slug[0]
delete(values, "mp-slug") delete(values, "mp-slug")
} }
@ -275,11 +211,7 @@ func (a *goBlog) convertMPValueMapToPost(values map[string][]string) (*post, err
for n, p := range values { for n, p := range values {
entry.Parameters[n] = append(entry.Parameters[n], p...) entry.Parameters[n] = append(entry.Parameters[n], p...)
} }
err := a.computeExtraPostParameters(entry) return nil
if err != nil {
return nil, err
}
return entry, nil
} }
type micropubAction string type micropubAction string
@ -315,40 +247,44 @@ type microformatProperties struct {
Audio []string `json:"audio,omitempty"` Audio []string `json:"audio,omitempty"`
} }
func (a *goBlog) convertMPMfToPost(mf *microformatItem) (*post, error) { func (a *goBlog) micropubParsePostParamsMfItem(entry *post, mf *microformatItem) error {
if len(mf.Type) != 1 || mf.Type[0] != "h-entry" { if len(mf.Type) != 1 || mf.Type[0] != "h-entry" {
return nil, errors.New("only entry type is supported so far") return errors.New("only entry type is supported so far")
} }
entry := &post{ entry.Parameters = map[string][]string{}
Parameters: map[string][]string{}, if mf.Properties == nil {
return nil
} }
// Content // Content
if mf.Properties != nil && len(mf.Properties.Content) == 1 && len(mf.Properties.Content[0]) > 0 { if len(mf.Properties.Content) > 0 && mf.Properties.Content[0] != "" {
entry.Content = mf.Properties.Content[0] entry.Content = mf.Properties.Content[0]
} }
if len(mf.Properties.Published) == 1 { if len(mf.Properties.Published) > 0 {
entry.Published = mf.Properties.Published[0] entry.Published = mf.Properties.Published[0]
} }
if len(mf.Properties.Updated) == 1 { if len(mf.Properties.Updated) > 0 {
entry.Updated = mf.Properties.Updated[0] entry.Updated = mf.Properties.Updated[0]
} }
if len(mf.Properties.PostStatus) == 1 { if len(mf.Properties.PostStatus) > 0 {
entry.Status = postStatus(mf.Properties.PostStatus[0]) entry.Status = postStatus(mf.Properties.PostStatus[0])
} }
if len(mf.Properties.MpSlug) > 0 {
entry.Slug = mf.Properties.MpSlug[0]
}
// Parameter // Parameter
if len(mf.Properties.Name) == 1 { if len(mf.Properties.Name) > 0 {
entry.Parameters["title"] = mf.Properties.Name entry.Parameters["title"] = mf.Properties.Name
} }
if len(mf.Properties.Category) > 0 { if len(mf.Properties.Category) > 0 {
entry.Parameters[a.cfg.Micropub.CategoryParam] = mf.Properties.Category entry.Parameters[a.cfg.Micropub.CategoryParam] = mf.Properties.Category
} }
if len(mf.Properties.InReplyTo) == 1 { if len(mf.Properties.InReplyTo) > 0 {
entry.Parameters[a.cfg.Micropub.ReplyParam] = mf.Properties.InReplyTo entry.Parameters[a.cfg.Micropub.ReplyParam] = mf.Properties.InReplyTo
} }
if len(mf.Properties.LikeOf) == 1 { if len(mf.Properties.LikeOf) > 0 {
entry.Parameters[a.cfg.Micropub.LikeParam] = mf.Properties.LikeOf entry.Parameters[a.cfg.Micropub.LikeParam] = mf.Properties.LikeOf
} }
if len(mf.Properties.BookmarkOf) == 1 { if len(mf.Properties.BookmarkOf) > 0 {
entry.Parameters[a.cfg.Micropub.BookmarkParam] = mf.Properties.BookmarkOf entry.Parameters[a.cfg.Micropub.BookmarkParam] = mf.Properties.BookmarkOf
} }
if len(mf.Properties.Audio) > 0 { if len(mf.Properties.Audio) > 0 {
@ -365,15 +301,7 @@ func (a *goBlog) convertMPMfToPost(mf *microformatItem) (*post, error) {
} }
} }
} }
if len(mf.Properties.MpSlug) == 1 { return nil
entry.Slug = mf.Properties.MpSlug[0]
}
err := a.computeExtraPostParameters(entry)
if err != nil {
return nil, err
}
return entry, nil
} }
func (a *goBlog) computeExtraPostParameters(p *post) error { func (a *goBlog) computeExtraPostParameters(p *post) error {
@ -456,24 +384,70 @@ func (a *goBlog) computeExtraPostParameters(p *post) error {
return nil return nil
} }
func (a *goBlog) micropubDelete(w http.ResponseWriter, r *http.Request, u *url.URL) { func (a *goBlog) micropubCreatePostFromForm(w http.ResponseWriter, r *http.Request) {
p := &post{}
err := a.micropubParseValuePostParamsValueMap(p, r.Form)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
a.micropubCreate(w, r, p)
}
func (a *goBlog) micropubCreatePostFromJson(w http.ResponseWriter, r *http.Request, parsedMfItem *microformatItem) {
p := &post{}
err := a.micropubParsePostParamsMfItem(p, parsedMfItem)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
a.micropubCreate(w, r, p)
}
func (a *goBlog) micropubCreate(w http.ResponseWriter, r *http.Request, p *post) {
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "create") {
a.serveError(w, r, "create scope missing", http.StatusForbidden)
return
}
if err := a.computeExtraPostParameters(p); err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
if err := a.createPost(p); err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
http.Redirect(w, r, a.fullPostURL(p), http.StatusAccepted)
}
func (a *goBlog) micropubDelete(w http.ResponseWriter, r *http.Request, u string) {
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "delete") { if !strings.Contains(r.Context().Value(indieAuthScope).(string), "delete") {
a.serveError(w, r, "delete scope missing", http.StatusForbidden) a.serveError(w, r, "delete scope missing", http.StatusForbidden)
return return
} }
if err := a.deletePost(u.Path); err != nil { uu, err := url.Parse(u)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest) a.serveError(w, r, err.Error(), http.StatusBadRequest)
return return
} }
http.Redirect(w, r, u.String(), http.StatusNoContent) if err := a.deletePost(uu.Path); err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
http.Redirect(w, r, uu.String(), http.StatusNoContent)
} }
func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *microformatItem) { func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string, mf *microformatItem) {
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") { if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") {
a.serveError(w, r, "update scope missing", http.StatusForbidden) a.serveError(w, r, "update scope missing", http.StatusForbidden)
return return
} }
p, err := a.db.getPost(u.Path) uu, err := url.Parse(u)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
p, err := a.db.getPost(uu.Path)
if err != nil { if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest) a.serveError(w, r, err.Error(), http.StatusBadRequest)
return return

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"html/template" "html/template"
"log" "log"
"strings" "strings"
@ -9,6 +10,7 @@ import (
gogeouri "git.jlel.se/jlelse/go-geouri" gogeouri "git.jlel.se/jlelse/go-geouri"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/araddon/dateparse" "github.com/araddon/dateparse"
"gopkg.in/yaml.v3"
) )
func (a *goBlog) fullPostURL(p *post) string { func (a *goBlog) fullPostURL(p *post) string {
@ -117,6 +119,36 @@ func (p *post) isPublishedSectionPost() bool {
return p.Published != "" && p.Section != "" && p.Status == statusPublished return p.Published != "" && p.Section != "" && p.Status == statusPublished
} }
func (a *goBlog) postToMfItem(p *post) *microformatItem {
params := p.Parameters
params["path"] = []string{p.Path}
params["section"] = []string{p.Section}
params["blog"] = []string{p.Blog}
params["published"] = []string{p.Published}
params["updated"] = []string{p.Updated}
params["status"] = []string{string(p.Status)}
pb, _ := yaml.Marshal(p.Parameters)
content := fmt.Sprintf("---\n%s---\n%s", string(pb), p.Content)
return &microformatItem{
Type: []string{"h-entry"},
Properties: &microformatProperties{
Name: p.Parameters["title"],
Published: []string{p.Published},
Updated: []string{p.Updated},
PostStatus: []string{string(p.Status)},
Category: p.Parameters[a.cfg.Micropub.CategoryParam],
Content: []string{content},
URL: []string{a.fullPostURL(p)},
InReplyTo: p.Parameters[a.cfg.Micropub.ReplyParam],
LikeOf: p.Parameters[a.cfg.Micropub.LikeParam],
BookmarkOf: p.Parameters[a.cfg.Micropub.BookmarkParam],
MpSlug: []string{p.Slug},
Audio: p.Parameters[a.cfg.Micropub.AudioParam],
// TODO: Photos
},
}
}
// Public because of rendering // Public because of rendering
func (p *post) Title() string { func (p *post) Title() string {

View File

@ -112,42 +112,9 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, template string, data *renderData) { func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, template string, data *renderData) {
// Server timing // Server timing
t := servertiming.FromContext(r.Context()).NewMetric("r").Start() t := servertiming.FromContext(r.Context()).NewMetric("r").Start()
defer t.Stop()
// Check render data // Check render data
if data.User == nil { a.checkRenderData(r, data)
data.User = a.cfg.User
}
if data.Blog == nil {
if len(data.BlogString) == 0 {
data.BlogString = a.cfg.DefaultBlog
}
data.Blog = a.cfg.Blogs[data.BlogString]
}
if data.BlogString == "" {
for s, b := range a.cfg.Blogs {
if b == data.Blog {
data.BlogString = s
break
}
}
}
if a.cfg.Server.Tor && a.torAddress != "" {
data.TorAddress = fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI)
}
if data.Data == nil {
data.Data = map[string]interface{}{}
}
// Check login
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
data.LoggedIn = true
}
// Check if comments enabled
data.CommentsEnabled = data.Blog.Comments != nil && data.Blog.Comments.Enabled
// Check if able to receive webmentions
data.WebmentionReceivingEnabled = a.cfg.Webmention == nil || !a.cfg.Webmention.DisableReceiving
// Check if Tor request
if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed {
data.TorUsed = true
}
// Set content type // Set content type
w.Header().Set(contentType, contenttype.HTMLUTF8) w.Header().Set(contentType, contenttype.HTMLUTF8)
// Minify and write response // Minify and write response
@ -163,8 +130,47 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// Server timing }
t.Stop()
func (a *goBlog) checkRenderData(r *http.Request, data *renderData) {
// User
if data.User == nil {
data.User = a.cfg.User
}
// Blog
if data.Blog == nil {
if data.BlogString == "" {
data.BlogString = a.cfg.DefaultBlog
}
data.Blog = a.cfg.Blogs[data.BlogString]
}
if data.BlogString == "" {
for s, b := range a.cfg.Blogs {
if b == data.Blog {
data.BlogString = s
break
}
}
}
// Tor
if a.cfg.Server.Tor && a.torAddress != "" {
data.TorAddress = fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI)
}
if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed {
data.TorUsed = true
}
// Check login
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
data.LoggedIn = true
}
// Check if comments enabled
data.CommentsEnabled = data.Blog.Comments != nil && data.Blog.Comments.Enabled
// Check if able to receive webmentions
data.WebmentionReceivingEnabled = a.cfg.Webmention == nil || !a.cfg.Webmention.DisableReceiving
// Data
if data.Data == nil {
data.Data = map[string]interface{}{}
}
} }
func (a *goBlog) includeRenderedTemplate(templateName string, data ...interface{}) (template.HTML, error) { func (a *goBlog) includeRenderedTemplate(templateName string, data ...interface{}) (template.HTML, error) {

View File

@ -50,16 +50,18 @@ tags:
<input class="fw" type="file" name="file"> <input class="fw" type="file" name="file">
<input type="submit" value="{{ string .Blog.Lang "upload" }}"> <input type="submit" value="{{ string .Blog.Lang "upload" }}">
</form> </form>
{{ if .Data.Drafts }}
<h2>{{ string .Blog.Lang "drafts" }}</h2> <h2>{{ string .Blog.Lang "drafts" }}</h2>
<form class="fw-form p" method="post"> <form class="fw-form p" method="post">
<input type="hidden" name="editoraction" value="loadupdate"> <input type="hidden" name="editoraction" value="viewdraft">
<select name="url" class="fw"> <select name="url" class="fw">
{{ range $i, $draft := .Data.Drafts }} {{ range $i, $draft := .Data.Drafts }}
<option value="{{ absolute $draft.Path }}">{{ with ($draft.Title) }}{{ . }}{{ else }}{{ $draft.Path }}{{ end }}</option> <option value="{{ absolute $draft.Path }}">{{ with ($draft.Title) }}{{ . }}{{ else }}{{ $draft.Path }}{{ end }}</option>
{{ end }} {{ end }}
</select> </select>
<input type="submit" value="{{ string .Blog.Lang "update" }}"> <input type="submit" value="{{ string .Blog.Lang "view" }}">
</form> </form>
{{ end }}
<h2>{{ string .Blog.Lang "location" }}</h2> <h2>{{ string .Blog.Lang "location" }}</h2>
<form class="fw-form p"> <form class="fw-form p">
<input id="geobtn" type="button" class="fw" value="{{ string .Blog.Lang "locationget" }}" data-failed="{{ string .Blog.Lang "locationfailed" }}" data-notsupported="{{ string .Blog.Lang "locationnotsupported" }}"> <input id="geobtn" type="button" class="fw" value="{{ string .Blog.Lang "locationget" }}" data-failed="{{ string .Blog.Lang "locationfailed" }}" data-notsupported="{{ string .Blog.Lang "locationnotsupported" }}">