Passed
Pull Request — master (#222)
by Rustam
02:34
created

IpHandler::validateValueParts()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7

Importance

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