source: subversion/trunk/plugins/managesieve/lib/rcube_sieve_script.php @ 5441

Last change on this file since 5441 was 5441, checked in by alec, 18 months ago
  • Added 'address' and 'envelope' tests support
  • Added 'body' extension support (RFC5173)
  • Added 'subaddress' extension support (RFC5233)
  • Added comparators support
  • Changed Sender/Recipient? labels to From/To?
  • Fixed importing rule names from Ingo
  • Fixed handling of extensions disabled in config
  • Property svn:keywords set to Date Author Id Revision
File size: 35.4 KB
Line 
1<?php
2
3/**
4 *  Class for operations on Sieve scripts
5 *
6 * Copyright (C) 2008-2011, The Roundcube Dev Team
7 * Copyright (C) 2011, Kolab Systems AG
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License version 2
11 * as published by the Free Software Foundation.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 *
22 * $Id$
23 *
24 */
25
26class rcube_sieve_script
27{
28    public $content = array();      // script rules array
29
30    private $vars = array();        // "global" variables
31    private $prefix = '';           // script header (comments)
32    private $capabilities = array(); // Sieve extensions supported by server
33    private $supported = array(     // Sieve extensions supported by class
34        'fileinto',                 // RFC5228
35        'envelope',                 // RFC5228
36        'reject',                   // RFC5429
37        'ereject',                  // RFC5429
38        'copy',                     // RFC3894
39        'vacation',                 // RFC5230
40        'relational',               // RFC3431
41        'regex',                    // draft-ietf-sieve-regex-01
42        'imapflags',                // draft-melnikov-sieve-imapflags-06
43        'imap4flags',               // RFC5232
44        'include',                  // draft-ietf-sieve-include-12
45        'variables',                // RFC5229
46        'body',                     // RFC5173
47        'subaddress',               // RFC5233
48        // @TODO: enotify/notify, spamtest+virustest, mailbox, date
49    );
50
51    /**
52     * Object constructor
53     *
54     * @param  string  Script's text content
55     * @param  array   List of capabilities supported by server
56     */
57    public function __construct($script, $capabilities=array())
58    {
59        $this->capabilities = array_map('strtolower', (array) $capabilities);
60
61        // disable features by server capabilities
62        if (!empty($this->capabilities)) {
63            foreach ($this->supported as $idx => $ext) {
64                if (!in_array($ext, $this->capabilities)) {
65                    unset($this->supported[$idx]);
66                }
67            }
68        }
69
70        // Parse text content of the script
71        $this->_parse_text($script);
72    }
73
74    /**
75     * Adds rule to the script (at the end)
76     *
77     * @param string Rule name
78     * @param array  Rule content (as array)
79     *
80     * @return int The index of the new rule
81     */
82    public function add_rule($content)
83    {
84        // TODO: check this->supported
85        array_push($this->content, $content);
86        return sizeof($this->content)-1;
87    }
88
89    public function delete_rule($index)
90    {
91        if(isset($this->content[$index])) {
92            unset($this->content[$index]);
93            return true;
94        }
95        return false;
96    }
97
98    public function size()
99    {
100        return sizeof($this->content);
101    }
102
103    public function update_rule($index, $content)
104    {
105        // TODO: check this->supported
106        if ($this->content[$index]) {
107            $this->content[$index] = $content;
108            return $index;
109        }
110        return false;
111    }
112
113    /**
114     * Sets "global" variable
115     *
116     * @param string $name  Variable name
117     * @param string $value Variable value
118     * @param array  $mods  Variable modifiers
119     */
120    public function set_var($name, $value, $mods = array())
121    {
122        // Check if variable exists
123        for ($i=0, $len=count($this->vars); $i<$len; $i++) {
124            if ($this->vars[$i]['name'] == $name) {
125                break;
126            }
127        }
128
129        $var = array_merge($mods, array('name' => $name, 'value' => $value));
130        $this->vars[$i] = $var;
131    }
132
133    /**
134     * Unsets "global" variable
135     *
136     * @param string $name  Variable name
137     */
138    public function unset_var($name)
139    {
140        // Check if variable exists
141        foreach ($this->vars as $idx => $var) {
142            if ($var['name'] == $name) {
143                unset($this->vars[$idx]);
144                break;
145            }
146        }
147    }
148
149    /**
150     * Gets the value of  "global" variable
151     *
152     * @param string $name  Variable name
153     *
154     * @return string Variable value
155     */
156    public function get_var($name)
157    {
158        // Check if variable exists
159        for ($i=0, $len=count($this->vars); $i<$len; $i++) {
160            if ($this->vars[$i]['name'] == $name) {
161                return $this->vars[$i]['name'];
162            }
163        }
164    }
165
166    /**
167     * Sets script header content
168     *
169     * @param string $text  Header content
170     */
171    public function set_prefix($text)
172    {
173        $this->prefix = $text;
174    }
175
176    /**
177     * Returns script as text
178     */
179    public function as_text()
180    {
181        $output = '';
182        $exts   = array();
183        $idx    = 0;
184
185        if (!empty($this->vars)) {
186            if (in_array('variables', (array)$this->supported)) {
187                $has_vars = true;
188                array_push($exts, 'variables');
189            }
190            foreach ($this->vars as $var) {
191                if (empty($has_vars)) {
192                    // 'variables' extension not supported, put vars in comments
193                    $output .= sprintf("# %s %s\n", $var['name'], $var['value']);
194                }
195                else {
196                    $output .= 'set ';
197                    foreach (array_diff(array_keys($var), array('name', 'value')) as $opt) {
198                        $output .= ":$opt ";
199                    }
200                    $output .= self::escape_string($var['name']) . ' ' . self::escape_string($var['value']) . ";\n";
201                }
202            }
203        }
204
205        // rules
206        foreach ($this->content as $rule) {
207            $extension = '';
208            $script    = '';
209            $tests     = array();
210            $i         = 0;
211
212            // header
213            if (!empty($rule['name']) && strlen($rule['name'])) {
214                $script .= '# rule:[' . $rule['name'] . "]\n";
215            }
216
217            // constraints expressions
218            if (!empty($rule['tests'])) {
219                foreach ($rule['tests'] as $test) {
220                    $tests[$i] = '';
221                    switch ($test['test']) {
222                    case 'size':
223                        $tests[$i] .= ($test['not'] ? 'not ' : '');
224                        $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
225                        break;
226
227                    case 'true':
228                        $tests[$i] .= ($test['not'] ? 'false' : 'true');
229                        break;
230
231                    case 'exists':
232                        $tests[$i] .= ($test['not'] ? 'not ' : '');
233                        $tests[$i] .= 'exists ' . self::escape_string($test['arg']);
234                        break;
235
236                    case 'header':
237                        $tests[$i] .= ($test['not'] ? 'not ' : '');
238                        $tests[$i] .= 'header';
239
240                        if (!empty($test['type'])) {
241                            // relational operator + comparator
242                            if (preg_match('/^(value|count)-([gteqnl]{2})/', $test['type'], $m)) {
243                                array_push($exts, 'relational');
244                                array_push($exts, 'comparator-i;ascii-numeric');
245
246                                $tests[$i] .= ' :' . $m[1] . ' "' . $m[2] . '" :comparator "i;ascii-numeric"';
247                            }
248                            else {
249                                $this->add_comparator($test, $tests[$i], $exts);
250
251                                if ($test['type'] == 'regex') {
252                                    array_push($exts, 'regex');
253                                }
254
255                                $tests[$i] .= ' :' . $test['type'];
256                            }
257                        }
258
259                        $tests[$i] .= ' ' . self::escape_string($test['arg1']);
260                        $tests[$i] .= ' ' . self::escape_string($test['arg2']);
261                        break;
262
263                    case 'address':
264                    case 'envelope':
265                        if ($test['test'] == 'envelope') {
266                            array_push($exts, 'envelope');
267                        }
268
269                        $tests[$i] .= ($test['not'] ? 'not ' : '');
270                        $tests[$i] .= $test['test'];
271
272                        if (!empty($test['part'])) {
273                            $tests[$i] .= ' :' . $test['part'];
274                            if ($test['part'] == 'user' || $test['part'] == 'detail') {
275                                array_push($exts, 'subaddress');
276                            }
277                        }
278
279                        $this->add_comparator($test, $tests[$i], $exts);
280
281                        if (!empty($test['type'])) {
282                            if ($test['type'] == 'regex') {
283                                array_push($exts, 'regex');
284                            }
285                            $tests[$i] .= ' :' . $test['type'];
286                        }
287
288                        $tests[$i] .= ' ' . self::escape_string($test['arg1']);
289                        $tests[$i] .= ' ' . self::escape_string($test['arg2']);
290                        break;
291
292                    case 'body':
293                        array_push($exts, 'body');
294
295                        $tests[$i] .= ($test['not'] ? 'not ' : '') . 'body';
296
297                        $this->add_comparator($test, $tests[$i], $exts);
298
299                        if (!empty($test['part'])) {
300                            $tests[$i] .= ' :' . $test['part'];
301
302                            if (!empty($test['content']) && $test['part'] == 'content') {
303                                $tests[$i] .= ' ' . self::escape_string($test['content']);
304                            }
305                        }
306
307                        if (!empty($test['type'])) {
308                            if ($test['type'] == 'regex') {
309                                array_push($exts, 'regex');
310                            }
311                            $tests[$i] .= ' :' . $test['type'];
312                        }
313
314                        $tests[$i] .= ' ' . self::escape_string($test['arg']);
315                        break;
316                    }
317                    $i++;
318                }
319            }
320
321            // disabled rule: if false #....
322            if (!empty($tests)) {
323                $script .= 'if ' . ($rule['disabled'] ? 'false # ' : '');
324
325                if (count($tests) > 1) {
326                    $tests_str = implode(', ', $tests);
327                }
328                else {
329                    $tests_str = $tests[0];
330                }
331
332                if ($rule['join'] || count($tests) > 1) {
333                    $script .= sprintf('%s (%s)', $rule['join'] ? 'allof' : 'anyof', $tests_str);
334                }
335                else {
336                    $script .= $tests_str;
337                }
338                $script .= "\n{\n";
339            }
340
341            // action(s)
342            if (!empty($rule['actions'])) {
343                foreach ($rule['actions'] as $action) {
344                    $action_script = '';
345
346                    switch ($action['type']) {
347
348                    case 'fileinto':
349                        array_push($exts, 'fileinto');
350                        $action_script .= 'fileinto ';
351                        if ($action['copy']) {
352                            $action_script .= ':copy ';
353                            array_push($exts, 'copy');
354                        }
355                        $action_script .= self::escape_string($action['target']);
356                        break;
357
358                    case 'redirect':
359                        $action_script .= 'redirect ';
360                        if ($action['copy']) {
361                            $action_script .= ':copy ';
362                            array_push($exts, 'copy');
363                        }
364                        $action_script .= self::escape_string($action['target']);
365                        break;
366
367                    case 'reject':
368                    case 'ereject':
369                        array_push($exts, $action['type']);
370                        $action_script .= $action['type'].' '
371                            . self::escape_string($action['target']);
372                        break;
373
374                    case 'addflag':
375                    case 'setflag':
376                    case 'removeflag':
377                        if (in_array('imap4flags', $this->supported))
378                            array_push($exts, 'imap4flags');
379                        else
380                            array_push($exts, 'imapflags');
381
382                        $action_script .= $action['type'].' '
383                            . self::escape_string($action['target']);
384                        break;
385
386                    case 'keep':
387                    case 'discard':
388                    case 'stop':
389                        $action_script .= $action['type'];
390                        break;
391
392                    case 'include':
393                        array_push($exts, 'include');
394                        $action_script .= 'include ';
395                        foreach (array_diff(array_keys($action), array('target', 'type')) as $opt) {
396                            $action_script .= ":$opt ";
397                        }
398                        $action_script .= self::escape_string($action['target']);
399                        break;
400
401                    case 'set':
402                        array_push($exts, 'variables');
403                        $action_script .= 'set ';
404                        foreach (array_diff(array_keys($action), array('name', 'value', 'type')) as $opt) {
405                            $action_script .= ":$opt ";
406                        }
407                        $action_script .= self::escape_string($action['name']) . ' ' . self::escape_string($action['value']);
408                        break;
409
410                    case 'vacation':
411                        array_push($exts, 'vacation');
412                        $action_script .= 'vacation';
413                        if (!empty($action['days']))
414                            $action_script .= " :days " . $action['days'];
415                        if (!empty($action['addresses']))
416                            $action_script .= " :addresses " . self::escape_string($action['addresses']);
417                        if (!empty($action['subject']))
418                            $action_script .= " :subject " . self::escape_string($action['subject']);
419                        if (!empty($action['handle']))
420                            $action_script .= " :handle " . self::escape_string($action['handle']);
421                        if (!empty($action['from']))
422                            $action_script .= " :from " . self::escape_string($action['from']);
423                        if (!empty($action['mime']))
424                            $action_script .= " :mime";
425                        $action_script .= " " . self::escape_string($action['reason']);
426                        break;
427                    }
428
429                    if ($action_script) {
430                        $script .= !empty($tests) ? "\t" : '';
431                        $script .= $action_script . ";\n";
432                    }
433                }
434            }
435
436            if ($script) {
437                $output .= $script . (!empty($tests) ? "}\n" : '');
438                $idx++;
439            }
440        }
441
442        // requires
443        if (!empty($exts))
444            $output = 'require ["' . implode('","', array_unique($exts)) . "\"];\n" . $output;
445
446        if (!empty($this->prefix)) {
447            $output = $this->prefix . "\n\n" . $output;
448        }
449
450        return $output;
451    }
452
453    /**
454     * Returns script object
455     *
456     */
457    public function as_array()
458    {
459        return $this->content;
460    }
461
462    /**
463     * Returns array of supported extensions
464     *
465     */
466    public function get_extensions()
467    {
468        return array_values($this->supported);
469    }
470
471    /**
472     * Converts text script to rules array
473     *
474     * @param string Text script
475     */
476    private function _parse_text($script)
477    {
478        $prefix     = '';
479        $options = array();
480
481        while ($script) {
482            $script = trim($script);
483            $rule   = array();
484
485            // Comments
486            while (!empty($script) && $script[0] == '#') {
487                $endl = strpos($script, "\n");
488                $line = $endl ? substr($script, 0, $endl) : $script;
489
490                // Roundcube format
491                if (preg_match('/^# rule:\[(.*)\]/', $line, $matches)) {
492                    $rulename = $matches[1];
493                }
494                // KEP:14 variables
495                else if (preg_match('/^# (EDITOR|EDITOR_VERSION) (.+)$/', $line, $matches)) {
496                    $this->set_var($matches[1], $matches[2]);
497                }
498                // Horde-Ingo format
499                else if (!empty($options['format']) && $options['format'] == 'INGO'
500                    && preg_match('/^# (.*)/', $line, $matches)
501                ) {
502                    $rulename = $matches[1];
503                }
504                else if (empty($options['prefix'])) {
505                    $prefix .= $line . "\n";
506                }
507
508                $script = ltrim(substr($script, strlen($line) + 1));
509            }
510
511            // handle script header
512            if (empty($options['prefix'])) {
513                $options['prefix'] = true;
514                if ($prefix && strpos($prefix, 'horde.org/ingo')) {
515                    $options['format'] = 'INGO';
516                }
517            }
518
519            // Control structures/blocks
520            if (preg_match('/^(if|else|elsif)/i', $script)) {
521                $rule = $this->_tokenize_rule($script);
522                if (strlen($rulename) && !empty($rule)) {
523                    $rule['name'] = $rulename;
524                }
525            }
526            // Simple commands
527            else {
528                $rule = $this->_parse_actions($script, ';');
529                if (!empty($rule[0]) && is_array($rule)) {
530                    // set "global" variables
531                    if ($rule[0]['type'] == 'set') {
532                        unset($rule[0]['type']);
533                        $this->vars[] = $rule[0];
534                    }
535                    else {
536                        $rule = array('actions' => $rule);
537                    }
538                }
539            }
540
541            $rulename = '';
542
543            if (!empty($rule)) {
544                $this->content[] = $rule;
545            }
546        }
547
548        if (!empty($prefix)) {
549            $this->prefix = trim($prefix);
550        }
551    }
552
553    /**
554     * Convert text script fragment to rule object
555     *
556     * @param string Text rule
557     *
558     * @return array Rule data
559     */
560    private function _tokenize_rule(&$content)
561    {
562        $cond = strtolower(self::tokenize($content, 1));
563
564        if ($cond != 'if' && $cond != 'elsif' && $cond != 'else') {
565            return null;
566        }
567
568        $disabled = false;
569        $join     = false;
570
571        // disabled rule (false + comment): if false # .....
572        if (preg_match('/^\s*false\s+#/i', $content)) {
573            $content = preg_replace('/^\s*false\s+#\s*/i', '', $content);
574            $disabled = true;
575        }
576
577        while (strlen($content)) {
578            $tokens = self::tokenize($content, true);
579            $separator = array_pop($tokens);
580
581            if (!empty($tokens)) {
582                $token = array_shift($tokens);
583            }
584            else {
585                $token = $separator;
586            }
587
588            $token = strtolower($token);
589
590            if ($token == 'not') {
591                $not = true;
592                $token = strtolower(array_shift($tokens));
593            }
594            else {
595                $not = false;
596            }
597
598            switch ($token) {
599            case 'allof':
600                $join = true;
601                break;
602            case 'anyof':
603                break;
604
605            case 'size':
606                $size = array('test' => 'size', 'not'  => $not);
607                for ($i=0, $len=count($tokens); $i<$len; $i++) {
608                    if (!is_array($tokens[$i])
609                        && preg_match('/^:(under|over)$/i', $tokens[$i])
610                    ) {
611                        $size['type'] = strtolower(substr($tokens[$i], 1));
612                    }
613                    else {
614                        $size['arg'] = $tokens[$i];
615                    }
616                }
617
618                $tests[] = $size;
619                break;
620
621            case 'header':
622                $header = array('test' => 'header', 'not' => $not, 'arg1' => '', 'arg2' => '');
623                for ($i=0, $len=count($tokens); $i<$len; $i++) {
624                    if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
625                        $header['comparator'] = $tokens[++$i];
626                    }
627                    else if (!is_array($tokens[$i]) && preg_match('/^:(count|value)$/i', $tokens[$i])) {
628                        $header['type'] = strtolower(substr($tokens[$i], 1)) . '-' . $tokens[++$i];
629                    }
630                    else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
631                        $header['type'] = strtolower(substr($tokens[$i], 1));
632                    }
633                    else {
634                        $header['arg1'] = $header['arg2'];
635                        $header['arg2'] = $tokens[$i];
636                    }
637                }
638
639                $tests[] = $header;
640                break;
641
642            case 'address':
643            case 'envelope':
644                $header = array('test' => $token, 'not' => $not, 'arg1' => '', 'arg2' => '');
645                for ($i=0, $len=count($tokens); $i<$len; $i++) {
646                    if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
647                        $header['comparator'] = $tokens[++$i];
648                    }
649                    else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
650                        $header['type'] = strtolower(substr($tokens[$i], 1));
651                    }
652                    else if (!is_array($tokens[$i]) && preg_match('/^:(localpart|domain|all|user|detail)$/i', $tokens[$i])) {
653                        $header['part'] = strtolower(substr($tokens[$i], 1));
654                    }
655                    else {
656                        $header['arg1'] = $header['arg2'];
657                        $header['arg2'] = $tokens[$i];
658                    }
659                }
660
661                $tests[] = $header;
662                break;
663
664            case 'body':
665                $header = array('test' => 'body', 'not' => $not, 'arg' => '');
666                for ($i=0, $len=count($tokens); $i<$len; $i++) {
667                    if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
668                        $header['comparator'] = $tokens[++$i];
669                    }
670                    else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
671                        $header['type'] = strtolower(substr($tokens[$i], 1));
672                    }
673                    else if (!is_array($tokens[$i]) && preg_match('/^:(raw|content|text)$/i', $tokens[$i])) {
674                        $header['part'] = strtolower(substr($tokens[$i], 1));
675
676                        if ($header['part'] == 'content') {
677                            $header['content'] = $tokens[++$i];
678                        }
679                    }
680                    else {
681                        $header['arg'] = $tokens[$i];
682                    }
683                }
684
685                $tests[] = $header;
686                break;
687
688            case 'exists':
689                $tests[] = array('test' => 'exists', 'not'  => $not,
690                    'arg'  => array_pop($tokens));
691                break;
692
693            case 'true':
694                $tests[] = array('test' => 'true', 'not'  => $not);
695                break;
696
697            case 'false':
698                $tests[] = array('test' => 'true', 'not'  => !$not);
699                break;
700            }
701
702            // goto actions...
703            if ($separator == '{') {
704                break;
705            }
706        }
707
708        // ...and actions block
709        $actions = $this->_parse_actions($content);
710
711        if ($tests && $actions) {
712            $result = array(
713                'type'     => $cond,
714                'tests'    => $tests,
715                'actions'  => $actions,
716                'join'     => $join,
717                'disabled' => $disabled,
718            );
719        }
720
721        return $result;
722    }
723
724    /**
725     * Parse body of actions section
726     *
727     * @param string $content  Text body
728     * @param string $end      End of text separator
729     *
730     * @return array Array of parsed action type/target pairs
731     */
732    private function _parse_actions(&$content, $end = '}')
733    {
734        $result = null;
735
736        while (strlen($content)) {
737            $tokens = self::tokenize($content, true);
738            $separator = array_pop($tokens);
739
740            if (!empty($tokens)) {
741                $token = array_shift($tokens);
742            }
743            else {
744                $token = $separator;
745            }
746
747            switch ($token) {
748            case 'discard':
749            case 'keep':
750            case 'stop':
751                $result[] = array('type' => $token);
752                break;
753
754            case 'fileinto':
755            case 'redirect':
756                $copy   = false;
757                $target = '';
758
759                for ($i=0, $len=count($tokens); $i<$len; $i++) {
760                    if (strtolower($tokens[$i]) == ':copy') {
761                        $copy = true;
762                    }
763                    else {
764                        $target = $tokens[$i];
765                    }
766                }
767
768                $result[] = array('type' => $token, 'copy' => $copy,
769                    'target' => $target);
770                break;
771
772            case 'reject':
773            case 'ereject':
774                $result[] = array('type' => $token, 'target' => array_pop($tokens));
775                break;
776
777            case 'vacation':
778                $vacation = array('type' => 'vacation', 'reason' => array_pop($tokens));
779
780                for ($i=0, $len=count($tokens); $i<$len; $i++) {
781                    $tok = strtolower($tokens[$i]);
782                    if ($tok == ':days') {
783                        $vacation['days'] = $tokens[++$i];
784                    }
785                    else if ($tok == ':subject') {
786                        $vacation['subject'] = $tokens[++$i];
787                    }
788                    else if ($tok == ':addresses') {
789                        $vacation['addresses'] = $tokens[++$i];
790                    }
791                    else if ($tok == ':handle') {
792                        $vacation['handle'] = $tokens[++$i];
793                    }
794                    else if ($tok == ':from') {
795                        $vacation['from'] = $tokens[++$i];
796                    }
797                    else if ($tok == ':mime') {
798                        $vacation['mime'] = true;
799                    }
800                }
801
802                $result[] = $vacation;
803                break;
804
805            case 'setflag':
806            case 'addflag':
807            case 'removeflag':
808                $result[] = array('type' => $token,
809                    // Flags list: last token (skip optional variable)
810                    'target' => $tokens[count($tokens)-1]
811                );
812                break;
813
814            case 'include':
815                $include = array('type' => 'include', 'target' => array_pop($tokens));
816
817                // Parameters: :once, :optional, :global, :personal
818                for ($i=0, $len=count($tokens); $i<$len; $i++) {
819                    $tok = strtolower($tokens[$i]);
820                    if ($tok[0] == ':') {
821                        $include[substr($tok, 1)] = true;
822                    }
823                }
824
825                $result[] = $include;
826                break;
827
828            case 'set':
829                $set = array('type' => 'set', 'value' => array_pop($tokens), 'name' => array_pop($tokens));
830
831                // Parameters: :lower :upper :lowerfirst :upperfirst :quotewildcard :length
832                for ($i=0, $len=count($tokens); $i<$len; $i++) {
833                    $tok = strtolower($tokens[$i]);
834                    if ($tok[0] == ':') {
835                        $set[substr($tok, 1)] = true;
836                    }
837                }
838
839                $result[] = $set;
840                break;
841
842            case 'require':
843                // skip, will be build according to used commands
844                // $result[] = array('type' => 'require', 'target' => $tokens);
845                break;
846
847            }
848
849            if ($separator == $end)
850                break;
851        }
852
853        return $result;
854    }
855
856    /**
857     *
858     */
859    private function add_comparator($test, &$out, &$exts)
860    {
861        if (empty($test['comparator'])) {
862            return;
863        }
864
865        if ($test['comparator'] == 'i;ascii-numeric') {
866            array_push($exts, 'relational');
867            array_push($exts, 'comparator-i;ascii-numeric');
868        }
869        else if (!in_array($test['comparator'], array('i;octet', 'i;ascii-casemap'))) {
870            array_push($exts, 'comparator-' . $test['comparator']);
871        }
872
873        // skip default comparator
874        if ($test['comparator'] != 'i;ascii-casemap') {
875            $out .= ' :comparator ' . self::escape_string($test['comparator']);
876        }
877    }
878
879    /**
880     * Escape special chars into quoted string value or multi-line string
881     * or list of strings
882     *
883     * @param string $str Text or array (list) of strings
884     *
885     * @return string Result text
886     */
887    static function escape_string($str)
888    {
889        if (is_array($str) && count($str) > 1) {
890            foreach($str as $idx => $val)
891                $str[$idx] = self::escape_string($val);
892
893            return '[' . implode(',', $str) . ']';
894        }
895        else if (is_array($str)) {
896            $str = array_pop($str);
897        }
898
899        // multi-line string
900        if (preg_match('/[\r\n\0]/', $str) || strlen($str) > 1024) {
901            return sprintf("text:\n%s\n.\n", self::escape_multiline_string($str));
902        }
903        // quoted-string
904        else {
905            return '"' . addcslashes($str, '\\"') . '"';
906        }
907    }
908
909    /**
910     * Escape special chars in multi-line string value
911     *
912     * @param string $str Text
913     *
914     * @return string Text
915     */
916    static function escape_multiline_string($str)
917    {
918        $str = preg_split('/(\r?\n)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE);
919
920        foreach ($str as $idx => $line) {
921            // dot-stuffing
922            if (isset($line[0]) && $line[0] == '.') {
923                $str[$idx] = '.' . $line;
924            }
925        }
926
927        return implode($str);
928    }
929
930    /**
931     * Splits script into string tokens
932     *
933     * @param string &$str    The script
934     * @param mixed  $num     Number of tokens to return, 0 for all
935     *                        or True for all tokens until separator is found.
936     *                        Separator will be returned as last token.
937     * @param int    $in_list Enable to call recursively inside a list
938     *
939     * @return mixed Tokens array or string if $num=1
940     */
941    static function tokenize(&$str, $num=0, $in_list=false)
942    {
943        $result = array();
944
945        // remove spaces from the beginning of the string
946        while (($str = ltrim($str)) !== ''
947            && (!$num || $num === true || count($result) < $num)
948        ) {
949            switch ($str[0]) {
950
951            // Quoted string
952            case '"':
953                $len = strlen($str);
954
955                for ($pos=1; $pos<$len; $pos++) {
956                    if ($str[$pos] == '"') {
957                        break;
958                    }
959                    if ($str[$pos] == "\\") {
960                        if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
961                            $pos++;
962                        }
963                    }
964                }
965                if ($str[$pos] != '"') {
966                    // error
967                }
968                // we need to strip slashes for a quoted string
969                $result[] = stripslashes(substr($str, 1, $pos - 1));
970                $str      = substr($str, $pos + 1);
971                break;
972
973            // Parenthesized list
974            case '[':
975                $str = substr($str, 1);
976                $result[] = self::tokenize($str, 0, true);
977                break;
978            case ']':
979                $str = substr($str, 1);
980                return $result;
981                break;
982
983            // list/test separator
984            case ',':
985            // command separator
986            case ';':
987            // block/tests-list
988            case '(':
989            case ')':
990            case '{':
991            case '}':
992                $sep = $str[0];
993                $str = substr($str, 1);
994                if ($num === true) {
995                    $result[] = $sep;
996                    break 2;
997                }
998                break;
999
1000            // bracket-comment
1001            case '/':
1002                if ($str[1] == '*') {
1003                    if ($end_pos = strpos($str, '*/')) {
1004                        $str = substr($str, $end_pos + 2);
1005                    }
1006                    else {
1007                        // error
1008                        $str = '';
1009                    }
1010                }
1011                break;
1012
1013            // hash-comment
1014            case '#':
1015                if ($lf_pos = strpos($str, "\n")) {
1016                    $str = substr($str, $lf_pos);
1017                    break;
1018                }
1019                else {
1020                    $str = '';
1021                }
1022
1023            // String atom
1024            default:
1025                // empty or one character
1026                if ($str === '' || $str === null) {
1027                    break 2;
1028                }
1029                if (strlen($str) < 2) {
1030                    $result[] = $str;
1031                    $str = '';
1032                    break;
1033                }
1034
1035                // tag/identifier/number
1036                if (preg_match('/^([a-z0-9:_]+)/i', $str, $m)) {
1037                    $str = substr($str, strlen($m[1]));
1038
1039                    if ($m[1] != 'text:') {
1040                        $result[] = $m[1];
1041                    }
1042                    // multiline string
1043                    else {
1044                        // possible hash-comment after "text:"
1045                        if (preg_match('/^( |\t)*(#[^\n]+)?\n/', $str, $m)) {
1046                            $str = substr($str, strlen($m[0]));
1047                        }
1048                        // get text until alone dot in a line
1049                        if (preg_match('/^(.*)\r?\n\.\r?\n/sU', $str, $m)) {
1050                            $text = $m[1];
1051                            // remove dot-stuffing
1052                            $text = str_replace("\n..", "\n.", $text);
1053                            $str = substr($str, strlen($m[0]));
1054                        }
1055                        else {
1056                            $text = '';
1057                        }
1058
1059                        $result[] = $text;
1060                    }
1061                }
1062
1063                break;
1064            }
1065        }
1066
1067        return $num === 1 ? (isset($result[0]) ? $result[0] : null) : $result;
1068    }
1069
1070}
Note: See TracBrowser for help on using the repository browser.