1<?php
2
3namespace Egulias\EmailValidator\Parser;
4
5use Egulias\EmailValidator\EmailLexer;
6use Egulias\EmailValidator\Exception\AtextAfterCFWS;
7use Egulias\EmailValidator\Exception\ConsecutiveDot;
8use Egulias\EmailValidator\Exception\CRLFAtTheEnd;
9use Egulias\EmailValidator\Exception\CRLFX2;
10use Egulias\EmailValidator\Exception\CRNoLF;
11use Egulias\EmailValidator\Exception\ExpectedQPair;
12use Egulias\EmailValidator\Exception\ExpectingATEXT;
13use Egulias\EmailValidator\Exception\ExpectingCTEXT;
14use Egulias\EmailValidator\Exception\UnclosedComment;
15use Egulias\EmailValidator\Exception\UnclosedQuotedString;
16use Egulias\EmailValidator\Warning\CFWSNearAt;
17use Egulias\EmailValidator\Warning\CFWSWithFWS;
18use Egulias\EmailValidator\Warning\Comment;
19use Egulias\EmailValidator\Warning\QuotedPart;
20use Egulias\EmailValidator\Warning\QuotedString;
21
22abstract class Parser
23{
24    protected $warnings = [];
25    protected $lexer;
26    protected $openedParenthesis = 0;
27
28    public function __construct(EmailLexer $lexer)
29    {
30        $this->lexer = $lexer;
31    }
32
33    public function getWarnings()
34    {
35        return $this->warnings;
36    }
37
38    abstract public function parse($str);
39
40    /** @return int */
41    public function getOpenedParenthesis()
42    {
43        return $this->openedParenthesis;
44    }
45
46    /**
47     * validateQuotedPair
48     */
49    protected function validateQuotedPair()
50    {
51        if (!($this->lexer->token['type'] === EmailLexer::INVALID
52            || $this->lexer->token['type'] === EmailLexer::C_DEL)) {
53            throw new ExpectedQPair();
54        }
55
56        $this->warnings[QuotedPart::CODE] =
57            new QuotedPart($this->lexer->getPrevious()['type'], $this->lexer->token['type']);
58    }
59
60    protected function parseComments()
61    {
62        $this->openedParenthesis = 1;
63        $this->isUnclosedComment();
64        $this->warnings[Comment::CODE] = new Comment();
65        while (!$this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) {
66            if ($this->lexer->isNextToken(EmailLexer::S_OPENPARENTHESIS)) {
67                $this->openedParenthesis++;
68            }
69            $this->warnEscaping();
70            $this->lexer->moveNext();
71        }
72
73        $this->lexer->moveNext();
74        if ($this->lexer->isNextTokenAny(array(EmailLexer::GENERIC, EmailLexer::S_EMPTY))) {
75            throw new ExpectingATEXT();
76        }
77
78        if ($this->lexer->isNextToken(EmailLexer::S_AT)) {
79            $this->warnings[CFWSNearAt::CODE] = new CFWSNearAt();
80        }
81    }
82
83    protected function isUnclosedComment()
84    {
85        try {
86            $this->lexer->find(EmailLexer::S_CLOSEPARENTHESIS);
87            return true;
88        } catch (\RuntimeException $e) {
89            throw new UnclosedComment();
90        }
91    }
92
93    protected function parseFWS()
94    {
95        $previous = $this->lexer->getPrevious();
96
97        $this->checkCRLFInFWS();
98
99        if ($this->lexer->token['type'] === EmailLexer::S_CR) {
100            throw new CRNoLF();
101        }
102
103        if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous['type']  !== EmailLexer::S_AT) {
104            throw new AtextAfterCFWS();
105        }
106
107        if ($this->lexer->token['type'] === EmailLexer::S_LF || $this->lexer->token['type'] === EmailLexer::C_NUL) {
108            throw new ExpectingCTEXT();
109        }
110
111        if ($this->lexer->isNextToken(EmailLexer::S_AT) || $previous['type']  === EmailLexer::S_AT) {
112            $this->warnings[CFWSNearAt::CODE] = new CFWSNearAt();
113        } else {
114            $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
115        }
116    }
117
118    protected function checkConsecutiveDots()
119    {
120        if ($this->lexer->token['type'] === EmailLexer::S_DOT && $this->lexer->isNextToken(EmailLexer::S_DOT)) {
121            throw new ConsecutiveDot();
122        }
123    }
124
125    protected function isFWS()
126    {
127        if ($this->escaped()) {
128            return false;
129        }
130
131        if ($this->lexer->token['type'] === EmailLexer::S_SP ||
132            $this->lexer->token['type'] === EmailLexer::S_HTAB ||
133            $this->lexer->token['type'] === EmailLexer::S_CR ||
134            $this->lexer->token['type'] === EmailLexer::S_LF ||
135            $this->lexer->token['type'] === EmailLexer::CRLF
136        ) {
137            return true;
138        }
139
140        return false;
141    }
142
143    protected function escaped()
144    {
145        $previous = $this->lexer->getPrevious();
146
147        if ($previous['type'] === EmailLexer::S_BACKSLASH
148            &&
149            $this->lexer->token['type'] !== EmailLexer::GENERIC
150        ) {
151            return true;
152        }
153
154        return false;
155    }
156
157    protected function warnEscaping()
158    {
159        if ($this->lexer->token['type'] !== EmailLexer::S_BACKSLASH) {
160            return false;
161        }
162
163        if ($this->lexer->isNextToken(EmailLexer::GENERIC)) {
164            throw new ExpectingATEXT();
165        }
166
167        if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB, EmailLexer::C_DEL))) {
168            return false;
169        }
170
171        $this->warnings[QuotedPart::CODE] =
172            new QuotedPart($this->lexer->getPrevious()['type'], $this->lexer->token['type']);
173        return true;
174
175    }
176
177    protected function checkDQUOTE($hasClosingQuote)
178    {
179        if ($this->lexer->token['type'] !== EmailLexer::S_DQUOTE) {
180            return $hasClosingQuote;
181        }
182        if ($hasClosingQuote) {
183            return $hasClosingQuote;
184        }
185        $previous = $this->lexer->getPrevious();
186        if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous['type'] === EmailLexer::GENERIC) {
187            throw new ExpectingATEXT();
188        }
189
190        try {
191            $this->lexer->find(EmailLexer::S_DQUOTE);
192            $hasClosingQuote = true;
193        } catch (\Exception $e) {
194            throw new UnclosedQuotedString();
195        }
196        $this->warnings[QuotedString::CODE] = new QuotedString($previous['value'], $this->lexer->token['value']);
197
198        return $hasClosingQuote;
199    }
200
201    protected function checkCRLFInFWS()
202    {
203        if ($this->lexer->token['type'] !== EmailLexer::CRLF) {
204            return;
205        }
206
207        if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) {
208            throw new CRLFX2();
209        }
210
211        if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) {
212            throw new CRLFAtTheEnd();
213        }
214    }
215}
216