update relnotes
[lhc/web/wiklou.git] / includes / Sanitizer.php
index e6c56e9..51c751c 100644 (file)
@@ -1,14 +1,13 @@
 <?php
-
 /**
- * (X)HTML sanitizer for MediaWiki
+ * XHTML sanitizer for MediaWiki
  *
  * Copyright (C) 2002-2005 Brion Vibber <brion@pobox.com> et al
  * http://www.mediawiki.org/
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or 
+ * the Free Software Foundation; either version 2 of the License, or
  * (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful,
  *
  * You should have received a copy of the GNU General Public License along
  * with this program; if not, write to the Free Software Foundation, Inc.,
- * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  * http://www.gnu.org/copyleft/gpl.html
  *
- * @package MediaWiki
- * @subpackage Parser
+ * @addtogroup Parser
  */
 
+/**
+ * Regular expression to match various types of character references in
+ * Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences
+ */
+define( 'MW_CHAR_REFS_REGEX',
+       '/&([A-Za-z0-9]+);
+        |&\#([0-9]+);
+        |&\#x([0-9A-Za-z]+);
+        |&\#X([0-9A-Za-z]+);
+        |(&)/x' );
+
+/**
+ * Regular expression to match HTML/XML attribute pairs within a tag.
+ * Allows some... latitude.
+ * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes
+ */
+$attrib = '[A-Za-z0-9]';
+$space = '[\x09\x0a\x0d\x20]';
+define( 'MW_ATTRIBS_REGEX',
+       "/(?:^|$space)($attrib+)
+         ($space*=$space*
+               (?:
+                # The attribute value: quoted or alone
+                 \"([^<\"]*)\"
+                | '([^<']*)'
+                |  ([a-zA-Z0-9!#$%&()*,\\-.\\/:;<>?@[\\]^_`{|}~]+)
+                |  (\#[0-9a-fA-F]+) # Technically wrong, but lots of
+                                                        # colors are specified like this.
+                                                        # We'll be normalizing it.
+               )
+          )?(?=$space|\$)/sx" );
+
+/**
+ * List of all named character entities defined in HTML 4.01
+ * http://www.w3.org/TR/html4/sgml/entities.html
+ * @private
+ */
+global $wgHtmlEntities;
+$wgHtmlEntities = array(
+       'Aacute'   => 193,
+       'aacute'   => 225,
+       'Acirc'    => 194,
+       'acirc'    => 226,
+       'acute'    => 180,
+       'AElig'    => 198,
+       'aelig'    => 230,
+       'Agrave'   => 192,
+       'agrave'   => 224,
+       'alefsym'  => 8501,
+       'Alpha'    => 913,
+       'alpha'    => 945,
+       'amp'      => 38,
+       'and'      => 8743,
+       'ang'      => 8736,
+       'Aring'    => 197,
+       'aring'    => 229,
+       'asymp'    => 8776,
+       'Atilde'   => 195,
+       'atilde'   => 227,
+       'Auml'     => 196,
+       'auml'     => 228,
+       'bdquo'    => 8222,
+       'Beta'     => 914,
+       'beta'     => 946,
+       'brvbar'   => 166,
+       'bull'     => 8226,
+       'cap'      => 8745,
+       'Ccedil'   => 199,
+       'ccedil'   => 231,
+       'cedil'    => 184,
+       'cent'     => 162,
+       'Chi'      => 935,
+       'chi'      => 967,
+       'circ'     => 710,
+       'clubs'    => 9827,
+       'cong'     => 8773,
+       'copy'     => 169,
+       'crarr'    => 8629,
+       'cup'      => 8746,
+       'curren'   => 164,
+       'dagger'   => 8224,
+       'Dagger'   => 8225,
+       'darr'     => 8595,
+       'dArr'     => 8659,
+       'deg'      => 176,
+       'Delta'    => 916,
+       'delta'    => 948,
+       'diams'    => 9830,
+       'divide'   => 247,
+       'Eacute'   => 201,
+       'eacute'   => 233,
+       'Ecirc'    => 202,
+       'ecirc'    => 234,
+       'Egrave'   => 200,
+       'egrave'   => 232,
+       'empty'    => 8709,
+       'emsp'     => 8195,
+       'ensp'     => 8194,
+       'Epsilon'  => 917,
+       'epsilon'  => 949,
+       'equiv'    => 8801,
+       'Eta'      => 919,
+       'eta'      => 951,
+       'ETH'      => 208,
+       'eth'      => 240,
+       'Euml'     => 203,
+       'euml'     => 235,
+       'euro'     => 8364,
+       'exist'    => 8707,
+       'fnof'     => 402,
+       'forall'   => 8704,
+       'frac12'   => 189,
+       'frac14'   => 188,
+       'frac34'   => 190,
+       'frasl'    => 8260,
+       'Gamma'    => 915,
+       'gamma'    => 947,
+       'ge'       => 8805,
+       'gt'       => 62,
+       'harr'     => 8596,
+       'hArr'     => 8660,
+       'hearts'   => 9829,
+       'hellip'   => 8230,
+       'Iacute'   => 205,
+       'iacute'   => 237,
+       'Icirc'    => 206,
+       'icirc'    => 238,
+       'iexcl'    => 161,
+       'Igrave'   => 204,
+       'igrave'   => 236,
+       'image'    => 8465,
+       'infin'    => 8734,
+       'int'      => 8747,
+       'Iota'     => 921,
+       'iota'     => 953,
+       'iquest'   => 191,
+       'isin'     => 8712,
+       'Iuml'     => 207,
+       'iuml'     => 239,
+       'Kappa'    => 922,
+       'kappa'    => 954,
+       'Lambda'   => 923,
+       'lambda'   => 955,
+       'lang'     => 9001,
+       'laquo'    => 171,
+       'larr'     => 8592,
+       'lArr'     => 8656,
+       'lceil'    => 8968,
+       'ldquo'    => 8220,
+       'le'       => 8804,
+       'lfloor'   => 8970,
+       'lowast'   => 8727,
+       'loz'      => 9674,
+       'lrm'      => 8206,
+       'lsaquo'   => 8249,
+       'lsquo'    => 8216,
+       'lt'       => 60,
+       'macr'     => 175,
+       'mdash'    => 8212,
+       'micro'    => 181,
+       'middot'   => 183,
+       'minus'    => 8722,
+       'Mu'       => 924,
+       'mu'       => 956,
+       'nabla'    => 8711,
+       'nbsp'     => 160,
+       'ndash'    => 8211,
+       'ne'       => 8800,
+       'ni'       => 8715,
+       'not'      => 172,
+       'notin'    => 8713,
+       'nsub'     => 8836,
+       'Ntilde'   => 209,
+       'ntilde'   => 241,
+       'Nu'       => 925,
+       'nu'       => 957,
+       'Oacute'   => 211,
+       'oacute'   => 243,
+       'Ocirc'    => 212,
+       'ocirc'    => 244,
+       'OElig'    => 338,
+       'oelig'    => 339,
+       'Ograve'   => 210,
+       'ograve'   => 242,
+       'oline'    => 8254,
+       'Omega'    => 937,
+       'omega'    => 969,
+       'Omicron'  => 927,
+       'omicron'  => 959,
+       'oplus'    => 8853,
+       'or'       => 8744,
+       'ordf'     => 170,
+       'ordm'     => 186,
+       'Oslash'   => 216,
+       'oslash'   => 248,
+       'Otilde'   => 213,
+       'otilde'   => 245,
+       'otimes'   => 8855,
+       'Ouml'     => 214,
+       'ouml'     => 246,
+       'para'     => 182,
+       'part'     => 8706,
+       'permil'   => 8240,
+       'perp'     => 8869,
+       'Phi'      => 934,
+       'phi'      => 966,
+       'Pi'       => 928,
+       'pi'       => 960,
+       'piv'      => 982,
+       'plusmn'   => 177,
+       'pound'    => 163,
+       'prime'    => 8242,
+       'Prime'    => 8243,
+       'prod'     => 8719,
+       'prop'     => 8733,
+       'Psi'      => 936,
+       'psi'      => 968,
+       'quot'     => 34,
+       'radic'    => 8730,
+       'rang'     => 9002,
+       'raquo'    => 187,
+       'rarr'     => 8594,
+       'rArr'     => 8658,
+       'rceil'    => 8969,
+       'rdquo'    => 8221,
+       'real'     => 8476,
+       'reg'      => 174,
+       'rfloor'   => 8971,
+       'Rho'      => 929,
+       'rho'      => 961,
+       'rlm'      => 8207,
+       'rsaquo'   => 8250,
+       'rsquo'    => 8217,
+       'sbquo'    => 8218,
+       'Scaron'   => 352,
+       'scaron'   => 353,
+       'sdot'     => 8901,
+       'sect'     => 167,
+       'shy'      => 173,
+       'Sigma'    => 931,
+       'sigma'    => 963,
+       'sigmaf'   => 962,
+       'sim'      => 8764,
+       'spades'   => 9824,
+       'sub'      => 8834,
+       'sube'     => 8838,
+       'sum'      => 8721,
+       'sup'      => 8835,
+       'sup1'     => 185,
+       'sup2'     => 178,
+       'sup3'     => 179,
+       'supe'     => 8839,
+       'szlig'    => 223,
+       'Tau'      => 932,
+       'tau'      => 964,
+       'there4'   => 8756,
+       'Theta'    => 920,
+       'theta'    => 952,
+       'thetasym' => 977,
+       'thinsp'   => 8201,
+       'THORN'    => 222,
+       'thorn'    => 254,
+       'tilde'    => 732,
+       'times'    => 215,
+       'trade'    => 8482,
+       'Uacute'   => 218,
+       'uacute'   => 250,
+       'uarr'     => 8593,
+       'uArr'     => 8657,
+       'Ucirc'    => 219,
+       'ucirc'    => 251,
+       'Ugrave'   => 217,
+       'ugrave'   => 249,
+       'uml'      => 168,
+       'upsih'    => 978,
+       'Upsilon'  => 933,
+       'upsilon'  => 965,
+       'Uuml'     => 220,
+       'uuml'     => 252,
+       'weierp'   => 8472,
+       'Xi'       => 926,
+       'xi'       => 958,
+       'Yacute'   => 221,
+       'yacute'   => 253,
+       'yen'      => 165,
+       'Yuml'     => 376,
+       'yuml'     => 255,
+       'Zeta'     => 918,
+       'zeta'     => 950,
+       'zwj'      => 8205,
+       'zwnj'     => 8204 );
+
 class Sanitizer {
        /**
         * Cleans up HTML, removes dangerous tags and attributes, and
         * removes HTML comments
-        * @access private
+        * @private
         * @param string $text
+        * @param callback $processCallback to do any variable or parameter replacements in HTML attribute values
+        * @param array $args for the processing callback
         * @return string
         */
-       function removeHTMLtags( $text ) {
+       static function removeHTMLtags( $text, $processCallback = null, $args = array() ) {
                global $wgUseTidy, $wgUserHtml;
-               $fname = 'Parser::removeHTMLtags';
-               wfProfileIn( $fname );
-
-               if( $wgUserHtml ) {
-                       $htmlpairs = array( # Tags that must be closed
-                               'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
-                               'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
-                               'strike', 'strong', 'tt', 'var', 'div', 'center',
-                               'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
-                               'ruby', 'rt' , 'rb' , 'rp', 'p', 'span'
-                       );
-                       $htmlsingle = array(
-                               'br', 'hr', 'li', 'dt', 'dd'
-                       );
-                       $htmlnest = array( # Tags that can be nested--??
-                               'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul',
-                               'dl', 'font', 'big', 'small', 'sub', 'sup', 'span'
-                       );
-                       $tabletags = array( # Can only appear inside table
-                               'td', 'th', 'tr'
-                       );
-               } else {
-                       $htmlpairs = array();
-                       $htmlsingle = array();
-                       $htmlnest = array();
-                       $tabletags = array();
-               }
 
-               $htmlsingle = array_merge( $tabletags, $htmlsingle );
-               $htmlelements = array_merge( $htmlsingle, $htmlpairs );
+               static $htmlpairs, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags,
+                       $htmllist, $listtags, $htmlsingleallowed, $htmlelements, $staticInitialised;
+
+               wfProfileIn( __METHOD__ );
+
+               if ( !$staticInitialised ) {
+                       if( $wgUserHtml ) {
+                               $htmlpairs = array( # Tags that must be closed
+                                       'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
+                                       'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
+                                       'strike', 'strong', 'tt', 'var', 'div', 'center',
+                                       'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
+                                       'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u'
+                               );
+                               $htmlsingle = array(
+                                       'br', 'hr', 'li', 'dt', 'dd'
+                               );
+                               $htmlsingleonly = array( # Elements that cannot have close tags
+                                       'br', 'hr'
+                               );
+                               $htmlnest = array( # Tags that can be nested--??
+                                       'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul',
+                                       'dl', 'font', 'big', 'small', 'sub', 'sup', 'span'
+                               );
+                               $tabletags = array( # Can only appear inside table, we will close them
+                                       'td', 'th', 'tr',
+                               );
+                               $htmllist = array( # Tags used by list
+                                       'ul','ol',
+                               );
+                               $listtags = array( # Tags that can appear in a list
+                                       'li',
+                               );
+
+                       } else {
+                               $htmlpairs = array();
+                               $htmlsingle = array();
+                               $htmlnest = array();
+                               $tabletags = array();
+                       }
+
+                       $htmlsingleallowed = array_merge( $htmlsingle, $tabletags );
+                       $htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest );
+
+                       # Convert them all to hashtables for faster lookup
+                       $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags', 
+                               'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelements' );
+                       foreach ( $vars as $var ) {
+                               $$var = array_flip( $$var );
+                       }
+                       $staticInitialised = true;
+               }
 
                # Remove HTML comments
                $text = Sanitizer::removeHTMLcomments( $text );
-
                $bits = explode( '<', $text );
-               $text = array_shift( $bits );
+               $text = str_replace( '>', '&gt;', array_shift( $bits ) );
                if(!$wgUseTidy) {
-                       $tagstack = array(); $tablestack = array();
+                       $tagstack = $tablestack = array();
                        foreach ( $bits as $x ) {
-                               $prev = error_reporting( E_ALL & ~( E_NOTICE | E_WARNING ) );
-                               preg_match( '/^(\\/?)(\\w+)([^>]*)(\\/{0,1}>)([^<]*)$/',
-                               $x, $regs );
-                               list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
-                               error_reporting( $prev );
+                               $regs = array();
+                               if( preg_match( '!^(/?)(\\w+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ) ) {
+                                       list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
+                               } else {
+                                       $slash = $t = $params = $brace = $rest = null;
+                               }
 
                                $badtag = 0 ;
-                               if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
+                               if ( isset( $htmlelements[$t = strtolower( $t )] ) ) {
                                        # Check our stack
                                        if ( $slash ) {
                                                # Closing a tag...
-                                               if ( ! in_array( $t, $htmlsingle ) &&
-                                               ( $ot = @array_pop( $tagstack ) ) != $t ) {
-                                                       @array_push( $tagstack, $ot );
+                                               if( isset( $htmlsingleonly[$t] ) ) {
                                                        $badtag = 1;
+                                               } elseif ( ( $ot = @array_pop( $tagstack ) ) != $t ) {
+                                                       if ( isset( $htmlsingleallowed[$ot] ) ) {
+                                                               # Pop all elements with an optional close tag
+                                                               # and see if we find a match below them
+                                                               $optstack = array();
+                                                               array_push ($optstack, $ot);
+                                                               while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) &&
+                                                                               isset( $htmlsingleallowed[$ot] ) ) 
+                                                               {
+                                                                       array_push ($optstack, $ot);
+                                                               }
+                                                               if ( $t != $ot ) {
+                                                                       # No match. Push the optinal elements back again
+                                                                       $badtag = 1;
+                                                                       while ( $ot = @array_pop( $optstack ) ) {
+                                                                               array_push( $tagstack, $ot );
+                                                                       }
+                                                               }
+                                                       } else {
+                                                               @array_push( $tagstack, $ot );
+                                                               # <li> can be nested in <ul> or <ol>, skip those cases:
+                                                               if(!(isset( $htmllist[$ot] ) && isset( $listtags[$t] ) )) {
+                                                                       $badtag = 1;
+                                                               }
+                                                       }
                                                } else {
                                                        if ( $t == 'table' ) {
                                                                $tagstack = array_pop( $tablestack );
                                                        }
-                                                       $newparams = '';
                                                }
+                                               $newparams = '';
                                        } else {
                                                # Keep track for later
-                                               if ( in_array( $t, $tabletags ) &&
+                                               if ( isset( $tabletags[$t] ) &&
                                                ! in_array( 'table', $tagstack ) ) {
                                                        $badtag = 1;
                                                } else if ( in_array( $t, $tagstack ) &&
-                                               ! in_array ( $t , $htmlnest ) ) {
+                                               ! isset( $htmlnest [$t ] ) ) {
                                                        $badtag = 1 ;
-                                               } else if ( ! in_array( $t, $htmlsingle ) ) {
+                                               # Is it a self closed htmlpair ? (bug 5487)
+                                               } else if( $brace == '/>' &&
+                                               isset( $htmlpairs[$t] ) ) {
+                                                       $badtag = 1;
+                                               } elseif( isset( $htmlsingleonly[$t] ) ) {
+                                                       # Hack to force empty tag for uncloseable elements
+                                                       $brace = '/>';
+                                               } else if( isset( $htmlsingle[$t] ) ) {
+                                                       # Hack to not close $htmlsingle tags
+                                                       $brace = NULL;
+                                               } else if( isset( $tabletags[$t] )
+                                               &&  in_array($t ,$tagstack) ) {
+                                                       // New table tag but forgot to close the previous one
+                                                       $text .= "</$t>";
+                                               } else {
                                                        if ( $t == 'table' ) {
                                                                array_push( $tablestack, $tagstack );
                                                                $tagstack = array();
                                                        }
                                                        array_push( $tagstack, $t );
                                                }
+
+                                               # Replace any variables or template parameters with
+                                               # plaintext results.
+                                               if( is_callable( $processCallback ) ) {
+                                                       call_user_func_array( $processCallback, array( &$params, $args ) );
+                                               }
+
                                                # Strip non-approved attributes from the tag
                                                $newparams = Sanitizer::fixTagAttributes( $params, $t );
                                        }
                                        if ( ! $badtag ) {
                                                $rest = str_replace( '>', '&gt;', $rest );
-                                               $text .= "<$slash$t$newparams$brace$rest";
+                                               $close = ( $brace == '/>' && !$slash ) ? ' /' : '';
+                                               $text .= "<$slash$t$newparams$close>$rest";
                                                continue;
                                        }
                                }
@@ -129,10 +489,13 @@ class Sanitizer {
                } else {
                        # this might be possible using tidy itself
                        foreach ( $bits as $x ) {
-                               preg_match( '/^(\\/?)(\\w+)([^>]*)(\\/{0,1}>)([^<]*)$/',
+                               preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/',
                                $x, $regs );
-                               @list( $qbar, $slash, $t, $params, $brace, $rest ) = $regs;
-                               if ( in_array( $t = strtolower( $t ), $htmlelements ) ) {
+                               @list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
+                               if ( isset( $htmlelements[$t = strtolower( $t )] ) ) {
+                                       if( is_callable( $processCallback ) ) {
+                                               call_user_func_array( $processCallback, array( &$params, $args ) );
+                                       }
                                        $newparams = Sanitizer::fixTagAttributes( $params, $t );
                                        $rest = str_replace( '>', '&gt;', $rest );
                                        $text .= "<$slash$t$newparams$brace$rest";
@@ -141,7 +504,7 @@ class Sanitizer {
                                }
                        }
                }
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
@@ -150,14 +513,13 @@ class Sanitizer {
         * To avoid leaving blank lines, when a comment is both preceded
         * and followed by a newline (ignoring spaces), trim leading and
         * trailing spaces and one of the newlines.
-        * 
-        * @access private
+        *
+        * @private
         * @param string $text
         * @return string
         */
-       function removeHTMLcomments( $text ) {
-               $fname='Parser::removeHTMLcomments';
-               wfProfileIn( $fname );
+       static function removeHTMLcomments( $text ) {
+               wfProfileIn( __METHOD__ );
                while (($start = strpos($text, '<!--')) !== false) {
                        $end = strpos($text, '-->', $start + 4);
                        if ($end === false) {
@@ -187,13 +549,86 @@ class Sanitizer {
                                $text = substr_replace($text, '', $start, $end - $start);
                        }
                }
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
+       /**
+        * Take an array of attribute names and values and normalize or discard
+        * illegal values for the given element type.
+        *
+        * - Discards attributes not on a whitelist for the given element
+        * - Unsafe style attributes are discarded
+        *
+        * @param array $attribs
+        * @param string $element
+        * @return array
+        *
+        * @todo Check for legal values where the DTD limits things.
+        * @todo Check for unique id attribute :P
+        */
+       static function validateTagAttributes( $attribs, $element ) {
+               $whitelist = array_flip( Sanitizer::attributeWhitelist( $element ) );
+               $out = array();
+               foreach( $attribs as $attribute => $value ) {
+                       if( !isset( $whitelist[$attribute] ) ) {
+                               continue;
+                       }
+                       # Strip javascript "expression" from stylesheets.
+                       # http://msdn.microsoft.com/workshop/author/dhtml/overview/recalc.asp
+                       if( $attribute == 'style' ) {
+                               $value = Sanitizer::checkCss( $value );
+                               if( $value === false ) {
+                                       # haxx0r
+                                       continue;
+                               }
+                       }
+
+                       if ( $attribute === 'id' )
+                               $value = Sanitizer::escapeId( $value );
+
+                       // If this attribute was previously set, override it.
+                       // Output should only have one attribute of each name.
+                       $out[$attribute] = $value;
+               }
+               return $out;
+       }
+       
+       /**
+        * Pick apart some CSS and check it for forbidden or unsafe structures.
+        * Returns a sanitized string, or false if it was just too evil.
+        *
+        * Currently URL references, 'expression', 'tps' are forbidden.
+        *
+        * @param string $value
+        * @return mixed
+        */
+       static function checkCss( $value ) {
+               $stripped = Sanitizer::decodeCharReferences( $value );
+
+               // Remove any comments; IE gets token splitting wrong
+               $stripped = StringUtils::delimiterReplace( '/*', '*/', ' ', $stripped );
+               
+               $value = $stripped;
+
+               // ... and continue checks
+               $stripped = preg_replace( '!\\\\([0-9A-Fa-f]{1,6})[ \\n\\r\\t\\f]?!e',
+                       'codepointToUtf8(hexdec("$1"))', $stripped );
+               $stripped = str_replace( '\\', '', $stripped );
+               if( preg_match( '/(expression|tps*:\/\/|url\\s*\().*/is',
+                               $stripped ) ) {
+                       # haxx0r
+                       return false;
+               }
+               
+               return $value;
+       }
+
        /**
         * Take a tag soup fragment listing an HTML element's attributes
         * and normalize it to well-formed XML, discarding unwanted attributes.
+        * Output is safe for further wikitext processing, with escaping of
+        * values that could trigger problems.
         *
         * - Normalizes attribute names to lowercase
         * - Discards attributes not on a whitelist for the given element
@@ -207,83 +642,202 @@ class Sanitizer {
         * @param string $text
         * @param string $element
         * @return string
-        *
-        * @todo Check for legal values where the DTD limits things.
-        * @todo Check for unique id attribute :P
         */
-       function fixTagAttributes( $text, $element ) {
+       static function fixTagAttributes( $text, $element ) {
                if( trim( $text ) == '' ) {
                        return '';
                }
-               
-               $attrib = '[A-Za-z0-9]'; #FIXME
-               $space = '[\x09\x0a\x0d\x20]';
+
+               $stripped = Sanitizer::validateTagAttributes(
+                       Sanitizer::decodeTagAttributes( $text ), $element );
+
+               $attribs = array();
+               foreach( $stripped as $attribute => $value ) {
+                       $encAttribute = htmlspecialchars( $attribute );
+                       $encValue = Sanitizer::safeEncodeAttribute( $value );
+
+                       $attribs[] = "$encAttribute=\"$encValue\"";
+               }
+               return count( $attribs ) ? ' ' . implode( ' ', $attribs ) : '';
+       }
+
+       /**
+        * Encode an attribute value for HTML output.
+        * @param $text
+        * @return HTML-encoded text fragment
+        */
+       static function encodeAttribute( $text ) {
+               $encValue = htmlspecialchars( $text );
+
+               // Whitespace is normalized during attribute decoding,
+               // so if we've been passed non-spaces we must encode them
+               // ahead of time or they won't be preserved.
+               $encValue = strtr( $encValue, array(
+                       "\n" => '&#10;',
+                       "\r" => '&#13;',
+                       "\t" => '&#9;',
+               ) );
+
+               return $encValue;
+       }
+
+       /**
+        * Encode an attribute value for HTML tags, with extra armoring
+        * against further wiki processing.
+        * @param $text
+        * @return HTML-encoded text fragment
+        */
+       static function safeEncodeAttribute( $text ) {
+               $encValue = Sanitizer::encodeAttribute( $text );
+
+               # Templates and links may be expanded in later parsing,
+               # creating invalid or dangerous output. Suppress this.
+               $encValue = strtr( $encValue, array(
+                       '<'    => '&lt;',   // This should never happen,
+                       '>'    => '&gt;',   // we've received invalid input
+                       '"'    => '&quot;', // which should have been escaped.
+                       '{'    => '&#123;',
+                       '['    => '&#91;',
+                       "''"   => '&#39;&#39;',
+                       'ISBN' => '&#73;SBN',
+                       'RFC'  => '&#82;FC',
+                       'PMID' => '&#80;MID',
+                       '|'    => '&#124;',
+                       '__'   => '&#95;_',
+               ) );
+
+               # Stupid hack
+               $encValue = preg_replace_callback(
+                       '/(' . wfUrlProtocols() . ')/',
+                       array( 'Sanitizer', 'armorLinksCallback' ),
+                       $encValue );
+               return $encValue;
+       }
+
+       /**
+        * Given a value escape it so that it can be used in an id attribute and
+        * return it, this does not validate the value however (see first link)
+        *
+        * @link http://www.w3.org/TR/html401/types.html#type-name Valid characters
+        *                                                          in the id and
+        *                                                          name attributes
+        * @link http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute
+        *
+        * @bug 4461
+        *
+        * @static
+        *
+        * @param string $id
+        * @return string
+        */
+       static function escapeId( $id ) {
+               static $replace = array(
+                       '%3A' => ':',
+                       '%' => '.'
+               );
+
+               $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) );
+
+               return str_replace( array_keys( $replace ), array_values( $replace ), $id );
+       }
+
+       /**
+        * Given a value, escape it so that it can be used as a CSS class and
+        * return it.
+        *
+        * @todo For extra validity, input should be validated UTF-8.
+        *
+        * @link http://www.w3.org/TR/CSS21/syndata.html Valid characters/format
+        *
+        * @param string $class
+        * @return string
+        */
+       static function escapeClass( $class ) {
+               // Convert ugly stuff to underscores and kill underscores in ugly places
+               return rtrim(preg_replace(
+                       array('/(^[0-9\\-])|[\\x00-\\x20!"#$%&\'()*+,.\\/:;<=>?@[\\]^`{|}~]|\\xC2\\xA0/','/_+/'),
+                       '_',
+                       $class ), '_');
+       }
+
+       /**
+        * Regex replace callback for armoring links against further processing.
+        * @param array $matches
+        * @return string
+        * @private
+        */
+       private static function armorLinksCallback( $matches ) {
+               return str_replace( ':', '&#58;', $matches[1] );
+       }
+
+       /**
+        * Return an associative array of attribute names and values from
+        * a partial tag string. Attribute names are forces to lowercase,
+        * character references are decoded to UTF-8 text.
+        *
+        * @param string
+        * @return array
+        */
+       static function decodeTagAttributes( $text ) {
+               $attribs = array();
+
+               if( trim( $text ) == '' ) {
+                       return $attribs;
+               }
+
+               $pairs = array();
                if( !preg_match_all(
-                       "/(?:^|$space)($attrib+)
-                         ($space*=$space*
-                           (?:
-                            # The attribute value: quoted or alone
-                             \"([^<\"]*)\"
-                            | '([^<']*)'
-                            |  ([a-zA-Z0-9._:-]+)
-                            |  (\#[0-9a-fA-F]+) # Technically wrong, but lots of
-                                                # colors are specified like this.
-                                                # We'll be normalizing it.
-                           )
-                          )?(?=$space|\$)/sx",
+                       MW_ATTRIBS_REGEX,
                        $text,
                        $pairs,
                        PREG_SET_ORDER ) ) {
-                       return '';
+                       return $attribs;
                }
 
-               $whitelist = array_flip( Sanitizer::attributeWhitelist( $element ) );
-               $attribs = array();
                foreach( $pairs as $set ) {
                        $attribute = strtolower( $set[1] );
-                       if( !isset( $whitelist[$attribute] ) ) {
-                               continue;
-                       }
-                       if( !isset( $set[2] ) ) {
-                               # In XHTML, attributes must have a value.
-                               $value = $set[1];
-                       } elseif( $set[3] != '' ) {
-                               # Double-quoted
-                               $value = Sanitizer::normalizeAttributeValue( $set[3] );
-                       } elseif( $set[4] != '' ) {
-                               # Single-quoted
-                               $value = str_replace( '"', '&quot;',
-                                       Sanitizer::normalizeAttributeValue( $set[4] ) );
-                       } elseif( $set[5] != '' ) {
-                               # No quotes.
-                               $value = Sanitizer::normalizeAttributeValue( $set[5] );
-                       } elseif( $set[6] != '' ) {
-                               # Illegal #XXXXXX color with no quotes.
-                               $value = Sanitizer::normalizeAttributeValue( $set[6] );
-                       } else {
-                               wfDebugDieBacktrace( "Tag conditions not met. Something's very odd." );
-                       }
-                       
-                       # Strip javascript "expression" from stylesheets.
-                       # http://msdn.microsoft.com/workshop/author/dhtml/overview/recalc.asp
-                       if( $attribute == 'style' && preg_match(
-                               '/(expression|tps*:\/\/|url\\s*\().*/is',
-                                       wfMungeToUtf8( $value ) ) ) {
-                               # haxx0r
-                               continue;
-                       }
-                       
-                       if( !isset( $attribs[$attribute] ) ) {
-                               $attribs[$attribute] = "$attribute=\"$value\"";
-                       }
+                       $value = Sanitizer::getTagAttributeCallback( $set );
+
+                       // Normalize whitespace
+                       $value = preg_replace( '/[\t\r\n ]+/', ' ', $value );
+                       $value = trim( $value );
+
+                       // Decode character references
+                       $attribs[$attribute] = Sanitizer::decodeCharReferences( $value );
                }
-               if( empty( $attribs ) ) {
-                       return '';
+               return $attribs;
+       }
+
+       /**
+        * Pick the appropriate attribute value from a match set from the
+        * MW_ATTRIBS_REGEX matches.
+        *
+        * @param array $set
+        * @return string
+        * @private
+        */
+       private static function getTagAttributeCallback( $set ) {
+               if( isset( $set[6] ) ) {
+                       # Illegal #XXXXXX color with no quotes.
+                       return $set[6];
+               } elseif( isset( $set[5] ) ) {
+                       # No quotes.
+                       return $set[5];
+               } elseif( isset( $set[4] ) ) {
+                       # Single-quoted
+                       return $set[4];
+               } elseif( isset( $set[3] ) ) {
+                       # Double-quoted
+                       return $set[3];
+               } elseif( !isset( $set[2] ) ) {
+                       # In XHTML, attributes must have a value.
+                       # For 'reduced' form, return explicitly the attribute name here.
+                       return $set[1];
                } else {
-                       return ' ' . implode( ' ', $attribs );
+                       throw new MWException( "Tag conditions not met. This should never happen and is a bug." );
                }
        }
-       
+
        /**
         * Normalize whitespace and character references in an XML source-
         * encoded text for an attribute value.
@@ -294,15 +848,16 @@ class Sanitizer {
         *
         * @param string $text
         * @return string
-        * @access private
+        * @private
         */
-       function normalizeAttributeValue( $text ) {
-               return preg_replace(
-                       '/\r\n|[\x20\x0d\x0a\x09]/',
-                       ' ',
-                       Sanitizer::normalizeCharReferences( $text ) );
+       private static function normalizeAttributeValue( $text ) {
+               return str_replace( '"', '&quot;',
+                       preg_replace(
+                               '/\r\n|[\x20\x0d\x0a\x09]/',
+                               ' ',
+                               Sanitizer::normalizeCharReferences( $text ) ) );
        }
-       
+
        /**
         * Ensure that any entities and character references are legal
         * for XML and XHTML specifically. Any stray bits will be
@@ -315,15 +870,11 @@ class Sanitizer {
         *
         * @param string $text
         * @return string
-        * @access private
+        * @private
         */
-       function normalizeCharReferences( $text ) {
+       static function normalizeCharReferences( $text ) {
                return preg_replace_callback(
-                       '/&([A-Za-z0-9]+);
-                        |&\#([0-9]+);
-                        |&\#x([0-9A-Za-z]+);
-                        |&\#X([0-9A-Za-z]+);
-                        |(&)/x',
+                       MW_CHAR_REFS_REGEX,
                        array( 'Sanitizer', 'normalizeCharReferencesCallback' ),
                        $text );
        }
@@ -331,7 +882,7 @@ class Sanitizer {
         * @param string $matches
         * @return string
         */
-       function normalizeCharReferencesCallback( $matches ) {
+       static function normalizeCharReferencesCallback( $matches ) {
                $ret = null;
                if( $matches[1] != '' ) {
                        $ret = Sanitizer::normalizeEntity( $matches[1] );
@@ -348,7 +899,7 @@ class Sanitizer {
                        return $ret;
                }
        }
-       
+
        /**
         * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
         * return the named entity reference as is. Otherwise, returns
@@ -356,281 +907,27 @@ class Sanitizer {
         *
         * @param string $name
         * @return string
+        * @static
         */
-       function normalizeEntity( $name ) {
-               # List of all named character entities defined in HTML 4.01
-               # http://www.w3.org/TR/html4/sgml/entities.html
-               static $htmlEntities = array(
-                       'aacute' => true,
-                       'Aacute' => true,
-                       'acirc' => true,
-                       'Acirc' => true,
-                       'acute' => true,
-                       'aelig' => true,
-                       'AElig' => true,
-                       'agrave' => true,
-                       'Agrave' => true,
-                       'alefsym' => true,
-                       'alpha' => true,
-                       'Alpha' => true,
-                       'amp' => true,
-                       'and' => true,
-                       'ang' => true,
-                       'apos' => true,
-                       'aring' => true,
-                       'Aring' => true,
-                       'asymp' => true,
-                       'atilde' => true,
-                       'Atilde' => true,
-                       'auml' => true,
-                       'Auml' => true,
-                       'bdquo' => true,
-                       'beta' => true,
-                       'Beta' => true,
-                       'brvbar' => true,
-                       'bull' => true,
-                       'cap' => true,
-                       'ccedil' => true,
-                       'Ccedil' => true,
-                       'cedil' => true,
-                       'cent' => true,
-                       'chi' => true,
-                       'Chi' => true,
-                       'circ' => true,
-                       'clubs' => true,
-                       'cong' => true,
-                       'copy' => true,
-                       'crarr' => true,
-                       'cup' => true,
-                       'curren' => true,
-                       'dagger' => true,
-                       'Dagger' => true,
-                       'darr' => true,
-                       'dArr' => true,
-                       'deg' => true,
-                       'delta' => true,
-                       'Delta' => true,
-                       'diams' => true,
-                       'divide' => true,
-                       'eacute' => true,
-                       'Eacute' => true,
-                       'ecirc' => true,
-                       'Ecirc' => true,
-                       'egrave' => true,
-                       'Egrave' => true,
-                       'empty' => true,
-                       'emsp' => true,
-                       'ensp' => true,
-                       'epsilon' => true,
-                       'Epsilon' => true,
-                       'equiv' => true,
-                       'eta' => true,
-                       'Eta' => true,
-                       'eth' => true,
-                       'ETH' => true,
-                       'euml' => true,
-                       'Euml' => true,
-                       'euro' => true,
-                       'exist' => true,
-                       'fnof' => true,
-                       'forall' => true,
-                       'frac12' => true,
-                       'frac14' => true,
-                       'frac34' => true,
-                       'frasl' => true,
-                       'gamma' => true,
-                       'Gamma' => true,
-                       'ge' => true,
-                       'gt' => true,
-                       'harr' => true,
-                       'hArr' => true,
-                       'hearts' => true,
-                       'hellip' => true,
-                       'iacute' => true,
-                       'Iacute' => true,
-                       'icirc' => true,
-                       'Icirc' => true,
-                       'iexcl' => true,
-                       'igrave' => true,
-                       'Igrave' => true,
-                       'image' => true,
-                       'infin' => true,
-                       'int' => true,
-                       'iota' => true,
-                       'Iota' => true,
-                       'iquest' => true,
-                       'isin' => true,
-                       'iuml' => true,
-                       'Iuml' => true,
-                       'kappa' => true,
-                       'Kappa' => true,
-                       'lambda' => true,
-                       'Lambda' => true,
-                       'lang' => true,
-                       'laquo' => true,
-                       'larr' => true,
-                       'lArr' => true,
-                       'lceil' => true,
-                       'ldquo' => true,
-                       'le' => true,
-                       'lfloor' => true,
-                       'lowast' => true,
-                       'loz' => true,
-                       'lrm' => true,
-                       'lsaquo' => true,
-                       'lsquo' => true,
-                       'lt' => true,
-                       'macr' => true,
-                       'mdash' => true,
-                       'micro' => true,
-                       'middot' => true,
-                       'minus' => true,
-                       'mu' => true,
-                       'Mu' => true,
-                       'nabla' => true,
-                       'nbsp' => true,
-                       'ndash' => true,
-                       'ne' => true,
-                       'ni' => true,
-                       'not' => true,
-                       'notin' => true,
-                       'nsub' => true,
-                       'ntilde' => true,
-                       'Ntilde' => true,
-                       'nu' => true,
-                       'Nu' => true,
-                       'oacute' => true,
-                       'Oacute' => true,
-                       'ocirc' => true,
-                       'Ocirc' => true,
-                       'oelig' => true,
-                       'OElig' => true,
-                       'ograve' => true,
-                       'Ograve' => true,
-                       'oline' => true,
-                       'omega' => true,
-                       'Omega' => true,
-                       'omicron' => true,
-                       'Omicron' => true,
-                       'oplus' => true,
-                       'or' => true,
-                       'ordf' => true,
-                       'ordm' => true,
-                       'oslash' => true,
-                       'Oslash' => true,
-                       'otilde' => true,
-                       'Otilde' => true,
-                       'otimes' => true,
-                       'ouml' => true,
-                       'Ouml' => true,
-                       'para' => true,
-                       'part' => true,
-                       'permil' => true,
-                       'perp' => true,
-                       'phi' => true,
-                       'Phi' => true,
-                       'pi' => true,
-                       'Pi' => true,
-                       'piv' => true,
-                       'plusmn' => true,
-                       'pound' => true,
-                       'prime' => true,
-                       'Prime' => true,
-                       'prod' => true,
-                       'prop' => true,
-                       'psi' => true,
-                       'Psi' => true,
-                       'quot' => true,
-                       'radic' => true,
-                       'rang' => true,
-                       'raquo' => true,
-                       'rarr' => true,
-                       'rArr' => true,
-                       'rceil' => true,
-                       'rdquo' => true,
-                       'real' => true,
-                       'reg' => true,
-                       'rfloor' => true,
-                       'rho' => true,
-                       'Rho' => true,
-                       'rlm' => true,
-                       'rsaquo' => true,
-                       'rsquo' => true,
-                       'sbquo' => true,
-                       'scaron' => true,
-                       'Scaron' => true,
-                       'sdot' => true,
-                       'sect' => true,
-                       'shy' => true,
-                       'sigma' => true,
-                       'Sigma' => true,
-                       'sigmaf' => true,
-                       'sim' => true,
-                       'spades' => true,
-                       'sub' => true,
-                       'sube' => true,
-                       'sum' => true,
-                       'sup' => true,
-                       'sup1' => true,
-                       'sup2' => true,
-                       'sup3' => true,
-                       'supe' => true,
-                       'szlig' => true,
-                       'tau' => true,
-                       'Tau' => true,
-                       'there4' => true,
-                       'theta' => true,
-                       'Theta' => true,
-                       'thetasym' => true,
-                       'thinsp' => true,
-                       'thorn' => true,
-                       'THORN' => true,
-                       'tilde' => true,
-                       'times' => true,
-                       'trade' => true,
-                       'uacute' => true,
-                       'Uacute' => true,
-                       'uarr' => true,
-                       'uArr' => true,
-                       'ucirc' => true,
-                       'Ucirc' => true,
-                       'ugrave' => true,
-                       'Ugrave' => true,
-                       'uml' => true,
-                       'upsih' => true,
-                       'upsilon' => true,
-                       'Upsilon' => true,
-                       'uuml' => true,
-                       'Uuml' => true,
-                       'weierp' => true,
-                       'xi' => true,
-                       'Xi' => true,
-                       'yacute' => true,
-                       'Yacute' => true,
-                       'yen' => true,
-                       'yuml' => true,
-                       'Yuml' => true,
-                       'zeta' => true,
-                       'Zeta' => true,
-                       'zwj' => true,
-                       'zwnj' => true );
-               if( isset( $htmlEntities[$name] ) ) {
+       static function normalizeEntity( $name ) {
+               global $wgHtmlEntities;
+               if( isset( $wgHtmlEntities[$name] ) ) {
                        return "&$name;";
                } else {
                        return "&amp;$name;";
                }
        }
-       
-       function decCharReference( $codepoint ) {
-               $point = IntVal( $codepoint );
+
+       static function decCharReference( $codepoint ) {
+               $point = intval( $codepoint );
                if( Sanitizer::validateCodepoint( $point ) ) {
                        return sprintf( '&#%d;', $point );
                } else {
                        return null;
                }
        }
-       
-       function hexCharReference( $codepoint ) {
+
+       static function hexCharReference( $codepoint ) {
                $point = hexdec( $codepoint );
                if( Sanitizer::validateCodepoint( $point ) ) {
                        return sprintf( '&#x%x;', $point );
@@ -638,13 +935,13 @@ class Sanitizer {
                        return null;
                }
        }
-       
+
        /**
         * Returns true if a given Unicode codepoint is a valid character in XML.
         * @param int $codepoint
         * @return bool
         */
-       function validateCodepoint( $codepoint ) {
+       private static function validateCodepoint( $codepoint ) {
                return ($codepoint ==    0x09)
                        || ($codepoint ==    0x0a)
                        || ($codepoint ==    0x0d)
@@ -653,6 +950,72 @@ class Sanitizer {
                        || ($codepoint >= 0x10000 && $codepoint <= 0x10ffff);
        }
 
+       /**
+        * Decode any character references, numeric or named entities,
+        * in the text and return a UTF-8 string.
+        *
+        * @param string $text
+        * @return string
+        * @public
+        * @static
+        */
+       public static function decodeCharReferences( $text ) {
+               return preg_replace_callback(
+                       MW_CHAR_REFS_REGEX,
+                       array( 'Sanitizer', 'decodeCharReferencesCallback' ),
+                       $text );
+       }
+
+       /**
+        * @param string $matches
+        * @return string
+        */
+       static function decodeCharReferencesCallback( $matches ) {
+               if( $matches[1] != '' ) {
+                       return Sanitizer::decodeEntity( $matches[1] );
+               } elseif( $matches[2] != '' ) {
+                       return  Sanitizer::decodeChar( intval( $matches[2] ) );
+               } elseif( $matches[3] != ''  ) {
+                       return  Sanitizer::decodeChar( hexdec( $matches[3] ) );
+               } elseif( $matches[4] != '' ) {
+                       return  Sanitizer::decodeChar( hexdec( $matches[4] ) );
+               }
+               # Last case should be an ampersand by itself
+               return $matches[0];
+       }
+
+       /**
+        * Return UTF-8 string for a codepoint if that is a valid
+        * character reference, otherwise U+FFFD REPLACEMENT CHARACTER.
+        * @param int $codepoint
+        * @return string
+        * @private
+        */
+       static function decodeChar( $codepoint ) {
+               if( Sanitizer::validateCodepoint( $codepoint ) ) {
+                       return codepointToUtf8( $codepoint );
+               } else {
+                       return UTF8_REPLACEMENT;
+               }
+       }
+
+       /**
+        * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
+        * return the UTF-8 encoding of that character. Otherwise, returns
+        * pseudo-entity source (eg &foo;)
+        *
+        * @param string $name
+        * @return string
+        */
+       static function decodeEntity( $name ) {
+               global $wgHtmlEntities;
+               if( isset( $wgHtmlEntities[$name] ) ) {
+                       return codepointToUtf8( $wgHtmlEntities[$name] );
+               } else {
+                       return "&$name;";
+               }
+       }
+
        /**
         * Fetch the whitelist of acceptable attributes for a given
         * element name.
@@ -660,17 +1023,21 @@ class Sanitizer {
         * @param string $element
         * @return array
         */
-       function attributeWhitelist( $element ) {
-               $list = Sanitizer::setupAttributeWhitelist();
+       static function attributeWhitelist( $element ) {
+               static $list;
+               if( !isset( $list ) ) {
+                       $list = Sanitizer::setupAttributeWhitelist();
+               }
                return isset( $list[$element] )
                        ? $list[$element]
                        : array();
        }
-       
+
        /**
+        * @todo Document it a bit
         * @return array
         */
-       function setupAttributeWhitelist() {
+       static function setupAttributeWhitelist() {
                $common = array( 'id', 'class', 'lang', 'dir', 'title', 'style' );
                $block = array_merge( $common, array( 'align' ) );
                $tablealign = array( 'align', 'char', 'charoff', 'valign' );
@@ -681,10 +1048,11 @@ class Sanitizer {
                                    'rowspan',
                                    'colspan',
                                    'nowrap', # deprecated
-                                   'width', # deprecated
-                                   'height' # deprecated
+                                   'width',  # deprecated
+                                   'height', # deprecated
+                                   'bgcolor' # deprecated
                                    );
-               
+
                # Numbers refer to sections in HTML 4.01 standard describing the element.
                # See: http://www.w3.org/TR/html4/
                $whitelist = array (
@@ -692,7 +1060,7 @@ class Sanitizer {
                        'div'        => $block,
                        'center'     => $common, # deprecated
                        'span'       => $block, # ??
-               
+
                        # 7.5.5
                        'h1'         => $block,
                        'h2'         => $block,
@@ -700,13 +1068,13 @@ class Sanitizer {
                        'h4'         => $block,
                        'h5'         => $block,
                        'h6'         => $block,
-                       
+
                        # 7.5.6
                        # address
-                       
+
                        # 8.2.4
                        # bdo
-               
+
                        # 9.2.1
                        'em'         => $common,
                        'strong'     => $common,
@@ -718,64 +1086,64 @@ class Sanitizer {
                        'var'        => $common,
                        # abbr
                        # acronym
-                       
+
                        # 9.2.2
                        'blockquote' => array_merge( $common, array( 'cite' ) ),
                        # q
-                       
+
                        # 9.2.3
                        'sub'        => $common,
                        'sup'        => $common,
-                       
+
                        # 9.3.1
                        'p'          => $block,
-                       
+
                        # 9.3.2
                        'br'         => array( 'id', 'class', 'title', 'style', 'clear' ),
-                       
+
                        # 9.3.4
                        'pre'        => array_merge( $common, array( 'width' ) ),
-                       
+
                        # 9.4
                        'ins'        => array_merge( $common, array( 'cite', 'datetime' ) ),
                        'del'        => array_merge( $common, array( 'cite', 'datetime' ) ),
-                       
+
                        # 10.2
                        'ul'         => array_merge( $common, array( 'type' ) ),
                        'ol'         => array_merge( $common, array( 'type', 'start' ) ),
                        'li'         => array_merge( $common, array( 'type', 'value' ) ),
-                       
+
                        # 10.3
                        'dl'         => $common,
                        'dd'         => $common,
                        'dt'         => $common,
-               
+
                        # 11.2.1
                        'table'      => array_merge( $common,
                                                                array( 'summary', 'width', 'border', 'frame',
-                                                                                        'rules', 'cellspacing', 'cellpadding',
-                                                                                        'align', 'bgcolor', 'frame', 'rules',
-                                                                                        'border' ) ),
-                       
+                                                                               'rules', 'cellspacing', 'cellpadding',
+                                                                               'align', 'bgcolor',
+                                                               ) ),
+
                        # 11.2.2
                        'caption'    => array_merge( $common, array( 'align' ) ),
-                       
+
                        # 11.2.3
                        'thead'      => array_merge( $common, $tablealign ),
                        'tfoot'      => array_merge( $common, $tablealign ),
                        'tbody'      => array_merge( $common, $tablealign ),
-                       
+
                        # 11.2.4
                        'colgroup'   => array_merge( $common, array( 'span', 'width' ), $tablealign ),
                        'col'        => array_merge( $common, array( 'span', 'width' ), $tablealign ),
-                       
+
                        # 11.2.5
                        'tr'         => array_merge( $common, array( 'bgcolor' ), $tablealign ),
-                       
+
                        # 11.2.6
                        'td'         => array_merge( $common, $tablecell, $tablealign ),
                        'th'         => array_merge( $common, $tablecell, $tablealign ),
-                       
+
                        # 15.2.1
                        'tt'         => $common,
                        'b'          => $common,
@@ -785,14 +1153,14 @@ class Sanitizer {
                        'strike'     => $common,
                        's'          => $common,
                        'u'          => $common,
-               
+
                        # 15.2.2
                        'font'       => array_merge( $common, array( 'size', 'color', 'face' ) ),
                        # basefont
-                       
+
                        # 15.3
                        'hr'         => array_merge( $common, array( 'noshade', 'size', 'width' ) ),
-                       
+
                        # XHTML Ruby annotation text module, simple ruby only.
                        # http://www.w3c.org/TR/ruby/
                        'ruby'       => $common,
@@ -804,7 +1172,7 @@ class Sanitizer {
                        );
                return $whitelist;
        }
-       
+
        /**
         * Take a fragment of (potentially invalid) HTML and return
         * a version with any tags removed, encoded suitably for literal
@@ -813,23 +1181,85 @@ class Sanitizer {
         * @param string $text HTML fragment
         * @return string
         */
-       function stripAllTags( $text ) {
+       static function stripAllTags( $text ) {
                # Actual <tags>
-               $text = preg_replace( '/<[^>]*>/', '', $text );
-               
+               $text = StringUtils::delimiterReplace( '<', '>', '', $text );
+
                # Normalize &entities and whitespace
                $text = Sanitizer::normalizeAttributeValue( $text );
-               
+
                # Will be placed into "double-quoted" attributes,
                # make sure remaining bits are safe.
                $text = str_replace(
                        array('<', '>', '"'),
                        array('&lt;', '&gt;', '&quot;'),
                        $text );
-               
+
                return $text;
        }
 
+       /**
+        * Hack up a private DOCTYPE with HTML's standard entity declarations.
+        * PHP 4 seemed to know these if you gave it an HTML doctype, but
+        * PHP 5.1 doesn't.
+        *
+        * Use for passing XHTML fragments to PHP's XML parsing functions
+        *
+        * @return string
+        * @static
+        */
+       static function hackDocType() {
+               global $wgHtmlEntities;
+               $out = "<!DOCTYPE html [\n";
+               foreach( $wgHtmlEntities as $entity => $codepoint ) {
+                       $out .= "<!ENTITY $entity \"&#$codepoint;\">";
+               }
+               $out .= "]>\n";
+               return $out;
+       }
+
+       static function cleanUrl( $url, $hostname=true ) {
+               # Normalize any HTML entities in input. They will be
+               # re-escaped by makeExternalLink().
+               $url = Sanitizer::decodeCharReferences( $url );
+
+               # Escape any control characters introduced by the above step
+               $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url );
+
+               # Validate hostname portion
+               $matches = array();
+               if( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) {
+                       list( /* $whole */, $protocol, $host, $rest ) = $matches;
+
+                       // Characters that will be ignored in IDNs.
+                       // http://tools.ietf.org/html/3454#section-3.1
+                       // Strip them before further processing so blacklists and such work.
+                       $strip = "/
+                               \\s|          # general whitespace
+                               \xc2\xad|     # 00ad SOFT HYPHEN
+                               \xe1\xa0\x86| # 1806 MONGOLIAN TODO SOFT HYPHEN
+                               \xe2\x80\x8b| # 200b ZERO WIDTH SPACE
+                               \xe2\x81\xa0| # 2060 WORD JOINER
+                               \xef\xbb\xbf| # feff ZERO WIDTH NO-BREAK SPACE
+                               \xcd\x8f|     # 034f COMBINING GRAPHEME JOINER
+                               \xe1\xa0\x8b| # 180b MONGOLIAN FREE VARIATION SELECTOR ONE
+                               \xe1\xa0\x8c| # 180c MONGOLIAN FREE VARIATION SELECTOR TWO
+                               \xe1\xa0\x8d| # 180d MONGOLIAN FREE VARIATION SELECTOR THREE
+                               \xe2\x80\x8c| # 200c ZERO WIDTH NON-JOINER
+                               \xe2\x80\x8d| # 200d ZERO WIDTH JOINER
+                               [\xef\xb8\x80-\xef\xb8\x8f] # fe00-fe00f VARIATION SELECTOR-1-16
+                               /xuD";
+
+                       $host = preg_replace( $strip, '', $host );
+
+                       // @fixme: validate hostnames here
+
+                       return $protocol . $host . $rest;
+               } else {
+                       return $url;
+               }
+       }
+
 }
 
 ?>