161 lines
4.8 KiB
PHP
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';
|
||
|
|
}
|
||
|
|
}
|