Skip to content

Navigation Menu

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 4b270ff

Browse filesBrowse files
committed
[Validator] Validate SVG ratio in Image validator
1 parent 0a9eb28 commit 4b270ff
Copy full SHA for 4b270ff

10 files changed

+223
-7
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/CHANGELOG.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Allow `Image` constraint to check SVG ratio
8+
49
7.2
510
---
611

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Constraints/ImageValidator.php
+70-7Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Validator\Constraints;
1313

14+
use Symfony\Component\HttpFoundation\File\File;
15+
use Symfony\Component\Mime\MimeTypes;
1416
use Symfony\Component\Validator\Constraint;
1517
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1618
use Symfony\Component\Validator\Exception\LogicException;
@@ -50,7 +52,13 @@ public function validate(mixed $value, Constraint $constraint): void
5052
return;
5153
}
5254

53-
$size = @getimagesize($value);
55+
$isSvg = $this->isSvg($value);
56+
57+
if ($isSvg) {
58+
$size = $this->getSvgSize($value);
59+
} else {
60+
$size = @getimagesize($value);
61+
}
5462

5563
if (!$size || (0 === $size[0]) || (0 === $size[1])) {
5664
$this->context->buildViolation($constraint->sizeNotDetectedMessage)
@@ -63,7 +71,7 @@ public function validate(mixed $value, Constraint $constraint): void
6371
$width = $size[0];
6472
$height = $size[1];
6573

66-
if ($constraint->minWidth) {
74+
if (!$isSvg && $constraint->minWidth) {
6775
if (!ctype_digit((string) $constraint->minWidth)) {
6876
throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum width.', $constraint->minWidth));
6977
}
@@ -79,7 +87,7 @@ public function validate(mixed $value, Constraint $constraint): void
7987
}
8088
}
8189

82-
if ($constraint->maxWidth) {
90+
if (!$isSvg && $constraint->maxWidth) {
8391
if (!ctype_digit((string) $constraint->maxWidth)) {
8492
throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum width.', $constraint->maxWidth));
8593
}
@@ -95,7 +103,7 @@ public function validate(mixed $value, Constraint $constraint): void
95103
}
96104
}
97105

98-
if ($constraint->minHeight) {
106+
if (!$isSvg && $constraint->minHeight) {
99107
if (!ctype_digit((string) $constraint->minHeight)) {
100108
throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum height.', $constraint->minHeight));
101109
}
@@ -111,7 +119,7 @@ public function validate(mixed $value, Constraint $constraint): void
111119
}
112120
}
113121

114-
if ($constraint->maxHeight) {
122+
if (!$isSvg && $constraint->maxHeight) {
115123
if (!ctype_digit((string) $constraint->maxHeight)) {
116124
throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum height.', $constraint->maxHeight));
117125
}
@@ -127,7 +135,7 @@ public function validate(mixed $value, Constraint $constraint): void
127135

128136
$pixels = $width * $height;
129137

130-
if (null !== $constraint->minPixels) {
138+
if (!$isSvg && null !== $constraint->minPixels) {
131139
if (!ctype_digit((string) $constraint->minPixels)) {
132140
throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid minimum amount of pixels.', $constraint->minPixels));
133141
}
@@ -143,7 +151,7 @@ public function validate(mixed $value, Constraint $constraint): void
143151
}
144152
}
145153

146-
if (null !== $constraint->maxPixels) {
154+
if (!$isSvg && null !== $constraint->maxPixels) {
147155
if (!ctype_digit((string) $constraint->maxPixels)) {
148156
throw new ConstraintDefinitionException(\sprintf('"%s" is not a valid maximum amount of pixels.', $constraint->maxPixels));
149157
}
@@ -231,4 +239,59 @@ public function validate(mixed $value, Constraint $constraint): void
231239
imagedestroy($resource);
232240
}
233241
}
242+
243+
/**
244+
* Check whether a value is an SVG image.
245+
*
246+
* Return true if value is an SVG, false if it's not, or if we can't detect its MimeType.
247+
*/
248+
private function isSvg(mixed $value): bool
249+
{
250+
if ($value instanceof File) {
251+
$mime = $value->getMimeType();
252+
} elseif (class_exists(MimeTypes::class)) {
253+
$mime = MimeTypes::getDefault()->guessMimeType($value);
254+
} elseif (!class_exists(File::class)) {
255+
return false;
256+
} else {
257+
$mime = (new File($value))->getMimeType();
258+
}
259+
260+
return 'image/svg+xml' === $mime;
261+
}
262+
263+
/**
264+
* Extract width and height from an SVG image.
265+
*
266+
* @return array{int, int}|null index 0 and 1 contains respectively the width and the height of the image, null if size can't be found
267+
*/
268+
private function getSvgSize(mixed $value): ?array
269+
{
270+
if ($value instanceof File) {
271+
$content = $value->getContent();
272+
} elseif (!class_exists(File::class)) {
273+
return null;
274+
} else {
275+
$content = (new File($value))->getContent();
276+
}
277+
278+
if (1 === preg_match('/<svg[^<>]+width="([0-9]+)"[^<>]*>/', $content, $widthMatches)) {
279+
$width = (int) $widthMatches[1];
280+
}
281+
282+
if (1 === preg_match('/<svg[^<>]+height="([0-9]+)"[^<>]*>/', $content, $heightMatches)) {
283+
$height = (int) $heightMatches[1];
284+
}
285+
286+
if (1 === preg_match('/<svg[^<>]+viewBox="(-?[0-9]+) (-?[0-9]+) (-?[0-9]+) (-?[0-9]+)"[^<>]*>/', $content, $viewBoxMatches)) {
287+
$width ??= (int) $viewBoxMatches[3];
288+
$height ??= (int) $viewBoxMatches[4];
289+
}
290+
291+
if (isset($width) && isset($height)) {
292+
return [$width, $height];
293+
}
294+
295+
return null;
296+
}
234297
}
+2Lines changed: 2 additions & 0 deletions
Loading
+2Lines changed: 2 additions & 0 deletions
Loading
+2Lines changed: 2 additions & 0 deletions
Loading
+2Lines changed: 2 additions & 0 deletions
Loading
+2Lines changed: 2 additions & 0 deletions
Loading
+2Lines changed: 2 additions & 0 deletions
Loading
+2Lines changed: 2 additions & 0 deletions
Loading

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php
+134Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,4 +498,138 @@ public static function provideInvalidMimeTypeWithNarrowedSet()
498498
]),
499499
];
500500
}
501+
502+
/** @dataProvider provideSvg */
503+
public function testSvg(string $image, Image $constraint, ?string $violation, array $parameters = [])
504+
{
505+
$this->validator->validate($image, $constraint);
506+
507+
if ($violation) {
508+
$this->buildViolation('myMessage')
509+
->setCode($violation)
510+
->setParameters($parameters)
511+
->assertRaised();
512+
} else {
513+
$this->assertNoViolation();
514+
}
515+
}
516+
517+
public static function provideSvg(): iterable
518+
{
519+
yield 'No size svg' => [
520+
__DIR__.'/Fixtures/test_no_size.svg',
521+
new Image(allowLandscape: false, sizeNotDetectedMessage: 'myMessage'),
522+
Image::SIZE_NOT_DETECTED_ERROR,
523+
];
524+
525+
yield 'Landscape SVG allowed' => [
526+
__DIR__.'/Fixtures/test_landscape.svg',
527+
new Image(allowLandscape: true, allowLandscapeMessage: 'myMessage'),
528+
null,
529+
];
530+
531+
yield 'Landscape SVG not allowed' => [
532+
__DIR__.'/Fixtures/test_landscape.svg',
533+
new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'),
534+
Image::LANDSCAPE_NOT_ALLOWED_ERROR,
535+
[
536+
'{{ width }}' => 500,
537+
'{{ height }}' => 200,
538+
],
539+
];
540+
541+
yield 'Portrait SVG allowed' => [
542+
__DIR__.'/Fixtures/test_portrait.svg',
543+
new Image(allowPortrait: true, allowPortraitMessage: 'myMessage'),
544+
null,
545+
];
546+
547+
yield 'Portrait SVG not allowed' => [
548+
__DIR__.'/Fixtures/test_portrait.svg',
549+
new Image(allowPortrait: false, allowPortraitMessage: 'myMessage'),
550+
Image::PORTRAIT_NOT_ALLOWED_ERROR,
551+
[
552+
'{{ width }}' => 200,
553+
'{{ height }}' => 500,
554+
],
555+
];
556+
557+
yield 'Square SVG allowed' => [
558+
__DIR__.'/Fixtures/test_square.svg',
559+
new Image(allowSquare: true, allowSquareMessage: 'myMessage'),
560+
null,
561+
];
562+
563+
yield 'Square SVG not allowed' => [
564+
__DIR__.'/Fixtures/test_square.svg',
565+
new Image(allowSquare: false, allowSquareMessage: 'myMessage'),
566+
Image::SQUARE_NOT_ALLOWED_ERROR,
567+
[
568+
'{{ width }}' => 500,
569+
'{{ height }}' => 500,
570+
],
571+
];
572+
573+
yield 'Landscape with width attribute SVG allowed' => [
574+
__DIR__.'/Fixtures/test_landscape_width.svg',
575+
new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'),
576+
Image::LANDSCAPE_NOT_ALLOWED_ERROR,
577+
[
578+
'{{ width }}' => 600,
579+
'{{ height }}' => 200,
580+
],
581+
];
582+
583+
yield 'Landscape with height attribute SVG not allowed' => [
584+
__DIR__.'/Fixtures/test_landscape_height.svg',
585+
new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'),
586+
Image::LANDSCAPE_NOT_ALLOWED_ERROR,
587+
[
588+
'{{ width }}' => 500,
589+
'{{ height }}' => 300,
590+
],
591+
];
592+
593+
yield 'Landscape with width and height attribute SVG not allowed' => [
594+
__DIR__.'/Fixtures/test_landscape_width_height.svg',
595+
new Image(allowLandscape: false, allowLandscapeMessage: 'myMessage'),
596+
Image::LANDSCAPE_NOT_ALLOWED_ERROR,
597+
[
598+
'{{ width }}' => 600,
599+
'{{ height }}' => 300,
600+
],
601+
];
602+
603+
yield 'SVG Min ratio 1' => [
604+
__DIR__.'/Fixtures/test_square.svg',
605+
new Image(minRatio: 1, minRatioMessage: 'myMessage'),
606+
null,
607+
];
608+
609+
yield 'SVG Min ratio 2' => [
610+
__DIR__.'/Fixtures/test_square.svg',
611+
new Image(minRatio: 2, minRatioMessage: 'myMessage'),
612+
Image::RATIO_TOO_SMALL_ERROR,
613+
[
614+
'{{ ratio }}' => '1',
615+
'{{ min_ratio }}' => '2',
616+
],
617+
];
618+
619+
yield 'SVG Max ratio 1' => [
620+
__DIR__.'/Fixtures/test_square.svg',
621+
new Image(maxRatio: 1, maxRatioMessage: 'myMessage'),
622+
null,
623+
];
624+
625+
yield 'SVG Min ratio 0.5' => [
626+
__DIR__.'/Fixtures/test_square.svg',
627+
new Image(maxRatio: 0.5, maxRatioMessage: 'myMessage'),
628+
Image::RATIO_TOO_BIG_ERROR,
629+
[
630+
'{{ ratio }}' => '1',
631+
'{{ max_ratio }}' => '0.5',
632+
],
633+
];
634+
}
501635
}

0 commit comments

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