From e084246ba2ad7b859d586e1de09f6b24d0cbcb4c Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 20 Dec 2023 10:38:42 +0100 Subject: [PATCH] [Validator] Add the `Charset` constraint --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Validator/Constraints/Charset.php | 43 ++++++++++ .../Constraints/CharsetValidator.php | 46 ++++++++++ .../Tests/Constraints/CharsetTest.php | 65 ++++++++++++++ .../Constraints/CharsetValidatorTest.php | 86 +++++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 src/Symfony/Component/Validator/Constraints/Charset.php create mode 100644 src/Symfony/Component/Validator/Constraints/CharsetValidator.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/CharsetTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index c2c41d6daa4a6..6e65a1355fdaf 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `list` and `associative_array` types to `Type` constraint + * Add the `Charset` constraint 7.0 --- diff --git a/src/Symfony/Component/Validator/Constraints/Charset.php b/src/Symfony/Component/Validator/Constraints/Charset.php new file mode 100644 index 0000000000000..a864a440fec04 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Charset.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +/** + * @author Alexandre Daubois + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class Charset extends Constraint +{ + public const BAD_ENCODING_ERROR = '94c5e58b-f892-4e25-8fd6-9d89c80bfe81'; + + protected const ERROR_NAMES = [ + self::BAD_ENCODING_ERROR => 'BAD_ENCODING_ERROR', + ]; + + public array|string $encodings = []; + public string $message = 'The detected encoding "{{ detected }}" does not match one of the accepted encoding: "{{ encodings }}".'; + + public function __construct(array|string $encodings = null, string $message = null, array $groups = null, mixed $payload = null, array $options = null) + { + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->encodings = (array) ($encodings ?? $this->encodings); + + if ([] === $this->encodings) { + throw new ConstraintDefinitionException(sprintf('The "%s" constraint requires at least one encoding.', static::class)); + } + } +} diff --git a/src/Symfony/Component/Validator/Constraints/CharsetValidator.php b/src/Symfony/Component/Validator/Constraints/CharsetValidator.php new file mode 100644 index 0000000000000..2a4ca66a44832 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/CharsetValidator.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Alexandre Daubois + */ +final class CharsetValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof Charset) { + throw new UnexpectedTypeException($constraint, Charset::class); + } + + if (null === $value) { + return; + } + + if (!\is_string($value)) { + throw new UnexpectedValueException($value, 'string'); + } + + if (!\in_array($detected = mb_detect_encoding($value, $constraint->encodings, true), $constraint->encodings, true)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ detected }}', $detected) + ->setParameter('{{ encodings }}', implode('", "', $constraint->encodings)) + ->setCode(Charset::BAD_ENCODING_ERROR) + ->addViolation(); + } + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CharsetTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CharsetTest.php new file mode 100644 index 0000000000000..893066645a94c --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/CharsetTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\Charset; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; + +class CharsetTest extends TestCase +{ + public function testSingleEncodingCanBeSet() + { + $encoding = new Charset('UTF-8'); + + $this->assertSame(['UTF-8'], $encoding->encodings); + } + + public function testMultipleEncodingCanBeSet() + { + $encoding = new Charset(['ASCII', 'UTF-8']); + + $this->assertSame(['ASCII', 'UTF-8'], $encoding->encodings); + } + + public function testThrowsOnNoCharset() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Charset" constraint requires at least one encoding.'); + + new Charset(); + } + + public function testAttributes() + { + $metadata = new ClassMetadata(CharsetDummy::class); + $loader = new AttributeLoader(); + $this->assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + $this->assertSame(['UTF-8'], $aConstraint->encodings); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + $this->assertSame(['ASCII', 'UTF-8'], $bConstraint->encodings); + } +} + +class CharsetDummy +{ + #[Charset('UTF-8')] + private string $a; + + #[Charset(['ASCII', 'UTF-8'])] + private string $b; +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php new file mode 100644 index 0000000000000..20a3fe884d25e --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/CharsetValidatorTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\Charset; +use Symfony\Component\Validator\Constraints\CharsetValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +class CharsetValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): CharsetValidator + { + return new CharsetValidator(); + } + + /** + * @dataProvider provideValidValues + */ + public function testEncodingIsValid(string $value, array $encodings) + { + $this->validator->validate($value, new Charset(encodings: $encodings)); + + $this->assertNoViolation(); + } + + /** + * @dataProvider provideInvalidValues + */ + public function testInvalidValues(string $value, array $encodings) + { + $this->validator->validate($value, new Charset(encodings: $encodings)); + + $this->buildViolation('The detected encoding "{{ detected }}" does not match one of the accepted encoding: "{{ encodings }}".') + ->setParameter('{{ detected }}', mb_detect_encoding($value, $encodings, true)) + ->setParameter('{{ encodings }}', implode(', ', $encodings)) + ->setCode(Charset::BAD_ENCODING_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testNonStringValues(mixed $value) + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessageMatches('/Expected argument of type "string", ".*" given/'); + + $this->validator->validate($value, new Charset(encodings: ['UTF-8'])); + } + + public static function provideValidValues() + { + yield ['my ascii string', ['ASCII']]; + yield ['my ascii string', ['UTF-8']]; + yield ['my ascii string', ['ASCII', 'UTF-8']]; + yield ['my ûtf 8', ['ASCII', 'UTF-8']]; + yield ['my ûtf 8', ['UTF-8']]; + yield ['ώ', ['UTF-16']]; + } + + public static function provideInvalidValues() + { + yield ['my non-Äscîi string', ['ASCII']]; + yield ['😊', ['7bit']]; + } + + public static function provideInvalidTypes() + { + yield [true]; + yield [false]; + yield [1]; + yield [1.1]; + yield [[]]; + yield [new \stdClass()]; + } +}