Passed
Push — master ( 9562b0...4734f7 )
by Sergei
04:27 queued 02:02
created

Ip::__construct()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 168
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

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

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