Passed
Push — master ( ef615f...68e418 )
by Eduardo Gulias
01:58
created

DomainPart::doParseDomainPart()   B

Complexity

Conditions 9
Paths 51

Size

Total Lines 48

Duplication

Lines 7
Ratio 14.58 %

Code Coverage

Tests 32
CRAP Score 9.0164

Importance

Changes 0
Metric Value
dl 7
loc 48
ccs 32
cts 34
cp 0.9412
rs 7.5789
c 0
b 0
f 0
cc 9
nc 51
nop 0
crap 9.0164
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 94
    public function parse($domainPart)
45
    {
46 94
        $this->lexer->moveNext();
47
48 94
        $this->performDomainStartChecks();
49
50 84
        $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 94
    private function performDomainStartChecks()
71
    {
72 94
        $this->checkInvalidTokensAfterAT();
73 93
        $this->checkEmptyDomain();
74
75 86
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
76 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
77 3
            $this->parseDomainComments();
78 1
        }
79 84
    }
80
81 93
    private function checkEmptyDomain()
82
    {
83 93
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
84 87
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
85 93
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
86
87 93
        if ($thereIsNoDomain) {
88 7
            throw new NoDomainPart();
89
        }
90 86
    }
91
92 94
    private function checkInvalidTokensAfterAT()
93
    {
94 94
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
95 1
            throw new DotAtStart();
96
        }
97 93
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
98
            throw new DomainHyphened();
99
        }
100 93
    }
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 84
    protected function doParseDomainPart()
161
    {
162 84
        $domain = '';
163 84
        $openedParenthesis = 0;
164
        do {
165 84
            $prev = $this->lexer->getPrevious();
166
167 84
            $this->checkNotAllowedChars($this->lexer->token);
168
169 84
            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 84 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 83
            $this->checkConsecutiveDots();
187 83
            $this->checkDomainPartExceptions($prev);
188
189 82
            if ($this->hasBrackets()) {
190 13
                $this->parseDomainLiteral();
191 11
            }
192
193 80
            $this->checkLabelLength($prev);
194
195 80
            if ($this->isFWS()) {
196 3
                $this->parseFWS();
197 2
            }
198
199 80
            $domain .= $this->lexer->token['value'];
200 80
            $this->lexer->moveNext();
201 80
            if ($this->lexer->token['type'] === EmailLexer::S_SP) {
202 8
                throw new CharNotAllowed();
203
            }
204 80
        } while (null !== $this->lexer->token['type']);
205
206 56
        return $domain;
207
    }
208
209 84
    private function checkNotAllowedChars(array $token)
210
    {
211 84
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
212 84
        if (isset($notAllowed[$token['type']])) {
213 3
            throw new CharNotAllowed();
214
        }
215 84
    }
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 83
    protected function checkDomainPartExceptions(array $prev)
335
    {
336
        $invalidDomainTokens = array(
337 83
            EmailLexer::S_DQUOTE => true,
338 83
            EmailLexer::S_SEMICOLON => true,
339 83
            EmailLexer::S_GREATERTHAN => true,
340 83
            EmailLexer::S_LOWERTHAN => true,
341 83
        );
342
343 83
        if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
344 4
            throw new ExpectingATEXT();
345
        }
346
347 83
        if ($this->lexer->token['type'] === EmailLexer::S_COMMA) {
348 1
            throw new CommaInDomain();
349
        }
350
351 83
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
352 2
            throw new ConsecutiveAt();
353
        }
354
355 82
        if ($this->lexer->token['type'] === EmailLexer::S_OPENQBRACKET && $prev['type'] !== EmailLexer::S_AT) {
356 1
            throw new ExpectingATEXT();
357
        }
358
359 82 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...
360 1
            throw new DomainHyphened();
361
        }
362
363 82 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...
364 82
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
365
            throw new ExpectingATEXT();
366
        }
367 82
    }
368
369
    /**
370
     * @return bool
371
     */
372 82
    protected function hasBrackets()
373
    {
374 82
        if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) {
375 69
            return false;
376
        }
377
378
        try {
379 13
            $this->lexer->find(EmailLexer::S_CLOSEBRACKET);
380 13
        } catch (\RuntimeException $e) {
381
            throw new ExpectingDomainLiteralClose();
382
        }
383
384 13
        return true;
385
    }
386
387 80
    protected function checkLabelLength(array $prev)
388
    {
389 80
        if ($this->lexer->token['type'] === EmailLexer::S_DOT &&
390 80
            $prev['type'] === EmailLexer::GENERIC &&
391 43
            strlen($prev['value']) > 63
392 80
        ) {
393 1
            $this->warnings[LabelTooLong::CODE] = new LabelTooLong();
394 1
        }
395 80
    }
396
397 3
    protected function parseDomainComments()
398
    {
399 3
        $this->isUnclosedComment();
400 1
        while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
401 1
            $this->warnEscaping();
402 1
            $this->lexer->moveNext();
403 1
        }
404
405 1
        $this->lexer->moveNext();
406 1
        if ($this->lexer->isNextToken(EmailLexer::S_DOT)) {
407
            throw new ExpectingATEXT();
408
        }
409 1
    }
410
411
    protected function addTLDWarnings()
412
    {
413
        if ($this->warnings[DomainLiteral::CODE]) {
414
            $this->warnings[TLD::CODE] = new TLD();
415
        }
416
    }
417
}
418