Ip::__construct()   A
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 40
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6

Importance

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

How to fix   Many Parameters   

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