source: subversion/branches/release-0.7/plugins/managesieve/lib/rcube_sieve_script.php @ 6015

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