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

Skip to content

Commit 9e7ecc1

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

10 files changed

+223
-7
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

+5
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

+70-7
289
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 <b>TRUE</b> if <i>value</i> is an SVG, <b>FALSE</b> 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|false Index 0 and 1 contains respectively the width and the height of the image. <i>FALSE</b> if size can't be found.
267+
*/
268+
private function getSvgSize(mixed $value): array|false
269+
{
270+
if ($value instanceof File) {
271+
$content = $value->getContent();
272+
} elseif (!class_exists(File::class)) {
273+
return false;
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];
+
}
290+
291+
if (isset($width) && isset($height)) {
292+
return [$width, $height];
293+
}
294+
295+
return false;
296+
}
234297
}
Loading
Loading
Loading
Loading
Loading
Loading

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

+134
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