Test Setup Failed
Push — 3.0.0-dev ( 302b98...138fa6 )
by Eduardo Gulias
01:59
created

DomainPart::parseDomainComments()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 3
nc 4
nop 0
1
<?php
2
3
namespace Egulias\EmailValidator\Parser;
4
5
use Egulias\EmailValidator\EmailLexer;
6
use Egulias\EmailValidator\Exception\CharNotAllowed;
7
use Egulias\EmailValidator\Exception\CommaInDomain;
8
use Egulias\EmailValidator\Exception\ConsecutiveAt;
9
use Egulias\EmailValidator\Exception\CRLFAtTheEnd;
10
use Egulias\EmailValidator\Exception\CRNoLF;
11
use Egulias\EmailValidator\Exception\DomainHyphened;
12
use Egulias\EmailValidator\Exception\DotAtEnd;
13
use Egulias\EmailValidator\Exception\DotAtStart;
14
use Egulias\EmailValidator\Exception\ExpectingATEXT;
15
use Egulias\EmailValidator\Exception\ExpectingDomainLiteralClose;
16
use Egulias\EmailValidator\Exception\ExpectingDTEXT;
17
use Egulias\EmailValidator\Exception\NoDomainPart;
18
use Egulias\EmailValidator\Exception\UnopenedComment;
19
use Egulias\EmailValidator\Result\InvalidEmail;
20
use Egulias\EmailValidator\Result\Reason\DomainHyphened as ReasonDomainHyphened;
21
use Egulias\EmailValidator\Result\Reason\DotAtStart as ReasonDotAtStart;
22
use Egulias\EmailValidator\Result\Result;
23
use Egulias\EmailValidator\Result\ValidEmail;
24
use Egulias\EmailValidator\Warning\AddressLiteral;
25
use Egulias\EmailValidator\Warning\CFWSWithFWS;
26
use Egulias\EmailValidator\Warning\DeprecatedComment;
27
use Egulias\EmailValidator\Warning\DomainLiteral;
28
use Egulias\EmailValidator\Warning\DomainTooLong;
29
use Egulias\EmailValidator\Warning\IPV6BadChar;
30
use Egulias\EmailValidator\Warning\IPV6ColonEnd;
31
use Egulias\EmailValidator\Warning\IPV6ColonStart;
32
use Egulias\EmailValidator\Warning\IPV6Deprecated;
33
use Egulias\EmailValidator\Warning\IPV6DoubleColon;
34
use Egulias\EmailValidator\Warning\IPV6GroupCount;
35
use Egulias\EmailValidator\Warning\IPV6MaxGroups;
36
use Egulias\EmailValidator\Warning\LabelTooLong;
37
use Egulias\EmailValidator\Warning\ObsoleteDTEXT;
38
use Egulias\EmailValidator\Warning\TLD;
39
40
class DomainPart extends Parser
41
{
42
    const DOMAIN_MAX_LENGTH = 254;
43
44
    /**
45
     * @var string
46
     */
47
    protected $domainPart = '';
48
49
    public function parse($domainPart)
50
    {
51
        $this->lexer->moveNext();
52
53
        $domainChecks = $this->performDomainStartChecks();
54
        if ($domainChecks->isInvalid()) {
55
            return $domainChecks;
56
        }
57
58
        $domain = $this->doParseDomainPart();
59
60
        $prev = $this->lexer->getPrevious();
61
        $length = strlen($domain);
62
63
        if ($prev['type'] === EmailLexer::S_DOT) {
64
            throw new DotAtEnd();
65
        }
66
        if ($prev['type'] === EmailLexer::S_HYPHEN) {
67
            throw new DomainHyphened();
68
        }
69
        if ($length > self::DOMAIN_MAX_LENGTH) {
70
            $this->warnings[DomainTooLong::CODE] = new DomainTooLong();
71
        }
72
        if ($prev['type'] === EmailLexer::S_CR) {
73
            throw new CRLFAtTheEnd();
74
        }
75
        $this->domainPart = $domain;
76
77
        return new ValidEmail();
78
    }
79
80
    private function performDomainStartChecks() : Result
81
    {
82
        $invalidTokens = $this->checkInvalidTokensAfterAT();
83
        if ($invalidTokens->isInvalid()) {
84
            return $invalidTokens;
85
        }
86
        $this->checkEmptyDomain();
87
88
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
89
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
90
            $this->parseDomainComments();
91
        }
92
        return new ValidEmail();
93
    }
94
95
    private function checkEmptyDomain()
96
    {
97
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
98
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
99
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
100
101
        if ($thereIsNoDomain) {
102
            throw new NoDomainPart();
103
        }
104
    }
105
106
    private function checkInvalidTokensAfterAT() : Result
107
    {
108 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
109
            return new InvalidEmail(new ReasonDotAtStart(), $this->lexer->token['value']);
110
        }
