* Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences
*/
define( 'MW_CHAR_REFS_REGEX',
- '/&([A-Za-z0-9]+);
+ '/&([A-Za-z0-9\x80-\xff]+);
|&\#([0-9]+);
|&\#x([0-9A-Za-z]+);
|&\#X([0-9A-Za-z]+);
'zwj' => 8205,
'zwnj' => 8204 );
+/**
+ * Character entity aliases accepted by MediaWiki
+ */
+global $wgHtmlEntityAliases;
+$wgHtmlEntityAliases = array(
+ 'רלמ' => 'rlm',
+ 'رلم' => 'rlm',
+);
+
+
+/**
+ * XHTML sanitizer for MediaWiki
+ * @addtogroup Parser
+ */
class Sanitizer {
+ const NONE = 0;
+ const INITIAL_NONLETTER = 1;
+
/**
* Cleans up HTML, removes dangerous tags and attributes, and
* removes HTML comments
* @param array $args for the processing callback
* @return string
*/
- static function removeHTMLtags( $text, $processCallback = null, $args = array() ) {
- global $wgUseTidy, $wgUserHtml;
+ static function removeHTMLtags( $text, $processCallback = null, $args = array(), $extratags = array() ) {
+ global $wgUseTidy;
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();
- }
+
+ $htmlpairs = array_merge( $extratags, 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',
+ );
$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',
+ $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags',
'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelements' );
foreach ( $vars as $var ) {
$$var = array_flip( $$var );
$optstack = array();
array_push ($optstack, $ot);
while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) &&
- isset( $htmlsingleallowed[$ot] ) )
+ isset( $htmlsingleallowed[$ot] ) )
{
array_push ($optstack, $ot);
}
*
* - Discards attributes not on a whitelist for the given element
* - Unsafe style attributes are discarded
+ * - Invalid id attributes are reencoded
*
* @param array $attribs
* @param string $element
* @todo Check for unique id attribute :P
*/
static function validateTagAttributes( $attribs, $element ) {
- $whitelist = array_flip( Sanitizer::attributeWhitelist( $element ) );
+ return Sanitizer::validateAttributes( $attribs,
+ Sanitizer::attributeWhitelist( $element ) );
+ }
+
+ /**
+ * Take an array of attribute names and values and normalize or discard
+ * illegal values for the given whitelist.
+ *
+ * - Discards attributes not the given whitelist
+ * - Unsafe style attributes are discarded
+ * - Invalid id attributes are reencoded
+ *
+ * @param array $attribs
+ * @param array $whitelist list of allowed attribute names
+ * @return array
+ *
+ * @todo Check for legal values where the DTD limits things.
+ * @todo Check for unique id attribute :P
+ */
+ static function validateAttributes( $attribs, $whitelist ) {
+ $whitelist = array_flip( $whitelist );
$out = array();
foreach( $attribs as $attribute => $value ) {
if( !isset( $whitelist[$attribute] ) ) {
}
return $out;
}
-
+
+ /**
+ * Merge two sets of HTML attributes.
+ * Conflicting items in the second set will override those
+ * in the first, except for 'class' attributes which will be
+ * combined.
+ *
+ * @todo implement merging for other attributes such as style
+ * @param array $a
+ * @param array $b
+ * @return array
+ */
+ static function mergeAttributes( $a, $b ) {
+ $out = array_merge( $a, $b );
+ if( isset( $a['class'] )
+ && isset( $b['class'] )
+ && $a['class'] !== $b['class'] ) {
+
+ $out['class'] = implode( ' ',
+ array_unique(
+ preg_split( '/\s+/',
+ $a['class'] . ' ' . $b['class'],
+ -1,
+ PREG_SPLIT_NO_EMPTY ) ) );
+ }
+ 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.
// 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',
+ if( preg_match( '/(?:expression|tps*:\/\/|url\\s*\().*/is',
$stripped ) ) {
# haxx0r
return false;
}
-
+
return $value;
}
* @return HTML-encoded text fragment
*/
static function encodeAttribute( $text ) {
- $encValue = htmlspecialchars( $text );
+ $encValue = htmlspecialchars( $text, ENT_QUOTES );
// Whitespace is normalized during attribute decoding,
// so if we've been passed non-spaces we must encode them
* 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
+ * @see 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
- *
- * @static
+ * @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute
*
- * @param string $id
+ * @param string $id Id to validate
+ * @param int $flags Currently only two values: Sanitizer::INITIAL_NONLETTER
+ * (default) permits initial non-letter characters,
+ * such as if you're adding a prefix to them.
+ * Sanitizer::NONE will prepend an 'x' if the id
+ * would otherwise start with a nonletter.
* @return string
*/
- static function escapeId( $id ) {
+ static function escapeId( $id, $flags = Sanitizer::INITIAL_NONLETTER ) {
static $replace = array(
'%3A' => ':',
'%' => '.'
);
$id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) );
+ $id = str_replace( array_keys( $replace ), array_values( $replace ), $id );
- return str_replace( array_keys( $replace ), array_values( $replace ), $id );
+ if( ~$flags & Sanitizer::INITIAL_NONLETTER
+ && !preg_match( '/[a-zA-Z]/', $id[0] ) ) {
+ // Initial character must be a letter!
+ $id = "x$id";
+ }
+ return $id;
}
/**
*
* @todo For extra validity, input should be validated UTF-8.
*
- * @link http://www.w3.org/TR/CSS21/syndata.html Valid characters/format
+ * @see http://www.w3.org/TR/CSS21/syndata.html Valid characters/format
*
* @param string $class
* @return string
*/
private static function normalizeAttributeValue( $text ) {
return str_replace( '"', '"',
- preg_replace(
- '/\r\n|[\x20\x0d\x0a\x09]/',
- ' ',
+ self::normalizeWhitespace(
Sanitizer::normalizeCharReferences( $text ) ) );
}
+ private static function normalizeWhitespace( $text ) {
+ return preg_replace(
+ '/\r\n|[\x20\x0d\x0a\x09]/',
+ ' ',
+ $text );
+ }
+
/**
* Ensure that any entities and character references are legal
* for XML and XHTML specifically. Any stray bits will be
/**
* If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
- * return the named entity reference as is. Otherwise, returns
- * HTML-escaped text of pseudo-entity source (eg &foo;)
+ * return the named entity reference as is. If the entity is a
+ * MediaWiki-specific alias, returns the HTML equivalent. Otherwise,
+ * returns HTML-escaped text of pseudo-entity source (eg &foo;)
*
* @param string $name
* @return string
* @static
*/
static function normalizeEntity( $name ) {
- global $wgHtmlEntities;
- if( isset( $wgHtmlEntities[$name] ) ) {
+ global $wgHtmlEntities, $wgHtmlEntityAliases;
+ if ( isset( $wgHtmlEntityAliases[$name] ) ) {
+ return "&{$wgHtmlEntityAliases[$name]};";
+ } elseif( isset( $wgHtmlEntities[$name] ) ) {
return "&$name;";
} else {
return "&$name;";
* @return string
*/
static function decodeEntity( $name ) {
- global $wgHtmlEntities;
+ global $wgHtmlEntities, $wgHtmlEntityAliases;
+ if ( isset( $wgHtmlEntityAliases[$name] ) ) {
+ $name = $wgHtmlEntityAliases[$name];
+ }
if( isset( $wgHtmlEntities[$name] ) ) {
return codepointToUtf8( $wgHtmlEntities[$name] );
} else {
'td' => array_merge( $common, $tablecell, $tablealign ),
'th' => array_merge( $common, $tablecell, $tablealign ),
+ # 13.2
+ # Not usually allowed, but may be used for extension-style hooks
+ # such as <math> when it is rasterized
+ 'img' => array_merge( $common, array( 'alt' ) ),
+
# 15.2.1
'tt' => $common,
'b' => $common,
'rb' => $common,
'rt' => $common, #array_merge( $common, array( 'rbspan' ) ),
'rp' => $common,
+
+ # MathML root element, where used for extensions
+ # 'title' may not be 100% valid here; it's XHTML
+ # http://www.w3.org/TR/REC-MathML/
+ 'math' => array( 'class', 'style', 'id', 'title' ),
);
return $whitelist;
}
/**
* Take a fragment of (potentially invalid) HTML and return
- * a version with any tags removed, encoded suitably for literal
- * inclusion in an attribute value.
+ * a version with any tags removed, encoded as plain text.
+ *
+ * Warning: this return value must be further escaped for literal
+ * inclusion in HTML output as of 1.10!
*
* @param string $text HTML fragment
* @return string
$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('<', '>', '"'),
- $text );
+ $text = self::decodeCharReferences( $text );
+ $text = self::normalizeWhitespace( $text );
return $text;
}
}
}
-
-?>