2020-10-06 17:07:48 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-01-21 16:59:47 +00:00
|
|
|
"encoding/json"
|
2020-10-06 17:07:48 +00:00
|
|
|
"errors"
|
2021-06-23 17:20:50 +00:00
|
|
|
"io"
|
|
|
|
"mime"
|
2020-10-06 17:07:48 +00:00
|
|
|
"net/http"
|
2020-10-14 16:23:56 +00:00
|
|
|
"net/url"
|
|
|
|
"regexp"
|
2020-11-12 10:48:37 +00:00
|
|
|
"strconv"
|
2020-10-06 17:07:48 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/spf13/cast"
|
2021-07-31 11:20:51 +00:00
|
|
|
"github.com/thoas/go-funk"
|
2021-06-28 20:17:18 +00:00
|
|
|
"go.goblog.app/app/pkgs/contenttype"
|
2020-10-06 17:07:48 +00:00
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
)
|
|
|
|
|
2020-10-14 16:23:56 +00:00
|
|
|
const micropubPath = "/micropub"
|
2020-10-06 17:07:48 +00:00
|
|
|
|
|
|
|
type micropubConfig struct {
|
|
|
|
MediaEndpoint string `json:"media-endpoint,omitempty"`
|
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
2020-10-14 16:23:56 +00:00
|
|
|
switch r.URL.Query().Get("q") {
|
|
|
|
case "config":
|
2021-06-18 12:32:03 +00:00
|
|
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
2020-10-06 17:07:48 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2021-02-16 15:26:21 +00:00
|
|
|
b, _ := json.Marshal(µpubConfig{
|
2021-06-10 20:09:50 +00:00
|
|
|
MediaEndpoint: a.getFullAddress(micropubPath + micropubMediaSubPath),
|
2021-01-10 14:59:43 +00:00
|
|
|
})
|
2021-06-18 12:32:03 +00:00
|
|
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
2020-10-14 16:23:56 +00:00
|
|
|
case "source":
|
|
|
|
var mf interface{}
|
|
|
|
if urlString := r.URL.Query().Get("url"); urlString != "" {
|
|
|
|
u, err := url.Parse(r.URL.Query().Get("url"))
|
|
|
|
if err != nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
2020-10-14 16:23:56 +00:00
|
|
|
return
|
|
|
|
}
|
2021-08-05 06:09:34 +00:00
|
|
|
p, err := a.getPost(u.Path)
|
2020-10-14 16:23:56 +00:00
|
|
|
if err != nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
2020-10-14 16:23:56 +00:00
|
|
|
return
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
mf = a.postToMfItem(p)
|
2020-10-14 16:23:56 +00:00
|
|
|
} else {
|
2020-11-12 10:48:37 +00:00
|
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
|
|
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
2021-08-05 06:09:34 +00:00
|
|
|
posts, err := a.getPosts(&postsRequestConfig{
|
2020-11-12 10:48:37 +00:00
|
|
|
limit: limit,
|
|
|
|
offset: offset,
|
|
|
|
})
|
2020-10-14 16:23:56 +00:00
|
|
|
if err != nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
2020-10-14 16:23:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
list := map[string][]*microformatItem{}
|
2020-10-15 15:32:46 +00:00
|
|
|
for _, p := range posts {
|
2021-06-23 17:20:50 +00:00
|
|
|
list["items"] = append(list["items"], a.postToMfItem(p))
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
|
|
|
mf = list
|
|
|
|
}
|
2021-06-18 12:32:03 +00:00
|
|
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
2020-10-06 17:07:48 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2021-02-16 15:26:21 +00:00
|
|
|
b, _ := json.Marshal(mf)
|
2021-06-18 12:32:03 +00:00
|
|
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
2020-11-12 11:44:54 +00:00
|
|
|
case "category":
|
|
|
|
allCategories := []string{}
|
2021-06-06 12:39:42 +00:00
|
|
|
for blog := range a.cfg.Blogs {
|
|
|
|
values, err := a.db.allTaxonomyValues(blog, a.cfg.Micropub.CategoryParam)
|
2020-11-12 11:44:54 +00:00
|
|
|
if err != nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
2020-11-12 11:44:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
allCategories = append(allCategories, values...)
|
|
|
|
}
|
2021-06-18 12:32:03 +00:00
|
|
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
2020-11-12 11:44:54 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2021-02-16 15:26:21 +00:00
|
|
|
b, _ := json.Marshal(map[string]interface{}{
|
2020-11-12 11:44:54 +00:00
|
|
|
"categories": allCategories,
|
|
|
|
})
|
2021-06-18 12:32:03 +00:00
|
|
|
_, _ = a.min.Write(w, contenttype.JSON, b)
|
2020-10-14 16:23:56 +00:00
|
|
|
default:
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serve404(w, r)
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
2020-10-06 17:07:48 +00:00
|
|
|
defer r.Body.Close()
|
2021-06-23 17:20:50 +00:00
|
|
|
switch mt, _, _ := mime.ParseMediaType(r.Header.Get(contentType)); mt {
|
|
|
|
case contenttype.WWWForm, contenttype.MultipartForm:
|
|
|
|
_ = r.ParseForm()
|
|
|
|
_ = r.ParseMultipartForm(0)
|
|
|
|
if r.Form == nil {
|
|
|
|
a.serveError(w, r, "Failed to parse form", http.StatusBadRequest)
|
2020-11-22 19:30:02 +00:00
|
|
|
return
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2020-10-14 16:23:56 +00:00
|
|
|
if action := micropubAction(r.Form.Get("action")); action != "" {
|
2021-06-23 17:20:50 +00:00
|
|
|
switch action {
|
|
|
|
case actionDelete:
|
|
|
|
a.micropubDelete(w, r, r.Form.Get("url"))
|
|
|
|
default:
|
|
|
|
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2020-10-06 17:07:48 +00:00
|
|
|
return
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
a.micropubCreatePostFromForm(w, r)
|
|
|
|
case contenttype.JSON:
|
2020-10-06 17:07:48 +00:00
|
|
|
parsedMfItem := µformatItem{}
|
2021-06-23 17:20:50 +00:00
|
|
|
b, _ := io.ReadAll(io.LimitReader(r.Body, 10000000)) // 10 MB
|
|
|
|
if err := json.Unmarshal(b, parsedMfItem); err != nil {
|
|
|
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
2020-10-06 17:07:48 +00:00
|
|
|
return
|
|
|
|
}
|
2020-10-14 16:23:56 +00:00
|
|
|
if parsedMfItem.Action != "" {
|
2021-06-23 17:20:50 +00:00
|
|
|
switch parsedMfItem.Action {
|
|
|
|
case actionDelete:
|
|
|
|
a.micropubDelete(w, r, parsedMfItem.URL)
|
|
|
|
case actionUpdate:
|
|
|
|
a.micropubUpdate(w, r, parsedMfItem.URL, parsedMfItem)
|
|
|
|
default:
|
|
|
|
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2020-10-06 17:07:48 +00:00
|
|
|
return
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
a.micropubCreatePostFromJson(w, r, parsedMfItem)
|
|
|
|
default:
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "wrong content type", http.StatusBadRequest)
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-23 17:20:50 +00:00
|
|
|
func (a *goBlog) micropubParseValuePostParamsValueMap(entry *post, values map[string][]string) error {
|
2020-10-06 17:07:48 +00:00
|
|
|
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
|
2021-06-23 17:20:50 +00:00
|
|
|
return errors.New("only entry type is supported so far")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "h")
|
2021-06-23 17:20:50 +00:00
|
|
|
entry.Parameters = map[string][]string{}
|
|
|
|
if content, ok := values["content"]; ok && len(content) > 0 {
|
2020-10-06 17:07:48 +00:00
|
|
|
entry.Content = content[0]
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "content")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if published, ok := values["published"]; ok && len(published) > 0 {
|
2020-10-12 17:54:22 +00:00
|
|
|
entry.Published = published[0]
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "published")
|
2020-10-12 17:54:22 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if updated, ok := values["updated"]; ok && len(updated) > 0 {
|
2020-10-12 17:54:22 +00:00
|
|
|
entry.Updated = updated[0]
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "updated")
|
2020-10-12 17:54:22 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if slug, ok := values["mp-slug"]; ok && len(slug) > 0 {
|
2021-05-25 20:17:38 +00:00
|
|
|
entry.Slug = slug[0]
|
|
|
|
delete(values, "mp-slug")
|
2021-01-15 20:56:46 +00:00
|
|
|
}
|
2021-07-14 16:50:24 +00:00
|
|
|
// Status
|
|
|
|
statusStr := ""
|
|
|
|
if status, ok := values["post-status"]; ok && len(status) > 0 {
|
|
|
|
statusStr = status[0]
|
|
|
|
delete(values, "post-status")
|
|
|
|
}
|
|
|
|
visibilityStr := ""
|
|
|
|
if visibility, ok := values["visibility"]; ok && len(visibility) > 0 {
|
|
|
|
visibilityStr = visibility[0]
|
|
|
|
delete(values, "visibility")
|
|
|
|
}
|
|
|
|
if finalStatus := micropubStatus(statusNil, statusStr, visibilityStr); finalStatus != statusNil {
|
|
|
|
entry.Status = finalStatus
|
|
|
|
}
|
2020-10-06 17:07:48 +00:00
|
|
|
// Parameter
|
|
|
|
if name, ok := values["name"]; ok {
|
|
|
|
entry.Parameters["title"] = name
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "name")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if category, ok := values["category"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.CategoryParam] = category
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "category")
|
2020-10-06 17:07:48 +00:00
|
|
|
} else if categories, ok := values["category[]"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.CategoryParam] = categories
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "category[]")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if inReplyTo, ok := values["in-reply-to"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.ReplyParam] = inReplyTo
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "in-reply-to")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if likeOf, ok := values["like-of"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.LikeParam] = likeOf
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "like-of")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if bookmarkOf, ok := values["bookmark-of"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.BookmarkParam] = bookmarkOf
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "bookmark-of")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if audio, ok := values["audio"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.AudioParam] = audio
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "audio")
|
2020-10-06 17:07:48 +00:00
|
|
|
} else if audio, ok := values["audio[]"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.AudioParam] = audio
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "audio[]")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if photo, ok := values["photo"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.PhotoParam] = photo
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "photo")
|
2020-10-06 17:07:48 +00:00
|
|
|
} else if photos, ok := values["photo[]"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.PhotoParam] = photos
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "photo[]")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if photoAlt, ok := values["mp-photo-alt"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam] = photoAlt
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "mp-photo-alt")
|
2020-10-06 17:07:48 +00:00
|
|
|
} else if photoAlts, ok := values["mp-photo-alt[]"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam] = photoAlts
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "mp-photo-alt[]")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-05-25 20:17:38 +00:00
|
|
|
if location, ok := values["location"]; ok {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.LocationParam] = location
|
2021-05-25 20:17:38 +00:00
|
|
|
delete(values, "location")
|
|
|
|
}
|
|
|
|
for n, p := range values {
|
|
|
|
entry.Parameters[n] = append(entry.Parameters[n], p...)
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
return nil
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
|
2020-10-14 16:23:56 +00:00
|
|
|
type micropubAction string
|
|
|
|
|
|
|
|
const (
|
|
|
|
actionUpdate micropubAction = "update"
|
2021-02-08 17:51:07 +00:00
|
|
|
actionDelete micropubAction = "delete"
|
2020-10-14 16:23:56 +00:00
|
|
|
)
|
|
|
|
|
2020-10-06 17:07:48 +00:00
|
|
|
type microformatItem struct {
|
2020-10-14 16:23:56 +00:00
|
|
|
Type []string `json:"type,omitempty"`
|
|
|
|
URL string `json:"url,omitempty"`
|
|
|
|
Action micropubAction `json:"action,omitempty"`
|
|
|
|
Properties *microformatProperties `json:"properties,omitempty"`
|
|
|
|
Replace map[string][]interface{} `json:"replace,omitempty"`
|
|
|
|
Add map[string][]interface{} `json:"add,omitempty"`
|
|
|
|
Delete interface{} `json:"delete,omitempty"`
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type microformatProperties struct {
|
2020-10-14 16:23:56 +00:00
|
|
|
Name []string `json:"name,omitempty"`
|
|
|
|
Published []string `json:"published,omitempty"`
|
|
|
|
Updated []string `json:"updated,omitempty"`
|
2021-01-15 20:56:46 +00:00
|
|
|
PostStatus []string `json:"post-status,omitempty"`
|
2021-07-14 16:50:24 +00:00
|
|
|
Visibility []string `json:"visibility,omitempty"`
|
2020-10-14 16:23:56 +00:00
|
|
|
Category []string `json:"category,omitempty"`
|
|
|
|
Content []string `json:"content,omitempty"`
|
|
|
|
URL []string `json:"url,omitempty"`
|
|
|
|
InReplyTo []string `json:"in-reply-to,omitempty"`
|
|
|
|
LikeOf []string `json:"like-of,omitempty"`
|
|
|
|
BookmarkOf []string `json:"bookmark-of,omitempty"`
|
|
|
|
MpSlug []string `json:"mp-slug,omitempty"`
|
|
|
|
Photo []interface{} `json:"photo,omitempty"`
|
|
|
|
Audio []string `json:"audio,omitempty"`
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
|
2021-06-23 17:20:50 +00:00
|
|
|
func (a *goBlog) micropubParsePostParamsMfItem(entry *post, mf *microformatItem) error {
|
2020-10-06 17:07:48 +00:00
|
|
|
if len(mf.Type) != 1 || mf.Type[0] != "h-entry" {
|
2021-06-23 17:20:50 +00:00
|
|
|
return errors.New("only entry type is supported so far")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
entry.Parameters = map[string][]string{}
|
|
|
|
if mf.Properties == nil {
|
|
|
|
return nil
|
2020-12-08 18:36:19 +00:00
|
|
|
}
|
2020-10-06 17:07:48 +00:00
|
|
|
// Content
|
2021-06-23 17:20:50 +00:00
|
|
|
if len(mf.Properties.Content) > 0 && mf.Properties.Content[0] != "" {
|
2020-10-06 17:07:48 +00:00
|
|
|
entry.Content = mf.Properties.Content[0]
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if len(mf.Properties.Published) > 0 {
|
2020-10-12 17:54:22 +00:00
|
|
|
entry.Published = mf.Properties.Published[0]
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if len(mf.Properties.Updated) > 0 {
|
2020-10-12 17:54:22 +00:00
|
|
|
entry.Updated = mf.Properties.Updated[0]
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if len(mf.Properties.MpSlug) > 0 {
|
|
|
|
entry.Slug = mf.Properties.MpSlug[0]
|
|
|
|
}
|
2021-07-14 16:50:24 +00:00
|
|
|
// Status
|
|
|
|
status := ""
|
|
|
|
if len(mf.Properties.PostStatus) > 0 {
|
|
|
|
status = mf.Properties.PostStatus[0]
|
|
|
|
}
|
|
|
|
visibility := ""
|
|
|
|
if len(mf.Properties.Visibility) > 0 {
|
|
|
|
visibility = mf.Properties.Visibility[0]
|
|
|
|
}
|
|
|
|
if finalStatus := micropubStatus(statusNil, status, visibility); finalStatus != statusNil {
|
|
|
|
entry.Status = finalStatus
|
|
|
|
}
|
2020-10-06 17:07:48 +00:00
|
|
|
// Parameter
|
2021-06-23 17:20:50 +00:00
|
|
|
if len(mf.Properties.Name) > 0 {
|
2020-10-06 17:07:48 +00:00
|
|
|
entry.Parameters["title"] = mf.Properties.Name
|
|
|
|
}
|
|
|
|
if len(mf.Properties.Category) > 0 {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.CategoryParam] = mf.Properties.Category
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if len(mf.Properties.InReplyTo) > 0 {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.ReplyParam] = mf.Properties.InReplyTo
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if len(mf.Properties.LikeOf) > 0 {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.LikeParam] = mf.Properties.LikeOf
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
if len(mf.Properties.BookmarkOf) > 0 {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.BookmarkParam] = mf.Properties.BookmarkOf
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if len(mf.Properties.Audio) > 0 {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.AudioParam] = mf.Properties.Audio
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
if len(mf.Properties.Photo) > 0 {
|
|
|
|
for _, photo := range mf.Properties.Photo {
|
|
|
|
if theString, justString := photo.(string); justString {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.PhotoParam] = append(entry.Parameters[a.cfg.Micropub.PhotoParam], theString)
|
|
|
|
entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam] = append(entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam], "")
|
2020-10-06 17:07:48 +00:00
|
|
|
} else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto {
|
2021-06-06 12:39:42 +00:00
|
|
|
entry.Parameters[a.cfg.Micropub.PhotoParam] = append(entry.Parameters[a.cfg.Micropub.PhotoParam], cast.ToString(thePhoto["value"]))
|
|
|
|
entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam] = append(entry.Parameters[a.cfg.Micropub.PhotoDescriptionParam], cast.ToString(thePhoto["alt"]))
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
return nil
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) computeExtraPostParameters(p *post) error {
|
2021-08-04 20:48:50 +00:00
|
|
|
if p.Parameters == nil {
|
|
|
|
p.Parameters = map[string][]string{}
|
|
|
|
}
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Content = regexp.MustCompile("\r\n").ReplaceAllString(p.Content, "\n")
|
|
|
|
if split := strings.Split(p.Content, "---\n"); len(split) >= 3 && len(strings.TrimSpace(split[0])) == 0 {
|
2020-10-06 17:07:48 +00:00
|
|
|
// 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 {
|
2020-10-14 16:23:56 +00:00
|
|
|
// Delete existing content - replace
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Parameters[key] = []string{}
|
2020-10-06 17:07:48 +00:00
|
|
|
if a, ok := value.([]interface{}); ok {
|
|
|
|
for _, ae := range a {
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Parameters[key] = append(p.Parameters[key], cast.ToString(ae))
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
} else {
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Parameters[key] = append(p.Parameters[key], cast.ToString(value))
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Remove frontmatter from content
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Content = strings.Join(split[2:], "---\n")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
// Check settings
|
2020-10-15 15:32:46 +00:00
|
|
|
if blog := p.Parameters["blog"]; len(blog) == 1 && blog[0] != "" {
|
|
|
|
p.Blog = blog[0]
|
|
|
|
delete(p.Parameters, "blog")
|
2020-10-06 17:07:48 +00:00
|
|
|
} else {
|
2021-06-06 12:39:42 +00:00
|
|
|
p.Blog = a.cfg.DefaultBlog
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-01-15 20:56:46 +00:00
|
|
|
if path := p.Parameters["path"]; len(path) == 1 {
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Path = path[0]
|
|
|
|
delete(p.Parameters, "path")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-01-15 20:56:46 +00:00
|
|
|
if section := p.Parameters["section"]; len(section) == 1 {
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Section = section[0]
|
|
|
|
delete(p.Parameters, "section")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-01-15 20:56:46 +00:00
|
|
|
if slug := p.Parameters["slug"]; len(slug) == 1 {
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Slug = slug[0]
|
|
|
|
delete(p.Parameters, "slug")
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2021-01-15 20:56:46 +00:00
|
|
|
if published := p.Parameters["published"]; len(published) == 1 {
|
2020-11-10 19:09:32 +00:00
|
|
|
p.Published = published[0]
|
|
|
|
delete(p.Parameters, "published")
|
|
|
|
}
|
2021-01-15 20:56:46 +00:00
|
|
|
if updated := p.Parameters["updated"]; len(updated) == 1 {
|
2020-11-10 19:09:32 +00:00
|
|
|
p.Updated = updated[0]
|
|
|
|
delete(p.Parameters, "updated")
|
|
|
|
}
|
2021-01-15 20:56:46 +00:00
|
|
|
if status := p.Parameters["status"]; len(status) == 1 {
|
|
|
|
p.Status = postStatus(status[0])
|
|
|
|
delete(p.Parameters, "status")
|
|
|
|
}
|
2021-07-12 14:19:28 +00:00
|
|
|
if priority := p.Parameters["priority"]; len(priority) == 1 {
|
|
|
|
p.Priority = cast.ToInt(priority[0])
|
|
|
|
delete(p.Parameters, "priority")
|
|
|
|
}
|
2020-10-15 15:32:46 +00:00
|
|
|
if p.Path == "" && p.Section == "" {
|
2020-10-14 16:23:56 +00:00
|
|
|
// Has no path or section -> default section
|
2021-06-06 12:39:42 +00:00
|
|
|
p.Section = a.cfg.Blogs[p.Blog].DefaultSection
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2020-10-15 15:32:46 +00:00
|
|
|
if p.Published == "" && p.Section != "" {
|
2020-10-14 16:23:56 +00:00
|
|
|
// Has no published date, but section -> published now
|
2021-07-13 15:23:10 +00:00
|
|
|
p.Published = localNowString()
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
|
|
|
// Add images not in content
|
2021-06-06 12:39:42 +00:00
|
|
|
images := p.Parameters[a.cfg.Micropub.PhotoParam]
|
|
|
|
imageAlts := p.Parameters[a.cfg.Micropub.PhotoDescriptionParam]
|
2020-10-14 16:23:56 +00:00
|
|
|
useAlts := len(images) == len(imageAlts)
|
|
|
|
for i, image := range images {
|
2020-10-15 15:32:46 +00:00
|
|
|
if !strings.Contains(p.Content, image) {
|
2020-10-14 16:23:56 +00:00
|
|
|
if useAlts && len(imageAlts[i]) > 0 {
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Content += "\n\n![" + imageAlts[i] + "](" + image + " \"" + imageAlts[i] + "\")"
|
2020-10-14 16:23:56 +00:00
|
|
|
} else {
|
2020-10-15 15:32:46 +00:00
|
|
|
p.Content += "\n\n![](" + image + ")"
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-06-23 17:20:50 +00:00
|
|
|
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) {
|
2021-02-08 17:51:07 +00:00
|
|
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "delete") {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "delete scope missing", http.StatusForbidden)
|
2020-11-22 19:30:02 +00:00
|
|
|
return
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
uu, err := url.Parse(u)
|
|
|
|
if err != nil {
|
|
|
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := a.deletePost(uu.Path); err != nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
2020-11-22 19:30:02 +00:00
|
|
|
return
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
http.Redirect(w, r, uu.String(), http.StatusNoContent)
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
|
|
|
|
2021-06-23 17:20:50 +00:00
|
|
|
func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string, mf *microformatItem) {
|
2021-02-08 17:51:07 +00:00
|
|
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "update") {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, "update scope missing", http.StatusForbidden)
|
2020-11-22 19:30:02 +00:00
|
|
|
return
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2021-06-23 17:20:50 +00:00
|
|
|
uu, err := url.Parse(u)
|
|
|
|
if err != nil {
|
|
|
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
2021-08-05 06:09:34 +00:00
|
|
|
p, err := a.getPost(uu.Path)
|
2020-10-14 16:23:56 +00:00
|
|
|
if err != nil {
|
2021-06-06 12:39:42 +00:00
|
|
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
2020-10-14 16:23:56 +00:00
|
|
|
return
|
|
|
|
}
|
2021-01-15 20:56:46 +00:00
|
|
|
oldPath := p.Path
|
|
|
|
oldStatus := p.Status
|
2021-07-14 16:50:24 +00:00
|
|
|
a.micropubUpdateReplace(p, mf.Replace)
|
|
|
|
a.micropubUpdateAdd(p, mf.Add)
|
|
|
|
a.micropubUpdateDelete(p, mf.Delete)
|
|
|
|
err = a.computeExtraPostParameters(p)
|
|
|
|
if err != nil {
|
|
|
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2021-07-14 16:50:24 +00:00
|
|
|
err = a.replacePost(p, oldPath, oldStatus)
|
|
|
|
if err != nil {
|
|
|
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
http.Redirect(w, r, a.fullPostURL(p), http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *goBlog) micropubUpdateReplace(p *post, replace map[string][]interface{}) {
|
|
|
|
if content, ok := replace["content"]; ok && len(content) > 0 {
|
|
|
|
p.Content = cast.ToStringSlice(content)[0]
|
|
|
|
}
|
|
|
|
if published, ok := replace["published"]; ok && len(published) > 0 {
|
|
|
|
p.Published = cast.ToStringSlice(published)[0]
|
|
|
|
}
|
|
|
|
if updated, ok := replace["updated"]; ok && len(updated) > 0 {
|
|
|
|
p.Updated = cast.ToStringSlice(updated)[0]
|
|
|
|
}
|
|
|
|
// Status
|
|
|
|
statusStr := ""
|
|
|
|
if status, ok := replace["post-status"]; ok && len(status) > 0 {
|
|
|
|
statusStr = cast.ToStringSlice(status)[0]
|
|
|
|
}
|
|
|
|
visibilityStr := ""
|
|
|
|
if visibility, ok := replace["visibility"]; ok && len(visibility) > 0 {
|
|
|
|
visibilityStr = cast.ToStringSlice(visibility)[0]
|
|
|
|
}
|
|
|
|
if finalStatus := micropubStatus(p.Status, statusStr, visibilityStr); finalStatus != statusNil {
|
|
|
|
p.Status = finalStatus
|
|
|
|
}
|
|
|
|
// Parameters
|
|
|
|
if name, ok := replace["name"]; ok && name != nil {
|
|
|
|
p.Parameters["title"] = cast.ToStringSlice(name)
|
|
|
|
}
|
|
|
|
if category, ok := replace["category"]; ok && category != nil {
|
|
|
|
p.Parameters[a.cfg.Micropub.CategoryParam] = cast.ToStringSlice(category)
|
|
|
|
}
|
|
|
|
if reply, ok := replace["in-reply-to"]; ok && reply != nil {
|
|
|
|
p.Parameters[a.cfg.Micropub.ReplyParam] = cast.ToStringSlice(reply)
|
|
|
|
}
|
|
|
|
if like, ok := replace["like-of"]; ok && like != nil {
|
|
|
|
p.Parameters[a.cfg.Micropub.LikeParam] = cast.ToStringSlice(like)
|
|
|
|
}
|
|
|
|
if bookmark, ok := replace["bookmark-of"]; ok && bookmark != nil {
|
|
|
|
p.Parameters[a.cfg.Micropub.BookmarkParam] = cast.ToStringSlice(bookmark)
|
|
|
|
}
|
|
|
|
if audio, ok := replace["audio"]; ok && audio != nil {
|
|
|
|
p.Parameters[a.cfg.Micropub.AudioParam] = cast.ToStringSlice(audio)
|
|
|
|
}
|
|
|
|
// TODO: photos
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *goBlog) micropubUpdateAdd(p *post, add map[string][]interface{}) {
|
|
|
|
for key, value := range add {
|
|
|
|
switch key {
|
|
|
|
case "content":
|
|
|
|
p.Content += strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
|
|
|
case "published":
|
|
|
|
p.Published = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
|
|
|
case "updated":
|
|
|
|
p.Updated = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
|
|
|
case "category":
|
2021-07-31 11:20:51 +00:00
|
|
|
p.Parameters[a.cfg.Micropub.CategoryParam] = append(p.Parameters[a.cfg.Micropub.CategoryParam], cast.ToStringSlice(value)...)
|
2021-07-14 16:50:24 +00:00
|
|
|
case "in-reply-to":
|
|
|
|
p.Parameters[a.cfg.Micropub.ReplyParam] = cast.ToStringSlice(value)
|
|
|
|
case "like-of":
|
|
|
|
p.Parameters[a.cfg.Micropub.LikeParam] = cast.ToStringSlice(value)
|
|
|
|
case "bookmark-of":
|
|
|
|
p.Parameters[a.cfg.Micropub.BookmarkParam] = cast.ToStringSlice(value)
|
|
|
|
case "audio":
|
2021-07-31 11:20:51 +00:00
|
|
|
p.Parameters[a.cfg.Micropub.AudioParam] = append(p.Parameters[a.cfg.Micropub.AudioParam], cast.ToStringSlice(value)...)
|
2021-07-14 16:50:24 +00:00
|
|
|
// TODO: photo
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-14 16:50:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a *goBlog) micropubUpdateDelete(p *post, del interface{}) {
|
2021-07-31 11:20:51 +00:00
|
|
|
if del == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
deleteProperties, ok := del.([]interface{})
|
|
|
|
if ok {
|
|
|
|
// Completely remove properties
|
|
|
|
for _, prop := range deleteProperties {
|
|
|
|
switch prop {
|
|
|
|
case "content":
|
|
|
|
p.Content = ""
|
|
|
|
case "published":
|
|
|
|
p.Published = ""
|
|
|
|
case "updated":
|
|
|
|
p.Updated = ""
|
|
|
|
case "category":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.CategoryParam)
|
|
|
|
case "in-reply-to":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.ReplyParam)
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.ReplyTitleParam)
|
|
|
|
case "like-of":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.LikeParam)
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.LikeTitleParam)
|
|
|
|
case "bookmark-of":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.BookmarkParam)
|
|
|
|
case "audio":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.AudioParam)
|
|
|
|
case "photo":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.PhotoParam)
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.PhotoDescriptionParam)
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2021-07-31 11:20:51 +00:00
|
|
|
}
|
|
|
|
// Return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
toDelete, ok := del.(map[string]interface{})
|
|
|
|
if ok {
|
|
|
|
// Only delete parts of properties
|
|
|
|
for key, values := range toDelete {
|
|
|
|
switch key {
|
|
|
|
// Properties to completely delete
|
|
|
|
case "content":
|
|
|
|
p.Content = ""
|
|
|
|
case "published":
|
|
|
|
p.Published = ""
|
|
|
|
case "updated":
|
|
|
|
p.Updated = ""
|
|
|
|
case "in-reply-to":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.ReplyParam)
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.ReplyTitleParam)
|
|
|
|
case "like-of":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.LikeParam)
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.LikeTitleParam)
|
|
|
|
case "bookmark-of":
|
|
|
|
delete(p.Parameters, a.cfg.Micropub.BookmarkParam)
|
|
|
|
// Properties to delete part of
|
|
|
|
// TODO: Support partial deletes of more properties
|
|
|
|
case "category":
|
|
|
|
delValues := cast.ToStringSlice(values)
|
|
|
|
p.Parameters[a.cfg.Micropub.CategoryParam] = funk.FilterString(p.Parameters[a.cfg.Micropub.CategoryParam], func(s string) bool {
|
|
|
|
return !funk.ContainsString(delValues, s)
|
|
|
|
})
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-07-14 16:50:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func micropubStatus(defaultStatus postStatus, status string, visibility string) (final postStatus) {
|
|
|
|
final = defaultStatus
|
|
|
|
switch status {
|
|
|
|
case "published":
|
|
|
|
final = statusPublished
|
|
|
|
case "draft":
|
|
|
|
final = statusDraft
|
|
|
|
}
|
|
|
|
if final != statusDraft {
|
|
|
|
// Only override status if it's not a draft
|
|
|
|
switch visibility {
|
|
|
|
case "public":
|
|
|
|
final = statusPublished
|
|
|
|
case "unlisted":
|
|
|
|
final = statusUnlisted
|
|
|
|
case "private":
|
|
|
|
final = statusPrivate
|
|
|
|
}
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|
2021-07-14 16:50:24 +00:00
|
|
|
return final
|
2020-10-14 16:23:56 +00:00
|
|
|
}
|