Passed
Pull Request — master (#379)
by
unknown
03:40
created

IpHandler::validateCidr()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 19
ccs 8
cts 8
cp 1
rs 10
cc 4
nc 5
nop 5
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use InvalidArgumentException;
8
use Yiisoft\NetworkUtilities\IpHelper;
9
use Yiisoft\Validator\Exception\UnexpectedRuleException;
10
use Yiisoft\Validator\Result;
11
use Yiisoft\Validator\RuleHandlerInterface;
12
use Yiisoft\Validator\ValidationContext;
13
14
use function is_string;
15
16
/**
17
 * Checks if the value is a valid IPv4/IPv6 address or subnet.
18
 *
19
 * It also may change the value if normalization of IPv6 expansion is enabled.
20
 */
21
final class IpHandler implements RuleHandlerInterface
22
{
23
    /**
24
     * Negation char.
25
     *
26
     * Used to negate {@see $ranges} or {@see $network} or to negate validating value when {@see $allowNegation}
27
     * is used.
28
     */
29
    private const NEGATION_CHAR = '!';
30
31 135
    public function validate(mixed $value, object $rule, ValidationContext $context): Result
32
    {
33 135
        if (!$rule instanceof Ip) {
34 1
            throw new UnexpectedRuleException(Ip::class, $rule);
35
        }
36
37 134
        if (!is_string($value)) {
38 8
            return (new Result())->addError($rule->getIncorrectInputMessage(), [
39 8
                'attribute' => $context->getAttribute(),
40 8
                'type' => get_debug_type($value),
41
            ]);
42
        }
43
44 126
        if (preg_match($this->getIpParsePattern(), $value, $matches) === 0) {
45 26
            return self::getGenericErrorResult($rule->getMessage(), $context, $value);
46
        }
47
48 100
        $negation = !empty($matches['not'] ?? null);
49 100
        $ip = $matches['ip'];
50 100
        $cidr = $matches['cidr'] ?? null;
51 100
        $ipCidr = $matches['ipCidr'];
52
        /**
53
         * Exception handling and validation in IpHelper are not needed because of the check above (regular expression
54
         * in "getIpParsePattern()").
55
         *
56
         * @infection-ignore-all
57
         */
58 100
        $ipVersion = IpHelper::getIpVersion($ip, validate: false);
59
60 100
        $result = self::validateValueParts($rule, $cidr, $negation, $value, $context);
61 100
        if ($result !== null) {
62 15
            return $result;
63
        }
64
65 85
        $result = self::validateVersion($rule, $ipVersion, $value, $context);
66 85
        if ($result !== null) {
67 8
            return $result;
68
        }
69
70 77
        $result = self::validateCidr($rule, $cidr, $ipCidr, $value, $context);
71 77
        if ($result !== null) {
72 24
            return $result;
73
        }
74
75 53
        return new Result();
76
    }
77
78
    /**
79
     * Used to get the Regexp pattern for initial IP address parsing.
80
     */
81 126
    private function getIpParsePattern(): string
82
    {
83 126
        return '/^(?<not>' . preg_quote(
84
            self::NEGATION_CHAR,
85
            '/'
86
        ) . ')?(?<ipCidr>(?<ip>(?:' . IpHelper::IPV4_PATTERN . ')|(?:' . IpHelper::IPV6_PATTERN . '))(?:\/(?<cidr>-?\d+))?)$/';
87
    }
88
89 100
    private static function validateValueParts(
90
        Ip $rule,
91
        ?string $cidr,
92
        bool $negation,
93
        string $value,
94
        ValidationContext $context
95
    ): Result|null {
96 100
        if ($cidr === null && $rule->isRequireSubnet()) {
97 7
            return self::getGenericErrorResult($rule->getNoSubnetMessage(), $context, $value);
98
        }
99
100 93
        if ($cidr !== null && !$rule->isAllowSubnet()) {
101 5
            return self::getGenericErrorResult($rule->getHasSubnetMessage(), $context, $value);
102
        }
103
104 88
        if ($negation && !$rule->isAllowNegation()) {
105 3
            return self::getGenericErrorResult($rule->getMessage(), $context, $value);
106
        }
107
108 85
        return null;
109
    }
110
111 85
    private static function validateVersion(
112
        Ip $rule,
113
        int $ipVersion,
114
        string $value,
115
        ValidationContext $context
116
    ): Result|null {
117 85
        if ($ipVersion === IpHelper::IPV6 && !$rule->isAllowIpv6()) {
118 4
            return self::getGenericErrorResult($rule->getIpv6NotAllowedMessage(), $context, $value);
119
        }
120
121 81
        if ($ipVersion === IpHelper::IPV4 && !$rule->isAllowIpv4()) {
122 4
            return self::getGenericErrorResult($rule->getIpv4NotAllowedMessage(), $context, $value);
123
        }
124
125 77
        return null;
126
    }
127
128 77
    private static function validateCidr(Ip $rule,
129
        ?string $cidr,
130
        string $ipCidr,
131
        string $value,
132
        ValidationContext $context
133
    ): Result|null {
134 77
        if ($cidr !== null) {
135
            try {
136 31
                IpHelper::getCidrBits($ipCidr);
137 5
            } catch (InvalidArgumentException) {
138 5
                return self::getGenericErrorResult($rule->getWrongCidrMessage(), $context, $value);
139
            }
140
        }
141
142 72
        if (!$rule->isAllowed($ipCidr)) {
143 19
            return self::getGenericErrorResult($rule->getNotInRangeMessage(), $context, $value);
144
        }
145
146 53
        return null;
147
    }
148
149 73
    private static function getGenericErrorResult(string $message, ValidationContext $context, string $value): Result
150
    {
151 73
        return (new Result())->addError($message, [
152 73
            'attribute' => $context->getAttribute(),
153
            'value' => $value,
154
        ]);
155
    }
156
}
157