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

Ip::__construct()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 182
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

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

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
        private string $incorrectInputMessage = 'The value must have a string type.',
109
        /**
110
         * @var string Error message used when validation fails due to the wrong IP address format.
111
         *
112
         * You may use the following placeholders in the message:
113
         *
114
         * - `{attribute}`: the label of the attribute being validated.
115
         * - `{value}`: the value of the attribute being validated.
116
         */
117
        private string $message = 'Must be a valid IP address.',
118
        /**
119
         * @var string Error message used when validation fails due to the disabled IPv4 validation when
120
         * {@see $allowIpv4} is set.
121
         *
122
         * You may use the following placeholders in the message:
123
         *
124
         * - `{attribute}`: the label of the attribute being validated.
125
         * - `{value}`: the value of the attribute being validated.
126
         */
127
        private string $ipv4NotAllowedMessage = 'Must not be an IPv4 address.',
128
        /**
129
         * @var string Error message used when validation fails due to the disabled IPv6 validation when
130
         * {@see $allowIpv6} is set.
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
        private string $ipv6NotAllowedMessage = 'Must not be an IPv6 address.',
138
        /**
139
         * @var string Error message used when validation fails due to the wrong CIDR when
140
         * {@see $allowSubnet} is set.
141
         *
142
         * You may use the following placeholders in the message:
143
         *
144
         * - `{attribute}`: the label of the attribute being validated.
145
         * - `{value}`: the value of the attribute being validated.
146
         */
147
        private string $wrongCidrMessage = 'Contains wrong subnet mask.',
148
        /**
149
         * @var string Error message used when validation fails due to {@see $allowSubnet} is used, but
150
         * the CIDR prefix is not set.
151
         *
152
         * You may use the following placeholders in the message:
153
         *
154
         * - `{attribute}`: the label of the attribute being validated.
155
         * - `{value}`: the value of the attribute being validated.
156
         */
157
        private string $noSubnetMessage = 'Must be an IP address with specified subnet.',
158
        /**
159
         * @var string Error message used when validation fails due to {@see $allowSubnet} is false, but
160
         * CIDR prefix is present.
161
         *
162
         * You may use the following placeholders in the message:
163
         *
164
         * - `{attribute}`: the label of the attribute being validated.
165
         * - `{value}`: the value of the attribute being validated.
166
         */
167
        private string $hasSubnetMessage = 'Must not be a subnet.',
168
        /**
169
         * @var string Error message used when validation fails due to IP address is not allowed by
170
         * {@see $ranges} check.
171
         *
172
         * You may use the following placeholders in the message:
173
         *
174
         * - `{attribute}`: the label of the attribute being validated.
175
         * - `{value}`: the value of the attribute being validated.
176
         */
177
        private string $notInRangeMessage = 'Is not in the allowed range.',
178
        /**
179
         * @var string[] The IPv4 or IPv6 ranges that are allowed or forbidden.
180
         *
181
         * The following preparation tasks are performed:
182
         *
183
         * - Recursively substitute aliases (described in {@see $networks}) with their values.
184
         * - Remove duplicates.
185
         *
186
         * When the array is empty, or the option not set, all IP addresses are allowed.
187
         *
188
         * Otherwise, the rules are checked sequentially until the first match is found. An IP address is forbidden,
189
         * when it has not matched any of the rules.
190
         *
191
         * Example:
192
         *
193
         * ```php
194
         * new Ip(ranges: [
195
         *     '192.168.10.128'
196
         *     '!192.168.10.0/24',
197
         *     'any' // allows any other IP addresses
198
         * ]);
199
         * ```
200
         *
201
         * In this example, access is allowed for all the IPv4 and IPv6 addresses excluding the `192.168.10.0/24`
202
         * subnet. IPv4 address `192.168.10.128` is also allowed, because it is listed before the restriction.
203
         */
204
        private array $ranges = [],
205
        /**
206 12
         * @var bool|callable|null Whether to skip this rule if the value validated is empty.
207 1
         *
208
         * @see SkipOnEmptyInterface
209
         */
210 11
        private mixed $skipOnEmpty = null,
211 2
        /**
212 1
         * @var bool Whether to skip this rule if any of the previous rules gave an error.
213
         */
214
        private bool $skipOnError = false,
215
        /**
216 10
         * @var Closure|null A callable to define a condition for applying the rule.
217
         * @psalm-var WhenType
218 10
         *
219
         * @see WhenInterface
220
         */
221
        private Closure|null $when = null,
