Skip to content

Commit db7a75c

Browse files
popsorinCalinBolea
andcommitted
[Validator] Add CidrValidator to allow validation of CIDR notations
Co-authored-by: Calin Bolea <[email protected]>
1 parent aa5623c commit db7a75c

File tree

8 files changed

+580
-0
lines changed

8 files changed

+580
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.4
55
---
66

7+
* Add a `Cidr` constraint to validate CIDR notations
78
* Add a `CssColor` constraint to validate CSS colors
89
* Add support for `ConstraintViolationList::createFromMessage()`
910
* Add error's uid to `Count` and `Length` constraints with "exactly" option enabled

Constraints/Cidr.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
16+
17+
/**
18+
* Validates that a value is a valid CIDR notation.
19+
*
20+
* @Annotation
21+
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
22+
*
23+
* @author Sorin Pop <[email protected]>
24+
* @author Calin Bolea <[email protected]>
25+
*/
26+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
27+
class Cidr extends Constraint
28+
{
29+
public const INVALID_CIDR_ERROR = '5649e53a-5afb-47c5-a360-ffbab3be8567';
30+
public const OUT_OF_RANGE_ERROR = 'b9f14a51-acbd-401a-a078-8c6b204ab32f';
31+
32+
protected static $errorNames = [
33+
self::INVALID_CIDR_ERROR => 'INVALID_CIDR_ERROR',
34+
self::OUT_OF_RANGE_ERROR => 'OUT_OF_RANGE_VIOLATION',
35+
];
36+
37+
private const NET_MAXES = [
38+
Ip::ALL => 128,
39+
Ip::V4 => 32,
40+
Ip::V6 => 128,
41+
];
42+
43+
public $version = Ip::ALL;
44+
45+
public $message = 'This value is not a valid CIDR notation.';
46+
47+
public $netmaskRangeViolationMessage = 'The value of the netmask should be between {{ min }} and {{ max }}.';
48+
49+
public $netmaskMin = 0;
50+
51+
public $netmaskMax;
52+
53+
public function __construct(
54+
array $options = null,
55+
string $version = null,
56+
int $netmaskMin = null,
57+
int $netmaskMax = null,
58+
string $message = null,
59+
array $groups = null,
60+
$payload = null
61+
) {
62+
$this->version = $version ?? $options['version'] ?? $this->version;
63+
64+
if (!\in_array($this->version, array_keys(self::NET_MAXES))) {
65+
throw new ConstraintDefinitionException(sprintf('The option "version" must be one of "%s".', implode('", "', array_keys(self::NET_MAXES))));
66+
}
67+
68+
$this->netmaskMin = $netmaskMin ?? $options['netmaskMin'] ?? $this->netmaskMin;
69+
$this->netmaskMax = $netmaskMax ?? $options['netmaskMax'] ?? self::NET_MAXES[$this->version];
70+
$this->message = $message ?? $this->message;
71+
72+
unset($options['netmaskMin'], $options['netmaskMax'], $options['version']);
73+
74+
if ($this->netmaskMin < 0 || $this->netmaskMax > self::NET_MAXES[$this->version] || $this->netmaskMin > $this->netmaskMax) {
75+
throw new ConstraintDefinitionException(sprintf('The netmask range must be between 0 and %d.', self::NET_MAXES[$this->version]));
76+
}
77+
78+
parent::__construct($options, $groups, $payload);
79+
}
80+
}

Constraints/CidrValidator.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\ConstraintValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
19+
class CidrValidator extends ConstraintValidator
20+
{
21+
public function validate($value, Constraint $constraint): void
22+
{
23+
if (!$constraint instanceof Cidr) {
24+
throw new UnexpectedTypeException($constraint, Cidr::class);
25+
}
26+
27+
if (null === $value || '' === $value) {
28+
return;
29+
}
30+
31+
if (!\is_string($value)) {
32+
throw new UnexpectedValueException($value, 'string');
33+
}
34+
35+
$cidrParts = explode('/', $value, 2);
36+
37+
if (!isset($cidrParts[1])
38+
|| !ctype_digit($cidrParts[1])
39+
|| '' === $cidrParts[0]
40+
) {
41+
$this->context
42+
->buildViolation($constraint->message)
43+
->setCode(Cidr::INVALID_CIDR_ERROR)
44+
->addViolation();
45+
46+
return;
47+
}
48+
49+
$ipAddress = $cidrParts[0];
50+
$netmask = (int) $cidrParts[1];
51+
52+
$validV4 = Ip::V6 !== $constraint->version
53+
&& filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)
54+
&& $netmask <= 32;
55+
56+
$validV6 = Ip::V4 !== $constraint->version
57+
&& filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6);
58+
59+
if (!$validV4 && !$validV6) {
60+
$this->context
61+
->buildViolation($constraint->message)
62+
->setCode(Cidr::INVALID_CIDR_ERROR)
63+
->addViolation();
64+
65+
return;
66+
}
67+
68+
if ($netmask < $constraint->netmaskMin || $netmask > $constraint->netmaskMax) {
69+
$this->context
70+
->buildViolation($constraint->netmaskRangeViolationMessage)
71+
->setParameter('{{ min }}', $constraint->netmaskMin)
72+
->setParameter('{{ max }}', $constraint->netmaskMax)
73+
->setCode(Cidr::OUT_OF_RANGE_ERROR)
74+
->addViolation();
75+
}
76+
}
77+
}

