8000 [Validator] Validate SVG ratio in Image validator · symfony/symfony@8726444 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8726444

Browse files
committed
[Validator] Validate SVG ratio in Image validator
1 parent 0a9eb28 commit 8726444

10 files changed

+225
-7
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines 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

Lines 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 bool 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="(?<width>[0-9]+)"[^<>]*>/', $content, $widthMatches)) {
279+
$width = (int) $widthMatches['width'];
280+
}
281+
282+
if (1 === preg_match('/<svg[^<>]+height="(?<height>[0-9]+)"[^<>]*>/', $content, $heightMatches)) {
283+
$height = (int) $heightMatches['height'];
284+
}
285+
286+
if (1 === preg_match('/<svg[^<>]+viewBox="-?[0-9]+ -?[0-9]+ (?<width>-?[0-9]+) (?<height>-?[0-9]+)"[^<>]*>/', $content, $viewBoxMatches)) {
287+
$width ??= (int) $viewBoxMatches['width'];
288+
$height ??= (int) $viewBoxMatches['height'];
289+
}
290+
291+
if (isset($width) && isset($height)) {
292+
return [$width, $height];
293+
}
294+
295+
return null;
296+
}
234297
}
Lines changed: 2 additions & 0 deletions
Loading
Lines changed: 2 additions & 0 deletions
Loading
Lines changed: 2 additions & 0 deletions
Loading
Lines changed: 2 additions & 0 deletions
Loading
Lines changed: 2 additions & 0 deletions
Loading
Lines changed: 2 additions & 0 deletions
Loading
Lines changed: 2 additions & 0 deletions
Loading

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

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

0 commit comments

Comments
 (0)
0