Passed
Push — 3.x ( 451b43...8e526a )
by Eduardo Gulias
02:05
created

DomainPart::validateTokens()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19

Duplication

Lines 3
Ratio 15.79 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 3
loc 19
ccs 8
cts 8
cp 1
rs 9.6333
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3
1
<?php
2
3
namespace Egulias\EmailValidator\Parser;
4
5
use Egulias\EmailValidator\EmailLexer;
6
use Egulias\EmailValidator\Warning\TLD;
7
use Egulias\EmailValidator\Result\Result;
8
use Egulias\EmailValidator\Result\ValidEmail;
9
use Egulias\EmailValidator\Result\InvalidEmail;
10
use Egulias\EmailValidator\Result\Reason\DotAtEnd;
11
use Egulias\EmailValidator\Result\Reason\DotAtStart;
12
use Egulias\EmailValidator\Warning\DeprecatedComment;
13
use Egulias\EmailValidator\Result\Reason\CRLFAtTheEnd;
14
use Egulias\EmailValidator\Result\Reason\LabelTooLong;
15
use Egulias\EmailValidator\Result\Reason\NoDomainPart;
16
use Egulias\EmailValidator\Result\Reason\ConsecutiveAt;
17
use Egulias\EmailValidator\Result\Reason\DomainTooLong;
18
use Egulias\EmailValidator\Result\Reason\CharNotAllowed;
19
use Egulias\EmailValidator\Result\Reason\DomainHyphened;
20
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
21
use Egulias\EmailValidator\Parser\CommentStrategy\DomainComment;
22
use Egulias\EmailValidator\Result\Reason\ExpectingDomainLiteralClose;
23
use Egulias\EmailValidator\Parser\DomainLiteral as DomainLiteralParser;
24
25
class DomainPart extends PartParser
26
{
27
    const DOMAIN_MAX_LENGTH = 253;
28
    const LABEL_MAX_LENGTH = 63;
29
30
    /**
31
     * @var string
32
     */
33
    protected $domainPart = '';
34
35
    /**
36
     * @var string
37
     */
38
    protected $label = '';
39
40 147
    public function parse() : Result
41
    {
42 147
        $this->lexer->clearRecorded();
43 147
        $this->lexer->startRecording();
44
45 147
        $this->lexer->moveNext();
46
47 147
        $domainChecks = $this->performDomainStartChecks();
48 147
        if ($domainChecks->isInvalid()) {
49 12
            return $domainChecks;
50
        }
51
52 135
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
53 1
            return new InvalidEmail(new ConsecutiveAt(), $this->lexer->token['value']);
54
        }
55
56 134
        $result = $this->doParseDomainPart();
57 134
        if ($result->isInvalid()) {
58 67
            return $result;
59
        }
60
61 67
        $end = $this->checkEndOfDomain();
62 67
        if ($end->isInvalid()) {
63 4
            return $end;
64
        }
65
66 63
        $this->lexer->stopRecording();
67 63
        $this->domainPart = $this->lexer->getAccumulatedValues();
68
69 63
        $length = strlen($this->domainPart);
70 63
        if ($length > self::DOMAIN_MAX_LENGTH) {
71
            return new InvalidEmail(new DomainTooLong(), $this->lexer->token['value']);
72
        }
73
74 63
        return new ValidEmail();
75
    }
76
77 67
    private function checkEndOfDomain() : Result
78
    {
79 67
        $prev = $this->lexer->getPrevious();
80 67
        if ($prev['type'] === EmailLexer::S_DOT) {
81 3
            return new InvalidEmail(new DotAtEnd(), $this->lexer->token['value']);
82
        }
83 64
        if ($prev['type'] === EmailLexer::S_HYPHEN) {
84 1
            return new InvalidEmail(new DomainHyphened('Hypen found at the end of the domain'), $prev['value']);
85
        }
86
87 63
        if ($this->lexer->token['type'] === EmailLexer::S_SP) {
88
            return new InvalidEmail(new CRLFAtTheEnd(), $prev['value']);
89
        }
90 63
        return new ValidEmail();
91
92
    }
93
94 147
    private function performDomainStartChecks() : Result
95
    {
96 147
        $invalidTokens = $this->checkInvalidTokensAfterAT();
97 147
        if ($invalidTokens->isInvalid()) {
98 2
            return $invalidTokens;
99
        }
100
        
101 145
        $missingDomain = $this->checkEmptyDomain();
102 145
        if ($missingDomain->isInvalid()) {
103 10
            return $missingDomain;
104
        }
105
106 135
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
107 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
108
        }
109 135
        return new ValidEmail();
110
    }
111
112 145
    private function checkEmptyDomain() : Result
113
    {
114 145
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
115 136
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
116 145
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
117
118 145
        if ($thereIsNoDomain) {
119 10
            return new InvalidEmail(new NoDomainPart(), $this->lexer->token['value']);
120
        }
121
122 135
        return new ValidEmail();
123
    }
124
125 147
    private function checkInvalidTokensAfterAT() : Result
126
    {
127 147
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
128 1
            return new InvalidEmail(new DotAtStart(), $this->lexer->token['value']);
129
        }
130 146
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
131 1
            return new InvalidEmail(new DomainHyphened('After AT'), $this->lexer->token['value']);
132
        }
133 145
        return new ValidEmail();
134
    }
135
136 7
    protected function parseComments(): Result
137
    {
138 7
        $commentParser = new Comment($this->lexer, new DomainComment());
139 7
        $result = $commentParser->parse();
140 7
        $this->warnings = array_merge($this->warnings, $commentParser->getWarnings());
141
142 7
        return $result;
143
    }
144
145 134
    protected function doParseDomainPart() : Result
