Passed
Pull Request — master (#475)
by Alexander
02:38
created

Ip::__construct()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 40
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 6

Importance

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