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']);
|
|
}
|
|
}
|
|
}
|