Passed
Pull Request — master (#379)
by
unknown
13:21
created

IpHandler::validateValueParts()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 20
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 20
ccs 8
cts 8
cp 1
rs 8.8333
cc 7
nc 4
nop 5
crap 7
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(self::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 static 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(
129
        Ip $rule,
130
        ?string $cidr,
131
        string $ipCidr,
132
        string $value,
133
        ValidationContext $context
134
    ): Result|null {
135 77
        if ($cidr !== null) {
136
            try {
137 31
                IpHelper::getCidrBits($ipCidr);
138 5
            } catch (InvalidArgumentException) {
139 5
                return self::getGenericErrorResult($rule->getWrongCidrMessage(), $context, $value);
140
            }
141
        }
142
143 72
        if (!$rule->isAllowed($ipCidr)) {
144 19
            return self::getGenericErrorResult($rule->getNotInRangeMessage(), $context, $value);
145
        }
146
147 53
        return null;
148
    }
149
150 73
    private static function getGenericErrorResult(string $message, ValidationContext $context, string $value): Result
151
    {
152 73
        return (new Result())->addError($message, [
153 73
            'attribute' => $context->getAttribute(),
154
            'value' => $value,
155
        ]);
156
    }
157
}
158