612 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			612 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?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']);
 | |
|         }
 | |
|     }
 | |
| }
 | 
