8000 feature #59265 [Validator] Validate SVG ratio in Image validator (max… · symfony/symfony@b013b62 · GitHub
[go: up one dir, main page]

Skip to content

Commit b013b62

Browse files
committed
feature #59265 [Validator] Validate SVG ratio in Image validator (maximecolin)
This PR was merged into the 7.3 branch. Discussion ---------- [Validator] Validate SVG ratio in Image validator | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | Fix #45269 | License | MIT Implement ratio check for SVG images. Checking SGV size is not relevant as a SVG image can be enlarged without loss, but ratio can be important to check. Currently, the validator add a violation with `SIZE_NOT_DETECTED_ERROR` in case of SVG image. SVG size is guessed from viewbox, width and height attributes. Viewbox will provides default size, width and height can override viewbox size if they are number. Width and height as percentage are ignored as the final size will depend on the container. I use `preg_match` instead of `\DomDocument` or `simplexml` functions to extract viewBox, width and height in order to avoid new dependencies on `ext-dom` or `ext-simplexml`. In case of SVG, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minPixels` and `maxPixels` are ignored because not relevant. Only `maxRatio`, `minRatio`, `allowSquare`, `allowLandscape` and `allowPortrait` can generate violations, like suggested in the comments of the abandoned #45486. Commits ------- c4ad092 [Validator] Validate SVG ratio in Image validator
2 parents 78f4d9a + c4ad092 commit b013b62

File tree

10 files changed

+218
-7
lines changed

10 files changed

+218
-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+
* Add support for ratio checks for SVG files to the `Image` constraint
8+
49
7.2
510
---
611

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

Lines changed: 63 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,52 @@ public function validate(mixed $value, Constraint $constraint): void
231239
imagedestroy($resource);
232240
}
233241
}
242+
243+
private function isSvg(mixed $value): bool
244+
{
245+
if ($value instanceof File) {
246+
$mime = $value->getMimeType();
247+
} elseif (class_exists(MimeTypes::class)) {
248+
$mime = MimeTypes::getDefault()->guessMimeType($value);
249+
} elseif (!class_exists(File::class)) {
250+
return false;
251+
} else {
252+
$mime = (new File($value))->getMimeType();
253+
}
254+
255+
return 'image/svg+xml' === $mime;
256+
}
257+
258+
/**
259+
* @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
260+
*/
261+
private function getSvgSize(mixed $value): ?array
262+
{
263+
if ($value instanceof File) {
264+
$content = $value->getContent();
265+
} elseif (!class_exists(File::class)) {
266+
return null;
267+
} else {
268+
$content = (new File($value))->getContent();
269+
}
270+
271+
if (1 === preg_match('/<svg[^<>]+width="(?<width>[0-9]+)"[^<>]*>/', $content, $widthMatches)) {
272+
$width = (int) $widthMatches['width'];
273+
}
274+
275+
if (1 === preg_match('/<svg[^<>]+height="(?<height>[0-9]+)"[^<>]*>/', $content, $heightMatches)) {
276+
$height = (int) $heightMatches['height'];
277+
}
278+
279+
if (1 === preg_match('/<svg[^<>]+viewBox="-?[0-9]+ -?[0-9]+ (?<width>-?[0-9]+) (?<height>-?[0-9]+)"[^<>]*>/', $content, $viewBoxMatches)) {
280+
$width ??= (int) $viewBoxMatches['width'];
281+
$height ??= (int) $viewBoxMatches['height'];
282+
}
283+
284+
if (isset($width) && isset($height)) {
285+
return [$width, $height];
286+
}
287+
288+
return null;
289+
}
234290
}
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 = [])
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)
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