diff options
Diffstat (limited to 'internal/web/handlers_design.go')
| -rw-r--r-- | internal/web/handlers_design.go | 174 |
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.")) -} |
