Passed
Push — master ( 97eb53...6d8f00 )
by Alexander
08:20 queued 05:46
created

Ip::__construct()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 169
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

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

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