Skip to content

Commit

Permalink
[Validator] #18156 In "strict" mode, email validator inaccurately cla…
Browse files Browse the repository at this point in the history
…ims certain valid emails are invalid

Email validation can now occur in one of four different modes/profiles:
 - Basic regex
 - HTML5 regex
 - RFC-compliant (non-strict; informational warnings raised during validation are ignored)
 - RFC-compliant (strict; warnings will cause an otherwise-valid email to be considered invalid)
  • Loading branch information
natechicago committed Apr 4, 2016
1 parent cef7e5b commit f69769d
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 47 deletions.
8 changes: 8 additions & 0 deletions UPGRADE-3.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,11 @@ Validator

* The `DateTimeValidator::PATTERN` constant has been deprecated and will be
removed in Symfony 4.0.

* Two new types of email address validation have been added to `EmailValidator`:
"HTML5 regex" and "RFC (allow warnings)". They supplement the two existing
types "basic regex" and "RFC (disallow warnings)". The desired mode of
validation can be specified using the `Email` constraint's new `profile`
option. The constraint's `strict` option (equivalent to the 'rfc-no-warn'
profile) has been deprecated and will be removed in Symfony 4.0.

Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,8 @@ private function addValidationSection(ArrayNodeDefinition $rootNode)
->end()
->end()
->scalarNode('translation_domain')->defaultValue('validators')->end()
->booleanNode('strict_email')->defaultFalse()->end()
->booleanNode('strict_email')->defaultFalse()->end() // deprecated
->scalarNode('email_profile')->defaultValue('basic')->end()
->end()
->end()
->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,8 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
}

$definition = $container->findDefinition('validator.email');
$definition->replaceArgument(0, $config['strict_email']);
$definition->replaceArgument(0, $config['strict_email']); // deprecated
$definition->replaceArgument(1, $config['email_profile']);

