From cf0410812efb2b89e2843003e7f02850c783fb3c Mon Sep 17 00:00:00 2001 From: Billie Thompson Date: Thu, 5 Oct 2017 16:50:07 +0100 Subject: [PATCH] [Validator] Html5 Email Validation Currently we only support a very loose validation. There is now a standard HTML5 element with matching regex. This will add the ability to set a `mode` on the email validator. The mode will change the validation that is applied to the field as a whole. These modes are: * loose: The pattern from previous Symfony versions (default) * strict: Strictly matching the RFC * html5: The regex used for the HTML5 Element Deprecates the `strict=true` parameter in favour of `mode='strict'` --- UPGRADE-4.1.md | 6 + UPGRADE-5.0.md | 7 + .../Component/Validator/Constraints/Email.php | 33 +++++ .../Validator/Constraints/EmailValidator.php | 58 +++++++- .../Validator/Tests/Constraints/EmailTest.php | 45 ++++++ .../Tests/Constraints/EmailValidatorTest.php | 140 +++++++++++++++++- 6 files changed, 280 insertions(+), 9 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/EmailTest.php diff --git a/UPGRADE-4.1.md b/UPGRADE-4.1.md index 764e1f1da9340..8710008fed04f 100644 --- a/UPGRADE-4.1.md +++ b/UPGRADE-4.1.md @@ -17,6 +17,12 @@ Translation * The `FileDumper::setBackup()` method is deprecated and will be removed in 5.0. * The `TranslationWriter::disableBackup()` method is deprecated and will be removed in 5.0. +Validator +-------- + + * The `Email::__construct()` 'strict' property is deprecated and will be removed in 5.0. Use 'mode'=>"strict" instead. + * Calling `EmailValidator::__construct()` method with a boolean parameter is deprecated and will be removed in 5.0, use `EmailValidator("strict")` instead. + Workflow -------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 9c95ba427686f..ca1f64367e9a7 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -17,6 +17,13 @@ Translation * The `FileDumper::setBackup()` method has been removed. * The `TranslationWriter::disableBackup()` method has been removed. +Validator +-------- + + * The `Email::__construct()` 'strict' property has been removed. Use 'mode'=>"strict" instead. + * Calling `EmailValidator::__construct()` method with a boolean parameter has been removed, use `EmailValidator("strict")` instead. + + Workflow -------- diff --git a/src/Symfony/Component/Validator/Constraints/Email.php b/src/Symfony/Component/Validator/Constraints/Email.php index a9d9ab15391fa..1a8bf35bc87d9 100644 --- a/src/Symfony/Component/Validator/Constraints/Email.php +++ b/src/Symfony/Component/Validator/Constraints/Email.php @@ -21,6 +21,10 @@ */ class Email extends Constraint { + public const VALIDATION_MODE_HTML5 = 'html5'; + public const VALIDATION_MODE_STRICT = 'strict'; + public const VALIDATION_MODE_LOOSE = 'loose'; + const INVALID_FORMAT_ERROR = 'bd79c0ab-ddba-46cc-a703-a7a4b08de310'; const MX_CHECK_FAILED_ERROR = 'bf447c1c-0266-4e10-9c6c-573df282e413'; const HOST_CHECK_FAILED_ERROR = '7da53a8b-56f3-4288-bb3e-ee9ede4ef9a1'; @@ -31,8 +35,37 @@ class Email extends Constraint self::HOST_CHECK_FAILED_ERROR => 'HOST_CHECK_FAILED_ERROR', ); + /** + * @var string[] + * + * @internal + */ + public static $validationModes = array( + self::VALIDATION_MODE_HTML5, + self::VALIDATION_MODE_STRICT, + self::VALIDATION_MODE_LOOSE, + ); + public $message = 'This value is not a valid email address.'; public $checkMX = false; public $checkHost = false; + + /** + * @deprecated since version 4.1, to be removed in 5.0. Set mode to "strict" instead. + */ public $strict; + public $mode; + + public function __construct($options = null) + { + if (is_array($options) && array_key_exists('strict', $options)) { + @trigger_error(sprintf('The \'strict\' property is deprecated since version 4.1 and will be removed in 5.0. Use \'mode\'=>"%s" instead.', self::VALIDATION_MODE_STRICT), E_USER_DEPRECATED); + } + + if (is_array($options) && array_key_exists('mode', $options) && !in_array($options['mode'], self::$validationModes, true)) { + throw new \InvalidArgumentException('The \'mode\' parameter value is not valid.'); + } + + parent::__construct($options); + } } diff --git a/src/Symfony/Component/Validator/Constraints/EmailValidator.php b/src/Symfony/Component/Validator/Constraints/EmailValidator.php index 04e8e71c312a0..d683d5b21b970 100644 --- a/src/Symfony/Component/Validator/Constraints/EmailValidator.php +++ b/src/Symfony/Component/Validator/Constraints/EmailValidator.php @@ -23,11 +23,41 @@ */ class EmailValidator extends ConstraintValidator { - private $isStrict; + /** + * @internal + */ + const PATTERN_HTML5 = '/^[a-zA-Z0-9.!#$%&\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/'; + /** + * @internal + */ + const PATTERN_LOOSE = '/^.+\@\S+\.\S+$/'; + + private static $emailPatterns = array( + Email::VALIDATION_MODE_LOOSE => self::PATTERN_LOOSE, + Email::VALIDATION_MODE_HTML5 => self::PATTERN_HTML5, + ); - public function __construct(bool $strict = false) + /** + * @var string + */ + private $defaultMode; + + /** + * @param string $defaultMode + */ + public function __construct($defaultMode = Email::VALIDATION_MODE_LOOSE) { - $this->isStrict = $strict; + if (is_bool($defaultMode)) { + @trigger_error(sprintf('Calling `new %s(%s)` is deprecated since version 4.1 and will be removed in 5.0, use `new %s("%s")` instead.', self::class, $defaultMode ? 'true' : 'false', self::class, $defaultMode ? Email::VALIDATION_MODE_STRICT : Email::VALIDATION_MODE_LOOSE), E_USER_DEPRECATED); + + $defaultMode = $defaultMode ? Email::VALIDATION_MODE_STRICT : Email::VALIDATION_MODE_LOOSE; + } + + if (!in_array($defaultMode, Email::$validationModes, true)) { + throw new \InvalidArgumentException('The "defaultMode" parameter value is not valid.'); + } + + $this->defaultMode = $defaultMode; } /** @@ -49,11 +79,25 @@ public function validate($value, Constraint $constraint) $value = (string) $value; - if (null === $constraint->strict) { - $constraint->strict = $this->isStrict; + if (null !== $constraint->strict) { + @trigger_error(sprintf('The %s::$strict property is deprecated since version 4.1 and will be removed in 5.0. Use %s::mode="%s" instead.', Email::class, Email::class, Email::VALIDATION_MODE_STRICT), E_USER_DEPRECATED); + + if ($constraint->strict) { + $constraint->mode = Email::VALIDATION_MODE_STRICT; + } else { + $constraint->mode = Email::VALIDATION_MODE_LOOSE; + } + } + + if (null === $constraint->mode) { + $constraint->mode = $this->defaultMode; + } + + if (!in_array($constraint->mode, Email::$validationModes, true)) { + throw new \InvalidArgumentException(sprintf('The %s::$mode parameter value is not valid.', get_class($constraint))); } - if ($constraint->strict) { + if (Email::VALIDATION_MODE_STRICT === $constraint->mode) { if (!class_exists('\Egulias\EmailValidator\EmailValidator')) { throw new RuntimeException('Strict email validation requires egulias/email-validator ~1.2|~2.0'); } @@ -75,7 +119,7 @@ public function validate($value, Constraint $constraint) return; } - } elseif (!preg_match('/^.+\@\S+\.\S+$/', $value)) { + } elseif (!preg_match(self::$emailPatterns[$constraint->mode], $value)) { $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Email::INVALID_FORMAT_ERROR) diff --git a/src/Symfony/Component/Validator/Tests/Constraints/EmailTest.php b/src/Symfony/Component/Validator/Tests/Constraints/EmailTest.php new file mode 100644 index 0000000000000..e9c20cf86bf11 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/EmailTest.php @@ -0,0 +1,45 @@ + + * + * 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\Email; + +class EmailTest extends TestCase +{ + /** + * @expectedDeprecation The 'strict' property is deprecated since version 4.1 and will be removed in 5.0. Use 'mode'=>"strict" instead. + * @group legacy + */ + public function testLegacyConstructorStrict() + { + $subject = new Email(array('strict' => true)); + + $this->assertTrue($subject->strict); + } + + public function testConstructorStrict() + { + $subject = new Email(array('mode' => Email::VALIDATION_MODE_STRICT)); + + $this->assertEquals(Email::VALIDATION_MODE_STRICT, $subject->mode); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The 'mode' parameter value is not valid. + */ + public function testUnknownModesTriggerException() + { + new Email(array('mode' => 'Unknown Mode')); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php index 94857c1784173..ff107a40810ff 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php @@ -23,7 +23,29 @@ class EmailValidatorTest extends ConstraintValidatorTestCase { protected function createValidator() { - return new EmailValidator(false); + return new EmailValidator(Email::VALIDATION_MODE_LOOSE); + } + + /** + * @expectedDeprecation Calling `new Symfony\Component\Validator\Constraints\EmailValidator(true)` is deprecated since version 4.1 and will be removed in 5.0, use `new Symfony\Component\Validator\Constraints\EmailValidator("strict")` instead. + * @group legacy + */ + public function testLegacyValidatorConstructorStrict() + { + $this->validator = new EmailValidator(true); + $this->validator->initialize($this->context); + $this->validator->validate('example@localhost', new Email()); + + $this->assertNoViolation(); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The "defaultMode" parameter value is not valid. + */ + public function testUnknownDefaultModeTriggerException() + { + new EmailValidator('Unknown Mode'); } public function testNullIsValid() @@ -64,6 +86,31 @@ public function getValidEmails() array('fabien@symfony.com'), array('example@example.co.uk'), array('fabien_potencier@example.fr'), + array('example@example.co..uk'), + array('{}~!@!@£$%%^&*().!@£$%^&*()'), + array('example@example.co..uk'), + array('example@-example.com'), + array(sprintf('example@%s.com', str_repeat('a', 64))), + ); + } + + /** + * @dataProvider getValidEmailsHtml5 + */ + public function testValidEmailsHtml5($email) + { + $this->validator->validate($email, new Email(array('mode' => Email::VALIDATION_MODE_HTML5))); + + $this->assertNoViolation(); + } + + public function getValidEmailsHtml5() + { + return array( + array('fabien@symfony.com'), + array('example@example.co.uk'), + array('fabien_potencier@example.fr'), + array('{}~!@example.com'), ); } @@ -94,6 +141,95 @@ public function getInvalidEmails() ); } + /** + * @dataProvider getInvalidHtml5Emails + */ + public function testInvalidHtml5Emails($email) + { + $constraint = new Email( + array( + 'message' => 'myMessage', + 'mode' => Email::VALIDATION_MODE_HTML5, + ) + ); + + $this->validator->validate($email, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '"'.$email.'"') + ->setCode(Email::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function getInvalidHtml5Emails() + { + return array( + array('example'), + array('example@'), + array('example@localhost'), + array('example@example.co..uk'), + array('foo@example.com bar'), + array('example@example.'), + array('example@.fr'), + array('@example.com'), + array('example@example.com;example@example.com'), + array('example@.'), + array(' example@example.com'), + array('example@ '), + array(' example@example.com '), + array(' example @example .com '), + array('example@-example.com'), + array(sprintf('example@%s.com', str_repeat('a', 64))), + ); + } + + public function testModeStrict() + { + $constraint = new Email(array('mode' => Email::VALIDATION_MODE_STRICT)); + + $this->validator->validate('example@localhost', $constraint); + + $this->assertNoViolation(); + } + + public function testModeHtml5() + { + $constraint = new Email(array('mode' => Email::VALIDATION_MODE_HTML5)); + + $this->validator->validate('example@example..com', $constraint); + + $this->buildViolation('This value is not a valid email address.') + ->setParameter('{{ value }}', '"example@example..com"') + ->setCode(Email::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + public function testModeLoose() + { + $constraint = new Email(array('mode' => Email::VALIDATION_MODE_LOOSE)); + + $this->validator->validate('example@example..com', $constraint); + + $this->assertNoViolation(); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The Symfony\Component\Validator\Constraints\Email::$mode parameter value is not valid. + */ + public function testUnknownModesOnValidateTriggerException() + { + $constraint = new Email(); + $constraint->mode = 'Unknown Mode'; + + $this->validator->validate('example@example..com', $constraint); + } + + /** + * @expectedDeprecation The 'strict' property is deprecated since version 4.1 and will be removed in 5.0. Use 'mode'=>"strict" instead. + * @expectedDeprecation The Symfony\Component\Validator\Constraints\Email::$strict property is deprecated since version 4.1 and will be removed in 5.0. Use Symfony\Component\Validator\Constraints\Email::mode="strict" instead. + * @group legacy + */ public function testStrict() { $constraint = new Email(array('strict' => true)); @@ -110,7 +246,7 @@ public function testStrictWithInvalidEmails($email) { $constraint = new Email(array( 'message' => 'myMessage', - 'strict' => true, + 'mode' => Email::VALIDATION_MODE_STRICT, )); $this->validator->validate($email, $constraint);