Passed
Pull Request — master (#300)
by Alexander
06:13 queued 03:06
created

Ip::__construct()   A

Complexity

Conditions 4
Paths 5

Size

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