SieveSemantics.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <?php namespace Sieve;
  2. require_once('SieveKeywordRegistry.php');
  3. require_once('SieveToken.php');
  4. require_once('SieveException.php');
  5. class SieveSemantics
  6. {
  7. protected static $requiredExtensions_ = array();
  8. protected $comparator_;
  9. protected $matchType_;
  10. protected $addressPart_;
  11. protected $tags_ = array();
  12. protected $arguments_;
  13. protected $deps_ = array();
  14. protected $followupToken_;
  15. public function __construct($token, $prevToken)
  16. {
  17. $this->registry_ = SieveKeywordRegistry::get();
  18. $command = strtolower($token->text);
  19. // Check the registry for $command
  20. if ($this->registry_->isCommand($command))
  21. {
  22. $xml = $this->registry_->command($command);
  23. $this->arguments_ = $this->makeArguments_($xml);
  24. $this->followupToken_ = SieveToken::Semicolon;
  25. }
  26. else if ($this->registry_->isTest($command))
  27. {
  28. $xml = $this->registry_->test($command);
  29. $this->arguments_ = $this->makeArguments_($xml);
  30. $this->followupToken_ = SieveToken::BlockStart;
  31. }
  32. else
  33. {
  34. throw new SieveException($token, 'unknown command '. $command);
  35. }
  36. // Check if command may appear at this position within the script
  37. if ($this->registry_->isTest($command))
  38. {
  39. if (is_null($prevToken))
  40. throw new SieveException($token, $command .' may not appear as first command');
  41. if (!preg_match('/^(if|elsif|anyof|allof|not)$/i', $prevToken->text))
  42. throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
  43. }
  44. else if (isset($prevToken))
  45. {
  46. switch ($command)
  47. {
  48. case 'require':
  49. $valid_after = 'require';
  50. break;
  51. case 'elsif':
  52. case 'else':
  53. $valid_after = '(if|elsif)';
  54. break;
  55. default:
  56. $valid_after = $this->commandsRegex_();
  57. }
  58. if (!preg_match('/^'. $valid_after .'$/i', $prevToken->text))
  59. throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
  60. }
  61. // Check for extension arguments to add to the command
  62. foreach ($this->registry_->arguments($command) as $arg)
  63. {
  64. switch ((string) $arg['type'])
  65. {
  66. case 'tag':
  67. array_unshift($this->arguments_, array(
  68. 'type' => SieveToken::Tag,
  69. 'occurrence' => $this->occurrence_($arg),
  70. 'regex' => $this->regex_($arg),
  71. 'call' => 'tagHook_',
  72. 'name' => $this->name_($arg),
  73. 'subArgs' => $this->makeArguments_($arg->children())
  74. ));
  75. break;
  76. }
  77. }
  78. }
  79. public function __destruct()
  80. {
  81. $this->registry_->put();
  82. }
  83. // TODO: the *Regex functions could possibly also be static properties
  84. protected function requireStringsRegex_()
  85. {
  86. return '('. implode('|', $this->registry_->requireStrings()) .')';
  87. }
  88. protected function matchTypeRegex_()
  89. {
  90. return '('. implode('|', $this->registry_->matchTypes()) .')';
  91. }
  92. protected function addressPartRegex_()
  93. {
  94. return '('. implode('|', $this->registry_->addressParts()) .')';
  95. }
  96. protected function commandsRegex_()
  97. {
  98. return '('. implode('|', $this->registry_->commands()) .')';
  99. }
  100. protected function testsRegex_()
  101. {
  102. return '('. implode('|', $this->registry_->tests()) .')';
  103. }
  104. protected function comparatorRegex_()
  105. {
  106. return '('. implode('|', $this->registry_->comparators()) .')';
  107. }
  108. protected function occurrence_($arg)
  109. {
  110. if (isset($arg['occurrence']))
  111. {
  112. switch ((string) $arg['occurrence'])
  113. {
  114. case 'optional':
  115. return '?';
  116. case 'any':
  117. return '*';
  118. case 'some':
  119. return '+';
  120. }
  121. }
  122. return '1';
  123. }
  124. protected function name_($arg)
  125. {
  126. if (isset($arg['name']))
  127. {
  128. return (string) $arg['name'];
  129. }
  130. return (string) $arg['type'];
  131. }
  132. protected function regex_($arg)
  133. {
  134. if (isset($arg['regex']))
  135. {
  136. return (string) $arg['regex'];
  137. }
  138. return '.*';
  139. }
  140. protected function case_($arg)
  141. {
  142. if (isset($arg['case']))
  143. {
  144. return (string) $arg['case'];
  145. }
  146. return 'adhere';
  147. }
  148. protected function follows_($arg)
  149. {
  150. if (isset($arg['follows']))
  151. {
  152. return (string) $arg['follows'];
  153. }
  154. return '.*';
  155. }
  156. protected function makeValue_($arg)
  157. {
  158. if (isset($arg->value))
  159. {
  160. $res = $this->makeArguments_($arg->value);
  161. return array_shift($res);
  162. }
  163. return null;
  164. }
  165. /**
  166. * Convert an extension (test) commands parameters from XML to
  167. * a PHP array the {@see Semantics} class understands.
  168. * @param array(SimpleXMLElement) $parameters
  169. * @return array
  170. */
  171. protected function makeArguments_($parameters)
  172. {
  173. $arguments = array();
  174. foreach ($parameters as $arg)
  175. {
  176. // Ignore anything not a <parameter>
  177. if ($arg->getName() != 'parameter')
  178. continue;
  179. switch ((string) $arg['type'])
  180. {
  181. case 'addresspart':
  182. array_push($arguments, array(
  183. 'type' => SieveToken::Tag,
  184. 'occurrence' => $this->occurrence_($arg),
  185. 'regex' => $this->addressPartRegex_(),
  186. 'call' => 'addressPartHook_',
  187. 'name' => 'address part',
  188. 'subArgs' => $this->makeArguments_($arg)
  189. ));
  190. break;
  191. case 'block':
  192. array_push($arguments, array(
  193. 'type' => SieveToken::BlockStart,
  194. 'occurrence' => '1',
  195. 'regex' => '{',
  196. 'name' => 'block',
  197. 'subArgs' => $this->makeArguments_($arg)
  198. ));
  199. break;
  200. case 'comparator':
  201. array_push($arguments, array(
  202. 'type' => SieveToken::Tag,
  203. 'occurrence' => $this->occurrence_($arg),
  204. 'regex' => 'comparator',
  205. 'name' => 'comparator',
  206. 'subArgs' => array( array(
  207. 'type' => SieveToken::String,
  208. 'occurrence' => '1',
  209. 'call' => 'comparatorHook_',
  210. 'case' => 'adhere',
  211. 'regex' => $this->comparatorRegex_(),
  212. 'name' => 'comparator string',
  213. 'follows' => 'comparator'
  214. ))
  215. ));
  216. break;
  217. case 'matchtype':
  218. array_push($arguments, array(
  219. 'type' => SieveToken::Tag,
  220. 'occurrence' => $this->occurrence_($arg),
  221. 'regex' => $this->matchTypeRegex_(),
  222. 'call' => 'matchTypeHook_',
  223. 'name' => 'match type',
  224. 'subArgs' => $this->makeArguments_($arg)
  225. ));
  226. break;
  227. case 'number':
  228. array_push($arguments, array(
  229. 'type' => SieveToken::Number,
  230. 'occurrence' => $this->occurrence_($arg),
  231. 'regex' => $this->regex_($arg),
  232. 'name' => $this->name_($arg),
  233. 'follows' => $this->follows_($arg)
  234. ));
  235. break;
  236. case 'requirestrings':
  237. array_push($arguments, array(
  238. 'type' => SieveToken::StringList,
  239. 'occurrence' => $this->occurrence_($arg),
  240. 'call' => 'setRequire_',
  241. 'case' => 'adhere',
  242. 'regex' => $this->requireStringsRegex_(),
  243. 'name' => $this->name_($arg)
  244. ));
  245. break;
  246. case 'string':
  247. array_push($arguments, array(
  248. 'type' => SieveToken::String,
  249. 'occurrence' => $this->occurrence_($arg),
  250. 'regex' => $this->regex_($arg),
  251. 'case' => $this->case_($arg),
  252. 'name' => $this->name_($arg),
  253. 'follows' => $this->follows_($arg)
  254. ));
  255. break;
  256. case 'stringlist':
  257. array_push($arguments, array(
  258. 'type' => SieveToken::StringList,
  259. 'occurrence' => $this->occurrence_($arg),
  260. 'regex' => $this->regex_($arg),
  261. 'case' => $this->case_($arg),
  262. 'name' => $this->name_($arg),
  263. 'follows' => $this->follows_($arg)
  264. ));
  265. break;
  266. case 'tag':
  267. array_push($arguments, array(
  268. 'type' => SieveToken::Tag,
  269. 'occurrence' => $this->occurrence_($arg),
  270. 'regex' => $this->regex_($arg),
  271. 'call' => 'tagHook_',
  272. 'name' => $this->name_($arg),
  273. 'subArgs' => $this->makeArguments_($arg->children()),
  274. 'follows' => $this->follows_($arg)
  275. ));
  276. break;
  277. case 'test':
  278. array_push($arguments, array(
  279. 'type' => SieveToken::Identifier,
  280. 'occurrence' => $this->occurrence_($arg),
  281. 'regex' => $this->testsRegex_(),
  282. 'name' => $this->name_($arg),
  283. 'subArgs' => $this->makeArguments_($arg->children())
  284. ));
  285. break;
  286. case 'testlist':
  287. array_push($arguments, array(
  288. 'type' => SieveToken::LeftParenthesis,
  289. 'occurrence' => '1',
  290. 'regex' => '\(',
  291. 'name' => $this->name_($arg),
  292. 'subArgs' => null
  293. ));
  294. array_push($arguments, array(
  295. 'type' => SieveToken::Identifier,
  296. 'occurrence' => '+',
  297. 'regex' => $this->testsRegex_(),
  298. 'name' => $this->name_($arg),
  299. 'subArgs' => $this->makeArguments_($arg->children())
  300. ));
  301. break;
  302. }
  303. }
  304. return $arguments;
  305. }
  306. /**
  307. * Add argument(s) expected / allowed to appear next.
  308. * @param array $value
  309. */
  310. protected function addArguments_($identifier, $subArgs)
  311. {
  312. for ($i = count($subArgs); $i > 0; $i--)
  313. {
  314. $arg = $subArgs[$i-1];
  315. if (preg_match('/^'. $arg['follows'] .'$/si', $identifier))
  316. array_unshift($this->arguments_, $arg);
  317. }
  318. }
  319. /**
  320. * Add dependency that is expected to be fullfilled when parsing
  321. * of the current command is {@see done}.
  322. * @param array $dependency
  323. */
  324. protected function addDependency_($type, $name, $dependencies)
  325. {
  326. foreach ($dependencies as $d)
  327. {
  328. array_push($this->deps_, array(
  329. 'o_type' => $type,
  330. 'o_name' => $name,
  331. 'type' => $d['type'],
  332. 'name' => $d['name'],
  333. 'regex' => $d['regex']
  334. ));
  335. }
  336. }
  337. protected function invoke_($token, $func, $arg = array())
  338. {
  339. if (!is_array($arg))
  340. $arg = array($arg);
  341. $err = call_user_func_array(array(&$this, $func), $arg);
  342. if ($err)
  343. throw new SieveException($token, $err);
  344. }
  345. protected function setRequire_($extension)
  346. {
  347. array_push(self::$requiredExtensions_, $extension);
  348. $this->registry_->activate($extension);
  349. }
  350. /**
  351. * Hook function that is called after a address part match was found
  352. * in a command. The kind of address part is remembered in case it's
  353. * needed later {@see done}. For address parts from a extension
  354. * dependency information and valid values are looked up as well.
  355. * @param string $addresspart
  356. */
  357. protected function addressPartHook_($addresspart)
  358. {
  359. $this->addressPart_ = $addresspart;
  360. $xml = $this->registry_->addresspart($this->addressPart_);
  361. if (isset($xml))
  362. {
  363. // Add possible value and dependancy
  364. $this->addArguments_($this->addressPart_, $this->makeArguments_($xml));
  365. $this->addDependency_('address part', $this->addressPart_, $xml->requires);
  366. }
  367. }
  368. /**
  369. * Hook function that is called after a match type was found in a
  370. * command. The kind of match type is remembered in case it's
  371. * needed later {@see done}. For a match type from extensions
  372. * dependency information and valid values are looked up as well.
  373. * @param string $matchtype
  374. */
  375. protected function matchTypeHook_($matchtype)
  376. {
  377. $this->matchType_ = $matchtype;
  378. $xml = $this->registry_->matchtype($this->matchType_);
  379. if (isset($xml))
  380. {
  381. // Add possible value and dependancy
  382. $this->addArguments_($this->matchType_, $this->makeArguments_($xml));
  383. $this->addDependency_('match type', $this->matchType_, $xml->requires);
  384. }
  385. }
  386. /**
  387. * Hook function that is called after a comparator was found in
  388. * a command. The comparator is remembered in case it's needed for
  389. * comparsion later {@see done}. For a comparator from extensions
  390. * dependency information is looked up as well.
  391. * @param string $comparator
  392. */
  393. protected function comparatorHook_($comparator)
  394. {
  395. $this->comparator_ = $comparator;
  396. $xml = $this->registry_->comparator($this->comparator_);
  397. if (isset($xml))
  398. {
  399. // Add possible dependancy
  400. $this->addDependency_('comparator', $this->comparator_, $xml->requires);
  401. }
  402. }
  403. /**
  404. * Hook function that is called after a tag was found in
  405. * a command. The tag is remembered in case it's needed for
  406. * comparsion later {@see done}. For a tags from extensions
  407. * dependency information is looked up as well.
  408. * @param string $tag
  409. */
  410. protected function tagHook_($tag)
  411. {
  412. array_push($this->tags_, $tag);
  413. $xml = $this->registry_->argument($tag);
  414. // Add possible dependancies
  415. if (isset($xml))
  416. $this->addDependency_('tag', $tag, $xml->requires);
  417. }
  418. protected function validType_($token)
  419. {
  420. foreach ($this->arguments_ as $arg)
  421. {
  422. if ($arg['occurrence'] == '0')
  423. {
  424. array_shift($this->arguments_);
  425. continue;
  426. }
  427. if ($token->is($arg['type']))
  428. return;
  429. // Is the argument required
  430. if ($arg['occurrence'] != '?' && $arg['occurrence'] != '*')
  431. throw new SieveException($token, $arg['type']);
  432. array_shift($this->arguments_);
  433. }
  434. // Check if command expects any (more) arguments
  435. if (empty($this->arguments_))
  436. throw new SieveException($token, $this->followupToken_);
  437. throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
  438. }
  439. public function startStringList($token)
  440. {
  441. $this->validType_($token);
  442. $this->arguments_[0]['type'] = SieveToken::String;
  443. $this->arguments_[0]['occurrence'] = '+';
  444. }
  445. public function continueStringList()
  446. {
  447. $this->arguments_[0]['occurrence'] = '+';
  448. }
  449. public function endStringList()
  450. {
  451. array_shift($this->arguments_);
  452. }
  453. public function validateToken($token)
  454. {
  455. // Make sure the argument has a valid type
  456. $this->validType_($token);
  457. foreach ($this->arguments_ as &$arg)
  458. {
  459. // Build regular expression according to argument type
  460. switch ($arg['type'])
  461. {
  462. case SieveToken::String:
  463. case SieveToken::StringList:
  464. $regex = '/^(?:text:[^\n]*\n(?P<one>'. $arg['regex'] .')\.\r?\n?|"(?P<two>'. $arg['regex'] .')")$/'
  465. . ($arg['case'] == 'ignore' ? 'si' : 's');
  466. break;
  467. case SieveToken::Tag:
  468. $regex = '/^:(?P<one>'. $arg['regex'] .')$/si';
  469. break;
  470. default:
  471. $regex = '/^(?P<one>'. $arg['regex'] .')$/si';
  472. }
  473. if (preg_match($regex, $token->text, $match))
  474. {
  475. $text = ($match['one'] ? $match['one'] : $match['two']);
  476. // Add argument(s) that may now appear after this one
  477. if (isset($arg['subArgs']))
  478. $this->addArguments_($text, $arg['subArgs']);
  479. // Call extra processing function if defined
  480. if (isset($arg['call']))
  481. $this->invoke_($token, $arg['call'], $text);
  482. // Check if a possible value of this argument may occur
  483. if ($arg['occurrence'] == '?' || $arg['occurrence'] == '1')
  484. {
  485. $arg['occurrence'] = '0';
  486. }
  487. else if ($arg['occurrence'] == '+')
  488. {
  489. $arg['occurrence'] = '*';
  490. }
  491. return;
  492. }
  493. if ($token->is($arg['type']) && $arg['occurrence'] == 1)
  494. {
  495. throw new SieveException($token,
  496. SieveToken::typeString($token->type) ." $token->text where ". $arg['name'] .' expected');
  497. }
  498. }
  499. throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
  500. }
  501. public function done($token)
  502. {
  503. // Check if there are required arguments left
  504. foreach ($this->arguments_ as $arg)
  505. {
  506. if ($arg['occurrence'] == '+' || $arg['occurrence'] == '1')
  507. throw new SieveException($token, $arg['type']);
  508. }
  509. // Check if the command depends on use of a certain tag
  510. foreach ($this->deps_ as $d)
  511. {
  512. switch ($d['type'])
  513. {
  514. case 'addresspart':
  515. $values = array($this->addressPart_);
  516. break;
  517. case 'matchtype':
  518. $values = array($this->matchType_);
  519. break;
  520. case 'comparator':
  521. $values = array($this->comparator_);
  522. break;
  523. case 'tag':
  524. $values = $this->tags_;
  525. break;
  526. }
  527. foreach ($values as $value)
  528. {
  529. if (preg_match('/^'. $d['regex'] .'$/mi', $value))
  530. break 2;
  531. }
  532. throw new SieveException($token,
  533. $d['o_type'] .' '. $d['o_name'] .' requires use of '. $d['type'] .' '. $d['name']);
  534. }
  535. }
  536. }