Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Libraries/Storage/Exceptions/FileUploadException.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,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(ExceptionMessages::MISSING_CONFIG, $name), E_ERROR);
}
}
77 changes: 64 additions & 13 deletions src/Libraries/Storage/UploadedFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'],
];

/**
Expand All @@ -130,9 +129,23 @@ public function __construct(array $meta)
$this->originalName = $meta['name'];
$this->errorCode = $meta['error'];

$this->loadAllowedMimeTypesFromConfig();

parent::__construct($meta['tmp_name']);
}

/**
* Sets the allowed mime types => extensions map
* @param array $allowedMimeTypes
* @param bool $merge
* @return $this
*/
public function setAllowedMimeTypes(array $allowedMimeTypes, bool $merge = true): UploadedFile
{
$this->setAllowedMimeTypesMap($allowedMimeTypes, $merge);
return $this;
}

/**
* Get name
* @return string
Expand Down Expand Up @@ -265,7 +278,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());
}

Expand Down Expand Up @@ -377,17 +390,55 @@ 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 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
* @throws FileUploadException
*/
protected function loadAllowedMimeTypesFromConfig(): void
{
if (!config()->has('uploads')) {
config()->import(new Setup('config', 'uploads'));
}

$allowedMimeTypesMap = config()->get('uploads.allowed_mime_types');

if ($allowedMimeTypesMap !== null && !is_array($allowedMimeTypesMap)) {
throw FileUploadException::incorrectMimeTypesConfig('uploads');
}

if (is_array($allowedMimeTypesMap)) {
Comment thread
armanist marked this conversation as resolved.
$this->setAllowedMimeTypesMap($allowedMimeTypesMap);
}
}

return false;
/**
* 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;
}
}

/**
Expand Down
47 changes: 47 additions & 0 deletions tests/Unit/Libraries/Storage/UploadedFileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand Down
26 changes: 26 additions & 0 deletions tests/_root/shared/config/uploads.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

return [
Comment thread
armanist marked this conversation as resolved.
/**
* Allowed mime types => extensions map for UploadedFile validation.
*/
'allowed_mime_types' => [
// Images
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'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'],
],
];