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