if (array_key_exists('enable_annotations', $config) && $config['enable_annotations']) {
$validatorBuilder->addMethodCall('enableAnnotationMapping', array(new Reference('annotation_reader')));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
</service>

<service id="validator.email" class="Symfony\Component\Validator\Constraints\EmailValidator">
<argument></argument>
<argument /> <!-- Strict (deprecated) -->
<argument /> <!-- Profile -->
<tag name="validator.constraint_validator" alias="Symfony\Component\Validator\Constraints\EmailValidator" />
</service>
</services>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ protected static function getBundleDefaultConfig()
'enable_annotations' => false,
'static_method' => array('loadValidatorMetadata'),
'translation_domain' => 'validators',
'strict_email' => false,
'strict_email' => false, // deprecated
'email_profile' => 'basic',
),
'annotations' => array(
'cache' => 'file',
Expand Down
23 changes: 23 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
*/
class Email extends Constraint
{
const PROFILE_BASIC_REGX = 'basic';
const PROFILE_HTML5_REGX = 'html5';
const PROFILE_RFC_ALLOW_WARNINGS = 'rfc';
const PROFILE_RFC_DISALLOW_WARNINGS = 'rfc-no-warn';

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 @@ -34,5 +39,23 @@ class Email extends Constraint
public $message = 'This value is not a valid email address.';
public $checkMX = false;
public $checkHost = false;

/**
* Defines the validation profile/mode that will be used.
* Options: basic, html5, rfc, rfc-no-warn.
*
* @var string
*/
public $profile;

/**
* Specifies whether the rfc-no-warn (strict) or basic
* validation profile should be used. This option is
* now deprecated in favor of the 'profile' option.
*
* @deprecated
*
* @var bool
*/
public $strict;
}
67 changes: 47 additions & 20 deletions src/Symfony/Component/Validator/Constraints/EmailValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,22 @@
class EmailValidator extends ConstraintValidator
{
/**
* @var bool
* @var string
*/
private $isStrict;
private $defaultProfile;

public function __construct($strict = false)
/**
* @param bool $strict Deprecated. If the constraint does not define a
* validation profile, this will determine if
* rfc-no-warn should be used as the default profile.
* @param string $profile If the constraint does not define a validation
* profile, this will specify which profile to use.
*/
public function __construct($strict = false, $profile = Email::PROFILE_BASIC_REGX)
{
$this->isStrict = $strict;
$this->defaultProfile = $strict
? Email::PROFILE_RFC_DISALLOW_WARNINGS
: $profile;
}

/**
Expand All @@ -50,26 +59,44 @@ public function validate($value, Constraint $constraint)

$value = (string) $value;

if (null === $constraint->strict) {
$constraint->strict = $this->isStrict;
if (isset($constraint->strict)) {
$constraint->profile = $constraint->strict
? Email::PROFILE_RFC_DISALLOW_WARNINGS
: Email::PROFILE_BASIC_REGX;
}

if ($constraint->strict) {
if (!class_exists('\Egulias\EmailValidator\EmailValidator')) {
throw new RuntimeException('Strict email validation requires egulias/email-validator');
}

$strictValidator = new \Egulias\EmailValidator\EmailValidator();
if (null === $constraint->profile) {
$constraint->profile = $this->defaultProfile;
}

if (!$strictValidator->isValid($value, false, true)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode(Email::INVALID_FORMAT_ERROR)
->addViolation();
// Determine if the email address is valid
switch ($constraint->profile) {
case Email::PROFILE_BASIC_REGX:
case Email::PROFILE_HTML5_REGX:
$regex = ($constraint->profile === Email::PROFILE_BASIC_REGX)
? '/^.+\@\S+\.\S+$/'
: '/^[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/';
$emailAddressIsValid = (bool) preg_match($regex, $value);
break;
case Email::PROFILE_RFC_ALLOW_WARNINGS:
case Email::PROFILE_RFC_DISALLOW_WARNINGS:
if (!class_exists('\Egulias\EmailValidator\EmailValidator')) {
throw new RuntimeException(
'Standards-compliant email validation requires egulias/email-validator'
);
}
$rfcValidator = new \Egulias\EmailValidator\EmailValidator();
$emailAddressIsValid = $rfcValidator->isValid(
$value,
false,
$constraint->profile === Email::PROFILE_RFC_DISALLOW_WARNINGS
);
break;
default:
throw new RuntimeException('Unrecognized email validation profile');
}

return;
}
} elseif (!preg_match('/^.+\@\S+\.\S+$/', $value)) {
if (!$emailAddressIsValid) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode(Email::INVALID_FORMAT_ERROR)
Expand Down
154 changes: 131 additions & 23 deletions src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,51 +48,159 @@ public function testExpectsStringCompatibleType()
}

/**
* @dataProvider getValidEmails
* @dataProvider provideEmailAddresses
*
* @param array $emailData
*/
public function testValidEmails($email)
public function testBasicValidationProfile($emailData)
{
$this->validator->validate($email, new Email());
$this->runValidationProfileTest(Email::PROFILE_BASIC_REGX, $emailData);
}

$this->assertNoViolation();
/**
* @dataProvider provideEmailAddresses
*
* @param array $emailData
*/
public function testHtml5ValidationProfile($emailData)
{
$this->runValidationProfileTest(Email::PROFILE_HTML5_REGX, $emailData);
}

public function getValidEmails()
/**
* @dataProvider provideEmailAddresses
*
* @param array $emailData
*/
public function testRfcValidationProfile($emailData)
{
return array(
array('fabien@symfony.com'),
array('example@example.co.uk'),
array('fabien_potencier@example.fr'),
);
$this->runValidationProfileTest(Email::PROFILE_RFC_ALLOW_WARNINGS, $emailData);
}

/**
* @dataProvider provideEmailAddresses
*
* @param array $emailData
*/
public function testRfcNoWarnValidationProfile($emailData)
{
$this->runValidationProfileTest(Email::PROFILE_RFC_DISALLOW_WARNINGS, $emailData);
}

/**
* @dataProvider getInvalidEmails
* @param string $validationProfile
* @param array $emailData
*/
public function testInvalidEmails($email)
protected function runValidationProfileTest($validationProfile, $emailData)
{
$emailAddress = $emailData[0];
$isValidForProfile = $emailData[1][$validationProfile];

$constraint = new Email(array(
'message' => 'myMessage',
'profile' => $validationProfile,
'message' => 'error message',
));

$this->validator->validate($email, $constraint);
$this->validator->validate($emailAddress, $constraint);

$this->buildViolation('myMessage')
->setParameter('{{ value }}', '"'.$email.'"')
->setCode(Email::INVALID_FORMAT_ERROR)
->assertRaised();
if ($isValidForProfile) {
$this->assertNoViolation();
} else {
$this->buildViolation('error message')
->setParameter('{{ value }}', '"'.$emailAddress.'"')
->setCode(Email::INVALID_FORMAT_ERROR)
->assertRaised();
}
}

public function getInvalidEmails()
/**
* @return array
*/
public function provideEmailAddresses()
{
return array(
array('example'),
array('example@'),
array('example@localhost'),
array('foo@example.com bar'),
// Valid for all validation profiles.
$this->buildEmailData('hello@world.com', true, true, true, true),
$this->buildEmailData('gday@mate.co.uk', true, true, true, true),
$this->buildEmailData('bon@jour.fr', true, true, true, true),
$this->buildEmailData('aa+@bb.com', true, true, true, true),
$this->buildEmailData('aa+b@cc.com', true, true, true, true),
$this->buildEmailData('aa.bb-cc@dd.com', true, true, true, true),
// Invalid for all validation profiles.
$this->buildEmailData('test', false, false, false, false),
$this->buildEmailData('test@', false, false, false, false),
$this->buildEmailData('foo@bar.com baz', false, false, false, false),
$this->buildEmailData('aa@local\host', false, false, false, false),
$this->buildEmailData('aa@localhost.', false, false, false, false),
$this->buildEmailData('aa@bb.com test', false, false, false, false),
$this->buildEmailData('aa@ bb . com', false, false, false, false),
$this->buildEmailData('aa@bb,com', false, false, false, false),
$this->buildEmailData('test@email<', false, false, false, false),
// Validity depends on the chosen validation profile.
$this->buildEmailData('test@localhost', false, true, true, true),
$this->buildEmailData('test@email&', false, false, true, true),
$this->buildEmailData('.abc@localhost', false, true, false, false),
$this->buildEmailData('example.@aa.co.uk', true, true, false, false),
$this->buildEmailData("fab'ien@test.com", true, false, true, true),
$this->buildEmailData('fab\ ien@test.com', true, false, true, false),
$this->buildEmailData('aa((bb))@cc.co.uk', true, false, true, false),
$this->buildEmailData('aa@bb(cc).co.uk', true, false, true, false),
$this->buildEmailData('инфо@письмо.рф', true, false, true, true),
$this->buildEmailData('"aa@bb"@cc.com', true, false, true, false),
$this->buildEmailData('"\""@iana.org', true, false, true, false),
$this->buildEmailData('""@iana.org', true, false, true, false),
$this->buildEmailData('"aa\ bb"@cc.org', true, false, true, false),
$this->buildEmailData('aa@(bb).com', true, false, false, false),
$this->buildEmailData('aa@(bb.com', true, false, false, false),
$this->buildEmailData('usern,ame@cc.com', true, false, false, false),
$this->buildEmailData('user[na]me@cc.com', true, false, false, false),
$this->buildEmailData('"""@iana.org', true, false, false, false),
$this->buildEmailData('"\"@iana.org', true, false, false, false),
$this->buildEmailData('"aa"bb@cc.org', true, false, false, false),
$this->buildEmailData('"aa""bb"@cc.org', true, false, false, false),
$this->buildEmailData('"aa"."bb"@cc.org', true, false, false, false),
$this->buildEmailData('"aa".bb@cc.org', true, false, false, false),
$this->buildEmailData('aa@bb@cc.co.uk', true, false, false, false),
$this->buildEmailData('(aa@bb.cc)', true, false, false, false),
$this->buildEmailData('aa(bb)cc@dd.co.uk', true, false, false, false),
$this->buildEmailData('user name@aa.com', true, false, false, false),
$this->buildEmailData('user name@aa.com', true, false, false, false),
$this->buildEmailData('test@aa/bb.org', true, false, false, false),
$this->buildEmailData('test@foo;bar.com', true, false, false, false),
$this->buildEmailData('test;123@bb.com', true, false, false, false),
$this->buildEmailData('test@example..com', true, false, false, false),
$this->buildEmailData('aa.bb@cc."', true, false, false, false),
$this->buildEmailData('test@ema[il.com', true, false, false, false),
);
}

/**
* @param string $email The email address to validate.
* @param bool $basic Whether the email is valid per the 'basic' profile.
* @param bool $html5 Whether the email is valid per the 'html5' profile.
* @param bool $rfc Whether the email is valid per the 'rfc' profile.
* @param bool $rfcNoWarn Whether the email is valid per the 'rfc-no-warn' profile.
*
* @return array
*/
protected function buildEmailData($email, $basic, $html5, $rfc, $rfcNoWarn)
{
return array(
array(
$email,
array(
Email::PROFILE_BASIC_REGX => $basic,
Email::PROFILE_HTML5_REGX => $html5,
Email::PROFILE_RFC_ALLOW_WARNINGS => $rfc,
Email::PROFILE_RFC_DISALLOW_WARNINGS => $rfcNoWarn,
),
),
);
}

/**
* @deprecated
*/
public function testStrict()
{
$constraint = new Email(array('strict' => true));
Expand Down

0 comments on commit f69769d

Please sign in to comment.