Joomla Platform  13.1
Documentation des API du framework Joomla Platform
 Tout Classes Espaces de nommage Fichiers Fonctions Variables Pages
input.php
Aller à la documentation de ce fichier.
1 <?php
2 /**
3  * @package Joomla.Platform
4  * @subpackage Filter
5  *
6  * @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
7  * @license GNU General Public License version 2 or later; see LICENSE
8  */
9 
10 defined('JPATH_PLATFORM') or die;
11 
12 /**
13  * JFilterInput is a class for filtering input from any data source
14  *
15  * Forked from the php input filter library by: Daniel Morris <dan@rootcube.com>
16  * Original Contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie.
17  *
18  * @package Joomla.Platform
19  * @subpackage Filter
20  * @since 11.1
21  */
23 {
24  /**
25  * A container for JFilterInput instances.
26  *
27  * @var array
28  * @since 11.3
29  */
30  protected static $instances = array();
31 
32  /**
33  * The array of permitted tags (white list).
34  *
35  * @var array
36  * @since 11.1
37  */
38  public $tagsArray;
39 
40  /**
41  * The array of permitted tag attributes (white list).
42  *
43  * @var array
44  * @since 11.1
45  */
46  public $attrArray;
47 
48  /**
49  * The method for sanitising tags: WhiteList method = 0 (default), BlackList method = 1
50  *
51  * @var integer
52  * @since 11.1
53  */
54  public $tagsMethod;
55 
56  /**
57  * The method for sanitising attributes: WhiteList method = 0 (default), BlackList method = 1
58  *
59  * @var integer
60  * @since 11.1
61  */
62  public $attrMethod;
63 
64  /**
65  * A flag for XSS checks. Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1
66  *
67  * @var integer
68  * @since 11.1
69  */
70  public $xssAuto;
71 
72  /**
73  * The list of the default blacklisted tags.
74  *
75  * @var array
76  * @since 11.1
77  */
78  public $tagBlacklist = array(
79  'applet',
80  'body',
81  'bgsound',
82  'base',
83  'basefont',
84  'embed',
85  'frame',
86  'frameset',
87  'head',
88  'html',
89  'id',
90  'iframe',
91  'ilayer',
92  'layer',
93  'link',
94  'meta',
95  'name',
96  'object',
97  'script',
98  'style',
99  'title',
100  'xml'
101  );
102 
103  /**
104  * The list of the default blacklisted tag attributes. All event handlers implicit.
105  *
106  * @var array
107  * @since 11.1
108  */
109  public $attrBlacklist = array(
110  'action',
111  'background',
112  'codebase',
113  'dynsrc',
114  'lowsrc'
115  );
116 
117  /**
118  * Constructor for inputFilter class. Only first parameter is required.
119  *
120  * @param array $tagsArray List of user-defined tags
121  * @param array $attrArray List of user-defined attributes
122  * @param integer $tagsMethod WhiteList method = 0, BlackList method = 1
123  * @param integer $attrMethod WhiteList method = 0, BlackList method = 1
124  * @param integer $xssAuto Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1
125  *
126  * @since 11.1
127  */
128  public function __construct($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1)
129  {
130  // Make sure user defined arrays are in lowercase
131  $tagsArray = array_map('strtolower', (array) $tagsArray);
132  $attrArray = array_map('strtolower', (array) $attrArray);
133 
134  // Assign member variables
135  $this->tagsArray = $tagsArray;
136  $this->attrArray = $attrArray;
137  $this->tagsMethod = $tagsMethod;
138  $this->attrMethod = $attrMethod;
139  $this->xssAuto = $xssAuto;
140  }
141 
142  /**
143  * Returns an input filter object, only creating it if it doesn't already exist.
144  *
145  * @param array $tagsArray List of user-defined tags
146  * @param array $attrArray List of user-defined attributes
147  * @param integer $tagsMethod WhiteList method = 0, BlackList method = 1
148  * @param integer $attrMethod WhiteList method = 0, BlackList method = 1
149  * @param integer $xssAuto Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1
150  *
151  * @return JFilterInput The JFilterInput object.
152  *
153  * @since 11.1
154  */
155  public static function &getInstance($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1)
156  {
157  $sig = md5(serialize(array($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto)));
158 
159  if (empty(self::$instances[$sig]))
160  {
161  self::$instances[$sig] = new JFilterInput($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto);
162  }
163 
164  return self::$instances[$sig];
165  }
166 
167  /**
168  * Method to be called by another php script. Processes for XSS and
169  * specified bad code.
170  *
171  * @param mixed $source Input string/array-of-string to be 'cleaned'
172  * @param string $type The return type for the variable:
173  * INT: An integer,
174  * UINT: An unsigned integer,
175  * FLOAT: A floating point number,
176  * BOOLEAN: A boolean value,
177  * WORD: A string containing A-Z or underscores only (not case sensitive),
178  * ALNUM: A string containing A-Z or 0-9 only (not case sensitive),
179  * CMD: A string containing A-Z, 0-9, underscores, periods or hyphens (not case sensitive),
180  * BASE64: A string containing A-Z, 0-9, forward slashes, plus or equals (not case sensitive),
181  * STRING: A fully decoded and sanitised string (default),
182  * HTML: A sanitised string,
183  * ARRAY: An array,
184  * PATH: A sanitised file path,
185  * USERNAME: Do not use (use an application specific filter),
186  * RAW: The raw string is returned with no filtering,
187  * unknown: An unknown filter will act like STRING. If the input is an array it will return an
188  * array of fully decoded and sanitised strings.
189  *
190  * @return mixed 'Cleaned' version of input parameter
191  *
192  * @since 11.1
193  */
194  public function clean($source, $type = 'string')
195  {
196  // Handle the type constraint
197  switch (strtoupper($type))
198  {
199  case 'INT':
200  case 'INTEGER':
201  // Only use the first integer value
202  preg_match('/-?[0-9]+/', (string) $source, $matches);
203  $result = @ (int) $matches[0];
204  break;
205 
206  case 'UINT':
207  // Only use the first integer value
208  preg_match('/-?[0-9]+/', (string) $source, $matches);
209  $result = @ abs((int) $matches[0]);
210  break;
211 
212  case 'FLOAT':
213  case 'DOUBLE':
214  // Only use the first floating point value
215  preg_match('/-?[0-9]+(\.[0-9]+)?/', (string) $source, $matches);
216  $result = @ (float) $matches[0];
217  break;
218 
219  case 'BOOL':
220  case 'BOOLEAN':
221  $result = (bool) $source;
222  break;
223 
224  case 'WORD':
225  $result = (string) preg_replace('/[^A-Z_]/i', '', $source);
226  break;
227 
228  case 'ALNUM':
229  $result = (string) preg_replace('/[^A-Z0-9]/i', '', $source);
230  break;
231 
232  case 'CMD':
233  $result = (string) preg_replace('/[^A-Z0-9_\.-]/i', '', $source);
234  $result = ltrim($result, '.');
235  break;
236 
237  case 'BASE64':
238  $result = (string) preg_replace('/[^A-Z0-9\/+=]/i', '', $source);
239  break;
240 
241  case 'STRING':
242  $result = (string) $this->_remove($this->_decode((string) $source));
243  break;
244 
245  case 'HTML':
246  $result = (string) $this->_remove((string) $source);
247  break;
248 
249  case 'ARRAY':
250  $result = (array) $source;
251  break;
252 
253  case 'PATH':
254  $pattern = '/^[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*([\\\\\/][A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$/';
255  preg_match($pattern, (string) $source, $matches);
256  $result = @ (string) $matches[0];
257  break;
258 
259  case 'USERNAME':
260  $result = (string) preg_replace('/[\x00-\x1F\x7F<>"\'%&]/', '', $source);
261  break;
262 
263  case 'RAW':
264  $result = $source;
265  break;
266 
267  default:
268  // Are we dealing with an array?
269  if (is_array($source))
270  {
271  foreach ($source as $key => $value)
272  {
273  // Filter element for XSS and other 'bad' code etc.
274  if (is_string($value))
275  {
276  $source[$key] = $this->_remove($this->_decode($value));
277  }
278  }
279  $result = $source;
280  }
281  else
282  {
283  // Or a string?
284  if (is_string($source) && !empty($source))
285  {
286  // Filter source for XSS and other 'bad' code etc.
287  $result = $this->_remove($this->_decode($source));
288  }
289  else
290  {
291  // Not an array or string.. return the passed parameter
292  $result = $source;
293  }
294  }
295  break;
296  }
297 
298  return $result;
299  }
300 
301  /**
302  * Function to determine if contents of an attribute are safe
303  *
304  * @param array $attrSubSet A 2 element array for attribute's name, value
305  *
306  * @return boolean True if bad code is detected
307  *
308  * @since 11.1
309  */
310  public static function checkAttribute($attrSubSet)
311  {
312  $attrSubSet[0] = strtolower($attrSubSet[0]);
313  $attrSubSet[1] = strtolower($attrSubSet[1]);
314 
315  return (((strpos($attrSubSet[1], 'expression') !== false) && ($attrSubSet[0]) == 'style') || (strpos($attrSubSet[1], 'javascript:') !== false) ||
316  (strpos($attrSubSet[1], 'behaviour:') !== false) || (strpos($attrSubSet[1], 'vbscript:') !== false) ||
317  (strpos($attrSubSet[1], 'mocha:') !== false) || (strpos($attrSubSet[1], 'livescript:') !== false));
318  }
319 
320  /**
321  * Internal method to iteratively remove all unwanted tags and attributes
322  *
323  * @param string $source Input string to be 'cleaned'
324  *
325  * @return string 'Cleaned' version of input parameter
326  *
327  * @since 11.1
328  */
329  protected function _remove($source)
330  {
331  $loopCounter = 0;
332 
333  // Iteration provides nested tag protection
334  while ($source != $this->_cleanTags($source))
335  {
336  $source = $this->_cleanTags($source);
337  $loopCounter++;
338  }
339 
340  return $source;
341  }
342 
343  /**
344  * Internal method to strip a string of certain tags
345  *
346  * @param string $source Input string to be 'cleaned'
347  *
348  * @return string 'Cleaned' version of input parameter
349  *
350  * @since 11.1
351  */
352  protected function _cleanTags($source)
353  {
354  // First, pre-process this for illegal characters inside attribute values
355  $source = $this->_escapeAttributeValues($source);
356 
357  // In the beginning we don't really have a tag, so everything is postTag
358  $preTag = null;
359  $postTag = $source;
360  $currentSpace = false;
361 
362  // Setting to null to deal with undefined variables
363  $attr = '';
364 
365  // Is there a tag? If so it will certainly start with a '<'.
366  $tagOpen_start = strpos($source, '<');
367 
368  while ($tagOpen_start !== false)
369  {
370  // Get some information about the tag we are processing
371  $preTag .= substr($postTag, 0, $tagOpen_start);
372  $postTag = substr($postTag, $tagOpen_start);
373  $fromTagOpen = substr($postTag, 1);
374  $tagOpen_end = strpos($fromTagOpen, '>');
375 
376  // Check for mal-formed tag where we have a second '<' before the first '>'
377  $nextOpenTag = (strlen($postTag) > $tagOpen_start) ? strpos($postTag, '<', $tagOpen_start + 1) : false;
378 
379  if (($nextOpenTag !== false) && ($nextOpenTag < $tagOpen_end))
380  {
381  // At this point we have a mal-formed tag -- remove the offending open
382  $postTag = substr($postTag, 0, $tagOpen_start) . substr($postTag, $tagOpen_start + 1);
383  $tagOpen_start = strpos($postTag, '<');
384  continue;
385  }
386 
387  // Let's catch any non-terminated tags and skip over them
388  if ($tagOpen_end === false)
389  {
390  $postTag = substr($postTag, $tagOpen_start + 1);
391  $tagOpen_start = strpos($postTag, '<');
392  continue;
393  }
394 
395  // Do we have a nested tag?
396  $tagOpen_nested = strpos($fromTagOpen, '<');
397 
398  if (($tagOpen_nested !== false) && ($tagOpen_nested < $tagOpen_end))
399  {
400  $preTag .= substr($postTag, 0, ($tagOpen_nested + 1));
401  $postTag = substr($postTag, ($tagOpen_nested + 1));
402  $tagOpen_start = strpos($postTag, '<');
403  continue;
404  }
405 
406  // Let's get some information about our tag and setup attribute pairs
407  $tagOpen_nested = (strpos($fromTagOpen, '<') + $tagOpen_start + 1);
408  $currentTag = substr($fromTagOpen, 0, $tagOpen_end);
409  $tagLength = strlen($currentTag);
410  $tagLeft = $currentTag;
411  $attrSet = array();
412  $currentSpace = strpos($tagLeft, ' ');
413 
414  // Are we an open tag or a close tag?
415  if (substr($currentTag, 0, 1) == '/')
416  {
417  // Close Tag
418  $isCloseTag = true;
419  list ($tagName) = explode(' ', $currentTag);
420  $tagName = substr($tagName, 1);
421  }
422  else
423  {
424  // Open Tag
425  $isCloseTag = false;
426  list ($tagName) = explode(' ', $currentTag);
427  }
428 
429  /*
430  * Exclude all "non-regular" tagnames
431  * OR no tagname
432  * OR remove if xssauto is on and tag is blacklisted
433  */
434  if ((!preg_match("/^[a-z][a-z0-9]*$/i", $tagName)) || (!$tagName) || ((in_array(strtolower($tagName), $this->tagBlacklist)) && ($this->xssAuto)))
435  {
436  $postTag = substr($postTag, ($tagLength + 2));
437  $tagOpen_start = strpos($postTag, '<');
438 
439  // Strip tag
440  continue;
441  }
442 
443  /*
444  * Time to grab any attributes from the tag... need this section in
445  * case attributes have spaces in the values.
446  */
447  while ($currentSpace !== false)
448  {
449  $attr = '';
450  $fromSpace = substr($tagLeft, ($currentSpace + 1));
451  $nextEqual = strpos($fromSpace, '=');
452  $nextSpace = strpos($fromSpace, ' ');
453  $openQuotes = strpos($fromSpace, '"');
454  $closeQuotes = strpos(substr($fromSpace, ($openQuotes + 1)), '"') + $openQuotes + 1;
455 
456  $startAtt = '';
457  $startAttPosition = 0;
458 
459  // Find position of equal and open quotes ignoring
460  if (preg_match('#\s*=\s*\"#', $fromSpace, $matches, PREG_OFFSET_CAPTURE))
461  {
462  $startAtt = $matches[0][0];
463  $startAttPosition = $matches[0][1];
464  $closeQuotes = strpos(substr($fromSpace, ($startAttPosition + strlen($startAtt))), '"') + $startAttPosition + strlen($startAtt);
465  $nextEqual = $startAttPosition + strpos($startAtt, '=');
466  $openQuotes = $startAttPosition + strpos($startAtt, '"');
467  $nextSpace = strpos(substr($fromSpace, $closeQuotes), ' ') + $closeQuotes;
468  }
469 
470  // Do we have an attribute to process? [check for equal sign]
471  if ($fromSpace != '/' && (($nextEqual && $nextSpace && $nextSpace < $nextEqual) || !$nextEqual))
472  {
473  if (!$nextEqual)
474  {
475  $attribEnd = strpos($fromSpace, '/') - 1;
476  }
477  else
478  {
479  $attribEnd = $nextSpace - 1;
480  }
481  // If there is an ending, use this, if not, do not worry.
482  if ($attribEnd > 0)
483  {
484  $fromSpace = substr($fromSpace, $attribEnd + 1);
485  }
486  }
487  if (strpos($fromSpace, '=') !== false)
488  {
489  // If the attribute value is wrapped in quotes we need to grab the substring from
490  // the closing quote, otherwise grab until the next space.
491  if (($openQuotes !== false) && (strpos(substr($fromSpace, ($openQuotes + 1)), '"') !== false))
492  {
493  $attr = substr($fromSpace, 0, ($closeQuotes + 1));
494  }
495  else
496  {
497  $attr = substr($fromSpace, 0, $nextSpace);
498  }
499  }
500  // No more equal signs so add any extra text in the tag into the attribute array [eg. checked]
501  else
502  {
503  if ($fromSpace != '/')
504  {
505  $attr = substr($fromSpace, 0, $nextSpace);
506  }
507  }
508 
509  // Last Attribute Pair
510  if (!$attr && $fromSpace != '/')
511  {
512  $attr = $fromSpace;
513  }
514 
515  // Add attribute pair to the attribute array
516  $attrSet[] = $attr;
517 
518  // Move search point and continue iteration
519  $tagLeft = substr($fromSpace, strlen($attr));
520  $currentSpace = strpos($tagLeft, ' ');
521  }
522 
523  // Is our tag in the user input array?
524  $tagFound = in_array(strtolower($tagName), $this->tagsArray);
525 
526  // If the tag is allowed let's append it to the output string.
527  if ((!$tagFound && $this->tagsMethod) || ($tagFound && !$this->tagsMethod))
528  {
529  // Reconstruct tag with allowed attributes
530  if (!$isCloseTag)
531  {
532  // Open or single tag
533  $attrSet = $this->_cleanAttributes($attrSet);
534  $preTag .= '<' . $tagName;
535 
536  for ($i = 0, $count = count($attrSet); $i < $count; $i++)
537  {
538  $preTag .= ' ' . $attrSet[$i];
539  }
540 
541  // Reformat single tags to XHTML
542  if (strpos($fromTagOpen, '</' . $tagName))
543  {
544  $preTag .= '>';
545  }
546  else
547  {
548  $preTag .= ' />';
549  }
550  }
551  // Closing tag
552  else
553  {
554  $preTag .= '</' . $tagName . '>';
555  }
556  }
557 
558  // Find next tag's start and continue iteration
559  $postTag = substr($postTag, ($tagLength + 2));
560  $tagOpen_start = strpos($postTag, '<');
561  }
562 
563  // Append any code after the end of tags and return
564  if ($postTag != '<')
565  {
566  $preTag .= $postTag;
567  }
568 
569  return $preTag;
570  }
571 
572  /**
573  * Internal method to strip a tag of certain attributes
574  *
575  * @param array $attrSet Array of attribute pairs to filter
576  *
577  * @return array Filtered array of attribute pairs
578  *
579  * @since 11.1
580  */
581  protected function _cleanAttributes($attrSet)
582  {
583  $newSet = array();
584 
585  $count = count($attrSet);
586 
587  // Iterate through attribute pairs
588  for ($i = 0; $i < $count; $i++)
589  {
590  // Skip blank spaces
591  if (!$attrSet[$i])
592  {
593  continue;
594  }
595 
596  // Split into name/value pairs
597  $attrSubSet = explode('=', trim($attrSet[$i]), 2);
598 
599  // Take the last attribute in case there is an attribute with no value
600  $attrSubSet[0] = array_pop(explode(' ', trim($attrSubSet[0])));
601 
602  // Remove all "non-regular" attribute names
603  // AND blacklisted attributes
604 
605  if ((!preg_match('/[a-z]*$/i', $attrSubSet[0]))
606  || (($this->xssAuto) && ((in_array(strtolower($attrSubSet[0]), $this->attrBlacklist))
607  || (substr($attrSubSet[0], 0, 2) == 'on'))))
608  {
609  continue;
610  }
611 
612  // XSS attribute value filtering
613  if (isset($attrSubSet[1]))
614  {
615  // Trim leading and trailing spaces
616  $attrSubSet[1] = trim($attrSubSet[1]);
617 
618  // Strips unicode, hex, etc
619  $attrSubSet[1] = str_replace('&#', '', $attrSubSet[1]);
620 
621  // Strip normal newline within attr value
622  $attrSubSet[1] = preg_replace('/[\n\r]/', '', $attrSubSet[1]);
623 
624  // Strip double quotes
625  $attrSubSet[1] = str_replace('"', '', $attrSubSet[1]);
626 
627  // Convert single quotes from either side to doubles (Single quotes shouldn't be used to pad attr values)
628  if ((substr($attrSubSet[1], 0, 1) == "'") && (substr($attrSubSet[1], (strlen($attrSubSet[1]) - 1), 1) == "'"))
629  {
630  $attrSubSet[1] = substr($attrSubSet[1], 1, (strlen($attrSubSet[1]) - 2));
631  }
632  // Strip slashes
633  $attrSubSet[1] = stripslashes($attrSubSet[1]);
634  }
635  else
636  {
637  continue;
638  }
639 
640  // Autostrip script tags
641  if (self::checkAttribute($attrSubSet))
642  {
643  continue;
644  }
645 
646  // Is our attribute in the user input array?
647  $attrFound = in_array(strtolower($attrSubSet[0]), $this->attrArray);
648 
649  // If the tag is allowed lets keep it
650  if ((!$attrFound && $this->attrMethod) || ($attrFound && !$this->attrMethod))
651  {
652  // Does the attribute have a value?
653  if (empty($attrSubSet[1]) === false)
654  {
655  $newSet[] = $attrSubSet[0] . '="' . $attrSubSet[1] . '"';
656  }
657  elseif ($attrSubSet[1] === "0")
658  {
659  // Special Case
660  // Is the value 0?
661  $newSet[] = $attrSubSet[0] . '="0"';
662  }
663  else
664  {
665  // Leave empty attributes alone
666  $newSet[] = $attrSubSet[0] . '=""';
667  }
668  }
669  }
670 
671  return $newSet;
672  }
673 
674  /**
675  * Try to convert to plaintext
676  *
677  * @param string $source The source string.
678  *
679  * @return string Plaintext string
680  *
681  * @since 11.1
682  */
683  protected function _decode($source)
684  {
685  static $ttr;
686 
687  if (!is_array($ttr))
688  {
689  // Entity decode
690  if (version_compare(PHP_VERSION, '5.3.4', '>='))
691  {
692  $trans_tbl = get_html_translation_table(HTML_ENTITIES, ENT_COMPAT, 'ISO-8859-1');
693  }
694  else
695  {
696  $trans_tbl = get_html_translation_table(HTML_ENTITIES, ENT_COMPAT);
697  }
698 
699  foreach ($trans_tbl as $k => $v)
700  {
701  $ttr[$v] = utf8_encode($k);
702  }
703  }
704 
705  $source = strtr($source, $ttr);
706 
707  // Convert decimal
708  $source = preg_replace_callback('/&#(\d+);/m', function($m)
709  {
710  return utf8_encode(chr($m[1]));
711  }, $source
712  );
713 
714  // Convert hex
715  $source = preg_replace_callback('/&#x([a-f0-9]+);/mi', function($m)
716  {
717  return utf8_encode(chr('0x' . $m[1]));
718  }, $source
719  );
720 
721  return $source;
722  }
723 
724  /**
725  * Escape < > and " inside attribute values
726  *
727  * @param string $source The source string.
728  *
729  * @return string Filtered string
730  *
731  * @since 11.1
732  */
733  protected function _escapeAttributeValues($source)
734  {
735  $alreadyFiltered = '';
736  $remainder = $source;
737  $badChars = array('<', '"', '>');
738  $escapedChars = array('&lt;', '&quot;', '&gt;');
739 
740  // Process each portion based on presence of =" and "<space>, "/>, or ">
741  // See if there are any more attributes to process
742  while (preg_match('#<[^>]*?=\s*?(\"|\')#s', $remainder, $matches, PREG_OFFSET_CAPTURE))
743  {
744  // Get the portion before the attribute value
745  $quotePosition = $matches[0][1];
746  $nextBefore = $quotePosition + strlen($matches[0][0]);
747 
748  // Figure out if we have a single or double quote and look for the matching closing quote
749  // Closing quote should be "/>, ">, "<space>, or " at the end of the string
750  $quote = substr($matches[0][0], -1);
751  $pregMatch = ($quote == '"') ? '#(\"\s*/\s*>|\"\s*>|\"\s+|\"$)#' : "#(\'\s*/\s*>|\'\s*>|\'\s+|\'$)#";
752 
753  // Get the portion after attribute value
754  if (preg_match($pregMatch, substr($remainder, $nextBefore), $matches, PREG_OFFSET_CAPTURE))
755  {
756  // We have a closing quote
757  $nextAfter = $nextBefore + $matches[0][1];
758  }
759  else
760  {
761  // No closing quote
762  $nextAfter = strlen($remainder);
763  }
764 
765  // Get the actual attribute value
766  $attributeValue = substr($remainder, $nextBefore, $nextAfter - $nextBefore);
767 
768  // Escape bad chars
769  $attributeValue = str_replace($badChars, $escapedChars, $attributeValue);
770  $attributeValue = $this->_stripCSSExpressions($attributeValue);
771  $alreadyFiltered .= substr($remainder, 0, $nextBefore) . $attributeValue . $quote;
772  $remainder = substr($remainder, $nextAfter + 1);
773  }
774 
775  // At this point, we just have to return the $alreadyFiltered and the $remainder
776  return $alreadyFiltered . $remainder;
777  }
778 
779  /**
780  * Remove CSS Expressions in the form of <property>:expression(...)
781  *
782  * @param string $source The source string.
783  *
784  * @return string Filtered string
785  *
786  * @since 11.1
787  */
788  protected function _stripCSSExpressions($source)
789  {
790  // Strip any comments out (in the form of /*...*/)
791  $test = preg_replace('#\/\*.*\*\/#U', '', $source);
792 
793  // Test for :expression
794  if (!stripos($test, ':expression'))
795  {
796  // Not found, so we are done
797  $return = $source;
798  }
799  else
800  {
801  // At this point, we have stripped out the comments and have found :expression
802  // Test stripped string for :expression followed by a '('
803  if (preg_match_all('#:expression\s*\(#', $test, $matches))
804  {
805  // If found, remove :expression
806  $test = str_ireplace(':expression', '', $test);
807  $return = $test;
808  }
809  }
810 
811  return $return;
812  }
813 }