1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Egulias\EmailValidator\Parser; |
4
|
|
|
|
5
|
|
|
use Egulias\EmailValidator\EmailLexer; |
6
|
|
|
use Egulias\EmailValidator\Warning\TLD; |
7
|
|
|
use Egulias\EmailValidator\Result\Result; |
8
|
|
|
use Egulias\EmailValidator\Result\ValidEmail; |
9
|
|
|
use Egulias\EmailValidator\Result\InvalidEmail; |
10
|
|
|
use Egulias\EmailValidator\Result\Reason\DotAtEnd; |
11
|
|
|
use Egulias\EmailValidator\Result\Reason\DotAtStart; |
12
|
|
|
use Egulias\EmailValidator\Warning\DeprecatedComment; |
13
|
|
|
use Egulias\EmailValidator\Result\Reason\CRLFAtTheEnd; |
14
|
|
|
use Egulias\EmailValidator\Result\Reason\LabelTooLong; |
15
|
|
|
use Egulias\EmailValidator\Result\Reason\NoDomainPart; |
16
|
|
|
use Egulias\EmailValidator\Result\Reason\ConsecutiveAt; |
17
|
|
|
use Egulias\EmailValidator\Result\Reason\DomainTooLong; |
18
|
|
|
use Egulias\EmailValidator\Result\Reason\CharNotAllowed; |
19
|
|
|
use Egulias\EmailValidator\Result\Reason\DomainHyphened; |
20
|
|
|
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT; |
21
|
|
|
use Egulias\EmailValidator\Parser\CommentStrategy\DomainComment; |
22
|
|
|
use Egulias\EmailValidator\Result\Reason\ExpectingDomainLiteralClose; |
23
|
|
|
use Egulias\EmailValidator\Parser\DomainLiteral as DomainLiteralParser; |
24
|
|
|
|
25
|
|
|
class DomainPart extends Parser |
26
|
|
|
{ |
27
|
|
|
const DOMAIN_MAX_LENGTH = 253; |
28
|
|
|
const LABEL_MAX_LENGTH = 63; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var string |
32
|
|
|
*/ |
33
|
|
|
protected $domainPart = ''; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var string |
37
|
|
|
*/ |
38
|
|
|
protected $label = ''; |
39
|
|
|
|
40
|
143 |
|
public function parse() : Result |
41
|
|
|
{ |
42
|
143 |
|
$this->lexer->moveNext(); |
43
|
|
|
|
44
|
143 |
|
$domainChecks = $this->performDomainStartChecks(); |
45
|
143 |
|
if ($domainChecks->isInvalid()) { |
46
|
11 |
|
return $domainChecks; |
47
|
|
|
} |
48
|
|
|
|
49
|
132 |
|
if ($this->lexer->token['type'] === EmailLexer::S_AT) { |
50
|
1 |
|
return new InvalidEmail(new ConsecutiveAt(), $this->lexer->token['value']); |
51
|
|
|
} |
52
|
131 |
|
$domain = $this->doParseDomainPart(); |
53
|
131 |
|
if ($domain->isInvalid()) { |
54
|
68 |
|
return $domain; |
55
|
|
|
} |
56
|
|
|
|
57
|
63 |
|
$length = strlen($this->domainPart); |
58
|
|
|
|
59
|
63 |
|
$end = $this->checkEndOfDomain(); |
60
|
63 |
|
if ($end->isInvalid()) { |
61
|
3 |
|
return $end; |
62
|
|
|
} |
63
|
|
|
|
64
|
60 |
|
if ($length > self::DOMAIN_MAX_LENGTH) { |
65
|
|
|
//$this->warnings[DomainTooLong::CODE] = new DomainTooLong(); |
66
|
|
|
return new InvalidEmail(new DomainTooLong(), $this->lexer->token['value']); |
67
|
|
|
} |
68
|
|
|
|
69
|
60 |
|
return new ValidEmail(); |
70
|
|
|
} |
71
|
|
|
|
72
|
63 |
|
private function checkEndOfDomain() : Result |
73
|
|
|
{ |
74
|
63 |
|
$prev = $this->lexer->getPrevious(); |
75
|
63 |
|
if ($prev['type'] === EmailLexer::S_DOT) { |
76
|
2 |
|
return new InvalidEmail(new DotAtEnd(), $this->lexer->token['value']); |
77
|
|
|
} |
78
|
61 |
|
if ($prev['type'] === EmailLexer::S_HYPHEN) { |
79
|
1 |
|
return new InvalidEmail(new DomainHyphened('Hypen found at the end of the domain'), $prev['value']); |
80
|
|
|
} |
81
|
|
|
|
82
|
60 |
|
if ($this->lexer->token['type'] === EmailLexer::S_SP) { |
83
|
|
|
return new InvalidEmail(new CRLFAtTheEnd(), $prev['value']); |
84
|
|
|
} |
85
|
60 |
|
return new ValidEmail(); |
86
|
|
|
|
87
|
|
|
} |
88
|
|
|
|
89
|
143 |
|
private function performDomainStartChecks() : Result |
90
|
|
|
{ |
91
|
143 |
|
$invalidTokens = $this->checkInvalidTokensAfterAT(); |
92
|
143 |
|
if ($invalidTokens->isInvalid()) { |
93
|
2 |
|
return $invalidTokens; |
94
|
|
|
} |
95
|
|
|
|
96
|
141 |
|
$missingDomain = $this->checkEmptyDomain(); |
97
|
141 |
|
if ($missingDomain->isInvalid()) { |
98
|
9 |
|
return $missingDomain; |
99
|
|
|
} |
100
|
|
|
|
101
|
132 |
|
if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) { |
102
|
3 |
|
$this->warnings[DeprecatedComment::CODE] = new DeprecatedComment(); |
103
|
|
|
} |
104
|
132 |
|
return new ValidEmail(); |
105
|
|
|
} |
106
|
|
|
|
107
|
141 |
|
private function checkEmptyDomain() : Result |
108
|
|
|
{ |
109
|
141 |
|
$thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY || |
110
|
133 |
|
($this->lexer->token['type'] === EmailLexer::S_SP && |
111
|
141 |
|
!$this->lexer->isNextToken(EmailLexer::GENERIC)); |
112
|
|
|
|
113
|
141 |
|
if ($thereIsNoDomain) { |
114
|
9 |
|
return new InvalidEmail(new NoDomainPart(), $this->lexer->token['value']); |
115
|
|
|
} |
116
|
|
|
|
117
|
132 |
|
return new ValidEmail(); |
118
|
|
|
} |
119
|
|
|
|
120
|
143 |
|
private function checkInvalidTokensAfterAT() : Result |
121
|
|
|
{ |
122
|
143 |
|
if ($this->lexer->token['type'] === EmailLexer::S_DOT) { |
123
|
1 |
|
return new InvalidEmail(new DotAtStart(), $this->lexer->token['value']); |
124
|
|
|
} |
125
|
142 |
|
if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) { |
126
|
1 |
|
return new InvalidEmail(new DomainHyphened('After AT'), $this->lexer->token['value']); |
127
|
|
|
} |
128
|
141 |
|
return new ValidEmail(); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @return string |
133
|
|
|
*/ |
134
|
|
|
public function getDomainPart() |
135
|
|
|
{ |
136
|
|
|
return $this->domainPart; |
137
|
|
|
} |
138
|
|
|
|
139
|
7 |
View Code Duplication |
protected function parseComments(): Result |
|
|
|
|
140
|
|
|
{ |
141
|
7 |
|
$commentParser = new Comment($this->lexer, new DomainComment()); |
142
|
7 |
|
$result = $commentParser->parse(); |
143
|
7 |
|
$this->warnings = array_merge($this->warnings, $commentParser->getWarnings()); |
144
|
|
|
|
145
|
7 |
|
return $result; |
146
|
|
|
} |
147
|
|
|
|
148
|
131 |
|
protected function doParseDomainPart() : Result |
149
|
|
|
{ |
150
|
131 |
|
$tldMissing = true; |
151
|
131 |
|
$hasComments = false; |
152
|
131 |
|
$domain = ''; |
153
|
|
|
do { |
154
|
131 |
|
$prev = $this->lexer->getPrevious(); |
155
|
|
|
|
156
|
131 |
|
$notAllowedChars = $this->checkNotAllowedChars($this->lexer->token); |
157
|
131 |
|
if ($notAllowedChars->isInvalid()) { |
158
|
3 |
|
return $notAllowedChars; |
159
|
|
|
} |
160
|
|
|
|
161
|
131 |
View Code Duplication |
if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS || |
|
|
|
|
162
|
131 |
|
$this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS ) { |
163
|
7 |
|
$hasComments = true; |
164
|
7 |
|
$commentsResult = $this->parseComments(); |
165
|
|
|
|
166
|
|
|
//Invalid comment parsing |
167
|
7 |
|
if($commentsResult->isInvalid()) { |
168
|
5 |
|
return $commentsResult; |
169
|
|
|
} |
170
|
|
|
} |
171
|
|
|
|
172
|
128 |
|
$dotsResult = $this->checkConsecutiveDots(); |
173
|
128 |
|
if ($dotsResult->isInvalid()) { |
174
|
2 |
|
return $dotsResult; |
175
|
|
|
} |
176
|
|
|
|
177
|
128 |
|
if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET) { |
178
|
18 |
|
$literalResult = $this->parseDomainLiteral(); |
179
|
|
|
|
180
|
18 |
|
$this->addTLDWarnings($tldMissing); |
181
|
18 |
|
return $literalResult; |
182
|
|
|
} |
183
|
|
|
|
184
|
110 |
|
$labelCheck = $this->checkLabelLength(); |
185
|
110 |
|
if ($labelCheck->isInvalid()) { |
186
|
5 |
|
return $labelCheck; |
187
|
|
|
} |
188
|
|
|
|
189
|
110 |
|
$FwsResult = $this->parseFWS(); |
190
|
110 |
|
if($FwsResult->isInvalid()) { |
191
|
4 |
|
return $FwsResult; |
192
|
|
|
} |
193
|
|
|
|
194
|
110 |
|
$domain .= $this->lexer->token['value']; |
195
|
|
|
|
196
|
110 |
View Code Duplication |
if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::GENERIC)) { |
|
|
|
|
197
|
54 |
|
$tldMissing = false; |
198
|
|
|
} |
199
|
|
|
|
200
|
110 |
|
$exceptionsResult = $this->checkDomainPartExceptions($prev, $hasComments); |
201
|
110 |
|
if ($exceptionsResult->isInvalid()) { |
202
|
44 |
|
return $exceptionsResult; |
203
|
|
|
} |
204
|
108 |
|
$this->lexer->moveNext(); |
205
|
|
|
|
206
|
108 |
|
} while (null !== $this->lexer->token['type']); |
207
|
|
|
|
208
|
50 |
|
$labelCheck = $this->checkLabelLength(true); |
209
|
50 |
|
if ($labelCheck->isInvalid()) { |
210
|
2 |
|
return $labelCheck; |
211
|
|
|
} |
212
|
48 |
|
$this->addTLDWarnings($tldMissing); |
213
|
|
|
|
214
|
48 |
|
$this->domainPart = $domain; |
215
|
48 |
|
return new ValidEmail(); |
216
|
|
|
} |
217
|
|
|
|
218
|
131 |
|
private function checkNotAllowedChars(array $token) : Result |
219
|
|
|
{ |
220
|
131 |
|
$notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true]; |
221
|
131 |
|
if (isset($notAllowed[$token['type']])) { |
222
|
3 |
|
return new InvalidEmail(new CharNotAllowed(), $token['value']); |
223
|
|
|
} |
224
|
131 |
|
return new ValidEmail(); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* @return Result |
229
|
|
|
*/ |
230
|
18 |
|
protected function parseDomainLiteral() : Result |
231
|
|
|
{ |
232
|
|
|
|
233
|
|
|
try { |
234
|
18 |
|
$this->lexer->find(EmailLexer::S_CLOSEBRACKET); |
235
|
|
|
} catch (\RuntimeException $e) { |
236
|
|
|
return new InvalidEmail(new ExpectingDomainLiteralClose(), $this->lexer->token['value']); |
237
|
|
|
} |
238
|
|
|
|
239
|
18 |
|
$domainLiteralParser = new DomainLiteralParser($this->lexer); |
240
|
18 |
|
$result = $domainLiteralParser->parse(); |
241
|
18 |
|
$this->warnings = array_merge($this->warnings, $domainLiteralParser->getWarnings()); |
242
|
18 |
|
return $result; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* @return InvalidEmail|ValidEmail |
247
|
|
|
*/ |
248
|
110 |
|
protected function checkDomainPartExceptions(array $prev, bool $hasComments) : Result |
249
|
|
|
{ |
250
|
|
|
$validDomainTokens = array( |
251
|
110 |
|
EmailLexer::GENERIC => true, |
252
|
|
|
EmailLexer::S_HYPHEN => true, |
253
|
|
|
EmailLexer::S_DOT => true, |
254
|
|
|
); |
255
|
|
|
|
256
|
110 |
|
if ($hasComments) { |
257
|
2 |
|
$validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true; |
258
|
2 |
|
$validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true; |
259
|
|
|
} |
260
|
|
|
|
261
|
110 |
|
if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET && $prev['type'] !== EmailLexer::S_AT) { |
262
|
|
|
return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), $this->lexer->token['value']); |
263
|
|
|
} |
264
|
|
|
|
265
|
110 |
View Code Duplication |
if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN && $this->lexer->isNextToken(EmailLexer::S_DOT)) { |
|
|
|
|
266
|
1 |
|
return new InvalidEmail(new DomainHyphened('Hypen found near DOT'), $this->lexer->token['value']); |
267
|
|
|
} |
268
|
|
|
|
269
|
110 |
View Code Duplication |
if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH |
|
|
|
|
270
|
110 |
|
&& $this->lexer->isNextToken(EmailLexer::GENERIC)) { |
271
|
|
|
return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->token['value']); |
272
|
|
|
} |
273
|
|
|
|
274
|
110 |
|
if (!isset($validDomainTokens[$this->lexer->token['type']])) { |
275
|
43 |
|
return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']); |
276
|
|
|
} |
277
|
|
|
|
278
|
108 |
|
return new ValidEmail(); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
|
282
|
110 |
|
private function checkLabelLength(bool $isEndOfDomain = false) : Result |
283
|
|
|
{ |
284
|
110 |
View Code Duplication |
if ($this->lexer->token['type'] === EmailLexer::S_DOT || $isEndOfDomain) { |
|
|
|
|
285
|
67 |
|
if ($this->isLabelTooLong($this->label)) { |
286
|
7 |
|
$this->label = ''; |
287
|
7 |
|
return new InvalidEmail(new LabelTooLong(), $this->lexer->token['value']); |
288
|
|
|
} |
289
|
|
|
} |
290
|
110 |
|
$this->label .= $this->lexer->token['value']; |
291
|
110 |
|
return new ValidEmail(); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
|
295
|
67 |
|
private function isLabelTooLong(string $label) : bool |
296
|
|
|
{ |
297
|
67 |
|
if (preg_match('/[^\x00-\x7F]/', $label)) { |
298
|
9 |
|
idn_to_ascii(utf8_decode($label), IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo); |
299
|
9 |
|
return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG); |
300
|
|
|
} |
301
|
59 |
|
return strlen($label) > self::LABEL_MAX_LENGTH; |
302
|
|
|
} |
303
|
|
|
|
304
|
66 |
|
private function addTLDWarnings(bool $isTLDMissing) : void |
305
|
|
|
{ |
306
|
66 |
|
if ($isTLDMissing) { |
307
|
23 |
|
$this->warnings[TLD::CODE] = new TLD(); |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.