cerca

lean forum software (pmc local branch)
Log | Files | Refs | README | LICENSE

commit 6d7ec1622eab73d6a24f196feadb1e62e3472eda
parent bbdbda8cda8d035dfcafd8349767578f72151cb6
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Wed,  3 Jan 2024 22:20:38 -0500

Add post editing functionality

Diffstat:
Mdatabase/database.go | 8++++----
Ahtml/edit-post.html | 14++++++++++++++
Mhtml/thread.html | 10++++++----
Mi18n/i18n.go | 12++++++++++++
Mserver/server.go | 59+++++++++++++++++++++++++++++++++++++++++++++--------------
Mutil/util.go | 7++++++-
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) }