Passed
Pull Request — master (#268)
by Eduardo Gulias
01:49
created

DomainPart::parse()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5.005

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 16
cts 17
cp 0.9412
rs 9.2088
c 0
b 0
f 0
cc 5
nc 6
nop 1
crap 5.005
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\Warning\AddressLiteral;
20
use Egulias\EmailValidator\Warning\CFWSWithFWS;
21
use Egulias\EmailValidator\Warning\DeprecatedComment;
22
use Egulias\EmailValidator\Warning\DomainLiteral;
23
use Egulias\EmailValidator\Warning\DomainTooLong;
24
use Egulias\EmailValidator\Warning\IPV6BadChar;
25
use Egulias\EmailValidator\Warning\IPV6ColonEnd;
26
use Egulias\EmailValidator\Warning\IPV6ColonStart;
27
use Egulias\EmailValidator\Warning\IPV6Deprecated;
28
use Egulias\EmailValidator\Warning\IPV6DoubleColon;
29
use Egulias\EmailValidator\Warning\IPV6GroupCount;
30
use Egulias\EmailValidator\Warning\IPV6MaxGroups;
31
use Egulias\EmailValidator\Warning\LabelTooLong;
32
use Egulias\EmailValidator\Warning\ObsoleteDTEXT;
33
use Egulias\EmailValidator\Warning\TLD;
34
35
class DomainPart extends Parser
36
{
37
    const DOMAIN_MAX_LENGTH = 254;
38
39
    /**
40
     * @var string
41
     */
42
    protected $domainPart = '';
43
44 96
    public function parse($domainPart)
45
    {
46 96
        $this->lexer->moveNext();
47
48 96
        $this->performDomainStartChecks();
49
50 86
        $domain = $this->doParseDomainPart();
51
52 56
        $prev = $this->lexer->getPrevious();
53 56
        $length = strlen($domain);
54
55 56
        if ($prev['type'] === EmailLexer::S_DOT) {
56 2
            throw new DotAtEnd();
57
        }
58 54
        if ($prev['type'] === EmailLexer::S_HYPHEN) {
59 1
            throw new DomainHyphened();
60
        }
61 53
        if ($length > self::DOMAIN_MAX_LENGTH) {
62 2
            $this->warnings[DomainTooLong::CODE] = new DomainTooLong();
63 2
        }
64 53
        if ($prev['type'] === EmailLexer::S_CR) {
65
            throw new CRLFAtTheEnd();
66
        }
67 53
        $this->domainPart = $domain;
68 53
    }
69
70 96
    private function performDomainStartChecks()
71
    {
72 96
        $this->checkInvalidTokensAfterAT();
73 95
        $this->checkEmptyDomain();
74
75 88
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
76 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
77 3
            $this->parseDomainComments();
78 1
        }
79 86
    }
80
81 95
    private function checkEmptyDomain()
82
    {
83 95
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
84 89
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
85 95
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
86
87 95
        if ($thereIsNoDomain) {
88 7
            throw new NoDomainPart();
89
        }
90 88
    }
91
92 96
    private function checkInvalidTokensAfterAT()
93
    {
94 96
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
95 1
            throw new DotAtStart();
96
        }
97 95
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
98
            throw new DomainHyphened();
99
        }
100 95
    }
101
102
    /**
103
     * @return string
104
     */
105 53
    public function getDomainPart()
106
    {
107 53
        return $this->domainPart;
108
    }
109
110
    /**
111
     * @param string $addressLiteral
112
     * @param int $maxGroups
113
     */
114 7
    public function checkIPV6Tag($addressLiteral, $maxGroups = 8)
115
    {
116 7
        $prev = $this->lexer->getPrevious();
117 7
        if ($prev['type'] === EmailLexer::S_COLON) {
118 1
            $this->warnings[IPV6ColonEnd::CODE] = new IPV6ColonEnd();
119 1
        }
120
121 7
        $IPv6       = substr($addressLiteral, 5);
122
        //Daniel Marschall's new IPv6 testing strategy
123 7
        $matchesIP  = explode(':', $IPv6);
124 7
        $groupCount = count($matchesIP);
125 7
        $colons     = strpos($IPv6, '::');
126
127 7
        if (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) {
128 1
            $this->warnings[IPV6BadChar::CODE] = new IPV6BadChar();
129 1
        }
130
131 7
        if ($colons === false) {
132
            // We need exactly the right number of groups
133 4
            if ($groupCount !== $maxGroups) {
134 1
                $this->warnings[IPV6GroupCount::CODE] = new IPV6GroupCount();
135 1
            }
136 4
            return;
137
        }
138
139 3
        if ($colons !== strrpos($IPv6, '::')) {
140 1
            $this->warnings[IPV6DoubleColon::CODE] = new IPV6DoubleColon();
141 1
            return;
142
        }
143
144 2
        if ($colons === 0 || $colons === (strlen($IPv6) - 2)) {
145
            // RFC 4291 allows :: at the start or end of an address
146
            //with 7 other groups in addition
147 2
            ++$maxGroups;
148 2
        }
149
150 2
        if ($groupCount > $maxGroups) {
151 1
            $this->warnings[IPV6MaxGroups::CODE] = new IPV6MaxGroups();
152 2
        } elseif ($groupCount === $maxGroups) {
153 1
            $this->warnings[IPV6Deprecated::CODE] = new IPV6Deprecated();
154 1
        }
155 2
    }
