Completed
Push — master ( 4d504b...3e6761 )
by Alexander
13:18
created

Ip::validateValue()   F

Complexity

Conditions 20
Paths 196

Size

Total Lines 58
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

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