Passed
Push — master ( 087837...0dbf5d )
by Eduardo Gulias
23:16 queued 23:16
created

DomainPart::isLabelTooLong()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
416
417 5
            return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG);
418
        }
419
420 68
        return strlen($label) > self::LABEL_MAX_LENGTH;
421
    }
422
423 3
    protected function parseDomainComments()
424
    {
425 3
        $this->isUnclosedComment();
426 1
        while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
427 1
            $this->warnEscaping();
428 1
            $this->lexer->moveNext();
429 1
        }
430
431 1
        $this->lexer->moveNext();
432 1
        if ($this->lexer->isNextToken(EmailLexer::S_DOT)) {
433
            throw new ExpectingATEXT();
434
        }
435 1
    }
436
437
    protected function addTLDWarnings()
438
    {
439
        if ($this->warnings[DomainLiteral::CODE]) {
440
            $this->warnings[TLD::CODE] = new TLD();
441
        }
442
    }
443
}
444