PageRenderTime 472ms CodeModel.GetById 41ms app.highlight 173ms RepoModel.GetById 39ms app.codeStats 157ms

/libraries/joomla/filter/input.php

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