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