222
    ) {
223
        if (!$this->allowIpv4 && !$this->allowIpv6) {
224
            throw new InvalidArgumentException('Both IPv4 and IPv6 checks can not be disabled at the same time.');
225 10
        }
226
227
        foreach ($networks as $key => $_values) {
228 1
            if (array_key_exists($key, $this->defaultNetworks)) {
229
                throw new InvalidArgumentException("Network alias \"{$key}\" already set as default.");
230 1
            }
231
        }
232
233 2
        $this->networks = array_merge($this->defaultNetworks, $this->networks);
234
235 2
        if ($requireSubnet) {
236
            // Might be a bug of XDebug, because this line is covered by tests (see "IpTest").
237
            // @codeCoverageIgnoreStart
238 54
            $this->allowSubnet = true;
239
            // @codeCoverageIgnoreEnd
240 54
        }
241
242
        $this->ranges = $this->prepareRanges($ranges);
243 31
    }
244
245 31
    public function getName(): string
246
    {
247
        return 'ip';
248 38
    }
249
250 38
    /**
251
     * @return array Custom network aliases, that can be used in {@see $ranges}.
252
     *
253 62
     * @see $networks
254
     */
255 62
    public function getNetworks(): array
256
    {
257
        return $this->networks;
258 7
    }
259
260 7
    /**
261
     * @return bool Whether the validating value can be an IPv4 address. Defaults to `true`.
262
     *
263 8
     * @see $allowIpv4
264
     */
265 8
    public function isIpv4Allowed(): bool
266
    {
267
        return $this->allowIpv4;
268 29
    }
269
270 29
    /**
271
     * @return bool Whether the validating value can be an IPv6 address. Defaults to `true`.
272
     *
273 4
     * @see $allowIpv6
274
     */
275 4
    public function isIpv6Allowed(): bool
276
    {
277
        return $this->allowIpv6;
278 4
    }
279
280 4
    /**
281
     * @return bool Whether the address can be an IP with CIDR subnet, like `192.168.10.0/24`.
282
     *
283 5
     * @see $allowSubnet
284
     */
285 5
    public function isSubnetAllowed(): bool
286
    {
287
        return $this->allowSubnet;
288 7
    }
289
290 7
    /**
291
     * @return bool Whether subnet is required.
292
     *
293 5
     * @see $requireSubnet
294
     */
295 5
    public function isSubnetRequired(): bool
296
    {
297
        return $this->requireSubnet;
298 19
    }
299
300 19
    /**
301
     * @return bool Whether an address may have a {@see NEGATION_CHARACTER} character at the beginning.
302
     *
303 5
     * @see $allowNegation
304
     */
305 5
    public function isNegationAllowed(): bool
306
    {
307
        return $this->allowNegation;
308
    }
309
310
    /**
311
     * @return string A message used when the input it incorrect.
312
     *
313
     * @see $incorrectInputMessage
314
     */
315 46
    public function getIncorrectInputMessage(): string
316
    {
317 46
        return $this->incorrectInputMessage;
318 46
    }
319
320
    /**
321
     * @return string Error message used when validation fails due to the wrong IP address format.
322
     *
323
     * @see $message
324
     */
325
    public function getMessage(): string
326
    {
327
        return $this->message;
328
    }
329
330
    /**
331 10
     * @return string Error message used when validation fails due to the disabled IPv4 validation when
332
     * {@see $allowIpv4} is set.
333 10
     *
334 10
     * @see $ipv4NotAllowedMessage
335 5
     */
336 5
    public function getIpv4NotAllowedMessage(): string
337 3
    {
338 3
        return $this->ipv4NotAllowedMessage;
339 3
    }
340 3
341
    /**
342
     * @return string Error message used when validation fails due to the disabled IPv6 validation when
343 5
     * {@see $allowIpv6} is set.
344
     *
345
     * @see $ipv6NotAllowedMessage
346
     */
347 10
    public function getIpv6NotAllowedMessage(): string
348
    {
349
        return $this->ipv6NotAllowedMessage;
350
    }
351
352
    /**
353 72
     * @return string Error message used when validation fails due to the wrong CIDR when
354
     * {@see $allowSubnet} is set.
355 72
     *
356 31
     * @see $wrongCidrMessage
357
     */
358
    public function getWrongCidrMessage(): string
359 41
    {
360 41
        return $this->wrongCidrMessage;
361 41
    }
362 35
363
    /**
364
     * @return string Error message used when validation fails due to {@see $allowSubnet} is used, but
365
     * the CIDR prefix is not set.
366 6
     *
367
     * @see $getNoSubnetMessage
368
     */
369 7
    public function getNoSubnetMessage(): string
370
    {
371
        return $this->noSubnetMessage;
372 7
    }
373 7
374 7
    /**
375 7
     * @return string Error message used when validation fails due to {@see $allowSubnet} is false, but
376 7
     * CIDR prefix is present.
377 7
     *
378
     * @see $hasSubnetMessage
379 7
     */
