Passed
Push — master ( ade688...2f38a4 )
by Eduardo Gulias
02:31
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 93
    public function parse($domainPart)
45
    {
46 93
        $this->lexer->moveNext();
47
48 93
        $this->performDomainStartChecks();
49
50 83
        $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 93
    private function performDomainStartChecks()
71
    {
72 93
        $this->checkInvalidTokensAfterAT();
73 92
        $this->checkEmptyDomain();
74
75 85
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
76 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
77 3
            $this->parseDomainComments();
78 1
        }
79 83
    }
80
81 92
    private function checkEmptyDomain()
82
    {
83 92
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
84 86
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
85 92
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
86
87 92
        if ($thereIsNoDomain) {
88 7
            throw new NoDomainPart();
89
        }
90 85
    }
91
92 93
    private function checkInvalidTokensAfterAT()
93
    {
94 93
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
95 1
            throw new DotAtStart();
96
        }
97 92
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
98
            throw new DomainHyphened();
99
        }
100 92
    }
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 83
    protected function doParseDomainPart()
161
    {
162 83
        $domain = '';
163 83
        $openedParenthesis = 0;
164
        do {
165 83
            $prev = $this->lexer->getPrevious();
166
167 83
            $this->checkNotAllowedChars($this->lexer->token);
168
169 83
            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 83 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 82
            $this->checkConsecutiveDots();
187 82
            $this->checkDomainPartExceptions($prev);
188
189 81
            if ($this->hasBrackets()) {
190 13
                $this->parseDomainLiteral();
191 11
            }
192
193 79
            $this->checkLabelLength($prev);
194
195 79
            if ($this->isFWS()) {
196 9
                $this->parseFWS();
197 7
            }
198
199 79
            $domain .= $this->lexer->token['value'];
200 79
            $this->lexer->moveNext();
201 79
        } while (null !== $this->lexer->token['type']);
202
203 56
        return $domain;
204
    }
205
206 83
    private function checkNotAllowedChars(array $token)
207
    {
208 83
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
209 83
        if (isset($notAllowed[$token['type']])) {
210 8
            throw new CharNotAllowed();
211
        }
212 83
    }
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 82
    protected function checkDomainPartExceptions(array $prev)
332
    {
333
        $invalidDomainTokens = array(
334 82
            EmailLexer::S_DQUOTE => true,
335 82
            EmailLexer::S_SEMICOLON => true,
336 82
            EmailLexer::S_GREATERTHAN => true,
337 82
            EmailLexer::S_LOWERTHAN => true,
338 82
        );
339
340 82
        if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
341 4
            throw new ExpectingATEXT();
342
        }
343
344 82
        if ($this->lexer->token['type'] === EmailLexer::S_COMMA) {
345 1
            throw new CommaInDomain();
346
        }
347
348 82
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
349 2
            throw new ConsecutiveAt();
350
        }
351
352 81
        if ($this->lexer->token['type'] === EmailLexer::S_OPENQBRACKET && $prev['type'] !== EmailLexer::S_AT) {
353 1
            throw new ExpectingATEXT();
354
        }
355
356 81 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 81 View Code Duplication
        if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH
361 81
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
362
            throw new ExpectingATEXT();
363
        }
364 81
    }
365
366
    /**
367
     * @return bool
368
     */
369 81
    protected function hasBrackets()
370
    {
371 81
        if ($this->lexer->token['type'] !== EmailLexer::S_OPENBRACKET) {
372 68
            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 79
    protected function checkLabelLength(array $prev)
385
    {
386 79
        if ($this->lexer->token['type'] === EmailLexer::S_DOT &&
387 79
            $prev['type'] === EmailLexer::GENERIC &&
388 42
            strlen($prev['value']) > 63
389 79
        ) {
390 1
            $this->warnings[LabelTooLong::CODE] = new LabelTooLong();
391 1
        }
392 79
    }
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