Completed
Push — remove-intl-polyfills ( 129df4...a6e727 )
by Alexander
16:22 queued 12:49
created

IpValidator::getRanges()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\validators;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\helpers\IpHelper;
13
14
/**
15
 * The validator checks if the attribute value is a valid IPv4/IPv6 address or subnet.
16
 *
17
 * It also may change attribute's value if normalization of IPv6 expansion is enabled.
18
 *
19
 * The following are examples of validation rules using this validator:
20
 *
21
 * ```php
22
 * ['ip_address', 'ip'], // IPv4 or IPv6 address
23
 * ['ip_address', 'ip', 'ipv6' => false], // IPv4 address (IPv6 is disabled)
24
 * ['ip_address', 'ip', 'subnet' => true], // requires a CIDR prefix (like 10.0.0.1/24) for the IP address
25
 * ['ip_address', 'ip', 'subnet' => null], // CIDR prefix is optional
26
 * ['ip_address', 'ip', 'subnet' => null, 'normalize' => true], // CIDR prefix is optional and will be added when missing
27
 * ['ip_address', 'ip', 'ranges' => ['192.168.0.0/24']], // only IP addresses from the specified subnet are allowed
28
 * ['ip_address', 'ip', 'ranges' => ['!192.168.0.0/24', 'any']], // any IP is allowed except IP in the specified subnet
29
 * ['ip_address', 'ip', 'expandIPv6' => true], // expands IPv6 address to a full notation format
30
 * ```
31
 *
32
 * @property array $ranges The IPv4 or IPv6 ranges that are allowed or forbidden. See [[setRanges()]] for
33
 * detailed description.
34
 *
35
 * @author Dmitry Naumenko <[email protected]>
36
 * @since 2.0.7
37
 */
