+ This parameter sets the maximum allowed length on img tags,
+ effectively the width and height properties.
+ Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is
+ in place to prevent imagecrash attacks, disable with null at your own risk.
+ This directive is similar to %HTML.MaxImgLength, and both should be
+ concurrently edited, although there are
+ subtle differences in the input format (the CSS max is a number with
+ a unit).
+
+ This directive controls the maximum number of pixels in the width and
+ height attributes in img tags. This is
+ in place to prevent imagecrash attacks, disable with null at your own risk.
+ This directive is similar to %CSS.MaxImgLength, and both should be
+ concurrently edited, although there are
+ subtle differences in the input format (the HTML max is an integer).
+
+ Whether or not to permit embed tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to embed tags. Embed is a proprietary + element and will cause your website to stop validating. You probably want + to enable this with %HTML.SafeObject. + Highly experimental. +
\ No newline at end of file diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt new file mode 100644 index 00000000000..6270abea47d --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt @@ -0,0 +1,13 @@ +HTML.SafeObject +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- ++ Whether or not to permit object tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to object tags. You may also want to + enable %HTML.SafeEmbed for maximum interoperability with Internet Explorer, + although embed tags will cause your website to stop validating. + Highly experimental. +
diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt index b1207aab799..7743ac29239 100644 --- a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -6,7 +6,7 @@ DEFAULT: NULL
Munges all browsable (usually http, https and ftp)
- absolute URI's into another URI, usually a URI redirection service.
+ absolute URIs into another URI, usually a URI redirection service.
This directive accepts a URI, formatted with a %s where
the url-encoded original URI should be inserted (sample:
http://www.google.com/url?q=%s).
@@ -19,13 +19,64 @@ DEFAULT: NULL
Prevent PageRank leaks, while being fairly transparent
to users (you may also want to add some client side JavaScript to
override the text in the statusbar). Notice:
- Many security experts believe that this form of protection does
-not deter spam-bots.
+ Many security experts believe that this form of protection does not deter spam-bots.
+ Prior to HTML Purifier 3.1.1, this directive also enabled the munging
+ of browsable external resources, which could break things if your redirection
+ script was a splash page or used meta tags. To revert to
+ previous behavior, please use %URI.MungeResources.
+
+ You may want to also use %URI.MungeSecretKey along with this directive + in order to enforce what URIs your redirector script allows. Open + redirector scripts can be a security risk and negatively affect the + reputation of your domain name. +
++ Starting with HTML Purifier 3.1.1, there is also these substitutions: +
+| Key | +Description | +Example <a href=""> |
+
|---|---|---|
| %r | +1 - The URI embeds a resource (blank) - The URI is merely a link |
+ + |
| %n | +The name of the tag this URI came from | +a | +
| %m | +The name of the attribute this URI came from | +href | +
| %p | +The name of the CSS property this URI came from, or blank if irrelevant | ++ |
+ Admittedly, these letters are somewhat arbitrary; the only stipulation + was that they couldn't be a through f. r is for resource (I would have preferred + e, but you take what you can get), n is for name, m + was picked because it came after n (and I couldn't use a), p is for + property. +
diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt new file mode 100644 index 00000000000..69b1ea83064 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt @@ -0,0 +1,16 @@ +URI.MungeResources +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +
+ If true, any URI munging directives like %URI.Munge
+ will also apply to embedded resources, such as <img src="">.
+ Be careful enabling this directive if you have a redirector script
+ that does not use the Location HTTP header; all of your images
+ and other embedded resources will break.
+
+ Warning: It is strongly advised you use this in conjunction + %URI.MungeSecretKey to mitigate the security risk of an open redirector. +
diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt new file mode 100644 index 00000000000..a2f5a02cc5f --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt @@ -0,0 +1,29 @@ +URI.MungeSecretKey +TYPE: string/null +VERSION: 3.1.1 +DEFAULT: NULL +--DESCRIPTION-- ++ This directive enables secure checksum generation along with %URI.Munge. + It should be set to a secure key that is not shared with anyone else. + The checksum can be placed in the URI using %t. Use of this checksum + affords an additional level of protection by allowing a redirector + to check if a URI has passed through HTML Purifier with this line: +
+ +$checksum === sha1($secret_key . ':' . $url)+ +
+ If the output is TRUE, the redirector script should accept the URI. +
+ ++ Please note that it would still be possible for an attacker to procure + secure hashes en-mass by abusing your website's Preview feature or the + like, but this service affords an additional level of protection + that should be combined with website blacklisting. +
+ ++ Remember this has no effect if %URI.Munge is not on. +
diff --git a/lib/htmlpurifier/HTMLPurifier/Encoder.php b/lib/htmlpurifier/HTMLPurifier/Encoder.php index 763684f789b..0518814ec36 100644 --- a/lib/htmlpurifier/HTMLPurifier/Encoder.php +++ b/lib/htmlpurifier/HTMLPurifier/Encoder.php @@ -46,35 +46,13 @@ class HTMLPurifier_Encoder */ public static function cleanUTF8($str, $force_php = false) { - static $non_sgml_chars = array(); - if (empty($non_sgml_chars)) { - for ($i = 0; $i <= 31; $i++) { - // non-SGML ASCII chars - // save \r, \t and \n - if ($i == 9 || $i == 13 || $i == 10) continue; - $non_sgml_chars[chr($i)] = ''; - } - for ($i = 127; $i <= 159; $i++) { - $non_sgml_chars[HTMLPurifier_Encoder::unichr($i)] = ''; - } - } - - static $iconv = null; - if ($iconv === null) $iconv = function_exists('iconv'); - // UTF-8 validity is checked since PHP 4.3.5 // This is an optimization: if the string is already valid UTF-8, no - // need to do iconv/php stuff. 99% of the time, this will be the case. - if (preg_match('/^.{1}/us', $str)) { - return strtr($str, $non_sgml_chars); - } - - if ($iconv && !$force_php) { - // do the shortcut way - set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); - $str = iconv('UTF-8', 'UTF-8//IGNORE', $str); - restore_error_handler(); - return strtr($str, $non_sgml_chars); + // need to do PHP stuff. 99% of the time, this will be the case. + // The regexp matches the XML char production, as well as well as excluding + // non-SGML codepoints U+007F to U+009F + if (preg_match('/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', $str)) { + return $str; } $mState = 0; // cached expected number of octets after the current octet @@ -185,7 +163,17 @@ class HTMLPurifier_Encoder ) { } elseif (0xFEFF != $mUcs4 && // omit BOM - !($mUcs4 >= 128 && $mUcs4 <= 159) // omit non-SGML + // check for valid Char unicode codepoints + ( + 0x9 == $mUcs4 || + 0xA == $mUcs4 || + 0xD == $mUcs4 || + (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || + // 7F-9F is not strictly prohibited by XML, + // but it is non-SGML, and thus we don't allow it + (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || + (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) + ) ) { $out .= $char; } @@ -276,17 +264,20 @@ class HTMLPurifier_Encoder * Converts a string to UTF-8 based on configuration. */ public static function convertToUTF8($str, $config, $context) { - static $iconv = null; - if ($iconv === null) $iconv = function_exists('iconv'); $encoding = $config->get('Core', 'Encoding'); if ($encoding === 'utf-8') return $str; + static $iconv = null; + if ($iconv === null) $iconv = function_exists('iconv'); + set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); if ($iconv && !$config->get('Test', 'ForceNoIconv')) { - set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); $str = iconv($encoding, 'utf-8//IGNORE', $str); + // If the string is bjorked by Shift_JIS or a similar encoding + // that doesn't support all of ASCII, convert the naughty + // characters to their true byte-wise ASCII/UTF-8 equivalents. + $str = strtr($str, HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding)); restore_error_handler(); return $str; } elseif ($encoding === 'iso-8859-1') { - set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); $str = utf8_encode($str); restore_error_handler(); return $str; @@ -300,20 +291,28 @@ class HTMLPurifier_Encoder * characters being omitted. */ public static function convertFromUTF8($str, $config, $context) { - static $iconv = null; - if ($iconv === null) $iconv = function_exists('iconv'); $encoding = $config->get('Core', 'Encoding'); if ($encoding === 'utf-8') return $str; - if ($config->get('Core', 'EscapeNonASCIICharacters')) { + static $iconv = null; + if ($iconv === null) $iconv = function_exists('iconv'); + if ($escape = $config->get('Core', 'EscapeNonASCIICharacters')) { $str = HTMLPurifier_Encoder::convertToASCIIDumbLossless($str); } + set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); if ($iconv && !$config->get('Test', 'ForceNoIconv')) { - set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); + // Undo our previous fix in convertToUTF8, otherwise iconv will barf + $ascii_fix = HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding); + if (!$escape && !empty($ascii_fix)) { + $clear_fix = array(); + foreach ($ascii_fix as $utf8 => $native) $clear_fix[$utf8] = ''; + $str = strtr($str, $clear_fix); + } + $str = strtr($str, array_flip($ascii_fix)); + // Normal stuff $str = iconv('utf-8', $encoding . '//IGNORE', $str); restore_error_handler(); return $str; } elseif ($encoding === 'iso-8859-1') { - set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); $str = utf8_decode($str); restore_error_handler(); return $str; @@ -368,6 +367,47 @@ class HTMLPurifier_Encoder return $result; } + /** + * This expensive function tests whether or not a given character + * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will + * fail this test, and require special processing. Variable width + * encodings shouldn't ever fail. + * + * @param string $encoding Encoding name to test, as per iconv format + * @param bool $bypass Whether or not to bypass the precompiled arrays. + * @return Array of UTF-8 characters to their corresponding ASCII, + * which can be used to "undo" any overzealous iconv action. + */ + public static function testEncodingSupportsASCII($encoding, $bypass = false) { + static $encodings = array(); + if (!$bypass) { + if (isset($encodings[$encoding])) return $encodings[$encoding]; + $lenc = strtolower($encoding); + switch ($lenc) { + case 'shift_jis': + return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); + case 'johab': + return array("\xE2\x82\xA9" => '\\'); + } + if (strpos($lenc, 'iso-8859-') === 0) return array(); + } + $ret = array(); + set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); + if (iconv('UTF-8', $encoding, 'a') === false) return false; + for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars + $c = chr($i); + if (iconv('UTF-8', "$encoding//IGNORE", $c) === '') { + // Reverse engineer: what's the UTF-8 equiv of this byte + // sequence? This assumes that there's no variable width + // encoding that doesn't support ASCII. + $ret[iconv($encoding, 'UTF-8//IGNORE', $c)] = $c; + } + } + restore_error_handler(); + $encodings[$encoding] = $ret; + return $ret; + } + } diff --git a/lib/htmlpurifier/HTMLPurifier/Generator.php b/lib/htmlpurifier/HTMLPurifier/Generator.php index e35ce8cf355..42ea17fe6bc 100644 --- a/lib/htmlpurifier/HTMLPurifier/Generator.php +++ b/lib/htmlpurifier/HTMLPurifier/Generator.php @@ -35,8 +35,7 @@ class HTMLPurifier_Generator * @param $config Instance of HTMLPurifier_Config * @param $context Instance of HTMLPurifier_Context */ - public function __construct($config = null, $context = null) { - if (!$config) $config = HTMLPurifier_Config::createDefault(); + public function __construct($config, $context) { $this->config = $config; $this->_scriptFix = $config->get('Output', 'CommentScriptContents'); $this->_def = $config->getHTMLDefinition(); diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLDefinition.php b/lib/htmlpurifier/HTMLPurifier/HTMLDefinition.php index ce299f79cee..e647228c548 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLDefinition.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLDefinition.php @@ -76,6 +76,11 @@ class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition */ public $info_content_sets = array(); + /** + * Indexed list of HTMLPurifier_Injector to be used. + */ + public $info_injector = array(); + /** * Doctype object */ @@ -186,18 +191,22 @@ class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition $this->doctype = $this->manager->doctype; foreach ($this->manager->modules as $module) { - foreach($module->info_tag_transform as $k => $v) { + foreach($module->info_tag_transform as $k => $v) { if ($v === false) unset($this->info_tag_transform[$k]); else $this->info_tag_transform[$k] = $v; } - foreach($module->info_attr_transform_pre as $k => $v) { + foreach($module->info_attr_transform_pre as $k => $v) { if ($v === false) unset($this->info_attr_transform_pre[$k]); else $this->info_attr_transform_pre[$k] = $v; } - foreach($module->info_attr_transform_post as $k => $v) { + foreach($module->info_attr_transform_post as $k => $v) { if ($v === false) unset($this->info_attr_transform_post[$k]); else $this->info_attr_transform_post[$k] = $v; } + foreach ($module->info_injector as $k => $v) { + if ($v === false) unset($this->info_injector[$k]); + else $this->info_injector[$k] = $v; + } } $this->info = $this->manager->getElements(); @@ -356,6 +365,14 @@ class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition } } + // setup injectors ----------------------------------------------------- + foreach ($this->info_injector as $i => $injector) { + if ($injector->checkNeeded($config) !== false) { + // remove injector that does not have it's required + // elements/attributes present, and is thus not needed. + unset($this->info_injector[$i]); + } + } } /** diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule.php index db78983b2b1..22af1546901 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule.php @@ -71,6 +71,14 @@ class HTMLPurifier_HTMLModule */ public $info_attr_transform_post = array(); + /** + * List of HTMLPurifier_Injector to be performed during well-formedness fixing. + * An injector will only be invoked if all of it's pre-requisites are met; + * if an injector fails setup, there will be no error; it will simply be + * silently disabled. + */ + public $info_injector = array(); + /** * Boolean flag that indicates whether or not getChildDef is implemented. * For optimization reasons: may save a call to a function. Be sure @@ -222,5 +230,14 @@ class HTMLPurifier_HTMLModule } return $ret; } + + /** + * Lazy load construction of the module after determining whether + * or not it's needed, and also when a finalized configuration object + * is available. + * @param $config Instance of HTMLPurifier_Config + */ + public function setup($config) {} + } diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Bdo.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Bdo.php index b65057cabc4..0d643e8172f 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Bdo.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Bdo.php @@ -12,7 +12,7 @@ class HTMLPurifier_HTMLModule_Bdo extends HTMLPurifier_HTMLModule 'I18N' => array('dir' => false) ); - public function __construct() { + public function setup($config) { $bdo = $this->addElement( 'bdo', 'Inline', 'Inline', array('Core', 'Lang'), array( diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Edit.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Edit.php index 07d972e3bbe..157fca16d7c 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Edit.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Edit.php @@ -9,7 +9,7 @@ class HTMLPurifier_HTMLModule_Edit extends HTMLPurifier_HTMLModule public $name = 'Edit'; - public function __construct() { + public function setup($config) { $contents = 'Chameleon: #PCDATA | Inline ! #PCDATA | Flow'; $attr = array( 'cite' => 'URI', diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Hypertext.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Hypertext.php index 4a347e6f4cf..4451f2bbc7f 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Hypertext.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Hypertext.php @@ -8,7 +8,7 @@ class HTMLPurifier_HTMLModule_Hypertext extends HTMLPurifier_HTMLModule public $name = 'Hypertext'; - public function __construct() { + public function setup($config) { $a = $this->addElement( 'a', 'Inline', 'Inline', 'Common', array( diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Image.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Image.php index 7d2a98c3f82..7a80e327162 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Image.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Image.php @@ -10,17 +10,25 @@ class HTMLPurifier_HTMLModule_Image extends HTMLPurifier_HTMLModule public $name = 'Image'; - public function __construct() { + public function setup($config) { + $max = $config->get('HTML', 'MaxImgLength'); $img = $this->addElement( 'img', 'Inline', 'Empty', 'Common', array( 'alt*' => 'Text', - 'height' => 'Length', + // According to the spec, it's Length, but percents can + // be abused, so we allow only Pixels. + 'height' => 'Pixels#' . $max, + 'width' => 'Pixels#' . $max, 'longdesc' => 'URI', 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded - 'width' => 'Length' ) ); + if ($max === null || $config->get('HTML', 'Trusted')) { + $img->attr['height'] = + $img->attr['width'] = 'Length'; + } + // kind of strange, but splitting things up would be inefficient $img->attr_transform_pre[] = $img->attr_transform_post[] = diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Legacy.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Legacy.php index 903273ea4ce..dd1f04e000e 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Legacy.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Legacy.php @@ -21,7 +21,7 @@ class HTMLPurifier_HTMLModule_Legacy extends HTMLPurifier_HTMLModule public $name = 'Legacy'; - public function __construct() { + public function setup($config) { $this->addElement('basefont', 'Inline', 'Empty', false, array( 'color' => 'Color', diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/List.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/List.php index 7d4f9cf2ef7..a2cf2f56002 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/List.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/List.php @@ -19,7 +19,7 @@ class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule public $content_sets = array('Flow' => 'List'); - public function __construct() { + public function setup($config) { $this->addElement('ol', 'List', 'Required: li', 'Common'); $this->addElement('ul', 'List', 'Required: li', 'Common'); $this->addElement('dl', 'List', 'Required: dt | dd', 'Common'); diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Object.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Object.php index 609d3351038..7dfd6b31341 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Object.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Object.php @@ -11,7 +11,7 @@ class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule public $name = 'Object'; public $safe = false; - public function __construct() { + public function setup($config) { $this->addElement('object', 'Inline', 'Optional: #PCDATA | Flow | param', 'Common', array( diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Presentation.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Presentation.php index bd8368a8f80..857b3abf01b 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Presentation.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Presentation.php @@ -15,7 +15,7 @@ class HTMLPurifier_HTMLModule_Presentation extends HTMLPurifier_HTMLModule public $name = 'Presentation'; - public function __construct() { + public function setup($config) { $this->addElement('b', 'Inline', 'Inline', 'Common'); $this->addElement('big', 'Inline', 'Inline', 'Common'); $this->addElement('hr', 'Block', 'Empty', 'Common'); diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Proprietary.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Proprietary.php index d5c72418735..bcbcd124ff0 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Proprietary.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Proprietary.php @@ -9,7 +9,7 @@ class HTMLPurifier_HTMLModule_Proprietary extends HTMLPurifier_HTMLModule public $name = 'Proprietary'; - public function __construct() { + public function setup($config) { $this->addElement('marquee', 'Inline', 'Flow', 'Common', array( diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Ruby.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Ruby.php index 21ec79a2612..caee035fcf9 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Ruby.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Ruby.php @@ -9,7 +9,7 @@ class HTMLPurifier_HTMLModule_Ruby extends HTMLPurifier_HTMLModule public $name = 'Ruby'; - public function __construct() { + public function setup($config) { $this->addElement('ruby', 'Inline', 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))', 'Common'); diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/SafeEmbed.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/SafeEmbed.php new file mode 100644 index 00000000000..8e57752199d --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/SafeEmbed.php @@ -0,0 +1,31 @@ +get('HTML', 'MaxImgLength'); + $embed = $this->addElement( + 'embed', 'Inline', 'Empty', 'Common', + array( + 'src*' => 'URI#embedded', + 'type' => 'Enum#application/x-shockwave-flash', + 'width' => 'Pixels#' . $max, + 'height' => 'Pixels#' . $max, + 'allowscriptaccess' => 'Enum#never', + 'allownetworking' => 'Enum#internal', + 'wmode' => 'Enum#window', + 'name' => 'ID', + ) + ); + $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed(); + + } + +} diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/SafeObject.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/SafeObject.php new file mode 100644 index 00000000000..a33be7d4fce --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/SafeObject.php @@ -0,0 +1,48 @@ +get('HTML', 'MaxImgLength'); + $object = $this->addElement( + 'object', + 'Inline', + 'Optional: param | Flow | #PCDATA', + 'Common', + array( + // While technically not required by the spec, we're forcing + // it to this value. + 'type' => 'Enum#application/x-shockwave-flash', + 'width' => 'Pixels#' . $max, + 'height' => 'Pixels#' . $max, + 'data' => 'URI#embedded' + ) + ); + $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject(); + + $param = $this->addElement('param', false, 'Empty', false, + array( + 'id' => 'ID', + 'name*' => 'Text', + 'value' => 'Text' + ) + ); + $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam(); + $this->info_injector[] = 'SafeObject'; + + } + +} diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Scripting.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Scripting.php index b3371b3a2bd..2b7e2ced49b 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Scripting.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Scripting.php @@ -20,7 +20,7 @@ class HTMLPurifier_HTMLModule_Scripting extends HTMLPurifier_HTMLModule public $content_sets = array('Block' => 'script | noscript', 'Inline' => 'script | noscript'); public $safe = false; - public function __construct() { + public function setup($config) { // TODO: create custom child-definition for noscript that // auto-wraps stray #PCDATA in a similar manner to // blockquote's custom definition (we would use it but diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/StyleAttribute.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/StyleAttribute.php index 3963e22c023..9775536dccb 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/StyleAttribute.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/StyleAttribute.php @@ -15,7 +15,7 @@ class HTMLPurifier_HTMLModule_StyleAttribute extends HTMLPurifier_HTMLModule 'Core' => array(0 => array('Style')) ); - public function __construct() { + public function setup($config) { $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS(); } diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Tables.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Tables.php index d6a13fff33f..dd70bddfc99 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Tables.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Tables.php @@ -8,7 +8,7 @@ class HTMLPurifier_HTMLModule_Tables extends HTMLPurifier_HTMLModule public $name = 'Tables'; - public function __construct() { + public function setup($config) { $this->addElement('caption', false, 'Inline', 'Common'); diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Target.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Target.php index 18203d046e5..042d458c588 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Target.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Target.php @@ -8,7 +8,7 @@ class HTMLPurifier_HTMLModule_Target extends HTMLPurifier_HTMLModule public $name = 'Target'; - public function __construct() { + public function setup($config) { $elements = array('a'); foreach ($elements as $name) { $e = $this->addBlankElement($name); diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Text.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Text.php index aff71e9ad28..3fce4ee53fa 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Text.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Text.php @@ -20,7 +20,7 @@ class HTMLPurifier_HTMLModule_Text extends HTMLPurifier_HTMLModule 'Flow' => 'Heading | Block | Inline' ); - public function __construct() { + public function setup($config) { // Inline Phrasal ------------------------------------------------- $this->addElement('abbr', 'Inline', 'Inline', 'Common'); diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Tidy.php b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Tidy.php index 1606ffc676c..a95d453b407 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModule/Tidy.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModule/Tidy.php @@ -35,7 +35,7 @@ class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule * @todo Wildcard matching and error reporting when an added or * subtracted fix has no effect. */ - public function construct($config) { + public function setup($config) { // create fixes, initialize fixesForLevel $fixes = $this->makeFixes(); diff --git a/lib/htmlpurifier/HTMLPurifier/HTMLModuleManager.php b/lib/htmlpurifier/HTMLPurifier/HTMLModuleManager.php index 612c3c01393..3664e88fcad 100644 --- a/lib/htmlpurifier/HTMLPurifier/HTMLModuleManager.php +++ b/lib/htmlpurifier/HTMLPurifier/HTMLModuleManager.php @@ -221,15 +221,35 @@ class HTMLPurifier_HTMLModuleManager $modules[] = 'Proprietary'; } + // add SafeObject/Safeembed modules + if ($config->get('HTML', 'SafeObject')) { + $modules[] = 'SafeObject'; + } + if ($config->get('HTML', 'SafeEmbed')) { + $modules[] = 'SafeEmbed'; + } + foreach ($modules as $module) { $this->processModule($module); + $this->modules[$module]->setup($config); } foreach ($this->doctype->tidyModules as $module) { $this->processModule($module); - if (method_exists($this->modules[$module], 'construct')) { - $this->modules[$module]->construct($config); + $this->modules[$module]->setup($config); + } + + // prepare any injectors + foreach ($this->modules as $module) { + $n = array(); + foreach ($module->info_injector as $i => $injector) { + if (!is_object($injector)) { + $class = "HTMLPurifier_Injector_$injector"; + $injector = new $class; + } + $n[$injector->name] = $injector; } + $module->info_injector = $n; } // setup lookup table based on all valid modules diff --git a/lib/htmlpurifier/HTMLPurifier/Injector.php b/lib/htmlpurifier/HTMLPurifier/Injector.php index c9f9f2eb55a..1947c340fe8 100644 --- a/lib/htmlpurifier/HTMLPurifier/Injector.php +++ b/lib/htmlpurifier/HTMLPurifier/Injector.php @@ -58,29 +58,45 @@ abstract class HTMLPurifier_Injector * Prepares the injector by giving it the config and context objects: * this allows references to important variables to be made within * the injector. This function also checks if the HTML environment - * will work with the Injector: if p tags are not allowed, the - * Auto-Paragraphing injector should not be enabled. + * will work with the Injector (see checkNeeded()). * @param $config Instance of HTMLPurifier_Config * @param $context Instance of HTMLPurifier_Context * @return Boolean false if success, string of missing needed element/attribute if failure */ public function prepare($config, $context) { $this->htmlDefinition = $config->getHTMLDefinition(); - // perform $needed checks - foreach ($this->needed as $element => $attributes) { - if (is_int($element)) $element = $attributes; - if (!isset($this->htmlDefinition->info[$element])) return $element; - if (!is_array($attributes)) continue; - foreach ($attributes as $name) { - if (!isset($this->htmlDefinition->info[$element]->attr[$name])) return "$element.$name"; - } - } + // Even though this might fail, some unit tests ignore this and + // still test checkNeeded, so be careful. Maybe get rid of that + // dependency. + $result = $this->checkNeeded($config); + if ($result !== false) return $result; $this->currentNesting =& $context->get('CurrentNesting'); $this->inputTokens =& $context->get('InputTokens'); $this->inputIndex =& $context->get('InputIndex'); return false; } + /** + * This function checks if the HTML environment + * will work with the Injector: if p tags are not allowed, the + * Auto-Paragraphing injector should not be enabled. + * @param $config Instance of HTMLPurifier_Config + * @param $context Instance of HTMLPurifier_Context + * @return Boolean false if success, string of missing needed element/attribute if failure + */ + public function checkNeeded($config) { + $def = $config->getHTMLDefinition(); + foreach ($this->needed as $element => $attributes) { + if (is_int($element)) $element = $attributes; + if (!isset($def->info[$element])) return $element; + if (!is_array($attributes)) continue; + foreach ($attributes as $name) { + if (!isset($def->info[$element]->attr[$name])) return "$element.$name"; + } + } + return false; + } + /** * Tests if the context node allows a certain element * @param $name Name of element to test for diff --git a/lib/htmlpurifier/HTMLPurifier/Injector/SafeObject.php b/lib/htmlpurifier/HTMLPurifier/Injector/SafeObject.php new file mode 100644 index 00000000000..f379406866b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/Injector/SafeObject.php @@ -0,0 +1,83 @@ + 'never', + 'allowNetworking' => 'internal', + ); + protected $allowedParam = array( + 'wmode' => true, + 'movie' => true, + ); + + public function prepare($config, $context) { + parent::prepare($config, $context); + } + + public function handleElement(&$token) { + if ($token->name == 'object') { + $this->objectStack[] = $token; + $this->paramStack[] = array(); + $new = array($token); + foreach ($this->addParam as $name => $value) { + $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value)); + } + $token = $new; + } elseif ($token->name == 'param') { + $nest = count($this->currentNesting) - 1; + if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') { + $i = count($this->objectStack) - 1; + if (!isset($token->attr['name'])) { + $token = false; + return; + } + $n = $token->attr['name']; + // We need this fix because YouTube doesn't supply a data + // attribute, which we need if a type is specified. This is + // *very* Flash specific. + if (!isset($this->objectStack[$i]->attr['data']) && $token->attr['name'] == 'movie') { + $this->objectStack[$i]->attr['data'] = $token->attr['value']; + } + // Check if the parameter is the correct value but has not + // already been added + if ( + !isset($this->paramStack[$i][$n]) && + isset($this->addParam[$n]) && + $token->attr['name'] === $this->addParam[$n] + ) { + // keep token, and add to param stack + $this->paramStack[$i][$n] = true; + } elseif (isset($this->allowedParam[$n])) { + // keep token, don't do anything to it + // (could possibly check for duplicates here) + } else { + $token = false; + } + } else { + // not directly inside an object, DENY! + $token = false; + } + } + } + + public function notifyEnd($token) { + if ($token->name == 'object') { + array_pop($this->objectStack); + array_pop($this->paramStack); + } + } + +} + diff --git a/lib/htmlpurifier/HTMLPurifier/Length.php b/lib/htmlpurifier/HTMLPurifier/Length.php new file mode 100644 index 00000000000..758f9bb0e4d --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/Length.php @@ -0,0 +1,113 @@ + true, 'ex' => true, 'px' => true, 'in' => true, + 'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true + ); + + /** + * @param number $n Magnitude + * @param string $u Unit + */ + public function __construct($n = '0', $u = false) { + $this->n = (string) $n; + $this->unit = $u !== false ? (string) $u : false; + } + + /** + * @param string $s Unit string, like '2em' or '3.4in' + * @warning Does not perform validation. + */ + static public function make($s) { + if ($s instanceof HTMLPurifier_Length) return $s; + $n_length = strspn($s, '1234567890.+-'); + $n = substr($s, 0, $n_length); + $unit = substr($s, $n_length); + if ($unit === '') $unit = false; + return new HTMLPurifier_Length($n, $unit); + } + + /** + * Validates the number and unit. + */ + protected function validate() { + // Special case: + if ($this->n === '+0' || $this->n === '-0') $this->n = '0'; + if ($this->n === '0' && $this->unit === false) return true; + if (!ctype_lower($this->unit)) $this->unit = strtolower($this->unit); + if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) return false; + // Hack: + $def = new HTMLPurifier_AttrDef_CSS_Number(); + $result = $def->validate($this->n, false, false); + if ($result === false) return false; + $this->n = $result; + return true; + } + + /** + * Returns string representation of number. + */ + public function toString() { + if (!$this->isValid()) return false; + return $this->n . $this->unit; + } + + /** + * Retrieves string numeric magnitude. + */ + public function getN() {return $this->n;} + + /** + * Retrieves string unit. + */ + public function getUnit() {return $this->unit;} + + /** + * Returns true if this length unit is valid. + */ + public function isValid() { + if ($this->isValid === null) $this->isValid = $this->validate(); + return $this->isValid; + } + + /** + * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal. + * @warning If both values are too large or small, this calculation will + * not work properly + */ + public function compareTo($l) { + if ($l === false) return false; + if ($l->unit !== $this->unit) { + $converter = new HTMLPurifier_UnitConverter(); + $l = $converter->convert($l, $this->unit); + if ($l === false) return false; + } + return $this->n - $l->n; + } + +} diff --git a/lib/htmlpurifier/HTMLPurifier/Printer.php b/lib/htmlpurifier/HTMLPurifier/Printer.php index 680ffa1d1a5..238cf951d8a 100644 --- a/lib/htmlpurifier/HTMLPurifier/Printer.php +++ b/lib/htmlpurifier/HTMLPurifier/Printer.php @@ -20,18 +20,15 @@ class HTMLPurifier_Printer * Initialize $generator. */ public function __construct() { - $this->generator = new HTMLPurifier_Generator(); } /** * Give generator necessary configuration if possible */ public function prepareGenerator($config) { - // hack for smoketests/configForm.php $all = $config->getAll(); - if (empty($all['HTML'])) return; $context = new HTMLPurifier_Context(); - $this->generator->generateFromTokens(array(), $config, $context); + $this->generator = new HTMLPurifier_Generator($config, $context); } /** diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php index 172d7bb48ed..9caf7e39a6b 100644 --- a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php +++ b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php @@ -1,5 +1,8 @@ name = $name; $this->compress = $compress; // initialize sub-printers - $this->fields['default'] = new HTMLPurifier_Printer_ConfigForm_default(); - $this->fields['bool'] = new HTMLPurifier_Printer_ConfigForm_bool(); + $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); + $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); } /** @@ -68,14 +71,23 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer /** * Returns HTML output for a configuration form - * @param $config Configuration object of current form state + * @param $config Configuration object of current form state, or an array + * where [0] has an HTML namespace and [1] is being rendered. * @param $allowed Optional namespace(s) and directives to restrict form to. */ public function render($config, $allowed = true, $render_controls = true) { - $this->config = $config; - $this->prepareGenerator($config); + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } - $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed); + $this->config = $config; + $this->genConfig = $gen_config; + $this->prepareGenerator($gen_config); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); $all = array(); foreach ($allowed as $key) { list($ns, $directive) = $key; @@ -148,13 +160,19 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer $ret .= $this->start('td'); $def = $this->config->def->info[$ns][$directive]; - $type = $def->type; - if (!isset($this->fields[$type])) $type = 'default'; + if (is_int($def)) { + $allow_null = $def < 0; + $type = abs($def); + } else { + $type = $def->type; + $allow_null = isset($def->allow_null); + } + if (!isset($this->fields[$type])) $type = 0; // default $type_obj = $this->fields[$type]; - if ($def->allow_null) { + if ($allow_null) { $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); } - $ret .= $type_obj->render($ns, $directive, $value, $this->name, $this->config); + $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); $ret .= $this->end('td'); $ret .= $this->end('tr'); } @@ -180,7 +198,14 @@ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer $this->obj = $obj; } public function render($ns, $directive, $value, $name, $config) { - $this->prepareGenerator($config); + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + $ret = ''; $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); @@ -202,7 +227,7 @@ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer $ret .= $this->elementEmpty('input', $attr); $ret .= $this->text(' or '); $ret .= $this->elementEmpty('br'); - $ret .= $this->obj->render($ns, $directive, $value, $name, $config); + $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); return $ret; } } @@ -214,22 +239,33 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { public $cols = 18; public $rows = 5; public function render($ns, $directive, $value, $name, $config) { - $this->prepareGenerator($config); + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); // this should probably be split up a little $ret = ''; $def = $config->def->info[$ns][$directive]; + if (is_int($def)) { + $type = abs($def); + } else { + $type = $def->type; + } if (is_array($value)) { - switch ($def->type) { - case 'lookup': + switch ($type) { + case HTMLPurifier_VarParser::LOOKUP: $array = $value; $value = array(); foreach ($array as $val => $b) { $value[] = $val; } - case 'list': + case HTMLPurifier_VarParser::ALIST: $value = implode(PHP_EOL, $value); break; - case 'hash': + case HTMLPurifier_VarParser::HASH: $nvalue = ''; foreach ($value as $i => $v) { $nvalue .= "$i:$v" . PHP_EOL; @@ -240,7 +276,7 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { $value = ''; } } - if ($def->type === 'mixed') { + if ($type === HTMLPurifier_VarParser::MIXED) { return 'Not supported'; $value = serialize($value); } @@ -249,7 +285,7 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { 'id' => "$name:$ns.$directive" ); if ($value === null) $attr['disabled'] = 'disabled'; - if (is_array($def->allowed)) { + if (isset($def->allowed)) { $ret .= $this->start('select', $attr); foreach ($def->allowed as $val => $b) { $attr = array(); @@ -258,8 +294,11 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { } $ret .= $this->end('select'); } elseif ( - $def->type == 'text' || $def->type == 'itext' || - $def->type == 'list' || $def->type == 'hash' || $def->type == 'lookup' + $type === HTMLPurifier_VarParser::TEXT || + $type === HTMLPurifier_VarParser::ITEXT || + $type === HTMLPurifier_VarParser::ALIST || + $type === HTMLPurifier_VarParser::HASH || + $type === HTMLPurifier_VarParser::LOOKUP ) { $attr['cols'] = $this->cols; $attr['rows'] = $this->rows; @@ -280,7 +319,13 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { */ class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer { public function render($ns, $directive, $value, $name, $config) { - $this->prepareGenerator($config); + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); $ret = ''; $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php b/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php index 8843627f13d..1ca62711584 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php @@ -38,6 +38,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy $this->injectors = array(); $injectors = $config->getBatch('AutoFormat'); + $def_injectors = $definition->info_injector; $custom_injectors = $injectors['Custom']; unset($injectors['Custom']); // special case foreach ($injectors as $injector => $b) { @@ -45,6 +46,10 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy if (!$b) continue; $this->injectors[] = new $injector; } + foreach ($def_injectors as $injector) { + // assumed to be objects + $this->injectors[] = $injector; + } foreach ($custom_injectors as $injector) { if (is_string($injector)) { $injector = "HTMLPurifier_Injector_$injector"; @@ -169,7 +174,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy if ($escape_invalid_tags) { if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text'); $result[] = new HTMLPurifier_Token_Text( - $generator->generateFromToken($token, $config, $context) + $generator->generateFromToken($token) ); } elseif ($e) { $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); @@ -209,7 +214,7 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy if ($skipped_tags === false) { if ($escape_invalid_tags) { $result[] = new HTMLPurifier_Token_Text( - $generator->generateFromToken($token, $config, $context) + $generator->generateFromToken($token) ); if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text'); } elseif ($e) { diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php b/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php index 9de69a11854..165308dd0cf 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php @@ -101,7 +101,7 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy // invalid tag, generate HTML representation and insert in if ($e) $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text'); $token = new HTMLPurifier_Token_Text( - $generator->generateFromToken($token, $config, $context) + $generator->generateFromToken($token) ); } else { // check if we need to destroy all of the tag's children diff --git a/lib/htmlpurifier/HTMLPurifier/Token.php b/lib/htmlpurifier/HTMLPurifier/Token.php index fd2ba53f259..8803307bda9 100644 --- a/lib/htmlpurifier/HTMLPurifier/Token.php +++ b/lib/htmlpurifier/HTMLPurifier/Token.php @@ -4,7 +4,6 @@ * Abstract base token class that all others inherit from. */ class HTMLPurifier_Token { - public $type; /**< Type of node to bypass is_a(). */ public $line; /**< Line number node was on in source document. Null if unknown. */ /** diff --git a/lib/htmlpurifier/HTMLPurifier/URI.php b/lib/htmlpurifier/HTMLPurifier/URI.php index 43f1b192875..cab09bfbafb 100644 --- a/lib/htmlpurifier/HTMLPurifier/URI.php +++ b/lib/htmlpurifier/HTMLPurifier/URI.php @@ -128,6 +128,17 @@ class HTMLPurifier_URI $this->path = ''; // just to be safe } + // qf = query and fragment + $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?'); + + if (!is_null($this->query)) { + $this->query = $qf_encoder->encode($this->query); + } + + if (!is_null($this->fragment)) { + $this->fragment = $qf_encoder->encode($this->fragment); + } + return true; } diff --git a/lib/htmlpurifier/HTMLPurifier/URIDefinition.php b/lib/htmlpurifier/HTMLPurifier/URIDefinition.php index 390d97ec157..6977338ae10 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIDefinition.php +++ b/lib/htmlpurifier/HTMLPurifier/URIDefinition.php @@ -5,6 +5,7 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition public $type = 'URI'; protected $filters = array(); + protected $postFilters = array(); protected $registeredFilters = array(); /** @@ -27,6 +28,7 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources()); $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist()); $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute()); + $this->registerFilter(new HTMLPurifier_URIFilter_Munge()); } public function registerFilter($filter) { @@ -34,8 +36,13 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition } public function addFilter($filter, $config) { - $filter->prepare($config); - $this->filters[$filter->name] = $filter; + $r = $filter->prepare($config); + if ($r === false) return; // null is ok, for backwards compat + if ($filter->post) { + $this->postFilters[$filter->name] = $filter; + } else { + $this->filters[$filter->name] = $filter; + } } protected function doSetup($config) { @@ -66,8 +73,16 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition } public function filter(&$uri, $config, $context) { - foreach ($this->filters as $name => $x) { - $result = $this->filters[$name]->filter($uri, $config, $context); + foreach ($this->filters as $name => $f) { + $result = $f->filter($uri, $config, $context); + if (!$result) return false; + } + return true; + } + + public function postFilter(&$uri, $config, $context) { + foreach ($this->postFilters as $name => $f) { + $result = $f->filter($uri, $config, $context); if (!$result) return false; } return true; diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter.php b/lib/htmlpurifier/HTMLPurifier/URIFilter.php index 0108258f7e7..6953ed43da5 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIFilter.php +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter.php @@ -19,10 +19,15 @@ abstract class HTMLPurifier_URIFilter */ public $name; + /** + * True if this filter should be run after scheme validation. + */ + public $post = false; + /** * Performs initialization for the filter */ - public function prepare($config) {} + public function prepare($config) {return true;} /** * Filter a URI object diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php index bd719701034..01320479622 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php @@ -6,6 +6,7 @@ class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter protected $blacklist = array(); public function prepare($config) { $this->blacklist = $config->get('URI', 'HostBlacklist'); + return true; } public function filter(&$uri, $config, $context) { foreach($this->blacklist as $blacklisted_host_fragment) { diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php index 647f779e9ae..289db51ad3a 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php @@ -11,14 +11,15 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter $def = $config->getDefinition('URI'); $this->base = $def->base; if (is_null($this->base)) { - trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_ERROR); - return; + trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING); + return false; } $this->base->fragment = null; // fragment is invalid for base URI $stack = explode('/', $this->base->path); array_pop($stack); // discard last segment $stack = $this->_collapseStack($stack); // do pre-parsing $this->basePathStack = $stack; + return true; } public function filter(&$uri, $config, $context) { if (is_null($this->base)) return true; // abort early diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php new file mode 100644 index 00000000000..ad6578e669b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php @@ -0,0 +1,48 @@ +target = $config->get('URI', $this->name); + $this->parser = new HTMLPurifier_URIParser(); + $this->doEmbed = $config->get('URI', 'MungeResources'); + $this->secretKey = $config->get('URI', 'MungeSecretKey'); + return true; + } + public function filter(&$uri, $config, $context) { + if ($context->get('EmbeddedURI', true) && !$this->doEmbed) return true; + + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) return true; // ignore unknown schemes, maybe another postfilter did it + if (is_null($uri->host) || empty($scheme_obj->browsable)) { + return true; + } + + $this->makeReplace($uri, $config, $context); + $this->replace = array_map('rawurlencode', $this->replace); + + $new_uri = strtr($this->target, $this->replace); + $uri = $this->parser->parse($new_uri); // overwrite + return true; + } + + protected function makeReplace($uri, $config, $context) { + $string = $uri->toString(); + // always available + $this->replace['%s'] = $string; + $this->replace['%r'] = $context->get('EmbeddedURI', true); + $token = $context->get('CurrentToken', true); + $this->replace['%n'] = $token ? $token->name : null; + $this->replace['%m'] = $context->get('CurrentAttr', true); + $this->replace['%p'] = $context->get('CurrentCSSProperty', true); + // not always available + if ($this->secretKey) $this->replace['%t'] = sha1($this->secretKey . ':' . $string); + } + +} diff --git a/lib/htmlpurifier/HTMLPurifier/UnitConverter.php b/lib/htmlpurifier/HTMLPurifier/UnitConverter.php new file mode 100644 index 00000000000..6e3046102ba --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/UnitConverter.php @@ -0,0 +1,240 @@ + array( + 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary + 'pt' => 4, + 'pc' => 48, + 'in' => 288, + self::METRIC => array('pt', '0.352777778', 'mm'), + ), + self::METRIC => array( + 'mm' => 1, + 'cm' => 10, + self::ENGLISH => array('mm', '2.83464567', 'pt'), + ), + ); + + /** + * Minimum bcmath precision for output. + */ + protected $outputPrecision; + + /** + * Bcmath precision for internal calculations. + */ + protected $internalPrecision; + + /** + * Whether or not BCMath is available + */ + private $bcmath; + + public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) { + $this->outputPrecision = $output_precision; + $this->internalPrecision = $internal_precision; + $this->bcmath = !$force_no_bcmath && function_exists('bcmul'); + } + + /** + * Converts a length object of one unit into another unit. + * @param HTMLPurifier_Length $length + * Instance of HTMLPurifier_Length to convert. You must validate() + * it before passing it here! + * @param string $to_unit + * Unit to convert to. + * @note + * About precision: This conversion function pays very special + * attention to the incoming precision of values and attempts + * to maintain a number of significant figure. Results are + * fairly accurate up to nine digits. Some caveats: + * - If a number is zero-padded as a result of this significant + * figure tracking, the zeroes will be eliminated. + * - If a number contains less than four sigfigs ($outputPrecision) + * and this causes some decimals to be excluded, those + * decimals will be added on. + */ + public function convert($length, $to_unit) { + + if (!$length->isValid()) return false; + + $n = $length->getN(); + $unit = $length->getUnit(); + + if ($n === '0' || $unit === false) { + return new HTMLPurifier_Length('0', false); + } + + $state = $dest_state = false; + foreach (self::$units as $k => $x) { + if (isset($x[$unit])) $state = $k; + if (isset($x[$to_unit])) $dest_state = $k; + } + if (!$state || !$dest_state) return false; + + // Some calculations about the initial precision of the number; + // this will be useful when we need to do final rounding. + $sigfigs = $this->getSigFigs($n); + if ($sigfigs < $this->outputPrecision) $sigfigs = $this->outputPrecision; + + // BCMath's internal precision deals only with decimals. Use + // our default if the initial number has no decimals, or increase + // it by how ever many decimals, thus, the number of guard digits + // will always be greater than or equal to internalPrecision. + $log = (int) floor(log(abs($n), 10)); + $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision + + for ($i = 0; $i < 2; $i++) { + + // Determine what unit IN THIS SYSTEM we need to convert to + if ($dest_state === $state) { + // Simple conversion + $dest_unit = $to_unit; + } else { + // Convert to the smallest unit, pending a system shift + $dest_unit = self::$units[$state][$dest_state][0]; + } + + // Do the conversion if necessary + if ($dest_unit !== $unit) { + $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp); + $n = $this->mul($n, $factor, $cp); + $unit = $dest_unit; + } + + // Output was zero, so bail out early. Shouldn't ever happen. + if ($n === '') { + $n = '0'; + $unit = $to_unit; + break; + } + + // It was a simple conversion, so bail out + if ($dest_state === $state) { + break; + } + + if ($i !== 0) { + // Conversion failed! Apparently, the system we forwarded + // to didn't have this unit. This should never happen! + return false; + } + + // Pre-condition: $i == 0 + + // Perform conversion to next system of units + $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp); + $unit = self::$units[$state][$dest_state][2]; + $state = $dest_state; + + // One more loop around to convert the unit in the new system. + + } + + // Post-condition: $unit == $to_unit + if ($unit !== $to_unit) return false; + + // Useful for debugging: + //echo "n"; + //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n\n"; + + $n = $this->round($n, $sigfigs); + if (strpos($n, '.') !== false) $n = rtrim($n, '0'); + $n = rtrim($n, '.'); + + return new HTMLPurifier_Length($n, $unit); + } + + /** + * Returns the number of significant figures in a string number. + * @param string $n Decimal number + * @return int number of sigfigs + */ + public function getSigFigs($n) { + $n = ltrim($n, '0+-'); + $dp = strpos($n, '.'); // decimal position + if ($dp === false) { + $sigfigs = strlen(rtrim($n, '0')); + } else { + $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character + if ($dp !== 0) $sigfigs--; + } + return $sigfigs; + } + + /** + * Adds two numbers, using arbitrary precision when available. + */ + private function add($s1, $s2, $scale) { + if ($this->bcmath) return bcadd($s1, $s2, $scale); + else return $this->scale($s1 + $s2, $scale); + } + + /** + * Multiples two numbers, using arbitrary precision when available. + */ + private function mul($s1, $s2, $scale) { + if ($this->bcmath) return bcmul($s1, $s2, $scale); + else return $this->scale($s1 * $s2, $scale); + } + + /** + * Divides two numbers, using arbitrary precision when available. + */ + private function div($s1, $s2, $scale) { + if ($this->bcmath) return bcdiv($s1, $s2, $scale); + else return $this->scale($s1 / $s2, $scale); + } + + /** + * Rounds a number according to the number of sigfigs it should have, + * using arbitrary precision when available. + */ + private function round($n, $sigfigs) { + $new_log = (int) floor(log(abs($n), 10)); // Number of digits left of decimal - 1 + $rp = $sigfigs - $new_log - 1; // Number of decimal places needed + $neg = $n < 0 ? '-' : ''; // Negative sign + if ($this->bcmath) { + if ($rp >= 0) { + $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1); + $n = bcdiv($n, '1', $rp); + } else { + // This algorithm partially depends on the standardized + // form of numbers that comes out of bcmath. + $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0); + $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1); + } + return $n; + } else { + return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1); + } + } + + /** + * Scales a float to $scale digits right of decimal point, like BCMath. + */ + private function scale($r, $scale) { + return sprintf('%.' . $scale . 'f', (float) $r); + } + +} diff --git a/lib/htmlpurifier/HTMLPurifier/VarParser.php b/lib/htmlpurifier/HTMLPurifier/VarParser.php index 418622be556..f9dfd84a76d 100644 --- a/lib/htmlpurifier/HTMLPurifier/VarParser.php +++ b/lib/htmlpurifier/HTMLPurifier/VarParser.php @@ -7,21 +7,34 @@ class HTMLPurifier_VarParser { + const STRING = 1; + const ISTRING = 2; + const TEXT = 3; + const ITEXT = 4; + const INT = 5; + const FLOAT = 6; + const BOOL = 7; + const LOOKUP = 8; + const ALIST = 9; + const HASH = 10; + const MIXED = 11; + /** - * Lookup table of allowed types. + * Lookup table of allowed types. Mainly for backwards compatibility, but + * also convenient for transforming string type names to the integer constants. */ static public $types = array( - 'string' => true, - 'istring' => true, - 'text' => true, - 'itext' => true, - 'int' => true, - 'float' => true, - 'bool' => true, - 'lookup' => true, - 'list' => true, - 'hash' => true, - 'mixed' => true + 'string' => self::STRING, + 'istring' => self::ISTRING, + 'text' => self::TEXT, + 'itext' => self::ITEXT, + 'int' => self::INT, + 'float' => self::FLOAT, + 'bool' => self::BOOL, + 'lookup' => self::LOOKUP, + 'list' => self::ALIST, + 'hash' => self::HASH, + 'mixed' => self::MIXED ); /** @@ -29,10 +42,10 @@ class HTMLPurifier_VarParser * allowed value lists. */ static public $stringTypes = array( - 'string' => true, - 'istring' => true, - 'text' => true, - 'itext' => true, + self::STRING => true, + self::ISTRING => true, + self::TEXT => true, + self::ITEXT => true, ); /** @@ -46,42 +59,46 @@ class HTMLPurifier_VarParser * @return Validated and type-coerced variable */ final public function parse($var, $type, $allow_null = false) { - if (!isset(HTMLPurifier_VarParser::$types[$type])) { - throw new HTMLPurifier_VarParserException("Invalid type '$type'"); + if (is_string($type)) { + if (!isset(HTMLPurifier_VarParser::$types[$type])) { + throw new HTMLPurifier_VarParserException("Invalid type '$type'"); + } else { + $type = HTMLPurifier_VarParser::$types[$type]; + } } $var = $this->parseImplementation($var, $type, $allow_null); if ($allow_null && $var === null) return null; // These are basic checks, to make sure nothing horribly wrong // happened in our implementations. switch ($type) { - case 'string': - case 'istring': - case 'text': - case 'itext': + case (self::STRING): + case (self::ISTRING): + case (self::TEXT): + case (self::ITEXT): if (!is_string($var)) break; - if ($type[0] == 'i') $var = strtolower($var); + if ($type == self::ISTRING || $type == self::ITEXT) $var = strtolower($var); return $var; - case 'int': + case (self::INT): if (!is_int($var)) break; return $var; - case 'float': + case (self::FLOAT): if (!is_float($var)) break; return $var; - case 'bool': + case (self::BOOL): if (!is_bool($var)) break; return $var; - case 'lookup': - case 'list': - case 'hash': + case (self::LOOKUP): + case (self::ALIST): + case (self::HASH): if (!is_array($var)) break; - if ($type === 'lookup') { + if ($type === self::LOOKUP) { foreach ($var as $k) if ($k !== true) $this->error('Lookup table contains value other than true'); - } elseif ($type === 'list') { + } elseif ($type === self::ALIST) { $keys = array_keys($var); if (array_keys($keys) !== $keys) $this->error('Indices for list are not uniform'); } return $var; - case 'mixed': + case (self::MIXED): return $var; default: $this->errorInconsistent(get_class($this), $type); @@ -111,7 +128,7 @@ class HTMLPurifier_VarParser * updating subclasses. */ protected function errorInconsistent($class, $type) { - throw new HTMLPurifier_Exception("Inconsistency in $class: $type not implemented"); + throw new HTMLPurifier_Exception("Inconsistency in $class: ".HTMLPurifier_VarParser::getTypeName($type)." not implemented"); } /** @@ -119,7 +136,17 @@ class HTMLPurifier_VarParser */ protected function errorGeneric($var, $type) { $vtype = gettype($var); - $this->error("Expected type $type, got $vtype"); + $this->error("Expected type ".HTMLPurifier_VarParser::getTypeName($type).", got $vtype"); + } + + static public function getTypeName($type) { + static $lookup; + if (!$lookup) { + // Lazy load the alternative lookup table + $lookup = array_flip(HTMLPurifier_VarParser::$types); + } + if (!isset($lookup[$type])) return 'unknown'; + return $lookup[$type]; } } diff --git a/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php b/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php index a2041fdd34d..ec2c90f4504 100644 --- a/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php +++ b/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php @@ -14,19 +14,19 @@ class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser // Note: if code "breaks" from the switch, it triggers a generic // exception to be thrown. Specific errors can be specifically // done here. - case 'mixed': - case 'istring': - case 'string': - case 'text': - case 'itext': + case self::MIXED : + case self::ISTRING : + case self::STRING : + case self::TEXT : + case self::ITEXT : return $var; - case 'int': + case self::INT : if (is_string($var) && ctype_digit($var)) $var = (int) $var; return $var; - case 'float': + case self::FLOAT : if ((is_string($var) && is_numeric($var)) || is_int($var)) $var = (float) $var; return $var; - case 'bool': + case self::BOOL : if (is_int($var) && ($var === 0 || $var === 1)) { $var = (bool) $var; } elseif (is_string($var)) { @@ -39,9 +39,9 @@ class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser } } return $var; - case 'list': - case 'hash': - case 'lookup': + case self::ALIST : + case self::HASH : + case self::LOOKUP : if (is_string($var)) { // special case: technically, this is an array with // a single empty string item, but having an empty @@ -56,7 +56,7 @@ class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser } // remove spaces foreach ($var as $i => $j) $var[$i] = trim($j); - if ($type === 'hash') { + if ($type === self::HASH) { // key:value,key2:value2 $nvar = array(); foreach ($var as $keypair) { @@ -70,8 +70,8 @@ class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser if (!is_array($var)) break; $keys = array_keys($var); if ($keys === array_keys($keys)) { - if ($type == 'list') return $var; - elseif ($type == 'lookup') { + if ($type == self::ALIST) return $var; + elseif ($type == self::LOOKUP) { $new = array(); foreach ($var as $key) { $new[$key] = true; @@ -79,7 +79,7 @@ class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser return $new; } else break; } - if ($type === 'lookup') { + if ($type === self::LOOKUP) { foreach ($var as $key => $value) { $var[$key] = true; } diff --git a/lib/htmlpurifier/readme_moodle.txt b/lib/htmlpurifier/readme_moodle.txt index b6bf56c3806..99e67dbc4f0 100644 --- a/lib/htmlpurifier/readme_moodle.txt +++ b/lib/htmlpurifier/readme_moodle.txt @@ -1,12 +1,10 @@ -Description of HTML Purifier v3.1.0 library import into Moodle +Description of HTML Purifier v3.1.1 library import into Moodle Changes: * HMLTModule/Text.php - added