Passed
Push — master ( 89f940...819311 )
by Alexander
02:27
created

Ip::__construct()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 169
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 169
ccs 7
cts 8
cp 0.875
rs 10
cc 4
nc 5
nop 17
crap 4.0312

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