EmailLexer.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <?php
  2. namespace Egulias\EmailValidator;
  3. use Doctrine\Common\Lexer\AbstractLexer;
  4. use Doctrine\Common\Lexer\Token;
  5. /** @extends AbstractLexer<int, string> */
  6. class EmailLexer extends AbstractLexer
  7. {
  8. //ASCII values
  9. public const S_EMPTY = -1;
  10. public const C_NUL = 0;
  11. public const S_HTAB = 9;
  12. public const S_LF = 10;
  13. public const S_CR = 13;
  14. public const S_SP = 32;
  15. public const EXCLAMATION = 33;
  16. public const S_DQUOTE = 34;
  17. public const NUMBER_SIGN = 35;
  18. public const DOLLAR = 36;
  19. public const PERCENTAGE = 37;
  20. public const AMPERSAND = 38;
  21. public const S_SQUOTE = 39;
  22. public const S_OPENPARENTHESIS = 40;
  23. public const S_CLOSEPARENTHESIS = 41;
  24. public const ASTERISK = 42;
  25. public const S_PLUS = 43;
  26. public const S_COMMA = 44;
  27. public const S_HYPHEN = 45;
  28. public const S_DOT = 46;
  29. public const S_SLASH = 47;
  30. public const S_COLON = 58;
  31. public const S_SEMICOLON = 59;
  32. public const S_LOWERTHAN = 60;
  33. public const S_EQUAL = 61;
  34. public const S_GREATERTHAN = 62;
  35. public const QUESTIONMARK = 63;
  36. public const S_AT = 64;
  37. public const S_OPENBRACKET = 91;
  38. public const S_BACKSLASH = 92;
  39. public const S_CLOSEBRACKET = 93;
  40. public const CARET = 94;
  41. public const S_UNDERSCORE = 95;
  42. public const S_BACKTICK = 96;
  43. public const S_OPENCURLYBRACES = 123;
  44. public const S_PIPE = 124;
  45. public const S_CLOSECURLYBRACES = 125;
  46. public const S_TILDE = 126;
  47. public const C_DEL = 127;
  48. public const INVERT_QUESTIONMARK = 168;
  49. public const INVERT_EXCLAMATION = 173;
  50. public const GENERIC = 300;
  51. public const S_IPV6TAG = 301;
  52. public const INVALID = 302;
  53. public const CRLF = 1310;
  54. public const S_DOUBLECOLON = 5858;
  55. public const ASCII_INVALID_FROM = 127;
  56. public const ASCII_INVALID_TO = 199;
  57. /**
  58. * US-ASCII visible characters not valid for atext (@link http://tools.ietf.org/html/rfc5322#section-3.2.3)
  59. *
  60. * @var array<string, int>
  61. */
  62. protected $charValue = [
  63. '{' => self::S_OPENCURLYBRACES,
  64. '}' => self::S_CLOSECURLYBRACES,
  65. '(' => self::S_OPENPARENTHESIS,
  66. ')' => self::S_CLOSEPARENTHESIS,
  67. '<' => self::S_LOWERTHAN,
  68. '>' => self::S_GREATERTHAN,
  69. '[' => self::S_OPENBRACKET,
  70. ']' => self::S_CLOSEBRACKET,
  71. ':' => self::S_COLON,
  72. ';' => self::S_SEMICOLON,
  73. '@' => self::S_AT,
  74. '\\' => self::S_BACKSLASH,
  75. '/' => self::S_SLASH,
  76. ',' => self::S_COMMA,
  77. '.' => self::S_DOT,
  78. "'" => self::S_SQUOTE,
  79. "`" => self::S_BACKTICK,
  80. '"' => self::S_DQUOTE,
  81. '-' => self::S_HYPHEN,
  82. '::' => self::S_DOUBLECOLON,
  83. ' ' => self::S_SP,
  84. "\t" => self::S_HTAB,
  85. "\r" => self::S_CR,
  86. "\n" => self::S_LF,
  87. "\r\n" => self::CRLF,
  88. 'IPv6' => self::S_IPV6TAG,
  89. '' => self::S_EMPTY,
  90. '\0' => self::C_NUL,
  91. '*' => self::ASTERISK,
  92. '!' => self::EXCLAMATION,
  93. '&' => self::AMPERSAND,
  94. '^' => self::CARET,
  95. '$' => self::DOLLAR,
  96. '%' => self::PERCENTAGE,
  97. '~' => self::S_TILDE,
  98. '|' => self::S_PIPE,
  99. '_' => self::S_UNDERSCORE,
  100. '=' => self::S_EQUAL,
  101. '+' => self::S_PLUS,
  102. '¿' => self::INVERT_QUESTIONMARK,
  103. '?' => self::QUESTIONMARK,
  104. '#' => self::NUMBER_SIGN,
  105. '¡' => self::INVERT_EXCLAMATION,
  106. ];
  107. public const INVALID_CHARS_REGEX = "/[^\p{S}\p{C}\p{Cc}]+/iu";
  108. public const VALID_UTF8_REGEX = '/\p{Cc}+/u';
  109. public const CATCHABLE_PATTERNS = [
  110. '[a-zA-Z]+[46]?', //ASCII and domain literal
  111. '[^\x00-\x7F]', //UTF-8
  112. '[0-9]+',
  113. '\r\n',
  114. '::',
  115. '\s+?',
  116. '.',
  117. ];
  118. public const NON_CATCHABLE_PATTERNS = [
  119. '[\xA0-\xff]+',
  120. ];
  121. public const MODIFIERS = 'iu';
  122. /** @var bool */
  123. protected $hasInvalidTokens = false;
  124. /**
  125. * @var Token<int, string>
  126. */
  127. protected Token $previous;
  128. /**
  129. * The last matched/seen token.
  130. *
  131. * @var Token<int, string>
  132. */
  133. public Token $current;
  134. /**
  135. * @var Token<int, string>
  136. */
  137. private Token $nullToken;
  138. /** @var string */
  139. private $accumulator = '';
  140. /** @var bool */
  141. private $hasToRecord = false;
  142. public function __construct()
  143. {
  144. /** @var Token<int, string> $nullToken */
  145. $nullToken = new Token('', self::S_EMPTY, 0);
  146. $this->nullToken = $nullToken;
  147. $this->current = $this->previous = $this->nullToken;
  148. $this->lookahead = null;
  149. }
  150. public function reset(): void
  151. {
  152. $this->hasInvalidTokens = false;
  153. parent::reset();
  154. $this->current = $this->previous = $this->nullToken;
  155. }
  156. /**
  157. * @param int $type
  158. * @throws \UnexpectedValueException
  159. * @return boolean
  160. *
  161. */
  162. public function find($type): bool
  163. {
  164. $search = clone $this;
  165. $search->skipUntil($type);
  166. if (!$search->lookahead) {
  167. throw new \UnexpectedValueException($type . ' not found');
  168. }
  169. return true;
  170. }
  171. /**
  172. * moveNext
  173. *
  174. * @return boolean
  175. */
  176. public function moveNext(): bool
  177. {
  178. if ($this->hasToRecord && $this->previous === $this->nullToken) {
  179. $this->accumulator .= $this->current->value;
  180. }
  181. $this->previous = $this->current;
  182. if ($this->lookahead === null) {
  183. $this->lookahead = $this->nullToken;
  184. }
  185. $hasNext = parent::moveNext();
  186. $this->current = $this->token ?? $this->nullToken;
  187. if ($this->hasToRecord) {
  188. $this->accumulator .= $this->current->value;
  189. }
  190. return $hasNext;
  191. }
  192. /**
  193. * Retrieve token type. Also processes the token value if necessary.
  194. *
  195. * @param string $value
  196. * @throws \InvalidArgumentException
  197. * @return integer
  198. */
  199. protected function getType(&$value): int
  200. {
  201. $encoded = $value;
  202. if (mb_detect_encoding($value, 'auto', true) !== 'UTF-8') {
  203. $encoded = mb_convert_encoding($value, 'UTF-8', 'Windows-1252');
  204. }
  205. if ($this->isValid($encoded)) {
  206. return $this->charValue[$encoded];
  207. }
  208. if ($this->isNullType($encoded)) {
  209. return self::C_NUL;
  210. }
  211. if ($this->isInvalidChar($encoded)) {
  212. $this->hasInvalidTokens = true;
  213. return self::INVALID;
  214. }
  215. return self::GENERIC;
  216. }
  217. protected function isValid(string $value): bool
  218. {
  219. return isset($this->charValue[$value]);
  220. }
  221. protected function isNullType(string $value): bool
  222. {
  223. return $value === "\0";
  224. }
  225. protected function isInvalidChar(string $value): bool
  226. {
  227. return !preg_match(self::INVALID_CHARS_REGEX, $value);
  228. }
  229. protected function isUTF8Invalid(string $value): bool
  230. {
  231. return preg_match(self::VALID_UTF8_REGEX, $value) !== false;
  232. }
  233. public function hasInvalidTokens(): bool
  234. {
  235. return $this->hasInvalidTokens;
  236. }
  237. /**
  238. * getPrevious
  239. *
  240. * @return Token<int, string>
  241. */
  242. public function getPrevious(): Token
  243. {
  244. return $this->previous;
  245. }
  246. /**
  247. * Lexical catchable patterns.
  248. *
  249. * @return string[]
  250. */
  251. protected function getCatchablePatterns(): array
  252. {
  253. return self::CATCHABLE_PATTERNS;
  254. }
  255. /**
  256. * Lexical non-catchable patterns.
  257. *
  258. * @return string[]
  259. */
  260. protected function getNonCatchablePatterns(): array
  261. {
  262. return self::NON_CATCHABLE_PATTERNS;
  263. }
  264. protected function getModifiers(): string
  265. {
  266. return self::MODIFIERS;
  267. }
  268. public function getAccumulatedValues(): string
  269. {
  270. return $this->accumulator;
  271. }
  272. public function startRecording(): void
  273. {
  274. $this->hasToRecord = true;
  275. }
  276. public function stopRecording(): void
  277. {
  278. $this->hasToRecord = false;
  279. }
  280. public function clearRecorded(): void
  281. {
  282. $this->accumulator = '';
  283. }
  284. }