Joomla! Platform 12.1

Source code for file /libraries/joomla/filter/input.php

Documentation is available at input.php

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