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

Last change on this file since 5440 was 5440, checked in by alec, 18 months ago
  • Fixed import of rules with unsupported tests
  • Property svn:keywords set to Date Author Id Revision
File size: 30.1 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',                 // RFC3028
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        // TODO: body, notify
46    );
47
48    /**
49     * Object constructor
50     *
51     * @param  string  Script's text content
52     * @param  array   List of disabled extensions
53     * @param  array   List of capabilities supported by server
54     */
55    public function __construct($script, $disabled=array(), $capabilities=array())
56    {
57        if (!empty($disabled)) {
58            // we're working on lower-cased names
59            $disabled = array_map('strtolower', (array) $disabled);
60            foreach ($disabled as $ext) {
61                if (($idx = array_search($ext, $this->supported)) !== false) {
62                    unset($this->supported[$idx]);
63                }
64            }
65        }
66
67        $this->capabilities = array_map('strtolower', (array) $capabilities);
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->capabilities)) {
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                    case 'true':
226                        $tests[$i] .= ($test['not'] ? 'false' : 'true');
227                        break;
228                    case 'exists':
229                        $tests[$i] .= ($test['not'] ? 'not ' : '');
230                        $tests[$i] .= 'exists ' . self::escape_string($test['arg']);
231                        break;
232                    case 'header':
233                        $tests[$i] .= ($test['not'] ? 'not ' : '');
234
235                        // relational operator + comparator
236                        if (preg_match('/^(value|count)-([gteqnl]{2})/', $test['type'], $m)) {
237                            array_push($exts, 'relational');
238                            array_push($exts, 'comparator-i;ascii-numeric');
239
240                            $tests[$i] .= 'header :' . $m[1] . ' "' . $m[2] . '" :comparator "i;ascii-numeric"';
241                        }
242                        else {
243                            if ($test['type'] == 'regex') {
244                                array_push($exts, 'regex');
245                            }
246
247                            $tests[$i] .= 'header :' . $test['type'];
248                        }
249
250                        $tests[$i] .= ' ' . self::escape_string($test['arg1']);
251                        $tests[$i] .= ' ' . self::escape_string($test['arg2']);
252                        break;
253                    }
254                    $i++;
255                }
256            }
257
258            // disabled rule: if false #....
259            if (!empty($tests)) {
260                $script .= 'if ' . ($rule['disabled'] ? 'false # ' : '');
261
262                if (count($tests) > 1) {
263                    $tests_str = implode(', ', $tests);
264                }
265                else {
266                    $tests_str = $tests[0];
267                }
268
269                if ($rule['join'] || count($tests) > 1) {
270                    $script .= sprintf('%s (%s)', $rule['join'] ? 'allof' : 'anyof', $tests_str);
271                }
272                else {
273                    $script .= $tests_str;
274                }
275                $script .= "\n{\n";
276            }
277
278            // action(s)
279            if (!empty($rule['actions'])) {
280                foreach ($rule['actions'] as $action) {
281                    $action_script = '';
282
283                    switch ($action['type']) {
284
285                    case 'fileinto':
286                        array_push($exts, 'fileinto');
287                        $action_script .= 'fileinto ';
288                        if ($action['copy']) {
289                            $action_script .= ':copy ';
290                            array_push($exts, 'copy');
291                        }
292                        $action_script .= self::escape_string($action['target']);
293                        break;
294
295                    case 'redirect':
296                        $action_script .= 'redirect ';
297                        if ($action['copy']) {
298                            $action_script .= ':copy ';
299                            array_push($exts, 'copy');
300                        }
301                        $action_script .= self::escape_string($action['target']);
302                        break;
303
304                    case 'reject':
305                    case 'ereject':
306                        array_push($exts, $action['type']);
307                        $action_script .= $action['type'].' '
308                            . self::escape_string($action['target']);
309                        break;
310
311                    case 'addflag':
312                    case 'setflag':
313                    case 'removeflag':
314                        if (is_array($this->capabilities) && in_array('imap4flags', $this->capabilities))
315                            array_push($exts, 'imap4flags');
316                        else
317                            array_push($exts, 'imapflags');
318
319                        $action_script .= $action['type'].' '
320                            . self::escape_string($action['target']);
321                        break;
322
323                    case 'keep':
324                    case 'discard':
325                    case 'stop':
326                        $action_script .= $action['type'];
327                        break;
328
329                    case 'include':
330                        array_push($exts, 'include');
331                        $action_script .= 'include ';
332                        foreach (array_diff(array_keys($action), array('target', 'type')) as $opt) {
333                            $action_script .= ":$opt ";
334                        }
335                        $action_script .= self::escape_string($action['target']);
336                        break;
337
338                    case 'set':
339                        array_push($exts, 'variables');
340                        $action_script .= 'set ';
341                        foreach (array_diff(array_keys($action), array('name', 'value', 'type')) as $opt) {
342                            $action_script .= ":$opt ";
343                        }
344                        $action_script .= self::escape_string($action['name']) . ' ' . self::escape_string($action['value']);
345                        break;
346
347                    case 'vacation':
348                        array_push($exts, 'vacation');
349                        $action_script .= 'vacation';
350                        if (!empty($action['days']))
351                            $action_script .= " :days " . $action['days'];
352                        if (!empty($action['addresses']))
353                            $action_script .= " :addresses " . self::escape_string($action['addresses']);
354                        if (!empty($action['subject']))
355                            $action_script .= " :subject " . self::escape_string($action['subject']);
356                        if (!empty($action['handle']))
357                            $action_script .= " :handle " . self::escape_string($action['handle']);
358                        if (!empty($action['from']))
359                            $action_script .= " :from " . self::escape_string($action['from']);
360                        if (!empty($action['mime']))
361                            $action_script .= " :mime";
362                        $action_script .= " " . self::escape_string($action['reason']);
363                        break;
364                    }
365
366                    if ($action_script) {
367                        $script .= !empty($tests) ? "\t" : '';
368                        $script .= $action_script . ";\n";
369                    }
370                }
371            }
372
373            if ($script) {
374                $output .= $script . (!empty($tests) ? "}\n" : '');
375                $idx++;
376            }
377        }
378
379        // requires
380        if (!empty($exts))
381            $output = 'require ["' . implode('","', array_unique($exts)) . "\"];\n" . $output;
382
383        if (!empty($this->prefix)) {
384            $output = $this->prefix . "\n\n" . $output;
385        }
386
387        return $output;
388    }
389
390    /**
391     * Returns script object
392     *
393     */
394    public function as_array()
395    {
396        return $this->content;
397    }
398
399    /**
400     * Returns array of supported extensions
401     *
402     */
403    public function get_extensions()
404    {
405        return array_values($this->supported);
406    }
407
408    /**
409     * Converts text script to rules array
410     *
411     * @param string Text script
412     */
413    private function _parse_text($script)
414    {
415        $prefix     = '';
416        $options = array();
417
418        while ($script) {
419            $script = trim($script);
420            $rule   = array();
421
422            // Comments
423            while (!empty($script) && $script[0] == '#') {
424                $endl = strpos($script, "\n");
425                $line = $endl ? substr($script, 0, $endl) : $script;
426
427                // Roundcube format
428                if (preg_match('/^# rule:\[(.*)\]/', $line, $matches)) {
429                    $rulename = $matches[1];
430                }
431                // KEP:14 variables
432                else if (preg_match('/^# (EDITOR|EDITOR_VERSION) (.+)$/', $line, $matches)) {
433                    $this->set_var($matches[1], $matches[2]);
434                }
435                // Horde-Ingo format
436                else if (!empty($options['format']) && $options['format'] == 'INGO'
437                    && preg_match('/^# (.*)/', $line, $matches)
438                ) {
439                    $rulename = $matches[1];
440                }
441                else if (empty($options['prefix'])) {
442                    $prefix .= $line . "\n";
443                }
444
445                $script = ltrim(substr($script, strlen($line) + 1));
446            }
447
448            // handle script header
449            if (empty($options['prefix'])) {
450                $options['prefix'] = true;
451                if ($prefix && strpos($prefix, 'Generated by Ingo')) {
452                    $options['format'] = 'INGO';
453                }
454            }
455
456            // Control structures/blocks
457            if (preg_match('/^(if|else|elsif)/i', $script)) {
458                $rule = $this->_tokenize_rule($script);
459                if (strlen($rulename) && !empty($rule)) {
460                    $rule['name'] = $rulename;
461                }
462            }
463            // Simple commands
464            else {
465                $rule = $this->_parse_actions($script, ';');
466                if (!empty($rule[0]) && is_array($rule)) {
467                    // set "global" variables
468                    if ($rule[0]['type'] == 'set') {
469                        unset($rule[0]['type']);
470                        $this->vars[] = $rule[0];
471                    }
472                    else {
473                        $rule = array('actions' => $rule);
474                    }
475                }
476            }
477
478            $rulename = '';
479
480            if (!empty($rule)) {
481                $this->content[] = $rule;
482            }
483        }
484
485        if (!empty($prefix)) {
486            $this->prefix = trim($prefix);
487        }
488    }
489
490    /**
491     * Convert text script fragment to rule object
492     *
493     * @param string Text rule
494     *
495     * @return array Rule data
496     */
497    private function _tokenize_rule(&$content)
498    {
499        $cond = strtolower(self::tokenize($content, 1));
500
501        if ($cond != 'if' && $cond != 'elsif' && $cond != 'else') {
502            return null;
503        }
504
505        $disabled = false;
506        $join     = false;
507
508        // disabled rule (false + comment): if false # .....
509        if (preg_match('/^\s*false\s+#/i', $content)) {
510            $content = preg_replace('/^\s*false\s+#\s*/i', '', $content);
511            $disabled = true;
512        }
513
514        while (strlen($content)) {
515            $tokens = self::tokenize($content, true);
516            $separator = array_pop($tokens);
517
518            if (!empty($tokens)) {
519                $token = array_shift($tokens);
520            }
521            else {
522                $token = $separator;
523            }
524
525            $token = strtolower($token);
526
527            if ($token == 'not') {
528                $not = true;
529                $token = strtolower(array_shift($tokens));
530            }
531            else {
532                $not = false;
533            }
534
535            switch ($token) {
536            case 'allof':
537                $join = true;
538                break;
539            case 'anyof':
540                break;
541
542            case 'size':
543                $size = array('test' => 'size', 'not'  => $not);
544                for ($i=0, $len=count($tokens); $i<$len; $i++) {
545                    if (!is_array($tokens[$i])
546                        && preg_match('/^:(under|over)$/i', $tokens[$i])
547                    ) {
548                        $size['type'] = strtolower(substr($tokens[$i], 1));
549                    }
550                    else {
551                        $size['arg'] = $tokens[$i];
552                    }
553                }
554
555                $tests[] = $size;
556                break;
557
558            case 'header':
559                $header = array('test' => 'header', 'not' => $not, 'arg1' => '', 'arg2' => '');
560                for ($i=0, $len=count($tokens); $i<$len; $i++) {
561                    if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) {
562                        $i++;
563                    }
564                    else if (!is_array($tokens[$i]) && preg_match('/^:(count|value)$/i', $tokens[$i])) {
565                        $header['type'] = strtolower(substr($tokens[$i], 1)) . '-' . $tokens[++$i];
566                    }
567                    else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches|regex)$/i', $tokens[$i])) {
568                        $header['type'] = strtolower(substr($tokens[$i], 1));
569                    }
570                    else {
571                        $header['arg1'] = $header['arg2'];
572                        $header['arg2'] = $tokens[$i];
573                    }
574                }
575
576                $tests[] = $header;
577                break;
578
579            case 'exists':
580                $tests[] = array('test' => 'exists', 'not'  => $not,
581                    'arg'  => array_pop($tokens));
582                break;
583
584            case 'true':
585                $tests[] = array('test' => 'true', 'not'  => $not);
586                break;
587
588            case 'false':
589                $tests[] = array('test' => 'true', 'not'  => !$not);
590                break;
591            }
592
593            // goto actions...
594            if ($separator == '{') {
595                break;
596            }
597        }
598
599        // ...and actions block
600        $actions = $this->_parse_actions($content);
601
602        if ($tests && $actions) {
603            $result = array(
604                'type'     => $cond,
605                'tests'    => $tests,
606                'actions'  => $actions,
607                'join'     => $join,
608                'disabled' => $disabled,
609            );
610        }
611
612        return $result;
613    }
614
615    /**
616     * Parse body of actions section
617     *
618     * @param string $content  Text body
619     * @param string $end      End of text separator
620     *
621     * @return array Array of parsed action type/target pairs
622     */
623    private function _parse_actions(&$content, $end = '}')
624    {
625        $result = null;
626
627        while (strlen($content)) {
628            $tokens = self::tokenize($content, true);
629            $separator = array_pop($tokens);
630
631            if (!empty($tokens)) {
632                $token = array_shift($tokens);
633            }
634            else {
635                $token = $separator;
636            }
637
638            switch ($token) {
639            case 'discard':
640            case 'keep':
641            case 'stop':
642                $result[] = array('type' => $token);
643                break;
644
645            case 'fileinto':
646            case 'redirect':
647                $copy   = false;
648                $target = '';
649
650                for ($i=0, $len=count($tokens); $i<$len; $i++) {
651                    if (strtolower($tokens[$i]) == ':copy') {
652                        $copy = true;
653                    }
654                    else {
655                        $target = $tokens[$i];
656                    }
657                }
658
659                $result[] = array('type' => $token, 'copy' => $copy,
660                    'target' => $target);
661                break;
662
663            case 'reject':
664            case 'ereject':
665                $result[] = array('type' => $token, 'target' => array_pop($tokens));
666                break;
667
668            case 'vacation':
669                $vacation = array('type' => 'vacation', 'reason' => array_pop($tokens));
670
671                for ($i=0, $len=count($tokens); $i<$len; $i++) {
672                    $tok = strtolower($tokens[$i]);
673                    if ($tok == ':days') {
674                        $vacation['days'] = $tokens[++$i];
675                    }
676                    else if ($tok == ':subject') {
677                        $vacation['subject'] = $tokens[++$i];
678                    }
679                    else if ($tok == ':addresses') {
680                        $vacation['addresses'] = $tokens[++$i];
681                    }
682                    else if ($tok == ':handle') {
683                        $vacation['handle'] = $tokens[++$i];
684                    }
685                    else if ($tok == ':from') {
686                        $vacation['from'] = $tokens[++$i];
687                    }
688                    else if ($tok == ':mime') {
689                        $vacation['mime'] = true;
690                    }
691                }
692
693                $result[] = $vacation;
694                break;
695
696            case 'setflag':
697            case 'addflag':
698            case 'removeflag':
699                $result[] = array('type' => $token,
700                    // Flags list: last token (skip optional variable)
701                    'target' => $tokens[count($tokens)-1]
702                );
703                break;
704
705            case 'include':
706                $include = array('type' => 'include', 'target' => array_pop($tokens));
707
708                // Parameters: :once, :optional, :global, :personal
709                for ($i=0, $len=count($tokens); $i<$len; $i++) {
710                    $tok = strtolower($tokens[$i]);
711                    if ($tok[0] == ':') {
712                        $include[substr($tok, 1)] = true;
713                    }
714                }
715
716                $result[] = $include;
717                break;
718
719            case 'set':
720                $set = array('type' => 'set', 'value' => array_pop($tokens), 'name' => array_pop($tokens));
721
722                // Parameters: :lower :upper :lowerfirst :upperfirst :quotewildcard :length
723                for ($i=0, $len=count($tokens); $i<$len; $i++) {
724                    $tok = strtolower($tokens[$i]);
725                    if ($tok[0] == ':') {
726                        $set[substr($tok, 1)] = true;
727                    }
728                }
729
730                $result[] = $set;
731                break;
732
733            case 'require':
734                // skip, will be build according to used commands
735                // $result[] = array('type' => 'require', 'target' => $tokens);
736                break;
737
738            }
739
740            if ($separator == $end)
741                break;
742        }
743
744        return $result;
745    }
746
747    /**
748     * Escape special chars into quoted string value or multi-line string
749     * or list of strings
750     *
751     * @param string $str Text or array (list) of strings
752     *
753     * @return string Result text
754     */
755    static function escape_string($str)
756    {
757        if (is_array($str) && count($str) > 1) {
758            foreach($str as $idx => $val)
759                $str[$idx] = self::escape_string($val);
760
761            return '[' . implode(',', $str) . ']';
762        }
763        else if (is_array($str)) {
764            $str = array_pop($str);
765        }
766
767        // multi-line string
768        if (preg_match('/[\r\n\0]/', $str) || strlen($str) > 1024) {
769            return sprintf("text:\n%s\n.\n", self::escape_multiline_string($str));
770        }
771        // quoted-string
772        else {
773            return '"' . addcslashes($str, '\\"') . '"';
774        }
775    }
776
777    /**
778     * Escape special chars in multi-line string value
779     *
780     * @param string $str Text
781     *
782     * @return string Text
783     */
784    static function escape_multiline_string($str)
785    {
786        $str = preg_split('/(\r?\n)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE);
787
788        foreach ($str as $idx => $line) {
789            // dot-stuffing
790            if (isset($line[0]) && $line[0] == '.') {
791                $str[$idx] = '.' . $line;
792            }
793        }
794
795        return implode($str);
796    }
797
798    /**
799     * Splits script into string tokens
800     *
801     * @param string &$str    The script
802     * @param mixed  $num     Number of tokens to return, 0 for all
803     *                        or True for all tokens until separator is found.
804     *                        Separator will be returned as last token.
805     * @param int    $in_list Enable to call recursively inside a list
806     *
807     * @return mixed Tokens array or string if $num=1
808     */
809    static function tokenize(&$str, $num=0, $in_list=false)
810    {
811        $result = array();
812
813        // remove spaces from the beginning of the string
814        while (($str = ltrim($str)) !== ''
815            && (!$num || $num === true || count($result) < $num)
816        ) {
817            switch ($str[0]) {
818
819            // Quoted string
820            case '"':
821                $len = strlen($str);
822
823                for ($pos=1; $pos<$len; $pos++) {
824                    if ($str[$pos] == '"') {
825                        break;
826                    }
827                    if ($str[$pos] == "\\") {
828                        if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
829                            $pos++;
830                        }
831                    }
832                }
833                if ($str[$pos] != '"') {
834                    // error
835                }
836                // we need to strip slashes for a quoted string
837                $result[] = stripslashes(substr($str, 1, $pos - 1));
838                $str      = substr($str, $pos + 1);
839                break;
840
841            // Parenthesized list
842            case '[':
843                $str = substr($str, 1);
844                $result[] = self::tokenize($str, 0, true);
845                break;
846            case ']':
847                $str = substr($str, 1);
848                return $result;
849                break;
850
851            // list/test separator
852            case ',':
853            // command separator
854            case ';':
855            // block/tests-list
856            case '(':
857            case ')':
858            case '{':
859            case '}':
860                $sep = $str[0];
861                $str = substr($str, 1);
862                if ($num === true) {
863                    $result[] = $sep;
864                    break 2;
865                }
866                break;
867
868            // bracket-comment
869            case '/':
870                if ($str[1] == '*') {
871                    if ($end_pos = strpos($str, '*/')) {
872                        $str = substr($str, $end_pos + 2);
873                    }
874                    else {
875                        // error
876                        $str = '';
877                    }
878                }
879                break;
880
881            // hash-comment
882            case '#':
883                if ($lf_pos = strpos($str, "\n")) {
884                    $str = substr($str, $lf_pos);
885                    break;
886                }
887                else {
888                    $str = '';
889                }
890
891            // String atom
892            default:
893                // empty or one character
894                if ($str === '' || $str === null) {
895                    break 2;
896                }
897                if (strlen($str) < 2) {
898                    $result[] = $str;
899                    $str = '';
900                    break;
901                }
902
903                // tag/identifier/number
904                if (preg_match('/^([a-z0-9:_]+)/i', $str, $m)) {
905                    $str = substr($str, strlen($m[1]));
906
907                    if ($m[1] != 'text:') {
908                        $result[] = $m[1];
909                    }
910                    // multiline string
911                    else {
912                        // possible hash-comment after "text:"
913                        if (preg_match('/^( |\t)*(#[^\n]+)?\n/', $str, $m)) {
914                            $str = substr($str, strlen($m[0]));
915                        }
916                        // get text until alone dot in a line
917                        if (preg_match('/^(.*)\r?\n\.\r?\n/sU', $str, $m)) {
918                            $text = $m[1];
919                            // remove dot-stuffing
920                            $text = str_replace("\n..", "\n.", $text);
921                            $str = substr($str, strlen($m[0]));
922                        }
923                        else {
924                            $text = '';
925                        }
926
927                        $result[] = $text;
928                    }
929                }
930
931                break;
932            }
933        }
934
935        return $num === 1 ? (isset($result[0]) ? $result[0] : null) : $result;
936    }
937
938}
Note: See TracBrowser for help on using the repository browser.