Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Validator] Html5 Email Validation #24442

Merged
merged 1 commit into from Dec 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions UPGRADE-4.1.md
Expand Up @@ -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
--------

Expand Down
7 changes: 7 additions & 0 deletions UPGRADE-5.0.md
Expand Up @@ -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
--------

Expand Down
33 changes: 33 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Email.php
Expand Up @@ -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';
Expand All @@ -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);
}
}
58 changes: 51 additions & 7 deletions src/Symfony/Component/Validator/Constraints/EmailValidator.php
Expand Up @@ -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;
}

/**
Expand All @@ -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');
}
Expand All @@ -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)
Expand Down
45 changes: 45 additions & 0 deletions src/Symfony/Component/Validator/Tests/Constraints/EmailTest.php
@@ -0,0 +1,45 @@
<?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;

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'));
}
}
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 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()
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 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));
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