*/ private array $app; public function __construct() { $this->app = Bootstrap::config('app'); } /** * @param array $files * @param array> $uploadFields * @return array{uploads: array>>, errors: array} */ public function processUploads(array $files, array $uploadFields, string $applicationKey): array { $uploaded = []; $errors = []; foreach ($uploadFields as $key => $fieldConfig) { $file = $this->pickUploadedFile($files, $key); if ($file === null) { continue; } $errorCode = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE); if ($errorCode === UPLOAD_ERR_NO_FILE) { continue; } if ($errorCode !== UPLOAD_ERR_OK) { $errors[$key] = 'Upload fehlgeschlagen.'; continue; } $tmpName = (string) ($file['tmp_name'] ?? ''); if ($tmpName === '' || !is_uploaded_file($tmpName)) { $errors[$key] = 'Ungültige Upload-Datei.'; continue; } $size = (int) ($file['size'] ?? 0); $maxSize = (int) ($fieldConfig['max_size'] ?? $this->app['uploads']['max_size']); if ($size <= 0 || $size > $maxSize) { $errors[$key] = 'Datei ist zu groß oder leer.'; continue; } $safeName = $this->sanitizeFilename((string) ($file['name'] ?? 'datei')); $mime = $this->detectMime($tmpName); $safeName = $this->ensureExtensionForMime($safeName, $mime); $extension = strtolower(pathinfo($safeName, PATHINFO_EXTENSION)); $allowedExtensions = $fieldConfig['extensions'] ?? $this->app['uploads']['allowed_extensions']; if (!in_array($extension, $allowedExtensions, true)) { $errors[$key] = 'Dateityp ist nicht erlaubt.'; continue; } $allowedMimes = $fieldConfig['mimes'] ?? $this->app['uploads']['allowed_mimes']; if (!in_array($mime, $allowedMimes, true)) { $errors[$key] = 'MIME-Typ ist nicht erlaubt.'; continue; } $rand8 = bin2hex(random_bytes(4)); $baseDir = rtrim((string) $this->app['storage']['uploads'], '/'); $targetDir = sprintf('%s/%s/%s/%s', $baseDir, $applicationKey, $key, $rand8); if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) { $errors[$key] = 'Upload-Ordner konnte nicht erstellt werden.'; continue; } $targetName = $this->ensureUniqueName($targetDir, $safeName); $targetPath = $targetDir . '/' . $targetName; if (!move_uploaded_file($tmpName, $targetPath)) { $errors[$key] = 'Datei konnte nicht gespeichert werden.'; continue; } $relativePath = sprintf('%s/%s/%s/%s', $applicationKey, $key, $rand8, $targetName); $uploaded[$key][] = [ 'original_filename' => $safeName, 'stored_dir' => sprintf('%s/%s/%s', $applicationKey, $key, $rand8), 'stored_filename' => $targetName, 'relative_path' => $relativePath, 'mime' => $mime, 'size' => filesize($targetPath) ?: $size, 'uploaded_at' => date('c'), ]; } return ['uploads' => $uploaded, 'errors' => $errors]; } /** @param array $files */ private function pickUploadedFile(array $files, string $fieldKey): ?array { $primary = $this->extractFirstFile($files[$fieldKey] ?? null); $camera = $this->extractFirstFile($files[$fieldKey . '__camera'] ?? null); if (is_array($primary) && ((int) ($primary['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK)) { return $primary; } if (is_array($camera) && ((int) ($camera['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK)) { return $camera; } if (is_array($primary) && ((int) ($primary['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE)) { return $primary; } if (is_array($camera) && ((int) ($camera['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE)) { return $camera; } return null; } private function extractFirstFile(mixed $fileArray): ?array { if (!is_array($fileArray)) { return null; } if (isset($fileArray['name']) && is_array($fileArray['name'])) { return [ 'name' => $fileArray['name'][0] ?? '', 'type' => $fileArray['type'][0] ?? '', 'tmp_name' => $fileArray['tmp_name'][0] ?? '', 'error' => $fileArray['error'][0] ?? UPLOAD_ERR_NO_FILE, 'size' => $fileArray['size'][0] ?? 0, ]; } return $fileArray; } private function sanitizeFilename(string $name): string { $name = str_replace(["\0", '/', '\\'], '', $name); $name = trim($name); if ($name === '') { return 'datei.bin'; } $name = preg_replace('/[^A-Za-z0-9._ -]/', '_', $name) ?? 'datei.bin'; $name = preg_replace('/\s+/', ' ', $name) ?? $name; $name = trim($name, " ."); if ($name === '' || $name === '.' || $name === '..') { return 'datei.bin'; } if (strlen($name) > 120) { $ext = pathinfo($name, PATHINFO_EXTENSION); $base = pathinfo($name, PATHINFO_FILENAME); $base = substr($base, 0, 100); $name = $ext !== '' ? $base . '.' . $ext : $base; } return $name; } private function ensureUniqueName(string $dir, string $fileName): string { $candidate = $fileName; $ext = pathinfo($fileName, PATHINFO_EXTENSION); $base = pathinfo($fileName, PATHINFO_FILENAME); $i = 1; while (is_file($dir . '/' . $candidate)) { $suffix = '_' . $i; $candidate = $ext !== '' ? $base . $suffix . '.' . $ext : $base . $suffix; $i++; } return $candidate; } private function ensureExtensionForMime(string $fileName, string $mime): string { $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); if ($ext !== '') { return $fileName; } $map = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'application/pdf' => 'pdf', ]; $mapped = $map[$mime] ?? ''; if ($mapped === '') { return $fileName; } return $fileName . '.' . $mapped; } private function detectMime(string $path): string { $finfo = finfo_open(FILEINFO_MIME_TYPE); if ($finfo === false) { return 'application/octet-stream'; } $mime = finfo_file($finfo, $path); finfo_close($finfo); return is_string($mime) ? $mime : 'application/octet-stream'; } }