Changeset 4447 in subversion


Ignore:
Timestamp:
Jan 23, 2011 3:26:17 AM (2 years ago)
Author:
alec
Message:
  • Rewritten sieve script parser, added tests
Location:
trunk/plugins/managesieve
Files:
4 added
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/plugins/managesieve/Changelog

    r4423 r4447  
    33- Apply forgotten changes for form errors handling 
    44- Fix multi-line strings parsing (#1487685) 
     5- Added tests for script parser 
     6- Rewritten script parser 
    57 
    68* version 2.10 [2010-10-10] 
  • trunk/plugins/managesieve/lib/rcube_sieve.php

    r4423 r4447  
    514514                    break; 
    515515                case 'true': 
    516                     $tests[$i] .= ($test['not'] ? 'not true' : 'true'); 
     516                    $tests[$i] .= ($test['not'] ? 'false' : 'true'); 
    517517                    break; 
    518518                case 'exists': 
    519519                    $tests[$i] .= ($test['not'] ? 'not ' : ''); 
    520                     if (is_array($test['arg'])) 
    521                         $tests[$i] .= 'exists ["' . implode('", "', $this->_escape_string($test['arg'])) . '"]'; 
    522                     else 
    523                         $tests[$i] .= 'exists "' . $this->_escape_string($test['arg']) . '"'; 
     520                    $tests[$i] .= 'exists ' . self::escape_string($test['arg']); 
    524521                    break; 
    525522                case 'header': 
     
    535532                        $tests[$i] .= 'header :' . $test['type']; 
    536533                     
    537                     if (is_array($test['arg1'])) 
    538                         $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg1'])) . '"]'; 
    539                     else 
    540                         $tests[$i] .= ' "' . $this->_escape_string($test['arg1']) . '"'; 
    541  
    542                     if (is_array($test['arg2'])) 
    543                         $tests[$i] .= ' ["' . implode('", "', $this->_escape_string($test['arg2'])) . '"]'; 
    544                     else 
    545                         $tests[$i] .= ' "' . $this->_escape_string($test['arg2']) . '"'; 
    546  
     534                    $tests[$i] .= ' ' . self::escape_string($test['arg1']); 
     535                    $tests[$i] .= ' ' . self::escape_string($test['arg2']); 
    547536                    break; 
    548537                } 
     
    572561                        array_push($exts, 'copy'); 
    573562                    } 
    574                     $script .= "\"" . $this->_escape_string($action['target']) . "\";\n"; 
     563                    $script .= self::escape_string($action['target']) . ";\n"; 
    575564                    break; 
    576565                case 'redirect': 
     
    580569                        array_push($exts, 'copy'); 
    581570                    } 
    582                     $script .= "\"" . $this->_escape_string($action['target']) . "\";\n"; 
     571                    $script .= self::escape_string($action['target']) . ";\n"; 
    583572                    break; 
    584573                case 'reject': 
    585574                case 'ereject': 
    586575                    array_push($exts, $action['type']); 
    587                     if (strpos($action['target'], "\n")!==false) 
    588                         $script .= "\t".$action['type']." text:\n" 
    589                             . $this->_escape_multiline_string($action['target']) . "\n.\n;\n"; 
    590                     else 
    591                         $script .= "\t".$action['type']." \"" 
    592                             . $this->_escape_string($action['target']) . "\";\n"; 
     576                    $script .= "\t".$action['type']." " 
     577                        . self::escape_string($action['target']) . ";\n"; 
    593578                    break; 
    594579                case 'keep': 
     
    600585                    array_push($exts, 'vacation'); 
    601586                    $script .= "\tvacation"; 
    602                     if ($action['days']) 
     587                    if (!empty($action['days'])) 
    603588                        $script .= " :days " . $action['days']; 
    604                     if ($action['addresses']) 
    605                         $script .= " :addresses " . $this->_print_list($action['addresses']); 
    606                     if ($action['subject']) 
    607                         $script .= " :subject \"" . $this->_escape_string($action['subject']) . "\""; 
    608                     if ($action['handle']) 
    609                         $script .= " :handle \"" . $this->_escape_string($action['handle']) . "\""; 
    610                     if ($action['from']) 
    611                         $script .= " :from \"" . $this->_escape_string($action['from']) . "\""; 
    612                     if ($action['mime']) 
     589                    if (!empty($action['addresses'])) 
     590                        $script .= " :addresses " . self::escape_string($action['addresses']); 
     591                    if (!empty($action['subject'])) 
     592                        $script .= " :subject " . self::escape_string($action['subject']); 
     593                    if (!empty($action['handle'])) 
     594                        $script .= " :handle " . self::escape_string($action['handle']); 
     595                    if (!empty($action['from'])) 
     596                        $script .= " :from " . self::escape_string($action['from']); 
     597                    if (!empty($action['mime'])) 
    613598                        $script .= " :mime"; 
    614                     if (strpos($action['reason'], "\n")!==false) 
    615                         $script .= " text:\n" . $this->_escape_multiline_string($action['reason']) . "\n.\n;\n"; 
    616                     else 
    617                         $script .= " \"" . $this->_escape_string($action['reason']) . "\";\n"; 
     599                    $script .= " " . self::escape_string($action['reason']) . ";\n"; 
    618600                    break; 
    619601                } 
     
    658640        $i = 0; 
    659641        $content = array(); 
    660  
    661         // remove C comments 
    662         $script = preg_replace('|/\*.*?\*/|sm', '', $script); 
    663642 
    664643        // tokenize rules 
     
    689668    private function _tokenize_rule($content) 
    690669    { 
    691         $result = NULL; 
    692  
    693         if (preg_match('/^(if|elsif|else)\s+((true|false|not\s+true|allof|anyof|exists|header|not|size)(.*))\s+\{(.*)\}$/sm', 
    694             trim($content), $matches)) { 
    695  
    696             $tests = trim($matches[2]); 
    697  
    698             // disabled rule (false + comment): if false #..... 
    699             if ($matches[3] == 'false') { 
    700                 $tests = preg_replace('/^false\s+#\s+/', '', $tests); 
    701                 $disabled = true; 
    702             } 
    703             else 
    704                 $disabled = false; 
    705  
    706             list($tests, $join) = $this->_parse_tests($tests); 
    707             $actions = $this->_parse_actions(trim($matches[5])); 
    708  
    709             if ($tests && $actions) 
    710                 $result = array( 
    711                     'type'     => $matches[1], 
    712                     'tests'    => $tests, 
    713                     'actions'  => $actions, 
    714                     'join'     => $join, 
    715                     'disabled' => $disabled, 
     670        $cond = strtolower(self::tokenize($content, 1)); 
     671 
     672        if ($cond != 'if' && $cond != 'elsif' && $cond != 'else') { 
     673            return NULL; 
     674        } 
     675 
     676        $disabled = false; 
     677        $join     = false; 
     678 
     679        // disabled rule (false + comment): if false # ..... 
     680        if (preg_match('/^\s*false\s+#/i', $content)) { 
     681            $content = preg_replace('/^\s*false\s+#\s*/i', '', $content); 
     682            $disabled = true; 
     683        } 
     684 
     685        while (strlen($content)) { 
     686            $tokens = self::tokenize($content, true); 
     687            $separator = array_pop($tokens); 
     688 
     689            if (!empty($tokens)) { 
     690                $token = array_shift($tokens); 
     691            } 
     692            else { 
     693                $token = $separator; 
     694            } 
     695 
     696            $token = strtolower($token); 
     697 
     698            if ($token == 'not') { 
     699                $not = true; 
     700                $token = strtolower(array_shift($tokens)); 
     701            } 
     702            else { 
     703                $not = false; 
     704            } 
     705 
     706            switch ($token) { 
     707            case 'allof': 
     708                $join = true; 
     709                break; 
     710            case 'anyof': 
     711                break; 
     712 
     713            case 'size': 
     714                $size = array('test' => 'size', 'not'  => $not); 
     715                for ($i=0, $len=count($tokens); $i<$len; $i++) { 
     716                    if (!is_array($tokens[$i]) 
     717                        && preg_match('/^:(under|over)$/i', $tokens[$i]) 
     718                    ) { 
     719                        $size['type'] = strtolower(substr($tokens[$i], 1)); 
     720                    } 
     721                    else { 
     722                        $size['arg'] = $tokens[$i]; 
     723                    } 
     724                } 
     725 
     726                $tests[] = $size; 
     727                break; 
     728 
     729            case 'header': 
     730                $header = array('test' => 'header', 'not' => $not, 'arg1' => '', 'arg2' => ''); 
     731                for ($i=0, $len=count($tokens); $i<$len; $i++) { 
     732                    if (!is_array($tokens[$i]) && preg_match('/^:comparator$/i', $tokens[$i])) { 
     733                        $i++; 
     734                    } 
     735                    else if (!is_array($tokens[$i]) && preg_match('/^:(count|value)$/i', $tokens[$i])) { 
     736                        $header['type'] = strtolower(substr($tokens[$i], 1)) . '-' . $tokens[++$i]; 
     737                    } 
     738                    else if (!is_array($tokens[$i]) && preg_match('/^:(is|contains|matches)$/i', $tokens[$i])) { 
     739                        $header['type'] = strtolower(substr($tokens[$i], 1)); 
     740                    } 
     741                    else { 
     742                        $header['arg1'] = $header['arg2']; 
     743                        $header['arg2'] = $tokens[$i]; 
     744                    } 
     745                } 
     746 
     747                $tests[] = $header; 
     748                break; 
     749 
     750            case 'exists': 
     751                $tests[] = array('test' => 'exists', 'not'  => $not, 
     752                    'arg'  => array_pop($tokens)); 
     753                break; 
     754 
     755            case 'true': 
     756                $tests[] = array('test' => 'true', 'not'  => $not); 
     757                break; 
     758 
     759            case 'false': 
     760                $tests[] = array('test' => 'true', 'not'  => !$not); 
     761                break; 
     762            } 
     763 
     764            // goto actions... 
     765            if ($separator == '{') { 
     766                break; 
     767            } 
     768        } 
     769 
     770        // ...and actions block 
     771        if ($tests) { 
     772            $actions = $this->_parse_actions($content); 
     773        } 
     774 
     775        if ($tests && $actions) { 
     776            $result = array( 
     777                'type'     => $cond, 
     778                'tests'    => $tests, 
     779                'actions'  => $actions, 
     780                'join'     => $join, 
     781                'disabled' => $disabled, 
    716782            ); 
    717783        } 
     
    730796        $result = NULL; 
    731797 
    732         // supported actions 
    733         $patterns[] = '^\s*discard;'; 
    734         $patterns[] = '^\s*keep;'; 
    735         $patterns[] = '^\s*stop;'; 
    736         $patterns[] = '^\s*redirect\s+(.*?[^\\\]);'; 
    737         if (in_array('fileinto', $this->supported)) 
    738             $patterns[] = '^\s*fileinto\s+(.*?[^\\\]);'; 
    739         if (in_array('reject', $this->supported)) { 
    740             $patterns[] = '^\s*reject\s+text:(.*)\n\.\n;'; 
    741             $patterns[] = '^\s*reject\s+(.*?[^\\\]);'; 
    742             $patterns[] = '^\s*ereject\s+text:(.*)\n\.\n;'; 
    743             $patterns[] = '^\s*ereject\s+(.*?[^\\\]);'; 
    744         } 
    745         if (in_array('vacation', $this->supported)) 
    746             $patterns[] = '^\s*vacation\s+(.*?[^\\\]);'; 
    747  
    748         $pattern = '/(' . implode('\s*$)|(', $patterns) . '$\s*)/ms'; 
    749  
    750         // parse actions body 
    751         if (preg_match_all($pattern, $content, $mm, PREG_SET_ORDER)) { 
    752             foreach ($mm as $m) { 
    753                 $content = trim($m[0]); 
    754  
    755                 if(preg_match('/^(discard|keep|stop)/', $content, $matches)) { 
    756                     $result[] = array('type' => $matches[1]); 
    757                 } 
    758                 else if(preg_match('/^fileinto/', $content)) { 
    759                     $target = $m[sizeof($m)-1]; 
    760                     $copy = false; 
    761                     if (preg_match('/^:copy\s+/', $target)) { 
    762                         $target = preg_replace('/^:copy\s+/', '', $target); 
     798        while (strlen($content)) { 
     799            $tokens = self::tokenize($content, true); 
     800            $separator = array_pop($tokens); 
     801 
     802            if (!empty($tokens)) { 
     803                $token = array_shift($tokens); 
     804            } 
     805            else { 
     806                $token = $separator; 
     807            } 
     808 
     809            switch ($token) { 
     810            case 'discard': 
     811            case 'keep': 
     812            case 'stop': 
     813                $result[] = array('type' => $token); 
     814                break; 
     815 
     816            case 'fileinto': 
     817            case 'redirect': 
     818                $copy   = false; 
     819                $target = ''; 
     820 
     821                for ($i=0, $len=count($tokens); $i<$len; $i++) { 
     822                    if (strtolower($tokens[$i]) == ':copy') { 
    763823                        $copy = true; 
    764824                    } 
    765                     $result[] = array('type' => 'fileinto', 'copy' => $copy, 
    766                         'target' => $this->_parse_string($target)); 
    767                 } 
    768                 else if(preg_match('/^redirect/', $content)) { 
    769                     $target = $m[sizeof($m)-1]; 
    770                     $copy = false; 
    771                     if (preg_match('/^:copy\s+/', $target)) { 
    772                         $target = preg_replace('/^:copy\s+/', '', $target); 
    773                         $copy = true; 
    774                     } 
    775                     $result[] = array('type' => 'redirect', 'copy' => $copy, 
    776                         'target' => $this->_parse_string($target)); 
    777                 } 
    778                 else if(preg_match('/^(reject|ereject)\s+(.*);$/sm', $content, $matches)) { 
    779                     $result[] = array('type' => $matches[1], 'target' => $this->_parse_string($matches[2])); 
    780                 } 
    781                 else if(preg_match('/^vacation\s+(.*);$/sm', $content, $matches)) { 
    782                     $vacation = array('type' => 'vacation'); 
    783  
    784                     if (preg_match('/:days\s+([0-9]+)/', $content, $vm)) { 
    785                         $vacation['days'] = $vm[1]; 
    786                         $content = preg_replace('/:days\s+([0-9]+)/', '', $content); 
    787                     } 
    788                     if (preg_match('/:subject\s+"(.*?[^\\\])"/', $content, $vm)) { 
    789                         $vacation['subject'] = $vm[1]; 
    790                         $content = preg_replace('/:subject\s+"(.*?[^\\\])"/', '', $content); 
    791                     } 
    792                     if (preg_match('/:addresses\s+\[(.*?[^\\\])\]/', $content, $vm)) { 
    793                         $vacation['addresses'] = $this->_parse_list($vm[1]); 
    794                         $content = preg_replace('/:addresses\s+\[(.*?[^\\\])\]/', '', $content); 
    795                     } 
    796                     if (preg_match('/:handle\s+"(.*?[^\\\])"/', $content, $vm)) { 
    797                         $vacation['handle'] = $vm[1]; 
    798                         $content = preg_replace('/:handle\s+"(.*?[^\\\])"/', '', $content); 
    799                     } 
    800                     if (preg_match('/:from\s+"(.*?[^\\\])"/', $content, $vm)) { 
    801                         $vacation['from'] = $vm[1]; 
    802                         $content = preg_replace('/:from\s+"(.*?[^\\\])"/', '', $content); 
    803                     } 
    804  
    805                     $content = preg_replace('/^vacation/', '', $content); 
    806                     $content = preg_replace('/;$/', '', $content); 
    807                     $content = trim($content); 
    808  
    809                     if (preg_match('/^:mime/', $content, $vm)) { 
     825                    else { 
     826                        $target = $tokens[$i]; 
     827                    } 
     828                } 
     829 
     830                $result[] = array('type' => $token, 'copy' => $copy, 
     831                    'target' => $target); 
     832                break; 
     833 
     834            case 'reject': 
     835            case 'ereject': 
     836                $result[] = array('type' => $token, 'target' => array_pop($tokens)); 
     837                break; 
     838 
     839            case 'vacation': 
     840                $vacation = array('type' => 'vacation', 'reason' => array_pop($tokens)); 
     841 
     842                for ($i=0, $len=count($tokens); $i<$len; $i++) { 
     843                    $tok = strtolower($tokens[$i]); 
     844                    if ($tok == ':days') { 
     845                        $vacation['days'] = $tokens[++$i]; 
     846                    } 
     847                    else if ($tok == ':subject') { 
     848                        $vacation['subject'] = $tokens[++$i]; 
     849                    } 
     850                    else if ($tok == ':addresses') { 
     851                        $vacation['addresses'] = $tokens[++$i]; 
     852                    } 
     853                    else if ($tok == ':handle') { 
     854                        $vacation['handle'] = $tokens[++$i]; 
     855                    } 
     856                    else if ($tok == ':from') { 
     857                        $vacation['from'] = $tokens[++$i]; 
     858                    } 
     859                    else if ($tok == ':mime') { 
    810860                        $vacation['mime'] = true; 
    811                         $content = preg_replace('/^:mime/', '', $content); 
    812                     } 
    813  
    814                     $vacation['reason'] = $this->_parse_string($content); 
    815  
    816                     $result[] = $vacation; 
    817                 } 
     861                    } 
     862                } 
     863 
     864                $result[] = $vacation; 
     865                break; 
    818866            } 
    819867        } 
     
    823871 
    824872    /** 
    825      * Parse test/conditions section 
    826      * 
    827      * @param string Text 
    828      */ 
    829     private function _parse_tests($content) 
    830     { 
    831         $result = NULL; 
    832  
    833         // lists 
    834         if (preg_match('/^(allof|anyof)\s+\((.*)\)$/sm', $content, $matches)) { 
    835             $content = $matches[2]; 
    836             $join = $matches[1]=='allof' ? true : false; 
    837         } 
    838         else 
    839             $join = false; 
    840  
    841         // supported tests regular expressions 
    842         // TODO: comparators, envelope 
    843         $patterns[] = '(not\s+)?(exists)\s+\[(.*?[^\\\])\]'; 
    844         $patterns[] = '(not\s+)?(exists)\s+(".*?[^\\\]")'; 
    845         $patterns[] = '(not\s+)?(true)'; 
    846         $patterns[] = '(not\s+)?(size)\s+:(under|over)\s+([0-9]+[KGM]{0,1})'; 
    847         $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)((\s+))\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]'; 
    848         $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)((\s+))(".*?[^\\\]")\s+(".*?[^\\\]")'; 
    849         $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)((\s+))\[(.*?[^\\\]")\]\s+(".*?[^\\\]")'; 
    850         $patterns[] = '(not\s+)?(header)\s+:(contains|is|matches)((\s+))(".*?[^\\\]")\s+\[(.*?[^\\\]")\]'; 
    851                 $patterns[] = '(not\s+)?(header)\s+:(count\s+"[gtleqn]{2}"|value\s+"[gtleqn]{2}")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]\s+\[(.*?[^\\\]")\]'; 
    852                 $patterns[] = '(not\s+)?(header)\s+:(count\s+"[gtleqn]{2}"|value\s+"[gtleqn]{2}")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")\s+(".*?[^\\\]")'; 
    853                 $patterns[] = '(not\s+)?(header)\s+:(count\s+"[gtleqn]{2}"|value\s+"[gtleqn]{2}")(\s+:comparator\s+"(.*?[^\\\])")?\s+\[(.*?[^\\\]")\]\s+(".*?[^\\\]")'; 
    854                 $patterns[] = '(not\s+)?(header)\s+:(count\s+"[gtleqn]{2}"|value\s+"[gtleqn]{2}")(\s+:comparator\s+"(.*?[^\\\])")?\s+(".*?[^\\\]")\s+\[(.*?[^\\\]")\]'; 
    855  
    856         // join patterns... 
    857         $pattern = '/(' . implode(')|(', $patterns) . ')/'; 
    858  
    859         // ...and parse tests list 
    860         if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) { 
    861             foreach ($matches as $match) { 
    862                 $size = sizeof($match); 
    863  
    864                 if (preg_match('/^(not\s+)?size/', $match[0])) { 
    865                     $result[] = array( 
    866                         'test' => 'size', 
    867                         'not'  => $match[$size-4] ? true : false, 
    868                         'type' => $match[$size-2], // under/over 
    869                         'arg'  => $match[$size-1], // value 
    870                     ); 
    871                 } 
    872                 else if (preg_match('/^(not\s+)?header/', $match[0])) { 
    873                     $type = $match[$size-5]; 
    874                     if (preg_match('/^(count|value)\s+"([gtleqn]{2})"/', $type, $m)) 
    875                         $type = $m[1] . '-' . $m[2]; 
    876                      
    877                     $result[] = array( 
    878                         'test' => 'header', 
    879                         'type' => $type, // is/contains/matches 
    880                                                 'not'  => $match[$size-7] ? true : false, 
    881                         'arg1' => $this->_parse_list($match[$size-2]), // header(s) 
    882                         'arg2' => $this->_parse_list($match[$size-1]), // string(s) 
    883                     ); 
    884                 } 
    885                 else if (preg_match('/^(not\s+)?exists/', $match[0])) { 
    886                     $result[] = array( 
    887                         'test' => 'exists', 
    888                         'not'  => $match[$size-3] ? true : false, 
    889                         'arg'  => $this->_parse_list($match[$size-1]), // header(s) 
    890                     ); 
    891                 } 
    892                 else if (preg_match('/^(not\s+)?true/', $match[0])) { 
    893                     $result[] = array( 
    894                         'test' => 'true', 
    895                         'not'  => $match[$size-2] ? true : false, 
    896                     ); 
    897                 } 
    898             } 
    899         } 
    900  
    901         return array($result, $join); 
    902     } 
    903  
    904     /** 
    905      * Parse string value 
    906      * 
    907      * @param string Text 
    908      */ 
    909     private function _parse_string($content) 
    910     { 
    911         $text = ''; 
    912         $content = trim($content); 
     873     * Escape special chars into quoted string value or multi-line string 
     874     * or list of strings 
     875     * 
     876     * @param string $str Text or array (list) of strings 
     877     * 
     878     * @return string Result text 
     879     */ 
     880    static function escape_string($str) 
     881    { 
     882        if (is_array($str)) { 
     883            foreach($str as $idx => $val) 
     884                $str[$idx] = self::escape_string($val); 
     885 
     886            return '[' . implode(',', $str) . ']'; 
     887        } 
    913888 
    914889        // multi-line string 
    915         if (preg_match('/^(text:[\t\s\r]*\n)/', $content, $matches)) { 
    916             $content = substr($content, strlen($matches[1])); 
    917             $lines   = preg_split('/(\r?\n)/', $content, -1, PREG_SPLIT_DELIM_CAPTURE); 
    918             $text    = ''; 
    919  
    920             foreach ($lines as $line) { 
    921                 // terminator 
    922                 if ($line == '.') { 
     890        if (preg_match('/[\r\n\0]/', $str)) { 
     891            return sprintf("text:\n%s\n.\n", self::escape_multiline_string($str)); 
     892        } 
     893        // quoted-string 
     894        else { 
     895            $replace['/"/'] = '\\"'; 
     896            return '"'. preg_replace(array_keys($replace), array_values($replace), 
     897                $str) . '"'; 
     898        } 
     899    } 
     900 
     901    /** 
     902     * Escape special chars in multi-line string value 
     903     * 
     904     * @param string $str Text 
     905     * 
     906     * @return string Text 
     907     */ 
     908    static function escape_multiline_string($str) 
     909    { 
     910        $str = preg_split('/(\r?\n)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE); 
     911 
     912        foreach ($str as $idx => $line) { 
     913            // dot-stuffing 
     914            if (isset($line[0]) && $line[0] == '.') { 
     915                $str[$idx] = '.' . $line; 
     916            } 
     917        } 
     918 
     919        return implode($str); 
     920    } 
     921 
     922    /** 
     923     * Splits script into string tokens 
     924     * 
     925     * @param string &$str    The script 
     926     * @param mixed  $num     Number of tokens to return, 0 for all 
     927     *                        or True for all tokens until separator is found. 
     928     *                        Separator will be returned as last token. 
     929     * @param int    $in_list Enable to called recursively inside a list 
     930     * 
     931     * @return mixed Tokens array or string if $num=1 
     932     */ 
     933    static function tokenize(&$str, $num=0, $in_list=false) 
     934    { 
     935        $result = array(); 
     936 
     937        // remove spaces from the beginning of the string 
     938        while (($str = ltrim($str)) !== '' 
     939            && (!$num || $num === true || count($result) < $num) 
     940        ) { 
     941            switch ($str[0]) { 
     942 
     943            // Quoted string 
     944            case '"': 
     945                $len = strlen($str); 
     946 
     947                for ($pos=1; $pos<$len; $pos++) { 
     948                    if ($str[$pos] == '"') { 
     949                        break; 
     950                    } 
     951                    if ($str[$pos] == "\\") { 
     952                        if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") { 
     953                            $pos++; 
     954                        } 
     955                    } 
     956                } 
     957                if ($str[$pos] != '"') { 
     958                    // error 
     959                } 
     960                // we need to strip slashes for a quoted string 
     961                $result[] = stripslashes(substr($str, 1, $pos - 1)); 
     962                $str      = substr($str, $pos + 1); 
     963                break; 
     964 
     965            // Parenthesized list 
     966            case '[': 
     967                $str = substr($str, 1); 
     968                $result[] = self::tokenize($str, 0, true); 
     969                break; 
     970            case ']': 
     971                $str = substr($str, 1); 
     972                return $result; 
     973                break; 
     974 
     975            // list/test separator 
     976            case ',': 
     977            // command separator 
     978            case ';': 
     979            // block/tests-list 
     980            case '(': 
     981            case ')': 
     982            case '{': 
     983            case '}': 
     984                $sep = $str[0]; 
     985                $str = substr($str, 1); 
     986                if ($num === true) { 
     987                    $result[] = $sep; 
     988                    break 2;  
     989                } 
     990                break; 
     991 
     992            // bracket-comment 
     993            case '/': 
     994                if ($str[1] == '*') { 
     995                    if ($end_pos = strpos($str, '*/')) { 
     996                        $str = substr($str, $end_pos + 2); 
     997                    } 
     998                    else { 
     999                        // error 
     1000                        $str = ''; 
     1001                    } 
     1002                } 
     1003                break; 
     1004 
     1005            // hash-comment 
     1006            case '#': 
     1007                if ($lf_pos = strpos($str, "\n")) { 
     1008                    $str = substr($str, $lf_pos); 
    9231009                    break; 
    9241010                } 
    925                 // dot-stuffing 
    926                 if ($line[0] == '.' && $line[1] == '.') { 
    927                     $line = substr($line, 1); 
    928                 } 
    929                 $text .= $line; 
    930             } 
    931             $text = rtrim($text, "\r\n"); 
    932         } 
    933         // quoted string 
    934         else if (preg_match('/^"(.*)"$/', $content, $matches)) { 
    935             $text = str_replace('\"', '"', $matches[1]); 
    936         } 
    937  
    938         return $text; 
    939     } 
    940  
    941     /** 
    942      * Escape special chars in quoted string value 
    943      * 
    944      * @param string Text 
    945      */ 
    946     private function _escape_string($content) 
    947     { 
    948         $replace['/"/'] = '\\"'; 
    949  
    950         if (is_array($content)) { 
    951             for ($x=0, $y=sizeof($content); $x<$y; $x++) { 
    952                 $content[$x] = preg_replace(array_keys($replace), 
    953                     array_values($replace), $content[$x]); 
    954             } 
    955  
    956             return $content; 
    957         } 
    958         else { 
    959             return preg_replace(array_keys($replace), array_values($replace), $content); 
    960         } 
    961     } 
    962  
    963     /** 
    964      * Escape special chars in multi-line string value 
    965      * 
    966      * @param string Text 
    967      */ 
    968     private function _escape_multiline_string($content) 
    969     { 
    970         $lines = preg_split('/(\r?\n)/', $content, -1, PREG_SPLIT_DELIM_CAPTURE); 
    971  
    972         foreach ($lines as $idx => $line) { 
    973             // dot-stuffing 
    974             if ($line[0] == '.') { 
    975                 $lines[$idx] = '.' . $line; 
    976             } 
    977         } 
    978  
    979         return implode($lines); 
    980     } 
    981  
    982     /** 
    983      * Parse string or list of strings to string or array of strings 
    984      * 
    985      * @param string Text 
    986      */ 
    987     private function _parse_list($content) 
    988     { 
    989         $result = array(); 
    990  
    991         for ($x=0, $len=strlen($content); $x<$len; $x++) { 
    992             switch ($content[$x]) { 
    993             case '\\': 
    994                 $str .= $content[++$x]; 
    995                 break; 
    996             case '"': 
    997                 if (isset($str)) { 
     1011                else { 
     1012                    $str = ''; 
     1013                } 
     1014 
     1015            // String atom 
     1016            default: 
     1017                // empty or one character 
     1018                if ($str === '') { 
     1019                    break 2; 
     1020                } 
     1021                if (strlen($str) < 2) { 
    9981022                    $result[] = $str; 
    999                     unset($str); 
    1000                 } 
    1001                 else 
    10021023                    $str = ''; 
    1003                 break; 
    1004             default: 
    1005                 if(isset($str)) 
    1006                     $str .= $content[$x]; 
    1007             break; 
    1008             } 
    1009         } 
    1010  
    1011         if (sizeof($result)>1) 
    1012             return $result; 
    1013         else if (sizeof($result) == 1) 
    1014             return $result[0]; 
    1015         else 
    1016             return NULL; 
    1017     } 
    1018  
    1019     /** 
    1020      * Convert array of elements to list of strings 
    1021      * 
    1022      * @param string Text 
    1023      */ 
    1024     private function _print_list($list) 
    1025     { 
    1026         $list = (array) $list; 
    1027         foreach($list as $idx => $val) 
    1028             $list[$idx] = $this->_escape_string($val); 
    1029  
    1030         return '["' . implode('","', $list) . '"]'; 
    1031     } 
     1024                    break; 
     1025                } 
     1026 
     1027                // tag/identifier/number 
     1028                if (preg_match('/^([a-z0-9:_]+)/i', $str, $m)) { 
     1029                    $str = substr($str, strlen($m[1])); 
     1030 
     1031                    if ($m[1] != 'text:') { 
     1032                        $result[] = $m[1]; 
     1033                    } 
     1034                    // multiline string 
     1035                    else { 
     1036                        // possible hash-comment after "text:" 
     1037                        if (preg_match('/^( |\t)*(#[^\n]+)?\n/', $str, $m)) { 
     1038                            $str = substr($str, strlen($m[0])); 
     1039                        } 
     1040                        // get text until alone dot in a line 
     1041                        if (preg_match('/^(.*)\r?\n\.\r?\n/sU', $str, $m)) { 
     1042                            $text = $m[1]; 
     1043                            // remove dot-stuffing 
     1044                            $text = str_replace("\n..", "\n.", $text); 
     1045                            $str = substr($str, strlen($m[0])); 
     1046                        } 
     1047                        else { 
     1048                            $text = ''; 
     1049                        } 
     1050 
     1051                        $result[] = $text; 
     1052                    } 
     1053                } 
     1054 
     1055                break; 
     1056            } 
     1057        } 
     1058 
     1059        return $num === 1 ? (isset($result[0]) ? $result[0] : NULL) : $result; 
     1060    } 
     1061 
    10321062} 
Note: See TracChangeset for help on using the changeset viewer.