Aleksander Machniak
2015-11-05 c7c09f85d9ccab83f720d1f938035884b9db5d6a
commit | author | age
7ac944 1 <?php
AM 2
7b9245 3 /*
7ac944 4  +-----------------------------------------------------------------------+
AM 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
7b9245 21 /*
7ac944 22  *                Washtml, a HTML sanityzer.
AM 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',
c5bfe6 98         'video', 'source',
7ac944 99         // form elements
AM 100         'button', 'input', 'textarea', 'select', 'option', 'optgroup'
101     );
102
103     /* Ignore these HTML tags and their content */
104     static $ignore_elements = array('script', 'applet', 'embed', 'object', 'style');
105
106     /* Allowed HTML attributes */
107     static $html_attribs = array('name', 'class', 'title', 'alt', 'width', 'height',
108         'align', 'nowrap', 'col', 'row', 'id', 'rowspan', 'colspan', 'cellspacing',
109         'cellpadding', 'valign', 'bgcolor', 'color', 'border', 'bordercolorlight',
110         'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border',
111         'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace',
112         'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media',
113         // attributes of form elements
114         'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value'
115     );
116
cb3e2f 117     /* Elements which could be empty and be returned in short form (<tag />) */
AM 118     static $void_elements = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr',
119         'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
7ac944 120     );
AM 121
122     /* State for linked objects in HTML */
123     public $extlinks = false;
124
125     /* Current settings */
126     private $config = array();
127
128     /* Registered callback functions for tags */
129     private $handlers = array();
130
131     /* Allowed HTML elements */
132     private $_html_elements = array();
133
134     /* Ignore these HTML tags but process their content */
135     private $_ignore_elements = array();
136
cb3e2f 137     /* Elements which could be empty and be returned in short form (<tag />) */
AM 138     private $_void_elements = array();
7ac944 139
AM 140     /* Allowed HTML attributes */
141     private $_html_attribs = array();
142
a89940 143     /* Max nesting level */
AM 144     private $max_nesting_level;
145
7ac944 146
AM 147     /**
148      * Class constructor
149      */
150     public function __construct($p = array())
151     {
152         $this->_html_elements   = array_flip((array)$p['html_elements']) + array_flip(self::$html_elements) ;
153         $this->_html_attribs    = array_flip((array)$p['html_attribs']) + array_flip(self::$html_attribs);
154         $this->_ignore_elements = array_flip((array)$p['ignore_elements']) + array_flip(self::$ignore_elements);
cb3e2f 155         $this->_void_elements   = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements);
7ac944 156
cb3e2f 157         unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']);
7ac944 158
AM 159         $this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array());
160     }
161
162     /**
163      * Register a callback function for a certain tag
164      */
165     public function add_callback($tagName, $callback)
166     {
167         $this->handlers[$tagName] = $callback;
168     }
169
170     /**
171      * Check CSS style
172      */
173     private function wash_style($style)
174     {
f96fec 175         $result = array();
7ac944 176
ca7fc7 177         // Remove unwanted white-space characters so regular expressions below work better
AM 178         $style = preg_replace('/[\n\r\s\t]+/', ' ', $style);
179
7ac944 180         foreach (explode(';', $style) as $declaration) {
AM 181             if (preg_match('/^\s*([a-z\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) {
182                 $cssid = $match[1];
183                 $str   = $match[2];
184                 $value = '';
185
f96fec 186                 foreach ($this->explode_style($str) as $val) {
AM 187                     if (preg_match('/^url\(/i', $val)) {
188                         if (preg_match('/^url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
189                             $url = $match[1];
190                             if (($src = $this->config['cid_map'][$url])
191                                 || ($src = $this->config['cid_map'][$this->config['base_url'].$url])
192                             ) {
193                                 $value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')';
7ac944 194                             }
f96fec 195                             else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $url, $m)) {
AM 196                                 if ($this->config['allow_remote']) {
197                                     $value .= ' url('.htmlspecialchars($m[0], ENT_QUOTES).')';
198                                 }
199                                 else {
200                                     $this->extlinks = true;
201                                 }
7ac944 202                             }
f96fec 203                             else if (preg_match('/^data:.+/i', $url)) { // RFC2397
AM 204                                 $value .= ' url('.htmlspecialchars($url, ENT_QUOTES).')';
205                             }
7ac944 206                         }
AM 207                     }
f96fec 208                     else if (!preg_match('/^(behavior|expression)/i', $val)) {
7ac944 209                         // whitelist ?
f96fec 210                         $value .= ' ' . $val;
7ac944 211
AM 212                         // #1488535: Fix size units, so width:800 would be changed to width:800px
5bf83d 213                         if (preg_match('/^(left|right|top|bottom|width|height)/i', $cssid)
f96fec 214                             && preg_match('/^[0-9]+$/', $val)
7ac944 215                         ) {
AM 216                             $value .= 'px';
217                         }
218                     }
219                 }
220
221                 if (isset($value[0])) {
f96fec 222                     $result[] = $cssid . ':' . $value;
7ac944 223                 }
AM 224             }
225         }
226
f96fec 227         return implode('; ', $result);
7ac944 228     }
AM 229
230     /**
231      * Take a node and return allowed attributes and check values
232      */
233     private function wash_attribs($node)
234     {
235         $t = '';
236         $washed = '';
237
238         foreach ($node->attributes as $key => $plop) {
239             $key   = strtolower($key);
240             $value = $node->getAttribute($key);
241
242             if (isset($this->_html_attribs[$key]) ||
1f910c 243                 ($key == 'href' && ($value = trim($value))
AM 244                     && !preg_match('!^(javascript|vbscript|data:text)!i', $value)
7ac944 245                     && preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value))
AM 246             ) {
247                 $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
248             }
249             else if ($key == 'style' && ($style = $this->wash_style($value))) {
786aa0 250                 // replace double quotes to prevent syntax error and XSS issues (#1490227)
AM 251                 $t .= ' style="' . str_replace('"', '&quot;', $style) . '"';
7ac944 252             }
c5bfe6 253             else if ($key == 'background'
AM 254                 || ($key == 'src' && preg_match('/^(img|source)$/i', $node->tagName))
255                 || ($key == 'poster' && strtolower($node->tagName) == 'video')
256             ) {
7ac944 257                 if (($src = $this->config['cid_map'][$value])
AM 258                     || ($src = $this->config['cid_map'][$this->config['base_url'].$value])
259                 ) {
260                     $t .= ' ' . $key . '="' . htmlspecialchars($src, ENT_QUOTES) . '"';
261                 }
262                 else if (preg_match('/^(http|https|ftp):.+/i', $value)) {
263                     if ($this->config['allow_remote']) {
264                         $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
265                     }
266                     else {
267                         $this->extlinks = true;
268                         if ($this->config['blocked_src']) {
269                             $t .= ' ' . $key . '="' . htmlspecialchars($this->config['blocked_src'], ENT_QUOTES) . '"';
270                         }
271                     }
272                 }
273                 else if (preg_match('/^data:.+/i', $value)) { // RFC2397
274                     $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
275                 }
276             }
277             else {
278                 $washed .= ($washed ? ' ' : '') . $key;
279             }
280         }
281
282         return $t . ($washed && $this->config['show_washed'] ? ' x-washed="'.$washed.'"' : '');
283     }
284
285     /**
286      * The main loop that recurse on a node tree.
c77a84 287      * It output only allowed tags with allowed attributes and allowed inline styles
AM 288      *
289      * @param DOMNode $node  HTML element
290      * @param int     $level Recurrence level (safe initial value found empirically)
7ac944 291      */
c77a84 292     private function dumpHtml($node, $level = 20)
7ac944 293     {
AM 294         if (!$node->hasChildNodes()) {
295             return '';
a89940 296         }
AM 297
298         $level++;
299
300         if ($this->max_nesting_level > 0 && $level == $this->max_nesting_level - 1) {
301             // log error message once
302             if (!$this->max_nesting_level_error) {
303                 $this->max_nesting_level_error = true;
304                 rcube::raise_error(array('code' => 500, 'type' => 'php',
305                     'line' => __LINE__, 'file' => __FILE__,
306                     'message' => "Maximum nesting level exceeded (xdebug.max_nesting_level={$this->max_nesting_level})"),
307                     true, false);
308             }
309             return '<!-- ignored -->';
7ac944 310         }
AM 311
312         $node = $node->firstChild;
313         $dump = '';
314
315         do {
c7c09f 316             switch ($node->nodeType) {
7ac944 317             case XML_ELEMENT_NODE: //Check element
AM 318                 $tagName = strtolower($node->tagName);
319                 if ($callback = $this->handlers[$tagName]) {
320                     $dump .= call_user_func($callback, $tagName,
a89940 321                         $this->wash_attribs($node), $this->dumpHtml($node, $level), $this);
7ac944 322                 }
AM 323                 else if (isset($this->_html_elements[$tagName])) {
a89940 324                     $content = $this->dumpHtml($node, $level);
7ac944 325                     $dump .= '<' . $tagName . $this->wash_attribs($node) .
cb3e2f 326                         ($content === '' && isset($this->_void_elements[$tagName]) ? ' />' : ">$content</$tagName>");
7ac944 327                 }
AM 328                 else if (isset($this->_ignore_elements[$tagName])) {
329                     $dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' not allowed -->';
330                 }
331                 else {
332                     $dump .= '<!-- ' . htmlspecialchars($tagName, ENT_QUOTES) . ' ignored -->';
a89940 333                     $dump .= $this->dumpHtml($node, $level); // ignore tags not its content
7ac944 334                 }
AM 335                 break;
336
337             case XML_CDATA_SECTION_NODE:
338                 $dump .= $node->nodeValue;
339                 break;
340
341             case XML_TEXT_NODE:
342                 $dump .= htmlspecialchars($node->nodeValue);
343                 break;
344
345             case XML_HTML_DOCUMENT_NODE:
a89940 346                 $dump .= $this->dumpHtml($node, $level);
7ac944 347                 break;
AM 348             }
c7c09f 349         }
AM 350         while($node = $node->nextSibling);
7ac944 351
AM 352         return $dump;
353     }
354
355     /**
356      * Main function, give it untrusted HTML, tell it if you allow loading
357      * remote images and give it a map to convert "cid:" urls.
358      */
359     public function wash($html)
360     {
361         // Charset seems to be ignored (probably if defined in the HTML document)
362         $node = new DOMDocument('1.0', $this->config['charset']);
363         $this->extlinks = false;
364
365         $html = $this->cleanup($html);
366
367         // Find base URL for images
368         if (preg_match('/<base\s+href=[\'"]*([^\'"]+)/is', $html, $matches)) {
369             $this->config['base_url'] = $matches[1];
370         }
371         else {
372             $this->config['base_url'] = '';
373         }
a89940 374
AM 375         // Detect max nesting level (for dumpHTML) (#1489110)
376         $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
7ac944 377
bfd24f 378         // Use optimizations if supported
75bbad 379         if (PHP_VERSION_ID >= 50400) {
bfd24f 380             @$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT);
AM 381         }
382         else {
383             @$node->loadHTML($html);
384         }
385
7ac944 386         return $this->dumpHtml($node);
AM 387     }
388
389     /**
390      * Getter for config parameters
391      */
392     public function get_config($prop)
393     {
394         return $this->config[$prop];
395     }
396
397     /**
398      * Clean HTML input
399      */
400     private function cleanup($html)
401     {
402         // special replacements (not properly handled by washtml class)
403         $html_search = array(
b7048d 404             // space(s) between <NOBR>
AM 405             '/(<\/nobr>)(\s+)(<nobr>)/i',
406             // PHP bug #32547 workaround: remove title tag
407             '/<title[^>]*>[^<]*<\/title>/i',
408             // remove <!doctype> before BOM (#1490291)
409             '/<\!doctype[^>]+>[^<]*/im',
410             // byte-order mark (only outlook?)
411             '/^(\0\0\xFE\xFF|\xFF\xFE\0\0|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)/',
412             // washtml/DOMDocument cannot handle xml namespaces
413             '/<html\s[^>]+>/i',
7ac944 414         );
AM 415
416         $html_replace = array(
417             '\\1'.' &nbsp; '.'\\3',
418             '',
419             '',
b7048d 420             '',
7ac944 421             '<html>',
AM 422         );
423         $html = preg_replace($html_search, $html_replace, trim($html));
424
b6a640 425         //-> Replace all of those weird MS Word quotes and other high characters
c72507 426         $badwordchars = array(
b6a640 427             "\xe2\x80\x98", // left single quote
R 428             "\xe2\x80\x99", // right single quote
429             "\xe2\x80\x9c", // left double quote
430             "\xe2\x80\x9d", // right double quote
431             "\xe2\x80\x94", // em dash
432             "\xe2\x80\xa6" // elipses
433         );
c72507 434         $fixedwordchars = array(
b6a640 435             "'",
R 436             "'",
437             '"',
438             '"',
439             '&mdash;',
440             '...'
441         );
c72507 442         $html = str_replace($badwordchars, $fixedwordchars, $html);
b6a640 443
7ac944 444         // PCRE errors handling (#1486856), should we use something like for every preg_* use?
AM 445         if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
446             $errstr = "Could not clean up HTML message! PCRE Error: $preg_error.";
447
448             if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) {
449                 $errstr .= " Consider raising pcre.backtrack_limit!";
450             }
451             if ($preg_error == PREG_RECURSION_LIMIT_ERROR) {
452                 $errstr .= " Consider raising pcre.recursion_limit!";
453             }
454
455             rcube::raise_error(array('code' => 620, 'type' => 'php',
456                 'line' => __LINE__, 'file' => __FILE__,
457                 'message' => $errstr), true, false);
a89940 458
7ac944 459             return '';
AM 460         }
461
462         // fix (unknown/malformed) HTML tags before "wash"
ffec85 463         $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html);
7ac944 464
AM 465         // Remove invalid HTML comments (#1487759)
466         // Don't remove valid conditional comments
1bce14 467         // Don't remove MSOutlook (<!-->) conditional comments (#1489004)
82ed25 468         $html = preg_replace('/<!--[^-<>\[\n]+>/', '', $html);
c72507 469
AM 470         // fix broken nested lists
471         self::fix_broken_lists($html);
7ac944 472
AM 473         // turn relative into absolute urls
474         $html = self::resolve_base($html);
475
476         return $html;
477     }
478
479     /**
480      * Callback function for HTML tags fixing
481      */
482     public static function html_tag_callback($matches)
483     {
484         $tagname = $matches[2];
485         $tagname = preg_replace(array(
486             '/:.*$/',               // Microsoft's Smart Tags <st1:xxxx>
487             '/[^a-z0-9_\[\]\!-]/i', // forbidden characters
488         ), '', $tagname);
489
ffec85 490         // fix invalid closing tags - remove any attributes (#1489446)
AM 491         if ($matches[1] == '</') {
492             $matches[3] = '';
493         }
494
495         return $matches[1] . $tagname . $matches[3];
7ac944 496     }
AM 497
498     /**
499      * Convert all relative URLs according to a <base> in HTML
500      */
501     public static function resolve_base($body)
502     {
503         // check for <base href=...>
504         if (preg_match('!(<base.*href=["\']?)([hftps]{3,5}://[a-z0-9/.%-]+)!i', $body, $regs)) {
505             $replacer = new rcube_base_replacer($regs[2]);
506             $body     = $replacer->replace($body);
507         }
508
509         return $body;
510     }
511
c72507 512     /**
AM 513      * Fix broken nested lists, they are not handled properly by DOMDocument (#1488768)
514      */
515     public static function fix_broken_lists(&$html)
516     {
517         // do two rounds, one for <ol>, one for <ul>
518         foreach (array('ol', 'ul') as $tag) {
519             $pos = 0;
520             while (($pos = stripos($html, '<' . $tag, $pos)) !== false) {
521                 $pos++;
522
523                 // make sure this is an ol/ul tag
524                 if (!in_array($html[$pos+2], array(' ', '>'))) {
525                     continue;
526                 }
527
528                 $p      = $pos;
529                 $in_li  = false;
530                 $li_pos = 0;
531
532                 while (($p = strpos($html, '<', $p)) !== false) {
533                     $tt = strtolower(substr($html, $p, 4));
534
535                     // li open tag
536                     if ($tt == '<li>' || $tt == '<li ') {
537                         $in_li = true;
538                         $p += 4;
539                     }
540                     // li close tag
541                     else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) {
542                         $li_pos = $p;
543                         $p += 4;
544                         $in_li = false;
545                     }
546                     // ul/ol closing tag
547                     else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) {
548                         break;
549                     }
550                     // nested ol/ul element out of li
551                     else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) {
552                         // find closing tag of this ul/ol element
553                         $element = substr($tt, 1, 2);
554                         $cpos    = $p;
555                         do {
556                             $tpos = stripos($html, '<' . $element, $cpos+1);
557                             $cpos = stripos($html, '</' . $element, $cpos+1);
558                         }
559                         while ($tpos !== false && $cpos !== false && $cpos > $tpos);
560
561                         // not found, this is invalid HTML, skip it
562                         if ($cpos === false) {
563                             break;
564                         }
565
566                         // get element content
567                         $end     = strpos($html, '>', $cpos);
568                         $len     = $end - $p + 1;
569                         $element = substr($html, $p, $len);
570
571                         // move element to the end of the last li
572                         $html    = substr_replace($html, '', $p, $len);
573                         $html    = substr_replace($html, $element, $li_pos, 0);
574
575                         $p = $end;
576                     }
577                     else {
578                         $p++;
579                     }
580                 }
581             }
582         }
583     }
f96fec 584
AM 585     /**
586      * Explode css style value
587      */
588     protected function explode_style($style)
589     {
590         $style = trim($style);
591
592         // first remove comments
593         $pos = 0;
594         while (($pos = strpos($style, '/*', $pos)) !== false) {
595             $end = strpos($style, '*/', $pos+2);
596
597             if ($end === false) {
598                 $style = substr($style, 0, $pos);
599             }
600             else {
601                 $style = substr_replace($style, '', $pos, $end - $pos + 2);
602             }
603         }
604
605         $strlen = strlen($style);
606         $result = array();
607
608         // explode value
609         for ($p=$i=0; $i < $strlen; $i++) {
610             if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
611                 if ($q == $style[$i]) {
612                     $q = false;
613                 }
614                 else if (!$q) {
615                     $q = $style[$i];
616                 }
617             }
618
619             if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
620                 $result[] = substr($style, $p, $i - $p);
621                 $p = $i + 1;
622             }
623         }
624
625         $result[] = (string) substr($style, $p);
626
627         return $result;
628     }
c72507 629 }