| <?php namespace Sieve; |
| |
| require_once('SieveKeywordRegistry.php'); |
| require_once('SieveToken.php'); |
| require_once('SieveException.php'); |
| |
| class SieveSemantics |
| { |
| protected static $requiredExtensions_ = array(); |
| |
| protected $comparator_; |
| protected $matchType_; |
| protected $addressPart_; |
| protected $tags_ = array(); |
| protected $arguments_; |
| protected $deps_ = array(); |
| protected $followupToken_; |
| |
| public function __construct($token, $prevToken) |
| { |
| $this->registry_ = SieveKeywordRegistry::get(); |
| $command = strtolower($token->text); |
| |
| // Check the registry for $command |
| if ($this->registry_->isCommand($command)) |
| { |
| $xml = $this->registry_->command($command); |
| $this->arguments_ = $this->makeArguments_($xml); |
| $this->followupToken_ = SieveToken::Semicolon; |
| } |
| else if ($this->registry_->isTest($command)) |
| { |
| $xml = $this->registry_->test($command); |
| $this->arguments_ = $this->makeArguments_($xml); |
| $this->followupToken_ = SieveToken::BlockStart; |
| } |
| else |
| { |
| throw new SieveException($token, 'unknown command '. $command); |
| } |
| |
| // Check if command may appear at this position within the script |
| if ($this->registry_->isTest($command)) |
| { |
| if (is_null($prevToken)) |
| throw new SieveException($token, $command .' may not appear as first command'); |
| |
| if (!preg_match('/^(if|elsif|anyof|allof|not)$/i', $prevToken->text)) |
| throw new SieveException($token, $command .' may not appear after '. $prevToken->text); |
| } |
| else if (isset($prevToken)) |
| { |
| switch ($command) |
| { |
| case 'require': |
| $valid_after = 'require'; |
| break; |
| case 'elsif': |
| case 'else': |
| $valid_after = '(if|elsif)'; |
| break; |
| default: |
| $valid_after = $this->commandsRegex_(); |
| } |
| |
| if (!preg_match('/^'. $valid_after .'$/i', $prevToken->text)) |
| throw new SieveException($token, $command .' may not appear after '. $prevToken->text); |
| } |
| |
| // Check for extension arguments to add to the command |
| foreach ($this->registry_->arguments($command) as $arg) |
| { |
| switch ((string) $arg['type']) |
| { |
| case 'tag': |
| array_unshift($this->arguments_, array( |
| 'type' => SieveToken::Tag, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => $this->regex_($arg), |
| 'call' => 'tagHook_', |
| 'name' => $this->name_($arg), |
| 'subArgs' => $this->makeArguments_($arg->children()) |
| )); |
| break; |
| } |
| } |
| } |
| |
| public function __destruct() |
| { |
| $this->registry_->put(); |
| } |
| |
| // TODO: the *Regex functions could possibly also be static properties |
| protected function requireStringsRegex_() |
| { |
| return '('. implode('|', $this->registry_->requireStrings()) .')'; |
| } |
| |
| protected function matchTypeRegex_() |
| { |
| return '('. implode('|', $this->registry_->matchTypes()) .')'; |
| } |
| |
| protected function addressPartRegex_() |
| { |
| return '('. implode('|', $this->registry_->addressParts()) .')'; |
| } |
| |
| protected function commandsRegex_() |
| { |
| return '('. implode('|', $this->registry_->commands()) .')'; |
| } |
| |
| protected function testsRegex_() |
| { |
| return '('. implode('|', $this->registry_->tests()) .')'; |
| } |
| |
| protected function comparatorRegex_() |
| { |
| return '('. implode('|', $this->registry_->comparators()) .')'; |
| } |
| |
| protected function occurrence_($arg) |
| { |
| if (isset($arg['occurrence'])) |
| { |
| switch ((string) $arg['occurrence']) |
| { |
| case 'optional': |
| return '?'; |
| case 'any': |
| return '*'; |
| case 'some': |
| return '+'; |
| } |
| } |
| return '1'; |
| } |
| |
| protected function name_($arg) |
| { |
| if (isset($arg['name'])) |
| { |
| return (string) $arg['name']; |
| } |
| return (string) $arg['type']; |
| } |
| |
| protected function regex_($arg) |
| { |
| if (isset($arg['regex'])) |
| { |
| return (string) $arg['regex']; |
| } |
| return '.*'; |
| } |
| |
| protected function case_($arg) |
| { |
| if (isset($arg['case'])) |
| { |
| return (string) $arg['case']; |
| } |
| return 'adhere'; |
| } |
| |
| protected function follows_($arg) |
| { |
| if (isset($arg['follows'])) |
| { |
| return (string) $arg['follows']; |
| } |
| return '.*'; |
| } |
| |
| protected function makeValue_($arg) |
| { |
| if (isset($arg->value)) |
| { |
| $res = $this->makeArguments_($arg->value); |
| return array_shift($res); |
| } |
| return null; |
| } |
| |
| /** |
| * Convert an extension (test) commands parameters from XML to |
| * a PHP array the {@see Semantics} class understands. |
| * @param array(SimpleXMLElement) $parameters |
| * @return array |
| */ |
| protected function makeArguments_($parameters) |
| { |
| $arguments = array(); |
| |
| foreach ($parameters as $arg) |
| { |
| // Ignore anything not a <parameter> |
| if ($arg->getName() != 'parameter') |
| continue; |
| |
| switch ((string) $arg['type']) |
| { |
| case 'addresspart': |
| array_push($arguments, array( |
| 'type' => SieveToken::Tag, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => $this->addressPartRegex_(), |
| 'call' => 'addressPartHook_', |
| 'name' => 'address part', |
| 'subArgs' => $this->makeArguments_($arg) |
| )); |
| break; |
| |
| case 'block': |
| array_push($arguments, array( |
| 'type' => SieveToken::BlockStart, |
| 'occurrence' => '1', |
| 'regex' => '{', |
| 'name' => 'block', |
| 'subArgs' => $this->makeArguments_($arg) |
| )); |
| break; |
| |
| case 'comparator': |
| array_push($arguments, array( |
| 'type' => SieveToken::Tag, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => 'comparator', |
| 'name' => 'comparator', |
| 'subArgs' => array( array( |
| 'type' => SieveToken::String, |
| 'occurrence' => '1', |
| 'call' => 'comparatorHook_', |
| 'case' => 'adhere', |
| 'regex' => $this->comparatorRegex_(), |
| 'name' => 'comparator string', |
| 'follows' => 'comparator' |
| )) |
| )); |
| break; |
| |
| case 'matchtype': |
| array_push($arguments, array( |
| 'type' => SieveToken::Tag, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => $this->matchTypeRegex_(), |
| 'call' => 'matchTypeHook_', |
| 'name' => 'match type', |
| 'subArgs' => $this->makeArguments_($arg) |
| )); |
| break; |
| |
| case 'number': |
| array_push($arguments, array( |
| 'type' => SieveToken::Number, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => $this->regex_($arg), |
| 'name' => $this->name_($arg), |
| 'follows' => $this->follows_($arg) |
| )); |
| break; |
| |
| case 'requirestrings': |
| array_push($arguments, array( |
| 'type' => SieveToken::StringList, |
| 'occurrence' => $this->occurrence_($arg), |
| 'call' => 'setRequire_', |
| 'case' => 'adhere', |
| 'regex' => $this->requireStringsRegex_(), |
| 'name' => $this->name_($arg) |
| )); |
| break; |
| |
| case 'string': |
| array_push($arguments, array( |
| 'type' => SieveToken::String, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => $this->regex_($arg), |
| 'case' => $this->case_($arg), |
| 'name' => $this->name_($arg), |
| 'follows' => $this->follows_($arg) |
| )); |
| break; |
| |
| case 'stringlist': |
| array_push($arguments, array( |
| 'type' => SieveToken::StringList, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => $this->regex_($arg), |
| 'case' => $this->case_($arg), |
| 'name' => $this->name_($arg), |
| 'follows' => $this->follows_($arg) |
| )); |
| break; |
| |
| case 'tag': |
| array_push($arguments, array( |
| 'type' => SieveToken::Tag, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => $this->regex_($arg), |
| 'call' => 'tagHook_', |
| 'name' => $this->name_($arg), |
| 'subArgs' => $this->makeArguments_($arg->children()), |
| 'follows' => $this->follows_($arg) |
| )); |
| break; |
| |
| case 'test': |
| array_push($arguments, array( |
| 'type' => SieveToken::Identifier, |
| 'occurrence' => $this->occurrence_($arg), |
| 'regex' => $this->testsRegex_(), |
| 'name' => $this->name_($arg), |
| 'subArgs' => $this->makeArguments_($arg->children()) |
| )); |
| break; |
| |
| case 'testlist': |
| array_push($arguments, array( |
| 'type' => SieveToken::LeftParenthesis, |
| 'occurrence' => '1', |
| 'regex' => '\(', |
| 'name' => $this->name_($arg), |
| 'subArgs' => null |
| )); |
| array_push($arguments, array( |
| 'type' => SieveToken::Identifier, |
| 'occurrence' => '+', |
| 'regex' => $this->testsRegex_(), |
| 'name' => $this->name_($arg), |
| 'subArgs' => $this->makeArguments_($arg->children()) |
| )); |
| break; |
| } |
| } |
| |
| return $arguments; |
| } |
| |
| /** |
| * Add argument(s) expected / allowed to appear next. |
| * @param array $value |
| */ |
| protected function addArguments_($identifier, $subArgs) |
| { |
| for ($i = count($subArgs); $i > 0; $i--) |
| { |
| $arg = $subArgs[$i-1]; |
| if (preg_match('/^'. $arg['follows'] .'$/si', $identifier)) |
| array_unshift($this->arguments_, $arg); |
| } |
| } |
| |
| /** |
| * Add dependency that is expected to be fullfilled when parsing |
| * of the current command is {@see done}. |
| * @param array $dependency |
| */ |
| protected function addDependency_($type, $name, $dependencies) |
| { |
| foreach ($dependencies as $d) |
| { |
| array_push($this->deps_, array( |
| 'o_type' => $type, |
| 'o_name' => $name, |
| 'type' => $d['type'], |
| 'name' => $d['name'], |
| 'regex' => $d['regex'] |
| )); |
| } |
| } |
| |
| protected function invoke_($token, $func, $arg = array()) |
| { |
| if (!is_array($arg)) |
| $arg = array($arg); |
| |
| $err = call_user_func_array(array(&$this, $func), $arg); |
| |
| if ($err) |
| throw new SieveException($token, $err); |
| } |
| |
| protected function setRequire_($extension) |
| { |
| array_push(self::$requiredExtensions_, $extension); |
| $this->registry_->activate($extension); |
| } |
| |
| /** |
| * Hook function that is called after a address part match was found |
| * in a command. The kind of address part is remembered in case it's |
| * needed later {@see done}. For address parts from a extension |
| * dependency information and valid values are looked up as well. |
| * @param string $addresspart |
| */ |
| protected function addressPartHook_($addresspart) |
| { |
| $this->addressPart_ = $addresspart; |
| $xml = $this->registry_->addresspart($this->addressPart_); |
| |
| if (isset($xml)) |
| { |
| // Add possible value and dependancy |
| $this->addArguments_($this->addressPart_, $this->makeArguments_($xml)); |
| $this->addDependency_('address part', $this->addressPart_, $xml->requires); |
| } |
| } |
| |
| /** |
| * Hook function that is called after a match type was found in a |
| * command. The kind of match type is remembered in case it's |
| * needed later {@see done}. For a match type from extensions |
| * dependency information and valid values are looked up as well. |
| * @param string $matchtype |
| */ |
| protected function matchTypeHook_($matchtype) |
| { |
| $this->matchType_ = $matchtype; |
| $xml = $this->registry_->matchtype($this->matchType_); |
| |
| if (isset($xml)) |
| { |
| // Add possible value and dependancy |
| $this->addArguments_($this->matchType_, $this->makeArguments_($xml)); |
| $this->addDependency_('match type', $this->matchType_, $xml->requires); |
| } |
| } |
| |
| /** |
| * Hook function that is called after a comparator was found in |
| * a command. The comparator is remembered in case it's needed for |
| * comparsion later {@see done}. For a comparator from extensions |
| * dependency information is looked up as well. |
| * @param string $comparator |
| */ |
| protected function comparatorHook_($comparator) |
| { |
| $this->comparator_ = $comparator; |
| $xml = $this->registry_->comparator($this->comparator_); |
| |
| if (isset($xml)) |
| { |
| // Add possible dependancy |
| $this->addDependency_('comparator', $this->comparator_, $xml->requires); |
| } |
| } |
| |
| /** |
| * Hook function that is called after a tag was found in |
| * a command. The tag is remembered in case it's needed for |
| * comparsion later {@see done}. For a tags from extensions |
| * dependency information is looked up as well. |
| * @param string $tag |
| */ |
| protected function tagHook_($tag) |
| { |
| array_push($this->tags_, $tag); |
| $xml = $this->registry_->argument($tag); |
| |
| // Add possible dependancies |
| if (isset($xml)) |
| $this->addDependency_('tag', $tag, $xml->requires); |
| } |
| |
| protected function validType_($token) |
| { |
| foreach ($this->arguments_ as $arg) |
| { |
| if ($arg['occurrence'] == '0') |
| { |
| array_shift($this->arguments_); |
| continue; |
| } |
| |
| if ($token->is($arg['type'])) |
| return; |
| |
| // Is the argument required |
| if ($arg['occurrence'] != '?' && $arg['occurrence'] != '*') |
| throw new SieveException($token, $arg['type']); |
| |
| array_shift($this->arguments_); |
| } |
| |
| // Check if command expects any (more) arguments |
| if (empty($this->arguments_)) |
| throw new SieveException($token, $this->followupToken_); |
| |
| throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text); |
| } |
| |
| public function startStringList($token) |
| { |
| $this->validType_($token); |
| $this->arguments_[0]['type'] = SieveToken::String; |
| $this->arguments_[0]['occurrence'] = '+'; |
| } |
| |
| public function continueStringList() |
| { |
| $this->arguments_[0]['occurrence'] = '+'; |
| } |
| |
| public function endStringList() |
| { |
| array_shift($this->arguments_); |
| } |
| |
| public function validateToken($token) |
| { |
| // Make sure the argument has a valid type |
| $this->validType_($token); |
| |
| foreach ($this->arguments_ as &$arg) |
| { |
| // Build regular expression according to argument type |
| switch ($arg['type']) |
| { |
| case SieveToken::String: |
| case SieveToken::StringList: |
| $regex = '/^(?:text:[^\n]*\n(?P<one>'. $arg['regex'] .')\.\r?\n?|"(?P<two>'. $arg['regex'] .')")$/' |
| . ($arg['case'] == 'ignore' ? 'si' : 's'); |
| break; |
| case SieveToken::Tag: |
| $regex = '/^:(?P<one>'. $arg['regex'] .')$/si'; |
| break; |
| default: |
| $regex = '/^(?P<one>'. $arg['regex'] .')$/si'; |
| } |
| |
| if (preg_match($regex, $token->text, $match)) |
| { |
| $text = ($match['one'] ? $match['one'] : $match['two']); |
| |
| // Add argument(s) that may now appear after this one |
| if (isset($arg['subArgs'])) |
| $this->addArguments_($text, $arg['subArgs']); |
| |
| // Call extra processing function if defined |
| if (isset($arg['call'])) |
| $this->invoke_($token, $arg['call'], $text); |
| |
| // Check if a possible value of this argument may occur |
| if ($arg['occurrence'] == '?' || $arg['occurrence'] == '1') |
| { |
| $arg['occurrence'] = '0'; |
| } |
| else if ($arg['occurrence'] == '+') |
| { |
| $arg['occurrence'] = '*'; |
| } |
| |
| return; |
| } |
| |
| if ($token->is($arg['type']) && $arg['occurrence'] == 1) |
| { |
| throw new SieveException($token, |
| SieveToken::typeString($token->type) ." $token->text where ". $arg['name'] .' expected'); |
| } |
| } |
| |
| throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text); |
| } |
| |
| public function done($token) |
| { |
| // Check if there are required arguments left |
| foreach ($this->arguments_ as $arg) |
| { |
| if ($arg['occurrence'] == '+' || $arg['occurrence'] == '1') |
| throw new SieveException($token, $arg['type']); |
| } |
| |
| // Check if the command depends on use of a certain tag |
| foreach ($this->deps_ as $d) |
| { |
| switch ($d['type']) |
| { |
| case 'addresspart': |
| $values = array($this->addressPart_); |
| break; |
| |
| case 'matchtype': |
| $values = array($this->matchType_); |
| break; |
| |
| case 'comparator': |
| $values = array($this->comparator_); |
| break; |
| |
| case 'tag': |
| $values = $this->tags_; |
| break; |
| } |
| |
| foreach ($values as $value) |
| { |
| if (preg_match('/^'. $d['regex'] .'$/mi', $value)) |
| break 2; |
| } |
| |
| throw new SieveException($token, |
| $d['o_type'] .' '. $d['o_name'] .' requires use of '. $d['type'] .' '. $d['name']); |
| } |
| } |
| } |