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:
Henrique Moody 2018-12-07 19:38:27 +01:00
parent 2aaec39dbb
commit bd63f65c91
No known key found for this signature in database
GPG key ID: 221E9281655813A6
5 changed files with 121 additions and 108 deletions

View file

@ -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

View file

@ -42,7 +42,7 @@ final class IpException extends ValidationException
*/
protected function chooseTemplate(): string
{
if (!$this->getParam('networkRange')) {
if (!$this->getParam('range')) {
return static::STANDARD;
}

View file

@ -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);
}
}

View file

@ -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()

View file

@ -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'],