Skip to content

[Validator] Add extensions option to File constraint #39063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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
5 changes: 5 additions & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

5.3.0
-----

* added the `extensions` option to the `File` constraint

5.2.0
-----

Expand Down
12 changes: 12 additions & 0 deletions src/Symfony/Component/Validator/Constraints/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,26 @@ 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',
self::NOT_READABLE_ERROR => 'NOT_READABLE_ERROR',
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.';
Expand All @@ -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,
Expand All @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions src/Symfony/Component/Validator/Constraints/FileValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$fileExtension = $value instanceof FileObject ? $value->getExtension() : (new FileObject($value))->getExtension();

} else {
$fileExtension = (new FileObject($value))->getExtension();
}

$extensions = (array) $constraint->extensions;

foreach ($extensions as $extension) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if(in_array($fileExtension, $extensions)) {
return;

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down