["\xFF\xD8\xFF"], 'image/png' => ["\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"], 'image/webp' => ["RIFF"], ]; private const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp']; public function __construct(private readonly int $maxSizeBytes = 5_242_880) {} public function validateUploadedFile(UploadedFile $file): string { if ($file->getSize() > $this->maxSizeBytes) { throw new AppException(ErrorCodes::ERR_FILE_002, null, 422); } $binaryContent = (string) file_get_contents($file->getPathname()); return $this->validate($binaryContent, $file->getClientOriginalName()); } public function validate(string $binaryContent, string $claimedFilename): string { if (strlen($binaryContent) > $this->maxSizeBytes) { throw new AppException(ErrorCodes::ERR_FILE_002, null, 422); } $detected = false; foreach (self::ALLOWED_SIGNATURES as $signatures) { foreach ($signatures as $sig) { if (str_starts_with($binaryContent, $sig)) { $detected = true; break 2; } } } if (!$detected) { throw new AppException(ErrorCodes::ERR_FILE_001, null, 422); } return $this->sanitizeFilename($claimedFilename); } public function sanitizeFilename(string $filename): string { $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '', basename($filename)); if (empty($safeName) || str_contains($safeName, '..')) { throw new AppException(ErrorCodes::ERR_FILE_001, null, 422); } $ext = strtolower(pathinfo($safeName, PATHINFO_EXTENSION)); if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) { throw new AppException(ErrorCodes::ERR_FILE_001, null, 422); } return bin2hex(random_bytes(16)) . '.' . $ext; } public function detectMimeType(string $filePath): string { $handle = fopen($filePath, 'rb'); $header = fread($handle, 12); fclose($handle); foreach (self::ALLOWED_SIGNATURES as $mime => $signatures) { foreach ($signatures as $sig) { if (str_starts_with($header, $sig)) { return $mime; } } } throw new AppException(ErrorCodes::ERR_FILE_001, 'نوع فایل پشتیبانی نمی‌شود', 422); } }