Passed
Pull Request — master (#41)
by Alexander
01:57 queued 36s
created

Ip::validateValue()   F

Complexity

Conditions 20
Paths 196

Size

Total Lines 54
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 20.0813

Importance

Changes 0
Metric Value
cc 20
eloc 35
nc 196
nop 2
dl 0
loc 54
ccs 32
cts 34
cp 0.9412
crap 20.0813
rs 3.3666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

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