stageUploadedFile($file, $subdir); if ($staged === null) { return null; } $this->finalizeStagedFile($staged); return $staged['final_name']; } public function stageUploadedFile($file, string $subdir): ?array { if (!$file instanceof UploadedFile) { return null; } if (!$file->isValid() || $file->hasMoved()) { return null; } $size = (int) ($file->getSize() ?? 0); if ($size <= 0) { return null; } $mime = strtolower((string) $file->getMimeType()); $ext = strtolower((string) $file->getClientExtension()); if (!in_array($mime, self::ALLOWED_MIMES, true) || !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { throw new \RuntimeException('Unsupported file type. Use PDF, JPG, or PNG.'); } if ($size > self::MAX_BYTES) { throw new \RuntimeException('File too large. Maximum size is 5 MB.'); } $name = $file->getRandomName(); $tmpSubdir = '_tmp' . DIRECTORY_SEPARATOR . trim($subdir, '/'); $tmpDir = $this->ensureSubdir($tmpSubdir); $tmpName = 'pending-' . bin2hex(random_bytes(8)) . '-' . $name; $file->move($tmpDir, $tmpName); return [ 'subdir' => trim($subdir, '/'), 'final_name' => $name, 'temporary_subdir' => $tmpSubdir, 'temporary_name' => $tmpName, 'temporary_path' => $tmpDir . DIRECTORY_SEPARATOR . $tmpName, ]; } public function finalizeStagedFile(array $staged): string { $tmpPath = (string)($staged['temporary_path'] ?? ''); $finalName = basename((string)($staged['final_name'] ?? '')); $subdir = (string)($staged['subdir'] ?? ''); if ($tmpPath === '' || $finalName === '' || $subdir === '' || !is_file($tmpPath)) { throw new \RuntimeException('Temporary upload is missing.'); } $finalDir = $this->ensureSubdir($subdir); $finalPath = $finalDir . DIRECTORY_SEPARATOR . $finalName; if (!@rename($tmpPath, $finalPath)) { throw new \RuntimeException('Unable to finalize uploaded evidence.'); } return $finalName; } public function discardStagedFile(?array $staged): void { if ($staged === null) { return; } $tmpPath = (string)($staged['temporary_path'] ?? ''); if ($tmpPath !== '' && is_file($tmpPath)) { @unlink($tmpPath); } } public function resolvePath(string $subdir, string $filename): ?string { $safeName = basename($filename); if ($safeName === '') { return null; } $path = $this->ensureSubdir($subdir) . DIRECTORY_SEPARATOR . $safeName; return is_file($path) ? $path : null; } public function ensureSubdir(string $subdir): string { $path = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . trim($subdir, '/'); if (!is_dir($path) && !mkdir($path, 0775, true) && !is_dir($path)) { throw new \RuntimeException('Unable to prepare upload directory.'); } return $path; } public function detectMime(string $path): string { if (function_exists('finfo_open')) { $handle = finfo_open(FILEINFO_MIME_TYPE); if ($handle) { $mime = finfo_file($handle, $path); finfo_close($handle); if (is_string($mime) && $mime !== '') { return $mime; } } } if (function_exists('mime_content_type')) { $mime = mime_content_type($path); if (is_string($mime) && $mime !== '') { return $mime; } } return 'application/octet-stream'; } }