380
    public function getHasSubnetMessage(): string
381
    {
382
        return $this->hasSubnetMessage;
383 7
    }
384
385
    /**
386
     * @return string Error message used when validation fails due to IP address is not allowed by
387 7
     * {@see $ranges} check.
388
     *
389
     * @see $notInRangeMessage
390
     */
391 7
    public function getNotInRangeMessage(): string
392
    {
393
        return $this->notInRangeMessage;
394
    }
395 7
396
    /**
397
     * @return string[] The IPv4 or IPv6 ranges that are allowed or forbidden.
398
     *
399 7
     * @see $ranges
400
     */
401
    public function getRanges(): array
402
    {
403 7
        return $this->ranges;
404
    }
405
406
    /**
407 7
     * Parses IP address/range for the negation with {@see NEGATION_CHARACTER}.
408
     *
409
     * @return array{0: bool, 1: string} The result array consists of 2 elements:
410 7
     * - `boolean`: whether the string is negated
411 7
     * - `string`: the string without negation (when the negation were present)
412 7
     */
413
    private function parseNegatedRange(string $string): array
414
    {
415
        $isNegated = str_starts_with($string, self::NEGATION_CHARACTER);
416 134
        return [$isNegated, $isNegated ? substr($string, strlen(self::NEGATION_CHARACTER)) : $string];
417
    }
418 134
419
    /**
420
     * Prepares array to fill in {@see $ranges}:
421
     *
422
     *  - Recursively substitutes aliases, described in {@see $networks} with their values.
423
     *  - Removes duplicates.
424
     *
425
     * @param string[] $ranges
426
     *
427
     * @return string[]
428
     */
429
    private function prepareRanges(array $ranges): array
430
    {
431
        $result = [];
432
        foreach ($ranges as $string) {
433
            [$isRangeNegated, $range] = $this->parseNegatedRange($string);
434
            if (isset($this->networks[$range])) {
435
                $replacements = $this->prepareRanges($this->networks[$range]);
436
                foreach ($replacements as &$replacement) {
437
                    [$isReplacementNegated, $replacement] = $this->parseNegatedRange($replacement);
438
                    $result[] = ($isRangeNegated && !$isReplacementNegated ? self::NEGATION_CHARACTER : '') . $replacement;
439
                }
440
            } else {
441
                $result[] = $string;
442
            }
443
        }
444
445
        return array_unique($result);
446
    }
447
448
    /**
449
     * The method checks whether the IP address with specified CIDR is allowed according to the {@see $ranges} list.
450
     */
451
    public function isAllowed(string $ip): bool
452
    {
453
        if (empty($this->ranges)) {
454
            return true;
455
        }
456
457
        foreach ($this->ranges as $string) {
458
            [$isNegated, $range] = $this->parseNegatedRange($string);
459
            if (IpHelper::inRange($ip, $range)) {
460
                return !$isNegated;
461
            }
462
        }
463
464
        return false;
465
    }
466
467
    public function getOptions(): array
468
    {
469
        return [
470
            'networks' => $this->networks,
471
            'allowIpv4' => $this->allowIpv4,
472
            'allowIpv6' => $this->allowIpv6,
473
            'allowSubnet' => $this->allowSubnet,
474
            'requireSubnet' => $this->requireSubnet,
475
            'allowNegation' => $this->allowNegation,
476
            'incorrectInputMessage' => [
477
                'template' => $this->incorrectInputMessage,
478
                'parameters' => [],
479
            ],
480
            'message' => [
481
                'template' => $this->message,
482
                'parameters' => [],
483
            ],
484
            'ipv4NotAllowedMessage' => [
485
                'template' => $this->ipv4NotAllowedMessage,
486
                'parameters' => [],
487
            ],
488
            'ipv6NotAllowedMessage' => [
489
                'template' => $this->ipv6NotAllowedMessage,
490
                'parameters' => [],
491
            ],
492
            'wrongCidrMessage' => [
493
                'template' => $this->wrongCidrMessage,
494
                'parameters' => [],
495
            ],
496
            'noSubnetMessage' => [
497
                'template' => $this->noSubnetMessage,
498
                'parameters' => [],
499
            ],
500
            'hasSubnetMessage' => [
501
                'template' => $this->hasSubnetMessage,
502
                'parameters' => [],
503
            ],
504
            'notInRangeMessage' => [
505
                'template' => $this->notInRangeMessage,
506
                'parameters' => [],
507
            ],
508
            'ranges' => $this->ranges,
509
            'skipOnEmpty' => $this->getSkipOnEmptyOption(),
510
            'skipOnError' => $this->skipOnError,
511
        ];
512
    }
513
514
    public function getHandler(): string
515
    {
516
        return IpHandler::class;
517
    }
518
}
519