146
    {
147 134
        $tldMissing = true;
148 134
        $hasComments = false;
149 134
        $domain = '';
150
        do {
151 134
            $prev = $this->lexer->getPrevious();
152
153 134
            $notAllowedChars = $this->checkNotAllowedChars($this->lexer->token);
154 134
            if ($notAllowedChars->isInvalid()) {
155 4
                return $notAllowedChars;
156
            }
157
158 134 View Code Duplication
            if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS || 
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...
159 134
                $this->lexer->token['type'] === EmailLexer::S_CLOSEPARENTHESIS ) {
160 7
                $hasComments = true;
161 7
                $commentsResult = $this->parseComments();
162
163
                //Invalid comment parsing
164 7
                if($commentsResult->isInvalid()) {
165 5
                    return $commentsResult;
166
                }
167
            }
168
169 131
            $dotsResult = $this->checkConsecutiveDots();
170 131
            if ($dotsResult->isInvalid()) {
171 2
                return $dotsResult;
172
            }
173
174 131
            if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET) {
175 18
                $literalResult = $this->parseDomainLiteral();
176
177 18
                $this->addTLDWarnings($tldMissing);
178 18
                return $literalResult;
179
            }
180
181 113
                $labelCheck = $this->checkLabelLength();
182 113
                if ($labelCheck->isInvalid()) {
183 5
                    return $labelCheck;
184
                }
185
186 113
            $FwsResult = $this->parseFWS();
187 113
            if($FwsResult->isInvalid()) {
188 5
                return $FwsResult;
189
            }
190
191 113
            $domain .= $this->lexer->token['value'];
192
193 113 View Code Duplication
            if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
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...
194 55
                $tldMissing = false;
195
            }
196
197 113
            $exceptionsResult = $this->checkDomainPartExceptions($prev, $hasComments);
198 113
            if ($exceptionsResult->isInvalid()) {
199 41
                return $exceptionsResult;
200
            }
201 111
            $this->lexer->moveNext();
202
203 111
        } while (null !== $this->lexer->token['type']);
204
205 54
        $labelCheck = $this->checkLabelLength(true);
206 54
        if ($labelCheck->isInvalid()) {
207 2
            return $labelCheck;
208
        }
209 52
        $this->addTLDWarnings($tldMissing);
210
211 52
        $this->domainPart = $domain;
212 52
        return new ValidEmail();
213
    }
214
215 134
    private function checkNotAllowedChars(array $token) : Result
216
    {
217 134
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
218 134
        if (isset($notAllowed[$token['type']])) {
219 4
            return new InvalidEmail(new CharNotAllowed(), $token['value']);
220
        }
221 134
        return new ValidEmail();
222
    }
223
224
    /**
225
     * @return Result
226
     */
227 18
    protected function parseDomainLiteral() : Result
228
    {
229
        try {
230 18
            $this->lexer->find(EmailLexer::S_CLOSEBRACKET);
231
        } catch (\RuntimeException $e) {
232
            return new InvalidEmail(new ExpectingDomainLiteralClose(), $this->lexer->token['value']);
233
        }
234
235 18
        $domainLiteralParser = new DomainLiteralParser($this->lexer);
236 18
        $result = $domainLiteralParser->parse();
237 18
        $this->warnings = array_merge($this->warnings, $domainLiteralParser->getWarnings());
238 18
        return $result;
239
    }
240
241 113
    protected function checkDomainPartExceptions(array $prev, bool $hasComments) : Result
242
    {
243 113
        if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET && $prev['type'] !== EmailLexer::S_AT) {
244
            return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), $this->lexer->token['value']);
245
        }
246
247 113 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...
248 1
            return new InvalidEmail(new DomainHyphened('Hypen found near DOT'), $this->lexer->token['value']);
249
        }
250
251 113 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...
252 113
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
253
            return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->token['value']);
254
        }
255
256 113
        return $this->validateTokens($hasComments);
257
    }
258
259 107
    protected function validateTokens(bool $hasComments) : Result
260
    {
261
        $validDomainTokens = array(
262 107
            EmailLexer::GENERIC => true,
263
            EmailLexer::S_HYPHEN => true,
264
            EmailLexer::S_DOT => true,
265
        );
266
267 107
        if ($hasComments) {
268 2
            $validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
269 2
            $validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
270
        }
271
272 107 View Code Duplication
        if (!isset($validDomainTokens[$this->lexer->token['type']])) {
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...
273 40
            return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']);
274
        }
275
276 105
        return new ValidEmail();
277
    }
278
279 113
    private function checkLabelLength(bool $isEndOfDomain = false) : Result
280
    {
281 113
        if ($this->lexer->token['type'] === EmailLexer::S_DOT || $isEndOfDomain) {
282 71
            if ($this->isLabelTooLong($this->label)) {
283 7
                $this->label = '';
284 7
                return new InvalidEmail(new LabelTooLong(), $this->lexer->token['value']);
285
            }
286
        }
287 113
        $this->label .= $this->lexer->token['value'];
288 113
        return new ValidEmail();
289
    }
290
291
292 71
    private function isLabelTooLong(string $label) : bool
293
    {
294 71
        if (preg_match('/[^\x00-\x7F]/', $label)) {
295 10
            idn_to_ascii(utf8_decode($label), IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
296 10
            return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG);
297
        }
298 62
        return strlen($label) > self::LABEL_MAX_LENGTH;
299
    }
300
301 70
    private function addTLDWarnings(bool $isTLDMissing) : void
302
    {
303 70
        if ($isTLDMissing) {
304 26
            $this->warnings[TLD::CODE] = new TLD();
305
        }
306 70
    }
307
308 147
    public function domainPart() : string
309
    {
310 147
        return $this->domainPart;
311
    }
312
}