Files
.github
data
Dockerfiles
assets
conf
web
css
fonts
img
inc
ajax
lib
sieve
extensions
SieveDumpable.php
SieveException.php
SieveKeywordRegistry.php
SieveParser.php
SieveScanner.php
SieveScript.php
SieveSemantics.php
SieveToken.php
SieveTree.php
keywords.xml
vendor
Yubico.php
composer.json
composer.lock
footer.inc.php
functions.acl.inc.php
functions.address_rewriting.inc.php
functions.admin.inc.php
functions.customize.inc.php
functions.dkim.inc.php
functions.docker.inc.php
functions.domain_admin.inc.php
functions.fail2ban.inc.php
functions.fwdhost.inc.php
functions.inc.php
functions.mailbox.inc.php
functions.mailq.inc.php
functions.policy.inc.php
functions.quarantine.inc.php
functions.ratelimit.inc.php
functions.relayhost.inc.php
functions.rsettings.inc.php
functions.tls_policy_maps.inc.php
header.inc.php
init_db.inc.php
languages.min.css
languages.png
prerequisites.inc.php
sessions.inc.php
spf.inc.php
triggers.inc.php
vars.inc.php
js
lang
modals
admin.php
autoconfig.php
autodiscover-json.php
autodiscover.php
debug.php
edit.php
favicon.png
index.php
json_api.php
mailbox.php
mobileconfig.php
quarantine.php
robots.txt
user.php
helper-scripts
.gitignore
.travis.yml
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
README.md
docker-compose.yml
generate_config.sh
update.sh
mailcow-dockerized/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']);
}
}
}