diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index b9af46e493aa9..57c4c4b5d3df6 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * Add the `WordCount` constraint * Add the `Week` constraint * Add `CompoundConstraintTestCase` to ease testing Compound Constraints + * Add the `DataUri` constraint for validating Data URI content 7.1 --- diff --git a/src/Symfony/Component/Validator/Constraints/DataUri.php b/src/Symfony/Component/Validator/Constraints/DataUri.php new file mode 100644 index 0000000000000..e87b1cf0ca337 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/DataUri.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; + +/** + * Validates that a value is a valid data URI string. + * + * @author Kev + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class DataUri extends Constraint +{ + public const INVALID_DATA_URI_ERROR = 'b9e175d1-8d7a-4e28-bf65-ad2448a3b3cf'; + + protected const ERROR_NAMES = [ + self::INVALID_DATA_URI_ERROR => 'INVALID_DATA_URI_ERROR', + ]; + + public string $message = 'This value is not a valid data URI.'; + + /** + * @param array|null $options + * @param string[]|null $groups + */ + public function __construct( + ?array $options = null, + ?string $message = null, + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct($options ?? [], $groups, $payload); + + $this->message = $message ?? $this->message; + } +} diff --git a/src/Symfony/Component/Validator/Constraints/DataUriValidator.php b/src/Symfony/Component/Validator/Constraints/DataUriValidator.php new file mode 100644 index 0000000000000..892f17847b678 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/DataUriValidator.php @@ -0,0 +1,69 @@ + + * + * 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 Kev + * + * @see https://datatracker.ietf.org/doc/html/rfc2397 + */ +class DataUriValidator extends ConstraintValidator +{ + /** maximum number of characters allowed in a error message */ + private const MAX_MESSAGE_VALUE_LENGTH = 30; + /** data-uri regexp */ + public const PATTERN = '~^ + data: + (?:\w+\/(?:(?!;).)+)? # MIME-type + (?:;[\w\W]*?[^;])* # parameters + (;base64)? # encoding + , + [^$]+ # data + $~ixuD'; + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof DataUri) { + throw new UnexpectedTypeException($constraint, DataUri::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = (string) $value; + if ('' === $value) { + return; + } + + if (!preg_match(static::PATTERN, $value)) { + if (\strlen($value) > self::MAX_MESSAGE_VALUE_LENGTH) { + $value = \sprintf('%s (truncated)', $this->formatValue(substr($value, 0, self::MAX_MESSAGE_VALUE_LENGTH).'...')); + } else { + $value = $this->formatValue($value); + } + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->setCode(DataUri::INVALID_DATA_URI_ERROR) + ->addViolation(); + } + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/DataUriValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/DataUriValidatorTest.php new file mode 100644 index 0000000000000..48d7bae943274 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/DataUriValidatorTest.php @@ -0,0 +1,99 @@ + + * + * 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\DataUri; +use Symfony\Component\Validator\Constraints\DataUriValidator; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +/** + * @author Kev + */ +class DataUriValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): DataUriValidator + { + return new DataUriValidator(); + } + + public function testNullIsValid() + { + $this->validator->validate(null, new DataUri()); + + $this->assertNoViolation(); + } + + public function testBlankIsValid() + { + $this->validator->validate('', new DataUri()); + + $this->assertNoViolation(); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues(string $value) + { + $this->validator->validate($value, new DataUri()); + + $this->assertNoViolation(); + } + + public static function getValidValues() + { + return [ + 'mime type is omitted' => ['data:,FooBar'], + 'just charset' => ['data:;charset=UTF-8,FooBar'], + 'plain text' => ['data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=='], + 'text html' => ['data:text/html,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E'], + 'plain text with charset' => ['data:text/plain;charset=UTF-8,the%20data:1234,5678'], + 'with meta key=value' => ['data:image/jpeg;key=value;base64,UEsDBBQAAAAI'], + 'without base64 key name' => ['data:image/jpeg;key=value,UEsDBBQAAAAI'], + 'jpeg image' => ['data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD'], + 'png image' => ['data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='], + 'gif image' => ['data:image/gif;base64,R0lGODlhyAAiALM...DfD0QAADs='], + 'svg' => ['data:image/svg+xml,%3Csvg%20version%3D%221.1%22%3E%3C%2Fsvg%3E'], + 'networking applications' => ['data:application/vnd-xxx-query,select_vcount,fcol_from_fieldtable/local,123456789'], + ]; + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value, $valueAsString) + { + $constraint = new DataUri([ + 'message' => 'myMessage', + ]); + + $this->validator->validate($value, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', $valueAsString) + ->setCode(DataUri::INVALID_DATA_URI_ERROR) + ->assertRaised(); + } + + public static function getInvalidValues() + { + return [ + 'random string' => ['foobar', '"foobar"'], + 'zero' => [0, '"0"'], + 'integer' => [1234, '"1234"'], + 'truncated invalid value' => [ + '1234567890123456789012345678901', // 31 chars + '"123456789012345678901234567890..." (truncated)', + ], + ]; + } +}