DomainLiteralValidator   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 34
eloc 56
c 1
b 0
f 0
dl 0
loc 202
rs 9.68

8 Methods

Rating   Name   Duplication   Size   Complexity  
A validateIPv6Group() 0 9 2
A validateIPv4Octet() 0 14 4
A hasInvalidCharacters() 0 3 1
A validateIPv6() 0 31 6
A validateIPv4() 0 18 4
A hasValidBrackets() 0 3 2
B validateCompressedIPv6() 0 38 10
A validate() 0 22 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EmailValidator\Validator\Domain;
6
7
/**
8
 * Validates domain literals (IPv4 and IPv6 addresses) according to RFC 5322
9
 */
10
class DomainLiteralValidator
11
{
12
    /**
13
     * Validates a domain literal
14
     *
15
     * @param string $domain The domain literal to validate
16
     * @return bool True if the domain literal is valid
17
     */
18
    public function validate(string $domain): bool
19
    {
20
        // Must be enclosed in square brackets
21
        if (!$this->hasValidBrackets($domain)) {
22
            return false;
23
        }
24
25
        // Remove brackets for content validation
26
        $content = substr($domain, 1, -1);
27
28
        // Empty domain literals are invalid
29
        if ($content === '') {
30
            return false;
31
        }
32
33
        // Check for whitespace or control characters
34
        if ($this->hasInvalidCharacters($content)) {
35
            return false;
36
        }
37
38
        // Try IPv4 first, then IPv6
39
        return $this->validateIPv4($content) || $this->validateIPv6($content);
40
    }
41
42
    /**
43
     * Checks if a domain literal has valid opening and closing brackets
44
     *
45
     * @param string $domain The domain literal to validate
46
     * @return bool True if the brackets are valid
47
     */
48
    private function hasValidBrackets(string $domain): bool
49
    {
50
        return substr($domain, 0, 1) === '[' && substr($domain, -1) === ']';
51
    }
52
53
    /**
54
     * Checks for whitespace or control characters
55
     *
56
     * @param string $content The content to check
57
     * @return bool True if invalid characters are found
58
     */
59
    private function hasInvalidCharacters(string $content): bool
60
    {
61
        return (bool)preg_match('/[\s\x00-\x1F\x7F]/', $content);
62
    }
63
64
    /**
65
     * Validates an IPv4 address
66
     *
67
     * @param string $address The IPv4 address to validate
68
     * @return bool True if the IPv4 address is valid
69
     */
70
    private function validateIPv4(string $address): bool
71
    {
72
        // Split into octets
73
        $octets = explode('.', $address);
74
75
        // Must have exactly 4 octets
76
        if (count($octets) !== 4) {
77
            return false;
78
        }
79
80
        // Validate each octet
81
        foreach ($octets as $octet) {
82
            if (!$this->validateIPv4Octet($octet)) {
83
                return false;
84
            }
85
        }
86
87
        return true;
88
    }
89
90
    /**
91
     * Validates a single IPv4 octet
92
     *
93
     * @param string $octet The octet to validate
94
     * @return bool True if the octet is valid
95
     */
96
    private function validateIPv4Octet(string $octet): bool
97
    {
98
        // Empty octets are invalid
99
        if ($octet === '') {
100
            return false;
101
        }
102
103
        // Must be numeric and in valid range
104
        if (!is_numeric($octet)) {
105
            return false;
106
        }
107
108
        $value = (int)$octet;
109
        return $value >= 0 && $value <= 255;
110
    }
111
112
    /**
113
     * Validates an IPv6 address
114
     *
115
     * @param string $address The IPv6 address to validate
116
     * @return bool True if the IPv6 address is valid
117
     */
118
    private function validateIPv6(string $address): bool
119
    {
120
        // Must start with 'IPv6:' (case-sensitive)
121
        if (substr($address, 0, 5) !== 'IPv6:') {
122
            return false;
123
        }
124
125
        // Remove prefix
126
        $address = substr($address, 5);
127
128
        // Handle compressed notation
129
        if (strpos($address, '::') !== false) {
130
            return $this->validateCompressedIPv6($address);
131
        }
132
133
        // Split into groups
134
        $groups = explode(':', $address);
135
136
        // Must have exactly 8 groups for uncompressed notation
137
        if (count($groups) !== 8) {
138
            return false;
139
        }
140
141
        // Validate each group
142
        foreach ($groups as $group) {
143
            if (!$this->validateIPv6Group($group)) {
144
                return false;
145
            }
146
        }
147
148
        return true;
149
    }
150
151
    /**
152
     * Validates a compressed IPv6 address
153
     *
154
     * @param string $address The IPv6 address to validate (without prefix)
155
     * @return bool True if the IPv6 address is valid
156
     */
157
    private function validateCompressedIPv6(string $address): bool
158
    {
159
        // Only one :: allowed
160
        if (substr_count($address, '::') > 1) {
161
            return false;
162
        }
163
164
        // Split on ::
165
        $parts = explode('::', $address);
166
        if (count($parts) !== 2) {
167
            return false;
168
        }
169
170
        // Split each part into groups
171
        $leftGroups = $parts[0] ? explode(':', $parts[0]) : [];
172
        $rightGroups = $parts[1] ? explode(':', $parts[1]) : [];
173
174
        // Calculate total groups
175
        $totalGroups = count($leftGroups) + count($rightGroups);
176
        if ($totalGroups >= 8) {
177
            return false;
178
        }
179
180
        // Validate left groups
181
        foreach ($leftGroups as $group) {
182
            if (!$this->validateIPv6Group($group)) {
183
                return false;
184
            }
185
        }
186
187
        // Validate right groups
188
        foreach ($rightGroups as $group) {
189
            if (!$this->validateIPv6Group($group)) {
190
                return false;
191
            }
192
        }
193
194
        return true;
195
    }
196
197
    /**
198
     * Validates a single IPv6 group
199
     *
200
     * @param string $group The group to validate
201
     * @return bool True if the group is valid
202
     */
203
    private function validateIPv6Group(string $group): bool
204
    {
205
        // Empty groups are invalid
206
        if ($group === '') {
207
            return false;
208
        }
209
210
        // Must be 1-4 hexadecimal digits (case-insensitive)
211
        return (bool)preg_match('/^[0-9A-Fa-f]{1,4}$/', $group);
212
    }
213
}