Passed
Pull Request — master (#25)
by Alexander
14:50
created

Ip::parseNegatedRange()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

588
        $binStr = array_shift(/** @scrutinizer ignore-type */ $unpack);
Loading history...
589
        $bytes = self::IPV6_ADDRESS_LENGTH / 8; // 128 bit / 8 = 16 bytes
590
        $result = '';
591
        while ($bytes-- > 0) {
592
            $result = sprintf('%08b', isset($binStr[$bytes]) ? ord($binStr[$bytes]) : '0') . $result;
593
        }
594
        return $result;
595
    }
596
597
    public function ipv4(bool $value): self
598
    {
599
        $this->ipv4 = $value;
600
601
        return $this;
602
    }
603
604
    public function ipv6(bool $value): self
605
    {
606
        $this->ipv6 = $value;
607
608
        return $this;
609
    }
610
611
    /**
612
     * @param bool|null $value
613
     * @return $this
614
     */
615
    public function subnet(bool $value = null): self
616
    {
617
        $this->subnet = $value;
618
619
        return $this;
620
    }
621
622
    public function negation(bool $value): self
623
    {
624
        $this->negation = $value;
625
626
        return $this;
627
    }
628
629
    public function normalize(bool $value): self
630
    {
631
        $this->normalize = $value;
632
633
        return $this;
634
    }
635
}
636