source: subversion/trunk/plugins/managesieve/lib/rcube_sieve.php @ 3592

Last change on this file since 3592 was 3592, checked in by alec, 3 years ago
  • Fix filters set activation, add possibility to deactivate sets (#1486699)
  • Property svn:keywords set to Author Id
File size: 26.6 KB
Line 
1<?php
2
3/*
4  Classes for managesieve operations (using PEAR::Net_Sieve)
5
6  Author: Aleksander Machniak <alec@alec.pl>
7
8  $Id$
9
10*/
11
12//  Sieve Language Basics: http://www.ietf.org/rfc/rfc5228.txt
13
14define('SIEVE_ERROR_CONNECTION', 1);
15define('SIEVE_ERROR_LOGIN', 2);
16define('SIEVE_ERROR_NOT_EXISTS', 3);    // script not exists
17define('SIEVE_ERROR_INSTALL', 4);       // script installation
18define('SIEVE_ERROR_ACTIVATE', 5);      // script activation
19define('SIEVE_ERROR_DELETE', 6);        // script deletion
20define('SIEVE_ERROR_INTERNAL', 7);      // internal error
21define('SIEVE_ERROR_DEACTIVATE', 8);    // script activation
22define('SIEVE_ERROR_OTHER', 255);       // other/unknown error
23
24
25class rcube_sieve
26{
27    private $sieve;                     // Net_Sieve object
28    private $error = false;             // error flag
29    private $list = array();            // scripts list
30
31    public $script;                     // rcube_sieve_script object
32    public $current;                    // name of currently loaded script
33    private $disabled;                  // array of disabled extensions
34
35    /**
36    * Object constructor
37    *
38    * @param  string  Username (to managesieve login)
39    * @param  string  Password (to managesieve login)
40    * @param  string  Managesieve server hostname/address
41    * @param  string  Managesieve server port number
42    * @param  string  Enable/disable TLS use
43    * @param  array   Disabled extensions
44    */
45    public function __construct($username, $password='', $host='localhost', $port=2000,
46                            $usetls=true, $disabled=array(), $debug=false)
47    {
48            $this->sieve = new Net_Sieve();
49     
50        if ($debug)
51            $this->sieve->setDebug(true, array($this, 'debug_handler'));
52     
53        if (PEAR::isError($this->sieve->connect($host, $port, NULL, $usetls)))
54            return $this->_set_error(SIEVE_ERROR_CONNECTION);
55
56        if (PEAR::isError($this->sieve->login($username, $password)))
57            return $this->_set_error(SIEVE_ERROR_LOGIN);
58
59        $this->disabled = $disabled;
60    }
61
62    public function __destruct() {
63        $this->sieve->disconnect();
64    }
65
66    /**
67    * Getter for error code
68    */
69    public function error()
70    {
71            return $this->error ? $this->error : false;
72    }
73                           
74    /**
75    * Saves current script into server
76    */
77    public function save($name = null)
78    {
79        if (!$this->sieve)
80            return $this->_set_error(SIEVE_ERROR_INTERNAL);
81       
82            if (!$this->script)
83                return $this->_set_error(SIEVE_ERROR_INTERNAL);
84       
85            if (!$name)
86                $name = $this->current;
87
88        $script = $this->script->as_text();
89
90        if (!$script)
91                $script = '/* empty script */';
92
93        if (PEAR::isError($this->sieve->installScript($name, $script)))
94            return $this->_set_error(SIEVE_ERROR_INSTALL);
95
96        return true;
97    }
98
99    /**
100    * Saves text script into server
101    */
102    public function save_script($name, $content = null)
103    {
104        if (!$this->sieve)
105            return $this->_set_error(SIEVE_ERROR_INTERNAL);
106       
107        if (!$content)
108                $content = '/* empty script */';
109
110        if (PEAR::isError($this->sieve->installScript($name, $content)))
111            return $this->_set_error(SIEVE_ERROR_INSTALL);
112
113        return true;
114    }
115
116    /**
117    * Activates specified script
118    */
119    public function activate($name = null)
120    {
121            if (!$this->sieve)
122            return $this->_set_error(SIEVE_ERROR_INTERNAL);
123
124            if (!$name)
125                $name = $this->current;
126
127        if (PEAR::isError($this->sieve->setActive($name)))
128            return $this->_set_error(SIEVE_ERROR_ACTIVATE);
129
130        return true;
131    }
132
133    /**
134    * De-activates specified script
135    */
136    public function deactivate()
137    {
138            if (!$this->sieve)
139            return $this->_set_error(SIEVE_ERROR_INTERNAL);
140
141        if (PEAR::isError($this->sieve->setActive('')))
142            return $this->_set_error(SIEVE_ERROR_DEACTIVATE);
143
144        return true;
145    }
146
147    /**
148    * Removes specified script
149    */
150    public function remove($name = null)
151    {
152            if (!$this->sieve)
153            return $this->_set_error(SIEVE_ERROR_INTERNAL);
154
155            if (!$name)
156                $name = $this->current;
157
158            // script must be deactivated first
159            if ($name == $this->sieve->getActive())
160            if (PEAR::isError($this->sieve->setActive('')))
161                    return $this->_set_error(SIEVE_ERROR_DELETE);
162
163        if (PEAR::isError($this->sieve->removeScript($name)))
164            return $this->_set_error(SIEVE_ERROR_DELETE);
165
166            if ($name == $this->current)
167                $this->current = null;
168
169        return true;
170    }
171
172    /**
173    * Gets list of supported by server Sieve extensions
174    */
175    public function get_extensions()
176    {
177        if (!$this->sieve)
178                return $this->_set_error(SIEVE_ERROR_INTERNAL);
179       
180            $ext = $this->sieve->getExtensions();
181            // we're working on lower-cased names
182            $ext = array_map('strtolower', (array) $ext); 
183
184            if ($this->script) {
185                $supported = $this->script->get_extensions();
186                foreach ($ext as $idx => $ext_name)
187                    if (!in_array($ext_name, $supported))
188                            unset($ext[$idx]);
189            }
190
191        return array_values($ext);
192    }
193
194    /**
195    * Gets list of scripts from server
196    */
197    public function get_scripts()
198    {
199        if (!$this->list) {
200
201            if (!$this->sieve)
202                    return $this->_set_error(SIEVE_ERROR_INTERNAL);
203   
204            $this->list = $this->sieve->listScripts();
205
206            if (PEAR::isError($this->list))
207                    return $this->_set_error(SIEVE_ERROR_OTHER);
208            }
209
210        return $this->list;
211    }
212
213    /**
214    * Returns active script name
215    */
216    public function get_active()
217    {
218            if (!$this->sieve)
219            return $this->_set_error(SIEVE_ERROR_INTERNAL);
220
221            return $this->sieve->getActive();
222    }
223   
224    /**
225    * Loads script by name
226    */
227    public function load($name)
228    {
229        if (!$this->sieve)
230            return $this->_set_error(SIEVE_ERROR_INTERNAL);
231
232        if ($this->current == $name)
233            return true;
234   
235        $script = $this->sieve->getScript($name);
236   
237        if (PEAR::isError($script))
238            return $this->_set_error(SIEVE_ERROR_OTHER);
239
240            // try to parse from Roundcube format
241        $this->script = $this->_parse($script);
242
243        $this->current = $name;
244
245        return true;
246    }
247
248    /**
249    * Loads script from text content
250    */
251    public function load_script($script)
252    {
253        if (!$this->sieve)
254            return $this->_set_error(SIEVE_ERROR_INTERNAL);
255
256            // try to parse from Roundcube format
257        $this->script = $this->_parse($script);
258    }
259
260    /**
261    * Creates rcube_sieve_script object from text script
262    */
263    private function _parse($txt)
264    {
265            // try to parse from Roundcube format
266        $script = new rcube_sieve_script($txt, $this->disabled);
267
268        // ... else try to import from different formats
269        if (empty($script->content)) {
270            $script = $this->_import_rules($txt);
271            $script = new rcube_sieve_script($script, $this->disabled);
272        }
273
274        // replace all elsif with if+stop, we support only ifs
275        foreach ($script->content as $idx => $rule) {
276            if (!isset($script->content[$idx+1])
277                || preg_match('/^else|elsif$/', $script->content[$idx+1]['type'])) {
278                // 'stop' not found?
279                if (!preg_match('/^(stop|vacation)$/', $rule['actions'][count($rule['actions'])-1]['type'])) {
280                    $script->content[$idx]['actions'][] = array(
281                        'type' => 'stop'
282                    );
283                }
284            }
285        }
286
287        return $script;
288    }
289
290    /**
291    * Gets specified script as text
292    */
293    public function get_script($name)
294    {
295        if (!$this->sieve)
296            return $this->_set_error(SIEVE_ERROR_INTERNAL);
297
298        $content = $this->sieve->getScript($name);
299
300        if (PEAR::isError($content))
301            return $this->_set_error(SIEVE_ERROR_OTHER);
302       
303        return $content;
304    }
305
306    /**
307    * Creates empty script or copy of other script
308    */
309    public function copy($name, $copy)
310    {
311        if (!$this->sieve)
312            return $this->_set_error(SIEVE_ERROR_INTERNAL);
313
314            if ($copy) {
315            $content = $this->sieve->getScript($copy);
316   
317            if (PEAR::isError($content))
318                    return $this->_set_error(SIEVE_ERROR_OTHER);
319            }
320       
321            return $this->save_script($name, $content);
322    }
323
324    private function _import_rules($script)
325    {
326        $i = 0;
327        $name = array();
328
329        // Squirrelmail (Avelsieve)
330        if ($tokens = preg_split('/(#START_SIEVE_RULE.*END_SIEVE_RULE)\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) {
331            foreach($tokens as $token) {
332                    if (preg_match('/^#START_SIEVE_RULE.*/', $token, $matches)) {
333                        $name[$i] = "unnamed rule ".($i+1);
334                    $content .= "# rule:[".$name[$i]."]\n";
335                }
336                    elseif (isset($name[$i])) {
337                        $content .= "if $token\n";
338                            $i++;
339                }
340            }
341        }
342        // Horde (INGO)
343        else if ($tokens = preg_split('/(# .+)\r?\n/i', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) {
344            foreach($tokens as $token) {
345                if (preg_match('/^# (.+)/i', $token, $matches)) {
346                    $name[$i] = $matches[1];
347                    $content .= "# rule:[" . $name[$i] . "]\n";
348                }
349                elseif (isset($name[$i])) {
350                    $token = str_replace(":comparator \"i;ascii-casemap\" ", "", $token);
351                    $content .= $token . "\n";
352                    $i++;
353                }
354            }
355        }
356
357        return $content;
358    }
359
360    private function _set_error($error)
361    {
362            $this->error = $error;
363        return false;
364    }
365
366    /**
367    * This is our own debug handler for connection
368    * @access public
369    */
370    public function debug_handler(&$sieve, $message)
371    {
372        write_log('sieve', preg_replace('/\r\n$/', '', $message));
373    }                   
374}
375
376class rcube_sieve_script
377{
378    public $content = array();  // script rules array   
379
380    private $supported = array( // extensions supported by class
381        'fileinto',
382        'reject',
383        'ereject',
384        'vacation',     // RFC5230
385    // TODO: (most wanted first) body, imapflags, notify, regex
386    );
387 
388    /**
389    * Object constructor
390    *
391    * @param  string  Script's text content
392    * @param  array   Disabled extensions
393    */
394    public function __construct($script, $disabled=NULL)
395    {
396        if (!empty($disabled))
397            foreach ($disabled as $ext)
398                if (($idx = array_search($ext, $this->supported)) !== false)
399                    unset($this->supported[$idx]);
400
401        $this->content = $this->_parse_text($script);
402    }
403
404    /**
405    * Adds script contents as text to the script array (at the end)
406    *
407    * @param    string  Text script contents
408    */
409    public function add_text($script)
410    {
411        $content = $this->_parse_text($script);
412        $result = false;
413
414        // check existsing script rules names
415        foreach ($this->content as $idx => $elem) {
416            $names[$elem['name']] = $idx;
417        }
418
419        foreach ($content as $elem) {
420            if (!isset($names[$elem['name']])) {
421                array_push($this->content, $elem);
422                    $result = true;
423                }
424        }
425
426        return $result;
427    }
428
429    /**
430    * Adds rule to the script (at the end)
431    *
432    * @param    string  Rule name
433    * @param    array   Rule content (as array)
434    */
435    public function add_rule($content)
436    {
437        // TODO: check this->supported
438        array_push($this->content, $content);
439        return sizeof($this->content)-1;
440    }
441
442    public function delete_rule($index)
443    {
444        if(isset($this->content[$index])) {
445            unset($this->content[$index]);
446                return true;
447            }
448        return false;
449    }
450
451    public function size()
452    {
453        return sizeof($this->content);
454    }
455
456    public function update_rule($index, $content)
457    {
458        // TODO: check this->supported
459        if ($this->content[$index]) {
460                $this->content[$index] = $content;
461                return $index;
462            }
463        return false;
464    }
465
466    /**
467    * Returns script as text
468    */
469    public function as_text()
470    {
471        $script = '';
472        $exts = array();
473        $idx = 0;
474
475        // rules
476        foreach ($this->content as $rule) {
477            $extension = '';
478            $tests = array();
479            $i = 0;
480
481            // header
482            $script .= '# rule:[' . $rule['name'] . "]\n";
483 
484            // constraints expressions
485            foreach ($rule['tests'] as $test) {
486                $tests[$i] = '';
487                switch ($test['test']) {
488                    case 'size':
489                        $tests[$i] .= ($test['not'] ? 'not ' : '');
490                        $tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
491                    break;
492                    case 'true':
493                        $tests[$i] .= ($test['not'] ? 'not true' : 'true');
494                    break;
495                    case 'exists':
496                        $tests[$i] .= ($test['not'] ? 'not ' : '');
497                        if (is_array($test['arg']))
498                            $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]';
499                        else
500                            $tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"';
501                    break;   
502                    case 'header':
503                        $tests[$i] .= ($test['not'] ? 'not ' : '');
504                        $tests[$i] .= 'header :' . $test['type'];
505                        if (is_array($test['arg1']))
506                            $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]';
507                        else
508                            $tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"';
509                        if (is_array($test['arg2']))
510                            $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]';
511                        else
512                            $tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"';
513                    break;
514                }
515                $i++;
516            }
517
518//            $script .= ($idx>0 ? 'els' : '').($rule['join'] ? 'if allof (' : 'if anyof (');
519            // disabled rule: if false #....
520            $script .= 'if' . ($rule['disabled'] ? ' false #' : '');
521            $script .= $rule['join'] ? ' allof (' : ' anyof (';
522            if (sizeof($tests) > 1)
523                $script .= implode(", ", $tests);
524            else if (sizeof($tests))
525                $script .= $tests[0];
526            else
527                $script .= 'true';
528            $script .= ")\n{\n";
529 
530            // action(s)
531            foreach ($rule['actions'] as $action) {
532                switch ($action['type']) {
533                    case 'fileinto':
534                        $extension = 'fileinto';
535                        $script .= "\tfileinto \"" . $this->_escape_string($action['target']) . "\";\n";
536                    break;
537                    case 'redirect':
538                        $script .= "\tredirect \"" . $this->_escape_string($action['target']) . "\";\n";
539                    break;
540                    case 'reject':
541                    case 'ereject':
542                        $extension = $action['type'];
543                        if (strpos($action['target'], "\n")!==false)
544                            $script .= "\t".$action['type']." text:\n" . $action['target'] . "\n.\n;\n";
545                        else
546                            $script .= "\t".$action['type']." \"" . $this->_escape_string($action['target']) . "\";\n";
547                    break;
548                    case 'keep':
549                    case 'discard':
550                    case 'stop':
551                        $script .= "\t" . $action['type'] .";\n";
552                    break;
553                    case 'vacation':
554                        $extension = 'vacation';
555                        $script .= "\tvacation";
556                        if ($action['days'])
557                            $script .= " :days " . $action['days'];
558                        if ($action['addresses'])
559                            $script .= " :addresses " . $this->_print_list($action['addresses']);
560                        if ($action['subject'])
561                            $script .= " :subject \"" . $this->_escape_string($action['subject']) . "\"";
562                        if ($action['handle'])
563                            $script .= " :handle \"" . $this->_escape_string($action['handle']) . "\"";
564                        if ($action['from'])
565                            $script .= " :from \"" . $this->_escape_string($action['from']) . "\"";
566                        if ($action['mime'])
567                            $script .= " :mime";
568                        if (strpos($action['reason'], "\n")!==false)
569                            $script .= " text:\n" . $action['reason'] . "\n.\n;\n";
570                        else
571                            $script .= " \"" . $this->_escape_string($action['reason']) . "\";\n";
572                    break;
573                }
574
575                if ($extension && !isset($exts[$extension]))
576                    $exts[$extension] = $extension;
577            }
578
579            $script .= "}\n";
580            $idx++;
581        }
582     
583        // requires
584        if (sizeof($exts))
585            $script = 'require ["' . implode('","', $exts) . "\"];\n" . $script;
586
587        return $script;
588    }
589
590    /**
591    * Returns script object
592    *
593    */
594    public function as_array()
595    {
596        return $this->content;
597    }
598
599    /**
600    * Returns array of supported extensions
601    *
602    */
603    public function get_extensions()
604    {
605        return array_values($this->supported);
606    }
607
608    /**
609    * Converts text script to rules array
610    *
611    * @param    string  Text script
612    */
613    private function _parse_text($script)
614    {
615        $i = 0;
616        $content = array();
617
618        // remove C comments
619        $script = preg_replace('|/\*.*?\*/|sm', '', $script);
620
621        // tokenize rules
622        if ($tokens = preg_split('/(# rule:\[.*\])\r?\n/', $script, -1, PREG_SPLIT_DELIM_CAPTURE)) {
623            foreach($tokens as $token) {
624                if (preg_match('/^# rule:\[(.*)\]/', $token, $matches)) {
625                    $content[$i]['name'] = $matches[1];
626                }
627                elseif (isset($content[$i]['name']) && sizeof($content[$i]) == 1) {
628                    if ($rule = $this->_tokenize_rule($token)) {
629                        $content[$i] = array_merge($content[$i], $rule);
630                        $i++;
631                    }
632                    else // unknown rule format
633                        unset($content[$i]);
634                }
635            }
636        }
637
638        return $content;
639    }
640
641    /**
642    * Convert text script fragment to rule object
643    *
644    * @param    string  Text rule
645    */
646    private function _tokenize_rule($content)
647    {
648        $result = NULL;
649   
650        if (preg_match('/^(if|elsif|else)\s+((true|false|not\s+true|allof|anyof|exists|header|not|size)(.*))\s+\{(.*)\}$/sm',
651            trim($content), $matches)) {
652
653            $tests = trim($matches[2]);
654
655            // disabled rule (false + comment): if false #.....
656            if ($matches[3] == 'false') {
657                $tests = preg_replace('/^false\s+#\s+/', '', $tests);
658                $disabled = true;
659            }
660            else
661                $disabled = false;
662
663            list($tests, $join) = $this->_parse_tests($tests);
664            $actions = $this->_parse_actions(trim($matches[5]));
665
666            if ($tests && $actions)
667                $result = array(
668                    'type' => $matches[1],
669                    'tests' => $tests,
670                    'actions' => $actions,
671                    'join' => $join,
672                    'disabled' => $disabled,
673                );
674        }
675
676        return $result;
677    }   
678
679    /**
680    * Parse body of actions section
681    *
682    * @param    string  Text body
683    * @return   array   Array of parsed action type/target pairs
684    */
685    private function _parse_actions($content)
686    {
687        $result = NULL;
688
689        // supported actions
690        $patterns[] = '^\s*discard;';
691        $patterns[] = '^\s*keep;';
692        $patterns[] = '^\s*stop;';
693        $patterns[] = '^\s*redirect\s+(.*?[^\\\]);';
694        if (in_array('fileinto', $this->supported))
695            $patterns[] = '^\s*fileinto\s+(.*?[^\\\]);';
696        if (in_array('reject', $this->supported)) {
697            $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;';
698            $patterns[] = '^\s*reject\s+(.*?[^\\\]);';
699            $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;';
700            $patterns[] = '^\s*ereject\s+(.*?[^\\\]);';
701        }
702        if (in_array('vacation', $this->supported))
703            $patterns[] = '^\s*vacation\s+(.*?[^\\\]);';
704
705        $pattern = '/(' . implode('$)|(', $patterns) . '$)/ms';
706
707        // parse actions body
708        if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER)) {
709            foreach ($mm as $m) {
710                $content = trim($m[0]);
711         
712                if(preg_match('/^(discard|keep|stop)/', $content, $matches)) {
713                    $result[] = array('type' => $matches[1]);
714                }
715                elseif(preg_match('/^fileinto/', $content)) {
716                    $result[] = array('type' => 'fileinto', 'target' => $this->_parse_string($m[sizeof($m)-1])); 
717                }
718                elseif(preg_match('/^redirect/', $content)) {
719                    $result[] = array('type' => 'redirect', 'target' => $this->_parse_string($m[sizeof($m)-1])); 
720                }
721                elseif(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches)) {
722                    $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2])); 
723                }
724                elseif(preg_match('/^vacation\s+(.*);$/sm', $content, $matches)) {
725                    $vacation = array('type' => 'vacation');
726
727                    if (preg_match('/:(days)\s+([0-9]+)/', $content, $vm)) {
728                        $vacation['days'] = $vm[2];
729                        $content = preg_replace('/:(days)\s+([0-9]+)/', '', $content); 
730                    }
731                    if (preg_match('/:(subject)\s+(".*?[^\\\]")/', $content, $vm)) {
732                        $vacation['subject'] = $vm[2];
733                        $content = preg_replace('/:(subject)\s+(".*?[^\\\]")/', '', $content); 
734                    }
735                    if (preg_match('/:(addresses)\s+\[(.*?[^\\\])\]/', $content, $vm)) {
736                        $vacation['addresses'] = $this->_parse_list($vm[2]);
737                        $content = preg_replace('/:(addresses)\s+\[(.*?[^\\\])\]/', '', $content); 
738                    }
739                    if (preg_match('/:(handle)\s+(".*?[^\\\]")/', $content, $vm)) {
740                        $vacation['handle'] = $vm[2];
741                        $content = preg_replace('/:(handle)\s+(".*?[^\\\]")/', '', $content); 
742                    }
743                    if (preg_match('/:(from)\s+(".*?[^\\\]")/', $content, $vm)) {
744                        $vacation['from'] = $vm[2];
745                        $content = preg_replace('/:(from)\s+(".*?[^\\\]")/', '', $content); 
746                    }
747
748                    $content = preg_replace('/^vacation/', '', $content);           
749                    $content = preg_replace('/;$/', '', $content);
750                    $content = trim($content);
751
752                    if (preg_match('/^:(mime)/', $content, $vm)) {
753                        $vacation['mime'] = true;
754                        $content = preg_replace('/^:mime/', '', $content); 
755                    }
756
757                    $vacation['reason'] = $this->_parse_string($content);
758
759                    $result[] = $vacation;
760                }
761            }
762        }
763
764        return $result;
765    }   
766   
767    /**
768    * Parse test/conditions section
769    *
770    * @param    string  Text
771    */
772    private function _parse_tests($content)
773    {
774        $result = NULL;
775
776        // lists
777        if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches)) {
778            $content = $matches[2];
779            $join = $matches[1]=='allof' ? true : false;
780        }
781        else
782            $join = false;
783     
784        // supported tests regular expressions
785        // TODO: comparators, envelope
786        $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]';
787        $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")';
788        $patterns[] = '(not\s+)?(true)';
789        $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})';
790        $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]';
791        $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+(".*?[^\\\]")';
792        $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")';
793        $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]';
794     
795        // join patterns...
796        $pattern = '/(' . implode(')|(', $patterns) . ')/';
797
798        // ...and parse tests list
799        if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
800            foreach ($matches as $match) {
801                $size = sizeof($match);
802             
803                if (preg_match('/^(not\s+)?size/', $match[0])) {
804                    $result[] = array(
805                        'test'  => 'size',
806                        'not'   => $match[$size-4] ? true : false,
807                        'type'  => $match[$size-2], // under/over
808                        'arg'   => $match[$size-1], // value
809                    );
810                }
811                elseif (preg_match('/^(not\s+)?header/', $match[0])) {
812                    $result[] = array(
813                        'test'  => 'header',
814                        'not'   => $match[$size-5] ? true : false,
815                        'type'  => $match[$size-3], // is/contains/matches
816                        'arg1'  => $this->_parse_list($match[$size-2]), // header(s)
817                        'arg2'  => $this->_parse_list($match[$size-1]), // string(s)
818                    ); 
819                }
820                elseif (preg_match('/^(not\s+)?exists/', $match[0])) {
821                    $result[] = array(
822                        'test'  => 'exists',
823                        'not'   => $match[$size-3] ? true : false,
824                        'arg'   => $this->_parse_list($match[$size-1]), // header(s)
825                    );
826                }
827                elseif (preg_match('/^(not\s+)?true/', $match[0])) {
828                    $result[] = array(
829                        'test'  => 'true',
830                        'not'   => $match[$size-2] ? true : false,
831                    );
832                }
833            }
834        }
835
836        return array($result, $join);
837    }   
838
839    /**
840    * Parse string value
841    *
842    * @param    string  Text
843    */
844    private function _parse_string($content)
845    {
846        $text = '';
847        $content = trim($content);
848
849        if (preg_match('/^text:(.*)\.$/sm', $content, $matches))
850            $text = trim($matches[1]);
851        elseif (preg_match('/^"(.*)"$/', $content, $matches))
852            $text = str_replace('\"', '"', $matches[1]);
853
854        return $text;
855    }   
856
857    /**
858    * Escape special chars in string value
859    *
860    * @param    string  Text
861    */
862    private function _escape_string($content)
863    {
864        $replace['/"/'] = '\\"';
865     
866        if (is_array($content)) {
867            for ($x=0, $y=sizeof($content); $x<$y; $x++)
868                $content[$x] = preg_replace(array_keys($replace), array_values($replace), $content[$x]);
869       
870            return $content;
871        }
872        else
873            return preg_replace(array_keys($replace), array_values($replace), $content);
874    }
875
876    /**
877    * Parse string or list of strings to string or array of strings
878    *
879    * @param    string  Text
880    */
881    private function _parse_list($content)
882    {
883        $result = array();
884     
885        for ($x=0, $len=strlen($content); $x<$len; $x++) {
886            switch ($content[$x]) {
887                case '\\':
888                    $str .= $content[++$x];
889                break;
890                case '"':
891                    if (isset($str)) {
892                        $result[] = $str;
893                        unset($str);
894                    }
895                    else
896                        $str = '';
897                break;
898                default:
899                    if(isset($str))
900                        $str .= $content[$x];
901                break;
902            }
903        }
904     
905        if (sizeof($result)>1)
906            return $result;
907        elseif (sizeof($result) == 1)
908            return $result[0];
909        else
910            return NULL;
911    }   
912
913    /**
914    * Convert array of elements to list of strings
915    *
916    * @param    string  Text
917    */
918    private function _print_list($list)
919    {
920        $list = (array) $list;
921        foreach($list as $idx => $val)
922            $list[$idx] = $this->_escape_string($val);
923   
924        return '["' . implode('","', $list) . '"]';
925    }
926}
927
928?>
Note: See TracBrowser for help on using the repository browser.