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