From 391435b4bf68738a1287903956e165dd9f473cd7 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 10:32:25 +0100 Subject: [PATCH] feat: stored HTML keeps link rel and media names are slugged - the HTML policy keeps rel on a when it holds only nofollow, sponsored and ugc tokens (D-83); any other rel value is removed - classes.MediaFileName slugs uploaded image names (lowercase, Polish letters transliterated, hyphens, at most 80 characters, image when empty) for the editor upload and POST /media/upload (D-85) - the editor corpus gains an external link with rel and a decorative image, pinned by TestEditorOutputPassesPolicy --- categories_admin_editor_test.go | 4 +- classes/blog_editor.go | 37 ++---------- classes/blog_editor_test.go | 8 +-- classes/editor_policy_test.go | 13 ++++- classes/format_html.go | 4 ++ classes/media_names.go | 61 ++++++++++++++++++++ classes/media_names_test.go | 30 ++++++++++ classes/media_store.go | 10 ++-- classes/media_store_test.go | 4 +- controllers/posts_editor_smoke_test.go | 4 +- tests/blogeditor/corpus.test.ts | 38 ++++++++++-- tests/blogeditor/fixtures/editor-corpus.json | 10 +++- 12 files changed, 167 insertions(+), 56 deletions(-) create mode 100644 classes/media_names.go create mode 100644 classes/media_names_test.go diff --git a/categories_admin_editor_test.go b/categories_admin_editor_test.go index b23fbc2..7404fb5 100644 --- a/categories_admin_editor_test.go +++ b/categories_admin_editor_test.go @@ -236,14 +236,14 @@ func TestCategoriesAdminBlogEditor(t *testing.T) { if err := json.Unmarshal(adminDecode(t, rec).Data, &result); err != nil { t.Fatal(err) } - if result.URL != attach.PublicURL("journal/categories/ok-adka.png") { + if result.URL != attach.PublicURL("journal/categories/okladka.png") { t.Fatalf("url = %q", result.URL) } bucket, ok := env.app.Lookup[*blob.Bucket]() if !ok { t.Fatal("no media bucket") } - if data, err := bucket.ReadAll(t.Context(), "journal/categories/ok-adka.png"); err != nil || !bytes.Equal(data, editorPNG) { + if data, err := bucket.ReadAll(t.Context(), "journal/categories/okladka.png"); err != nil || !bytes.Equal(data, editorPNG) { t.Fatalf("stored upload: %v", err) } diff --git a/classes/blog_editor.go b/classes/blog_editor.go index a719949..a97bb1b 100644 --- a/classes/blog_editor.go +++ b/classes/blog_editor.go @@ -450,38 +450,9 @@ func uploadBlogImage(app func() *backpack.App) func(context.Context, pact.FormFi } } -// safeUploadName is the stored name of an editor upload: the base name, -// lowercased, every character outside a-z, 0-9, '.', '_' and '-' turned -// into '-' with runs collapsed and no leading or trailing '-' or '.', and -// the extension kept. An empty name becomes "image". The result needs no -// escaping in an src attribute. +// safeUploadName is the stored name of an editor upload: MediaFileName of +// its base name (D-85), so it needs no escaping in an src attribute. An +// empty name becomes "image". func safeUploadName(filename string) string { - base := path.Base(strings.ReplaceAll(filename, `\`, "/")) - ext := strings.ToLower(path.Ext(base)) - if !blogUploadExtPattern.MatchString(ext) { - ext = "" - } - stem := strings.ToLower(strings.TrimSuffix(base, path.Ext(base))) - if ext == "" { - stem = strings.ToLower(base) - } - var b strings.Builder - dash := false - for _, r := range stem { - ok := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' - if !ok || r == '-' { - if !dash { - b.WriteByte('-') - dash = true - } - continue - } - b.WriteRune(r) - dash = false - } - stem = strings.Trim(b.String(), "-.") - if stem == "" { - stem = "image" - } - return stem + ext + return MediaFileName(filename) } diff --git a/classes/blog_editor_test.go b/classes/blog_editor_test.go index 65f6d7f..5f4d532 100644 --- a/classes/blog_editor_test.go +++ b/classes/blog_editor_test.go @@ -699,17 +699,17 @@ func TestBlogEditorUpload(t *testing.T) { t.Run("names are made safe", func(t *testing.T) { for in, want := range map[string]string{ - "Zdjęcie 1.PNG": "zdj-cie-1.png", + "Zdjęcie 1.PNG": "zdjecie-1.png", `C:\\Users\\x\\My Photo.jpg`: "my-photo.jpg", "../../etc/passwd.png": "passwd.png", "...": "image", "": "image", "-.-a-.png": "a.png", - "a--b__c.webp": "a-b__c.webp", + "a--b__c.webp": "a-b-c.webp", "