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 \Egulias\EmailValidator\Warning\Warning[] 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