['application/zip', 'application/x-zip', 'application/x-zip-compressed'], 'rar' => ['application/vnd.rar', 'application/x-rar', 'application/x-rar-compressed'], '7z' => ['application/x-7z-compressed'], 'tar' => ['application/x-tar'], 'gz' => ['application/gzip', 'application/x-gzip'], 'bz2' => ['application/x-bzip2', 'application/x-bzip'], 'pdf' => ['application/pdf'], 'doc' => ['application/msword', 'application/x-ole-storage', 'application/CDFV2'], 'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip'], 'xls' => ['application/vnd.ms-excel', 'application/x-ole-storage', 'application/CDFV2'], 'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip'], 'ppt' => ['application/vnd.ms-powerpoint', 'application/x-ole-storage', 'application/CDFV2'], 'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/zip'], 'jpg' => ['image/jpeg'], 'jpeg' => ['image/jpeg'], 'png' => ['image/png'], 'gif' => ['image/gif'], 'webp' => ['image/webp'], ]; public function __construct( private readonly Config $config, private readonly Mime $mime ) { } /** * Return extensions with an explicit MIME policy. * * @return string[] */ public static function getSupportedExtensions(): array { return array_keys(self::MIME_TYPES); } /** * @return array{extension: string, mime_type: string} * @throws LocalizedException */ public function validate(array $file): array { $name = (string) ($file['name'] ?? ''); $tmpName = (string) ($file['tmp_name'] ?? ''); $size = (int) ($file['size'] ?? 0); $error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE); if ($error !== UPLOAD_ERR_OK || $tmpName === '' || !is_uploaded_file($tmpName)) { throw new LocalizedException(__('The file "%1" was not uploaded correctly.', $name)); } if ($size <= 0 || $size > $this->config->getMaximumFileSizeBytes()) { throw new LocalizedException(__('The file "%1" exceeds the configured size limit or is empty.', $name)); } $extension = strtolower((string) pathinfo($name, PATHINFO_EXTENSION)); $parts = preg_split('/\./', strtolower($name), -1, PREG_SPLIT_NO_EMPTY) ?: []; if (array_intersect($parts, self::BLOCKED_EXTENSIONS)) { throw new LocalizedException(__('The file "%1" contains a forbidden extension.', $name)); } if ($extension === '' || !in_array($extension, $this->config->getAllowedExtensions(), true)) { throw new LocalizedException(__('The extension of "%1" is not allowed.', $name)); } if (!isset(self::MIME_TYPES[$extension])) { throw new LocalizedException(__('No secure MIME policy is defined for extension "%1".', $extension)); } $mimeType = $this->mime->getMimeType($tmpName); if (!in_array($mimeType, self::MIME_TYPES[$extension], true)) { throw new LocalizedException( __('The MIME type "%1" does not match the .%2 extension.', $mimeType, $extension) ); } return ['extension' => $extension, 'mime_type' => $mimeType]; } }