mailcow/data/web/inc/lib/sieve/SieveSemantics.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']);
}
}
}