Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d13a183

Browse filesBrowse files
committed
feature #47710 [Validator] File: add option to check extension (dunglas)
This PR was squashed before being merged into the 6.2 branch. Discussion ---------- [Validator] File: add option to check extension | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | no <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | n/a | License | MIT | Doc PR | todo This patch adds an `extensions` option to the `File` constraint as an alternative to `mimeTypes` which checks the mime type of the file, its extension, and the consistency between them. I have a use case where I want to assert that: 1. the file is of a given mime type 2. the file has an extension, the extension is in the allow list, and the extension corresponds with the actual mime type of the content I added a new `extension` option to the `File` constraint to do so. Usage: ```php #[File(extensions: 'jpg')] // image.jpg is allowed, image.jpeg isn't, allowed mime types are autodetected, the content of the file is automatically checked #[File(extensions: ['xml' => ['text/xml', 'application/xml'], 'txt'])] // XML files are allowed as long as the extension is .XML, .txt files are allowed if their mime type is text (allowed mime type are auto-detected) ``` Commits ------- 1613e55 [Validator] File: add option to check extension
2 parents 70e0b99 + 1613e55 commit d13a183
Copy full SHA for d13a183

File tree

7 files changed

+155
-6
lines changed
Filter options

7 files changed

+155
-6
lines changed

‎src/Symfony/Component/Validator/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add the `When` constraint and validator
99
* Deprecate the "loose" e-mail validation mode, use "html5" instead
1010
* Add the `negate` option to the `Expression` constraint, to inverse the logic of the violation's creation
11+
* Add the `extensions` option to the `File` constraint as an alternative to `mimeTypes` which checks the mime type of the file, its extension, and the consistency between them
1112

1213
6.1
1314
---

‎src/Symfony/Component/Validator/Constraints/File.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Constraints/File.php
+12-1Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class File extends Constraint
3232
public const EMPTY_ERROR = '5d743385-9775-4aa5-8ff5-495fb1e60137';
3333
public const TOO_LARGE_ERROR = 'df8637af-d466-48c6-a59d-e7126250a654';
3434
public const INVALID_MIME_TYPE_ERROR = '744f00bc-4389-4c74-92de-9a43cde55534';
35+
public const INVALID_EXTENSION_ERROR = 'c8c7315c-6186-4719-8b71-5659e16bdcb7';
3536

