Aleksander Machniak
2015-08-10 f4c512336dd707cf16b39beaae1055acea048891
commit | author | age
7ac944 1 <?php
AM 2
a95874 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
f4c512 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 {
316             switch($node->nodeType) {
317             case XML_ELEMENT_NODE: //Check element
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
349             case XML_DOCUMENT_TYPE_NODE:
350                 break;
351
352             default:
a89940 353                 $dump .= '<!-- node type ' . $node->nodeType . ' -->';
7ac944 354             }
AM 355         } while($node = $node->nextSibling);
356
357         return $dump;
358     }
359
360     /**
361      * Main function, give it untrusted HTML, tell it if you allow loading
362      * remote images and give it a map to convert "cid:" urls.
363      */
364     public function wash($html)
365     {
366         // Charset seems to be ignored (probably if defined in the HTML document)
367         $node = new DOMDocument('1.0', $this->config['charset']);
368         $this->extlinks = false;
369
370         $html = $this->cleanup($html);
371
372         // Find base URL for images
373         if (preg_match('/<base\s+href=[\'"]*([^\'"]+)/is', $html, $matches)) {
374             $this->config['base_url'] = $matches[1];
375         }
376         else {
377             $this->config['base_url'] = '';
378         }
a89940 379
AM 380         // Detect max nesting level (for dumpHTML) (#1489110)
381         $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
7ac944 382
bfd24f 383         // Use optimizations if supported
75bbad 384         if (PHP_VERSION_ID >= 50400) {
bfd24f 385             @$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT);
AM 386         }
387         else {
388             @$node->loadHTML($html);
389         }
390
7ac944 391         return $this->dumpHtml($node);
AM 392     }
393
394     /**
395      * Getter for config parameters
396      */
397     public function get_config($prop)
398     {
399         return $this->config[$prop];
400     }
401
402     /**
403      * Clean HTML input
404      */
405     private function cleanup($html)
406     {
407         // special replacements (not properly handled by washtml class)
408         $html_search = array(
759566 409             // space(s) between <NOBR>
AM 410             '/(<\/nobr>)(\s+)(<nobr>)/i',
411             // PHP bug #32547 workaround: remove title tag
412             '/<title[^>]*>[^<]*<\/title>/i',
413             // remove <!doctype> before BOM (#1490291)
414             '/<\!doctype[^>]+>[^<]*/im',
415             // byte-order mark (only outlook?)
416             '/^(\0\0\xFE\xFF|\xFF\xFE\0\0|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)/',
417             // washtml/DOMDocument cannot handle xml namespaces
418             '/<html\s[^>]+>/i',
7ac944 419         );
AM 420
421         $html_replace = array(
422             '\\1'.' &nbsp; '.'\\3',
423             '',
424             '',
759566 425             '',
7ac944 426             '<html>',
AM 427         );
428         $html = preg_replace($html_search, $html_replace, trim($html));
429
b6a640 430         //-> Replace all of those weird MS Word quotes and other high characters
c72507 431         $badwordchars = array(
b6a640 432             "\xe2\x80\x98", // left single quote
R 433             "\xe2\x80\x99", // right single quote
434             "\xe2\x80\x9c", // left double quote
435             "\xe2\x80\x9d", // right double quote
436             "\xe2\x80\x94", // em dash
437             "\xe2\x80\xa6" // elipses
438         );
c72507 439         $fixedwordchars = array(
b6a640 440             "'",
R 441             "'",
442             '"',
443             '"',
444             '&mdash;',
445             '...'
446         );
c72507 447         $html = str_replace($badwordchars, $fixedwordchars, $html);
b6a640 448
7ac944 449         // PCRE errors handling (#1486856), should we use something like for every preg_* use?
AM 450         if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
451             $errstr = "Could not clean up HTML message! PCRE Error: $preg_error.";
452
453             if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) {
454                 $errstr .= " Consider raising pcre.backtrack_limit!";
455             }
456             if ($preg_error == PREG_RECURSION_LIMIT_ERROR) {
457                 $errstr .= " Consider raising pcre.recursion_limit!";
458             }
459
460             rcube::raise_error(array('code' => 620, 'type' => 'php',
461                 'line' => __LINE__, 'file' => __FILE__,
462                 'message' => $errstr), true, false);
a89940 463
7ac944 464             return '';
AM 465         }
466
467         // fix (unknown/malformed) HTML tags before "wash"
ffec85 468         $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html);
7ac944 469
AM 470         // Remove invalid HTML comments (#1487759)
471         // Don't remove valid conditional comments
1bce14 472         // Don't remove MSOutlook (<!-->) conditional comments (#1489004)
82ed25 473         $html = preg_replace('/<!--[^-<>\[\n]+>/', '', $html);
c72507 474
AM 475         // fix broken nested lists
476         self::fix_broken_lists($html);
7ac944 477
AM 478         // turn relative into absolute urls
479         $html = self::resolve_base($html);
480
481         return $html;
482     }
483
484     /**
485      * Callback function for HTML tags fixing
486      */
487     public static function html_tag_callback($matches)
488     {
489         $tagname = $matches[2];
490         $tagname = preg_replace(array(
491             '/:.*$/',               // Microsoft's Smart Tags <st1:xxxx>
492             '/[^a-z0-9_\[\]\!-]/i', // forbidden characters
493         ), '', $tagname);
494
ffec85 495         // fix invalid closing tags - remove any attributes (#1489446)
AM 496         if ($matches[1] == '</') {
497             $matches[3] = '';
498         }
499
500         return $matches[1] . $tagname . $matches[3];
7ac944 501     }
AM 502
503     /**
504      * Convert all relative URLs according to a <base> in HTML
505      */
506     public static function resolve_base($body)
507     {
508         // check for <base href=...>
509         if (preg_match('!(<base.*href=["\']?)([hftps]{3,5}://[a-z0-9/.%-]+)!i', $body, $regs)) {
510             $replacer = new rcube_base_replacer($regs[2]);
511             $body     = $replacer->replace($body);
512         }
513
514         return $body;
515     }
516
c72507 517     /**
AM 518      * Fix broken nested lists, they are not handled properly by DOMDocument (#1488768)
519      */
520     public static function fix_broken_lists(&$html)
521     {
522         // do two rounds, one for <ol>, one for <ul>
523         foreach (array('ol', 'ul') as $tag) {
524             $pos = 0;
525             while (($pos = stripos($html, '<' . $tag, $pos)) !== false) {
526                 $pos++;
527
528                 // make sure this is an ol/ul tag
529                 if (!in_array($html[$pos+2], array(' ', '>'))) {
530                     continue;
531                 }
532
533                 $p      = $pos;
534                 $in_li  = false;
535                 $li_pos = 0;
536
537                 while (($p = strpos($html, '<', $p)) !== false) {
538                     $tt = strtolower(substr($html, $p, 4));
539
540                     // li open tag
541                     if ($tt == '<li>' || $tt == '<li ') {
542                         $in_li = true;
543                         $p += 4;
544                     }
545                     // li close tag
546                     else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) {
547                         $li_pos = $p;
548                         $p += 4;
549                         $in_li = false;
550                     }
551                     // ul/ol closing tag
552                     else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) {
553                         break;
554                     }
555                     // nested ol/ul element out of li
556                     else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) {
557                         // find closing tag of this ul/ol element
558                         $element = substr($tt, 1, 2);
559                         $cpos    = $p;
560                         do {
561                             $tpos = stripos($html, '<' . $element, $cpos+1);
562                             $cpos = stripos($html, '</' . $element, $cpos+1);
563                         }
564                         while ($tpos !== false && $cpos !== false && $cpos > $tpos);
565
566                         // not found, this is invalid HTML, skip it
567                         if ($cpos === false) {
568                             break;
569                         }
570
571                         // get element content
572                         $end     = strpos($html, '>', $cpos);
573                         $len     = $end - $p + 1;
574                         $element = substr($html, $p, $len);
575
576                         // move element to the end of the last li
577                         $html    = substr_replace($html, '', $p, $len);
578                         $html    = substr_replace($html, $element, $li_pos, 0);
579
580                         $p = $end;
581                     }
582                     else {
583                         $p++;
584                     }
585                 }
586             }
587         }
588     }
f96fec 589
AM 590     /**
591      * Explode css style value
592      */
593     protected function explode_style($style)
594     {
595         $style = trim($style);
596
597         // first remove comments
598         $pos = 0;
599         while (($pos = strpos($style, '/*', $pos)) !== false) {
600             $end = strpos($style, '*/', $pos+2);
601
602             if ($end === false) {
603                 $style = substr($style, 0, $pos);
604             }
605             else {
606                 $style = substr_replace($style, '', $pos, $end - $pos + 2);
607             }
608         }
609
610         $strlen = strlen($style);
611         $result = array();
612
613         // explode value
614         for ($p=$i=0; $i < $strlen; $i++) {
615             if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
616                 if ($q == $style[$i]) {
617                     $q = false;
618                 }
619                 else if (!$q) {
620                     $q = $style[$i];
621                 }
622             }
623
624             if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
625                 $result[] = substr($style, $p, $i - $p);
626                 $p = $i + 1;
627             }
628         }
629
630         $result[] = (string) substr($style, $p);
631
632         return $result;
633     }
c72507 634 }