瀏覽代碼

Transliterate German characters in filenames and S3 keys

The filename sanitizer stripped ä/ö/ü/ß into dashes (Straße.jpg -> Stra-e),
while slugify() already transliterated them. Share one ascii_transliterate()
helper (ä→ae, ö→oe, ü→ue, ß→ss) used by slugify() and a new safe_filename(),
so local media names and S3 keys become Strasse.jpg / Weiss-Oel.png instead.
Displayed titles and image captions still keep the original UTF-8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Medowar 1 月之前
父節點
當前提交
d6a747e67d
共有 2 個文件被更改,包括 44 次插入11 次删除
  1. 1 3
      admin/api.php
  2. 43 8
      app/storage.php

+ 1 - 3
admin/api.php

@@ -32,9 +32,7 @@ $slug = $gallery['slug'];
 
 
 switch ($action) {
 switch ($action) {
     case 'presign':
     case 'presign':
-        $name = basename((string)($body['name'] ?? ''));
-        $name = preg_replace('/[^A-Za-z0-9._-]+/', '-', $name) ?: 'file';
-        $name = substr($name, 0, 120);
+        $name = substr(safe_filename((string)($body['name'] ?? '')), 0, 120);
         // Random prefix avoids overwrites when two files share a name.
         // Random prefix avoids overwrites when two files share a name.
         $token = random_token(6);
         $token = random_token(6);
         $key   = "galleries/$slug/originals/$token-$name";
         $key   = "galleries/$slug/originals/$token-$name";

+ 43 - 8
app/storage.php

@@ -52,20 +52,55 @@ function random_token(int $chars = 8): string
     return $out;
     return $out;
 }
 }
 
 
-/** Turn a title into a URL slug fragment ("Wedding Müller" → "wedding-mueller"). */
-function slugify(string $title): string
+/**
+ * Transliterate German (and, best effort, other accented) characters to ASCII
+ * so they survive in slugs, filenames and S3 keys instead of being dropped:
+ * ä→ae, ö→oe, ü→ue, ß→ss, é→e, … Case is preserved.
+ */
+function ascii_transliterate(string $s): string
 {
 {
-    $map = ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'Ä' => 'ae', 'Ö' => 'oe', 'Ü' => 'ue', 'ß' => 'ss'];
-    $s = strtr($title, $map);
+    $map = [
+        'ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue',
+        'Ä' => 'Ae', 'Ö' => 'Oe', 'Ü' => 'Ue', 'ß' => 'ss',
+    ];
+    $s = strtr($s, $map);
     if (function_exists('iconv')) {
     if (function_exists('iconv')) {
-        $s = (string)@iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
+        $converted = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
+        if ($converted !== false) {
+            $s = $converted;
+        }
     }
     }
-    $s = strtolower($s);
+    return $s;
+}
+
+/** Turn a title into a URL slug fragment ("Wedding Müller" → "wedding-mueller"). */
+function slugify(string $title): string
+{
+    $s = strtolower(ascii_transliterate($title));
     $s = preg_replace('/[^a-z0-9]+/', '-', $s) ?? '';
     $s = preg_replace('/[^a-z0-9]+/', '-', $s) ?? '';
     $s = trim($s, '-');
     $s = trim($s, '-');
     return $s !== '' ? $s : 'gallery';
     return $s !== '' ? $s : 'gallery';
 }
 }
 
 
+/**
+ * Sanitize an upload filename to a safe ASCII basename, keeping the extension.
+ * German characters are transliterated rather than replaced by dashes, so
+ * "Straße.jpg" becomes "Strasse.jpg" instead of "Stra-e.jpg".
+ */
+function safe_filename(string $name, string $fallback = 'file'): string
+{
+    $name = basename($name);
+    $ext  = strtolower(pathinfo($name, PATHINFO_EXTENSION));
+    $base = ascii_transliterate(pathinfo($name, PATHINFO_FILENAME));
+    $base = preg_replace('/[^A-Za-z0-9._-]+/', '-', $base) ?? '';
+    $base = trim($base, '-.');
+    if ($base === '') {
+        $base = $fallback;
+    }
+    $ext = preg_replace('/[^A-Za-z0-9]+/', '', $ext) ?? '';
+    return $ext !== '' ? $base . '.' . $ext : $base;
+}
+
 // ---------------------------------------------------------------------------
 // ---------------------------------------------------------------------------
 // Local media (hero + showreel images in media/)
 // Local media (hero + showreel images in media/)
 // ---------------------------------------------------------------------------
 // ---------------------------------------------------------------------------
@@ -89,8 +124,8 @@ function media_store_upload(array $file): ?string
     if (function_exists('getimagesize') && @getimagesize($file['tmp_name']) === false) {
     if (function_exists('getimagesize') && @getimagesize($file['tmp_name']) === false) {
         return null;
         return null;
     }
     }
-    $base = pathinfo($file['name'], PATHINFO_FILENAME);
-    $base = preg_replace('/[^A-Za-z0-9._-]+/', '-', $base) ?: 'image';
+    $base = ascii_transliterate(pathinfo($file['name'], PATHINFO_FILENAME));
+    $base = trim(preg_replace('/[^A-Za-z0-9._-]+/', '-', $base) ?? '', '-.') ?: 'image';
     $name = substr($base, 0, 60) . '-' . random_token(6) . '.' . $ext;
     $name = substr($base, 0, 60) . '-' . random_token(6) . '.' . $ext;
     if (!move_uploaded_file($file['tmp_name'], MEDIA_DIR . '/' . $name)) {
     if (!move_uploaded_file($file['tmp_name'], MEDIA_DIR . '/' . $name)) {
         return null;
         return null;