commit 6d7ec1622eab73d6a24f196feadb1e62e3472eda
parent bbdbda8cda8d035dfcafd8349767578f72151cb6
Author: alex wennerberg <alex@alexwennerberg.com>
Date: Wed, 3 Jan 2024 22:20:38 -0500
Add post editing functionality
Diffstat:
6 files changed, 87 insertions(+), 23 deletions(-)
diff --git a/database/database.go b/database/database.go
@@ -6,7 +6,6 @@ import (
"cerca/crypto"
"errors"
"fmt"
- "html/template"
"log"
"net/url"
"os"
@@ -226,7 +225,8 @@ func (d DB) CreateThread(title, content string, authorid, topicid int) (int, err
type Post struct {
ID int
ThreadTitle string
- Content template.HTML
+ ThreadID int
+ Content string // markdown
Author string
AuthorID int
Publish time.Time
@@ -274,14 +274,14 @@ func (d DB) GetThread(threadid int) []Post {
func (d DB) GetPost(postid int) (Post, error) {
stmt := `
- SELECT p.id, t.title, content, u.name, p.authorid, p.publishtime, p.lastedit
+ SELECT p.id, t.title, t.id, content, u.name, p.authorid, p.publishtime, p.lastedit
FROM posts p
INNER JOIN users u ON u.id = p.authorid
INNER JOIN threads t ON t.id = p.threadid
WHERE p.id = ?
`
var data Post
- err := d.db.QueryRow(stmt, postid).Scan(&data.ID, &data.ThreadTitle, &data.Content, &data.Author, &data.AuthorID, &data.Publish, &data.LastEdit)
+ err := d.db.QueryRow(stmt, postid).Scan(&data.ID, &data.ThreadTitle, &data.ThreadID, &data.Content, &data.Author, &data.AuthorID, &data.Publish, &data.LastEdit)
err = util.Eout(err, "get data for thread %d", postid)
return data, err
}
diff --git a/html/edit-post.html b/html/edit-post.html
@@ -0,0 +1,14 @@
+{{ template "head" . }}
+<main>
+ <h1>{{ "PostEdit" | translate }} <a href="/thread/{{.Data.ThreadID}}/#{{.Data.ID}}">{{.Data.ID}}</a></h1>
+ <article>{{.Data.Content | markup }}
+ </article>
+ <form method="POST">
+ <div class="post-container" >
+ <label for="content">{{ "Content" | translate }}:</label>
+ <textarea required name="content" id="content" placeholder='{{ "TextareaPlaceholder" | translate }}'>{{.Data.Content}}</textarea>
+ <button type="submit">{{ "Save" | translate }}</button>
+ </div>
+ </form>
+</main>
+{{ template "footer" . }}
diff --git a/html/thread.html b/html/thread.html
@@ -14,6 +14,7 @@
<input type="hidden" name="thread" value="{{ $threadURL }}">
</form>
</span>
+ <span style="float: right; margin-right:0.5rem"><a href="/post/edit/{{ $post.ID }}">edit</a></span>
{{ end }}
<span class="visually-hidden">{{ "Author" | translate }}:</span>
<span><b>{{ $post.Author }}</b>
@@ -21,11 +22,12 @@
</span>
<a href="#{{ $post.ID }}">
<span style="margin-left: 0.5rem;">
- <time title="{{ $post.Publish | formatDateTime }}" datetime="{{ $post.Publish | formatDate }}">{{ $post.Publish | formatDateRelative }}</time>
- </span>
- </a>
+ <time title="{{ $post.Publish | formatDateTime }}" datetime="{{ $post.Publish | formatDate }}">{{ $post.Publish | formatDateRelative }}</time></span></a>
+ {{ if $post.LastEdit.Valid }}<span style="margin-left: 0.5rem;">
+ <em>(edited {{ $post.Publish | formatDateRelative }})</em></span>
+ {{ end }}
</section>
- {{ $post.Content }}
+ {{ $post.Content | markup }}
</article>
{{ end }}
{{ if .LoggedIn }}
diff --git a/i18n/i18n.go b/i18n/i18n.go
@@ -117,6 +117,10 @@ var English = map[string]string{
"RegisterSuccess": "registered successfully",
"ErrUnaccepted": "Unaccepted request",
+ "ErrGeneric401": "Unauthorized",
+ "ErrGeneric401Message": "You do not have permissions to perform this action.",
+ "ErrEdit404": "Post not found",
+ "ErrEdit404Message": "This post cannot be found for editing",
"ErrThread404": "Thread not found",
"ErrThread404Message": "The thread does not exist (anymore?)",
"ErrGeneric404": "Page not found",
@@ -126,13 +130,16 @@ var English = map[string]string{
"NewThreadLinkMessage": "If you are a member,",
"NewThreadCreateError": "Error creating thread",
"NewThreadCreateErrorMessage": "There was a database error when creating the thread, apologies.",
+ "PostEdit": "Editing post",
"AriaPostMeta": "Post meta",
"AriaDeletePost": "Delete this post",
"AriaRespondIntoThread": "Respond into this thread",
"PromptDeleteQuestion": "Delete post for all posterity?",
"Delete": "delete",
+ "Edit": "edit",
"Post": "post",
+ "Save": "Save",
"Author": "Author",
"Responded": "responded",
"YourAnswer": "Your answer",
@@ -211,12 +218,14 @@ var Swedish = map[string]string{
"NewThreadLinkMessage": "Om du är en medlem,",
"NewThreadCreateError": "Fel uppstod vid trådskapning",
"NewThreadCreateErrorMessage": "Det uppstod ett databasfel under trådskapningen, ursäkta.",
+ "PostEdit": "Editing post",
"AriaPostMeta": "Post meta",
"AriaDeletePost": "Delete this post",
"AriaRespondIntoThread": "Respond into this thread",
"PromptDeleteQuestion": "Radera post för alltid?",
"Delete": "radera",
+ "Edit": "redigera",
"Post": "post",
"Author": "Författare",
"Responded": "svarade",
@@ -298,6 +307,7 @@ var EspanolMexicano = map[string]string{
"NewThreadLinkMessage": "If you are a member,",
"NewThreadCreateError": "Error creating thread",
"NewThreadCreateErrorMessage": "There was a database error when creating the thread, apologies.",
+ "PostEdit": "Editing post",
"ThreadStartNew": "Start a new thread",
"AriaPostMeta": "Post meta",
@@ -306,7 +316,9 @@ var EspanolMexicano = map[string]string{
"AriaHome": "Home",
"PromptDeleteQuestion": "Delete post for all posterity?",
"Delete": "delete",
+ "Edit": "editar",
"Post": "post",
+ "Save": "Save",
"Author": "Author",
"Responded": "responded",
"YourAnswer": "Your answer",
diff --git a/server/server.go b/server/server.go
@@ -2,6 +2,7 @@ package server
import (
"context"
+ "database/sql"
"errors"
"fmt"
"html/template"
@@ -11,7 +12,6 @@ import (
"net/url"
"os"
"path/filepath"
- "regexp"
"strings"
"syscall"
"time"
@@ -83,6 +83,11 @@ type ThreadData struct {
ThreadURL string
}
+type EditPostData struct {
+ Title string
+ Content string
+}
+
type RequestHandler struct {
db *database.DB
session *session.Session
@@ -204,6 +209,7 @@ func generateTemplates(config types.Config, translator i18n.Translator) (*templa
return translator.TranslateWithData(key, i18n.TranslationData{data})
},
"capitalize": util.Capitalize,
+ "markup": util.Markup,
"tohtml": func(s string) template.HTML {
// use of this function is risky cause it interprets the passed in string and renders it as unescaped html.
// can allow for attacks!
@@ -219,6 +225,7 @@ func generateTemplates(config types.Config, translator i18n.Translator) (*templa
"footer",
"generic-message",
"head",
+ "edit-post",
"index",
"login",
"login-component",
@@ -322,16 +329,6 @@ func (h *RequestHandler) ThreadRoute(res http.ResponseWriter, req *http.Request)
// TODO (2022-01-07):
// * handle error
thread := h.db.GetThread(threadid)
- pattern := regexp.MustCompile("<img")
- // markdownize content (but not title)
- for i, post := range thread {
- content := []byte(util.Markup(post.Content))
- // make sure images are lazy loaded
- if pattern.Match(content) {
- content = pattern.ReplaceAll(content, []byte(`<img loading="lazy"`))
- }
- thread[i].Content = template.HTML(content)
- }
data := ThreadData{Posts: thread, ThreadURL: req.URL.Path}
view := TemplateData{Data: &data, IsAdmin: isAdmin, QuickNav: loggedIn, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, LoggedInID: userid}
if len(thread) > 0 {
@@ -578,8 +575,8 @@ func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request
return
}
- rules := util.Markup(template.HTML(h.files["rules"]))
- verification := util.Markup(template.HTML(h.files["verification-instructions"]))
+ rules := util.Markup(string(h.files["rules"]))
+ verification := util.Markup(string(h.files["verification-instructions"]))
conduct := h.config.Community.ConductLink
var verificationCode string
renderErr := func(errFmt string, args ...interface{}) {
@@ -683,7 +680,7 @@ func (h RequestHandler) GenericRoute(res http.ResponseWriter, req *http.Request)
func (h RequestHandler) AboutRoute(res http.ResponseWriter, req *http.Request) {
loggedIn, _ := h.IsLoggedIn(req)
- input := util.Markup(template.HTML(h.files["about"]))
+ input := util.Markup(string(h.files["about"]))
h.renderView(res, "about-template", TemplateData{Data: input, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, Title: h.translator.Translate("About")})
}
@@ -791,6 +788,39 @@ func (h *RequestHandler) DeletePostRoute(res http.ResponseWriter, req *http.Requ
http.Redirect(res, req, threadURL, http.StatusSeeOther)
}
+func (h *RequestHandler) EditPostRoute(res http.ResponseWriter, req *http.Request) {
+ postid, ok := util.GetURLPortion(req, 3)
+ loggedIn, userid := h.IsLoggedIn(req)
+ post, err := h.db.GetPost(postid)
+
+ if !ok || errors.Is(err, sql.ErrNoRows) {
+ title := h.translator.Translate("ErrEdit404")
+ data := GenericMessageData{
+ Title: title,
+ Message: h.translator.Translate("ErrEdit404Message"),
+ }
+ h.renderGenericMessage(res, req, data)
+ return
+ }
+ if !loggedIn || userid != post.AuthorID {
+ res.WriteHeader(401)
+ title := h.translator.Translate("ErrGeneric401")
+ data := GenericMessageData{
+ Title: title,
+ Message: h.translator.Translate("ErrGeneric401Message"),
+ }
+ h.renderGenericMessage(res, req, data)
+ return
+ }
+ if req.Method == "POST" {
+ content := req.PostFormValue("content")
+ h.db.EditPost(content, postid)
+ post.Content = content
+ }
+ view := TemplateData{Data: post, QuickNav: loggedIn, HasRSS: h.config.RSS.URL != "", LoggedIn: loggedIn, LoggedInID: userid}
+ h.renderView(res, "edit-post", view)
+}
+
func Serve(allowlist []string, sessionKey string, isdev bool, dir string, conf types.Config) {
port := ":8272"
@@ -892,6 +922,7 @@ func NewServer(allowlist []string, sessionKey, dir string, config types.Config)
s.ServeMux.HandleFunc("/login", handler.LoginRoute)
s.ServeMux.HandleFunc("/register", handler.RegisterRoute)
s.ServeMux.HandleFunc("/post/delete/", handler.DeletePostRoute)
+ s.ServeMux.HandleFunc("/post/edit/", handler.EditPostRoute)
s.ServeMux.HandleFunc("/thread/new/", handler.NewThreadRoute)
s.ServeMux.HandleFunc("/thread/", handler.ThreadRoute)
s.ServeMux.HandleFunc("/robots.txt", handler.RobotsRoute)
diff --git a/util/util.go b/util/util.go
@@ -94,7 +94,7 @@ var contentGuardian = bluemonday.UGCPolicy()
var strictContentGuardian = bluemonday.StrictPolicy()
// Turns Markdown input into HTML
-func Markup(md template.HTML) template.HTML {
+func Markup(md string) template.HTML {
mdBytes := []byte(string(md))
// fix newlines
mdBytes = markdown.NormalizeNewlines(mdBytes)
@@ -102,6 +102,11 @@ func Markup(md template.HTML) template.HTML {
maybeUnsafeHTML := markdown.ToHTML(mdBytes, mdParser, nil)
// guard against malicious code being embedded
html := contentGuardian.SanitizeBytes(maybeUnsafeHTML)
+ // lazy load images
+ pattern := regexp.MustCompile("<img")
+ if pattern.Match(html) {
+ html = pattern.ReplaceAll(html, []byte(`<img loading="lazy"`))
+ }
return template.HTML(html)
}