From 46ec54d52ddd7fc7440d5a7bca0cb42356d0db4b Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Thu, 9 Apr 2020 22:45:49 +0200 Subject: [PATCH] Validate PSR-compatible file upload Instead of converting the uploaded file object to an UploadedFile instance from Symfony, because the latter is compatible with Laravel's validation, let's re-implement the validation for the three rules we were using. The benefit: we can now avoid copying the uploaded file to a temporary location just to do the wrapping. In the next step, we will remove the temporary file and let the uploader / Intervention Image handle the PSR stream directly. --- framework/core/src/User/AvatarValidator.php | 76 +++++++++++++++++-- .../src/User/Command/UploadAvatarHandler.php | 11 +-- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/framework/core/src/User/AvatarValidator.php b/framework/core/src/User/AvatarValidator.php index 49addf194..19ee3ca0e 100644 --- a/framework/core/src/User/AvatarValidator.php +++ b/framework/core/src/User/AvatarValidator.php @@ -10,14 +10,76 @@ namespace Flarum\User; use Flarum\Foundation\AbstractValidator; +use Flarum\Foundation\ValidationException; +use Psr\Http\Message\UploadedFileInterface; +use Symfony\Component\Mime\MimeTypes; class AvatarValidator extends AbstractValidator { - protected $rules = [ - 'avatar' => [ - 'required', - 'mimes:jpeg,png,bmp,gif', - 'max:2048' - ] - ]; + /** + * Throw an exception if a model is not valid. + * + * @param array $attributes + */ + public function assertValid(array $attributes) + { + $this->assertFileRequired($attributes['avatar']); + $this->assertFileMimes($attributes['avatar']); + $this->assertFileSize($attributes['avatar']); + } + + protected function assertFileRequired(UploadedFileInterface $file) + { + if ($file->getError() !== UPLOAD_ERR_OK) { + $this->raise('required'); + } + } + + protected function assertFileMimes(UploadedFileInterface $file) + { + $allowedTypes = $this->getAllowedTypes(); + + // Block PHP files masquerading as images + $phpExtensions = ['php', 'php3', 'php4', 'php5', 'phtml']; + $fileExtension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION); + + if (in_array(trim(strtolower($fileExtension)), $phpExtensions)) { + $this->raise('mimes', [':values' => implode(', ', $allowedTypes)]); + } + + $guessedExtension = MimeTypes::getDefault()->getExtensions($file->getClientMediaType())[0] ?? null; + + if (! in_array($guessedExtension, $allowedTypes)) { + $this->raise('mimes', [':values' => implode(', ', $allowedTypes)]); + } + } + + protected function assertFileSize(UploadedFileInterface $file) + { + $maxSize = $this->getMaxSize(); + + if ($file->getSize() / 1024 > $maxSize) { + $this->raise('max.file', [':max' => $maxSize]); + } + } + + protected function raise($error, array $parameters = []) + { + $message = $this->translator->trans( + "validation.$error", + $parameters + [':attribute' => 'avatar'] + ); + + throw new ValidationException(['avatar' => $message]); + } + + protected function getMaxSize() + { + return 2048; + } + + protected function getAllowedTypes() + { + return ['jpeg', 'png', 'bmp', 'gif']; + } } diff --git a/framework/core/src/User/Command/UploadAvatarHandler.php b/framework/core/src/User/Command/UploadAvatarHandler.php index 02e9e04c2..1803c4cc5 100644 --- a/framework/core/src/User/Command/UploadAvatarHandler.php +++ b/framework/core/src/User/Command/UploadAvatarHandler.php @@ -18,7 +18,6 @@ use Flarum\User\Event\AvatarSaving; use Flarum\User\UserRepository; use Illuminate\Contracts\Events\Dispatcher; use Intervention\Image\ImageManager; -use Symfony\Component\HttpFoundation\File\UploadedFile; class UploadAvatarHandler { @@ -65,6 +64,7 @@ class UploadAvatarHandler * @param UploadAvatar $command * @return \Flarum\User\User * @throws \Flarum\User\Exception\PermissionDeniedException + * @throws \Flarum\Foundation\ValidationException */ public function handle(UploadAvatar $command) { @@ -82,15 +82,6 @@ class UploadAvatarHandler $file->moveTo($tmpFile); try { - $file = new UploadedFile( - $tmpFile, - $file->getClientFilename(), - $file->getClientMediaType(), - $file->getSize(), - $file->getError(), - true - ); - $this->validator->assertValid(['avatar' => $file]); $image = (new ImageManager)->make($tmpFile);