Skip to content

Commit

Permalink
[Validator] Html5 Email Validation
Browse files Browse the repository at this point in the history
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'`
  • Loading branch information
PurpleBooth committed Oct 29, 2017
1 parent 98dae3e commit a7af07e
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 10 deletions.
47 changes: 47 additions & 0 deletions UPGRADE-3.4.md
Expand Up @@ -413,6 +413,53 @@ Validator
* Not setting the `strict` option of the `Choice` constraint to `true` is
deprecated and will throw an exception in Symfony 4.0.

* The `strict` option of the `Email` constraint is deprecated and will
be removed in Symfony 4.0, use the `mode` option instead.

Before:

```php
use Symfony\Component\Validator\Constraints as Assert;

/**
* @Assert\Email(strict=true)
*/
private $property;
```

After:

```php
use Symfony\Component\Validator\Constraints as Assert;

/**
* @Assert\Email(mode="strict")
*/
private $property;
```

* Calling the `EmailValidator` with a boolean constructor argument is deprecated and will
be removed in Symfony 4.0.

Before:

```php
use Symfony\Component\Validator\Constraints\EmailValidator;

$strictValidator = new EmailValidator(true);
$looseValidator = new EmailValidator(false);
```

After:

```php
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\EmailValidator;

$strictValidator = new EmailValidator(Email::VALIDATION_MODE_STRICT);
$looseValidator = new EmailValidator(Email::VALIDATION_MODE_LOOSE);
```

Yaml
----

Expand Down
32 changes: 32 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Email.php
Expand Up @@ -21,6 +21,10 @@
*/
class Email extends Constraint
{
const VALIDATION_MODE_HTML5 = 'html5';
const VALIDATION_MODE_STRICT = 'strict';
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';
Expand All @@ -31,8 +35,36 @@ 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 3.4, to be removed in 4.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 3.4 and will be removed in 4.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);
}
}
57 changes: 49 additions & 8 deletions src/Symfony/Component/Validator/Constraints/EmailValidator.php
Expand Up @@ -24,13 +24,40 @@
class EmailValidator extends ConstraintValidator
{
/**
* @var bool
* @internal
*/
private $isStrict;
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,
);

/**
* @var string
*/
private $defaultMode;

public function __construct($strict = false)
/**
* @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 3.4 and will be removed in 4.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;
}

/**
Expand All @@ -52,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 3.4 and will be removed in 4.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');
}
Expand All @@ -78,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)
Expand Down
46 changes: 46 additions & 0 deletions src/Symfony/Component/Validator/Tests/Constraints/EmailTest.php
@@ -0,0 +1,46 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
use Symfony\Component\Validator\Constraints\File;

class EmailTest extends TestCase
{
/**
* @expectedDeprecation The 'strict' property is deprecated since version 3.4 and will be removed in 4.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'));
}
}
Expand Up @@ -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 3.4 and will be removed in 4.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()
Expand Down Expand Up @@ -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'),
);
}

Expand Down Expand Up @@ -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 3.4 and will be removed in 4.0. Use 'mode'=>"strict" instead.
* @expectedDeprecation The Symfony\Component\Validator\Constraints\Email::$strict property is deprecated since version 3.4 and will be removed in 4.0. Use Symfony\Component\Validator\Constraints\Email::mode="strict" instead.
* @group legacy
*/
public function testStrict()
{
$constraint = new Email(array('strict' => true));
Expand All @@ -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);
Expand Down

0 comments on commit a7af07e

Please sign in to comment.