Passed
Push — master ( cd2db1...fdabae )
by Alexander
02:29
created

Ip::__construct()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 175
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 175
ccs 8
cts 9
cp 0.8889
rs 10
cc 4
nc 5
nop 18
crap 4.0218

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 RuntimeException;
10
use Yiisoft\NetworkUtilities\IpHelper;
11
use Yiisoft\Validator\BeforeValidationInterface;
12
use Yiisoft\Validator\Rule\Trait\BeforeValidationTrait;
13
use Yiisoft\Validator\Rule\Trait\RuleNameTrait;
14
use Yiisoft\Validator\SerializableRuleInterface;
15
use Yiisoft\Validator\ValidationContext;
16
17
use function array_key_exists;
18
use function strlen;
19
20
/**
21
 * Checks if the value is a valid IPv4/IPv6 address or subnet.
22
 *
23
 * It also may change the value if normalization of IPv6 expansion is enabled.
24
 */
25
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
26
final class Ip implements SerializableRuleInterface, BeforeValidationInterface
27
{
28
    use BeforeValidationTrait;
29
    use RuleNameTrait;
30
31
    /**
32
     * Negation char.
33
     *
34
     * Used to negate {@see $ranges} or {@see $network} or to negate validating value when {@see $allowNegation}
35
     * is used.
36
     */
37
    private const NEGATION_CHAR = '!';
38
    /**
39
     * @see $networks
40
     */
41
    private array $defaultNetworks = [
42
        '*' => ['any'],
43
        'any' => ['0.0.0.0/0', '::/0'],
44
        'private' => ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fd00::/8'],
45
        'multicast' => ['224.0.0.0/4', 'ff00::/8'],
46
        'linklocal' => ['169.254.0.0/16', 'fe80::/10'],
47
        'localhost' => ['127.0.0.0/8', '::1'],
48
        'documentation' => ['192.0.2.0/24', '198.51.100.0/24', '203.0.113.0/24', '2001:db8::/32'],
49
        'system' => ['multicast', 'linklocal', 'localhost', 'documentation'],
50
    ];
51
52 9
    public function __construct(
53
        /**
54
         * @var array Custom network aliases, that can be used in {@see $ranges}.
55
         *
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 {@see NEGATION_CHAR} (independent of {@see $allowNegation} option).
59
         *
60
         * The following aliases are defined by default in {@see $defaultNetworks} and will be merged with custom ones:
61
         *
62
         *  - `*`: `any`
63
         *  - `any`: `0.0.0.0/0, ::/0`
64
         *  - `private`: `10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fd00::/8`
65
         *  - `multicast`: `224.0.0.0/4, ff00::/8`
66
         *  - `linklocal`: `169.254.0.0/16, fe80::/10`
67
         *  - `localhost`: `127.0.0.0/8', ::1`
68
         *  - `documentation`: `192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32`
69
         *  - `system`: `multicast, linklocal, localhost, documentation`
70
         *
71
         * @see $defaultNetworks
72
         */
73
        private array $networks = [],
74
        /**
75
         * @var bool whether the validating value can be an IPv4 address. Defaults to `true`.
76
         */
77
        private bool $allowIpv4 = true,
78
        /**
79
         * @var bool whether the validating value can be an IPv6 address. Defaults to `true`.
80
         */
81
        private bool $allowIpv6 = true,
82
        /**
83
         * @var bool 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
         */
89
        private bool $allowSubnet = false,
90
        private bool $requireSubnet = false,
91
        /**
92
         * @var bool whether address may have a {@see NEGATION_CHAR} character at the beginning.
93
         * Defaults to `false`.
94
         */
95
        private bool $allowNegation = false,
96
        /**
97
         * @var string user-defined error message is used when validation fails due to the wrong IP address format.
98
         *
99
         * You may use the following placeholders in the message:
100
         *
101
         * - `{attribute}`: the label of the attribute being validated
102
         * - `{value}`: the value of the attribute being validated
103
         */
104
        private string $message = 'Must be a valid IP address.',
105
        /**
106
         * @var string user-defined error message is used when validation fails due to the disabled IPv4 validation.
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
         * @see $allowIpv4
114
         */
115
        private string $ipv4NotAllowedMessage = 'Must not be an IPv4 address.',
116
        /**
117
         * @var string user-defined error message is used when validation fails due to the disabled IPv6 validation.
118
         *
119
         * You may use the following placeholders in the message:
120
         *
121
         * - `{attribute}`: the label of the attribute being validated
122
         * - `{value}`: the value of the attribute being validated
123
         *
124
         * @see $allowIpv6
125
         */
126
        private string $ipv6NotAllowedMessage = 'Must not be an IPv6 address.',
127
        /**
128
         * @var string user-defined error message is used when validation fails due to the wrong CIDR.
129
         *
130
         * You may use the following placeholders in the message:
131
         *
132
         * - `{attribute}`: the label of the attribute being validated
133
         * - `{value}`: the value of the attribute being validated
134
         *
135
         * @see $allowSubnet
136
         */
137
        private string $wrongCidrMessage = 'Contains wrong subnet mask.',
138
        /**
139
         * @var string user-defined error message is used when validation fails due to subnet {@see $allowSubnet} is
140
         * used, but the CIDR prefix is not 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
         * @see $allowSubnet
148
         */
149
        private string $noSubnetMessage = 'Must be an IP address with specified subnet.',
150
        /**
151
         * @var string user-defined error message is used when validation fails
152
         * due to {@see $allowSubnet} is false, but CIDR prefix is present.
153
         *
154
         * You may use the following placeholders in the message:
155
         *
156
         * - `{attribute}`: the label of the attribute being validated
157
         * - `{value}`: the value of the attribute being validated
158
         *
159
         * @see $allowSubnet
160
         */
161
        private string $hasSubnetMessage = 'Must not be a subnet.',
162
        /**
163
         * @var string user-defined error message is used when validation fails due to IP address
164
         * is not allowed by {@see $ranges} check.
165
         *
166
         * You may use the following placeholders in the message:
167
         *
168
         * - `{attribute}`: the label of the attribute being validated
169
         * - `{value}`: the value of the attribute being validated
170
         *
171
         * @see $ranges
172
         */
173
        private string $notInRangeMessage = 'Is not in the allowed range.',
174
        /**
175
         * @var string[] The IPv4 or IPv6 ranges that are allowed or forbidden.
176
         *
177
         * The following preparation tasks are performed:
178
         *
179
         * - Recursively substitutes aliases (described in {@see $networks}) with their values.
180
         * - Removes duplicates.
181
         *
182
         * When the array is empty, or the option not set, all IP addresses are allowed.
183
         *
184
         * Otherwise, the rules are checked sequentially until the first match is found.
185
         * An IP address is forbidden, when it has not matched any of the rules.
186
         *
187
         * Example:
188
         *
189
         * ```php
190
         * (new Ip(ranges: [
191
         *     '192.168.10.128'
192
         *     '!192.168.10.0/24',
193
         *     'any' // allows any other IP addresses
194
         * ]);
195
         * ```
196
         *
197
         * In this example, access is allowed for all the IPv4 and IPv6 addresses excluding the `192.168.10.0/24`
198
         * subnet. IPv4 address `192.168.10.128` is also allowed, because it is listed before the restriction.
199
         */
200
        private array $ranges = [],
201
        private bool $skipOnEmpty = false,
202
        /**
203
         * @var callable
204
         */
205
        private $skipOnEmptyCallback = null,
206
        private bool $skipOnError = false,
207
        /**
208
         * @var Closure(mixed, ValidationContext):bool|null
209
         */
210
        private ?Closure $when = null,
211
    ) {
212 9
        $this->initSkipOnEmptyProperties($skipOnEmpty, $skipOnEmptyCallback);
213
214 9
        foreach ($networks as $key => $_values) {
215 1
            if (array_key_exists($key, $this->defaultNetworks)) {
216 1
                throw new RuntimeException("Network alias \"{$key}\" already set as default.");
217
            }
218
        }
219
220 8
        $this->networks = array_merge($this->defaultNetworks, $this->networks);
221
222 8
        if ($requireSubnet) {
223
            $this->allowSubnet = true;
224
        }
225
226 8
        $this->ranges = $this->prepareRanges($ranges);
227
    }
228
229
    /**
230
     * @return array
231
     */
232
    public function getNetworks(): array
233
    {
234
        return $this->networks;
235
    }
236
237
    /**
238
     * @return bool
239
     */
240 117
    public function isAllowIpv4(): bool
241
    {
242 117
        return $this->allowIpv4;
243
    }
244
245
    /**
246
     * @return bool
247
     */
248 35
    public function isAllowIpv6(): bool
249
    {
250 35
        return $this->allowIpv6;
251
    }
252
253
    /**
254
     * @return bool
255
     */
256 34
    public function isAllowSubnet(): bool
257
    {
258 34
        return $this->allowSubnet;
259
    }
260
261
    /**
262
     * @return bool
263
     */
264 52
    public function isRequireSubnet(): bool
265
    {
266 52
        return $this->requireSubnet;
267
    }
268
269
    /**
270
     * @return bool
271
     */
272 8
    public function isAllowNegation(): bool
273
    {
274 8
        return $this->allowNegation;
275
    }
276
277
    /**
278
     * @return string
279
     */
280 116
    public function getMessage(): string
281
    {
282 116
        return $this->message;
283
    }
284
285
    /**
286
     * @return string
287
     */
288 2
    public function getIpv4NotAllowedMessage(): string
289
    {
290 2
        return $this->ipv4NotAllowedMessage;
291
    }
292
293
    /**
294
     * @return string
295
     */
296 1
    public function getIpv6NotAllowedMessage(): string
297
    {
298 1
        return $this->ipv6NotAllowedMessage;
299
    }
300
301
    /**
302
     * @return string
303
     */
304 2
    public function getWrongCidrMessage(): string
305
    {
306 2
        return $this->wrongCidrMessage;
307
    }
308
309
    /**
310
     * @return string
311
     */
312 4
    public function getNoSubnetMessage(): string
313
    {
314 4
        return $this->noSubnetMessage;
315
    }
316
317
    /**
318
     * @return string
319
     */
320 4
    public function getHasSubnetMessage(): string
321
    {
322 4
        return $this->hasSubnetMessage;
323
    }
324
325
    /**
326
     * @return string
327
     */
328 16
    public function getNotInRangeMessage(): string
329
    {
330 16
        return $this->notInRangeMessage;
331
    }
332
333 4
    public function getRanges(): array
334
    {
335 4
        return $this->ranges;
336
    }
337
338
    /**
339
     * Parses IP address/range for the negation with {@see NEGATION_CHAR}.
340
     *
341
     * @param $string
342
     *
343
     * @return array `[0 => bool, 1 => string]`
344
     *  - boolean: whether the string is negated
345
     *  - string: the string without negation (when the negation were present)
346
     */
347 42
    private function parseNegatedRange($string): array
348
    {
349 42
        $isNegated = str_starts_with($string, self::NEGATION_CHAR);
350 42
        return [$isNegated, $isNegated ? substr($string, strlen(self::NEGATION_CHAR)) : $string];
351
    }
352
353
    /**
354
     * Prepares array to fill in {@see $ranges}.
355
     *
356
     *  - Recursively substitutes aliases, described in {@see $networks} with their values,
357
     *  - Removes duplicates.
358
     *
359
     * @see $networks
360
     */
361 8
    private function prepareRanges(array $ranges): array
362
    {
363 8
        $result = [];
364 8
        foreach ($ranges as $string) {
365 4
            [$isRangeNegated, $range] = $this->parseNegatedRange($string);
366 4
            if (isset($this->networks[$range])) {
367 3
                $replacements = $this->prepareRanges($this->networks[$range]);
368 3
                foreach ($replacements as &$replacement) {
369 3
                    [$isReplacementNegated, $replacement] = $this->parseNegatedRange($replacement);
370 3
                    $result[] = ($isRangeNegated && !$isReplacementNegated ? self::NEGATION_CHAR : '') . $replacement;
371
                }
372
            } else {
373 4
                $result[] = $string;
374
            }
375
        }
376
377 8
        return array_unique($result);
378
    }
379
380
    /**
381
     * The method checks whether the IP address with specified CIDR is allowed according to the {@see $ranges} list.
382
     */
383 69
    public function isAllowed(string $ip): bool
384
    {
385 69
        if (empty($this->ranges)) {
386 31
            return true;
387
        }
388
389 38
        foreach ($this->ranges as $string) {
390 38
            [$isNegated, $range] = $this->parseNegatedRange($string);
391 38
            if (IpHelper::inRange($ip, $range)) {
392 32
                return !$isNegated;
393
            }
394
        }
395
396 6
        return false;
397
    }
398
399
    /**
400
     * Used to get the Regexp pattern for initial IP address parsing.
401
     */
402 112
    public function getIpParsePattern(): string
403
    {
404 112
        return '/^(?<not>' . preg_quote(
405
            self::NEGATION_CHAR,
406
            '/'
407
        ) . ')?(?<ipCidr>(?<ip>(?:' . IpHelper::IPV4_PATTERN . ')|(?:' . IpHelper::IPV6_PATTERN . '))(?:\/(?<cidr>-?\d+))?)$/';
408
    }
409
410 7
    public function getOptions(): array
411
    {
412
        return [
413 7
            'networks' => $this->networks,
414 7
            'allowIpv4' => $this->allowIpv4,
415 7
            'allowIpv6' => $this->allowIpv6,
416 7
            'allowSubnet' => $this->allowSubnet,
417 7
            'requireSubnet' => $this->requireSubnet,
418 7
            'allowNegation' => $this->allowNegation,
419
            'message' => [
420 7
                'message' => $this->message,
421
            ],
422
            'ipv4NotAllowedMessage' => [
423 7
                'message' => $this->ipv4NotAllowedMessage,
424
            ],
425
            'ipv6NotAllowedMessage' => [
426 7
                'message' => $this->ipv6NotAllowedMessage,
427
            ],
428
            'wrongCidrMessage' => [
429 7
                'message' => $this->wrongCidrMessage,
430
            ],
431
            'noSubnetMessage' => [
432 7
                'message' => $this->noSubnetMessage,
433
            ],
434
            'hasSubnetMessage' => [
435 7
                'message' => $this->hasSubnetMessage,
436
            ],
437
            'notInRangeMessage' => [
438 7
                'message' => $this->notInRangeMessage,
439
            ],
440 7
            'ranges' => $this->ranges,
441 7
            'skipOnEmpty' => $this->skipOnEmpty,
442 7
            'skipOnError' => $this->skipOnError,
443
        ];
444
    }
445
446 1
    public function getHandlerClassName(): string
447
    {
448 1
        return IpHandler::class;
449
    }
450
}
451