-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #31518 [Validator] Added HostnameValidator (karser)
This PR was merged into the 5.1-dev branch. Discussion ---------- [Validator] Added HostnameValidator | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | BC breaks? | no <!-- see https://symfony.com/bc --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tests pass? | yes <!-- please add some, will be required by reviewers --> | Fixed tickets | #10088 <!-- #-prefixed issue number(s), if any --> | License | MIT | Doc PR | symfony/symfony-docs#... <!-- required for new features --> This PR adds HostnameValidator support. I encountered this need in my project and was surprised that this issue has been open for years. Here is short example: ``` App\Entity\Acme: properties: domain: - Hostname: ~ non_tld_domain: - Hostname: { requireTld: false } ``` The option `requireTld` is `true` by default and disallows domains like localhost and etc. Commits ------- 8a08c20 Added HostnameValidator
- Loading branch information
Showing
9 changed files
with
322 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,10 @@ | ||
CHANGELOG | ||
========= | ||
|
||
5.1.0 | ||
----- | ||
* added the `Hostname` constraint and validator | ||
|
||
5.0.0 | ||
----- | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?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\Constraints; | ||
|
||
use Symfony\Component\Validator\Constraint; | ||
|
||
/** | ||
* @Annotation | ||
* @Target({"PROPERTY", "METHOD", "ANNOTATION"}) | ||
* | ||
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com> | ||
*/ | ||
class Hostname extends Constraint | ||
{ | ||
const INVALID_HOSTNAME_ERROR = '7057ffdb-0af4-4f7e-bd5e-e9acfa6d7a2d'; | ||
|
||
protected static $errorNames = [ | ||
self::INVALID_HOSTNAME_ERROR => 'INVALID_HOSTNAME_ERROR', | ||
]; | ||
|
||
public $message = 'This value is not a valid hostname.'; | ||
public $requireTld = true; | ||
} |
69 changes: 69 additions & 0 deletions
69
src/Symfony/Component/Validator/Constraints/HostnameValidator.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
<?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\Constraints; | ||
|
||
use Symfony\Component\Validator\Constraint; | ||
use Symfony\Component\Validator\ConstraintValidator; | ||
use Symfony\Component\Validator\Exception\UnexpectedTypeException; | ||
use Symfony\Component\Validator\Exception\UnexpectedValueException; | ||
|
||
/** | ||
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com> | ||
*/ | ||
class HostnameValidator extends ConstraintValidator | ||
{ | ||
/** | ||
* https://tools.ietf.org/html/rfc2606. | ||
*/ | ||
private const RESERVED_TLDS = [ | ||
'example', | ||
'invalid', | ||
'localhost', | ||
'test', | ||
]; | ||
|
||
public function validate($value, Constraint $constraint) | ||
{ | ||
if (!$constraint instanceof Hostname) { | ||
throw new UnexpectedTypeException($constraint, Hostname::class); | ||
} | ||
|
||
if (null === $value || '' === $value) { | ||
return; | ||
} | ||
|
||
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { | ||
throw new UnexpectedValueException($value, 'string'); | ||
} | ||
|
||
$value = (string) $value; | ||
if ('' === $value) { | ||
return; | ||
} | ||
if (!$this->isValid($value) || ($constraint->requireTld && !$this->hasValidTld($value))) { | ||
$this->context->buildViolation($constraint->message) | ||
->setParameter('{{ value }}', $this->formatValue($value)) | ||
->setCode(Hostname::INVALID_HOSTNAME_ERROR) | ||
->addViolation(); | ||
} | ||
} | ||
|
||
private function isValid(string $domain): bool | ||
{ | ||
return false !== filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); | ||
} | ||
|
||
private function hasValidTld(string $domain): bool | ||
{ | ||
return false !== strpos($domain, '.') && !\in_array(substr($domain, strrpos($domain, '.') + 1), self::RESERVED_TLDS, true); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
src/Symfony/Component/Validator/Tests/Constraints/HostnameValidatorTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
<?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 Symfony\Component\Validator\Constraints\Hostname; | ||
use Symfony\Component\Validator\Constraints\HostnameValidator; | ||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; | ||
|
||
/** | ||
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com> | ||
*/ | ||
class HostnameValidatorTest extends ConstraintValidatorTestCase | ||
{ | ||
public function testNullIsValid() | ||
{ | ||
$this->validator->validate(null, new Hostname()); | ||
|
||
$this->assertNoViolation(); | ||
} | ||
|
||
public function testEmptyStringIsValid() | ||
{ | ||
$this->validator->validate('', new Hostname()); | ||
|
||
$this->assertNoViolation(); | ||
} | ||
|
||
public function testExpectsStringCompatibleType() | ||
{ | ||
$this->expectException(\Symfony\Component\Validator\Exception\UnexpectedValueException::class); | ||
|
||
$this->validator->validate(new \stdClass(), new Hostname()); | ||
} | ||
|
||
/** | ||
* @dataProvider getValidMultilevelDomains | ||
*/ | ||
public function testValidTldDomainsPassValidationIfTldRequired($domain) | ||
{ | ||
$this->validator->validate($domain, new Hostname()); | ||
|
||
$this->assertNoViolation(); | ||
} | ||
|
||
/** | ||
* @dataProvider getValidMultilevelDomains | ||
*/ | ||
public function testValidTldDomainsPassValidationIfTldNotRequired($domain) | ||
{ | ||
$this->validator->validate($domain, new Hostname(['requireTld' => false])); | ||
|
||
$this->assertNoViolation(); | ||
} | ||
|
||
public function getValidMultilevelDomains() | ||
{ | ||
return [ | ||
['symfony.com'], | ||
['example.co.uk'], | ||
['example.fr'], | ||
['example.com'], | ||
['xn--diseolatinoamericano-66b.com'], | ||
['xn--ggle-0nda.com'], | ||
['www.xn--simulateur-prt-2kb.fr'], | ||
[sprintf('%s.com', str_repeat('a', 20))], | ||
]; | ||
} | ||
|
||
/** | ||
* @dataProvider getInvalidDomains | ||
*/ | ||
public function testInvalidDomainsRaiseViolationIfTldRequired($domain) | ||
{ | ||
$this->validator->validate($domain, new Hostname([ | ||
'message' => 'myMessage', | ||
])); | ||
|
||
$this->buildViolation('myMessage') | ||
->setParameter('{{ value }}', '"'.$domain.'"') | ||
->setCode(Hostname::INVALID_HOSTNAME_ERROR) | ||
->assertRaised(); | ||
} | ||
|
||
/** | ||
* @dataProvider getInvalidDomains | ||
*/ | ||
public function testInvalidDomainsRaiseViolationIfTldNotRequired($domain) | ||
{ | ||
$this->validator->validate($domain, new Hostname([ | ||
'message' => 'myMessage', | ||
'requireTld' => false, | ||
])); | ||
|
||
$this->buildViolation('myMessage') | ||
->setParameter('{{ value }}', '"'.$domain.'"') | ||
->setCode(Hostname::INVALID_HOSTNAME_ERROR) | ||
->assertRaised(); | ||
} | ||
|
||
public function getInvalidDomains() | ||
{ | ||
return [ | ||
['acme..com'], | ||
['qq--.com'], | ||
['-example.com'], | ||
['example-.com'], | ||
[sprintf('%s.com', str_repeat('a', 300))], | ||
]; | ||
} | ||
|
||
/** | ||
* @dataProvider getReservedDomains | ||
*/ | ||
public function testReservedDomainsPassValidationIfTldNotRequired($domain) | ||
{ | ||
$this->validator->validate($domain, new Hostname(['requireTld' => false])); | ||
|
||
$this->assertNoViolation(); | ||
} | ||
|
||
/** | ||
* @dataProvider getReservedDomains | ||
*/ | ||
public function testReservedDomainsRaiseViolationIfTldRequired($domain) | ||
{ | ||
$this->validator->validate($domain, new Hostname([ | ||
'message' => 'myMessage', | ||
'requireTld' => true, | ||
])); | ||
|
||
$this->buildViolation('myMessage') | ||
->setParameter('{{ value }}', '"'.$domain.'"') | ||
->setCode(Hostname::INVALID_HOSTNAME_ERROR) | ||
->assertRaised(); | ||
} | ||
|
||
public function getReservedDomains() | ||
{ | ||
return [ | ||
['example'], | ||
['foo.example'], | ||
['invalid'], | ||
['bar.invalid'], | ||
['localhost'], | ||
['lol.localhost'], | ||
['test'], | ||
['abc.test'], | ||
]; | ||
} | ||
|
||
/** | ||
* @dataProvider getTopLevelDomains | ||
*/ | ||
public function testTopLevelDomainsPassValidationIfTldNotRequired($domain) | ||
{ | ||
$this->validator->validate($domain, new Hostname(['requireTld' => false])); | ||
|
||
$this->assertNoViolation(); | ||
} | ||
|
||
/** | ||
* @dataProvider getTopLevelDomains | ||
*/ | ||
public function testTopLevelDomainsRaiseViolationIfTldRequired($domain) | ||
{ | ||
$this->validator->validate($domain, new Hostname([ | ||
'message' => 'myMessage', | ||
'requireTld' => true, | ||
])); | ||
|
||
$this->buildViolation('myMessage') | ||
->setParameter('{{ value }}', '"'.$domain.'"') | ||
->setCode(Hostname::INVALID_HOSTNAME_ERROR) | ||
->assertRaised(); | ||
} | ||
|
||
public function getTopLevelDomains() | ||
{ | ||
return [ | ||
['com'], | ||
['net'], | ||
['org'], | ||
['etc'], | ||
]; | ||
} | ||
|
||
protected function createValidator() | ||
{ | ||
return new HostnameValidator(); | ||
} | ||
} |