Slim: sanitizeFileName kürzt hinten statt vorne und behält die Dateiendung

- Lange Namen werden am Ende gekürzt (vorher substr(-33): "Bildschirmfoto…"
  wurde zu "ldschirmfoto…", der Namensanfang fiel weg)
- Whitespace inkl. geschütztem Leerzeichen (macOS-Screenshots) wird zu "-",
  damit der Dateiname ohne Encoding in URLs funktioniert
- Fallback "datei", wenn nach der Bereinigung nichts Verwertbares übrig bleibt
- 5 Unit-Tests für Kürzung, Whitespace, Sonderzeichen und Fallback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-08-25 11:11:01 +00:00
parent 8afaf9d4e2
commit 0bdb6eabc6
2 changed files with 64 additions and 2 deletions

View file

@ -11,6 +11,9 @@ abstract class SlimStatus
class Slim class Slim
{ {
/** Maximale Länge des bereinigten Dateinamens inklusive Endung. */
private const MAX_FILENAME_LENGTH = 33;
/** /**
* Speichert ein Slim-Cropper-Upload (Base64-Payload aus getImages()) im * Speichert ein Slim-Cropper-Upload (Base64-Payload aus getImages()) im
* public-Disk und liefert Dateiname + Metadaten — gemeinsame Logik für * public-Disk und liefert Dateiname + Metadaten — gemeinsame Logik für
@ -237,16 +240,41 @@ class Slim
* *
* @return string * @return string
*/ */
/**
* Bereinigt einen hochgeladenen Dateinamen für die Ablage im public-Disk.
*
* Kürzt bei Bedarf am ENDE des Namens und behält die Endung — die frühere
* Fassung schnitt mit substr($str, -33) vorne ab, wodurch aus
* "Bildschirmfoto ....png" ein "ldschirmfoto ....png" wurde. Leerzeichen
* werden zu "-", damit die Datei ohne Encoding in einer URL stehen kann.
*/
public static function sanitizeFileName($str) public static function sanitizeFileName($str)
{ {
// Unicode-Trennzeichen (z. B. geschütztes Leerzeichen in macOS-Screenshot-
// Namen) zuerst zu einem normalen Leerzeichen machen. Der ASCII-Filter
// darunter arbeitet byteweise und würde sie sonst spurlos entfernen,
// sodass Wörter zusammenkleben. Bei ungültigem UTF-8 liefert preg_replace
// null — dann bleibt der Originalwert stehen.
$str = preg_replace('/[\p{Z}\s]+/u', ' ', (string) $str) ?? $str;
// Basic clean up // Basic clean up
$str = preg_replace('([^\w\s\d\-_~,;\[\]\(\).])', '', $str); $str = preg_replace('([^\w\s\d\-_~,;\[\]\(\).])', '', $str);
// Remove any runs of periods // Remove any runs of periods
$str = preg_replace('([\.]{2,})', '', $str); $str = preg_replace('([\.]{2,})', '', $str);
// Whitespace (auch geschützte Leerzeichen aus macOS-Screenshots) zu "-"
$str = preg_replace('(\s+)', '-', $str);
$str = trim($str, '-');
$str = (strlen($str) > 33) ? substr($str, -33) : $str; $extension = pathinfo($str, PATHINFO_EXTENSION);
$suffix = $extension !== '' ? '.'.$extension : '';
$base = $suffix !== '' ? substr($str, 0, -strlen($suffix)) : $str;
return $str; $maxBase = max(1, self::MAX_FILENAME_LENGTH - strlen($suffix));
if (strlen($base) > $maxBase) {
$base = rtrim(substr($base, 0, $maxBase), '-.');
}
return ($base === '' ? 'datei' : $base).$suffix;
} }
/** /**

View file

@ -0,0 +1,34 @@
<?php
use App\Services\Slim;
it('kuerzt lange Dateinamen hinten und behaelt die Endung', function (): void {
// Frueher lieferte substr($str, -33) hier "ldschirmfoto-2026-08-17-um-11.png"
// - der Anfang des Namens fiel weg.
$name = Slim::sanitizeFileName('Bildschirmfoto 2026-08-17 um 11.png');
expect($name)->toStartWith('Bildschirmfoto')
->and($name)->toEndWith('.png')
->and(strlen($name))->toBeLessThanOrEqual(33);
});
it('ersetzt Leerzeichen, damit der Name ohne Encoding in eine URL passt', function (): void {
expect(Slim::sanitizeFileName('Bild Kersrin.jpeg'))->toBe('Bild-Kersrin.jpeg')
// Geschuetztes Leerzeichen (macOS-Screenshots) zaehlt ebenfalls als Whitespace
->and(Slim::sanitizeFileName("Foto\u{00A0}Neu.png"))->toBe('Foto-Neu.png');
});
it('laesst kurze, saubere Namen unveraendert', function (): void {
expect(Slim::sanitizeFileName('IMG_6524.jpeg'))->toBe('IMG_6524.jpeg')
->and(Slim::sanitizeFileName('ruth-thomas.jpg'))->toBe('ruth-thomas.jpg');
});
it('entfernt unerlaubte Zeichen und Punktfolgen', function (): void {
expect(Slim::sanitizeFileName('re/port*?.png'))->toBe('report.png')
->and(Slim::sanitizeFileName('archiv..tar.gz'))->toBe('archivtar.gz');
});
it('liefert auch ohne verwertbaren Namen etwas Brauchbares', function (): void {
expect(Slim::sanitizeFileName('***.png'))->toBe('datei.png')
->and(Slim::sanitizeFileName('bild'))->toBe('bild');
});