Passed
Pull Request — master (#369)
by
unknown
03:01
created

Ip::__construct()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 164
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 164
ccs 7
cts 7
cp 1
rs 10
cc 4
nc 5
nop 18
crap 4

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