Passed
Push — master ( c4b8d1...e834ee )
by Eduardo Gulias
02:09
created

EmailValidator/Parser/DomainPart.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 92
    public function parse($domainPart)
45
    {
46 92
        $this->lexer->moveNext();
47
48 92
        $this->performDomainStartChecks();
49
50 82
        $domain = $this->doParseDomainPart();
51
52 55
        $prev = $this->lexer->getPrevious();
53 55
        $length = strlen($domain);
54
55 55
        if ($prev['type'] === EmailLexer::S_DOT) {
56 2
            throw new DotAtEnd();
57
        }
58 53
        if ($prev['type'] === EmailLexer::S_HYPHEN) {
59 1
            throw new DomainHyphened();
60
        }
61 52
        if ($length > self::DOMAIN_MAX_LENGTH) {
62 2
            $this->warnings[DomainTooLong::CODE] = new DomainTooLong();
63 2
        }
64 52
        if ($prev['type'] === EmailLexer::S_CR) {
65
            throw new CRLFAtTheEnd();
66
        }
67 52
        $this->domainPart = $domain;
68 52
    }
69
70 92
    private function performDomainStartChecks()
71
    {
72 92
        $this->checkInvalidTokensAfterAT();
73 91
        $this->checkEmptyDomain();
74
75 84
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
76 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
77 3
            $this->parseDomainComments();
78 1
        }
79 82
    }
80
81 91
    private function checkEmptyDomain()
82
    {
83 91
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
84 85
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
85 91
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
86
87 91
        if ($thereIsNoDomain) {
88 7
            throw new NoDomainPart();
89
        }
90 84
    }
91
92 92
    private function checkInvalidTokensAfterAT()
93
    {
94 92
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
95 1
            throw new DotAtStart();
96
        }
97 91
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
98
            throw new DomainHyphened();
99
        }
100 91
    }
101
102
    /**
103
     * @return string
104
     */
105 52
    public function getDomainPart()
106
    {
107 52
        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 82
    protected function doParseDomainPart()
161
    {
162 82
        $domain = '';
163 82
        $openedParenthesis = 0;
164
        do {
165 82
            $prev = $this->lexer->getPrevious();
166
167 82
            $this->checkNotAllowedChars($this->lexer->token);
168
169 82
            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 82 View Code Duplication
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS) {
0 ignored issues
show
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 81
            $this->checkConsecutiveDots();
187 81
            $this->checkDomainPartExceptions($prev);
188
189 80
            if ($this->hasBrackets()) {
190 13
                $this->parseDomainLiteral();
191 11
            }
192
193 78
            $this->checkLabelLength($prev);
194
195 78
            if ($this->isFWS()) {
196 9
                $this->parseFWS();
197 7
            }
198
199 78
            $domain .= $this->lexer->token['value'];
200 78
            $this->lexer->moveNext();
201 78
        } while (null !== $this->lexer->token['type']);
202
203 55
        return $domain;
204
    }
205
206 82
    private function checkNotAllowedChars(array $token)
207
    {
208 82
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
209 82
        if (isset($notAllowed[$token['type']])) {
210 8
            throw new CharNotAllowed();
211
        }
212 82
    }
213
214
    /**
215
     * @return string|false
216
     */
217 13
    protected function parseDomainLiteral()
218
    {
219 13
        if ($this->lexer->isNextToken(EmailLexer::S_COLON)) {
220
            $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
221
        }
222 13
        if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) {
223 7
            $lexer = clone $this->lexer;
224 7
            $lexer->moveNext();
225 7
            if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) {
226 1
                $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart();
227 1
            }
228 7
        }
229
230 13
        return $this->doParseDomainLiteral();
231
    }
232
233
    /**
234
     * @return string|false
235
     */
236 13
    protected function doParseDomainLiteral()
237
    {
238 13
        $IPv6TAG = false;
239 13
        $addressLiteral = '';
240
        do {
241 13
            if ($this->lexer->token['type'] === EmailLexer::C_NUL) {
242
                throw new ExpectingDTEXT();
243
            }
244
245 13
            if ($this->lexer->token['type'] === EmailLexer::INVALID ||
246 13
                $this->lexer->token['type'] === EmailLexer::C_DEL   ||
247 13
                $this->lexer->token['type'] === EmailLexer::S_LF
248 13
            ) {
249 1
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
250 1
            }
251
252 13
            if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENQBRACKET, EmailLexer::S_OPENBRACKET))) {
253 1
                throw new ExpectingDTEXT();
254
            }
255
256 12
            if ($this->lexer->isNextTokenAny(
257 12
                array(EmailLexer::S_HTAB, EmailLexer::S_SP, $this->lexer->token['type'] === EmailLexer::CRLF)
258 12
            )) {
259
                $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
260
                $this->parseFWS();
261
            }
262
263 12
            if ($this->lexer->isNextToken(EmailLexer::S_CR)) {
264 1
                throw new CRNoLF();
265
            }
