Fix manage handlers ignoring multipart form bodies

The /manage page posts with FormData, i.e. multipart/form-data, but the edit,
delete, tags, restore and empty-trash handlers parsed with r.ParseForm, which
only reads urlencoded bodies. So every field came back empty: delete returned
200 with deleted:0 and moved nothing, edits saved nothing — while curl -d
(urlencoded) worked, which is why tests and manual checks missed it.

Parse with ParseMultipartForm (tolerating ErrNotMultipart so urlencoded still
works), via a shared parseForm helper used by all manage handlers. handleUpload
already did this. Also stop the UI reporting deleted:0 / updated:0 as success,
and add a multipart regression test.
This commit is contained in:
puttaalu committed 2026-10-18 22:32:18 +02:00
1 parent 25384d3f3b
commit bf75c9fe8e
4 files changed
+75 -9

No files matched your search

+47
View File
@@ -1,9 +1,11 @@
package main
import (
"bytes"
"encoding/json"
"image"
"image/png"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
@@ -58,6 +60,20 @@ func form(v url.Values) *http.Request {
return r
}
// multipartForm builds a multipart/form-data request, as a browser's FormData
// does — the content type the manage handlers must parse, not just urlencoded.
func multipartForm(fields map[string]string) *http.Request {
var b bytes.Buffer
mw := multipart.NewWriter(&b)
for k, v := range fields {
mw.WriteField(k, v)
}
mw.Close()
r := httptest.NewRequest(http.MethodPost, "/", &b)
r.Header.Set("Content-Type", mw.FormDataContentType())
return r
}
func readMeta(t *testing.T, content, slug string) PhotoMeta {
t.Helper()
var m PhotoMeta
@@ -213,6 +229,37 @@ func TestHandleDeleteMovesToTrashAndPrunes(t *testing.T) {
}
}
// TestMultipartBodies guards the regression where the manage handlers used
// r.ParseForm (urlencoded only) and silently ignored the multipart bodies that
// browsers send via FormData, returning 200 with deleted:0 / no change.
func TestMultipartBodies(t *testing.T) {
s, content, _ := newTestServer(t)
// Edit via multipart.
rec := httptest.NewRecorder()
s.handleEdit(rec, multipartForm(map[string]string{
"slug": "foo", "caption": "Multipart", "date": "2021-02-03", "tags": "x,y",
}))
if rec.Code != http.StatusOK {
t.Fatalf("multipart edit: code=%d", rec.Code)
}
if m := readMeta(t, content, "foo"); m.Caption != "Multipart" || strings.Join(m.Tags, ",") != "x,y" {
t.Errorf("multipart edit not applied: %+v", m)
}
// Delete via multipart actually moves the file.
rec = httptest.NewRecorder()
s.handleDelete(rec, multipartForm(map[string]string{"slug": "bar"}))
var res map[string]int
json.Unmarshal(rec.Body.Bytes(), &res)
if res["deleted"] != 1 {
t.Fatalf("multipart delete: deleted=%d, want 1 (body=%s)", res["deleted"], rec.Body)
}
if _, err := os.Stat(filepath.Join(content, "bar.png")); !os.IsNotExist(err) {
t.Error("bar.png should have moved to trash")
}
}
func TestCSRFGuard(t *testing.T) {
s, _, _ := newTestServer(t) // generator.baseURL == "http://x"