Resources/translations/validators.en.xlf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,14 @@
394394
<source>This value is not a valid CSS color.</source>
395395
<target>This value is not a valid CSS color.</target>
396396
</trans-unit>
397+
<trans-unit id="102">
398+
<source>This value is not a valid CIDR notation.</source>
399+
<target>This value is not a valid CIDR notation.</target>
400+
</trans-unit>
401+
<trans-unit id="103">
402+
<source>The value of the netmask should be between {{ min }} and {{ max }}.</source>
403+
<target>The value of the netmask should be between {{ min }} and {{ max }}.</target>
404+
</trans-unit>
397405
</body>
398406
</file>
399407
</xliff>

Resources/translations/validators.fr.xlf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,14 @@
394394
<source>This value is not a valid CSS color.</source>
395395
<target>Cette valeur n'est pas une couleur CSS valide.</target>
396396
</trans-unit>
397+
<trans-unit id="102">
398+
<source>This value is not a valid CIDR notation.</source>
399+
<target>Cette valeur n'est pas une notation CIDR valide.</target>
400+
</trans-unit>
401+
<trans-unit id="103">
402+
<source>The value of the netmask should be between {{ min }} and {{ max }}.</source>
403+
<target>La valeur du masque de réseau doit être comprise entre {{ min }} et {{ max }}.</target>
404+
</trans-unit>
397405
</body>
398406
</file>
399407
</xliff>

Resources/translations/validators.ro.xlf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,14 @@
390390
<source>This value should be a valid expression.</source>
391391
<target>Această valoare ar trebui să fie o expresie validă.</target>
392392
</trans-unit>
393+
<trans-unit id="102">
394+
<source>This value is not a valid CIDR notation.</source>
395+
<target>Această valoare nu este o notație CIDR validă.</target>
396+
</trans-unit>
397+
<trans-unit id="103">
398+
<source>The value of the netmask should be between {{ min }} and {{ max }}.</source>
399+
<target>Valoarea netmask-ului trebuie sa fie intre {{ min }} si {{ max }}.</target>
400+
</trans-unit>
393401
</body>
394402
</file>
395403
</xliff>

Tests/Constraints/CidrTest.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace Symfony\Component\Validator\Tests\Constraints;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\Validator\Constraints\Cidr;
7+
use Symfony\Component\Validator\Constraints\Ip;
8+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
9+
use Symfony\Component\Validator\Mapping\ClassMetadata;
10+
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
11+
12+
class CidrTest extends TestCase
13+
{
14+
public function testForAll()
15+
{
16+
$cidrConstraint = new Cidr();
17+
18+
self::assertEquals(Ip::ALL, $cidrConstraint->version);
19+
self::assertEquals(0, $cidrConstraint->netmaskMin);
20+
self::assertEquals(128, $cidrConstraint->netmaskMax);
21+
}
22+
23+
public function testForV4()
24+
{
25+
$cidrConstraint = new Cidr(['version' => Ip::V4]);
26+
27+
self::assertEquals(Ip::V4, $cidrConstraint->version);
28+
self::assertEquals(0, $cidrConstraint->netmaskMin);
29+
self::assertEquals(32, $cidrConstraint->netmaskMax);
30+
}
31+
32+
public function testForV6()
33+
{
34+
$cidrConstraint = new Cidr(['version' => Ip::V6]);
35+
36+
self::assertEquals(Ip::V6, $cidrConstraint->version);
37+
self::assertEquals(0, $cidrConstraint->netmaskMin);
38+
self::assertEquals(128, $cidrConstraint->netmaskMax);
39+
}
40+
41+
public function testWithInvalidVersion()
42+
{
43+
$availableVersions = [Ip::ALL, Ip::V4, Ip::V6];
44+
45+
self::expectException(ConstraintDefinitionException::class);
46+
self::expectExceptionMessage(sprintf('The option "version" must be one of "%s".', implode('", "', $availableVersions)));
47+
48+
new Cidr(['version' => '8']);
49+
}
50+
51+
/**
52+
* @dataProvider getValidMinMaxValues
53+
*/
54+
public function testWithValidMinMaxValues(string $ipVersion, int $netmaskMin, int $netmaskMax)
55+
{
56+
$cidrConstraint = new Cidr([
57+
'version' => $ipVersion,
58+
'netmaskMin' => $netmaskMin,
59+
'netmaskMax' => $netmaskMax,
60+
]);
61+
62+
self::assertEquals($ipVersion, $cidrConstraint->version);
63+
self::assertEquals($netmaskMin, $cidrConstraint->netmaskMin);
64+
self::assertEquals($netmaskMax, $cidrConstraint->netmaskMax);
65+
}
66+
67+
/**
68+
* @dataProvider getInvalidMinMaxValues
69+
*/
70+
public function testWithInvalidMinMaxValues(string $ipVersion, int $netmaskMin, int $netmaskMax)
71+
{
72+
$expectedMax = Ip::V4 == $ipVersion ? 32 : 128;
73+
74+
self::expectException(ConstraintDefinitionException::class);
75+
self::expectExceptionMessage(sprintf('The netmask range must be between 0 and %d.', $expectedMax));
76+
77+
new Cidr([
78+
'version' => $ipVersion,
79+
'netmaskMin' => $netmaskMin,
80+
'netmaskMax' => $netmaskMax,
81+
]);
82+
}
83+
84+
public function getInvalidMinMaxValues(): array
85+
{
86+
return [
87+
[Ip::ALL, -1, 23],
88+
[Ip::ALL, 23, 130],
89+
[Ip::ALL, 2, -4],
90+
[Ip::ALL, -12, -40],
91+
[Ip::V4, 0, 33],
92+
[Ip::V4, 2, -10],
93+
[Ip::V4, -4, 128],
94+
[Ip::V4, -5, -1],
95+
[Ip::V6, 5, 200],
96+
[Ip::V6, -1, 120],
97+
[Ip::V6, 0, -10],
98+
[Ip::V6, -15, -20],
99+
];
100+
}
101+
102+
public function getValidMinMaxValues(): array
103+
{
104+
return [
105+
[Ip::ALL, 0, 23],
106+
[Ip::ALL, 23, 120],
107+
[Ip::V4, 0, 5],
108+
[Ip::V4, 2, 10],
109+
[Ip::V6, 0, 43],
110+
[Ip::V6, 33, 100],
111+
];
112+
}
113+
114+
/**
115+
* @requires PHP 8
116+
*/
117+
public function testAttributes()
118+
{
119+
$metadata = new ClassMetadata(CidrDummy::class);
120+
$loader = new AnnotationLoader();
121+
self::assertTrue($loader->loadClassMetadata($metadata));
122+
123+
[$aConstraint] = $metadata->properties['a']->getConstraints();
124+
self::assertSame(Ip::ALL, $aConstraint->version);
125+
self::assertSame(0, $aConstraint->netmaskMin);
126+
self::assertSame(128, $aConstraint->netmaskMax);
127+
128+
[$bConstraint] = $metadata->properties['b']->getConstraints();
129+
self::assertSame(Ip::V6, $bConstraint->version);
130+
self::assertSame('myMessage', $bConstraint->message);
131+
self::assertSame(10, $bConstraint->netmaskMin);
132+
self::assertSame(126, $bConstraint->netmaskMax);
133+
self::assertSame(['Default', 'CidrDummy'], $bConstraint->groups);
134+
135+
[$cConstraint] = $metadata->properties['c']->getConstraints();
136+
self::assertSame(['my_group'], $cConstraint->groups);
137+
self::assertSame('some attached data', $cConstraint->payload);
138+
}
139+
}
140+
141+
class CidrDummy
142+
{
143+
#[Cidr]
144+
private $a;
145+
146+
#[Cidr(version: Ip::V6, message: 'myMessage', netmaskMin: 10, netmaskMax: 126)]
147+
private $b;
148+
149+
#[Cidr(groups: ['my_group'], payload: 'some attached data')]
150+
private $c;
151+
}

0 commit comments

Comments
 (0)