Passed
Pull Request — master (#475)
by Alexander
29:15 queued 26:48
created

Ip::__construct()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 187
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 6
eloc 9
nc 6
nop 18
dl 0
loc 187
ccs 16
cts 16
cp 1
crap 6
rs 9.2222
c 2
b 1
f 0

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Attribute;
8
use Closure;
9
use InvalidArgumentException;
10
use Yiisoft\NetworkUtilities\IpHelper;
11
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
12
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;
13
use Yiisoft\Validator\Rule\Trait\WhenTrait;
14
use Yiisoft\Validator\RuleWithOptionsInterface;
15
use Yiisoft\Validator\SkipOnEmptyInterface;
16
use Yiisoft\Validator\SkipOnErrorInterface;
17
use Yiisoft\Validator\WhenInterface;
18
19
use function array_key_exists;
20
use function strlen;
21
22
/**
23
 * Checks if the value is a valid IPv4/IPv6 address or subnet.
24
 *
25
 * It also may change the value if normalization of IPv6 expansion is enabled.
26
 *
27
 * @psalm-import-type WhenType from WhenInterface
28
 */
29
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
30
final class Ip implements RuleWithOptionsInterface, SkipOnErrorInterface, WhenInterface, SkipOnEmptyInterface
31
{
32
    use SkipOnEmptyTrait;
33
    use SkipOnErrorTrait;
34
    use WhenTrait;
35
36
    /**
37
     * Negation character.
38
     *
39
     * Used to negate {@see $ranges} or {@see $network} or to negate value validated when {@see $allowNegation}
40
     * is used.
41
     */
42
    private const NEGATION_CHARACTER = '!';
43
    /**
44
     * @psalm-var array<string, list<string>>
45
     *
46
     * @var array Default network aliases that can be used in {@see $ranges}.
47
     *
48
     * @see $networks
49
     */
50
    private array $defaultNetworks = [
51
        '*' => ['any'],
52
        'any' => ['0.0.0.0/0', '::/0'],
53
        'private' => ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fd00::/8'],
54
        'multicast' => ['224.0.0.0/4', 'ff00::/8'],
55
        'linklocal' => ['169.254.0.0/16', 'fe80::/10'],
56
        'localhost' => ['127.0.0.0/8', '::1'],
57
        'documentation' => ['192.0.2.0/24', '198.51.100.0/24', '203.0.113.0/24', '2001:db8::/32'],
58 12
        'system' => ['multicast', 'linklocal', 'localhost', 'documentation'],
59
    ];
60
61
    public function __construct(
62
        /**
63
         * @var array<string, list<string>> Custom network aliases, that can be used in {@see $ranges}:
64
         *
65
         *  - key - alias name.
66
         *  - value - array of strings. String can be an IP range, IP address or another alias. String can be negated
67
         * with {@see NEGATION_CHARACTER} (independent of {@see $allowNegation} option).
68
         *
69
         * The following aliases are defined by default in {@see $defaultNetworks} and will be merged with custom ones:
70
         *
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
        private array $networks = [],
81
        /**
82
         * @var bool Whether the validating value can be an IPv4 address. Defaults to `true`.
83
         */
84
        private bool $allowIpv4 = true,
85
        /**
86
         * @var bool Whether the validating value can be an IPv6 address. Defaults to `true`.
87
         */
88
        private bool $allowIpv6 = true,
89
        /**
90
         * @var bool Whether the address can be an IP with CIDR subnet, like `192.168.10.0/24`. The following values are
91
         * possible:
92
         *
93
         * - `false` - the address must not have a subnet (default).
94
         * - `true` - specifying a subnet is optional.
95
         */
96
        private bool $allowSubnet = false,
97
        /**
98
         * @var bool Whether subnet is required.
99
         */
100
        private bool $requireSubnet = false,
101
        /**
102
         * @var bool Whether an address may have a {@see NEGATION_CHARACTER} character at the beginning.
103
         */
104
        private bool $allowNegation = false,
105
        /**
106
         * @var string A message used when the input it incorrect.
107
         *
108
         * You may use the following placeholders in the message:
109
         *
110
         * - `{attribute}`: the label of the attribute being validated.
111
         * - `{value}`: the value of the attribute being validated.
112
         */
113
        private string $incorrectInputMessage = 'The value must have a string type.',
114
        /**
115
         * @var string Error message used when validation fails due to the wrong IP address format.
116
         *
117
         * You may use the following placeholders in the message:
118
         *
119
         * - `{attribute}`: the label of the attribute being validated.
120
         * - `{value}`: the value of the attribute being validated.
121
         */
122
        private string $message = 'Must be a valid IP address.',
123
        /**
124
         * @var string Error message used when validation fails due to the disabled IPv4 validation when
125
         * {@see $allowIpv4} is set.
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 string $ipv4NotAllowedMessage = 'Must not be an IPv4 address.',
133
        /**
134
         * @var string Error message used when validation fails due to the disabled IPv6 validation when
135
         * {@see $allowIpv6} is set.
136
         *
137
         * You may use the following placeholders in the message:
138
         *
139
         * - `{attribute}`: the label of the attribute being validated.
140
         * - `{value}`: the value of the attribute being validated.
141
         */
142
        private string $ipv6NotAllowedMessage = 'Must not be an IPv6 address.',
143
        /**
144
         * @var string Error message used when validation fails due to the wrong CIDR when
145
         * {@see $allowSubnet} is set.
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
        private string $wrongCidrMessage = 'Contains wrong subnet mask.',
153
        /**
154
         * @var string Error message used when validation fails due to {@see $allowSubnet} is used, but
155
         * the CIDR prefix is not set.
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
         */
162
        private string $noSubnetMessage = 'Must be an IP address with specified subnet.',
163
        /**
164
         * @var string Error message used when validation fails due to {@see $allowSubnet} is false, but
165
         * CIDR prefix is present.
166
         *
167
         * You may use the following placeholders in the message:
168
         *
169
         * - `{attribute}`: the label of the attribute being validated.
170
         * - `{value}`: the value of the attribute being validated.
171
         */
172
        private string $hasSubnetMessage = 'Must not be a subnet.',
173
        /**
174
         * @var string Error message used when validation fails due to IP address is not allowed by
175
         * {@see $ranges} check.
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
        private string $notInRangeMessage = 'Is not in the allowed range.',
183
        /**
184
         * @var string[] The IPv4 or IPv6 ranges that are allowed or forbidden.
185
         *
186
         * The following preparation tasks are performed:
187
         *
188
         * - Recursively substitute aliases (described in {@see $networks}) with their values.
189
         * - Remove duplicates.
190
         *
191
         * When the array is empty, or the option not set, all IP addresses are allowed.
192
         *
193
         * Otherwise, the rules are checked sequentially until the first match is found. An IP address is forbidden,
194
         * when it has not matched any of the rules.
195
         *
196
         * Example:
197
         *
198
         * ```php
199
         * new Ip(ranges: [
200
         *     '192.168.10.128'
201
         *     '!192.168.10.0/24',
202
         *     'any' // allows any other IP addresses
203
         * ]);
204
         * ```
205
         *
206 12
         * In this example, access is allowed for all the IPv4 and IPv6 addresses excluding the `192.168.10.0/24`
207 1
         * subnet. IPv4 address `192.168.10.128` is also allowed, because it is listed before the restriction.
208
         */
209
        private array $ranges = [],
210 11
        /**
211 2
         * @var bool|callable|null Whether to skip this rule if the value validated is empty.
212 1
         *
213
         * @see SkipOnEmptyInterface
214
         */
215
        private mixed $skipOnEmpty = null,
216 10
        /**
217
         * @var bool Whether to skip this rule if any of the previous rules gave an error.
218 10
         */
219
        private bool $skipOnError = false,
220
        /**
221
         * @var Closure|null A callable to define a condition for applying the rule.
222
         * @psalm-var WhenType
223
         *
224
         * @see WhenInterface
225 10
         */
226
        private Closure|null $when = null,
227
    ) {
228 1
        if (!$this->allowIpv4 && !$this->allowIpv6) {
229
            throw new InvalidArgumentException('Both IPv4 and IPv6 checks can not be disabled at the same time.');
230 1
        }
231
232
        foreach ($networks as $key => $_values) {
233 2
            if (array_key_exists($key, $this->defaultNetworks)) {
234
                throw new InvalidArgumentException("Network alias \"{$key}\" already set as default.");
235 2
            }
236
        }
237
238 54
        $this->networks = array_merge($this->defaultNetworks, $this->networks);
239
240 54
        if ($requireSubnet) {
241
            // Might be a bug of XDebug, because this line is covered by tests (see "IpTest").
242
            // @codeCoverageIgnoreStart
243 31
            $this->allowSubnet = true;
244
            // @codeCoverageIgnoreEnd
245 31
        }
246
247
        $this->ranges = $this->prepareRanges($ranges);
248 38
    }
249
250 38
    public function getName(): string
251
    {
252
        return 'ip';
253 62
    }
254
255 62
    /**
256
     * @return array Custom network aliases, that can be used in {@see $ranges}.
257
     *
258 7
     * @see $networks
259
     */
260 7
    public function getNetworks(): array
261
    {
262
        return $this->networks;
263 8
    }
264
265 8
    /**
266
     * @return bool Whether the validating value can be an IPv4 address. Defaults to `true`.
267
     *
268 29
     * @see $allowIpv4
269
     */
270 29
    public function isIpv4Allowed(): bool
271
    {
272
        return $this->allowIpv4;
273 4
    }
274
275 4
    /**
276
     * @return bool Whether the validating value can be an IPv6 address. Defaults to `true`.
277
     *
278 4
     * @see $allowIpv6
279
     */
280 4
    public function isIpv6Allowed(): bool
281
    {
282
        return $this->allowIpv6;
283 5
    }
284
285 5
    /**
286
     * @return bool Whether the address can be an IP with CIDR subnet, like `192.168.10.0/24`.
287
     *
288 7
     * @see $allowSubnet
289
     */
290 7
    public function isSubnetAllowed(): bool
291
    {
292
        return $this->allowSubnet;
293 5
    }
294
295 5
    /**
296
     * @return bool Whether subnet is required.
297
     *
298 19
     * @see $requireSubnet
299
     */
300 19
    public function isSubnetRequired(): bool
301
    {
302
        return $this->requireSubnet;
303 5
    }
304
305 5
    /**
306
     * @return bool Whether an address may have a {@see NEGATION_CHARACTER} character at the beginning.
307
     *
308
     * @see $allowNegation
309
     */
310
    public function isNegationAllowed(): bool
311
    {
312
        return $this->allowNegation;
313
    }
314
315 46
    /**
316
     * @return string A message used when the input it incorrect.
317 46
     *
318 46
     * @see $incorrectInputMessage
319
     */
320
    public function getIncorrectInputMessage(): string
321
    {
322
        return $this->incorrectInputMessage;
323
    }
324
325
    /**
326
     * @return string Error message used when validation fails due to the wrong IP address format.
327
     *
328
     * @see $message
329
     */
330
    public function getMessage(): string
331 10
    {
332
        return $this->message;
333 10
    }
334 10
335 5
    /**
336 5
     * @return string Error message used when validation fails due to the disabled IPv4 validation when
337 3
     * {@see $allowIpv4} is set.
338 3
     *
339 3
     * @see $ipv4NotAllowedMessage
340 3
     */
341
    public function getIpv4NotAllowedMessage(): string
342
    {
343 5
        return $this->ipv4NotAllowedMessage;
344
    }
345
346
    /**
347 10
     * @return string Error message used when validation fails due to the disabled IPv6 validation when
348
     * {@see $allowIpv6} is set.
349
     *
350
     * @see $ipv6NotAllowedMessage
351
     */
352
    public function getIpv6NotAllowedMessage(): string
353 72
    {
354
        return $this->ipv6NotAllowedMessage;
355 72
    }
356 31
357
    /**
358
     * @return string Error message used when validation fails due to the wrong CIDR when
359 41
     * {@see $allowSubnet} is set.
360 41
     *
361 41
     * @see $wrongCidrMessage
362 35
     */
363
    public function getWrongCidrMessage(): string
364
    {
365
        return $this->wrongCidrMessage;
366 6
    }
367
368
    /**
369 7
     * @return string Error message used when validation fails due to {@see $allowSubnet} is used, but
370
     * the CIDR prefix is not set.
371
     *
372 7
     * @see $getNoSubnetMessage
373 7
     */
374 7
    public function getNoSubnetMessage(): string
375 7
    {
376 7
        return $this->noSubnetMessage;
377 7
    }
378
379 7
    /**
380
     * @return string Error message used when validation fails due to {@see $allowSubnet} is false, but
381
     * CIDR prefix is present.
382
     *
383 7
     * @see $hasSubnetMessage
384
     */
385
    public function getHasSubnetMessage(): string
386
    {
387 7
        return $this->hasSubnetMessage;
388
    }
389
390
    /**
391 7
     * @return string Error message used when validation fails due to IP address is not allowed by
392
     * {@see $ranges} check.
393
     *
394
     * @see $notInRangeMessage
395 7
     */
396
    public function getNotInRangeMessage(): string
397
    {
398
        return $this->notInRangeMessage;
399 7
    }
400
401
    /**
402
     * @return string[] The IPv4 or IPv6 ranges that are allowed or forbidden.
403 7
     *
404
     * @see $ranges
405
     */
406
    public function getRanges(): array
407 7
    {
408
        return $this->ranges;
409
    }
410 7
411 7
    /**
412 7
     * Parses IP address/range for the negation with {@see NEGATION_CHARACTER}.
413
     *
414
     * @return array{0: bool, 1: string} The result array consists of 2 elements:
415
     * - `boolean`: whether the string is negated
416 134
     * - `string`: the string without negation (when the negation were present)
417
     */
418 134
    private function parseNegatedRange(string $string): array
419
    {
420
        $isNegated = str_starts_with($string, self::NEGATION_CHARACTER);
421
        return [$isNegated, $isNegated ? substr($string, strlen(self::NEGATION_CHARACTER)) : $string];
422
    }
423
424
    /**
425
     * Prepares array to fill in {@see $ranges}:
426
     *
427
     *  - Recursively substitutes aliases, described in {@see $networks} with their values.
428
     *  - Removes duplicates.
429
     *
430
     * @param string[] $ranges
431
     *
432
     * @return string[]
433
     */
434
    private function prepareRanges(array $ranges): array
435
    {
436
        $result = [];
437
        foreach ($ranges as $string) {
438
            [$isRangeNegated, $range] = $this->parseNegatedRange($string);
439
            if (isset($this->networks[$range])) {
440
                $replacements = $this->prepareRanges($this->networks[$range]);
441
                foreach ($replacements as &$replacement) {
442
                    [$isReplacementNegated, $replacement] = $this->parseNegatedRange($replacement);
443
                    $result[] = ($isRangeNegated && !$isReplacementNegated ? self::NEGATION_CHARACTER : '') . $replacement;
444
                }
445
            } else {
446
                $result[] = $string;
447
            }
448
        }
449
450
        return array_unique($result);
451
    }
452
453
    /**
454
     * The method checks whether the IP address with specified CIDR is allowed according to the {@see $ranges} list.
455
     */
456
    public function isAllowed(string $ip): bool
457
    {
458
        if (empty($this->ranges)) {
459
            return true;
460
        }
461
462
        foreach ($this->ranges as $string) {
463
            [$isNegated, $range] = $this->parseNegatedRange($string);
464
            if (IpHelper::inRange($ip, $range)) {
465
                return !$isNegated;
466
            }
467
        }
468
469
        return false;
470
    }
471
472
    public function getOptions(): array
473
    {
474
        return [
475
            'networks' => $this->networks,
476
            'allowIpv4' => $this->allowIpv4,
477
            'allowIpv6' => $this->allowIpv6,
478
            'allowSubnet' => $this->allowSubnet,
479
            'requireSubnet' => $this->requireSubnet,
480
            'allowNegation' => $this->allowNegation,
481
            'incorrectInputMessage' => [
482
                'template' => $this->incorrectInputMessage,
483
                'parameters' => [],
484
            ],
485
            'message' => [
486
                'template' => $this->message,
487
                'parameters' => [],
488
            ],
489
            'ipv4NotAllowedMessage' => [
490
                'template' => $this->ipv4NotAllowedMessage,
491
                'parameters' => [],
492
            ],
493
            'ipv6NotAllowedMessage' => [
494
                'template' => $this->ipv6NotAllowedMessage,
495
                'parameters' => [],
496
            ],
497
            'wrongCidrMessage' => [
498
                'template' => $this->wrongCidrMessage,
499
                'parameters' => [],
500
            ],
501
            'noSubnetMessage' => [
502
                'template' => $this->noSubnetMessage,
503
                'parameters' => [],
504
            ],
505
            'hasSubnetMessage' => [
506
                'template' => $this->hasSubnetMessage,
507
                'parameters' => [],
508
            ],
509
            'notInRangeMessage' => [
510
                'template' => $this->notInRangeMessage,
511
                'parameters' => [],
512
            ],
513
            'ranges' => $this->ranges,
514
            'skipOnEmpty' => $this->getSkipOnEmptyOption(),
515
            'skipOnError' => $this->skipOnError,
516
        ];
517
    }
518
519
    public function getHandler(): string
520
    {
521
        return IpHandler::class;
522
    }
523
}
524