Passed
Pull Request — 3.x (#343)
by Alexander M.
02:37
created

DomainPart::checkNotAllowedChars()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
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
    public const DOMAIN_MAX_LENGTH = 253;
28
    public 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 148
    public function parse() : Result
41
    {
42 148
        $this->lexer->clearRecorded();
43 148
        $this->lexer->startRecording();
44
45 148
        $this->lexer->moveNext();
46
47 148
        $domainChecks = $this->performDomainStartChecks();
48 148
        if ($domainChecks->isInvalid()) {
49 12
            return $domainChecks;
50
        }
51
52 136
        if ($this->lexer->token['type'] === EmailLexer::S_AT) {
53 1
            return new InvalidEmail(new ConsecutiveAt(), $this->lexer->token['value']);
54
        }
55
56 135
        $result = $this->doParseDomainPart();
57 135
        if ($result->isInvalid()) {
58 67
            return $result;
59
        }
60
61 68
        $end = $this->checkEndOfDomain();
62 68
        if ($end->isInvalid()) {
63 4
            return $end;
64
        }
65
66 64
        $this->lexer->stopRecording();
67 64
        $this->domainPart = $this->lexer->getAccumulatedValues();
68
69 64
        $length = strlen($this->domainPart);
70 64
        if ($length > self::DOMAIN_MAX_LENGTH) {
71
            return new InvalidEmail(new DomainTooLong(), $this->lexer->token['value']);
72
        }
73
74 64
        return new ValidEmail();
75
    }
76
77 68
    private function checkEndOfDomain() : Result
78
    {
79 68
        $prev = $this->lexer->getPrevious();
80 68
        if ($prev['type'] === EmailLexer::S_DOT) {
81 3
            return new InvalidEmail(new DotAtEnd(), $this->lexer->token['value']);
82
        }
83 65
        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 64
        if ($this->lexer->token['type'] === EmailLexer::S_SP) {
88
            return new InvalidEmail(new CRLFAtTheEnd(), $prev['value']);
89
        }
90 64
        return new ValidEmail();
91
92
    }
93
94 148
    private function performDomainStartChecks() : Result
95
    {
96 148
        $invalidTokens = $this->checkInvalidTokensAfterAT();
97 148
        if ($invalidTokens->isInvalid()) {
98 2
            return $invalidTokens;
99
        }
100
        
101 146
        $missingDomain = $this->checkEmptyDomain();
102 146
        if ($missingDomain->isInvalid()) {
103 10
            return $missingDomain;
104
        }
105
106 136
        if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS) {
107 3
            $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment();
108
        }
109 136
        return new ValidEmail();
110
    }
111
112 146
    private function checkEmptyDomain() : Result
113
    {
114 146
        $thereIsNoDomain = $this->lexer->token['type'] === EmailLexer::S_EMPTY ||
115 146
            ($this->lexer->token['type'] === EmailLexer::S_SP &&
116 146
            !$this->lexer->isNextToken(EmailLexer::GENERIC));
117
118 146
        if ($thereIsNoDomain) {
119 10
            return new InvalidEmail(new NoDomainPart(), $this->lexer->token['value']);
120
        }
121
122 136
        return new ValidEmail();
123
    }
124
125 148
    private function checkInvalidTokensAfterAT() : Result
126
    {
127 148
        if ($this->lexer->token['type'] === EmailLexer::S_DOT) {
128 1
            return new InvalidEmail(new DotAtStart(), $this->lexer->token['value']);
129
        }
130 147
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN) {
131 1
            return new InvalidEmail(new DomainHyphened('After AT'), $this->lexer->token['value']);
132
        }
133 146
        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 135
    protected function doParseDomainPart() : Result
