mirror of https://github.com/jlelse/GoBlog
Various refactorings
This commit is contained in:
parent
8db544150d
commit
6bc70f0a0e
|
@ -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"])
|
||||||
|
if ot == "Follow" && actor == activityActor {
|
||||||
_ = a.db.apRemoveFollower(blogName, 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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
10
editor.go
10
editor.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
238
micropub.go
238
micropub.go
|
@ -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 µformatItem{
|
|
||||||
Type: []string{"h-entry"},
|
|
||||||
Properties: µformatProperties{
|
|
||||||
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:
|
||||||
}
|
|
||||||
if action == actionDelete {
|
|
||||||
a.micropubDelete(w, r, u)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
a.serveError(w, r, "Action not supported", http.StatusNotImplemented)
|
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 := µformatItem{}
|
parsedMfItem := µformatItem{}
|
||||||
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)
|
||||||
if parsedMfItem.Action == actionDelete {
|
default:
|
||||||
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)
|
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
|
||||||
|
|
|
@ -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 µformatItem{
|
||||||
|
Type: []string{"h-entry"},
|
||||||
|
Properties: µformatProperties{
|
||||||
|
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 {
|
||||||
|
|
80
render.go
80
render.go
|
@ -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) {
|
||||||
|
|
|
@ -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" }}">
|
||||||
|
|
Loading…
Reference in New Issue