aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/handlers_design.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/handlers_design.go')
-rw-r--r--internal/web/handlers_design.go174
1 files changed, 4 insertions, 170 deletions
diff --git a/internal/web/handlers_design.go b/internal/web/handlers_design.go
index 7424eca..3e9e600 100644
--- a/internal/web/handlers_design.go
+++ b/internal/web/handlers_design.go
@@ -1,27 +1,15 @@
package web
import (
- "bytes"
- "encoding/json"
- "errors"
- "io"
- "mime/multipart"
"net/http"
- "path/filepath"
- "strconv"
- "strings"
"time"
- "github.com/google/uuid"
"github.com/gramanas/blogspace/internal/store"
)
-// ICO is here for site icons; it is harmless anywhere else an image can go.
-var allowedImageTypes = map[string]bool{"image/png": true, "image/jpeg": true, "image/gif": true, "image/webp": true, "image/x-icon": true}
-
func (s *Server) handleDesignForm(w http.ResponseWriter, r *http.Request) {
blog := currentBlog(r)
- images, err := blogStore(r).ListImages(r.Context())
+ images, _, err := blogStore(r).ListFiles(r.Context(), "image", "", 0, 0)
if err != nil {
s.serverError(w, err)
return
@@ -30,22 +18,18 @@ func (s *Server) handleDesignForm(w http.ResponseWriter, r *http.Request) {
}
// designData is what design.html shows; "today" is the sample for the date formats.
-func designData(theme Theme, images []store.Image) map[string]any {
+func designData(theme Theme, images []store.File) map[string]any {
return map[string]any{"theme": theme, "images": images, "presets": Presets(), "today": time.Now()}
}
func (s *Server) handleDesign(w http.ResponseWriter, r *http.Request) {
blog := currentBlog(r)
- if err := r.ParseMultipartForm(1 << 20); err != nil && !errors.Is(err, http.ErrNotMultipart) {
- s.plainError(w, http.StatusBadRequest, s.tr(r, "Upload too large or malformed form."))
- return
- }
theme := ThemeFromForm(ParseTheme(blog.ThemeJSON), r.Form)
// Optional direct uploads from the design form.
for field, dst := range map[string]*string{"bg_image_file": &theme.BgImage, "header_image_file": &theme.HeaderImage, "logo_file": &theme.Logo, "favicon_file": &theme.Favicon} {
- img, err := s.readUpload(r, field)
+ img, err := s.readUpload(r, field, true)
if err != nil {
- images, _ := blogStore(r).ListImages(r.Context())
+ images, _, _ := blogStore(r).ListFiles(r.Context(), "image", "", 0, 0)
d := designData(theme, images)
d["error"] = err.Error()
s.renderStatus(w, r, http.StatusBadRequest, "dashboard/design.html", d)
@@ -93,153 +77,3 @@ func (s *Server) handleDesignReset(w http.ResponseWriter, r *http.Request) {
}
redirectOK(w, r, "/b/"+blog.Subdomain+"/design", s.tr(r, "Design and layout reset to the defaults."))
}
-
-// readUpload stores the file from a multipart field, returning nil if the field is empty.
-func (s *Server) readUpload(r *http.Request, field string) (*store.Image, error) {
- if r.MultipartForm == nil {
- return nil, nil
- }
- fhs := r.MultipartForm.File[field]
- if len(fhs) == 0 {
- return nil, nil
- }
- return s.storeUpload(r, fhs[0])
-}
-
-func (s *Server) storeUpload(r *http.Request, fh *multipart.FileHeader) (*store.Image, error) {
- tooBig := errors.New(s.trf(r, "Image is too large (max %s).", kbString(s.cfg.MaxUploadBytes)))
- if fh.Size > s.cfg.MaxUploadBytes {
- return nil, tooBig
- }
- f, err := fh.Open()
- if err != nil {
- return nil, err
- }
- defer f.Close()
- var buf bytes.Buffer
- if _, err := io.CopyN(&buf, f, s.cfg.MaxUploadBytes+1); err != nil && !errors.Is(err, io.EOF) {
- return nil, err
- }
- if int64(buf.Len()) > s.cfg.MaxUploadBytes {
- return nil, tooBig
- }
- ct := http.DetectContentType(buf.Bytes())
- if !allowedImageTypes[ct] {
- return nil, errors.New(s.tr(r, "Only PNG, JPEG, GIF, WebP and ICO images are accepted."))
- }
- name := filepath.Base(fh.Filename)
- if name == "" || name == "." || len(name) > 120 {
- name = "image"
- }
- return blogStore(r).CreateImage(r.Context(), name, ct, buf.Bytes())
-}
-
-// imageMarkdown is the line the editor inserts for an uploaded image.
-func imageMarkdown(img *store.Image) string {
- return "![" + strings.NewReplacer("]", "", "\n", " ").Replace(img.Filename) + "](/media/" + img.ID.String() + ")"
-}
-
-// appendImageMD is the no-JavaScript path of "Insert image": the file arrives
-// with the form itself and is appended to the end of the text on save.
-func appendImageMD(md string, img *store.Image) string {
- if img == nil {
- return md
- }
- md = strings.TrimRight(md, "\n")
- if md != "" {
- md += "\n\n"
- }
- return md + imageMarkdown(img) + "\n"
-}
-
-func kbString(n int64) string {
- if n >= 1<<20 {
- return strconv.FormatInt(n>>20, 10) + " MB"
- }
- return strconv.FormatInt(n>>10, 10) + " KB"
-}
-
-// ---- image library ---------------------------------------------------------
-
-func (s *Server) handleImages(w http.ResponseWriter, r *http.Request) {
- images, err := blogStore(r).ListImages(r.Context())
- if err != nil {
- s.serverError(w, err)
- return
- }
- s.render(w, r, "dashboard/images.html", map[string]any{"images": images})
-}
-
-// handleImageUpload serves the Images page form and, when the client asks for
-// JSON, the editor's "Insert image" script.
-func (s *Server) handleImageUpload(w http.ResponseWriter, r *http.Request) {
- blog := currentBlog(r)
- wantJSON := strings.Contains(r.Header.Get("Accept"), "application/json")
- fail := func(msg string) {
- if wantJSON {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": msg})
- return
- }
- s.plainError(w, http.StatusBadRequest, msg)
- }
- if err := r.ParseMultipartForm(1 << 20); err != nil {
- fail(s.tr(r, "Upload too large or malformed form."))
- return
- }
- img, err := s.readUpload(r, "file")
- if err != nil {
- fail(err.Error())
- return
- }
- if img == nil {
- fail(s.tr(r, "Choose a file first."))
- return
- }
- if wantJSON {
- writeJSON(w, http.StatusOK, map[string]string{"id": img.ID.String(), "filename": img.Filename, "markdown": imageMarkdown(img)})
- return
- }
- redirectOK(w, r, "/b/"+blog.Subdomain+"/images", s.trf(r, "Uploaded %s.", img.Filename))
-}
-
-func writeJSON(w http.ResponseWriter, status int, v any) {
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("X-Content-Type-Options", "nosniff")
- w.WriteHeader(status)
- _ = json.NewEncoder(w).Encode(v)
-}
-
-func (s *Server) handleImageDelete(w http.ResponseWriter, r *http.Request) {
- blog := currentBlog(r)
- id, err := uuid.Parse(r.PathValue("id"))
- if err != nil {
- http.NotFound(w, r)
- return
- }
- if err := blogStore(r).DeleteImage(r.Context(), id); err != nil {
- s.serverError(w, err)
- return
- }
- // Drop dangling references from the theme.
- theme := ParseTheme(blog.ThemeJSON)
- changed := false
- if theme.BgImage == id.String() {
- theme.BgImage, changed = "", true
- }
- if theme.HeaderImage == id.String() {
- theme.HeaderImage, changed = "", true
- }
- if theme.Favicon == id.String() {
- theme.Favicon, changed = "", true
- }
- if theme.Logo == id.String() {
- theme.Logo, changed = "", true
- }
- if changed {
- if err := blogStore(r).UpdateTheme(r.Context(), theme.JSON()); err != nil {
- s.serverError(w, err)
- return
- }
- }
- redirectOK(w, r, "/b/"+blog.Subdomain+"/images", s.tr(r, "Image deleted."))
-}