111 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
112
            return new InvalidEmail(new ReasonDomainHyphened('After AT'), $this->lexer->token['value']);
113
        }
114
        return new ValidEmail();
115
    }
116
117
    /**
118
     * @return string
119
     */
120
    public function getDomainPart()
121
    {
122
        return $this->domainPart;
123
    }
124
125
    /**
126
     * @param string $addressLiteral
127
     * @param int $maxGroups
128
     */
129
    public function checkIPV6Tag($addressLiteral, $maxGroups = 8)
130
    {
131
        $prev = $this->lexer->getPrevious();
132
        if ($prev['type'] === EmailLexer::S_COLON) {
133
            $this->warnings[IPV6ColonEnd::CODE] = new IPV6ColonEnd();
134
        }
135
136
        $IPv6       = substr($addressLiteral, 5);
137
        //Daniel Marschall's new IPv6 testing strategy
138
        $matchesIP  = explode(':', $IPv6);
139
        $groupCount = count($matchesIP);
140
        $colons     = strpos($IPv6, '::');
141
142
        if (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) {
143
            $this->warnings[IPV6BadChar::CODE] = new IPV6BadChar();
144
        }
145
146
        if ($colons === false) {
147
            // We need exactly the right number of groups
148
            if ($groupCount !== $maxGroups) {
149
                $this->warnings[IPV6GroupCount::CODE] = new IPV6GroupCount();
150
            }
151
            return;
152
        }
153
154
        if ($colons !== strrpos($IPv6, '::')) {
155
            $this->warnings[IPV6DoubleColon::CODE] = new IPV6DoubleColon();
156
            return;
157
        }
158
159
        if ($colons === 0 || $colons === (strlen($IPv6) - 2)) {
160
            // RFC 4291 allows :: at the start or end of an address
161
            //with 7 other groups in addition
162
            ++$maxGroups;
163
        }
164
165
        if ($groupCount > $maxGroups) {
166
            $this->warnings[IPV6MaxGroups::CODE] = new IPV6MaxGroups();
167
        } elseif ($groupCount === $maxGroups) {
168
            $this->warnings[IPV6Deprecated::CODE] = new IPV6Deprecated();
169
        }
170
    }
171
172
    /**
173
     * @return string
174
     */
175
    protected function doParseDomainPart()
176
    {
177
        $domain = '';
178
        $openedParenthesis = 0;
179
        do {
180
            $prev = $this->lexer->getPrevious();
181
182
            $this->checkNotAllowedChars($this->lexer->token);
183
184
            if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
185
                $this->parseComments();
186
                $openedParenthesis += $this->getOpenedParenthesis();
187
                $this->lexer->moveNext();
188
                $tmpPrev = $this->lexer->getPrevious();
189
                if ($tmpPrev['type'] === EmailLexer::S_CLOSEPARENTHESIS) {
190
                    $openedParenthesis--;
191
                }
192
            }
193
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS) {
194
                if ($openedParenthesis === 0) {
195
                    throw new UnopenedComment();
196
                } else {
197
                    $openedParenthesis--;
198
                }
199
            }
200
201
            $this->checkConsecutiveDots();
202
            $this->checkDomainPartExceptions($prev);
203
204
            if ($this->hasBrackets()) {
205
                $this->parseDomainLiteral();
206
            }
207
208
            $this->checkLabelLength($prev);
209
210
            if ($this->isFWS()) {
211
                $this->parseFWS();
212
            }
213
214
            $domain .= $this->lexer->token['value'];
215
            $this->lexer->moveNext();
216
        } while (null !== $this->lexer->token['type']);
217
218
        return $domain;
219
    }
220
221
    private function checkNotAllowedChars(array $token)
222
    {
223
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
224
        if (isset($notAllowed[$token['type']])) {
225
            throw new CharNotAllowed();
226
        }
227
    }
228
229
    /**
230
     * @return string|false
231
     */
232
    protected function parseDomainLiteral()
233
    {
234
        if ($this->lexer->isNextToken(EmailLexer::S_COLON)) {
235
            $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
236
        }
237
        if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) {
238
            $lexer = clone $this->lexer;
239
            $lexer->moveNext();
240
            if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) {
241
                $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
242
            }
243
        }
244
245
        return $this->doParseDomainLiteral();
246
    }
247
248
    /**
249
     * @return string|false
250
     */
251
    protected function doParseDomainLiteral()
252
    {
253
        $IPv6TAG = false;
254
        $addressLiteral = '';
255
        do {
256
            if ($this->lexer->token['type'] === EmailLexer::C_NUL) {
257
                throw new ExpectingDTEXT();
258
            }
259
260
            if ($this->lexer->token['type'] === EmailLexer::INVALID ||
261
                $this->lexer->token['type'] === EmailLexer::C_DEL   ||
262
                $this->lexer->token['type'] === EmailLexer::S_LF
263
            ) {
264
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
265
            }
266
267
            if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENQBRACKET, EmailLexer::S_OPENBRACKET))) {
268
                throw new ExpectingDTEXT();
269
            }
