Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

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