3637
protected const ERROR_NAMES = [
3738
self::NOT_FOUND_ERROR => 'NOT_FOUND_ERROR',
@@ -48,10 +49,12 @@ class File extends Constraint
4849

4950
public $binaryFormat;
5051
public $mimeTypes = [];
52+
public array|string|null $extensions = [];
5153
public $notFoundMessage = 'The file could not be found.';
5254
public $notReadableMessage = 'The file is not readable.';
5355
public $maxSizeMessage = 'The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.';
5456
public $mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.';
57+
public string $extensionsMessage = 'The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.';
5558
public $disallowEmptyMessage = 'An empty file is not allowed.';
5659

5760
public $uploadIniSizeErrorMessage = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.';
@@ -65,6 +68,9 @@ class File extends Constraint
6568

6669
protected $maxSize;
6770

71+
/**
72+
* @param array<string, string|string[]>|string[]|string $extensions
73+
*/
6874
public function __construct(
6975
array $options = null,
7076
int|string $maxSize = null,
@@ -85,17 +91,22 @@ public function __construct(
8591
string $uploadExtensionErrorMessage = null,
8692
string $uploadErrorMessage = null,
8793
array $groups = null,
88-
mixed $payload = null
94+
mixed $payload = null,
95+
96+
array|string $extensions = null,
97+
string $extensionsMessage = null,
8998
) {
9099
parent::__construct($options, $groups, $payload);
91100

92101
$this->maxSize = $maxSize ?? $this->maxSize;
93102
$this->binaryFormat = $binaryFormat ?? $this->binaryFormat;
94103
$this->mimeTypes = $mimeTypes ?? $this->mimeTypes;
104+
$this->extensions = $extensions ?? $this->extensions;
95105
$this->notFoundMessage = $notFoundMessage ?? $this->notFoundMessage;
96106
$this->notReadableMessage = $notReadableMessage ?? $this->notReadableMessage;
97107
$this->maxSizeMessage = $maxSizeMessage ?? $this->maxSizeMessage;
98108
$this->mimeTypesMessage = $mimeTypesMessage ?? $this->mimeTypesMessage;
109+
$this->extensionsMessage = $extensionsMessage ?? $this->extensionsMessage;
99110
$this->disallowEmptyMessage = $disallowEmptyMessage ?? $this->disallowEmptyMessage;
100111
$this->uploadIniSizeErrorMessage = $uploadIniSizeErrorMessage ?? $this->uploadIniSizeErrorMessage;
101112
$this->uploadFormSizeErrorMessage = $uploadFormSizeErrorMessage ?? $this->uploadFormSizeErrorMessage;

‎src/Symfony/Component/Validator/Constraints/FileValidator.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Constraints/FileValidator.php
+47-5Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,61 @@ public function validate(mixed $value, Constraint $constraint)
168168
}
169169
}
170170

171-
if ($constraint->mimeTypes) {
171+
$mimeTypes = (array) $constraint->mimeTypes;
172+
173+
if ($constraint->extensions) {
174+
$fileExtension = pathinfo($path, \PATHINFO_EXTENSION);
175+
176+
$found = false;
177+
$normalizedExtensions = [];
178+
foreach ((array) $constraint->extensions as $k => $v) {
179+
if (!\is_string($k)) {
180+
$k = $v;
181+
$v = null;
182+
}
183+
184+
$normalizedExtensions[] = $k;
185+
186+
if ($fileExtension !== $k) {
187+
continue;
188+
}
189+
190+
$found = true;
191+
if (null === $v) {
192+
if (!class_exists(MimeTypes::class)) {
193+
throw new LogicException('You cannot validate the mime-type of files as the Mime component is not installed. Try running "composer require symfony/mime".');
194+
}
195+
196+
$mimeTypesHelper = MimeTypes::getDefault();
197+
$v = $mimeTypesHelper->getMimeTypes($k);
198+
}
199+
200+
$mimeTypes = $mimeTypes ? array_intersect($v, $mimeTypes) : (array) $v;
201+
break;
202+
}
203+
204+
if (!$found) {
205+
$this->context->buildViolation($constraint->extensionsMessage)
206+
->setParameter('{{ file }}', $this->formatValue($path))
207+
->setParameter('{{ extension }}', $this->formatValue($fileExtension))
208+
->setParameter('{{ extensions }}', $this->formatValues($normalizedExtensions))
209+
->setParameter('{{ name }}', $this->formatValue($basename))
210+
->setCode(File::INVALID_EXTENSION_ERROR)
211+
->addViolation();
212+
}
213+
}
214+
215+
if ($mimeTypes) {
172216
if ($value instanceof FileObject) {
173217
$mime = $value->getMimeType();
174-
} elseif (class_exists(MimeTypes::class)) {
175-
$mime = MimeTypes::getDefault()->guessMimeType($path);
218+
} elseif (isset($mimeTypesHelper) || class_exists(MimeTypes::class)) {
219+
$mime = ($mimeTypesHelper ?? MimeTypes::getDefault())->guessMimeType($path);
176220
} elseif (!class_exists(FileObject::class)) {
177221
throw new LogicException('You cannot validate the mime-type of files as the Mime component is not installed. Try running "composer require symfony/mime".');
178222
} else {
179223
$mime = (new FileObject($value))->getMimeType();
180224
}
181225

182-
$mimeTypes = (array) $constraint->mimeTypes;
183-
184226
foreach ($mimeTypes as $mimeType) {
185227
if ($mimeType === $mime) {
186228
return;

‎src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php
+92Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,5 +530,97 @@ public function testNegativeMaxSize()
530530
$file->maxSize = -1;
531531
}
532532

533+
/**
534+
* @dataProvider validExtensionProvider
535+
*/
536+
public function testExtensionValid(string $name)
537+
{
538+
$path = __DIR__.'/Fixtures/'.$name;
539+
$file = new \Symfony\Component\HttpFoundation\File\File($path);
540+
541+
$constraint = new File(mimeTypes: [], extensions: ['gif', 'txt'], extensionsMessage: 'myMessage');
542+
543+
$this->validator->validate($file, $constraint);
544+
545+
$this->assertNoViolation();
546+
}
547+
548+
private function validExtensionProvider(): iterable
549+
{
550+
yield ['test.gif'];
551+
yield ['test.png.gif'];
552+
yield ['ccc.txt'];
553+
}
554+
555+
/**
556+
* @dataProvider invalidExtensionProvider
557+
*/
558+
public function testExtensionInvalid(string $name, string $extension)
559+
{
560+
$path = __DIR__.'/Fixtures/'.$name;
561+
$file = new \Symfony\Component\HttpFoundation\File\File($path);
562+
563+
$constraint = new File(extensions: ['png', 'svg'], extensionsMessage: 'myMessage');
564+
565+
$this->validator->validate($file, $constraint);
566+
567+
$this->buildViolation('myMessage')
568+
->setParameters([
569+
'{{ file }}' => '"'.$path.'"',
570+
'{{ extension }}' => '"'.$extension.'"',
571+
'{{ extensions }}' => '"png", "svg"',
572+
'{{ name }}' => '"'.$name.'"',
573+
])
574+
->setCode(File::INVALID_EXTENSION_ERROR)
575+
->assertRaised();
576+
}
577+
578+
private function invalidExtensionProvider(): iterable
579+
{
580+
yield ['test.gif', 'gif'];
581+
yield ['test.png.gif', 'gif'];
582+
yield ['bar', ''];
583+
}
584+
585+
public function testExtensionAutodetectMimeTypesInvalid()
586+
{
587+
$path = __DIR__.'/Fixtures/invalid-content.gif';
588+
$file = new \Symfony\Component\HttpFoundation\File\File($path);
589+
590+
$constraint = new File(mimeTypesMessage: 'myMessage', extensions: ['gif']);
591+
592+
$this->validator->validate($file, $constraint);
593+
594+
$this->buildViolation('myMessage')
595+
->setParameters([
596+
'{{ file }}' => '"'.$path.'"',
597+
'{{ name }}' => '"invalid-content.gif"',
598+
'{{ type }}' => '"text/plain"',
599+
'{{ types }}' => '"image/gif"',
600+
])
601+
->setCode(File::INVALID_MIME_TYPE_ERROR)
602+
->assertRaised();
603+
}
604+
605+
public function testExtensionTypesIncoherent()
606+
{
607+
$path = __DIR__.'/Fixtures/invalid-content.gif';
608+
$file = new \Symfony\Component\HttpFoundation\File\File($path);
609+
610+
$constraint = new File(mimeTypesMessage: 'myMessage', extensions: ['gif', 'txt']);
611+
612+
$this->validator->validate($file, $constraint);
613+
614+
$this->buildViolation('myMessage')
615+
->setParameters([
616+
'{{ file }}' => '"'.$path.'"',
617+
'{{ name }}' => '"invalid-content.gif"',
618+
'{{ type }}' => '"text/plain"',
619+
'{{ types }}' => '"image/gif"',
620+
])
621+
->setCode(File::INVALID_MIME_TYPE_ERROR)
622+
->assertRaised();
623+
}
624+
533625
abstract protected function getFile($filename);
534626
}
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bar
+2Lines changed: 2 additions & 0 deletions
Loading
Loading

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.