270
271
            if ($this->lexer->isNextTokenAny(
272
                array(EmailLexer::S_HTAB, EmailLexer::S_SP, $this->lexer->token['type'] === EmailLexer::CRLF)
273
            )) {
274
                $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
275
                $this->parseFWS();
276
            }
277
278
            if ($this->lexer->isNextToken(EmailLexer::S_CR)) {
279
                throw new CRNoLF();
280
            }
281
282
            if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH) {
283
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
284
                $addressLiteral .= $this->lexer->token['value'];
285
                $this->lexer->moveNext();
286
                $this->validateQuotedPair();
287
            }
288
            if ($this->lexer->token['type'] === EmailLexer::S_IPV6TAG) {
289
                $IPv6TAG = true;
290
            }
291
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEQBRACKET) {
292
                break;
293
            }
294
295
            $addressLiteral .= $this->lexer->token['value'];
296
297
        } while ($this->lexer->moveNext());
298
299
        $addressLiteral = str_replace('[', '', $addressLiteral);
300
        $addressLiteral = $this->checkIPV4Tag($addressLiteral);
301
302
        if (false === $addressLiteral) {
303
            return $addressLiteral;
304
        }
305
306
        if (!$IPv6TAG) {
307
            $this->warnings[DomainLiteral::CODE] = new DomainLiteral();
308
            return $addressLiteral;
309
        }
310
311
        $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
312
313
        $this->checkIPV6Tag($addressLiteral);
314
315
        return $addressLiteral;
316
    }
317
318
    /**
319
     * @param string $addressLiteral
320
     *
321
     * @return string|false
322
     */
323
    protected function checkIPV4Tag($addressLiteral)
324
    {
325
        $matchesIP  = array();
326
327
        // Extract IPv4 part from the end of the address-literal (if there is one)
328
        if (preg_match(
329
            '/\\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]?)$/',
330
            $addressLiteral,
331
            $matchesIP
332
        ) > 0
333
        ) {
334
            $index = strrpos($addressLiteral, $matchesIP[0]);
335
            if ($index === 0) {
336
                $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
337
                return false;
338
            }
339
            // Convert IPv4 part to IPv6 format for further testing
340
            $addressLiteral = substr($addressLiteral, 0, (int) $index) . '0:0';
341
        }
342
343
        return $addressLiteral;
344
    }
345
346
    protected function checkDomainPartExceptions(array $prev)
347
    {
348
        $invalidDomainTokens = array(
349
            EmailLexer::S_DQUOTE => true,
350
            EmailLexer::S_SEMICOLON => true,
351
            EmailLexer::S_GREATERTHAN => true,
352
            EmailLexer::S_LOWERTHAN => true,
353
        );
354
355
        if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
356
            throw new ExpectingATEXT();
357
        }
358
359
        if ($this->lexer->token['type'] === EmailLexer::S_COMMA) {
360
            throw new CommaInDomain();
361
        }
362
363
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
364
            throw new ConsecutiveAt();
365
        }
366
367
        if ($this->lexer->token['type'] === EmailLexer::S_OPENQBRACKET && $prev['type'] !== EmailLexer::S_AT) {
368
            throw new ExpectingATEXT();
369
        }
370
371 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
372
            throw new DomainHyphened();
373
        }
374
375 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
376
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
377
            throw new ExpectingATEXT();
378
        }
379
    }
380
381
    /**
382
     * @return bool
383
     */
384
    protected function hasBrackets()
385
    {
386
        if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) {
387
            return false;
388
        }
389
390
        try {
391
            $this->lexer->find(EmailLexer::S_CLOSEBRACKET);
392
        } catch (\RuntimeException $e) {
393
            throw new ExpectingDomainLiteralClose();
394
        }
395
396
        return true;
397
    }
398
399
    protected function checkLabelLength(array $prev)
400
    {
401
        if ($this->lexer->token['type'] === EmailLexer::S_DOT &&
402
            $prev['type'] === EmailLexer::GENERIC &&
403
            strlen($prev['value']) > 63
404
        ) {
405
            $this->warnings[LabelTooLong::CODE] = new LabelTooLong();
406
        }
407
    }
408
409
    protected function parseDomainComments()
410
    {
411
        $this->isUnclosedComment();
412
        while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
413
            $this->warnEscaping();
414
            $this->lexer->moveNext();
415
        }
416
417
        $this->lexer->moveNext();
418
        if ($this->lexer->isNextToken(EmailLexer::S_DOT)) {
419
            throw new ExpectingATEXT();
420
        }
421
    }
422
423
    protected function addTLDWarnings()
424
    {
425
        if ($this->warnings[DomainLiteral::CODE]) {
426
            $this->warnings[TLD::CODE] = new TLD();
427
        }
428
    }
429
}
430