146
    {
147 135
        $tldMissing = true;
148 135
        $hasComments = false;
149 135
        $domain = '';
150
        do {
151 135
            $prev = $this->lexer->getPrevious();
152
153 135
            $notAllowedChars = $this->checkNotAllowedChars($this->lexer->token);
154 135
            if ($notAllowedChars->isInvalid()) {
155 4
                return $notAllowedChars;
156
            }
157
158 135
            if ($this->lexer->token['type'] === EmailLexer::S_OPENPARENTHESIS || 
159 135
                $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 132
            $dotsResult = $this->checkConsecutiveDots();
170 132
            if ($dotsResult->isInvalid()) {
171 2
                return $dotsResult;
172
            }
173
174 132
            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 114
                $labelCheck = $this->checkLabelLength();
182 114
                if ($labelCheck->isInvalid()) {
183 5
                    return $labelCheck;
184
                }
185
186 114
            $FwsResult = $this->parseFWS();
187 114
            if($FwsResult->isInvalid()) {
188 5
                return $FwsResult;
189
            }
190
191 114
            $domain .= $this->lexer->token['value'];
192
193 114
            if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
194 56
                $tldMissing = false;
195
            }
196
197 114
            $exceptionsResult = $this->checkDomainPartExceptions($prev, $hasComments);
198 114
            if ($exceptionsResult->isInvalid()) {
199 41
                return $exceptionsResult;
200
            }
201 112
            $this->lexer->moveNext();
202
203 112
        } while (null !== $this->lexer->token['type']);
204
205 55
        $labelCheck = $this->checkLabelLength(true);
206 55
        if ($labelCheck->isInvalid()) {
207 2
            return $labelCheck;
208
        }
209 53
        $this->addTLDWarnings($tldMissing);
210
211 53
        $this->domainPart = $domain;
212 53
        return new ValidEmail();
213
    }
214
215 135
    private function checkNotAllowedChars(array $token) : Result
216
    {
217 135
        $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH=> true];
218 135
        if (isset($notAllowed[$token['type']])) {
219 4
            return new InvalidEmail(new CharNotAllowed(), $token['value']);
220
        }
221 135
        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 114
    protected function checkDomainPartExceptions(array $prev, bool $hasComments) : Result
242
    {
243 114
        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 114
        if ($this->lexer->token['type'] === EmailLexer::S_HYPHEN && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
248 1
            return new InvalidEmail(new DomainHyphened('Hypen found near DOT'), $this->lexer->token['value']);
249
        }
250
251 114
        if ($this->lexer->token['type'] === EmailLexer::S_BACKSLASH
252 114
            && $this->lexer->isNextToken(EmailLexer::GENERIC)) {
253
            return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->token['value']);
254
        }
255
256 114
        return $this->validateTokens($hasComments);
257
    }
258
259 108
    protected function validateTokens(bool $hasComments) : Result
260
    {
261 108
        $validDomainTokens = array(
262 108
            EmailLexer::GENERIC => true,
263 108
            EmailLexer::S_HYPHEN => true,
264 108
            EmailLexer::S_DOT => true,
265 108
        );
266
267 108
        if ($hasComments) {
268 2
            $validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
269 2
            $validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
270
        }
271
272 108
        if (!isset($validDomainTokens[$this->lexer->token['type']])) {
273 40
            return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']);
274
        }
275
276 106
        return new ValidEmail();
277
    }
278
279 114
    private function checkLabelLength(bool $isEndOfDomain = false) : Result
280
    {
281 114
        if ($this->lexer->token['type'] === EmailLexer::S_DOT || $isEndOfDomain) {
282 72
            if ($this->isLabelTooLong($this->label)) {
283 7
                return new InvalidEmail(new LabelTooLong(), $this->lexer->token['value']);
284
            }
285 66
            $this->label = '';
286
        }
287 114
        $this->label .= $this->lexer->token['value'];
288 114
        return new ValidEmail();
289
    }
290
291
292 72
    private function isLabelTooLong(string $label) : bool
293
    {
294 72
        if (preg_match('/[^\x00-\x7F]/', $label)) {
295 10
            idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
296 10
            return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG);
297
        }
298 66
        return strlen($label) > self::LABEL_MAX_LENGTH;
299
    }
300
301 71
    private function addTLDWarnings(bool $isTLDMissing) : void
302
    {
303 71
        if ($isTLDMissing) {
304 26
            $this->warnings[TLD::CODE] = new TLD();
305
        }
306
    }
307
308 148
    public function domainPart() : string
309
    {
310 148
        return $this->domainPart;
311
    }
312
}
313