Passed
Push — master ( d37d5a...db476d )
by Eduardo Gulias
02:17
created

DomainPart::hasBrackets()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

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