266
267 11
            if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH) {
268
                $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
269
                $addressLiteral .= $this->lexer->token['value'];
270
                $this->lexer->moveNext();
271
                $this->validateQuotedPair();
272
            }
273 11
            if ($this->lexer->token['type'] === EmailLexer::S_IPV6TAG) {
274 7
                $IPv6TAG = true;
275 7
            }
276 11
            if ($this->lexer->token['type'] === EmailLexer::S_CLOSEQBRACKET) {
277
                break;
278
            }
279
280 11
            $addressLiteral .= $this->lexer->token['value'];
281
282 11
        } while ($this->lexer->moveNext());
283
284 11
        $addressLiteral = str_replace('[', '', $addressLiteral);
285 11
        $addressLiteral = $this->checkIPV4Tag($addressLiteral);
286
287 11
        if (false === $addressLiteral) {
288 1
            return $addressLiteral;
289
        }
290
291 10
        if (!$IPv6TAG) {
292 3
            $this->warnings[DomainLiteral::CODE] = new DomainLiteral();
293 3
            return $addressLiteral;
294
        }
295
296 7
        $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
297
298 7
        $this->checkIPV6Tag($addressLiteral);
299
300 7
        return $addressLiteral;
301
    }
302
303
    /**
304
     * @param string $addressLiteral
305
     *
306
     * @return string|false
307
     */
308 11
    protected function checkIPV4Tag($addressLiteral)
309
    {
310 11
        $matchesIP  = array();
311
312
        // Extract IPv4 part from the end of the address-literal (if there is one)
313 11
        if (preg_match(
314 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]?)$/',
315 11
            $addressLiteral,
316
            $matchesIP
317 11
        ) > 0
318 11
        ) {
319 2
            $index = strrpos($addressLiteral, $matchesIP[0]);
320 2
            if ($index === 0) {
321 1
                $this->warnings[AddressLiteral::CODE] = new AddressLiteral();
322 1
                return false;
323
            }
324
            // Convert IPv4 part to IPv6 format for further testing
325 1
            $addressLiteral = substr($addressLiteral, 0, (int) $index) . '0:0';
326 1
        }
327
328 10
        return $addressLiteral;
329
    }
330
331 81
    protected function checkDomainPartExceptions(array $prev)
332
    {
333
        $invalidDomainTokens = array(
334 81
            EmailLexer::S_DQUOTE => true,
335 81
            EmailLexer::S_SEMICOLON => true,
336 81
            EmailLexer::S_GREATERTHAN => true,
337 81
            EmailLexer::S_LOWERTHAN => true,
338 81
        );
339
340 81
        if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
341 4
            throw new ExpectingATEXT();
342
        }
343
344 81
        if ($this->lexer->token['type'] === EmailLexer::S_COMMA) {
345 1
            throw new CommaInDomain();
346
        }
347
348 81
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
349 2
            throw new ConsecutiveAt();
350
        }
351
352 80
        if ($this->lexer->token['type'] === EmailLexer::S_OPENQBRACKET && $prev['type'] !== EmailLexer::S_AT) {
353 1
            throw new ExpectingATEXT();
354
        }
355
356 80 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
357 1
            throw new DomainHyphened();
358
        }
359
360 80 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH
361 80
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
362
            throw new ExpectingATEXT();
363
        }
364 80
    }
365
366
    /**
367
     * @return bool
368
     */
369 80
    protected function hasBrackets()
370
    {
371 80
        if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) {
372 67
            return false;
373
        }
374
375
        try {
376 13
            $this->lexer->find(EmailLexer::S_CLOSEBRACKET);
377 13
        } catch (\RuntimeException $e) {
378
            throw new ExpectingDomainLiteralClose();
379
        }
380
381 13
        return true;
382
    }
383
384 78
    protected function checkLabelLength(array $prev)
385
    {
386 78
        if ($this->lexer->token['type'] === EmailLexer::S_DOT &&
387 78
            $prev['type'] === EmailLexer::GENERIC &&
388 41
            strlen($prev['value']) > 63
389 78
        ) {
390 1
            $this->warnings[LabelTooLong::CODE] = new LabelTooLong();
391 1
        }
392 78
    }
393
394 3
    protected function parseDomainComments()
395
    {
396 3
        $this->isUnclosedComment();
397 1
        while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
398 1
            $this->warnEscaping();
399 1
            $this->lexer->moveNext();
400 1
        }
401
402 1
        $this->lexer->moveNext();
403 1
        if ($this->lexer->isNextToken(EmailLexer::S_DOT)) {
404
            throw new ExpectingATEXT();
405
        }
406 1
    }
407
408
    protected function addTLDWarnings()
409
    {
410
        if ($this->warnings[DomainLiteral::CODE]) {
411
            $this->warnings[TLD::CODE] = new TLD();
412
        }
413
    }
414
}
415