156
157
    /**
158
     * @return string
159
     */
160 86
    protected function doParseDomainPart()
161
    {
162 86
        $domain = '';
163 86
        $openedParenthesis = 0;
164
        do {
165 86
            $prev = $this->lexer->getPrevious();
166
167 86
            $this->checkNotAllowedChars($this->lexer->token);
168
169 86
            if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
170 3
                $this->parseComments();
171 3
                $openedParenthesis += $this->getOpenedParenthesis();
172 3
                $this->lexer->moveNext();
173 3
                $tmpPrev = $this->lexer->getPrevious();
174 3
                if ($tmpPrev['type'] === EmailLexer::S_CLOSEPARENTHESIS) {
175 3
                    $openedParenthesis--;
176 3
                }
177 3
            }
178 86 View Code Duplication
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS) {
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...
179 3
                if ($openedParenthesis === 0) {
180 3
                    throw new UnopenedComment();
181
                } else {
182
                    $openedParenthesis--;
183
                }
184
            }
185
186 85
            $this->checkConsecutiveDots();
187 85
            $this->checkDomainPartExceptions($prev);
188
189 84
            if ($this->hasBrackets()) {
190 13
                $this->parseDomainLiteral();
191 11
            }
192
193 82
            $this->checkLabelLength($prev);
194
195 82
            if ($this->isFWS()) {
196 3
                $this->parseFWS();
197 2
            }
198
199 82
            $domain .= $this->lexer->token['value'];
200 82
            $this->lexer->moveNext();
201 82
            if ($this->lexer->token['type'] === EmailLexer::S_SP) {
202 8
                throw new CharNotAllowed();
203
            }
204 82
        } while (null !== $this->lexer->token['type']);
205
206 56
        return $domain;
207
    }
208
209 86
    private function checkNotAllowedChars(array $token)
210
    {
211 86
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
212 86
        if (isset($notAllowed[$token['type']])) {
213 3
            throw new CharNotAllowed();
214
        }
215 86
    }
216
217
    /**
218
     * @return string|false
219
     */
220 13
    protected function parseDomainLiteral()
221
    {
222 13
        if ($this->lexer->isNextToken(EmailLexer::S_COLON)) {
223
            $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
224
        }
225 13
        if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) {
226 7
            $lexer = clone $this->lexer;
227 7
            $lexer->moveNext();
228 7
            if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) {
229 1
                $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
230 1
            }
231 7
        }
232
233 13
        return $this->doParseDomainLiteral();
234
    }
235
236
    /**
237
     * @return string|false
238
     */
239 13
    protected function doParseDomainLiteral()
240
    {
241 13
        $IPv6TAG = false;
242 13
        $addressLiteral = '';
243
        do {
244 13
            if ($this->lexer->token['type'] === EmailLexer::C_NUL) {
245
                throw new ExpectingDTEXT();
246
            }
247
248 13
            if ($this->lexer->token['type'] === EmailLexer::INVALID ||
249 13
                $this->lexer->token['type'] === EmailLexer::C_DEL   ||
250 13
                $this->lexer->token['type'] === EmailLexer::S_LF
251 13
            ) {
252 1
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
253 1
            }
254
255 13
            if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENQBRACKET, EmailLexer::S_OPENBRACKET))) {
256 1
                throw new ExpectingDTEXT();
257
            }
258
259 12
            if ($this->lexer->isNextTokenAny(
260 12
                array(EmailLexer::S_HTAB, EmailLexer::S_SP, $this->lexer->token['type'] === EmailLexer::CRLF)
261 12
            )) {
262
                $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
263
                $this->parseFWS();
264
            }
265
266 12
            if ($this->lexer->isNextToken(EmailLexer::S_CR)) {
267 1
                throw new CRNoLF();
268
            }
269
270 11
            if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH) {
271
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
272
                $addressLiteral .= $this->lexer->token['value'];
273
                $this->lexer->moveNext();
274
                $this->validateQuotedPair();
275
            }
