diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 85baf4340c129..be1c9015ae5d9 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -3,7 +3,8 @@ CHANGELOG 6.1 --- - + + * Support `SVGs` when validating image dimensions * Deprecate `Constraint::$errorNames`, use `Constraint::ERROR_NAMES` instead 6.0 diff --git a/src/Symfony/Component/Validator/Constraints/ImageValidator.php b/src/Symfony/Component/Validator/Constraints/ImageValidator.php index 3246215fef53a..a2442356f2708 100644 --- a/src/Symfony/Component/Validator/Constraints/ImageValidator.php +++ b/src/Symfony/Component/Validator/Constraints/ImageValidator.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\HttpFoundation\File\File as FileObject; +use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\LogicException; @@ -53,7 +55,18 @@ public function validate(mixed $value, Constraint $constraint) return; } - $size = @getimagesize($value); + if ($this->isSvg($value)) { + $size = $this->getSvgSize($value); + if (null === $size) { + $this->context->buildViolation($constraint->corruptedMessage) + ->setCode(Image::CORRUPTED_IMAGE_ERROR) + ->addViolation(); + + return; + } + } else { + $size = @getimagesize($value); + } if (empty($size) || (0 === $size[0]) || (0 === $size[1])) { $this->context->buildViolation($constraint->sizeNotDetectedMessage) @@ -234,4 +247,46 @@ public function validate(mixed $value, Constraint $constraint) imagedestroy($resource); } } + + /** + * Check whether a value is an SVG image + * + * @param mixed $value + * + * @return bool + * TRUE if value is an SVG, FALSE if it's not, or if we can't detect its MimeType; + */ + private function isSvg(mixed $value) + { + if ($value instanceof FileObject) { + $mime = $value->getMimeType(); + } elseif (class_exists(MimeTypes::class)) { + $mime = MimeTypes::getDefault()->guessMimeType($value); + } elseif (!class_exists(FileObject::class)) { + return false; + } else { + $mime = (new FileObject($value))->getMimeType(); + } + + return 'image/svg+xml' === $mime; + } + + /** + * @param mixed $value + * + * @return array|null + * array contains the width and height of the image + * null if the XML is corrupted + */ + private function getSvgSize(mixed $value) + { + if (false === $xmlValue = simplexml_load_file($value)) { + return null; + } + $svgAttributes = $xmlValue->attributes(); + $width = intval($svgAttributes->width); + $height = intval($svgAttributes->height); + + return [$width, $height]; + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg.svg b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg.svg new file mode 100644 index 0000000000000..5f5e4bf0e92d8 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg_landscape.svg b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg_landscape.svg new file mode 100644 index 0000000000000..274298eabc674 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg_landscape.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg_portrait.svg b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg_portrait.svg new file mode 100644 index 0000000000000..1c0126da5e543 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_svg_portrait.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php index cedab95e1e202..cca8419d0b44c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php @@ -30,6 +30,9 @@ class ImageValidatorTest extends ConstraintValidatorTestCase protected $image4By3; protected $imageCorrupted; protected $notAnImage; + protected $imageSvg; + protected $imageSvgLandscape; + protected $imageSvgPortrait; protected function createValidator() { @@ -47,6 +50,9 @@ protected function setUp(): void $this->image16By9 = __DIR__.'/Fixtures/test_16by9.gif'; $this->imageCorrupted = __DIR__.'/Fixtures/test_corrupted.gif'; $this->notAnImage = __DIR__.'/Fixtures/ccc.txt'; + $this->imageSvg = __DIR__.'/Fixtures/test_svg.svg'; + $this->imageSvgLandscape = __DIR__.'/Fixtures/test_svg_landscape.svg'; + $this->imageSvgPortrait = __DIR__.'/Fixtures/test_svg_portrait.svg'; } public function testNullIsValid() @@ -536,6 +542,62 @@ public function testInvalidMimeType() ->assertRaised(); } + public function testSvgSize() + { + $constraint = new Image([ + 'minWidth' => 1, + 'maxWidth' => 2, + 'minHeight' => 1, + 'maxHeight' => 2, + ]); + + $this->validator->validate($this->imageSvg, $constraint); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideAllowSquareConstraints + */ + public function testSquareNotAllowedOnSvg(Image $constraint) + { + $this->validator->validate($this->imageSvg, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 1) + ->setParameter('{{ height }}', 1) + ->setCode(Image::SQUARE_NOT_ALLOWED_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideAllowLandscapeConstraints + */ + public function testLandscapeNotAllowedOnSvg(Image $constraint) + { + $this->validator->validate($this->imageSvgLandscape, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 2) + ->setParameter('{{ height }}', 1) + ->setCode(Image::LANDSCAPE_NOT_ALLOWED_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideAllowPortraitConstraints + */ + public function testPortraitNotAllowedOnSvg(Image $constraint) + { + $this->validator->validate($this->imageSvgPortrait, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ width }}', 1) + ->setParameter('{{ height }}', 2) + ->setCode(Image::PORTRAIT_NOT_ALLOWED_ERROR) + ->assertRaised(); + } + public function provideDetectCorruptedConstraints(): iterable { yield 'Doctrine style' => [new Image([