1 | <?php |
||||
2 | |||||
3 | namespace Egulias\EmailValidator\Parser; |
||||
4 | |||||
5 | use Egulias\EmailValidator\EmailLexer; |
||||
6 | use Egulias\EmailValidator\Result\Result; |
||||
7 | use Egulias\EmailValidator\Result\ValidEmail; |
||||
8 | use Egulias\EmailValidator\Result\InvalidEmail; |
||||
9 | use Egulias\EmailValidator\Warning\CFWSWithFWS; |
||||
10 | use Egulias\EmailValidator\Warning\IPV6BadChar; |
||||
11 | use Egulias\EmailValidator\Result\Reason\CRNoLF; |
||||
12 | use Egulias\EmailValidator\Warning\IPV6ColonEnd; |
||||
13 | use Egulias\EmailValidator\Warning\IPV6MaxGroups; |
||||
14 | use Egulias\EmailValidator\Warning\ObsoleteDTEXT; |
||||
15 | use Egulias\EmailValidator\Warning\AddressLiteral; |
||||
16 | use Egulias\EmailValidator\Warning\IPV6ColonStart; |
||||
17 | use Egulias\EmailValidator\Warning\IPV6Deprecated; |
||||
18 | use Egulias\EmailValidator\Warning\IPV6GroupCount; |
||||
19 | use Egulias\EmailValidator\Warning\IPV6DoubleColon; |
||||
20 | use Egulias\EmailValidator\Result\Reason\ExpectingDTEXT; |
||||
21 | use Egulias\EmailValidator\Result\Reason\UnusualElements; |
||||
22 | use Egulias\EmailValidator\Warning\DomainLiteral as WarningDomainLiteral; |
||||
23 | |||||
24 | class DomainLiteral extends PartParser |
||||
25 | { |
||||
26 | public const IPV4_REGEX = '/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/'; |
||||
27 | |||||
28 | public const OBSOLETE_WARNINGS = [ |
||||
29 | EmailLexer::INVALID, |
||||
30 | EmailLexer::C_DEL, |
||||
31 | EmailLexer::S_LF, |
||||
32 | EmailLexer::S_BACKSLASH |
||||
33 | ]; |
||||
34 | 18 | ||||
35 | public function parse(): Result |
||||
36 | 18 | { |
|||
37 | $this->addTagWarnings(); |
||||
38 | 18 | ||||
39 | 18 | $IPv6TAG = false; |
|||
40 | $addressLiteral = ''; |
||||
41 | |||||
42 | 18 | do { |
|||
43 | if ($this->lexer->current->isA(EmailLexer::C_NUL)) { |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
44 | return new InvalidEmail(new ExpectingDTEXT(), $this->lexer->current->value); |
||||
45 | } |
||||
46 | 18 | ||||
47 | $this->addObsoleteWarnings(); |
||||
48 | 18 | ||||
49 | 1 | if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENBRACKET, EmailLexer::S_OPENBRACKET))) { |
|||
50 | return new InvalidEmail(new ExpectingDTEXT(), $this->lexer->current->value); |
||||
51 | } |
||||
52 | 17 | ||||
53 | 17 | if ($this->lexer->isNextTokenAny( |
|||
54 | 17 | array(EmailLexer::S_HTAB, EmailLexer::S_SP, EmailLexer::CRLF) |
|||
55 | )) { |
||||
56 | $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); |
||||
57 | $this->parseFWS(); |
||||
58 | } |
||||
59 | 17 | ||||
60 | 1 | if ($this->lexer->isNextToken(EmailLexer::S_CR)) { |
|||
61 | return new InvalidEmail(new CRNoLF(), $this->lexer->current->value); |
||||
62 | } |
||||
63 | 16 | ||||
64 | 1 | if ($this->lexer->current->isA(EmailLexer::S_BACKSLASH)) { |
|||
65 | return new InvalidEmail(new UnusualElements($this->lexer->current->value), $this->lexer->current->value); |
||||
66 | 16 | } |
|||
67 | 9 | if ($this->lexer->current->isA(EmailLexer::S_IPV6TAG)) { |
|||
68 | $IPv6TAG = true; |
||||
69 | } |
||||
70 | 16 | ||||
71 | if ($this->lexer->current->isA(EmailLexer::S_CLOSEBRACKET)) { |
||||
72 | break; |
||||
73 | } |
||||
74 | 16 | ||||
75 | $addressLiteral .= $this->lexer->current->value; |
||||
76 | 16 | } while ($this->lexer->moveNext()); |
|||
77 | |||||
78 | |||||
79 | //Encapsulate |
||||
80 | 15 | $addressLiteral = str_replace('[', '', $addressLiteral); |
|||
81 | 15 | $isAddressLiteralIPv4 = $this->checkIPV4Tag($addressLiteral); |
|||
82 | |||||
83 | 15 | if (!$isAddressLiteralIPv4) { |
|||
84 | 3 | return new ValidEmail(); |
|||
85 | } else { |
||||
86 | 12 | $addressLiteral = $this->convertIPv4ToIPv6($addressLiteral); |
|||
87 | } |
||||
88 | |||||
89 | 12 | if (!$IPv6TAG) { |
|||
90 | 3 | $this->warnings[WarningDomainLiteral::CODE] = new WarningDomainLiteral(); |
|||
91 | 3 | return new ValidEmail(); |
|||
92 | } |
||||
93 | |||||
94 | 9 | $this->warnings[AddressLiteral::CODE] = new AddressLiteral(); |
|||
95 | |||||
96 | 9 | $this->checkIPV6Tag($addressLiteral); |
|||
97 | |||||
98 | 9 | return new ValidEmail(); |
|||
99 | } |
||||
100 | |||||
101 | /** |
||||
102 | * @param string $addressLiteral |
||||
103 | * @param int $maxGroups |
||||
104 | */ |
||||
105 | 9 | public function checkIPV6Tag($addressLiteral, $maxGroups = 8): void |
|||
106 | { |
||||
107 | 9 | $prev = $this->lexer->getPrevious(); |
|||
108 | 9 | if ($prev->isA(EmailLexer::S_COLON)) { |
|||
0 ignored issues
–
show
Egulias\EmailValidator\EmailLexer::S_COLON of type integer is incompatible with the type Doctrine\Common\Lexer\T expected by parameter $types of Doctrine\Common\Lexer\Token::isA() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
109 | 1 | $this->warnings[IPV6ColonEnd::CODE] = new IPV6ColonEnd(); |
|||
110 | } |
||||
111 | |||||
112 | 9 | $IPv6 = substr($addressLiteral, 5); |
|||
113 | //Daniel Marschall's new IPv6 testing strategy |
||||
114 | 9 | $matchesIP = explode(':', $IPv6); |
|||
115 | 9 | $groupCount = count($matchesIP); |
|||
116 | 9 | $colons = strpos($IPv6, '::'); |
|||
117 | |||||
118 | 9 | if (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) { |
|||
119 | 1 | $this->warnings[IPV6BadChar::CODE] = new IPV6BadChar(); |
|||
120 | } |
||||
121 | |||||
122 | 9 | if ($colons === false) { |
|||
123 | // We need exactly the right number of groups |
||||
124 | 4 | if ($groupCount !== $maxGroups) { |
|||
125 | 1 | $this->warnings[IPV6GroupCount::CODE] = new IPV6GroupCount(); |
|||
126 | } |
||||
127 | 4 | return; |
|||
128 | } |
||||
129 | |||||
130 | 5 | if ($colons !== strrpos($IPv6, '::')) { |
|||
131 | 1 | $this->warnings[IPV6DoubleColon::CODE] = new IPV6DoubleColon(); |
|||
132 | 1 | return; |
|||
133 | } |
||||
134 | |||||
135 | 4 | if ($colons === 0 || $colons === (strlen($IPv6) - 2)) { |
|||
136 | // RFC 4291 allows :: at the start or end of an address |
||||
137 | //with 7 other groups in addition |
||||
138 | 2 | ++$maxGroups; |
|||
139 | } |
||||
140 | |||||
141 | 4 | if ($groupCount > $maxGroups) { |
|||
142 | 1 | $this->warnings[IPV6MaxGroups::CODE] = new IPV6MaxGroups(); |
|||
143 | 3 | } elseif ($groupCount === $maxGroups) { |
|||
144 | 1 | $this->warnings[IPV6Deprecated::CODE] = new IPV6Deprecated(); |
|||
145 | } |
||||
146 | } |
||||
147 | |||||
148 | 12 | public function convertIPv4ToIPv6(string $addressLiteralIPv4): string |
|||
149 | { |
||||
150 | 12 | $matchesIP = []; |
|||
151 | 12 | $IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteralIPv4, $matchesIP); |
|||
152 | |||||
153 | // Extract IPv4 part from the end of the address-literal (if there is one) |
||||
154 | 12 | if ($IPv4Match > 0) { |
|||
155 | 1 | $index = (int) strrpos($addressLiteralIPv4, $matchesIP[0]); |
|||
156 | //There's a match but it is at the start |
||||
157 | 1 | if ($index > 0) { |
|||
158 | // Convert IPv4 part to IPv6 format for further testing |
||||
159 | 1 | return substr($addressLiteralIPv4, 0, $index) . '0:0'; |
|||
160 | } |
||||
161 | } |
||||
162 | |||||
163 | 11 | return $addressLiteralIPv4; |
|||
164 | } |
||||
165 | |||||
166 | /** |
||||
167 | * @param string $addressLiteral |
||||
168 | * |
||||
169 | * @return bool |
||||
170 | */ |
||||
171 | 15 | protected function checkIPV4Tag($addressLiteral): bool |
|||
172 | { |
||||
173 | 15 | $matchesIP = []; |
|||
174 | 15 | $IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteral, $matchesIP); |
|||
175 | |||||
176 | // Extract IPv4 part from the end of the address-literal (if there is one) |
||||
177 | |||||
178 | 15 | if ($IPv4Match > 0) { |
|||
179 | 4 | $index = strrpos($addressLiteral, $matchesIP[0]); |
|||
180 | //There's a match but it is at the start |
||||
181 | 4 | if ($index === 0) { |
|||
182 | 3 | $this->warnings[AddressLiteral::CODE] = new AddressLiteral(); |
|||
183 | 3 | return false; |
|||
184 | } |
||||
185 | } |
||||
186 | |||||
187 | 12 | return true; |
|||
188 | } |
||||
189 | |||||
190 | 18 | private function addObsoleteWarnings(): void |
|||
191 | { |
||||
192 | 18 | if (in_array($this->lexer->current->type, self::OBSOLETE_WARNINGS)) { |
|||
193 | 2 | $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT(); |
|||
194 | } |
||||
195 | } |
||||
196 | |||||
197 | 18 | private function addTagWarnings(): void |
|||
198 | { |
||||
199 | 18 | if ($this->lexer->isNextToken(EmailLexer::S_COLON)) { |
|||
200 | $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart(); |
||||
201 | } |
||||
202 | 18 | if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) { |
|||
203 | 9 | $lexer = clone $this->lexer; |
|||
204 | 9 | $lexer->moveNext(); |
|||
205 | 9 | if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) { |
|||
206 | 1 | $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart(); |
|||
207 | } |
||||
208 | } |
||||
209 | } |
||||
210 | } |
||||
211 |