276 11
            if ($this->lexer->token['type'] === EmailLexer::S_IPV6TAG) {
277 7
                $IPv6TAG = true;
278 7
            }
279 11
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEQBRACKET) {
280
                break;
281
            }
282
283 11
            $addressLiteral .= $this->lexer->token['value'];
284
285 11
        } while ($this->lexer->moveNext());
286
287 11
        $addressLiteral = str_replace('[', '', $addressLiteral);
288 11
        $addressLiteral = $this->checkIPV4Tag($addressLiteral);
289
290 11
        if (false === $addressLiteral) {
291 1
            return $addressLiteral;
292
        }
293
294 10
        if (!$IPv6TAG) {
295 3
            $this->warnings[DomainLiteral::CODE] = new DomainLiteral();
296 3
            return $addressLiteral;
297
        }
298
299 7
        $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
300
301 7
        $this->checkIPV6Tag($addressLiteral);
302
303 7
        return $addressLiteral;
304
    }
305
306
    /**
307
     * @param string $addressLiteral
308
     *
309
     * @return string|false
310
     */
311 11
    protected function checkIPV4Tag($addressLiteral)
312
    {
313 11
        $matchesIP  = array();
314
315
        // Extract IPv4 part from the end of the address-literal (if there is one)
316 11
        if (preg_match(
317 11
            '/\\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]?)$/',
318 11
            $addressLiteral,
319
            $matchesIP
320 11
        ) > 0
321 11
        ) {
322 2
            $index = strrpos($addressLiteral, $matchesIP[0]);
323 2
            if ($index === 0) {
324 1
                $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
325 1
                return false;
326
            }
327
            // Convert IPv4 part to IPv6 format for further testing
328 1
            $addressLiteral = substr($addressLiteral, 0, (int) $index) . '0:0';
329 1
        }
330
331 10
        return $addressLiteral;
332
    }
333
334 85
    protected function checkDomainPartExceptions(array $prev)
335
    {
336
        $invalidDomainTokens = array(
337 85
            EmailLexer::S_DQUOTE => true,
338 85
            EmailLexer::S_SQUOTE => true,
339 85
            EmailLexer::S_SEMICOLON => true,
340 85
            EmailLexer::S_GREATERTHAN => true,
341 85
            EmailLexer::S_LOWERTHAN => true,
342 85
        );
343
344 85
        if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
345 6
            throw new ExpectingATEXT();
346
        }
347
348 85
        if ($this->lexer->token['type'] === EmailLexer::S_COMMA) {
349 1
            throw new CommaInDomain();
350
        }
351
352 85
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
353 2
            throw new ConsecutiveAt();
354
        }
355
356 84
        if ($this->lexer->token['type'] === EmailLexer::S_OPENQBRACKET && $prev['type'] !== EmailLexer::S_AT) {
357 1
            throw new ExpectingATEXT();
358
        }
359
360 84 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...
361 1
            throw new DomainHyphened();
362
        }
363
364 84 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...
365 84
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
366
            throw new ExpectingATEXT();
367
        }
368 84
    }
369
370
    /**
371
     * @return bool
372
     */
373 84
    protected function hasBrackets()
374
    {
375 84
        if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) {
376 71
            return false;
377
        }
378
379
        try {
380 13
            $this->lexer->find(EmailLexer::S_CLOSEBRACKET);
381 13
        } catch (\RuntimeException $e) {
382
            throw new ExpectingDomainLiteralClose();
383
        }
384
385 13
        return true;
386
    }
387
388 82
    protected function checkLabelLength(array $prev)
389
    {
390 82
        if ($this->lexer->token['type'] === EmailLexer::S_DOT &&
391 82
            $prev['type'] === EmailLexer::GENERIC &&
392 45
            strlen($prev['value']) > 63
393 82
        ) {
394 1
            $this->warnings[LabelTooLong::CODE] = new LabelTooLong();
395 1
        }
396 82
    }
397
398 3
    protected function parseDomainComments()
399
    {
400 3
        $this->isUnclosedComment();
401 1
        while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
402 1
            $this->warnEscaping();
403 1
            $this->lexer->moveNext();
404 1
        }
405
406 1
        $this->lexer->moveNext();
407 1
        if ($this->lexer->isNextToken(EmailLexer::S_DOT)) {
408
            throw new ExpectingATEXT();
409
        }
410 1
    }
411
412
    protected function addTLDWarnings()
413
    {
414
        if ($this->warnings[DomainLiteral::CODE]) {
415
            $this->warnings[TLD::CODE] = new TLD();
416
        }
417
    }
418
}
419