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] #18156 In "strict" mode, email validator inaccurately cla… #18428

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of the strict option must be documented in the UPGRADE_4.0.md file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we will need to trigger deprecations.


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')->defaultNull()->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' => null,
),
'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_REGEX = 'basic';
const PROFILE_HTML5_REGEX = '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;
}
68 changes: 48 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,25 @@
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' or 'basic' should be used as
* the default profile.
* @param string|null $profile If the constraint does not define a validation
* profile, this will specify which profile to use.
*/
public function __construct($strict = false, $profile = null)
{
$this->isStrict = $strict;
$this->defaultProfile = (null === $profile)
? $strict
? Email::PROFILE_RFC_DISALLOW_WARNINGS
: Email::PROFILE_BASIC_REGEX
: $profile;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using if instead of nesting the ternary operator would increase readability.

}

/**
Expand All @@ -50,26 +62,42 @@ 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_REGEX;
}

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_REGEX:
case Email::PROFILE_HTML5_REGEX:
$regex = (Email::PROFILE_BASIC_REGEX === $constraint->profile)
? '/^.+\@\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,
Email::PROFILE_RFC_DISALLOW_WARNINGS === $constraint->profile
);
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
137 changes: 114 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,142 @@ 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_REGEX, $emailData);
}

$this->assertNoViolation();
/**
* @dataProvider provideEmailAddresses
*
* @param array $emailData
*/
public function testHtml5ValidationProfile($emailData)
{
$this->runValidationProfileTest(Email::PROFILE_HTML5_REGEX, $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),
// 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('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('"\""@iana.org', true, false, true, false),
$this->buildEmailData('""@iana.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('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('test@example..com', 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_REGEX => $basic,
Email::PROFILE_HTML5_REGEX => $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