Files
awo-hamburg-intranet/module/tasks/lib/TaskUploadPolicy.php
Moritz Weinmann 743476f64a Module tasks + knowledgecenter_update aus Kundenprojekt übernehmen (bereinigt)
Beide Module wurden aus einem anderen Kundenprojekt kopiert und bereinigt:

Knowledgecenter:
- Bild-Cache (1.985 Dateien) geleert, Ordnerstruktur mit .gitkeep behalten
- Files-Gallery-Lizenzschlüssel entfernt (config.php)
- Kundenspezifische Kategorien gelöscht: Teil I/II/III QM-Struktur,
  nummerierte QM-Kapitel, Gremien (Präsidium/Finanzausschuss/Landesausschuss),
  Sitzungsservice-Serie, Protokoll-Abteilungen, Testeinträge
- 56 generische Kategorien bleiben (Downloads, Onboarding, Personalthemen …)

Tasks:
- Alle Betriebsdaten gelöscht (114 Tasks, 914 Submissions, 579 Assignments)
- Task-Definitionen und Verlinkungen geleert
- Kategoriestruktur bleibt (Allgemein, IT, Personal, Buchhaltung …)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 09:20:02 +02:00

161 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
/**
* Zentrale Regeln für Datei-Uploads in Formular-Feldern vom Typ "file".
*
* Eine einzige Quelle der Wahrheit für erlaubte Extensions, MIME-Prefixe und
* Limits. Das Nutzer-Formular liest diese Werte (accept-Attribut, Hinweistext),
* der Server validiert mit denselben Regeln. Bewusst dokumenttypen-lastig —
* in der Praxis sind Uploads PDFs, Screenshots und Office-Dateien.
*
* Modul-lokal gehalten (kein use auf module/tickets), weil module/tasks keinen
* Namespace verwendet und beide Module entkoppelt bleiben sollen.
*/
class TaskUploadPolicy
{
public const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
public const MAX_FILE_COUNT = 5;
/**
* Extensions werden case-insensitiv gegen das Dateinamen-Suffix geprüft.
*
* @var string[]
*/
public const ALLOWED_EXTENSIONS = [
'pdf',
'png', 'jpg', 'jpeg', 'webp',
'doc', 'docx',
'xls', 'xlsx',
'ppt', 'pptx',
];
/**
* Weiche MIME-Plausibilitätsprüfung. Wir lehnen nur ab, wenn fileinfo
* verfügbar ist UND der erkannte Typ zu keinem Prefix passt. Fehlt fileinfo,
* überspringt der Storage diesen Check, damit legitime Uploads nicht blind
* blockiert werden.
*
* @var string[]
*/
public const ALLOWED_MIME_PREFIXES = [
'application/pdf',
'application/msword',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.',
'image/png',
'image/jpeg',
'image/webp',
];
public const HUMAN_READABLE_FORMATS_SHORT = 'PDF, Office, Bilder';
/**
* Deterministisch sortiertes accept="…"-Attribut für <input type="file">.
*/
public static function acceptAttribute(): string
{
$parts = [];
foreach (self::ALLOWED_EXTENSIONS as $ext) {
$parts[] = '.' . $ext;
}
return implode(',', $parts);
}
/**
* Deterministische, sichere MIME-Zuordnung je erlaubter Extension. Wird
* sowohl beim Speichern als auch beim Ausliefern genutzt — der vom Client
* gemeldete MIME-Typ wird bewusst NIE verwendet (sonst Stored-XSS, z. B.
* als Bild getarntes SVG mit Skript). Nicht zuordenbare Endungen landen bei
* application/octet-stream (Download statt Inline-Render).
*
* @var array<string,string>
*/
public const EXTENSION_MIME_MAP = [
'pdf' => 'application/pdf',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
];
/**
* MIME-Typen, die gefahrlos inline im Browser angezeigt werden dürfen.
* Bewusst ohne image/svg+xml, text/*, application/xml o. Ä.
*
* @var string[]
*/
public const INLINE_SAFE_MIMES = [
'image/png',
'image/jpeg',
'image/webp',
'application/pdf',
];
public static function mimeForFilename(string $filename): string
{
$dot = strrpos($filename, '.');
if ($dot !== false) {
$ext = strtolower(substr($filename, $dot + 1));
if (isset(self::EXTENSION_MIME_MAP[$ext])) {
return self::EXTENSION_MIME_MAP[$ext];
}
}
return 'application/octet-stream';
}
public static function isInlineSafeMime(string $mime): bool
{
return in_array($mime, self::INLINE_SAFE_MIMES, true);
}
public static function isExtensionAllowed(string $filename): bool
{
$dot = strrpos($filename, '.');
if ($dot === false) {
return false;
}
$ext = strtolower(substr($filename, $dot + 1));
return in_array($ext, self::ALLOWED_EXTENSIONS, true);
}
public static function isMimeAllowedIfDetected(?string $mime): bool
{
if ($mime === null || $mime === '') {
return true;
}
foreach (self::ALLOWED_MIME_PREFIXES as $prefix) {
if (strpos($mime, $prefix) === 0) {
return true;
}
}
return false;
}
public static function formatBytes(int $bytes): string
{
if ($bytes < 1024) {
return $bytes . ' B';
}
if ($bytes < 1024 * 1024) {
return number_format($bytes / 1024, 1, ',', '.') . ' KB';
}
return number_format($bytes / (1024 * 1024), 1, ',', '.') . ' MB';
}
}