diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 69fc8a1b3fa0c..a978259ee58f9 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.3.0 +----- + + * added the `extensions` option to the `File` constraint + 5.2.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/File.php b/src/Symfony/Component/Validator/Constraints/File.php index 65e5bddf2c318..83d35f0218663 100644 --- a/src/Symfony/Component/Validator/Constraints/File.php +++ b/src/Symfony/Component/Validator/Constraints/File.php @@ -32,6 +32,7 @@ class File extends Constraint const EMPTY_ERROR = '5d743385-9775-4aa5-8ff5-495fb1e60137'; const TOO_LARGE_ERROR = 'df8637af-d466-48c6-a59d-e7126250a654'; const INVALID_MIME_TYPE_ERROR = '744f00bc-4389-4c74-92de-9a43cde55534'; + const INVALID_EXTENSION_ERROR = '4f89fcfb-f18a-4749-936b-b290860b9a8c'; protected static $errorNames = [ self::NOT_FOUND_ERROR => 'NOT_FOUND_ERROR', @@ -39,15 +40,18 @@ class File extends Constraint self::EMPTY_ERROR => 'EMPTY_ERROR', self::TOO_LARGE_ERROR => 'TOO_LARGE_ERROR', self::INVALID_MIME_TYPE_ERROR => 'INVALID_MIME_TYPE_ERROR', + self::INVALID_EXTENSION_ERROR => 'INVALID_EXTENSION_ERROR', ]; public $binaryFormat; public $mimeTypes = []; + public $extensions = []; public $notFoundMessage = 'The file could not be found.'; public $notReadableMessage = 'The file is not readable.'; public $maxSizeMessage = 'The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.'; public $mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.'; public $disallowEmptyMessage = 'An empty file is not allowed.'; + public $extensionsMessage = 'The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.'; public $uploadIniSizeErrorMessage = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'; public $uploadFormSizeErrorMessage = 'The file is too large.'; @@ -65,17 +69,20 @@ class File extends Constraint * * @param int|string|null $maxSize * @param string[]|string|null $mimeTypes + * @param string[]|string|null $extensions */ public function __construct( array $options = null, $maxSize = null, bool $binaryFormat = null, $mimeTypes = null, + $extensions = null, string $notFoundMessage = null, string $notReadableMessage = null, string $maxSizeMessage = null, string $mimeTypesMessage = null, string $disallowEmptyMessage = null, + string $extensionsMessage = null, string $uploadIniSizeErrorMessage = null, string $uploadFormSizeErrorMessage = null, @@ -94,17 +101,22 @@ public function __construct( if (null !== $mimeTypes && !\is_array($mimeTypes) && !\is_string($mimeTypes)) { throw new \TypeError(sprintf('"%s": Expected argument $mimeTypes to be either null, an array or a string, got "%s".', __METHOD__, get_debug_type($mimeTypes))); } + if (null !== $extensions && !\is_array($extensions) && !\is_string($extensions)) { + throw new \TypeError(sprintf('"%s": Expected argument $extensions to be either null, an array or a string, got "%s".', __METHOD__, get_debug_type($extensions))); + } parent::__construct($options, $groups, $payload); $this->maxSize = $maxSize ?? $this->maxSize; $this->binaryFormat = $binaryFormat ?? $this->binaryFormat; $this->mimeTypes = $mimeTypes ?? $this->mimeTypes; + $this->extensions = $extensions ?? $this->extensions; $this->notFoundMessage = $notFoundMessage ?? $this->notFoundMessage; $this->notReadableMessage = $notReadableMessage ?? $this->notReadableMessage; $this->maxSizeMessage = $maxSizeMessage ?? $this->maxSizeMessage; $this->mimeTypesMessage = $mimeTypesMessage ?? $this->mimeTypesMessage; $this->disallowEmptyMessage = $disallowEmptyMessage ?? $this->disallowEmptyMessage; + $this->extensionsMessage = $extensionsMessage ?? $this->extensionsMessage; $this->uploadIniSizeErrorMessage = $uploadIniSizeErrorMessage ?? $this->uploadIniSizeErrorMessage; $this->uploadFormSizeErrorMessage = $uploadFormSizeErrorMessage ?? $this->uploadFormSizeErrorMessage; $this->uploadPartialErrorMessage = $uploadPartialErrorMessage ?? $this->uploadPartialErrorMessage; diff --git a/src/Symfony/Component/Validator/Constraints/FileValidator.php b/src/Symfony/Component/Validator/Constraints/FileValidator.php index e5a756d893c8f..8692fe5620f45 100644 --- a/src/Symfony/Component/Validator/Constraints/FileValidator.php +++ b/src/Symfony/Component/Validator/Constraints/FileValidator.php @@ -204,6 +204,30 @@ public function validate($value, Constraint $constraint) ->setCode(File::INVALID_MIME_TYPE_ERROR) ->addViolation(); } + + if ($constraint->extensions) { + if ($value instanceof FileObject) { + $fileExtension = $value->getExtension(); + } else { + $fileExtension = (new FileObject($value))->getExtension(); + } + + $extensions = (array) $constraint->extensions; + + foreach ($extensions as $extension) { + if ($extension === $fileExtension) { + return; + } + } + + $this->context->buildViolation($constraint->extensionsMessage) + ->setParameter('{{ file }}', $this->formatValue($path)) + ->setParameter('{{ extension }}', $this->formatValue($fileExtension)) + ->setParameter('{{ extensions }}', $this->formatValues($extensions)) + ->setParameter('{{ name }}', $this->formatValue($basename)) + ->setCode(File::INVALID_EXTENSION_ERROR) + ->addViolation(); + } } private static function moreDecimalsThan(string $double, int $numberOfDecimals): bool diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php index 9894bf266c41a..12a29c836ac42 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php @@ -455,6 +455,73 @@ public function provideDisallowEmptyConstraints(): iterable } } + public function testValidExtension() + { + $file = $this + ->getMockBuilder('Symfony\Component\HttpFoundation\File\File') + ->setConstructorArgs([__DIR__.'/Fixtures/foo']) + ->getMock(); + $file + ->expects($this->once()) + ->method('getPathname') + ->willReturn($this->path); + $file + ->expects($this->once()) + ->method('getExtension') + ->willReturn('jpg'); + + $constraint = new File([ + 'extensions' => ['png', 'jpg'], + ]); + + $this->validator->validate($file, $constraint); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideExtensionConstraints + */ + public function testInvalidExtension(File $constraint) + { + $file = $this + ->getMockBuilder('Symfony\Component\HttpFoundation\File\File') + ->setConstructorArgs([__DIR__.'/Fixtures/foo']) + ->getMock(); + $file + ->expects($this->once()) + ->method('getPathname') + ->willReturn($this->path); + $file + ->expects($this->once()) + ->method('getExtension') + ->willReturn('pdf'); + + $this->validator->validate($file, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ extension }}', '"pdf"') + ->setParameter('{{ extensions }}', '"png", "jpg"') + ->setParameter('{{ file }}', '"'.$this->path.'"') + ->setParameter('{{ name }}', '"'.basename($this->path).'"') + ->setCode(File::INVALID_EXTENSION_ERROR) + ->assertRaised(); + } + + public function provideExtensionConstraints(): iterable + { + yield 'Doctrine style' => [new File([ + 'extensions' => ['png', 'jpg'], + 'extensionsMessage' => 'myMessage', + ])]; + + if (\PHP_VERSION_ID >= 80000) { + yield 'named arguments' => [ + eval('return new \Symfony\Component\Validator\Constraints\File(extensions: ["png", "jpg"], extensionsMessage: "myMessage");'), + ]; + } + } + /** * @dataProvider uploadedFileErrorProvider */
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: