mirror of
https://github.com/Respect/Validation.git
synced 2024-05-29 11:52:34 +02:00
Improve code and documentation of "Ip" rule
This commit will do many different things, but they are all improvements to the "Ip" rules: * Remove passing variables by reference: the "Ip" class uses that a lot to define the start address, end address, and the mask used to validate a network range; * Remove double-typed argument from the constructor: the class "Ip" class has only one argument that can be either a string with the range of the IP or an integer with options for the "filter_var()" function. This commit will split it into two different arguments, each of them used for one of this functionalities; * Update documentation to show how to validate IPv6. Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
parent
2aaec39dbb
commit
bd63f65c91
|
@ -2,7 +2,7 @@
|
|||
|
||||
- `Ip()`
|
||||
- `Ip(string $range)`
|
||||
- `Ip(int $options)`
|
||||
- `Ip(string $range, int $options)`
|
||||
|
||||
Validates whether the input is a valid IP address.
|
||||
|
||||
|
@ -17,13 +17,20 @@ v::ip('220.78.168.0/21')->validate('220.78.176.2'); // false
|
|||
You can pass a parameter with [filter_var()][] flags for IP.
|
||||
|
||||
```php
|
||||
v::ip(FILTER_FLAG_NO_PRIV_RANGE)->validate('192.168.0.1'); // false
|
||||
v::ip('*', FILTER_FLAG_NO_PRIV_RANGE)->validate('192.168.0.1'); // false
|
||||
```
|
||||
|
||||
If you want to validate IPv6 you can do as follow:
|
||||
|
||||
```php
|
||||
v::ip('*', FILTER_FLAG_IPV6)->validate('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'); // true
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
Version | Description
|
||||
--------|-------------
|
||||
2.0.0 | Allow to define range and options to the same instance
|
||||
0.5.0 | Implemented IP range validation
|
||||
0.3.9 | Created
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ final class IpException extends ValidationException
|
|||
*/
|
||||
protected function chooseTemplate(): string
|
||||
{
|
||||
if (!$this->getParam('networkRange')) {
|
||||
if (!$this->getParam('range')) {
|
||||
return static::STANDARD;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,11 +18,12 @@ use function bccomp;
|
|||
use function explode;
|
||||
use function filter_var;
|
||||
use function ip2long;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
use function long2ip;
|
||||
use function mb_strpos;
|
||||
use function mb_substr_count;
|
||||
use function sprintf;
|
||||
use function str_repeat;
|
||||
use function str_replace;
|
||||
use function strtr;
|
||||
|
||||
|
@ -38,38 +39,44 @@ use function strtr;
|
|||
*/
|
||||
final class Ip extends AbstractRule
|
||||
{
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $ipOptions;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $range;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
* @var int|null
|
||||
*/
|
||||
private $networkRange;
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* Initializes the rule defining the range or options for filter_var().
|
||||
* @var string|null
|
||||
*/
|
||||
private $startAddress;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $endAddress;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $mask;
|
||||
|
||||
/**
|
||||
* Initializes the rule defining the range and some options for filter_var().
|
||||
*
|
||||
* @param int|string $ipOptions
|
||||
* @param string $range
|
||||
* @param int|null $options
|
||||
*
|
||||
* @throws ComponentException In case the range is invalid
|
||||
*/
|
||||
public function __construct($ipOptions = null)
|
||||
public function __construct(string $range = '*', int $options = null)
|
||||
{
|
||||
if (is_int($ipOptions)) {
|
||||
$this->ipOptions = $ipOptions;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->networkRange = $this->parseRange($ipOptions);
|
||||
$this->parseRange($range);
|
||||
$this->range = $this->createRange();
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,126 +84,124 @@ final class Ip extends AbstractRule
|
|||
*/
|
||||
public function validate($input): bool
|
||||
{
|
||||
return $this->verifyAddress($input) && $this->verifyNetwork($input);
|
||||
if (!is_string($input)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->verifyAddress($input)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->mask) {
|
||||
return $this->belongsToSubnet($input);
|
||||
}
|
||||
|
||||
if ($this->startAddress && $this->endAddress) {
|
||||
return $this->verifyNetwork($input);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createRange(): ?string
|
||||
{
|
||||
if (!$this->networkRange) {
|
||||
return null;
|
||||
if ($this->endAddress && $this->endAddress) {
|
||||
return $this->startAddress.'-'.$this->endAddress;
|
||||
}
|
||||
|
||||
$range = $this->networkRange;
|
||||
$message = $range['min'];
|
||||
|
||||
if (isset($range['max'])) {
|
||||
$message .= '-'.$range['max'];
|
||||
} else {
|
||||
$message .= '/'.long2ip((int) $range['mask']);
|
||||
if ($this->startAddress && $this->mask) {
|
||||
return $this->startAddress.'/'.long2ip((int) $this->mask);
|
||||
}
|
||||
|
||||
return $message;
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseRange(?string $input): ?array
|
||||
private function parseRange(string $input): void
|
||||
{
|
||||
if (null === $input || '*' == $input || '*.*.*.*' == $input
|
||||
|| '0.0.0.0-255.255.255.255' == $input) {
|
||||
return null;
|
||||
if ('*' == $input || '*.*.*.*' == $input || '0.0.0.0-255.255.255.255' == $input) {
|
||||
return;
|
||||
}
|
||||
|
||||
$range = ['min' => null, 'max' => null, 'mask' => null];
|
||||
|
||||
if (false !== mb_strpos($input, '-')) {
|
||||
list($range['min'], $range['max']) = explode('-', $input);
|
||||
} elseif (false !== mb_strpos($input, '*')) {
|
||||
$this->parseRangeUsingWildcards($input, $range);
|
||||
} elseif (false !== mb_strpos($input, '/')) {
|
||||
$this->parseRangeUsingCidr($input, $range);
|
||||
} else {
|
||||
throw new ComponentException('Invalid network range');
|
||||
}
|
||||
list($this->startAddress, $this->endAddress) = explode('-', $input);
|
||||
|
||||
if (!$this->verifyAddress($range['min'])) {
|
||||
throw new ComponentException('Invalid network range');
|
||||
}
|
||||
if (!$this->verifyAddress($this->startAddress)) {
|
||||
throw new ComponentException('Invalid network range');
|
||||
}
|
||||
|
||||
if (isset($range['max']) && !$this->verifyAddress($range['max'])) {
|
||||
throw new ComponentException('Invalid network range');
|
||||
}
|
||||
|
||||
return $range;
|
||||
}
|
||||
|
||||
private function fillAddress(&$input, $char = '*'): void
|
||||
{
|
||||
while (mb_substr_count($input, '.') < 3) {
|
||||
$input .= '.'.$char;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseRangeUsingWildcards($input, &$range): void
|
||||
{
|
||||
$this->fillAddress($input);
|
||||
|
||||
$range['min'] = strtr($input, '*', '0');
|
||||
$range['max'] = str_replace('*', '255', $input);
|
||||
}
|
||||
|
||||
private function parseRangeUsingCidr($input, &$range): void
|
||||
{
|
||||
$input = explode('/', $input);
|
||||
$this->fillAddress($input[0], '0');
|
||||
|
||||
$range['min'] = $input[0];
|
||||
$isAddressMask = false !== mb_strpos($input[1], '.');
|
||||
|
||||
if ($isAddressMask && $this->verifyAddress($input[1])) {
|
||||
$range['mask'] = sprintf('%032b', ip2long($input[1]));
|
||||
if (!$this->verifyAddress($this->endAddress)) {
|
||||
throw new ComponentException('Invalid network range');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isAddressMask || $input[1] < 8 || $input[1] > 30) {
|
||||
if (false !== mb_strpos($input, '*')) {
|
||||
$this->parseRangeUsingWildcards($input);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (false !== mb_strpos($input, '/')) {
|
||||
$this->parseRangeUsingCidr($input);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ComponentException('Invalid network range');
|
||||
}
|
||||
|
||||
private function fillAddress(string $address, string $fill = '*'): string
|
||||
{
|
||||
return $address.str_repeat('.'.$fill, 3 - mb_substr_count($address, '.'));
|
||||
}
|
||||
|
||||
private function parseRangeUsingWildcards(string $input): void
|
||||
{
|
||||
$address = $this->fillAddress($input);
|
||||
|
||||
$this->startAddress = strtr($address, '*', '0');
|
||||
$this->endAddress = str_replace('*', '255', $address);
|
||||
}
|
||||
|
||||
private function parseRangeUsingCidr(string $input): void
|
||||
{
|
||||
$parts = explode('/', $input);
|
||||
|
||||
$this->startAddress = $this->fillAddress($parts[0], '0');
|
||||
$isAddressMask = false !== mb_strpos($parts[1], '.');
|
||||
|
||||
if ($isAddressMask && $this->verifyAddress($parts[1])) {
|
||||
$this->mask = sprintf('%032b', ip2long($parts[1]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isAddressMask || $parts[1] < 8 || $parts[1] > 30) {
|
||||
throw new ComponentException('Invalid network mask');
|
||||
}
|
||||
|
||||
$range['mask'] = sprintf('%032b', ip2long(long2ip(~(2 ** (32 - $input[1]) - 1))));
|
||||
$this->mask = sprintf('%032b', ip2long(long2ip(~(2 ** (32 - $parts[1]) - 1))));
|
||||
}
|
||||
|
||||
private function verifyAddress($address): bool
|
||||
private function verifyAddress(string $address): bool
|
||||
{
|
||||
return (bool) filter_var(
|
||||
$address,
|
||||
FILTER_VALIDATE_IP,
|
||||
[
|
||||
'flags' => $this->ipOptions,
|
||||
]
|
||||
);
|
||||
return false !== filter_var($address, FILTER_VALIDATE_IP, ['flags' => $this->options]);
|
||||
}
|
||||
|
||||
private function verifyNetwork($input): bool
|
||||
private function verifyNetwork(string $input): bool
|
||||
{
|
||||
if (null === $this->networkRange) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($this->networkRange['mask'])) {
|
||||
return $this->belongsToSubnet($input);
|
||||
}
|
||||
|
||||
$input = sprintf('%u', ip2long($input));
|
||||
|
||||
return bccomp($input, sprintf('%u', ip2long($this->networkRange['min']))) >= 0
|
||||
&& bccomp($input, sprintf('%u', ip2long($this->networkRange['max']))) <= 0;
|
||||
return bccomp($input, sprintf('%u', ip2long($this->startAddress))) >= 0
|
||||
&& bccomp($input, sprintf('%u', ip2long($this->endAddress))) <= 0;
|
||||
}
|
||||
|
||||
private function belongsToSubnet($input): bool
|
||||
private function belongsToSubnet(string $input): bool
|
||||
{
|
||||
$range = $this->networkRange;
|
||||
$min = sprintf('%032b', ip2long($range['min']));
|
||||
$min = sprintf('%032b', ip2long($this->startAddress));
|
||||
$input = sprintf('%032b', ip2long($input));
|
||||
|
||||
return ($input & $range['mask']) === ($min & $range['mask']);
|
||||
return ($input & $this->mask) === ($min & $this->mask);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
|
|||
* @method static Validator instance(string $instanceName)
|
||||
* @method static Validator intVal()
|
||||
* @method static Validator intType()
|
||||
* @method static Validator ip($ipOptions = null)
|
||||
* @method static Validator ip(string $range = '*', int $options = null)
|
||||
* @method static Validator isbn()
|
||||
* @method static Validator iterableType()
|
||||
* @method static Validator json()
|
||||
|
|
|
@ -54,6 +54,7 @@ final class IpTest extends RuleTestCase
|
|||
[new Ip('220.78.168/21'), '220.78.173.2'],
|
||||
[new Ip('220.78.168.0/21'), '220.78.173.2'],
|
||||
[new Ip('220.78.168.0/255.255.248.0'), '220.78.173.2'],
|
||||
[new Ip('*', FILTER_FLAG_IPV6), '2001:0db8:85a3:08d3:1319:8a2e:0370:7334'],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -71,7 +72,7 @@ final class IpTest extends RuleTestCase
|
|||
[new Ip(), 'j'],
|
||||
[new Ip(), ' '],
|
||||
[new Ip(), 'Foo'],
|
||||
[new Ip(FILTER_FLAG_NO_PRIV_RANGE), '192.168.0.1'],
|
||||
[new Ip('*', FILTER_FLAG_NO_PRIV_RANGE), '192.168.0.1'],
|
||||
[new Ip('127.0.1.*'), '127.0.0.1'],
|
||||
[new Ip('192.163.*.*'), '192.168.2.6'],
|
||||
[new Ip('193.*.*.*'), '192.10.2.6'],
|
||||
|
|
Loading…
Reference in a new issue