From 3692fdadd2563655fd440eddc48be43abfd4f804 Mon Sep 17 00:00:00 2001 From: Martin Grigoryan Date: Wed, 17 Dec 2025 19:56:43 +0400 Subject: [PATCH 1/3] Enhance UploadedFile to replace blacklisted extensions with configurable whitelisted formats --- src/Libraries/Storage/UploadedFile.php | 58 ++++++++++++++----- .../Libraries/Storage/UploadedFileTest.php | 47 +++++++++++++++ tests/_root/shared/config/uploads.php | 12 ++++ 3 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 tests/_root/shared/config/uploads.php diff --git a/src/Libraries/Storage/UploadedFile.php b/src/Libraries/Storage/UploadedFile.php index 390ae0052..02b599584 100644 --- a/src/Libraries/Storage/UploadedFile.php +++ b/src/Libraries/Storage/UploadedFile.php @@ -23,6 +23,7 @@ use Quantum\Config\Exceptions\ConfigException; use Quantum\App\Exceptions\BaseException; use Quantum\Di\Exceptions\DiException; +use Quantum\Loader\Setup; use Gumlet\ImageResizeException; use ReflectionException; use Gumlet\ImageResize; @@ -99,15 +100,13 @@ class UploadedFile extends SplFileInfo ]; /** - * Blacklisted extensions + * Allowed mime types => allowed extensions map * @var array */ - protected $blacklistedExtensions = [ - 'php([0-9])?', 'pht', 'phar', 'phpt', 'pgif', 'phtml', 'phtm', 'phps', - 'cgi', 'inc', 'env', 'htaccess', 'htpasswd', 'config', 'conf', - 'bat', 'exe', 'msi', 'cmd', 'dll', 'sh', 'com', 'app', 'sys', 'drv', - 'pl', 'jar', 'jsp', 'js', 'vb', 'vbscript', 'wsf', 'asp', 'py', - 'cer', 'csr', 'crt', + protected $allowedMimeTypes = [ + 'image/jpeg' => ['jpg', 'jpeg'], + 'image/png' => ['png'], + 'application/pdf' => ['pdf'], ]; /** @@ -130,9 +129,22 @@ public function __construct(array $meta) $this->originalName = $meta['name']; $this->errorCode = $meta['error']; + $this->loadAllowedMimeTypesFromConfig(); + parent::__construct($meta['tmp_name']); } + /** + * Overrides the default allowed mime types => extensions map + * @param array $allowedMimeTypes + * @return $this + */ + public function setAllowedMimeTypes(array $allowedMimeTypes): UploadedFile + { + $this->allowedMimeTypes = $allowedMimeTypes; + return $this; + } + /** * Get name * @return string @@ -265,7 +277,7 @@ public function save(string $dest, bool $overwrite = false): bool throw FileUploadException::fileNotFound($this->getPathname()); } - if (!$this->whitelisted($this->getExtension())) { + if (!$this->allowed($this->getExtension(), $this->getMimeType())) { throw FileUploadException::fileTypeNotAllowed($this->getExtension()); } @@ -377,17 +389,35 @@ protected function moveUploadedFile(string $filePath): bool } /** - * Whitelist the file extension + * Validates upload against allowed mime types => extensions map * @param string $extension + * @param string $mimeType * @return bool */ - protected function whitelisted(string $extension): bool + protected function allowed(string $extension, string $mimeType): bool { - if (!preg_match('/(' . implode('|', $this->blacklistedExtensions) . ')$/i', $extension)) { - return true; - } + $extension = strtolower($extension); + $mimeType = strtolower($mimeType); - return false; + return isset($this->allowedMimeTypes[$mimeType]) && + in_array($extension, $this->allowedMimeTypes[$mimeType], true); + } + + /** + * Loads allowed mime types from config (shared/config/uploads.php) if present. + * @return void + */ + protected function loadAllowedMimeTypesFromConfig(): void + { + if (!config()->has('uploads')) { + config()->import(new Setup('config', 'uploads')); + } + + $allowedMimeTypesMap = config()->get('uploads.allowed_mime_types'); + + if (is_array($allowedMimeTypesMap)) { + $this->allowedMimeTypes = $allowedMimeTypesMap; + } } /** diff --git a/tests/Unit/Libraries/Storage/UploadedFileTest.php b/tests/Unit/Libraries/Storage/UploadedFileTest.php index fc14f4d5a..014fa8aea 100644 --- a/tests/Unit/Libraries/Storage/UploadedFileTest.php +++ b/tests/Unit/Libraries/Storage/UploadedFileTest.php @@ -48,6 +48,10 @@ public function tearDown(): void if (file_exists(base_dir() . DS . 'foo.jpg')) { unlink(base_dir() . DS . 'foo.jpg'); } + + if (file_exists(base_dir() . DS . 'foo.txt')) { + unlink(base_dir() . DS . 'foo.txt'); + } } public function testUploadedFileConstructor() @@ -146,6 +150,49 @@ public function testUploadedFileUploadAndSave() $this->assertTrue(file_exists(base_dir() . DS . $uploadedFile->getNameWithExtension())); } + public function testUploadedFileUploadNotAllowedExtensionForMimeType() + { + $fileMeta = $this->fileMeta; + $fileMeta['name'] = 'foo.php'; + + $uploadedFile = new UploadedDouble($fileMeta); + + $this->expectException(FileUploadException::class); + $this->expectExceptionMessage('The file type `php` is not allowed.'); + + $uploadedFile->save(base_dir()); + } + + public function testUploadedFileUploadNotAllowedMimeType() + { + $fileMeta = $this->fileMeta; + $fileMeta['name'] = 'foo.jpg'; + $fileMeta['tmp_name'] = base_dir() . DS . 'journal.log'; + + $uploadedFile = new UploadedDouble($fileMeta); + + $this->expectException(FileUploadException::class); + $this->expectExceptionMessage('The file type `jpg` is not allowed.'); + + $uploadedFile->save(base_dir()); + } + + public function testUploadedFileUploadAllowedAfterSetterOverride() + { + $fileMeta = $this->fileMeta; + $fileMeta['name'] = 'foo.txt'; + $fileMeta['tmp_name'] = base_dir() . DS . 'journal.log'; + + $uploadedFile = new UploadedDouble($fileMeta); + + $uploadedFile->setAllowedMimeTypes([ + $uploadedFile->getMimeType() => ['txt'], + ]); + + $this->assertTrue($uploadedFile->save(base_dir())); + $this->assertTrue(file_exists(base_dir() . DS . $uploadedFile->getNameWithExtension())); + } + public function testUploadedFileSaveAndTryOverwrite() { $uploadedFile = new UploadedDouble($this->fileMeta); diff --git a/tests/_root/shared/config/uploads.php b/tests/_root/shared/config/uploads.php new file mode 100644 index 000000000..66f97d8f6 --- /dev/null +++ b/tests/_root/shared/config/uploads.php @@ -0,0 +1,12 @@ + extensions map for UploadedFile validation. + */ + 'allowed_mime_types' => [ + 'image/jpeg' => ['jpg', 'jpeg'], + 'image/png' => ['png'], + 'application/pdf' => ['pdf'], + ], +]; \ No newline at end of file From a4755d89e29e0de472209cdfd5ac28221c77a3b1 Mon Sep 17 00:00:00 2001 From: Martin Grigoryan Date: Fri, 19 Dec 2025 11:25:58 +0400 Subject: [PATCH 2/3] feat: enhance UploadedFile MIME type management - Add merge support to setAllowedMimeTypes() (default: merge=true) - Refactor shared logic into protected setAllowedMimeTypesMap() - Add config validation with proper exception handling --- .../Exceptions/FileUploadException.php | 10 +++++++ src/Libraries/Storage/UploadedFile.php | 29 ++++++++++++++++--- tests/_root/shared/config/uploads.php | 16 +++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/Libraries/Storage/Exceptions/FileUploadException.php b/src/Libraries/Storage/Exceptions/FileUploadException.php index 6f1256a43..6cf75aec1 100644 --- a/src/Libraries/Storage/Exceptions/FileUploadException.php +++ b/src/Libraries/Storage/Exceptions/FileUploadException.php @@ -15,6 +15,7 @@ namespace Quantum\Libraries\Storage\Exceptions; use Quantum\Libraries\Storage\Enums\ExceptionMessages; +use Quantum\App\Enums\ExceptionMessages as BaseExceptionMessages; use Quantum\App\Exceptions\BaseException; /** @@ -32,4 +33,13 @@ public static function fileTypeNotAllowed(string $name): FileUploadException { return new static(_message(ExceptionMessages::FILE_TYPE_NOT_ALLOWED, [$name]), E_WARNING); } + + /** + * @param string $name + * @return FileUploadException + */ + public static function incorrectMimeTypesConfig(string $name): FileUploadException + { + return new static(_message(BaseExceptionMessages::MISSING_CONFIG, $name), E_ERROR); + } } \ No newline at end of file diff --git a/src/Libraries/Storage/UploadedFile.php b/src/Libraries/Storage/UploadedFile.php index 02b599584..b0a0c7124 100644 --- a/src/Libraries/Storage/UploadedFile.php +++ b/src/Libraries/Storage/UploadedFile.php @@ -135,13 +135,14 @@ public function __construct(array $meta) } /** - * Overrides the default allowed mime types => extensions map + * Sets the allowed mime types => extensions map * @param array $allowedMimeTypes + * @param bool $merge * @return $this */ - public function setAllowedMimeTypes(array $allowedMimeTypes): UploadedFile + public function setAllowedMimeTypes(array $allowedMimeTypes, bool $merge = true): UploadedFile { - $this->allowedMimeTypes = $allowedMimeTypes; + $this->setAllowedMimeTypesMap($allowedMimeTypes, $merge); return $this; } @@ -406,6 +407,7 @@ protected function allowed(string $extension, string $mimeType): bool /** * Loads allowed mime types from config (shared/config/uploads.php) if present. * @return void + * @throws FileUploadException */ protected function loadAllowedMimeTypesFromConfig(): void { @@ -415,8 +417,27 @@ protected function loadAllowedMimeTypesFromConfig(): void $allowedMimeTypesMap = config()->get('uploads.allowed_mime_types'); + if ($allowedMimeTypesMap !== null && !is_array($allowedMimeTypesMap)) { + throw FileUploadException::incorrectMimeTypesConfig('uploads.allowed_mime_types'); + } + if (is_array($allowedMimeTypesMap)) { - $this->allowedMimeTypes = $allowedMimeTypesMap; + $this->setAllowedMimeTypesMap($allowedMimeTypesMap); + } + } + + /** + * Sets the allowed mime types => extensions map + * @param array $allowedMimeTypes + * @param bool $merge + * @return void + */ + protected function setAllowedMimeTypesMap(array $allowedMimeTypes, bool $merge = true): void + { + if ($merge) { + $this->allowedMimeTypes = array_merge_recursive($this->allowedMimeTypes, $allowedMimeTypes); + } else { + $this->allowedMimeTypes = $allowedMimeTypes; } } diff --git a/tests/_root/shared/config/uploads.php b/tests/_root/shared/config/uploads.php index 66f97d8f6..f8bca8a9b 100644 --- a/tests/_root/shared/config/uploads.php +++ b/tests/_root/shared/config/uploads.php @@ -5,8 +5,22 @@ * Allowed mime types => extensions map for UploadedFile validation. */ 'allowed_mime_types' => [ + // Images 'image/jpeg' => ['jpg', 'jpeg'], 'image/png' => ['png'], - 'application/pdf' => ['pdf'], + 'image/gif' => ['gif'], + 'image/webp' => ['webp'], + 'image/svg+xml' => ['svg'], + // Audio + 'audio/mpeg' => ['mp3'], + 'audio/wav' => ['wav'], + 'audio/ogg' => ['ogg'], + 'audio/webm' => ['weba'], + // Video + 'video/mp4' => ['mp4'], + 'video/mpeg' => ['mpeg', 'mpg'], + 'video/quicktime' => ['mov'], + 'video/webm' => ['webm'], + 'video/x-msvideo' => ['avi'], ], ]; \ No newline at end of file From a165599adbbe570f5487415193db7a862f43172d Mon Sep 17 00:00:00 2001 From: Martin Grigoryan Date: Fri, 19 Dec 2025 11:44:31 +0400 Subject: [PATCH 3/3] refactor: use base exception and improve method organization --- src/Libraries/Storage/Exceptions/FileUploadException.php | 3 +-- src/Libraries/Storage/UploadedFile.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Storage/Exceptions/FileUploadException.php b/src/Libraries/Storage/Exceptions/FileUploadException.php index 6cf75aec1..aa6c9cc9d 100644 --- a/src/Libraries/Storage/Exceptions/FileUploadException.php +++ b/src/Libraries/Storage/Exceptions/FileUploadException.php @@ -15,7 +15,6 @@ namespace Quantum\Libraries\Storage\Exceptions; use Quantum\Libraries\Storage\Enums\ExceptionMessages; -use Quantum\App\Enums\ExceptionMessages as BaseExceptionMessages; use Quantum\App\Exceptions\BaseException; /** @@ -40,6 +39,6 @@ public static function fileTypeNotAllowed(string $name): FileUploadException */ public static function incorrectMimeTypesConfig(string $name): FileUploadException { - return new static(_message(BaseExceptionMessages::MISSING_CONFIG, $name), E_ERROR); + return new static(_message(ExceptionMessages::MISSING_CONFIG, $name), E_ERROR); } } \ No newline at end of file diff --git a/src/Libraries/Storage/UploadedFile.php b/src/Libraries/Storage/UploadedFile.php index b0a0c7124..2038ca05f 100644 --- a/src/Libraries/Storage/UploadedFile.php +++ b/src/Libraries/Storage/UploadedFile.php @@ -418,7 +418,7 @@ protected function loadAllowedMimeTypesFromConfig(): void $allowedMimeTypesMap = config()->get('uploads.allowed_mime_types'); if ($allowedMimeTypesMap !== null && !is_array($allowedMimeTypesMap)) { - throw FileUploadException::incorrectMimeTypesConfig('uploads.allowed_mime_types'); + throw FileUploadException::incorrectMimeTypesConfig('uploads'); } if (is_array($allowedMimeTypesMap)) {