38
class IpValidator extends Validator
39
{
40
    /**
41
     * Negation char.
42
     *
43
     * Used to negate [[ranges]] or [[networks]] or to negate validating value when [[negation]] is set to `true`.
44
     * @see negation
45
     * @see networks
46
     * @see ranges
47
     */
48
    const NEGATION_CHAR = '!';
49
50
    /**
51
     * @var array The network aliases, that can be used in [[ranges]].
52
     *  - key - alias name
53
     *  - value - array of strings. String can be an IP range, IP address or another alias. String can be
54
     *    negated with [[NEGATION_CHAR]] (independent of `negation` option).
55
     *
56
     * The following aliases are defined by default:
57
     *  - `*`: `any`
58
     *  - `any`: `0.0.0.0/0, ::/0`
59
     *  - `private`: `10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fd00::/8`
60
     *  - `multicast`: `224.0.0.0/4, ff00::/8`
61
     *  - `linklocal`: `169.254.0.0/16, fe80::/10`
62
     *  - `localhost`: `127.0.0.0/8', ::1`
63
     *  - `documentation`: `192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32`
64
     *  - `system`: `multicast, linklocal, localhost, documentation`
65
     */
66
    public $networks = [
67
        '*' => ['any'],
68
        'any' => ['0.0.0.0/0', '::/0'],
69
        'private' => ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fd00::/8'],
70
        'multicast' => ['224.0.0.0/4', 'ff00::/8'],
71
        'linklocal' => ['169.254.0.0/16', 'fe80::/10'],
72
        'localhost' => ['127.0.0.0/8', '::1'],
73
        'documentation' => ['192.0.2.0/24', '198.51.100.0/24', '203.0.113.0/24', '2001:db8::/32'],
74
        'system' => ['multicast', 'linklocal', 'localhost', 'documentation'],
75
    ];
76
    /**
77
     * @var bool whether the validating value can be an IPv6 address. Defaults to `true`.
78
     */
79
    public $ipv6 = true;
80
    /**
81
     * @var bool whether the validating value can be an IPv4 address. Defaults to `true`.
82
     */
83
    public $ipv4 = true;
84
    /**
85
     * @var bool whether the address can be an IP with CIDR subnet, like `192.168.10.0/24`.
86
     * The following values are possible:
87
     *
88
     * - `false` - the address must not have a subnet (default).
89
     * - `true` - specifying a subnet is required.
90
     * - `null` - specifying a subnet is optional.
91
     */
92
    public $subnet = false;
93
    /**
94
     * @var bool whether to add the CIDR prefix with the smallest length (32 for IPv4 and 128 for IPv6) to an
95
     * address without it. Works only when `subnet` is not `false`. For example:
96
     *  - `10.0.1.5` will normalized to `10.0.1.5/32`
97
     *  - `2008:db0::1` will be normalized to `2008:db0::1/128`
98
     *    Defaults to `false`.
99
     * @see subnet
100
     */
101
    public $normalize = false;
102
    /**
103
     * @var bool whether address may have a [[NEGATION_CHAR]] character at the beginning.
104
     * Defaults to `false`.
105
     */
106
    public $negation = false;
107
    /**
108
     * @var bool whether to expand an IPv6 address to the full notation format.
109
     * Defaults to `false`.
110
     */
111
    public $expandIPv6 = false;
112
    /**
113
     * @var string Regexp-pattern to validate IPv4 address
114
     */
115
    public $ipv4Pattern = '/^(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))$/';
116
    /**
117
     * @var string Regexp-pattern to validate IPv6 address
118
     */
119
    public $ipv6Pattern = '/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/';
120
    /**
121
     * @var string user-defined error message is used when validation fails due to the wrong IP address format.
122
     *
123
     * You may use the following placeholders in the message:
124
     *
125
     * - `{attribute}`: the label of the attribute being validated
126
     * - `{value}`: the value of the attribute being validated
127
     */
128
    public $message;
129
    /**
130
     * @var string user-defined error message is used when validation fails due to the disabled IPv6 validation.
131
     *
132
     * You may use the following placeholders in the message:
133
     *
134
     * - `{attribute}`: the label of the attribute being validated
135
     * - `{value}`: the value of the attribute being validated
136
     *
137
     * @see ipv6
138
     */
139
    public $ipv6NotAllowed;
140
    /**
141
     * @var string user-defined error message is used when validation fails due to the disabled IPv4 validation.
142
     *
143
     * You may use the following placeholders in the message:
144
     *
145
     * - `{attribute}`: the label of the attribute being validated
146
     * - `{value}`: the value of the attribute being validated
147
     *
148
     * @see ipv4
149
     */
150
    public $ipv4NotAllowed;
151
    /**
152
     * @var string user-defined error message is used when validation fails due to the wrong CIDR.
153
     *
154
     * You may use the following placeholders in the message:
155
     *
156
     * - `{attribute}`: the label of the attribute being validated
157
     * - `{value}`: the value of the attribute being validated
158
     * @see subnet
159
     */
160
    public $wrongCidr;
161
    /**
162
     * @var string user-defined error message is used when validation fails due to subnet [[subnet]] set to 'only',
163
     * but the CIDR prefix is not set.
164
     *
165
     * You may use the following placeholders in the message:
166
     *
167
     * - `{attribute}`: the label of the attribute being validated
168
     * - `{value}`: the value of the attribute being validated
169
     *
170
     * @see subnet
171
     */
172
    public $noSubnet;
173
    /**
174
     * @var string user-defined error message is used when validation fails
175
     * due to [[subnet]] is false, but CIDR prefix is present.
176
     *
177
     * You may use the following placeholders in the message:
178
     *
179
     * - `{attribute}`: the label of the attribute being validated
180
     * - `{value}`: the value of the attribute being validated
181
     *
182
     * @see subnet
183
     */
184
    public $hasSubnet;
185
    /**
186
     * @var string user-defined error message is used when validation fails due to IP address
187
     * is not not allowed by [[ranges]] check.
188
     *
189
     * You may use the following placeholders in the message:
190
     *
191
     * - `{attribute}`: the label of the attribute being validated
192
     * - `{value}`: the value of the attribute being validated
193
     *
194
     * @see ranges
195
     */
196
    public $notInRange;
197
198
    /**
199
     * @var array
200
     */
201
    private $_ranges = [];
202
203
204
    /**
205
     * {@inheritdoc}
206
     */
207 51
    public function init()
208
    {
209 51
        parent::init();
210
211 51
        if (!$this->ipv4 && !$this->ipv6) {
212 1
            throw new InvalidConfigException('Both IPv4 and IPv6 checks can not be disabled at the same time');
213
        }
214 50
        if ($this->message === null) {
215 50
            $this->message = Yii::t('yii', '{attribute} must be a valid IP address.');
216
        }
217 50
        if ($this->ipv6NotAllowed === null) {
218 50
            $this->ipv6NotAllowed = Yii::t('yii', '{attribute} must not be an IPv6 address.');
219
        }
220 50
        if ($this->ipv4NotAllowed === null) {
221 50
            $this->ipv4NotAllowed = Yii::t('yii', '{attribute} must not be an IPv4 address.');
222
        }
223 50
        if ($this->wrongCidr === null) {
224 50
            $this->wrongCidr = Yii::t('yii', '{attribute} contains wrong subnet mask.');
225
        }
226 50
        if ($this->noSubnet === null) {
227 50
            $this->noSubnet = Yii::t('yii', '{attribute} must be an IP address with specified subnet.');
228
        }
229 50
        if ($this->hasSubnet === null) {
230 50
            $this->hasSubnet = Yii::t('yii', '{attribute} must not be a subnet.');
231
        }
232 50
        if ($this->notInRange === null) {
233 50
            $this->notInRange = Yii::t('yii', '{attribute} is not in the allowed range.');
234
        }
235 50
    }
236
237
    /**
238
     * Set the IPv4 or IPv6 ranges that are allowed or forbidden.
239
     *
240
     * The following preparation tasks are performed:
241
     *
242
     * - Recursively substitutes aliases (described in [[networks]]) with their values.
243
     * - Removes duplicates
244
     *
245
     * @property array the IPv4 or IPv6 ranges that are allowed or forbidden.
246
     * See [[setRanges()]] for detailed description.
247
     * @param array $ranges the IPv4 or IPv6 ranges that are allowed or forbidden.
248
     *
249
     * When the array is empty, or the option not set, all IP addresses are allowed.
250
     *
251
     * Otherwise, the rules are checked sequentially until the first match is found.
252
     * An IP address is forbidden, when it has not matched any of the rules.
253
     *
254
     * Example:
255
     *
256
     * ```php
257
     * [
258
     *      'ranges' => [
259
     *          '192.168.10.128'
260
     *          '!192.168.10.0/24',
261
     *          'any' // allows any other IP addresses
262
     *      ]
263
     * ]
264
     * ```
265
     *
266
     * In this example, access is allowed for all the IPv4 and IPv6 addresses excluding the `192.168.10.0/24` subnet.
267
     * IPv4 address `192.168.10.128` is also allowed, because it is listed before the restriction.
268
     */
269 33
    public function setRanges($ranges)
270
    {
271 33
        $this->_ranges = $this->prepareRanges((array) $ranges);
272 33
    }
273
274
    /**
275
     * @return array The IPv4 or IPv6 ranges that are allowed or forbidden.
276
     */
277 23
    public function getRanges()
278
    {
279 23
        return $this->_ranges;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285 38
    protected function validateValue($value)
286
    {
287 38
        $result = $this->validateSubnet($value);
288 38
        if (is_array($result)) {
289 33
            $result[1] = array_merge(['ip' => is_array($value) ? 'array()' : $value], $result[1]);
290 33
            return $result;
291
        }
292
293 12
        return null;
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     */
299 8
    public function validateAttribute($model, $attribute)
300
    {
301 8
        $value = $model->$attribute;
302
303 8
        $result = $this->validateSubnet($value);
304 8
        if (is_array($result)) {
305 8
            $result[1] = array_merge(['ip' => is_array($value) ? 'array()' : $value], $result[1]);
306 8
            $this->addError($model, $attribute, $result[0], $result[1]);
307
        } else {
308 2
            $model->$attribute = $result;
309
        }
310 8
    }
311
312
    /**
313
     * Validates an IPv4/IPv6 address or subnet.
314
     *
315
     * @param $ip string
316
     * @return string|array
317
     * string - the validation was successful;
318
     * array  - an error occurred during the validation.
319
     * Array[0] contains the text of an error, array[1] contains values for the placeholders in the error message
320
     */
321 46
    private function validateSubnet($ip)
322
    {
323 46
        if (!is_string($ip)) {
324 23
            return [$this->message, []];
325
        }
326
327 23
        $negation = null;
328 23
        $cidr = null;
329 23
        $isCidrDefault = false;
330
331 23
        if (preg_match($this->getIpParsePattern(), $ip, $matches)) {
332 23
            $negation = ($matches[1] !== '') ? $matches[1] : null;
333 23
            $ip = $matches[2];
334 23
            $cidr = $matches[4] ?? null;
335
        }
336
337 23
        if ($this->subnet === true && $cidr === null) {
338 3
            return [$this->noSubnet, []];
339
        }
340 23
        if ($this->subnet === false && $cidr !== null) {
341 5
            return [$this->hasSubnet, []];
342
        }
343 23
        if ($this->negation === false && $negation !== null) {
344 3
            return [$this->message, []];
345
        }
346
347 23
        if ($this->getIpVersion($ip) === IpHelper::IPV6) {
348 8
            if ($cidr !== null) {
349 3
                if ($cidr > IpHelper::IPV6_ADDRESS_LENGTH || $cidr < 0) {
350 3
                    return [$this->wrongCidr, []];
351
                }
352
            } else {
353 8
                $isCidrDefault = true;
354 8
                $cidr = IpHelper::IPV6_ADDRESS_LENGTH;
355
            }
356
357 8
            if (!$this->validateIPv6($ip)) {
358 4
                return [$this->message, []];
359
            }
360 6
            if (!$this->ipv6) {
361 2
                return [$this->ipv6NotAllowed, []];
362
            }
363
364 6
            if ($this->expandIPv6) {
365 6
                $ip = $this->expandIPv6($ip);
366
            }
367
        } else {
368 18
            if ($cidr !== null) {
369 4
                if ($cidr > IpHelper::IPV4_ADDRESS_LENGTH || $cidr < 0) {
370 4
                    return [$this->wrongCidr, []];
371
                }
372
            } else {
373 18
                $isCidrDefault = true;
374 18
                $cidr = IpHelper::IPV4_ADDRESS_LENGTH;
375
            }
376 18
            if (!$this->validateIPv4($ip)) {
377 4
                return [$this->message, []];
378
            }
379 16
            if (!$this->ipv4) {
380 2
                return [$this->ipv4NotAllowed, []];
381
            }
382
        }
383
384 19
        if (!$this->isAllowed($ip, $cidr)) {
385 9
            return [$this->notInRange, []];
386
        }
387
388 14
        $result = $negation . $ip;
389
390 14
        if ($this->subnet !== false && (!$isCidrDefault || $isCidrDefault && $this->normalize)) {
391 7
            $result .= "/$cidr";
392
        }
393
394 14
        return $result;
395
    }
396
397
    /**
398
     * Expands an IPv6 address to it's full notation.
399
     *
400
     * For example `2001:db8::1` will be expanded to `2001:0db8:0000:0000:0000:0000:0000:0001`.
401
     *
402
     * @param string $ip the original IPv6
403
     * @return string the expanded IPv6
404
     */
405 1
    private function expandIPv6($ip)
406
    {
407 1
        return IpHelper::expandIPv6($ip);
408
    }
409
410
    /**
411
     * The method checks whether the IP address with specified CIDR is allowed according to the [[ranges]] list.
412
     *
413
     * @param string $ip
414
     * @param int $cidr
415
     * @return bool
416
     * @see ranges
417
     */
418 19
    private function isAllowed($ip, $cidr)
419
    {
420 19
        if (empty($this->ranges)) {
421 5
            return true;
422
        }
423
424 14
        foreach ($this->ranges as $string) {
425 14
            [$isNegated, $range] = $this->parseNegatedRange($string);
0 ignored issues
show
Bug introduced by
The variable $isNegated does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $range does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
426 14
            if ($this->inRange($ip, $cidr, $range)) {
427 14
                return !$isNegated;
428
            }
429
        }
430
431 8
        return false;
432
    }
433
434
    /**
435
     * Parses IP address/range for the negation with [[NEGATION_CHAR]].
436
     *
437
     * @param $string
438
     * @return array `[0 => bool, 1 => string]`
439
     *  - boolean: whether the string is negated
440
     *  - string: the string without negation (when the negation were present)
441
     */
442 33
    private function parseNegatedRange($string)
443
    {
444 33
        $isNegated = strpos($string, static::NEGATION_CHAR) === 0;
445 33
        return [$isNegated, $isNegated ? substr($string, strlen(static::NEGATION_CHAR)) : $string];
446
    }
447
448
    /**
449
     * Prepares array to fill in [[ranges]].
450
     *
451
     *  - Recursively substitutes aliases, described in [[networks]] with their values,
452
     *  - Removes duplicates.
453
     *
454
     * @param $ranges
455
     * @return array
456
     * @see networks
457
     */
458 33
    private function prepareRanges($ranges)
459
    {
460 33
        $result = [];
461 33
        foreach ($ranges as $string) {
462 33
            [$isRangeNegated, $range] = $this->parseNegatedRange($string);
0 ignored issues
show
Bug introduced by
The variable $isRangeNegated does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $range does not exist. Did you mean $ranges?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
463 33
            if (isset($this->networks[$range])) {
0 ignored issues
show
Bug introduced by
The variable $range does not exist. Did you mean $ranges?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
464 6
                $replacements = $this->prepareRanges($this->networks[$range]);
0 ignored issues
show
Bug introduced by
The variable $range does not exist. Did you mean $ranges?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
465 6
                foreach ($replacements as &$replacement) {
466 6
                    [$isReplacementNegated, $replacement] = $this->parseNegatedRange($replacement);
0 ignored issues
show
Bug introduced by
The variable $isReplacementNegated does not exist. Did you mean $replacement?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
467 6
                    $result[] = ($isRangeNegated && !$isReplacementNegated ? static::NEGATION_CHAR : '') . $replacement;
0 ignored issues
show
Bug introduced by
The variable $isReplacementNegated does not exist. Did you mean $replacement?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
468
                }
469
            } else {
470 33
                $result[] = $string;
471
            }
472
        }
473
474 33
        return array_unique($result);
475
    }
476
477
    /**
478
     * Validates IPv4 address.
479
     *
480
     * @param string $value
481
     * @return bool
482
     */
483 18
    protected function validateIPv4($value)
484
    {
485 18
        return preg_match($this->ipv4Pattern, $value) !== 0;
486
    }
487
488
    /**
489
     * Validates IPv6 address.
490
     *
491
     * @param string $value
492
     * @return bool
493
     */
494 8
    protected function validateIPv6($value)
495
    {
496 8
        return preg_match($this->ipv6Pattern, $value) !== 0;
497
    }
498
499
    /**
500
     * Gets the IP version.
501
     *
502
     * @param string $ip
503
     * @return int
504
     */
505 23
    private function getIpVersion($ip)
506
    {
507 23
        return IpHelper::getIpVersion($ip);
508
    }
509
510
    /**
511
     * Used to get the Regexp pattern for initial IP address parsing.
512
     * @return string
513
     */
514 23
    public function getIpParsePattern()
515
    {
516 23
        return '/^(' . preg_quote(static::NEGATION_CHAR, '/') . '?)(.+?)(\/(\d+))?$/';
517
    }
518
519
    /**
520
     * Checks whether the IP is in subnet range.
521
     *
522
     * @param string $ip an IPv4 or IPv6 address
523
     * @param int $cidr
524
     * @param string $range subnet in CIDR format e.g. `10.0.0.0/8` or `2001:af::/64`
525
     * @return bool
526
     */
527 14
    private function inRange($ip, $cidr, $range)
528
    {
529 14
        return IpHelper::inRange($ip . '/' . $cidr, $range);
530
    }
531
}
532