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