PageRenderTime 54ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/libraries/joomla/filter/input.php

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