Aleksander Machniak
2015-11-05 e7d1a80a800f6f08c0a683d2be04b0db2a1f6523
commit | author | age
7ac944 1 <?php
AM 2
3 /**
4  +-----------------------------------------------------------------------+
5  | This file is part of the Roundcube Webmail client                     |
6  | Copyright (C) 2008-2012, The Roundcube Dev Team                       |
7  |                                                                       |
8  | Licensed under the GNU General Public License version 3 or            |
9  | any later version with exceptions for skins & plugins.                |
10  | See the README file for a full license statement.                     |
11  |                                                                       |
12  | PURPOSE:                                                              |
13  |   Utility class providing HTML sanityzer (based on Washtml class)     |
14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16  | Author: Aleksander Machniak <alec@alec.pl>                            |
17  | Author: Frederic Motte <fmotte@ubixis.com>                            |
18  +-----------------------------------------------------------------------+
19  */
20
21 /**
22  *                Washtml, a HTML sanityzer.
23  *
24  * Copyright (c) 2007 Frederic Motte <fmotte@ubixis.com>
25  * All rights reserved.
26  *
27  * Redistribution and use in source and binary forms, with or without
28  * modification, are permitted provided that the following conditions
29  * are met:
30  * 1. Redistributions of source code must retain the above copyright
31  *    notice, this list of conditions and the following disclaimer.
32  * 2. Redistributions in binary form must reproduce the above copyright
33  *    notice, this list of conditions and the following disclaimer in the
34  *    documentation and/or other materials provided with the distribution.
35  *
36  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
37  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
38  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
39  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
40  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
41  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
45  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46  *
47  * OVERVIEW:
48  *
49  * Wahstml take an untrusted HTML and return a safe html string.
50  *
51  * SYNOPSIS:
52  *
53  * $washer = new washtml($config);
54  * $washer->wash($html);
55  * It return a sanityzed string of the $html parameter without html and head tags.
56  * $html is a string containing the html code to wash.
57  * $config is an array containing options:
58  *   $config['allow_remote'] is a boolean to allow link to remote images.
59  *   $config['blocked_src'] string with image-src to be used for blocked remote images
60  *   $config['show_washed'] is a boolean to include washed out attributes as x-washed
61  *   $config['cid_map'] is an array where cid urls index urls to replace them.
62  *   $config['charset'] is a string containing the charset of the HTML document if it is not defined in it.
63  * $washer->extlinks is a reference to a boolean that is set to true if remote images were removed. (FE: show remote images link)
64  *
65  * INTERNALS:
66  *
67  * Only tags and attributes in the static lists $html_elements and $html_attributes
68  * are kept, inline styles are also filtered: all style identifiers matching
69  * /[a-z\-]/i are allowed. Values matching colors, sizes, /[a-z\-]/i and safe
70  * urls if allowed and cid urls if mapped are kept.
71  *
72  * Roundcube Changes:
73  * - added $block_elements
74  * - changed $ignore_elements behaviour
75  * - added RFC2397 support
76  * - base URL support
77  * - invalid HTML comments removal before parsing
78  * - "fixing" unitless CSS values for XHTML output
79  * - base url resolving
80  */
81
82 /**
83  * Utility class providing HTML sanityzer
84  *
85  * @package    Framework
86  * @subpackage Utils
87  */
88 class rcube_washtml
89 {
90     /* Allowed HTML elements (default) */
91     static $html_elements = array('a', 'abbr', 'acronym', 'address', 'area', 'b',
92         'basefont', 'bdo', 'big', 'blockquote', 'br', 'caption', 'center',
93         'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl',
94         'dt', 'em', 'fieldset', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i',
95         'ins', 'label', 'legend', 'li', 'map', 'menu', 'nobr', 'ol', 'p', 'pre', 'q',
96         's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
97         'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'wbr', 'img',
98         // form elements
99         'button', 'input', 'textarea', 'select', 'option', 'optgroup'
100     );
101
102     /* Ignore these HTML tags and their content */
103     static $ignore_elements = array('script', 'applet', 'embed', 'object', 'style');
104
105     /* Allowed HTML attributes */
106     static $html_attribs = array('name', 'class', 'title', 'alt', 'width', 'height',
107         'align', 'nowrap', 'col', 'row', 'id', 'rowspan', 'colspan', 'cellspacing',
108         'cellpadding', 'valign', 'bgcolor', 'color', 'border', 'bordercolorlight',
109         'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border',
110         'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace',
111         'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media',
112         // attributes of form elements
113         'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value'
114     );
115
cb3e2f 116     /* Elements which could be empty and be returned in short form (<tag />) */
AM 117     static $void_elements = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr',
118         'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
7ac944 119     );
AM 120
121     /* State for linked objects in HTML */
122     public $extlinks = false;
123
124     /* Current settings */
125     private $config = array();
126
127     /* Registered callback functions for tags */
128     private $handlers = array();
129
130     /* Allowed HTML elements */
131     private $_html_elements = array();
132
133     /* Ignore these HTML tags but process their content */
134     private $_ignore_elements = array();
135
cb3e2f 136     /* Elements which could be empty and be returned in short form (<tag />) */
AM 137     private $_void_elements = array();
7ac944 138
AM 139     /* Allowed HTML attributes */
140     private $_html_attribs = array();
141
a89940 142     /* Max nesting level */
AM 143     private $max_nesting_level;
144
7ac944 145
AM 146     /**
147      * Class constructor
148      */
149     public function __construct($p = array())
150     {
151         $this->_html_elements   = array_flip((array)$p['html_elements']) + array_flip(self::$html_elements) ;
152         $this->_html_attribs    = array_flip((array)$p['html_attribs']) + array_flip(self::$html_attribs);
153         $this->_ignore_elements = array_flip((array)$p['ignore_elements']) + array_flip(self::$ignore_elements);
cb3e2f 154         $this->_void_elements   = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements);
7ac944 155
cb3e2f 156         unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']);
7ac944 157
AM 158         $this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array());
159     }
160
161     /**
162      * Register a callback function for a certain tag
163      */
164     public function add_callback($tagName, $callback)
165     {
166         $this->handlers[$tagName] = $callback;
167     }
168
169     /**
170      * Check CSS style
171      */
172     private function wash_style($style)
173     {
05d419 174         $result = array();
7ac944 175
AM 176         foreach (explode(';', $style) as $declaration) {
177             if (preg_match('/^\s*([a-z\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) {
178                 $cssid = $match[1];
179                 $str   = $match[2];
180                 $value = '';
181
05d419 182                 foreach ($this->explode_style($str) as $val) {
AM 183                     if (preg_match('/^url\(/i', $val)) {
184                         if (preg_match('/^url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
185                             $url = $match[1];
186                             if (($src = $this->config['cid_map'][$url])
187                                 || ($src = $this->config['cid_map'][$this->config['base_url'].$url])
188                             ) {
189                                 $value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')';
7ac944 190                             }
05d419 191                             else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $url, $m)) {
AM 192                                 if ($this->config['allow_remote']) {
193                                     $value .= ' url('.htmlspecialchars($m[0], ENT_QUOTES).')';
194                                 }
195                                 else {
196                                     $this->extlinks = true;
197                                 }
7ac944 198                             }
05d419 199                             else if (preg_match('/^data:.+/i', $url)) { // RFC2397
AM 200                                 $value .= ' url('.htmlspecialchars($url, ENT_QUOTES).')';
201                             }
7ac944 202                         }
AM 203                     }
05d419 204                     else if (!preg_match('/^(behavior|expression)/i', $val)) {
7ac944 205                         // whitelist ?
05d419 206                         $value .= ' ' . $val;
7ac944 207
AM 208                         // #1488535: Fix size units, so width:800 would be changed to width:800px
49e260 209                         if (preg_match('/^(left|right|top|bottom|width|height)/i', $cssid)
05d419 210                             && preg_match('/^[0-9]+$/', $val)
7ac944 211                         ) {
AM 212                             $value .= 'px';
213                         }
214                     }
215                 }
216
217                 if (isset($value[0])) {
05d419 218                     $result[] = $cssid . ':' . $value;
7ac944 219                 }
AM 220             }
221         }
222
05d419 223         return implode('; ', $result);
7ac944 224     }
AM 225
226     /**
227      * Take a node and return allowed attributes and check values
228      */
229     private function wash_attribs($node)
230     {
231         $t = '';
232         $washed = '';
233
234         foreach ($node->attributes as $key => $plop) {
235             $key   = strtolower($key);
236             $value = $node->getAttribute($key);
237
238             if (isset($this->_html_attribs[$key]) ||
1f910c 239                 ($key == 'href' && ($value = trim($value))
AM 240                     && !preg_match('!^(javascript|vbscript|data:text)!i', $value)
7ac944 241                     && preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value))
AM 242             ) {
243                 $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
244             }
245             else if ($key == 'style' && ($style = $this->wash_style($value))) {
049940 246                 // replace double quotes to prevent syntax error and XSS issues (#1490227)
AM 247                 $t .= ' style="' . str_replace('"', '&quot;', $style) . '"';
7ac944 248             }
AM 249             else if ($key == 'background' || ($key == 'src' && strtolower($node->tagName) == 'img')) { //check tagName anyway
250                 if (($src = $this->config['cid_map'][$value])
251                     || ($src = $this->config['cid_map'][$this->config['base_url'].$value])
252                 ) {
253                     $t .= ' ' . $key . '="' . htmlspecialchars($src, ENT_QUOTES) . '"';
254                 }
255                 else if (preg_match('/^(http|https|ftp):.+/i', $value)) {
256                     if ($this->config['allow_remote']) {
257                         $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
258                     }
259                     else {
260                         $this->extlinks = true;
261                         if ($this->config['blocked_src']) {
262                             $t .= ' ' . $key . '="' . htmlspecialchars($this->config['blocked_src'], ENT_QUOTES) . '"';
263                         }
264                     }
265                 }
266                 else if (preg_match('/^data:.+/i', $value)) { // RFC2397
267                     $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
268                 }
269             }
270             else {
271                 $washed .= ($washed ? ' ' : '') . $key;
272             }
273         }
274
275         return $t . ($washed && $this->config['show_washed'] ? ' x-washed="'.$washed.'"' : '');
276     }
277
278     /**
279      * The main loop that recurse on a node tree.
7883ec 280      * It output only allowed tags with allowed attributes and allowed inline styles
AM 281      *
282      * @param DOMNode $node  HTML element
283      * @param int     $level Recurrence level (safe initial value found empirically)
7ac944 284      */
7883ec 285     private function dumpHtml($node, $level = 20)
7ac944 286     {
AM 287         if (!$node->hasChildNodes()) {
288             return '';
a89940 289         }
AM 290
291         $level++;
292
293         if ($this->max_nesting_level > 0 && $level == $this->max_nesting_level - 1) {
294             // log error message once
295             if (!$this->max_nesting_level_error) {
296                 $this->max_nesting_level_error = true;
297                 rcube::raise_error(array('code' => 500, 'type' => 'php',
298                     'line' => __LINE__, 'file' => __FILE__,
299                     'message' => "Maximum nesting level exceeded (xdebug.max_nesting_level={$this->max_nesting_level})"),
300                     true, false);
301             }
302             return '<!-- ignored -->';
7ac944 303         }
AM 304
305         $node = $node->firstChild;
306         $dump = '';
307
308         do {
e7d1a8 309             switch ($node->nodeType) {
7ac944 310             case XML_ELEMENT_NODE: //Check element
AM 311                 $tagName = strtolower($node->tagName);
312                 if ($callback = $this->handlers[$tagName]) {
313                     $dump .= call_user_func($callback, $tagName,
a89940 314                         $this->wash_attribs($node), $this->dumpHtml($node, $level), $this);
7ac944 315                 }
AM 316                 else if (isset($this->_html_elements[$tagName])) {
a89940 317                     $content = $this->dumpHtml($node, $level);
7ac944 318                     $dump .= '<' . $tagName . $this->wash_attribs($node) .
cb3e2f 319                         ($content === '' && isset($this->_void_elements[$tagName]) ? ' />' : ">$content</$tagName>");
7ac944 320                 }
AM 321                 else if (isset($this->_ignore_elements[$tagName])) {
322                     $dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' not allowed -->';
323                 }
324                 else {
325                     $dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' ignored -->';
a89940 326                     $dump .= $this->dumpHtml($node, $level); // ignore tags not its content
7ac944 327                 }
AM 328                 break;
329
330             case XML_CDATA_SECTION_NODE:
331                 $dump .= $node->nodeValue;
332                 break;
333
334             case XML_TEXT_NODE:
335                 $dump .= htmlspecialchars($node->nodeValue);
336                 break;
337
338             case XML_HTML_DOCUMENT_NODE:
a89940 339                 $dump .= $this->dumpHtml($node, $level);
7ac944 340                 break;
AM 341             }
e7d1a8 342         }
AM 343         while($node = $node->nextSibling);
7ac944 344
AM 345         return $dump;
346     }
347
348     /**
349      * Main function, give it untrusted HTML, tell it if you allow loading
350      * remote images and give it a map to convert "cid:" urls.
351      */
352     public function wash($html)
353     {
354         // Charset seems to be ignored (probably if defined in the HTML document)
355         $node = new DOMDocument('1.0', $this->config['charset']);
356         $this->extlinks = false;
357
358         $html = $this->cleanup($html);
359
360         // Find base URL for images
361         if (preg_match('/<base\s+href=[\'"]*([^\'"]+)/is', $html, $matches)) {
362             $this->config['base_url'] = $matches[1];
363         }
364         else {
365             $this->config['base_url'] = '';
366         }
a89940 367
AM 368         // Detect max nesting level (for dumpHTML) (#1489110)
369         $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
7ac944 370
bfd24f 371         // Use optimizations if supported
AM 372         if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
373             @$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT);
374         }
375         else {
376             @$node->loadHTML($html);
377         }
378
7ac944 379         return $this->dumpHtml($node);
AM 380     }
381
382     /**
383      * Getter for config parameters
384      */
385     public function get_config($prop)
386     {
387         return $this->config[$prop];
388     }
389
390     /**
391      * Clean HTML input
392      */
393     private function cleanup($html)
394     {
395         // special replacements (not properly handled by washtml class)
396         $html_search = array(
397             '/(<\/nobr>)(\s+)(<nobr>)/i',       // space(s) between <NOBR>
398             '/<title[^>]*>[^<]*<\/title>/i',    // PHP bug #32547 workaround: remove title tag
399             '/^(\0\0\xFE\xFF|\xFF\xFE\0\0|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)/',    // byte-order mark (only outlook?)
400             '/<html\s[^>]+>/i',                 // washtml/DOMDocument cannot handle xml namespaces
401         );
402
403         $html_replace = array(
404             '\\1'.' &nbsp; '.'\\3',
405             '',
406             '',
407             '<html>',
408         );
409         $html = preg_replace($html_search, $html_replace, trim($html));
410
b6a640 411         //-> Replace all of those weird MS Word quotes and other high characters
c72507 412         $badwordchars = array(
b6a640 413             "\xe2\x80\x98", // left single quote
R 414             "\xe2\x80\x99", // right single quote
415             "\xe2\x80\x9c", // left double quote
416             "\xe2\x80\x9d", // right double quote
417             "\xe2\x80\x94", // em dash
418             "\xe2\x80\xa6" // elipses
419         );
c72507 420         $fixedwordchars = array(
b6a640 421             "'",
R 422             "'",
423             '"',
424             '"',
425             '&mdash;',
426             '...'
427         );
c72507 428         $html = str_replace($badwordchars, $fixedwordchars, $html);
b6a640 429
7ac944 430         // PCRE errors handling (#1486856), should we use something like for every preg_* use?
AM 431         if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
432             $errstr = "Could not clean up HTML message! PCRE Error: $preg_error.";
433
434             if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) {
435                 $errstr .= " Consider raising pcre.backtrack_limit!";
436             }
437             if ($preg_error == PREG_RECURSION_LIMIT_ERROR) {
438                 $errstr .= " Consider raising pcre.recursion_limit!";
439             }
440
441             rcube::raise_error(array('code' => 620, 'type' => 'php',
442                 'line' => __LINE__, 'file' => __FILE__,
443                 'message' => $errstr), true, false);
a89940 444
7ac944 445             return '';
AM 446         }
447
448         // fix (unknown/malformed) HTML tags before "wash"
ffec85 449         $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html);
7ac944 450
AM 451         // Remove invalid HTML comments (#1487759)
452         // Don't remove valid conditional comments
1bce14 453         // Don't remove MSOutlook (<!-->) conditional comments (#1489004)
2d233b 454         $html = preg_replace('/<!--[^-<>\[\n]+>/', '', $html);
c72507 455
AM 456         // fix broken nested lists
457         self::fix_broken_lists($html);
7ac944 458
AM 459         // turn relative into absolute urls
460         $html = self::resolve_base($html);
461
462         return $html;
463     }
464
465     /**
466      * Callback function for HTML tags fixing
467      */
468     public static function html_tag_callback($matches)
469     {
470         $tagname = $matches[2];
471         $tagname = preg_replace(array(
472             '/:.*$/',               // Microsoft's Smart Tags <st1:xxxx>
473             '/[^a-z0-9_\[\]\!-]/i', // forbidden characters
474         ), '', $tagname);
475
ffec85 476         // fix invalid closing tags - remove any attributes (#1489446)
AM 477         if ($matches[1] == '</') {
478             $matches[3] = '';
479         }
480
481         return $matches[1] . $tagname . $matches[3];
7ac944 482     }
AM 483
484     /**
485      * Convert all relative URLs according to a <base> in HTML
486      */
487     public static function resolve_base($body)
488     {
489         // check for <base href=...>
490         if (preg_match('!(<base.*href=["\']?)([hftps]{3,5}://[a-z0-9/.%-]+)!i', $body, $regs)) {
491             $replacer = new rcube_base_replacer($regs[2]);
492             $body     = $replacer->replace($body);
493         }
494
495         return $body;
496     }
497
c72507 498     /**
AM 499      * Fix broken nested lists, they are not handled properly by DOMDocument (#1488768)
500      */
501     public static function fix_broken_lists(&$html)
502     {
503         // do two rounds, one for <ol>, one for <ul>
504         foreach (array('ol', 'ul') as $tag) {
505             $pos = 0;
506             while (($pos = stripos($html, '<' . $tag, $pos)) !== false) {
507                 $pos++;
508
509                 // make sure this is an ol/ul tag
510                 if (!in_array($html[$pos+2], array(' ', '>'))) {
511                     continue;
512                 }
513
514                 $p      = $pos;
515                 $in_li  = false;
516                 $li_pos = 0;
517
518                 while (($p = strpos($html, '<', $p)) !== false) {
519                     $tt = strtolower(substr($html, $p, 4));
520
521                     // li open tag
522                     if ($tt == '<li>' || $tt == '<li ') {
523                         $in_li = true;
524                         $p += 4;
525                     }
526                     // li close tag
527                     else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) {
528                         $li_pos = $p;
529                         $p += 4;
530                         $in_li = false;
531                     }
532                     // ul/ol closing tag
533                     else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) {
534                         break;
535                     }
536                     // nested ol/ul element out of li
537                     else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) {
538                         // find closing tag of this ul/ol element
539                         $element = substr($tt, 1, 2);
540                         $cpos    = $p;
541                         do {
542                             $tpos = stripos($html, '<' . $element, $cpos+1);
543                             $cpos = stripos($html, '</' . $element, $cpos+1);
544                         }
545                         while ($tpos !== false && $cpos !== false && $cpos > $tpos);
546
547                         // not found, this is invalid HTML, skip it
548                         if ($cpos === false) {
549                             break;
550                         }
551
552                         // get element content
553                         $end     = strpos($html, '>', $cpos);
554                         $len     = $end - $p + 1;
555                         $element = substr($html, $p, $len);
556
557                         // move element to the end of the last li
558                         $html    = substr_replace($html, '', $p, $len);
559                         $html    = substr_replace($html, $element, $li_pos, 0);
560
561                         $p = $end;
562                     }
563                     else {
564                         $p++;
565                     }
566                 }
567             }
568         }
569     }
05d419 570
AM 571     /**
572      * Explode css style value
573      */
574     protected function explode_style($style)
575     {
576         $style = trim($style);
577
578         // first remove comments
579         $pos = 0;
580         while (($pos = strpos($style, '/*', $pos)) !== false) {
581             $end = strpos($style, '*/', $pos+2);
582
583             if ($end === false) {
584                 $style = substr($style, 0, $pos);
585             }
586             else {
587                 $style = substr_replace($style, '', $pos, $end - $pos + 2);
588             }
589         }
590
591         $strlen = strlen($style);
592         $result = array();
593
594         // explode value
595         for ($p=$i=0; $i < $strlen; $i++) {
596             if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
597                 if ($q == $style[$i]) {
598                     $q = false;
599                 }
600                 else if (!$q) {
601                     $q = $style[$i];
602                 }
603             }
604
605             if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
606                 $result[] = substr($style, $p, $i - $p);
607                 $p = $i + 1;
608             }
609         }
610
611         $result[] = (string) substr($style, $p);
612
613         return $result;
614     }
c72507 615 }