Completed
Push — 2.1 ( 75349f...bf116e )
by Alexander
29:27
created

IpValidator::getClientOptions()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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