| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- <?php
- declare(strict_types=1);
- namespace App\Storage;
- use App\App\Bootstrap;
- final class FileUploadStore
- {
- /** @var array<string, mixed> */
- private array $app;
- public function __construct()
- {
- $this->app = Bootstrap::config('app');
- }
- /**
- * @param array<string, mixed> $files
- * @param array<string, array<string, mixed>> $uploadFields
- * @return array{uploads: array<string, array<int, array<string, mixed>>>, errors: array<string, string>}
- */
- 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<string, mixed> $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';
- }
- }
|