diff --git a/lib/htmlpurifier/CREDITS b/lib/htmlpurifier/CREDITS index c3e7bb8e2f8..7921b45af7d 100644 --- a/lib/htmlpurifier/CREDITS +++ b/lib/htmlpurifier/CREDITS @@ -5,3 +5,5 @@ Almost everything written by Edward Z. Yang (Ambush Commander). Lots of thanks to the DevNetwork Community for their help (see docs/ref-devnetwork.html for more details), Feyd especially (namely IPv6 and optimization). Thanks to RSnake for letting me package his fantastic XSS cheatsheet for a smoketest. + + vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier.auto.php b/lib/htmlpurifier/HTMLPurifier.auto.php deleted file mode 100644 index cb6a84265dd..00000000000 --- a/lib/htmlpurifier/HTMLPurifier.auto.php +++ /dev/null @@ -1,9 +0,0 @@ -purify($html, $config); -} - diff --git a/lib/htmlpurifier/HTMLPurifier.php b/lib/htmlpurifier/HTMLPurifier.php index 17cb1f80b81..fff46788621 100644 --- a/lib/htmlpurifier/HTMLPurifier.php +++ b/lib/htmlpurifier/HTMLPurifier.php @@ -1,12 +1,11 @@ Warning: -Currently this feature is very patchy and experimental, with lots of -possible error messages not yet implemented. It will not cause any problems, -but it may not help your users either. This directive has been available -since 2.0.0. -'); - /** * Facade that coordinates HTML Purifier's subsystems in order to purify HTML. - * - * @note There are several points in which configuration can be specified + * + * @note There are several points in which configuration can be specified * for HTML Purifier. The precedence of these (from lowest to * highest) is as follows: * -# Instance: new HTMLPurifier($config) * -# Invocation: purify($html, $config) * These configurations are entirely independent of each other and - * are *not* merged. - * - * @todo We need an easier way to inject strategies, it'll probably end - * up getting done through config though. + * are *not* merged (this behavior may change in the future). + * + * @todo We need an easier way to inject strategies using the configuration + * object. */ class HTMLPurifier { - - var $version = '2.1.5'; - - var $config; - var $filters = array(); - - var $strategy, $generator; - + + /** Version of HTML Purifier */ + public $version = '4.1.0'; + + /** Constant with version of HTML Purifier */ + const VERSION = '4.1.0'; + + /** Global configuration object */ + public $config; + + /** Array of extra HTMLPurifier_Filter objects to run on HTML, for backwards compatibility */ + private $filters = array(); + + /** Single instance of HTML Purifier */ + private static $instance; + + protected $strategy, $generator; + /** * Resultant HTMLPurifier_Context of last run purification. Is an array * of contexts if the last called method was purifyArray(). - * @public */ - var $context; - + public $context; + /** * Initializes the purifier. * @param $config Optional HTMLPurifier_Config object for all instances of @@ -105,26 +85,26 @@ class HTMLPurifier * The parameter can also be any type that * HTMLPurifier_Config::create() supports. */ - function HTMLPurifier($config = null) { - + public function __construct($config = null) { + $this->config = HTMLPurifier_Config::create($config); - + $this->strategy = new HTMLPurifier_Strategy_Core(); - $this->generator = new HTMLPurifier_Generator(); - + } - + /** * Adds a filter to process the output. First come first serve * @param $filter HTMLPurifier_Filter object */ - function addFilter($filter) { + public function addFilter($filter) { + trigger_error('HTMLPurifier->addFilter() is deprecated, use configuration directives in the Filter namespace or Filter.Custom', E_USER_WARNING); $this->filters[] = $filter; } - + /** * Filters an HTML snippet/document to be XSS-free and standards-compliant. - * + * * @param $html String of HTML to purify * @param $config HTMLPurifier_Config object for this operation, if omitted, * defaults to the config object specified during this @@ -132,44 +112,63 @@ class HTMLPurifier * that HTMLPurifier_Config::create() supports. * @return Purified HTML */ - function purify($html, $config = null) { - + public function purify($html, $config = null) { + + // :TODO: make the config merge in, instead of replace $config = $config ? HTMLPurifier_Config::create($config) : $this->config; - + // implementation is partially environment dependant, partially // configuration dependant $lexer = HTMLPurifier_Lexer::create($config); - + $context = new HTMLPurifier_Context(); - - // our friendly neighborhood generator, all primed with configuration too! - $this->generator->generateFromTokens(array(), $config, $context); + + // setup HTML generator + $this->generator = new HTMLPurifier_Generator($config, $context); $context->register('Generator', $this->generator); - + // set up global context variables - if ($config->get('Core', 'CollectErrors')) { + if ($config->get('Core.CollectErrors')) { // may get moved out if other facilities use it $language_factory = HTMLPurifier_LanguageFactory::instance(); $language = $language_factory->create($config, $context); $context->register('Locale', $language); - + $error_collector = new HTMLPurifier_ErrorCollector($context); $context->register('ErrorCollector', $error_collector); } - + // setup id_accumulator context, necessary due to the fact that // AttrValidator can be called from many places $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); $context->register('IDAccumulator', $id_accumulator); - + $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); - - for ($i = 0, $size = count($this->filters); $i < $size; $i++) { - $html = $this->filters[$i]->preFilter($html, $config, $context); + + // setup filters + $filter_flags = $config->getBatch('Filter'); + $custom_filters = $filter_flags['Custom']; + unset($filter_flags['Custom']); + $filters = array(); + foreach ($filter_flags as $filter => $flag) { + if (!$flag) continue; + if (strpos($filter, '.') !== false) continue; + $class = "HTMLPurifier_Filter_$filter"; + $filters[] = new $class; } - + foreach ($custom_filters as $filter) { + // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat + $filters[] = $filter; + } + $filters = array_merge($filters, $this->filters); + // maybe prepare(), but later + + for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) { + $html = $filters[$i]->preFilter($html, $config, $context); + } + // purified HTML - $html = + $html = $this->generator->generateFromTokens( // list of tokens $this->strategy->execute( @@ -179,26 +178,25 @@ class HTMLPurifier $html, $config, $context ), $config, $context - ), - $config, $context + ) ); - - for ($i = $size - 1; $i >= 0; $i--) { - $html = $this->filters[$i]->postFilter($html, $config, $context); + + for ($i = $filter_size - 1; $i >= 0; $i--) { + $html = $filters[$i]->postFilter($html, $config, $context); } - + $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context); $this->context =& $context; return $html; } - + /** * Filters an array of HTML snippets * @param $config Optional HTMLPurifier_Config object for this operation. * See HTMLPurifier::purify() for more details. * @return Array of purified HTML */ - function purifyArray($array_of_html, $config = null) { + public function purifyArray($array_of_html, $config = null) { $context_array = array(); foreach ($array_of_html as $key => $html) { $array_of_html[$key] = $this->purify($html, $config); @@ -207,29 +205,33 @@ class HTMLPurifier $this->context = $context_array; return $array_of_html; } - + /** * Singleton for enforcing just one HTML Purifier in your system * @param $prototype Optional prototype HTMLPurifier instance to - * overload singleton with. + * overload singleton with, or HTMLPurifier_Config + * instance to configure the generated version with. */ - function &instance($prototype = null) { - static $htmlpurifier; - if (!$htmlpurifier || $prototype) { - if (is_a($prototype, 'HTMLPurifier')) { - $htmlpurifier = $prototype; + public static function instance($prototype = null) { + if (!self::$instance || $prototype) { + if ($prototype instanceof HTMLPurifier) { + self::$instance = $prototype; } elseif ($prototype) { - $htmlpurifier = new HTMLPurifier($prototype); + self::$instance = new HTMLPurifier($prototype); } else { - $htmlpurifier = new HTMLPurifier(); + self::$instance = new HTMLPurifier(); } } - return $htmlpurifier; + return self::$instance; } - - function &getInstance($prototype = null) { + + /** + * @note Backwards compatibility, see instance() + */ + public static function getInstance($prototype = null) { return HTMLPurifier::instance($prototype); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier.safe-includes.php b/lib/htmlpurifier/HTMLPurifier.safe-includes.php new file mode 100644 index 00000000000..6402de04584 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier.safe-includes.php @@ -0,0 +1,204 @@ +attr_collections as $coll_i => $coll) { @@ -47,13 +45,13 @@ class HTMLPurifier_AttrCollections $this->expandIdentifiers($this->info[$name], $attr_types); } } - + /** * Takes a reference to an attribute associative array and performs * all inclusions specified by the zero index. * @param &$attr Reference to attribute array */ - function performInclusions(&$attr) { + public function performInclusions(&$attr) { if (!isset($attr[0])) return; $merge = $attr[0]; $seen = array(); // recursion guard @@ -74,25 +72,25 @@ class HTMLPurifier_AttrCollections } unset($attr[0]); } - + /** * Expands all string identifiers in an attribute array by replacing * them with the appropriate values inside HTMLPurifier_AttrTypes * @param &$attr Reference to attribute array * @param $attr_types HTMLPurifier_AttrTypes instance */ - function expandIdentifiers(&$attr, $attr_types) { - + public function expandIdentifiers(&$attr, $attr_types) { + // because foreach will process new elements we add, make sure we // skip duplicates $processed = array(); - + foreach ($attr as $def_i => $def) { // skip inclusions if ($def_i === 0) continue; - + if (isset($processed[$def_i])) continue; - + // determine whether or not attribute is required if ($required = (strpos($def_i, '*') !== false)) { // rename the definition @@ -100,21 +98,21 @@ class HTMLPurifier_AttrCollections $def_i = trim($def_i, '*'); $attr[$def_i] = $def; } - + $processed[$def_i] = true; - + // if we've already got a literal object, move on if (is_object($def)) { // preserve previous required $attr[$def_i]->required = ($required || $attr[$def_i]->required); continue; } - + if ($def === false) { unset($attr[$def_i]); continue; } - + if ($t = $attr_types->get($def)) { $attr[$def_i] = $t; $attr[$def_i]->required = $required; @@ -122,8 +120,9 @@ class HTMLPurifier_AttrCollections unset($attr[$def_i]); } } - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef.php b/lib/htmlpurifier/HTMLPurifier/AttrDef.php index 5417270e647..d32fa62d6ad 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef.php @@ -2,90 +2,86 @@ /** * Base class for all validating attribute definitions. - * + * * This family of classes forms the core for not only HTML attribute validation, * but also any sort of string that needs to be validated or cleaned (which - * means CSS properties and composite definitions are defined here too). + * means CSS properties and composite definitions are defined here too). * Besides defining (through code) what precisely makes the string valid, * subclasses are also responsible for cleaning the code if possible. */ -class HTMLPurifier_AttrDef +abstract class HTMLPurifier_AttrDef { - + /** * Tells us whether or not an HTML attribute is minimized. Has no * meaning in other contexts. */ - var $minimized = false; - + public $minimized = false; + /** * Tells us whether or not an HTML attribute is required. Has no * meaning in other contexts */ - var $required = false; - + public $required = false; + /** * Validates and cleans passed string according to a definition. - * - * @public + * * @param $string String to be validated and cleaned. * @param $config Mandatory HTMLPurifier_Config object. * @param $context Mandatory HTMLPurifier_AttrContext object. */ - function validate($string, $config, &$context) { - trigger_error('Cannot call abstract function', E_USER_ERROR); - } - + abstract public function validate($string, $config, $context); + /** * Convenience method that parses a string as if it were CDATA. - * + * * This method process a string in the manner specified at * by removing * leading and trailing whitespace, ignoring line feeds, and replacing * carriage returns and tabs with spaces. While most useful for HTML * attributes specified as CDATA, it can also be applied to most CSS * values. - * + * * @note This method is not entirely standards compliant, as trim() removes * more types of whitespace than specified in the spec. In practice, * this is rarely a problem, as those extra characters usually have * already been removed by HTMLPurifier_Encoder. - * + * * @warning This processing is inconsistent with XML's whitespace handling * as specified by section 3.3.3 and referenced XHTML 1.0 section * 4.7. However, note that we are NOT necessarily * parsing XML, thus, this behavior may still be correct. We * assume that newlines have been normalized. - * - * @public */ - function parseCDATA($string) { + public function parseCDATA($string) { $string = trim($string); $string = str_replace(array("\n", "\t", "\r"), ' ', $string); return $string; } - + /** * Factory method for creating this class from a string. * @param $string String construction info * @return Created AttrDef object corresponding to $string - * @public */ - function make($string) { - // default implementation, return flyweight of this object - // if overloaded, it is *necessary* for you to clone the - // object (usually by instantiating a new copy) and return that + public function make($string) { + // default implementation, return a flyweight of this object. + // If $string has an effect on the returned object (i.e. you + // need to overload this method), it is best + // to clone or instantiate new copies. (Instantiation is safer.) return $this; } - + /** * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work * properly. THIS IS A HACK! */ - function mungeRgb($string) { + protected function mungeRgb($string) { return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS.php index 71523be1f1d..953e7067555 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS.php @@ -1,8 +1,5 @@ parseCDATA($css); - + $definition = $config->getCSSDefinition(); - + // we're going to break the spec and explode by semicolons. // This is because semicolon rarely appears in escaped form // Doing this is generally flaky but fast // IT MIGHT APPEAR IN URIs, see HTMLPurifier_AttrDef_CSSURI // for details - + $declarations = explode(';', $css); $propvalues = array(); - + + /** + * Name of the current CSS property being validated. + */ + $property = false; + $context->register('CurrentCSSProperty', $property); + foreach ($declarations as $declaration) { if (!$declaration) continue; if (!strpos($declaration, ':')) continue; @@ -63,19 +66,22 @@ class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef if ($result === false) continue; $propvalues[$property] = $result; } - + + $context->destroy('CurrentCSSProperty'); + // procedure does not write the new CSS simultaneously, so it's // slightly inefficient, but it's the only way of getting rid of // duplicates. Perhaps config to optimize it, but not now. - + $new_declarations = ''; foreach ($propvalues as $prop => $value) { $new_declarations .= "$prop:$value;"; } - + return $new_declarations ? $new_declarations : false; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php new file mode 100644 index 00000000000..292c040d4bd --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php @@ -0,0 +1,21 @@ + 1.0) $result = '1'; + return $result; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Background.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Background.php index a5c1046a827..3a3d20cd6a7 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Background.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Background.php @@ -1,22 +1,19 @@ getCSSDefinition(); $this->info['background-color'] = $def->info['background-color']; $this->info['background-image'] = $def->info['background-image']; @@ -24,29 +21,29 @@ class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef $this->info['background-attachment'] = $def->info['background-attachment']; $this->info['background-position'] = $def->info['background-position']; } - - function validate($string, $config, &$context) { - + + public function validate($string, $config, $context) { + // regular pre-processing $string = $this->parseCDATA($string); if ($string === '') return false; - + // munge rgb() decl if necessary $string = $this->mungeRgb($string); - + // assumes URI doesn't have spaces in it $bits = explode(' ', strtolower($string)); // bits to process - + $caught = array(); $caught['color'] = false; $caught['image'] = false; $caught['repeat'] = false; $caught['attachment'] = false; $caught['position'] = false; - + $i = 0; // number of catches $none = false; - + foreach ($bits as $bit) { if ($bit === '') continue; foreach ($caught as $key => $status) { @@ -67,23 +64,24 @@ class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef break; } } - + if (!$i) return false; if ($caught['position'] !== false) { $caught['position'] = $this->info['background-position']-> validate($caught['position'], $config, $context); } - + $ret = array(); foreach ($caught as $value) { if ($value === false) continue; $ret[] = $value; } - + if (empty($ret)) return false; return implode(' ', $ret); - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php index 0d10ab681de..35df3985e23 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php @@ -1,9 +1,5 @@ | | left | center | right ] - [ + [ | | top | center | bottom ]? ] | @@ -32,10 +28,10 @@ require_once 'HTMLPurifier/AttrDef/CSS/Percentage.php'; /* QuirksMode says: keyword + length/percentage must be ordered correctly, as per W3C - + Internet Explorer and Opera, however, support arbitrary ordering. We should fix it up. - + Minor issue though, not strictly necessary. */ @@ -47,27 +43,27 @@ require_once 'HTMLPurifier/AttrDef/CSS/Percentage.php'; */ class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef { - - var $length; - var $percentage; - - function HTMLPurifier_AttrDef_CSS_BackgroundPosition() { + + protected $length; + protected $percentage; + + public function __construct() { $this->length = new HTMLPurifier_AttrDef_CSS_Length(); $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage(); } - - function validate($string, $config, &$context) { + + public function validate($string, $config, $context) { $string = $this->parseCDATA($string); $bits = explode(' ', $string); - + $keywords = array(); $keywords['h'] = false; // left, right $keywords['v'] = false; // top, bottom $keywords['c'] = false; // center $measures = array(); - + $i = 0; - + $lookup = array( 'top' => 'v', 'bottom' => 'v', @@ -75,10 +71,10 @@ class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef 'right' => 'h', 'center' => 'c' ); - + foreach ($bits as $bit) { if ($bit === '') continue; - + // test for keyword $lbit = ctype_lower($bit) ? $bit : strtolower($bit); if (isset($lookup[$lbit])) { @@ -86,28 +82,28 @@ class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef $keywords[$status] = $lbit; $i++; } - + // test for length $r = $this->length->validate($bit, $config, $context); if ($r !== false) { $measures[] = $r; $i++; } - + // test for percentage $r = $this->percentage->validate($bit, $config, $context); if ($r !== false) { $measures[] = $r; $i++; } - + } - + if (!$i) return false; // no valid values were caught - - + + $ret = array(); - + // first keyword if ($keywords['h']) $ret[] = $keywords['h']; elseif (count($measures)) $ret[] = array_shift($measures); @@ -115,15 +111,16 @@ class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef $ret[] = $keywords['c']; $keywords['c'] = false; // prevent re-use: center = center center } - + if ($keywords['v']) $ret[] = $keywords['v']; elseif (count($measures)) $ret[] = array_shift($measures); elseif ($keywords['c']) $ret[] = $keywords['c']; - + if (empty($ret)) return false; return implode(' ', $ret); - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Border.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Border.php index 4eb3e25abd5..42a1d1b4ae3 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Border.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Border.php @@ -1,26 +1,24 @@ getCSSDefinition(); $this->info['border-width'] = $def->info['border-width']; $this->info['border-style'] = $def->info['border-style']; $this->info['border-top-color'] = $def->info['border-top-color']; } - - function validate($string, $config, &$context) { + + public function validate($string, $config, $context) { $string = $this->parseCDATA($string); $string = $this->mungeRgb($string); $bits = explode(' ', $string); @@ -39,6 +37,7 @@ class HTMLPurifier_AttrDef_CSS_Border extends HTMLPurifier_AttrDef } return rtrim($ret); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Color.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Color.php index a6711f71755..07f95a67191 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Color.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Color.php @@ -1,49 +1,22 @@ '#800000', - 'red' => '#FF0000', - 'orange' => '#FFA500', - 'yellow' => '#FFFF00', - 'olive' => '#808000', - 'purple' => '#800080', - 'fuchsia' => '#FF00FF', - 'white' => '#FFFFFF', - 'lime' => '#00FF00', - 'green' => '#008000', - 'navy' => '#000080', - 'blue' => '#0000FF', - 'aqua' => '#00FFFF', - 'teal' => '#008080', - 'black' => '#000000', - 'silver' => '#C0C0C0', - 'gray' => '#808080' - ), 'hash', ' -Lookup array of color names to six digit hexadecimal number corresponding -to color, with preceding hash mark. Used when parsing colors. -This directive has been available since 2.0.0. -'); - /** * Validates Color as defined by CSS. */ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef { - - function validate($color, $config, &$context) { - + + public function validate($color, $config, $context) { + static $colors = null; - if ($colors === null) $colors = $config->get('Core', 'ColorKeywords'); - + if ($colors === null) $colors = $config->get('Core.ColorKeywords'); + $color = trim($color); if ($color === '') return false; - + $lower = strtolower($color); if (isset($colors[$lower])) return $colors[$lower]; - + if (strpos($color, 'rgb(') !== false) { // rgb literal handling $length = strlen($color); @@ -95,10 +68,11 @@ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef if ($length !== 3 && $length !== 6) return false; if (!ctype_xdigit($hex)) return false; } - + return $color; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Composite.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Composite.php index 44ad542153e..de1289cba8a 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Composite.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Composite.php @@ -2,7 +2,7 @@ /** * Allows multiple validators to attempt to validate attribute. - * + * * Composite is just what it sounds like: a composite of many validators. * This means that multiple HTMLPurifier_AttrDef objects will have a whack * at the string. If one of them passes, that's what is returned. This is @@ -11,27 +11,28 @@ */ class HTMLPurifier_AttrDef_CSS_Composite extends HTMLPurifier_AttrDef { - + /** * List of HTMLPurifier_AttrDef objects that may process strings - * @protected + * @todo Make protected */ - var $defs; - + public $defs; + /** * @param $defs List of HTMLPurifier_AttrDef objects */ - function HTMLPurifier_AttrDef_CSS_Composite($defs) { + public function __construct($defs) { $this->defs = $defs; } - - function validate($string, $config, &$context) { + + public function validate($string, $config, $context) { foreach ($this->defs as $i => $def) { $result = $this->defs[$i]->validate($string, $config, $context); if ($result !== false) return $result; } return false; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php index b0a6db9dee3..6599c5b2ddc 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php @@ -5,22 +5,24 @@ */ class HTMLPurifier_AttrDef_CSS_DenyElementDecorator extends HTMLPurifier_AttrDef { - var $def, $element; - + public $def, $element; + /** * @param $def Definition to wrap * @param $element Element to deny */ - function HTMLPurifier_AttrDef_CSS_DenyElementDecorator(&$def, $element) { - $this->def =& $def; + public function __construct($def, $element) { + $this->def = $def; $this->element = $element; } /** * Checks if CurrentToken is set and equal to $this->element */ - function validate($string, $config, $context) { + public function validate($string, $config, $context) { $token = $context->get('CurrentToken', true); if ($token && $token->name == $this->element) return false; return $this->def->validate($string, $config, $context); } } + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Filter.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Filter.php new file mode 100644 index 00000000000..147894b8619 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Filter.php @@ -0,0 +1,54 @@ +intValidator = new HTMLPurifier_AttrDef_Integer(); + } + + public function validate($value, $config, $context) { + $value = $this->parseCDATA($value); + if ($value === 'none') return $value; + // if we looped this we could support multiple filters + $function_length = strcspn($value, '('); + $function = trim(substr($value, 0, $function_length)); + if ($function !== 'alpha' && + $function !== 'Alpha' && + $function !== 'progid:DXImageTransform.Microsoft.Alpha' + ) return false; + $cursor = $function_length + 1; + $parameters_length = strcspn($value, ')', $cursor); + $parameters = substr($value, $cursor, $parameters_length); + $params = explode(',', $parameters); + $ret_params = array(); + $lookup = array(); + foreach ($params as $param) { + list($key, $value) = explode('=', $param); + $key = trim($key); + $value = trim($value); + if (isset($lookup[$key])) continue; + if ($key !== 'opacity') continue; + $value = $this->intValidator->validate($value, $config, $context); + if ($value === false) continue; + $int = (int) $value; + if ($int > 100) $value = '100'; + if ($int < 0) $value = '0'; + $ret_params[] = "$key=$value"; + $lookup[$key] = true; + } + $ret_parameters = implode(',', $ret_params); + $ret_function = "$function($ret_parameters)"; + return $ret_function; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Font.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Font.php index 6ce18efb809..699ee0b7012 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Font.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Font.php @@ -1,24 +1,22 @@ getCSSDefinition(); $this->info['font-style'] = $def->info['font-style']; $this->info['font-variant'] = $def->info['font-variant']; @@ -27,9 +25,9 @@ class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef $this->info['line-height'] = $def->info['line-height']; $this->info['font-family'] = $def->info['font-family']; } - - function validate($string, $config, &$context) { - + + public function validate($string, $config, $context) { + static $system_fonts = array( 'caption' => true, 'icon' => true, @@ -38,27 +36,27 @@ class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef 'small-caption' => true, 'status-bar' => true ); - + // regular pre-processing $string = $this->parseCDATA($string); if ($string === '') return false; - + // check if it's one of the keywords $lowercase_string = strtolower($string); if (isset($system_fonts[$lowercase_string])) { return $lowercase_string; } - + $bits = explode(' ', $string); // bits to process $stage = 0; // this indicates what we're looking for $caught = array(); // which stage 0 properties have we caught? $stage_1 = array('font-style', 'font-variant', 'font-weight'); $final = ''; // output - + for ($i = 0, $size = count($bits); $i < $size; $i++) { if ($bits[$i] === '') continue; switch ($stage) { - + // attempting to catch font-style, font-variant or font-weight case 0: foreach ($stage_1 as $validator_name) { @@ -74,7 +72,7 @@ class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef // all three caught, continue on if (count($caught) >= 3) $stage = 1; if ($r !== false) break; - + // attempting to catch font-size and perhaps line-height case 1: $found_slash = false; @@ -128,7 +126,7 @@ class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef break; } return false; - + // attempting to catch font-family case 2: $font_family = @@ -145,6 +143,7 @@ class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef } return false; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php index 7418368a174..705ac893d1b 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php @@ -1,16 +1,13 @@ true, 'sans-serif' => true, @@ -18,7 +15,7 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef 'fantasy' => true, 'cursive' => true ); - + // assume that no font names contain commas in them $fonts = explode(',', $string); $final = ''; @@ -37,7 +34,7 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef $quote = $font[0]; if ($font[$length - 1] !== $quote) continue; $font = substr($font, 1, $length - 2); - + $new_font = ''; for ($i = 0, $c = strlen($font); $i < $c; $i++) { if ($font[$i] === '\\') { @@ -65,19 +62,19 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef } $new_font .= $font[$i]; } - + $font = $new_font; } // $font is a pure representation of the font name - + if (ctype_alnum($font) && $font !== '') { // very simple font, allow it in unharmed $final .= $font . ', '; continue; } - + // complicated font, requires quoting - + // armor single quotes and new lines $font = str_replace("\\", "\\\\", $font); $font = str_replace("'", "\\'", $font); @@ -87,6 +84,7 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef if ($final === '') return false; return $final; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php new file mode 100644 index 00000000000..4e6b35e5a0d --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php @@ -0,0 +1,40 @@ +def = $def; + $this->allow = $allow; + } + /** + * Intercepts and removes !important if necessary + */ + public function validate($string, $config, $context) { + // test for ! and important tokens + $string = trim($string); + $is_important = false; + // :TODO: optimization: test directly for !important and ! important + if (strlen($string) >= 9 && substr($string, -9) === 'important') { + $temp = rtrim(substr($string, 0, -9)); + // use a temp, because we might want to restore important + if (strlen($temp) >= 1 && substr($temp, -1) === '!') { + $string = rtrim(substr($temp, 0, -1)); + $is_important = true; + } + } + $string = $this->def->validate($string, $config, $context); + if ($this->allow && $is_important) $string .= ' !important'; + return $string; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Length.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Length.php index 79ac5bfffb0..a07ec58135c 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Length.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Length.php @@ -1,36 +1,33 @@ min = $min !== null ? HTMLPurifier_Length::make($min) : null; $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null; } - - function validate($string, $config, $context) { + + public function validate($string, $config, $context) { $string = $this->parseCDATA($string); - + // Optimizations if ($string === '') return false; if ($string === '0') return '0'; if (strlen($string) === 1) return false; - + $length = HTMLPurifier_Length::make($string); if (!$length->isValid()) return false; - + if ($this->min) { $c = $length->compareTo($this->min); if ($c === false) return false; @@ -41,9 +38,10 @@ class HTMLPurifier_AttrDef_CSS_Length extends HTMLPurifier_AttrDef if ($c === false) return false; if ($c > 0) return false; } - + return $length->toString(); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php index a89d679274f..4406868c08b 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php @@ -1,44 +1,42 @@ getCSSDefinition(); $this->info['list-style-type'] = $def->info['list-style-type']; $this->info['list-style-position'] = $def->info['list-style-position']; $this->info['list-style-image'] = $def->info['list-style-image']; } - - function validate($string, $config, &$context) { - + + public function validate($string, $config, $context) { + // regular pre-processing $string = $this->parseCDATA($string); if ($string === '') return false; - + // assumes URI doesn't have spaces in it $bits = explode(' ', strtolower($string)); // bits to process - + $caught = array(); $caught['type'] = false; $caught['position'] = false; $caught['image'] = false; - + $i = 0; // number of catches $none = false; - + foreach ($bits as $bit) { if ($i >= 3) return; // optimization bit if ($bit === '') continue; @@ -56,24 +54,25 @@ class HTMLPurifier_AttrDef_CSS_ListStyle extends HTMLPurifier_AttrDef break; } } - + if (!$i) return false; - + $ret = array(); - + // construct type if ($caught['type']) $ret[] = $caught['type']; - + // construct image if ($caught['image']) $ret[] = $caught['image']; - + // construct position if ($caught['position']) $ret[] = $caught['position']; - + if (empty($ret)) return false; return implode(' ', $ret); - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Multiple.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Multiple.php index 9a818d108af..4d62a40d7fc 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Multiple.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Multiple.php @@ -1,41 +1,41 @@ single = $single; $this->max = $max; } - - function validate($string, $config, &$context) { + + public function validate($string, $config, $context) { $string = $this->parseCDATA($string); if ($string === '') return false; $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n @@ -52,6 +52,7 @@ class HTMLPurifier_AttrDef_CSS_Multiple extends HTMLPurifier_AttrDef if ($final === '') return false; return rtrim($final); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Number.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Number.php index 6fd11bb42b9..3f99e12ec26 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Number.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Number.php @@ -5,30 +5,30 @@ */ class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef { - + /** * Bool indicating whether or not only positive values allowed. */ - var $non_negative = false; - + protected $non_negative = false; + /** * @param $non_negative Bool indicating whether negatives are forbidden */ - function HTMLPurifier_AttrDef_CSS_Number($non_negative = false) { + public function __construct($non_negative = false) { $this->non_negative = $non_negative; } - + /** * @warning Some contexts do not pass $config, $context. These - * variables should not be used without checking HTMLPurifier_Length. - * This might not work properly in PHP4. + * variables should not be used without checking HTMLPurifier_Length */ - function validate($number, $config, &$context) { - + public function validate($number, $config, $context) { + $number = $this->parseCDATA($number); - + if ($number === '') return false; - + if ($number === '0') return '0'; + $sign = ''; switch ($number[0]) { case '-': @@ -37,29 +37,33 @@ class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef case '+': $number = substr($number, 1); } - + if (ctype_digit($number)) { $number = ltrim($number, '0'); return $number ? $sign . $number : '0'; } - if (!strpos($number, '.')) return false; - + + // Period is the only non-numeric character allowed + if (strpos($number, '.') === false) return false; + list($left, $right) = explode('.', $number, 2); - - if (!ctype_digit($left)) return false; - $left = ltrim($left, '0'); - + + if ($left === '' && $right === '') return false; + if ($left !== '' && !ctype_digit($left)) return false; + + $left = ltrim($left, '0'); $right = rtrim($right, '0'); - + if ($right === '') { return $left ? $sign . $left : '0'; } elseif (!ctype_digit($right)) { return false; } - + return $sign . $left . '.' . $right; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Percentage.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Percentage.php index 4625bde6d7e..c34b8fc3c39 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Percentage.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/Percentage.php @@ -1,42 +1,40 @@ number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative); } - - function validate($string, $config, &$context) { - + + public function validate($string, $config, $context) { + $string = $this->parseCDATA($string); - + if ($string === '') return false; $length = strlen($string); if ($length === 1) return false; if ($string[$length - 1] !== '%') return false; - + $number = substr($string, 0, $length - 1); $number = $this->number_def->validate($number, $config, $context); - + if ($number === false) return false; return "$number%"; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php index ab452780e96..772c922d80b 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php @@ -1,7 +1,5 @@ true, 'overline' => true, 'underline' => true, ); - + $string = strtolower($this->parseCDATA($string)); - + if ($string === 'none') return $string; - + $parts = explode(' ', $string); $final = ''; foreach ($parts as $part) { @@ -32,8 +30,9 @@ class HTMLPurifier_AttrDef_CSS_TextDecoration extends HTMLPurifier_AttrDef $final = rtrim($final); if ($final === '') return false; return $final; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/URI.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/URI.php index b71a8585727..54b7d63f12b 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/URI.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/CSS/URI.php @@ -1,7 +1,5 @@ parseCDATA($uri_string); if (strpos($uri_string, 'url(') !== 0) return false; $uri_string = substr($uri_string, 4); $new_length = strlen($uri_string) - 1; if ($uri_string[$new_length] != ')') return false; $uri = trim(substr($uri_string, 0, $new_length)); - + if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) { $quote = $uri[0]; $new_length = strlen($uri) - 1; if ($uri[$new_length] !== $quote) return false; $uri = substr($uri, 1, $new_length - 1); } - + $keys = array( '(', ')', ',', ' ', '"', "'"); $values = array('\\(', '\\)', '\\,', '\\ ', '\\"', "\\'"); $uri = str_replace($values, $keys, $uri); - + $result = parent::validate($uri, $config, $context); - + if ($result === false) return false; - + // escape necessary characters according to CSS spec // except for the comma, none of these should appear in the // URI at all $result = str_replace($keys, $values, $result); - - return "url($result)"; - + + return "url('$result')"; + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/Enum.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/Enum.php index 011adda8bd2..5d603ebcc67 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/Enum.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/Enum.php @@ -1,7 +1,5 @@ valid_values = array_flip($valid_values); $this->case_sensitive = $case_sensitive; } - - function validate($string, $config, &$context) { + + public function validate($string, $config, $context) { $string = trim($string); if (!$this->case_sensitive) { // we may want to do full case-insensitive libraries $string = ctype_lower($string) ? $string : strtolower($string); } $result = isset($this->valid_values[$string]); - + return $result ? $string : false; } - + /** * @param $string In form of comma-delimited list of case-insensitive * valid values. Example: "foo,bar,baz". Prepend "s:" to make * case sensitive */ - function make($string) { + public function make($string) { if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') { $string = substr($string, 2); $sensitive = true; @@ -60,6 +59,7 @@ class HTMLPurifier_AttrDef_Enum extends HTMLPurifier_AttrDef $values = explode(',', $string); return new HTMLPurifier_AttrDef_Enum($values, $sensitive); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php index ff6f0a8649c..e06987eb8db 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php @@ -1,29 +1,28 @@ name = $name;} - - function validate($string, $config, &$context) { + + protected $name; + public $minimized = true; + + public function __construct($name = false) {$this->name = $name;} + + public function validate($string, $config, $context) { if (empty($string)) return false; return $this->name; } - + /** * @param $string Name of attribute */ - function make($string) { + public function make($string) { return new HTMLPurifier_AttrDef_HTML_Bool($string); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Class.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Class.php new file mode 100644 index 00000000000..370068d9757 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Class.php @@ -0,0 +1,34 @@ +getDefinition('HTML')->doctype->name; + if ($name == "XHTML 1.1" || $name == "XHTML 2.0") { + return parent::split($string, $config, $context); + } else { + return preg_split('/\s+/', $string); + } + } + protected function filter($tokens, $config, $context) { + $allowed = $config->get('Attr.AllowedClasses'); + $forbidden = $config->get('Attr.ForbiddenClasses'); + $ret = array(); + foreach ($tokens as $token) { + if ( + ($allowed === null || isset($allowed[$token])) && + !isset($forbidden[$token]) && + // We need this O(n) check because of PHP's array + // implementation that casts -0 to 0. + !in_array($token, $ret, true) + ) { + $ret[] = $token; + } + } + return $ret; + } +} diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Color.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Color.php index d6fa6749fb0..d01e20454ea 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Color.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Color.php @@ -1,34 +1,32 @@ get('Core', 'ColorKeywords'); - + if ($colors === null) $colors = $config->get('Core.ColorKeywords'); + $string = trim($string); - + if (empty($string)) return false; if (isset($colors[$string])) return $colors[$string]; if ($string[0] === '#') $hex = substr($string, 1); else $hex = $string; - + $length = strlen($hex); if ($length !== 3 && $length !== 6) return false; if (!ctype_xdigit($hex)) return false; if ($length === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; - + return "#$hex"; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php index fdca8cb2ce2..ae6ea7c01d5 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php @@ -1,33 +1,21 @@ valid_values === false) $this->valid_values = $config->get('Attr', 'AllowedFrameTargets'); + + public $valid_values = false; // uninitialized value + protected $case_sensitive = false; + + public function __construct() {} + + public function validate($string, $config, $context) { + if ($this->valid_values === false) $this->valid_values = $config->get('Attr.AllowedFrameTargets'); return parent::validate($string, $config, $context); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/ID.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/ID.php index 641749cea0c..81d03762dea 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/ID.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/ID.php @@ -1,56 +1,5 @@ get('Attr', 'EnableID')) return false; - + + public function validate($id, $config, $context) { + + if (!$config->get('Attr.EnableID')) return false; + $id = trim($id); // trim it first - + if ($id === '') return false; - - $prefix = $config->get('Attr', 'IDPrefix'); + + $prefix = $config->get('Attr.IDPrefix'); if ($prefix !== '') { - $prefix .= $config->get('Attr', 'IDPrefixLocal'); + $prefix .= $config->get('Attr.IDPrefixLocal'); // prevent re-appending the prefix if (strpos($id, $prefix) !== 0) $id = $prefix . $id; - } elseif ($config->get('Attr', 'IDPrefixLocal') !== '') { + } elseif ($config->get('Attr.IDPrefixLocal') !== '') { trigger_error('%Attr.IDPrefixLocal cannot be used unless '. '%Attr.IDPrefix is set', E_USER_WARNING); } - + //if (!$this->ref) { $id_accumulator =& $context->get('IDAccumulator'); if (isset($id_accumulator->ids[$id])) return false; //} - + // we purposely avoid using regex, hopefully this is faster - + if (ctype_alpha($id)) { $result = true; } else { @@ -101,20 +50,21 @@ class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef ); $result = ($trim === ''); } - - $regexp = $config->get('Attr', 'IDBlacklistRegexp'); + + $regexp = $config->get('Attr.IDBlacklistRegexp'); if ($regexp && preg_match($regexp, $id)) { return false; } - + if (/*!$this->ref && */$result) $id_accumulator->add($id); - + // if no change was made to the ID, return the result // else, return the new id if stripping whitespace made it // valid, or return false. return $result ? $id : false; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Length.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Length.php index c4f98436ae7..a242f9c238e 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Length.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Length.php @@ -1,43 +1,41 @@ 100) return '100%'; - + return ((string) $points) . '%'; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php index 1122f0c8bd0..76d25ed088a 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php @@ -1,22 +1,5 @@ 'AllowedRel', 'rev' => 'AllowedRev' @@ -41,15 +24,15 @@ class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef } $this->name = $configLookup[$name]; } - - function validate($string, $config, &$context) { - - $allowed = $config->get('Attr', $this->name); + + public function validate($string, $config, $context) { + + $allowed = $config->get('Attr.' . $this->name); if (empty($allowed)) return false; - + $string = $this->parseCDATA($string); $parts = explode(' ', $string); - + // lookup to prevent duplicates $ret_lookup = array(); foreach ($parts as $part) { @@ -57,16 +40,14 @@ class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef if (!isset($allowed[$part])) continue; $ret_lookup[$part] = true; } - + if (empty($ret_lookup)) return false; - - $ret_array = array(); - foreach ($ret_lookup as $part => $bool) $ret_array[] = $part; - $string = implode(' ', $ret_array); - + $string = implode(' ', array_keys($ret_lookup)); + return $string; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php index 4c0c88c8ba3..c72fc76e4d9 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php @@ -1,43 +1,41 @@ split($string, $config, $context); + $tokens = $this->filter($tokens, $config, $context); + if (empty($tokens)) return false; + return implode(' ', $tokens); + + } + + /** + * Splits a space separated list of tokens into its constituent parts. + */ + protected function split($string, $config, $context) { // OPTIMIZABLE! // do the preg_match, capture all subpatterns for reformulation - + // we don't support U+00A1 and up codepoints or // escaping because I don't know how to do that with regexps // and plus it would complicate optimization efforts (you never // see that anyway). - $matches = array(); $pattern = '/(?:(?<=\s)|\A)'. // look behind for space or string start '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)'. '(?:(?=\s)|\z)/'; // look ahead for space or string end preg_match_all($pattern, $string, $matches); - - if (empty($matches[1])) return false; - - // reconstruct string - $new_string = ''; - foreach ($matches[1] as $token) { - $new_string .= $token . ' '; - } - $new_string = rtrim($new_string); - - return $new_string; - + return $matches[1]; } - + + /** + * Template method for removing certain tokens based on arbitrary criteria. + * @note If we wanted to be really functional, we'd do an array_filter + * with a callback. But... we're not. + */ + protected function filter($tokens, $config, $context) { + return $tokens; + } + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Pixels.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Pixels.php index ad20615141c..4cb2c1b8579 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Pixels.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Pixels.php @@ -1,21 +1,19 @@ max = $max; } - - function validate($string, $config, &$context) { - + + public function validate($string, $config, $context) { + $string = trim($string); if ($string === '0') return $string; if ($string === '') return false; @@ -25,25 +23,26 @@ class HTMLPurifier_AttrDef_HTML_Pixels extends HTMLPurifier_AttrDef } if (!is_numeric($string)) return false; $int = (int) $string; - + if ($int < 0) return '0'; - + // upper-bound value, extremely high values can // crash operating systems, see // WARNING, above link WILL crash you if you're using Windows - + if ($this->max !== null && $int > $this->max) return (string) $this->max; - + return (string) $int; - + } - - function make($string) { + + public function make($string) { if ($string === '') $max = null; else $max = (int) $string; $class = get_class($this); return new $class($max); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/Integer.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/Integer.php index 822ed677082..d59738d2a2b 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/Integer.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/Integer.php @@ -1,7 +1,5 @@ negative = $negative; $this->zero = $zero; $this->positive = $positive; } - - function validate($integer, $config, &$context) { - + + public function validate($integer, $config, $context) { + $integer = $this->parseCDATA($integer); if ($integer === '') return false; - + // we could possibly simply typecast it to integer, but there are // certain fringe cases that must not return an integer. - + // clip leading sign if ( $this->negative && $integer[0] === '-' ) { $digits = substr($integer, 1); @@ -57,18 +55,19 @@ class HTMLPurifier_AttrDef_Integer extends HTMLPurifier_AttrDef } else { $digits = $integer; } - + // test if it's numeric if (!ctype_digit($digits)) return false; - + // perform scope tests if (!$this->zero && $integer == 0) return false; if (!$this->positive && $integer > 0) return false; if (!$this->negative && $integer < 0) return false; - + return $integer; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/Lang.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/Lang.php index e9cdda7adf9..f70e7aa1bdd 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/Lang.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/Lang.php @@ -1,28 +1,26 @@ 8 || !ctype_alnum($subtags[1])) { return $new_string; } if (!ctype_lower($subtags[1])) $subtags[1] = strtolower($subtags[1]); - + $new_string .= '-' . $subtags[1]; if ($num_subtags == 2) return $new_string; - + // process all other subtags, index 2 and up for ($i = 2; $i < $num_subtags; $i++) { $length = strlen($subtags[$i]); @@ -69,10 +67,11 @@ class HTMLPurifier_AttrDef_Lang extends HTMLPurifier_AttrDef } $new_string .= '-' . $subtags[$i]; } - + return $new_string; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/Switch.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/Switch.php index 32e9501a31a..c9e3ed193e7 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/Switch.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/Switch.php @@ -5,22 +5,22 @@ */ class HTMLPurifier_AttrDef_Switch { - - var $tag; - var $withTag, $withoutTag; - + + protected $tag; + protected $withTag, $withoutTag; + /** * @param string $tag Tag name to switch upon * @param HTMLPurifier_AttrDef $with_tag Call if token matches tag * @param HTMLPurifier_AttrDef $without_tag Call if token doesn't match, or there is no token */ - function HTMLPurifier_AttrDef_Switch($tag, $with_tag, $without_tag) { + public function __construct($tag, $with_tag, $without_tag) { $this->tag = $tag; $this->withTag = $with_tag; $this->withoutTag = $without_tag; } - - function validate($string, $config, $context) { + + public function validate($string, $config, $context) { $token = $context->get('CurrentToken', true); if (!$token || $token->name !== $this->tag) { return $this->withoutTag->validate($string, $config, $context); @@ -28,5 +28,7 @@ class HTMLPurifier_AttrDef_Switch return $this->withTag->validate($string, $config, $context); } } - + } + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/Text.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/Text.php index c06346649b1..c6216cc531d 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/Text.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/Text.php @@ -1,16 +1,15 @@ parseCDATA($string); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI.php index 52b4193b982..01a6d83e952 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI.php @@ -1,140 +1,77 @@ - Munges all browsable (usually http, https and ftp) - absolute URI\'s 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). -

-

- Uses for this directive: -

-
    -
  • - 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. -
  • -
  • - Redirect users to a splash page telling them they are leaving your - website. While this is poor usability practice, it is often mandated - in corporate environments. -
  • -
-

- This directive has been available since 1.3.0. -

-'); - -// disabling directives - -HTMLPurifier_ConfigSchema::define( - 'URI', 'Disable', false, 'bool', ' -

- Disables all URIs in all forms. Not sure why you\'d want to do that - (after all, the Internet\'s founded on the notion of a hyperlink). - This directive has been available since 1.3.0. -

-'); -HTMLPurifier_ConfigSchema::defineAlias('Attr', 'DisableURI', 'URI', 'Disable'); - -HTMLPurifier_ConfigSchema::define( - 'URI', 'DisableResources', false, 'bool', ' -

- Disables embedding resources, essentially meaning no pictures. You can - still link to them though. See %URI.DisableExternalResources for why - this might be a good idea. This directive has been available since 1.3.0. -

-'); - /** * Validates a URI as defined by RFC 3986. * @note Scheme-specific mechanics deferred to HTMLPurifier_URIScheme */ class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef { - - var $parser; - var $embedsResource; - + + protected $parser; + protected $embedsResource; + /** * @param $embeds_resource_resource Does the URI here result in an extra HTTP request? */ - function HTMLPurifier_AttrDef_URI($embeds_resource = false) { + public function __construct($embeds_resource = false) { $this->parser = new HTMLPurifier_URIParser(); $this->embedsResource = (bool) $embeds_resource; } - - function validate($uri, $config, &$context) { - - if ($config->get('URI', 'Disable')) return false; - + + public function make($string) { + $embeds = (bool) $string; + return new HTMLPurifier_AttrDef_URI($embeds); + } + + public function validate($uri, $config, $context) { + + if ($config->get('URI.Disable')) return false; + $uri = $this->parseCDATA($uri); - + // parse the URI $uri = $this->parser->parse($uri); if ($uri === false) return false; - + // add embedded flag to context for validators - $context->register('EmbeddedURI', $this->embedsResource); - + $context->register('EmbeddedURI', $this->embedsResource); + $ok = false; do { - + // generic validation $result = $uri->validate($config, $context); if (!$result) break; - + // chained filtering - $uri_def =& $config->getDefinition('URI'); + $uri_def = $config->getDefinition('URI'); $result = $uri_def->filter($uri, $config, $context); if (!$result) break; - - // scheme-specific validation + + // scheme-specific validation $scheme_obj = $uri->getSchemeObj($config, $context); if (!$scheme_obj) break; if ($this->embedsResource && !$scheme_obj->browsable) break; $result = $scheme_obj->validate($uri, $config, $context); if (!$result) break; - + + // Post chained filtering + $result = $uri_def->postFilter($uri, $config, $context); + if (!$result) break; + // survived gauntlet $ok = true; - + } while (false); - + $context->destroy('EmbeddedURI'); if (!$ok) return false; - + // back to string - $result = $uri->toString(); - - // munge entire URI if necessary - if ( - !is_null($uri->host) && // indicator for authority - !empty($scheme_obj->browsable) && - !is_null($munge = $config->get('URI', 'Munge')) - ) { - $result = str_replace('%s', rawurlencode($result), $munge); - } - - return $result; - + return $uri->toString(); + } - + } - +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Email.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Email.php index ababd9eae07..bfee9d166c8 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Email.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Email.php @@ -1,18 +1,17 @@ " // that needs more percent encoding to be done if ($string == '') return false; @@ -17,6 +15,7 @@ class HTMLPurifier_AttrDef_URI_Email_SimpleCheck extends HTMLPurifier_AttrDef_UR $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string); return $result ? $string : false; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php index 4812ad1d3dc..2156c10c660 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php @@ -1,31 +1,27 @@ ipv4 = new HTMLPurifier_AttrDef_URI_IPv4(); $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6(); } - - function validate($string, $config, &$context) { + + public function validate($string, $config, $context) { $length = strlen($string); if ($string === '') return ''; if ($length > 1 && $string[0] === '[' && $string[$length-1] === ']') { @@ -35,17 +31,17 @@ class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef if ($valid === false) return false; return '['. $valid . ']'; } - + // need to do checks on unusual encodings too $ipv4 = $this->ipv4->validate($string, $config, $context); if ($ipv4 !== false) return $ipv4; - + // A regular domain name. - + // This breaks I18N domain names, but we don't have proper IRI support, - // so force users to insert Punycode. If there's complaining we'll + // so force users to insert Punycode. If there's complaining we'll // try to fix things into an international friendly form. - + // The productions describing this are: $a = '[a-z]'; // alpha $an = '[a-z0-9]'; // alphanum @@ -57,9 +53,10 @@ class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef // hostname = *( domainlabel "." ) toplabel [ "." ] $match = preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string); if (!$match) return false; - + return $string; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv4.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv4.php index 9a1af293bad..ec4cf591b82 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv4.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv4.php @@ -1,41 +1,39 @@ ip4) $this->_loadRegex(); - + if (preg_match('#^' . $this->ip4 . '$#s', $aIP)) { return $aIP; } - + return false; - + } - + /** * Lazy load function to prevent regex from being stuffed in * cache. */ - function _loadRegex() { + protected function _loadRegex() { $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255 $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})"; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv6.php b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv6.php index f48b803dd7f..9454e9be50e 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv6.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv6.php @@ -1,7 +1,5 @@ ip4) $this->_loadRegex(); - + $original = $aIP; - + $hex = '[0-9a-fA-F]'; $blk = '(?:' . $hex . '{1,4})'; $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128 - + // prefix check if (strpos($aIP, '/') !== false) { @@ -34,8 +32,8 @@ class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4 return false; } } - - // IPv4-compatiblity check + + // IPv4-compatiblity check if (preg_match('#(?<=:'.')' . $this->ip4 . '$#s', $aIP, $find)) { $aIP = substr($aIP, 0, 0-strlen($find[0])); @@ -44,7 +42,7 @@ class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4 $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3]; unset($find, $ip); } - + // compression check $aIP = explode('::', $aIP); $c = count($aIP); @@ -57,12 +55,12 @@ class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4 list($first, $second) = $aIP; $first = explode(':', $first); $second = explode(':', $second); - + if (count($first) + count($second) > 8) { return false; } - + while(count($first) < 8) { array_push($first, '0'); @@ -77,12 +75,12 @@ class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4 $aIP = explode(':', $aIP[0]); } $c = count($aIP); - + if ($c != 8) { return false; } - + // All the pieces should be 16-bit hex strings. Are they? foreach ($aIP as $piece) { @@ -91,10 +89,11 @@ class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4 return false; } } - + return $original; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform.php index ce69fcbe82c..e61d3e01b6d 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform.php @@ -2,56 +2,55 @@ /** * Processes an entire attribute array for corrections needing multiple values. - * + * * Occasionally, a certain attribute will need to be removed and popped onto * another value. Instead of creating a complex return syntax for * HTMLPurifier_AttrDef, we just pass the whole attribute array to a * specialized object and have that do the special work. That is the * family of HTMLPurifier_AttrTransform. - * + * * An attribute transformation can be assigned to run before or after * HTMLPurifier_AttrDef validation. See HTMLPurifier_HTMLDefinition for * more details. */ -class HTMLPurifier_AttrTransform +abstract class HTMLPurifier_AttrTransform { - + /** * Abstract: makes changes to the attributes dependent on multiple values. - * + * * @param $attr Assoc array of attributes, usually from * HTMLPurifier_Token_Tag::$attr * @param $config Mandatory HTMLPurifier_Config object. * @param $context Mandatory HTMLPurifier_Context object * @returns Processed attribute array. */ - function transform($attr, $config, &$context) { - trigger_error('Cannot call abstract function', E_USER_ERROR); - } - + abstract public function transform($attr, $config, $context); + /** * Prepends CSS properties to the style attribute, creating the * attribute if it doesn't exist. * @param $attr Attribute array to process (passed by reference) * @param $css CSS to prepend */ - function prependCSS(&$attr, $css) { + public function prependCSS(&$attr, $css) { $attr['style'] = isset($attr['style']) ? $attr['style'] : ''; $attr['style'] = $css . $attr['style']; } - + /** * Retrieves and removes an attribute * @param $attr Attribute array to process (passed by reference) * @param $key Key of attribute to confiscate */ - function confiscateAttr(&$attr, $key) { + public function confiscateAttr(&$attr, $key) { if (!isset($attr[$key])) return null; $value = $attr[$key]; unset($attr[$key]); return $value; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Background.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Background.php new file mode 100644 index 00000000000..0e1ff24a3ed --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Background.php @@ -0,0 +1,23 @@ +confiscateAttr($attr, 'background'); + // some validation should happen here + + $this->prependCSS($attr, "background-image:url($background);"); + + return $attr; + + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/BdoDir.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/BdoDir.php index f127feb2b29..4d1a05665e4 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/BdoDir.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/BdoDir.php @@ -1,30 +1,19 @@ get('Attr', 'DefaultTextDir'); + $attr['dir'] = $config->get('Attr.DefaultTextDir'); return $attr; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/BgColor.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/BgColor.php index de2867efdd6..ad3916bb967 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/BgColor.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/BgColor.php @@ -1,25 +1,23 @@ confiscateAttr($attr, 'bgcolor'); // some validation should happen here - + $this->prependCSS($attr, "background-color:$bgcolor;"); - + return $attr; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/BoolToCSS.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/BoolToCSS.php index 25548eea7c9..51159b67157 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/BoolToCSS.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/BoolToCSS.php @@ -1,38 +1,36 @@ attr = $attr; $this->css = $css; } - - function transform($attr, $config, &$context) { + + public function transform($attr, $config, $context) { if (!isset($attr[$this->attr])) return $attr; unset($attr[$this->attr]); $this->prependCSS($attr, $this->css); return $attr; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Border.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Border.php index 7da4f6a8041..476b0b079b1 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Border.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Border.php @@ -1,19 +1,18 @@ confiscateAttr($attr, 'border'); // some validation should happen here $this->prependCSS($attr, "border:{$border_width}px solid;"); return $attr; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/EnumToCSS.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/EnumToCSS.php index 0470413dd48..2a5b4514ab4 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/EnumToCSS.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/EnumToCSS.php @@ -1,59 +1,58 @@ attr = $attr; $this->enumToCSS = $enum_to_css; $this->caseSensitive = (bool) $case_sensitive; } - - function transform($attr, $config, &$context) { - + + public function transform($attr, $config, $context) { + if (!isset($attr[$this->attr])) return $attr; - + $value = trim($attr[$this->attr]); unset($attr[$this->attr]); - + if (!$this->caseSensitive) $value = strtolower($value); - + if (!isset($this->enumToCSS[$value])) { return $attr; } - + $this->prependCSS($attr, $this->enumToCSS[$value]); - + return $attr; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/ImgRequired.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/ImgRequired.php index d0428055381..7f0e4b7a59f 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/ImgRequired.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/ImgRequired.php @@ -1,24 +1,7 @@ get('Core', 'RemoveInvalidImg')) return $attr; - $attr['src'] = $config->get('Attr', 'DefaultInvalidImage'); + if ($config->get('Core.RemoveInvalidImg')) return $attr; + $attr['src'] = $config->get('Attr.DefaultInvalidImage'); $src = false; } - + if (!isset($attr['alt'])) { if ($src) { - $attr['alt'] = basename($attr['src']); + $alt = $config->get('Attr.DefaultImageAlt'); + if ($alt === null) { + // truncate if the alt is too long + $attr['alt'] = substr(basename($attr['src']),0,40); + } else { + $attr['alt'] = $alt; + } } else { - $attr['alt'] = $config->get('Attr', 'DefaultInvalidImageAlt'); + $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt'); } } - + return $attr; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/ImgSpace.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/ImgSpace.php index 60d5edc781f..fd84c10c361 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/ImgSpace.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/ImgSpace.php @@ -1,46 +1,44 @@ array('left', 'right'), 'vspace' => array('top', 'bottom') ); - - function HTMLPurifier_AttrTransform_ImgSpace($attr) { + + public function __construct($attr) { $this->attr = $attr; if (!isset($this->css[$attr])) { trigger_error(htmlspecialchars($attr) . ' is not valid space attribute'); } } - - function transform($attr, $config, &$context) { - + + public function transform($attr, $config, $context) { + if (!isset($attr[$this->attr])) return $attr; - + $width = $this->confiscateAttr($attr, $this->attr); // some validation could happen here - + if (!isset($this->css[$this->attr])) return $attr; - + $style = ''; foreach ($this->css[$this->attr] as $suffix) { $property = "margin-$suffix"; $style .= "$property:{$width}px;"; } - + $this->prependCSS($attr, $style); - + return $attr; - + } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Input.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Input.php new file mode 100644 index 00000000000..16829552d14 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Input.php @@ -0,0 +1,40 @@ +pixels = new HTMLPurifier_AttrDef_HTML_Pixels(); + } + + public function transform($attr, $config, $context) { + if (!isset($attr['type'])) $t = 'text'; + else $t = strtolower($attr['type']); + if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') { + unset($attr['checked']); + } + if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') { + unset($attr['maxlength']); + } + if (isset($attr['size']) && $t !== 'text' && $t !== 'password') { + $result = $this->pixels->validate($attr['size'], $config, $context); + if ($result === false) unset($attr['size']); + else $attr['size'] = $result; + } + if (isset($attr['src']) && $t !== 'image') { + unset($attr['src']); + } + if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) { + $attr['value'] = ''; + } + return $attr; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Lang.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Lang.php index 899f5c8dc53..5869e7f8203 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Lang.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Lang.php @@ -1,7 +1,5 @@ name = $name; $this->cssName = $css_name ? $css_name : $name; } - - function transform($attr, $config, &$context) { + + public function transform($attr, $config, $context) { if (!isset($attr[$this->name])) return $attr; $length = $this->confiscateAttr($attr, $this->name); if(ctype_digit($length)) $length .= 'px'; $this->prependCSS($attr, $this->cssName . ":$length;"); return $attr; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Name.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Name.php index 248d0e02fee..15315bc7353 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Name.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Name.php @@ -1,20 +1,21 @@ get('HTML.Attr.Name.UseCDATA')) return $attr; if (!isset($attr['name'])) return $attr; $id = $this->confiscateAttr($attr, 'name'); if ( isset($attr['id'])) return $attr; $attr['id'] = $id; return $attr; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/NameSync.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/NameSync.php new file mode 100644 index 00000000000..a95638c140e --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/NameSync.php @@ -0,0 +1,27 @@ +idDef = new HTMLPurifier_AttrDef_HTML_ID(); + } + + public function transform($attr, $config, $context) { + if (!isset($attr['name'])) return $attr; + $name = $attr['name']; + if (isset($attr['id']) && $attr['id'] === $name) return $attr; + $result = $this->idDef->validate($name, $config, $context); + if ($result === false) unset($attr['name']); + else $attr['name'] = $result; + return $attr; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/SafeEmbed.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/SafeEmbed.php new file mode 100644 index 00000000000..4da449981fb --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/SafeEmbed.php @@ -0,0 +1,15 @@ +uri = new HTMLPurifier_AttrDef_URI(true); // embedded + } + + public function transform($attr, $config, $context) { + // If we add support for other objects, we'll need to alter the + // transforms. + switch ($attr['name']) { + // application/x-shockwave-flash + // Keep this synchronized with Injector/SafeObject.php + case 'allowScriptAccess': + $attr['value'] = 'never'; + break; + case 'allowNetworking': + $attr['value'] = 'internal'; + break; + case 'wmode': + $attr['value'] = 'window'; + break; + case 'movie': + case 'src': + $attr['name'] = "movie"; + $attr['value'] = $this->uri->validate($attr['value'], $config, $context); + break; + case 'flashvars': + // we're going to allow arbitrary inputs to the SWF, on + // the reasoning that it could only hack the SWF, not us. + break; + // add other cases to support other param name/value pairs + default: + $attr['name'] = $attr['value'] = null; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/ScriptRequired.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/ScriptRequired.php new file mode 100644 index 00000000000..4499050a22a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/ScriptRequired.php @@ -0,0 +1,16 @@ + + */ +class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform +{ + public function transform($attr, $config, $context) { + if (!isset($attr['type'])) { + $attr['type'] = 'text/javascript'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTransform/Textarea.php b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Textarea.php new file mode 100644 index 00000000000..81ac3488ba8 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/AttrTransform/Textarea.php @@ -0,0 +1,18 @@ + + */ +class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform +{ + + public function transform($attr, $config, $context) { + // Calculated from Firefox + if (!isset($attr['cols'])) $attr['cols'] = '22'; + if (!isset($attr['rows'])) $attr['rows'] = '3'; + return $attr; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrTypes.php b/lib/htmlpurifier/HTMLPurifier/AttrTypes.php index 93abb0d02b7..fc2ea4e5881 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrTypes.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrTypes.php @@ -1,18 +1,5 @@ info['Enum'] = new HTMLPurifier_AttrDef_Enum(); $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool(); - + $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text(); $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID(); $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length(); @@ -43,43 +29,49 @@ class HTMLPurifier_AttrTypes $this->info['URI'] = new HTMLPurifier_AttrDef_URI(); $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang(); $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color(); - + // unimplemented aliases $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text(); - + $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Character'] = new HTMLPurifier_AttrDef_Text(); + + // "proprietary" types + $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class(); + // number is really a positive integer (one or more digits) // FIXME: ^^ not always, see start and value of list items $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); } - + /** * Retrieves a type * @param $type String type name * @return Object AttrDef for type */ - function get($type) { - + public function get($type) { + // determine if there is any extra info tacked on if (strpos($type, '#') !== false) list($type, $string) = explode('#', $type, 2); else $string = ''; - + if (!isset($this->info[$type])) { trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); return; } - + return $this->info[$type]->make($string); - + } - + /** * Sets a new implementation for a type * @param $type String type name * @param $impl Object AttrDef for type */ - function set($type, $impl) { + public function set($type, $impl) { $this->info[$type] = $impl; } } - +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/AttrValidator.php b/lib/htmlpurifier/HTMLPurifier/AttrValidator.php index f020e0080e5..829a0f8f225 100644 --- a/lib/htmlpurifier/HTMLPurifier/AttrValidator.php +++ b/lib/htmlpurifier/HTMLPurifier/AttrValidator.php @@ -7,7 +7,7 @@ */ class HTMLPurifier_AttrValidator { - + /** * Validates the attributes of a token, returning a modified token * that has valid tokens @@ -18,57 +18,64 @@ class HTMLPurifier_AttrValidator * @param $config Instance of HTMLPurifier_Config * @param $context Instance of HTMLPurifier_Context */ - function validateToken(&$token, &$config, &$context) { - + public function validateToken(&$token, &$config, $context) { + $definition = $config->getHTMLDefinition(); $e =& $context->get('ErrorCollector', true); - + // initialize IDAccumulator if necessary $ok =& $context->get('IDAccumulator', true); if (!$ok) { $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); $context->register('IDAccumulator', $id_accumulator); } - + // initialize CurrentToken if necessary $current_token =& $context->get('CurrentToken', true); if (!$current_token) $context->register('CurrentToken', $token); - - if ($token->type !== 'start' && $token->type !== 'empty') return $token; - + + if ( + !$token instanceof HTMLPurifier_Token_Start && + !$token instanceof HTMLPurifier_Token_Empty + ) return $token; + // create alias to global definition array, see also $defs // DEFINITION CALL $d_defs = $definition->info_global_attr; - + // don't update token until the very end, to ensure an atomic update $attr = $token->attr; - + // do global transformations (pre) // nothing currently utilizes this foreach ($definition->info_attr_transform_pre as $transform) { $attr = $transform->transform($o = $attr, $config, $context); - if ($e && ($attr != $o)) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + if ($e) { + if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } } - + // do local transformations only applicable to this element (pre) // ex.

to

foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { $attr = $transform->transform($o = $attr, $config, $context); - if ($e && ($attr != $o)) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + if ($e) { + if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } } - + // create alias to this element's attribute definition array, see // also $d_defs (global attribute definition array) // DEFINITION CALL $defs = $definition->info[$token->name]->attr; - + $attr_key = false; $context->register('CurrentAttr', $attr_key); - + // iterate through all the attribute keypairs // Watch out for name collisions: $key has previously been used foreach ($attr as $attr_key => $value) { - + // call the definition if ( isset($defs[$attr_key]) ) { // there is a local definition defined @@ -95,54 +102,61 @@ class HTMLPurifier_AttrValidator // system never heard of the attribute? DELETE! $result = false; } - + // put the results into effect if ($result === false || $result === null) { // this is a generic error message that should replaced // with more specific ones when possible if ($e) $e->send(E_ERROR, 'AttrValidator: Attribute removed'); - + // remove the attribute unset($attr[$attr_key]); } elseif (is_string($result)) { // generally, if a substitution is happening, there // was some sort of implicit correction going on. We'll // delegate it to the attribute classes to say exactly what. - + // simple substitution $attr[$attr_key] = $result; + } else { + // nothing happens } - + // we'd also want slightly more complicated substitution // involving an array as the return value, // although we're not sure how colliding attributes would // resolve (certain ones would be completely overriden, // others would prepend themselves). } - + $context->destroy('CurrentAttr'); - + // post transforms - + // global (error reporting untested) foreach ($definition->info_attr_transform_post as $transform) { $attr = $transform->transform($o = $attr, $config, $context); - if ($e && ($attr != $o)) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + if ($e) { + if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } } - + // local (error reporting untested) foreach ($definition->info[$token->name]->attr_transform_post as $transform) { $attr = $transform->transform($o = $attr, $config, $context); - if ($e && ($attr != $o)) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + if ($e) { + if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } } - + $token->attr = $attr; - + // destroy CurrentToken if we made it ourselves if (!$current_token) $context->destroy('CurrentToken'); - + } - - + + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Bootstrap.php b/lib/htmlpurifier/HTMLPurifier/Bootstrap.php new file mode 100644 index 00000000000..559f61a2320 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/Bootstrap.php @@ -0,0 +1,98 @@ + +if (!defined('PHP_EOL')) { + switch (strtoupper(substr(PHP_OS, 0, 3))) { + case 'WIN': + define('PHP_EOL', "\r\n"); + break; + case 'DAR': + define('PHP_EOL', "\r"); + break; + default: + define('PHP_EOL', "\n"); + } +} + +/** + * Bootstrap class that contains meta-functionality for HTML Purifier such as + * the autoload function. + * + * @note + * This class may be used without any other files from HTML Purifier. + */ +class HTMLPurifier_Bootstrap +{ + + /** + * Autoload function for HTML Purifier + * @param $class Class to load + */ + public static function autoload($class) { + $file = HTMLPurifier_Bootstrap::getPath($class); + if (!$file) return false; + require HTMLPURIFIER_PREFIX . '/' . $file; + return true; + } + + /** + * Returns the path for a specific class. + */ + public static function getPath($class) { + if (strncmp('HTMLPurifier', $class, 12) !== 0) return false; + // Custom implementations + if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) { + $code = str_replace('_', '-', substr($class, 22)); + $file = 'HTMLPurifier/Language/classes/' . $code . '.php'; + } else { + $file = str_replace('_', '/', $class) . '.php'; + } + if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) return false; + return $file; + } + + /** + * "Pre-registers" our autoloader on the SPL stack. + */ + public static function registerAutoload() { + $autoload = array('HTMLPurifier_Bootstrap', 'autoload'); + if ( ($funcs = spl_autoload_functions()) === false ) { + spl_autoload_register($autoload); + } elseif (function_exists('spl_autoload_unregister')) { + $compat = version_compare(PHP_VERSION, '5.1.2', '<=') && + version_compare(PHP_VERSION, '5.1.0', '>='); + foreach ($funcs as $func) { + if (is_array($func)) { + // :TRICKY: There are some compatibility issues and some + // places where we need to error out + $reflector = new ReflectionMethod($func[0], $func[1]); + if (!$reflector->isStatic()) { + throw new Exception(' + HTML Purifier autoloader registrar is not compatible + with non-static object methods due to PHP Bug #44144; + Please do not use HTMLPurifier.autoload.php (or any + file that includes this file); instead, place the code: + spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\')) + after your own autoloaders. + '); + } + // Suprisingly, spl_autoload_register supports the + // Class::staticMethod callback format, although call_user_func doesn't + if ($compat) $func = implode('::', $func); + } + spl_autoload_unregister($func); + } + spl_autoload_register($autoload); + foreach ($funcs as $func) spl_autoload_register($func); + } + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/CSSDefinition.php b/lib/htmlpurifier/HTMLPurifier/CSSDefinition.php index c90f5d91f49..6a2e6f56d99 100644 --- a/lib/htmlpurifier/HTMLPurifier/CSSDefinition.php +++ b/lib/htmlpurifier/HTMLPurifier/CSSDefinition.php @@ -1,79 +1,37 @@ - Revision identifier for your custom definition. See - %HTML.DefinitionRev for details. This directive has been available - since 2.0.0. -

-'); - -HTMLPurifier_ConfigSchema::define( - 'CSS', 'MaxImgLength', '1200px', 'string/null', ' -

- 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). -

-'); - /** * Defines allowed CSS attributes and what their values are. * @see HTMLPurifier_HTMLDefinition */ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition { - - var $type = 'CSS'; - + + public $type = 'CSS'; + /** * Assoc array of attribute name to definition object. */ - var $info = array(); - + public $info = array(); + /** * Constructs the info array. The meat of this class. */ - function doSetup($config) { - + protected function doSetup($config) { + $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum( array('left', 'right', 'center', 'justify'), false); - + $border_style = - $this->info['border-bottom-style'] = - $this->info['border-right-style'] = - $this->info['border-left-style'] = + $this->info['border-bottom-style'] = + $this->info['border-right-style'] = + $this->info['border-left-style'] = $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum( array('none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'), false); - + $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style); - + $this->info['clear'] = new HTMLPurifier_AttrDef_Enum( array('none', 'left', 'right', 'both'), false); $this->info['float'] = new HTMLPurifier_AttrDef_Enum( @@ -82,27 +40,27 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition array('normal', 'italic', 'oblique'), false); $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum( array('normal', 'small-caps'), false); - + $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite( array( new HTMLPurifier_AttrDef_Enum(array('none')), new HTMLPurifier_AttrDef_CSS_URI() ) ); - + $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum( array('inside', 'outside'), false); $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum( array('disc', 'circle', 'square', 'decimal', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha', 'none'), false); $this->info['list-style-image'] = $uri_or_none; - + $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config); - + $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum( array('capitalize', 'uppercase', 'lowercase', 'none'), false); $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color(); - + $this->info['background-image'] = $uri_or_none; $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum( array('repeat', 'repeat-x', 'repeat-y', 'no-repeat') @@ -111,42 +69,42 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition array('scroll', 'fixed') ); $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition(); - - $border_color = - $this->info['border-top-color'] = - $this->info['border-bottom-color'] = - $this->info['border-left-color'] = - $this->info['border-right-color'] = + + $border_color = + $this->info['border-top-color'] = + $this->info['border-bottom-color'] = + $this->info['border-left-color'] = + $this->info['border-right-color'] = $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_Enum(array('transparent')), new HTMLPurifier_AttrDef_CSS_Color() )); - + $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config); - + $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color); - - $border_width = - $this->info['border-top-width'] = - $this->info['border-bottom-width'] = - $this->info['border-left-width'] = + + $border_width = + $this->info['border-top-width'] = + $this->info['border-bottom-width'] = + $this->info['border-left-width'] = $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')), new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative )); - + $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width); - + $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_Enum(array('normal')), new HTMLPurifier_AttrDef_CSS_Length() )); - + $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_Enum(array('normal')), new HTMLPurifier_AttrDef_CSS_Length() )); - + $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_Enum(array('xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', @@ -154,53 +112,54 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition new HTMLPurifier_AttrDef_CSS_Percentage(), new HTMLPurifier_AttrDef_CSS_Length() )); - + $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_Enum(array('normal')), new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives new HTMLPurifier_AttrDef_CSS_Length('0'), new HTMLPurifier_AttrDef_CSS_Percentage(true) )); - + $margin = - $this->info['margin-top'] = - $this->info['margin-bottom'] = - $this->info['margin-left'] = + $this->info['margin-top'] = + $this->info['margin-bottom'] = + $this->info['margin-left'] = $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_CSS_Length(), new HTMLPurifier_AttrDef_CSS_Percentage(), new HTMLPurifier_AttrDef_Enum(array('auto')) )); - + $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin); - + // non-negative $padding = - $this->info['padding-top'] = - $this->info['padding-bottom'] = - $this->info['padding-left'] = + $this->info['padding-top'] = + $this->info['padding-bottom'] = + $this->info['padding-left'] = $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_CSS_Length('0'), new HTMLPurifier_AttrDef_CSS_Percentage(true) )); - + $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding); - + $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_CSS_Length(), new HTMLPurifier_AttrDef_CSS_Percentage() )); - + $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_CSS_Length('0'), new HTMLPurifier_AttrDef_CSS_Percentage(true), new HTMLPurifier_AttrDef_Enum(array('auto')) )); - $max = $config->get('CSS', 'MaxImgLength'); + $max = $config->get('CSS.MaxImgLength'); + $this->info['width'] = $this->info['height'] = $max === null ? - $trusted_wh : + $trusted_wh : new HTMLPurifier_AttrDef_Switch('img', // For img tags: new HTMLPurifier_AttrDef_CSS_Composite(array( @@ -210,49 +169,124 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition // For everyone else: $trusted_wh ); - + $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); - + $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); - + // this could use specialized code $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum( array('normal', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '700', '800', '900'), false); - + // MUST be called after other font properties, as it references // a CSSDefinition object $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config); - + // same here $this->info['border'] = - $this->info['border-bottom'] = - $this->info['border-top'] = - $this->info['border-left'] = + $this->info['border-bottom'] = + $this->info['border-top'] = + $this->info['border-left'] = $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config); - + $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum(array( 'collapse', 'separate')); - + $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum(array( 'top', 'bottom')); - + $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum(array( 'auto', 'fixed')); - + $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite(array( new HTMLPurifier_AttrDef_Enum(array('baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom')), new HTMLPurifier_AttrDef_CSS_Length(), new HTMLPurifier_AttrDef_CSS_Percentage() )); - + $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2); - + // partial support $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum(array('nowrap')); - + + if ($config->get('CSS.Proprietary')) { + $this->doSetupProprietary($config); + } + + if ($config->get('CSS.AllowTricky')) { + $this->doSetupTricky($config); + } + + $allow_important = $config->get('CSS.AllowImportant'); + // wrap all attr-defs with decorator that handles !important + foreach ($this->info as $k => $v) { + $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important); + } + + $this->setupConfigStuff($config); + } + + protected function doSetupProprietary($config) { + // Internet Explorer only scrollbar colors + $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + // technically not proprietary, but CSS3, and no one supports it + $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + + // only opacity, for now + $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter(); + + } + + protected function doSetupTricky($config) { + $this->info['display'] = new HTMLPurifier_AttrDef_Enum(array( + 'inline', 'block', 'list-item', 'run-in', 'compact', + 'marker', 'table', 'inline-table', 'table-row-group', + 'table-header-group', 'table-footer-group', 'table-row', + 'table-column-group', 'table-column', 'table-cell', 'table-caption', 'none' + )); + $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum(array( + 'visible', 'hidden', 'collapse' + )); + $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll')); + } + + + /** + * Performs extra config-based processing. Based off of + * HTMLPurifier_HTMLDefinition. + * @todo Refactor duplicate elements into common class (probably using + * composition, not inheritance). + */ + protected function setupConfigStuff($config) { + + // setup allowed elements + $support = "(for information on implementing this, see the ". + "support forums) "; + $allowed_attributes = $config->get('CSS.AllowedProperties'); + if ($allowed_attributes !== null) { + foreach ($this->info as $name => $d) { + if(!isset($allowed_attributes[$name])) unset($this->info[$name]); + unset($allowed_attributes[$name]); + } + // emit errors + foreach ($allowed_attributes as $name => $d) { + // :TODO: Is this htmlspecialchars() call really necessary? + $name = htmlspecialchars($name); + trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING); + } + } + } - } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ChildDef.php b/lib/htmlpurifier/HTMLPurifier/ChildDef.php index 5236d266c56..c5d5216dab2 100644 --- a/lib/htmlpurifier/HTMLPurifier/ChildDef.php +++ b/lib/htmlpurifier/HTMLPurifier/ChildDef.php @@ -1,50 +1,40 @@ elements; + } + /** * Validates nodes according to definition and returns modification. - * - * @public + * * @param $tokens_of_children Array of HTMLPurifier_Token * @param $config HTMLPurifier_Config object * @param $context HTMLPurifier_Context object @@ -52,9 +42,7 @@ class HTMLPurifier_ChildDef * @return bool false to remove parent node * @return array of replacement child tokens */ - function validateChildren($tokens_of_children, $config, &$context) { - trigger_error('Call to abstract function', E_USER_ERROR); - } + abstract public function validateChildren($tokens_of_children, $config, $context); } - +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ChildDef/Chameleon.php b/lib/htmlpurifier/HTMLPurifier/ChildDef/Chameleon.php index b338354d381..15c364ee33c 100644 --- a/lib/htmlpurifier/HTMLPurifier/ChildDef/Chameleon.php +++ b/lib/htmlpurifier/HTMLPurifier/ChildDef/Chameleon.php @@ -1,10 +1,8 @@ inline = new HTMLPurifier_ChildDef_Optional($inline); $this->block = new HTMLPurifier_ChildDef_Optional($block); $this->elements = $this->block->elements; } - - function validateChildren($tokens_of_children, $config, &$context) { + + public function validateChildren($tokens_of_children, $config, $context) { if ($context->get('IsInline') === false) { return $this->block->validateChildren( $tokens_of_children, $config, $context); @@ -49,3 +45,4 @@ class HTMLPurifier_ChildDef_Chameleon extends HTMLPurifier_ChildDef } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ChildDef/Custom.php b/lib/htmlpurifier/HTMLPurifier/ChildDef/Custom.php index ba722d05951..b68047b4b55 100644 --- a/lib/htmlpurifier/HTMLPurifier/ChildDef/Custom.php +++ b/lib/htmlpurifier/HTMLPurifier/ChildDef/Custom.php @@ -1,80 +1,76 @@ dtd_regex = $dtd_regex; $this->_compileRegex(); } /** * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex) */ - function _compileRegex() { + protected function _compileRegex() { $raw = str_replace(' ', '', $this->dtd_regex); if ($raw{0} != '(') { $raw = "($raw)"; } $el = '[#a-zA-Z0-9_.-]+'; $reg = $raw; - + // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M // DOING! Seriously: if there's problems, please report them. - + // collect all elements into the $elements array preg_match_all("/$el/", $reg, $matches); foreach ($matches[0] as $match) { $this->elements[$match] = true; } - + // setup all elements as parentheticals with leading commas $reg = preg_replace("/$el/", '(,\\0)', $reg); - + // remove commas when they were not solicited $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg); - + // remove all non-paranthetical commas: they are handled by first regex $reg = preg_replace("/,\(/", '(', $reg); - + $this->_pcre_regex = $reg; } - function validateChildren($tokens_of_children, $config, &$context) { + public function validateChildren($tokens_of_children, $config, $context) { $list_of_children = ''; $nesting = 0; // depth into the nest foreach ($tokens_of_children as $token) { if (!empty($token->is_whitespace)) continue; - + $is_child = ($nesting == 0); // direct - - if ($token->type == 'start') { + + if ($token instanceof HTMLPurifier_Token_Start) { $nesting++; - } elseif ($token->type == 'end') { + } elseif ($token instanceof HTMLPurifier_Token_End) { $nesting--; } - + if ($is_child) { $list_of_children .= $token->name . ','; } @@ -86,8 +82,9 @@ class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef '/^,?'.$this->_pcre_regex.'$/', $list_of_children ); - + return (bool) $okay; } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ChildDef/Empty.php b/lib/htmlpurifier/HTMLPurifier/ChildDef/Empty.php index 6e637307701..13171f6651d 100644 --- a/lib/htmlpurifier/HTMLPurifier/ChildDef/Empty.php +++ b/lib/htmlpurifier/HTMLPurifier/ChildDef/Empty.php @@ -1,7 +1,5 @@ whitespace) return $tokens_of_children; else return array(); } return $result; } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ChildDef/Required.php b/lib/htmlpurifier/HTMLPurifier/ChildDef/Required.php index f4d908b05d8..4889f249b86 100644 --- a/lib/htmlpurifier/HTMLPurifier/ChildDef/Required.php +++ b/lib/htmlpurifier/HTMLPurifier/ChildDef/Required.php @@ -1,7 +1,5 @@ elements = $elements; } - var $allow_empty = false; - var $type = 'required'; - function validateChildren($tokens_of_children, $config, &$context) { + public $allow_empty = false; + public $type = 'required'; + public function validateChildren($tokens_of_children, $config, $context) { + // Flag for subclasses + $this->whitespace = false; + // if there are no tokens, delete parent node if (empty($tokens_of_children)) return false; - + // the new set of children $result = array(); - + // current depth into the nest $nesting = 0; - + // whether or not we're deleting a node $is_deleting = false; - + // whether or not parsed character data is allowed // this controls whether or not we silently drop a tag // or generate escaped HTML from it $pcdata_allowed = isset($this->elements['#PCDATA']); - + // a little sanity check to make sure it's not ALL whitespace $all_whitespace = true; - + // some configuration - $escape_invalid_children = $config->get('Core', 'EscapeInvalidChildren'); - + $escape_invalid_children = $config->get('Core.EscapeInvalidChildren'); + // generator - static $gen = null; - if ($gen === null) { - $gen = new HTMLPurifier_Generator(); - } - + $gen = new HTMLPurifier_Generator($config, $context); + foreach ($tokens_of_children as $token) { if (!empty($token->is_whitespace)) { $result[] = $token; continue; } $all_whitespace = false; // phew, we're not talking about whitespace - + $is_child = ($nesting == 0); - - if ($token->type == 'start') { + + if ($token instanceof HTMLPurifier_Token_Start) { $nesting++; - } elseif ($token->type == 'end') { + } elseif ($token instanceof HTMLPurifier_Token_End) { $nesting--; } - + if ($is_child) { $is_deleting = false; if (!isset($this->elements[$token->name])) { $is_deleting = true; - if ($pcdata_allowed && $token->type == 'text') { + if ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text) { $result[] = $token; } elseif ($pcdata_allowed && $escape_invalid_children) { $result[] = new HTMLPurifier_Token_Text( - $gen->generateFromToken($token, $config) + $gen->generateFromToken($token) ); } continue; } } - if (!$is_deleting || ($pcdata_allowed && $token->type == 'text')) { + if (!$is_deleting || ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text)) { $result[] = $token; } elseif ($pcdata_allowed && $escape_invalid_children) { $result[] = new HTMLPurifier_Token_Text( - $gen->generateFromToken( $token, $config ) + $gen->generateFromToken($token) ); } else { // drop silently } } if (empty($result)) return false; - if ($all_whitespace) return false; + if ($all_whitespace) { + $this->whitespace = true; + return false; + } if ($tokens_of_children == $result) return true; return $result; } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ChildDef/StrictBlockquote.php b/lib/htmlpurifier/HTMLPurifier/ChildDef/StrictBlockquote.php index 60dcbc4a152..dfae8a6e5e1 100644 --- a/lib/htmlpurifier/HTMLPurifier/ChildDef/StrictBlockquote.php +++ b/lib/htmlpurifier/HTMLPurifier/ChildDef/StrictBlockquote.php @@ -1,43 +1,44 @@ getHTMLDefinition(); - if (!$this->init) { - // allow all inline elements - $this->real_elements = $this->elements; - $this->fake_elements = $def->info_content_sets['Flow']; - $this->fake_elements['#PCDATA'] = true; - $this->init = true; - } - + protected $real_elements; + protected $fake_elements; + public $allow_empty = true; + public $type = 'strictblockquote'; + protected $init = false; + + /** + * @note We don't want MakeWellFormed to auto-close inline elements since + * they might be allowed. + */ + public function getAllowedElements($config) { + $this->init($config); + return $this->fake_elements; + } + + public function validateChildren($tokens_of_children, $config, $context) { + + $this->init($config); + // trick the parent class into thinking it allows more $this->elements = $this->fake_elements; $result = parent::validateChildren($tokens_of_children, $config, $context); $this->elements = $this->real_elements; - + if ($result === false) return array(); if ($result === true) $result = $tokens_of_children; - + + $def = $config->getHTMLDefinition(); $block_wrap_start = new HTMLPurifier_Token_Start($def->info_block_wrapper); $block_wrap_end = new HTMLPurifier_Token_End( $def->info_block_wrapper); $is_inline = false; $depth = 0; $ret = array(); - + // assuming that there are no comment tokens foreach ($result as $i => $token) { $token = $result[$i]; @@ -45,8 +46,8 @@ extends HTMLPurifier_ChildDef_Required if (!$is_inline) { if (!$depth) { if ( - ($token->type == 'text' && !$token->is_whitespace) || - ($token->type != 'text' && !isset($this->elements[$token->name])) + ($token instanceof HTMLPurifier_Token_Text && !$token->is_whitespace) || + (!$token instanceof HTMLPurifier_Token_Text && !isset($this->elements[$token->name])) ) { $is_inline = true; $ret[] = $block_wrap_start; @@ -55,7 +56,7 @@ extends HTMLPurifier_ChildDef_Required } else { if (!$depth) { // starting tokens have been inline text / empty - if ($token->type == 'start' || $token->type == 'empty') { + if ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) { if (isset($this->elements[$token->name])) { // ended $ret[] = $block_wrap_end; @@ -65,11 +66,23 @@ extends HTMLPurifier_ChildDef_Required } } $ret[] = $token; - if ($token->type == 'start') $depth++; - if ($token->type == 'end') $depth--; + if ($token instanceof HTMLPurifier_Token_Start) $depth++; + if ($token instanceof HTMLPurifier_Token_End) $depth--; } if ($is_inline) $ret[] = $block_wrap_end; return $ret; } + + private function init($config) { + if (!$this->init) { + $def = $config->getHTMLDefinition(); + // allow all inline elements + $this->real_elements = $this->elements; + $this->fake_elements = $def->info_content_sets['Flow']; + $this->fake_elements['#PCDATA'] = true; + $this->init = true; + } + } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ChildDef/Table.php b/lib/htmlpurifier/HTMLPurifier/ChildDef/Table.php index ca3c83cc0e0..34f0227dd2c 100644 --- a/lib/htmlpurifier/HTMLPurifier/ChildDef/Table.php +++ b/lib/htmlpurifier/HTMLPurifier/ChildDef/Table.php @@ -1,52 +1,50 @@ true, 'tbody' => true, 'thead' => true, + public $allow_empty = false; + public $type = 'table'; + public $elements = array('tr' => true, 'tbody' => true, 'thead' => true, 'tfoot' => true, 'caption' => true, 'colgroup' => true, 'col' => true); - function HTMLPurifier_ChildDef_Table() {} - function validateChildren($tokens_of_children, $config, &$context) { + public function __construct() {} + public function validateChildren($tokens_of_children, $config, $context) { if (empty($tokens_of_children)) return false; - + // this ensures that the loop gets run one last time before closing // up. It's a little bit of a hack, but it works! Just make sure you // get rid of the token later. $tokens_of_children[] = false; - + // only one of these elements is allowed in a table $caption = false; $thead = false; $tfoot = false; - + // as many of these as you want $cols = array(); $content = array(); - + $nesting = 0; // current depth so we can determine nodes $is_collecting = false; // are we globbing together tokens to package // into one of the collectors? $collection = array(); // collected nodes $tag_index = 0; // the first node might be whitespace, // so this tells us where the start tag is - + foreach ($tokens_of_children as $token) { $is_child = ($nesting == 0); - + if ($token === false) { // terminating sequence started - } elseif ($token->type == 'start') { + } elseif ($token instanceof HTMLPurifier_Token_Start) { $nesting++; - } elseif ($token->type == 'end') { + } elseif ($token instanceof HTMLPurifier_Token_End) { $nesting--; } - + // handle node collection if ($is_collecting) { if ($is_child) { @@ -87,10 +85,10 @@ class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef $collection[] = $token; } } - + // terminate if ($token === false) break; - + if ($is_child) { // determine what we're dealing with if ($token->name == 'col') { @@ -112,7 +110,7 @@ class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef $collection[] = $token; continue; default: - if ($token->type == 'text' && $token->is_whitespace) { + if (!empty($token->is_whitespace)) { $collection[] = $token; $tag_index++; } @@ -120,9 +118,9 @@ class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef } } } - + if (empty($content)) return false; - + $ret = array(); if ($caption !== false) $ret = array_merge($ret, $caption); if ($cols !== false) foreach ($cols as $token_array) $ret = array_merge($ret, $token_array); @@ -133,11 +131,12 @@ class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef // grab the trailing space $ret = array_merge($ret, $collection); } - + array_pop($tokens_of_children); // remove phantom token - + return ($ret === $tokens_of_children) ? true : $ret; - + } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Config.php b/lib/htmlpurifier/HTMLPurifier/Config.php index bfdcc33e7a0..203831f9f5d 100644 --- a/lib/htmlpurifier/HTMLPurifier/Config.php +++ b/lib/htmlpurifier/HTMLPurifier/Config.php @@ -1,162 +1,192 @@ -if (!defined('PHP_EOL')) { - switch (strtoupper(substr(PHP_OS, 0, 3))) { - case 'WIN': - define('PHP_EOL', "\r\n"); - break; - case 'DAR': - define('PHP_EOL', "\r"); - break; - default: - define('PHP_EOL', "\n"); - } -} - /** * Configuration object that triggers customizable behavior. * * @warning This class is strongly defined: that means that the class * will fail if an undefined directive is retrieved or set. - * + * * @note Many classes that could (although many times don't) use the * configuration object make it a mandatory parameter. This is * because a configuration object should always be forwarded, * otherwise, you run the risk of missing a parameter and then * being stumped when a configuration directive doesn't work. + * + * @todo Reconsider some of the public member variables */ class HTMLPurifier_Config { - + /** * HTML Purifier's version */ - var $version = '2.1.5'; - + public $version = '4.1.0'; + /** - * Two-level associative array of configuration directives - */ - var $conf; - - /** - * Reference HTMLPurifier_ConfigSchema for value checking - */ - var $def; - - /** - * Indexed array of definitions - */ - var $definitions; - - /** - * Bool indicator whether or not config is finalized - */ - var $finalized = false; - - /** - * Bool indicator whether or not to automatically finalize + * Bool indicator whether or not to automatically finalize * the object if a read operation is done */ - var $autoFinalize = true; - + public $autoFinalize = true; + + // protected member variables + /** * Namespace indexed array of serials for specific namespaces (see - * getSerial for more info). + * getSerial() for more info). */ - var $serials = array(); - + protected $serials = array(); + /** * Serial for entire configuration object */ - var $serial; - + protected $serial; + + /** + * Parser for variables + */ + protected $parser; + + /** + * Reference HTMLPurifier_ConfigSchema for value checking + * @note This is public for introspective purposes. Please don't + * abuse! + */ + public $def; + + /** + * Indexed array of definitions + */ + protected $definitions; + + /** + * Bool indicator whether or not config is finalized + */ + protected $finalized = false; + + /** + * Property list containing configuration directives. + */ + protected $plist; + + /** + * Whether or not a set is taking place due to an + * alias lookup. + */ + private $aliasMode; + + /** + * Set to false if you do not want line and file numbers in errors + * (useful when unit testing) + */ + public $chatty = true; + + /** + * Current lock; only gets to this namespace are allowed. + */ + private $lock; + /** * @param $definition HTMLPurifier_ConfigSchema that defines what directives * are allowed. */ - function HTMLPurifier_Config(&$definition) { - $this->conf = $definition->defaults; // set up, copy in defaults - $this->def = $definition; // keep a copy around for checking + public function __construct($definition, $parent = null) { + $parent = $parent ? $parent : $definition->defaultPlist; + $this->plist = new HTMLPurifier_PropertyList($parent); + $this->def = $definition; // keep a copy around for checking + $this->parser = new HTMLPurifier_VarParser_Flexible(); } - + /** * Convenience constructor that creates a config object based on a mixed var - * @static * @param mixed $config Variable that defines the state of the config * object. Can be: a HTMLPurifier_Config() object, * an array of directives based on loadArray(), * or a string filename of an ini file. + * @param HTMLPurifier_ConfigSchema Schema object * @return Configured HTMLPurifier_Config object */ - function create($config) { - if (is_a($config, 'HTMLPurifier_Config')) { + public static function create($config, $schema = null) { + if ($config instanceof HTMLPurifier_Config) { // pass-through return $config; } - $ret = HTMLPurifier_Config::createDefault(); + if (!$schema) { + $ret = HTMLPurifier_Config::createDefault(); + } else { + $ret = new HTMLPurifier_Config($schema); + } if (is_string($config)) $ret->loadIni($config); elseif (is_array($config)) $ret->loadArray($config); return $ret; } - + + /** + * Creates a new config object that inherits from a previous one. + * @param HTMLPurifier_Config $config Configuration object to inherit + * from. + * @return HTMLPurifier_Config object with $config as its parent. + */ + public static function inherit(HTMLPurifier_Config $config) { + return new HTMLPurifier_Config($config->def, $config->plist); + } + /** * Convenience constructor that creates a default configuration object. - * @static * @return Default HTMLPurifier_Config object. */ - function createDefault() { - $definition =& HTMLPurifier_ConfigSchema::instance(); + public static function createDefault() { + $definition = HTMLPurifier_ConfigSchema::instance(); $config = new HTMLPurifier_Config($definition); return $config; } - + /** * Retreives a value from the configuration. - * @param $namespace String namespace * @param $key String key */ - function get($namespace, $key, $from_alias = false) { - if (!$this->finalized && $this->autoFinalize) $this->finalize(); - if (!isset($this->def->info[$namespace][$key])) { + public function get($key, $a = null) { + if ($a !== null) { + $this->triggerError("Using deprecated API: use \$config->get('$key.$a') instead", E_USER_WARNING); + $key = "$key.$a"; + } + if (!$this->finalized) $this->autoFinalize(); + if (!isset($this->def->info[$key])) { // can't add % due to SimpleTest bug - trigger_error('Cannot retrieve value of undefined directive ' . htmlspecialchars("$namespace.$key"), + $this->triggerError('Cannot retrieve value of undefined directive ' . htmlspecialchars($key), E_USER_WARNING); return; } - if ($this->def->info[$namespace][$key]->class == 'alias') { - $d = $this->def->info[$namespace][$key]; - trigger_error('Cannot get value from aliased directive, use real name ' . $d->namespace . '.' . $d->name, + if (isset($this->def->info[$key]->isAlias)) { + $d = $this->def->info[$key]; + $this->triggerError('Cannot get value from aliased directive, use real name ' . $d->key, E_USER_ERROR); return; } - return $this->conf[$namespace][$key]; + if ($this->lock) { + list($ns) = explode('.', $key); + if ($ns !== $this->lock) { + $this->triggerError('Cannot get value of namespace ' . $ns . ' when lock for ' . $this->lock . ' is active, this probably indicates a Definition setup method is accessing directives that are not within its namespace', E_USER_ERROR); + return; + } + } + return $this->plist->get($key); } - + /** * Retreives an array of directives to values from a given namespace * @param $namespace String namespace */ - function getBatch($namespace) { - if (!$this->finalized && $this->autoFinalize) $this->finalize(); - if (!isset($this->def->info[$namespace])) { - trigger_error('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace), + public function getBatch($namespace) { + if (!$this->finalized) $this->autoFinalize(); + $full = $this->getAll(); + if (!isset($full[$namespace])) { + $this->triggerError('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace), E_USER_WARNING); return; } - return $this->conf[$namespace]; + return $full[$namespace]; } - + /** * Returns a md5 signature of a segment of the configuration object * that uniquely identifies that particular configuration @@ -164,7 +194,7 @@ class HTMLPurifier_Config * before processing! * @param $namespace Namespace to get serial for */ - function getBatchSerial($namespace) { + public function getBatchSerial($namespace) { if (empty($this->serials[$namespace])) { $batch = $this->getBatch($namespace); unset($batch['DefinitionRev']); @@ -172,121 +202,149 @@ class HTMLPurifier_Config } return $this->serials[$namespace]; } - + /** * Returns a md5 signature for the entire configuration object * that uniquely identifies that particular configuration */ - function getSerial() { + public function getSerial() { if (empty($this->serial)) { $this->serial = md5(serialize($this->getAll())); } return $this->serial; } - + /** * Retrieves all directives, organized by namespace + * @warning This is a pretty inefficient function, avoid if you can */ - function getAll() { - if (!$this->finalized && $this->autoFinalize) $this->finalize(); - return $this->conf; + public function getAll() { + if (!$this->finalized) $this->autoFinalize(); + $ret = array(); + foreach ($this->plist->squash() as $name => $value) { + list($ns, $key) = explode('.', $name, 2); + $ret[$ns][$key] = $value; + } + return $ret; } - + /** * Sets a value to configuration. - * @param $namespace String namespace * @param $key String key * @param $value Mixed value */ - function set($namespace, $key, $value, $from_alias = false) { + public function set($key, $value, $a = null) { + if (strpos($key, '.') === false) { + $namespace = $key; + $directive = $value; + $value = $a; + $key = "$key.$directive"; + $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE); + } else { + list($namespace) = explode('.', $key); + } if ($this->isFinalized('Cannot set directive after finalization')) return; - if (!isset($this->def->info[$namespace][$key])) { - trigger_error('Cannot set undefined directive ' . htmlspecialchars("$namespace.$key") . ' to value', + if (!isset($this->def->info[$key])) { + $this->triggerError('Cannot set undefined directive ' . htmlspecialchars($key) . ' to value', E_USER_WARNING); return; } - if ($this->def->info[$namespace][$key]->class == 'alias') { - if ($from_alias) { - trigger_error('Double-aliases not allowed, please fix '. - 'ConfigSchema bug with' . "$namespace.$key"); + $def = $this->def->info[$key]; + + if (isset($def->isAlias)) { + if ($this->aliasMode) { + $this->triggerError('Double-aliases not allowed, please fix '. + 'ConfigSchema bug with' . $key, E_USER_ERROR); + return; } - $this->set($this->def->info[$namespace][$key]->namespace, - $this->def->info[$namespace][$key]->name, - $value, true); + $this->aliasMode = true; + $this->set($def->key, $value); + $this->aliasMode = false; + $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE); return; } - $value = $this->def->validate( - $value, - $type = $this->def->info[$namespace][$key]->type, - $this->def->info[$namespace][$key]->allow_null - ); - if (is_string($value)) { + + // Raw type might be negative when using the fully optimized form + // of stdclass, which indicates allow_null == true + $rtype = is_int($def) ? $def : $def->type; + if ($rtype < 0) { + $type = -$rtype; + $allow_null = true; + } else { + $type = $rtype; + $allow_null = isset($def->allow_null); + } + + try { + $value = $this->parser->parse($value, $type, $allow_null); + } catch (HTMLPurifier_VarParserException $e) { + $this->triggerError('Value for ' . $key . ' is of invalid type, should be ' . HTMLPurifier_VarParser::getTypeName($type), E_USER_WARNING); + return; + } + if (is_string($value) && is_object($def)) { // resolve value alias if defined - if (isset($this->def->info[$namespace][$key]->aliases[$value])) { - $value = $this->def->info[$namespace][$key]->aliases[$value]; + if (isset($def->aliases[$value])) { + $value = $def->aliases[$value]; } - if ($this->def->info[$namespace][$key]->allowed !== true) { - // check to see if the value is allowed - if (!isset($this->def->info[$namespace][$key]->allowed[$value])) { - trigger_error('Value not supported, valid values are: ' . - $this->_listify($this->def->info[$namespace][$key]->allowed), E_USER_WARNING); - return; - } + // check to see if the value is allowed + if (isset($def->allowed) && !isset($def->allowed[$value])) { + $this->triggerError('Value not supported, valid values are: ' . + $this->_listify($def->allowed), E_USER_WARNING); + return; } } - if ($this->def->isError($value)) { - trigger_error('Value for ' . "$namespace.$key" . ' is of invalid type, should be ' . $type, E_USER_WARNING); - return; - } - $this->conf[$namespace][$key] = $value; - + $this->plist->set($key, $value); + // reset definitions if the directives they depend on changed - // this is a very costly process, so it's discouraged + // this is a very costly process, so it's discouraged // with finalization - if ($namespace == 'HTML' || $namespace == 'CSS') { + if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') { $this->definitions[$namespace] = null; } - + $this->serials[$namespace] = false; } - + /** * Convenience function for error reporting - * @private */ - function _listify($lookup) { + private function _listify($lookup) { $list = array(); foreach ($lookup as $name => $b) $list[] = $name; return implode(', ', $list); } - + /** - * Retrieves reference to the HTML definition. + * Retrieves object reference to the HTML definition. * @param $raw Return a copy that has not been setup yet. Must be * called before it's been setup, otherwise won't work. */ - function &getHTMLDefinition($raw = false) { - $def =& $this->getDefinition('HTML', $raw); - return $def; // prevent PHP 4.4.0 from complaining + public function getHTMLDefinition($raw = false) { + return $this->getDefinition('HTML', $raw); } - + /** - * Retrieves reference to the CSS definition + * Retrieves object reference to the CSS definition + * @param $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. */ - function &getCSSDefinition($raw = false) { - $def =& $this->getDefinition('CSS', $raw); - return $def; + public function getCSSDefinition($raw = false) { + return $this->getDefinition('CSS', $raw); } - + /** * Retrieves a definition * @param $type Type of definition: HTML, CSS, etc * @param $raw Whether or not definition should be returned raw */ - function &getDefinition($type, $raw = false) { - if (!$this->finalized && $this->autoFinalize) $this->finalize(); + public function getDefinition($type, $raw = false) { + if (!$this->finalized) $this->autoFinalize(); + // temporarily suspend locks, so we can handle recursive definition calls + $lock = $this->lock; + $this->lock = null; $factory = HTMLPurifier_DefinitionCacheFactory::instance(); $cache = $factory->create($type, $this); + $this->lock = $lock; if (!$raw) { // see if we can quickly supply a definition if (!empty($this->definitions[$type])) { @@ -317,59 +375,56 @@ class HTMLPurifier_Config } elseif ($type == 'URI') { $this->definitions[$type] = new HTMLPurifier_URIDefinition(); } else { - trigger_error("Definition of $type type not supported"); - $false = false; - return $false; + throw new HTMLPurifier_Exception("Definition of $type type not supported"); } // quick abort if raw if ($raw) { - if (is_null($this->get($type, 'DefinitionID'))) { + if (is_null($this->get($type . '.DefinitionID'))) { // fatally error out if definition ID not set - trigger_error("Cannot retrieve raw version without specifying %$type.DefinitionID", E_USER_ERROR); - $false = new HTMLPurifier_Error(); - return $false; + throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID"); } return $this->definitions[$type]; } // set it up + $this->lock = $type; $this->definitions[$type]->setup($this); + $this->lock = null; // save in cache $cache->set($this->definitions[$type], $this); return $this->definitions[$type]; } - + /** * Loads configuration values from an array with the following structure: * Namespace.Directive => Value * @param $config_array Configuration associative array */ - function loadArray($config_array) { + public function loadArray($config_array) { if ($this->isFinalized('Cannot load directives after finalization')) return; foreach ($config_array as $key => $value) { $key = str_replace('_', '.', $key); if (strpos($key, '.') !== false) { - // condensed form - list($namespace, $directive) = explode('.', $key); - $this->set($namespace, $directive, $value); + $this->set($key, $value); } else { $namespace = $key; $namespace_values = $value; foreach ($namespace_values as $directive => $value) { - $this->set($namespace, $directive, $value); + $this->set($namespace .'.'. $directive, $value); } } } } - + /** * Returns a list of array(namespace, directive) for all directives * that are allowed in a web-form context as per an allowed * namespaces/directives list. * @param $allowed List of allowed namespaces/directives - * @static */ - function getAllowedDirectivesForForm($allowed) { - $schema = HTMLPurifier_ConfigSchema::instance(); + public static function getAllowedDirectivesForForm($allowed, $schema = null) { + if (!$schema) { + $schema = HTMLPurifier_ConfigSchema::instance(); + } if ($allowed !== true) { if (is_string($allowed)) $allowed = array($allowed); $allowed_ns = array(); @@ -390,54 +445,52 @@ class HTMLPurifier_Config } } $ret = array(); - foreach ($schema->info as $ns => $keypairs) { - foreach ($keypairs as $directive => $def) { - if ($allowed !== true) { - if (isset($blacklisted_directives["$ns.$directive"])) continue; - if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) continue; - } - if ($def->class == 'alias') continue; - if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') continue; - $ret[] = array($ns, $directive); + foreach ($schema->info as $key => $def) { + list($ns, $directive) = explode('.', $key, 2); + if ($allowed !== true) { + if (isset($blacklisted_directives["$ns.$directive"])) continue; + if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) continue; } + if (isset($def->isAlias)) continue; + if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') continue; + $ret[] = array($ns, $directive); } return $ret; } - + /** * Loads configuration values from $_GET/$_POST that were posted * via ConfigForm * @param $array $_GET or $_POST array to import * @param $index Index/name that the config variables are in - * @param $allowed List of allowed namespaces/directives + * @param $allowed List of allowed namespaces/directives * @param $mq_fix Boolean whether or not to enable magic quotes fix - * @static + * @param $schema Instance of HTMLPurifier_ConfigSchema to use, if not global copy */ - function loadArrayFromForm($array, $index, $allowed = true, $mq_fix = true) { - $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix); - $config = HTMLPurifier_Config::create($ret); + public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema); + $config = HTMLPurifier_Config::create($ret, $schema); return $config; } - + /** * Merges in configuration values from $_GET/$_POST to object. NOT STATIC. * @note Same parameters as loadArrayFromForm */ - function mergeArrayFromForm($array, $index, $allowed = true, $mq_fix = true) { - $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix); + public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def); $this->loadArray($ret); } - + /** * Prepares an array from a form into something usable for the more * strict parts of HTMLPurifier_Config - * @static */ - function prepareArrayFromForm($array, $index, $allowed = true, $mq_fix = true) { - $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); - $mq = get_magic_quotes_gpc() && $mq_fix; - - $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed); + public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) { + if ($index !== false) $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); + $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc(); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema); $ret = array(); foreach ($allowed as $key) { list($ns, $directive) = $key; @@ -452,43 +505,76 @@ class HTMLPurifier_Config } return $ret; } - + /** * Loads configuration values from an ini file * @param $filename Name of ini file */ - function loadIni($filename) { + public function loadIni($filename) { if ($this->isFinalized('Cannot load directives after finalization')) return; $array = parse_ini_file($filename, true); $this->loadArray($array); } - + /** * Checks whether or not the configuration object is finalized. * @param $error String error message, or false for no error */ - function isFinalized($error = false) { + public function isFinalized($error = false) { if ($this->finalized && $error) { - trigger_error($error, E_USER_ERROR); + $this->triggerError($error, E_USER_ERROR); } return $this->finalized; } - + /** * Finalizes configuration only if auto finalize is on and not * already finalized */ - function autoFinalize() { - if (!$this->finalized && $this->autoFinalize) $this->finalize(); + public function autoFinalize() { + if ($this->autoFinalize) { + $this->finalize(); + } else { + $this->plist->squash(true); + } } - + /** * Finalizes a configuration object, prohibiting further change */ - function finalize() { + public function finalize() { $this->finalized = true; + unset($this->parser); } - + + /** + * Produces a nicely formatted error message by supplying the + * stack frame information from two levels up and OUTSIDE of + * HTMLPurifier_Config. + */ + protected function triggerError($msg, $no) { + // determine previous stack frame + $backtrace = debug_backtrace(); + if ($this->chatty && isset($backtrace[1])) { + $frame = $backtrace[1]; + $extra = " on line {$frame['line']} in file {$frame['file']}"; + } else { + $extra = ''; + } + trigger_error($msg . $extra, $no); + } + + /** + * Returns a serialized form of the configuration object that can + * be reconstituted. + */ + public function serialize() { + $this->getDefinition('HTML'); + $this->getDefinition('CSS'); + $this->getDefinition('URI'); + return serialize($this); + } + } - +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigDef.php b/lib/htmlpurifier/HTMLPurifier/ConfigDef.php deleted file mode 100644 index 21825e01b8c..00000000000 --- a/lib/htmlpurifier/HTMLPurifier/ConfigDef.php +++ /dev/null @@ -1,9 +0,0 @@ -type = $type; - if ($descriptions !== null) $this->descriptions = $descriptions; - if ( $allow_null !== null) $this->allow_null = $allow_null; - if ( $allowed !== null) $this->allowed = $allowed; - if ( $aliases !== null) $this->aliases = $aliases; - } - - /** - * Allowed type of the directive. Values are: - * - string - * - istring (case insensitive string) - * - int - * - float - * - bool - * - lookup (array of value => true) - * - list (regular numbered index array) - * - hash (array of key => value) - * - mixed (anything goes) - */ - var $type = 'mixed'; - - /** - * Plaintext descriptions of the configuration entity is. Organized by - * file and line number, so multiple descriptions are allowed. - */ - var $descriptions = array(); - - /** - * Is null allowed? Has no effect for mixed type. - * @bool - */ - var $allow_null = false; - - /** - * Lookup table of allowed values of the element, bool true if all allowed. - */ - var $allowed = true; - - /** - * Hash of value aliases, i.e. values that are equivalent. - */ - var $aliases = array(); - - /** - * Advisory list of directive aliases, i.e. other directives that - * redirect here - */ - var $directiveAliases = array(); - - /** - * Adds a description to the array - */ - function addDescription($file, $line, $description) { - if (!isset($this->descriptions[$file])) $this->descriptions[$file] = array(); - $this->descriptions[$file][$line] = $description; - } - -} - diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigDef/DirectiveAlias.php b/lib/htmlpurifier/HTMLPurifier/ConfigDef/DirectiveAlias.php deleted file mode 100644 index 66378026214..00000000000 --- a/lib/htmlpurifier/HTMLPurifier/ConfigDef/DirectiveAlias.php +++ /dev/null @@ -1,26 +0,0 @@ -namespace = $namespace; - $this->name = $name; - } -} - diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigDef/Namespace.php b/lib/htmlpurifier/HTMLPurifier/ConfigDef/Namespace.php deleted file mode 100644 index 21d732114fd..00000000000 --- a/lib/htmlpurifier/HTMLPurifier/ConfigDef/Namespace.php +++ /dev/null @@ -1,22 +0,0 @@ -description = $description; - } - - var $class = 'namespace'; - - /** - * String description of what kinds of directives go in this namespace. - */ - var $description; - -} - diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema.php index d6700e6ec1b..67be5c71fd8 100644 --- a/lib/htmlpurifier/HTMLPurifier/ConfigSchema.php +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema.php @@ -1,436 +1,158 @@ array( + * 'Directive' => new stdclass(), + * ) + * ) + * + * The stdclass may have the following properties: + * + * - If isAlias isn't set: + * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions + * - allow_null: If set, this directive allows null values + * - aliases: If set, an associative array of value aliases to real values + * - allowed: If set, a lookup array of allowed (string) values + * - If isAlias is set: + * - namespace: Namespace this directive aliases to + * - name: Directive name this directive aliases to + * + * In certain degenerate cases, stdclass will actually be an integer. In + * that case, the value is equivalent to an stdclass with the type + * property set to the integer. If the integer is negative, type is + * equal to the absolute value of integer, and allow_null is true. + * + * This class is friendly with HTMLPurifier_Config. If you need introspection + * about the schema, you're better of using the ConfigSchema_Interchange, + * which uses more memory but has much richer information. */ - var $info_namespace = array(); - + public $info = array(); + /** - * Lookup table of allowed types. + * Application-wide singleton */ - var $types = array( - 'string' => 'String', - 'istring' => 'Case-insensitive string', - 'text' => 'Text', - 'itext' => 'Case-insensitive text', - 'int' => 'Integer', - 'float' => 'Float', - 'bool' => 'Boolean', - 'lookup' => 'Lookup array', - 'list' => 'Array list', - 'hash' => 'Associative array', - 'mixed' => 'Mixed' - ); - - /** - * Initializes the default namespaces. - */ - function initialize() { - $this->defineNamespace('Core', 'Core features that are always available.'); - $this->defineNamespace('Attr', 'Features regarding attribute validation.'); - $this->defineNamespace('URI', 'Features regarding Uniform Resource Identifiers.'); - $this->defineNamespace('HTML', 'Configuration regarding allowed HTML.'); - $this->defineNamespace('CSS', 'Configuration regarding allowed CSS.'); - $this->defineNamespace('AutoFormat', 'Configuration for activating auto-formatting functionality (also known as Injectors)'); - $this->defineNamespace('AutoFormatParam', 'Configuration for customizing auto-formatting functionality'); - $this->defineNamespace('Output', 'Configuration relating to the generation of (X)HTML.'); - $this->defineNamespace('Cache', 'Configuration for DefinitionCache and related subclasses.'); - $this->defineNamespace('Test', 'Developer testing configuration for our unit tests.'); + static protected $singleton; + + public function __construct() { + $this->defaultPlist = new HTMLPurifier_PropertyList(); } - + + /** + * Unserializes the default ConfigSchema. + */ + public static function makeFromSerial() { + return unserialize(file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser')); + } + /** * Retrieves an instance of the application-wide configuration definition. - * @static */ - function &instance($prototype = null) { - static $instance; + public static function instance($prototype = null) { if ($prototype !== null) { - $instance = $prototype; - } elseif ($instance === null || $prototype === true) { - $instance = new HTMLPurifier_ConfigSchema(); - $instance->initialize(); + HTMLPurifier_ConfigSchema::$singleton = $prototype; + } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) { + HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial(); } - return $instance; + return HTMLPurifier_ConfigSchema::$singleton; } - + /** * Defines a directive for configuration - * @static - * @warning Will fail of directive's namespace is defined + * @warning Will fail of directive's namespace is defined. + * @warning This method's signature is slightly different from the legacy + * define() static method! Beware! * @param $namespace Namespace the directive is in * @param $name Key of directive * @param $default Default value of directive * @param $type Allowed type of the directive. See * HTMLPurifier_DirectiveDef::$type for allowed values - * @param $description Description of directive for documentation + * @param $allow_null Whether or not to allow null values */ - function define($namespace, $name, $default, $type, $description) { - $def =& HTMLPurifier_ConfigSchema::instance(); - - // basic sanity checks - if (HTMLPURIFIER_SCHEMA_STRICT) { - if (!isset($def->info[$namespace])) { - trigger_error('Cannot define directive for undefined namespace', - E_USER_ERROR); - return; - } - if (!ctype_alnum($name)) { - trigger_error('Directive name must be alphanumeric', - E_USER_ERROR); - return; - } - if (empty($description)) { - trigger_error('Description must be non-empty', - E_USER_ERROR); - return; - } - } - - if (isset($def->info[$namespace][$name])) { - // already defined - if ( - $def->info[$namespace][$name]->type !== $type || - $def->defaults[$namespace][$name] !== $default - ) { - trigger_error('Inconsistent default or type, cannot redefine'); - return; - } - } else { - // needs defining - - // process modifiers (OPTIMIZE!) - $type_values = explode('/', $type, 2); - $type = $type_values[0]; - $modifier = isset($type_values[1]) ? $type_values[1] : false; - $allow_null = ($modifier === 'null'); - - if (HTMLPURIFIER_SCHEMA_STRICT) { - if (!isset($def->types[$type])) { - trigger_error('Invalid type for configuration directive', - E_USER_ERROR); - return; - } - $default = $def->validate($default, $type, $allow_null); - if ($def->isError($default)) { - trigger_error('Default value does not match directive type', - E_USER_ERROR); - return; - } - } - - $def->info[$namespace][$name] = - new HTMLPurifier_ConfigDef_Directive(); - $def->info[$namespace][$name]->type = $type; - $def->info[$namespace][$name]->allow_null = $allow_null; - $def->defaults[$namespace][$name] = $default; - } - if (!HTMLPURIFIER_SCHEMA_STRICT) return; - $backtrace = debug_backtrace(); - $file = $def->mungeFilename($backtrace[0]['file']); - $line = $backtrace[0]['line']; - $def->info[$namespace][$name]->addDescription($file,$line,$description); + public function add($key, $default, $type, $allow_null) { + $obj = new stdclass(); + $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; + if ($allow_null) $obj->allow_null = true; + $this->info[$key] = $obj; + $this->defaults[$key] = $default; + $this->defaultPlist->set($key, $default); } - - /** - * Defines a namespace for directives to be put into. - * @static - * @param $namespace Namespace's name - * @param $description Description of the namespace - */ - function defineNamespace($namespace, $description) { - $def =& HTMLPurifier_ConfigSchema::instance(); - if (HTMLPURIFIER_SCHEMA_STRICT) { - if (isset($def->info[$namespace])) { - trigger_error('Cannot redefine namespace', E_USER_ERROR); - return; - } - if (!ctype_alnum($namespace)) { - trigger_error('Namespace name must be alphanumeric', - E_USER_ERROR); - return; - } - if (empty($description)) { - trigger_error('Description must be non-empty', - E_USER_ERROR); - return; - } - } - $def->info[$namespace] = array(); - $def->info_namespace[$namespace] = new HTMLPurifier_ConfigDef_Namespace(); - $def->info_namespace[$namespace]->description = $description; - $def->defaults[$namespace] = array(); - } - + /** * Defines a directive value alias. - * + * * Directive value aliases are convenient for developers because it lets * them set a directive to several values and get the same result. - * @static * @param $namespace Directive's namespace * @param $name Name of Directive - * @param $alias Name of aliased value - * @param $real Value aliased value will be converted into + * @param $aliases Hash of aliased values to the real alias */ - function defineValueAliases($namespace, $name, $aliases) { - $def =& HTMLPurifier_ConfigSchema::instance(); - if (HTMLPURIFIER_SCHEMA_STRICT && !isset($def->info[$namespace][$name])) { - trigger_error('Cannot set value alias for non-existant directive', - E_USER_ERROR); - return; + public function addValueAliases($key, $aliases) { + if (!isset($this->info[$key]->aliases)) { + $this->info[$key]->aliases = array(); } foreach ($aliases as $alias => $real) { - if (HTMLPURIFIER_SCHEMA_STRICT) { - if (!$def->info[$namespace][$name] !== true && - !isset($def->info[$namespace][$name]->allowed[$real]) - ) { - trigger_error('Cannot define alias to value that is not allowed', - E_USER_ERROR); - return; - } - if (isset($def->info[$namespace][$name]->allowed[$alias])) { - trigger_error('Cannot define alias over allowed value', - E_USER_ERROR); - return; - } - } - $def->info[$namespace][$name]->aliases[$alias] = $real; + $this->info[$key]->aliases[$alias] = $real; } } - + /** * Defines a set of allowed values for a directive. - * @static + * @warning This is slightly different from the corresponding static + * method definition. * @param $namespace Namespace of directive * @param $name Name of directive - * @param $allowed_values Arraylist of allowed values + * @param $allowed Lookup array of allowed values */ - function defineAllowedValues($namespace, $name, $allowed_values) { - $def =& HTMLPurifier_ConfigSchema::instance(); - if (HTMLPURIFIER_SCHEMA_STRICT && !isset($def->info[$namespace][$name])) { - trigger_error('Cannot define allowed values for undefined directive', - E_USER_ERROR); - return; - } - $directive =& $def->info[$namespace][$name]; - $type = $directive->type; - if (HTMLPURIFIER_SCHEMA_STRICT && $type != 'string' && $type != 'istring') { - trigger_error('Cannot define allowed values for directive whose type is not string', - E_USER_ERROR); - return; - } - if ($directive->allowed === true) { - $directive->allowed = array(); - } - foreach ($allowed_values as $value) { - $directive->allowed[$value] = true; - } - if ( - HTMLPURIFIER_SCHEMA_STRICT && - $def->defaults[$namespace][$name] !== null && - !isset($directive->allowed[$def->defaults[$namespace][$name]]) - ) { - trigger_error('Default value must be in allowed range of variables', - E_USER_ERROR); - $directive->allowed = true; // undo undo! - return; - } + public function addAllowedValues($key, $allowed) { + $this->info[$key]->allowed = $allowed; } - + /** * Defines a directive alias for backwards compatibility - * @static * @param $namespace * @param $name Directive that will be aliased * @param $new_namespace * @param $new_name Directive that the alias will be to */ - function defineAlias($namespace, $name, $new_namespace, $new_name) { - $def =& HTMLPurifier_ConfigSchema::instance(); - if (HTMLPURIFIER_SCHEMA_STRICT) { - if (!isset($def->info[$namespace])) { - trigger_error('Cannot define directive alias in undefined namespace', - E_USER_ERROR); - return; - } - if (!ctype_alnum($name)) { - trigger_error('Directive name must be alphanumeric', - E_USER_ERROR); - return; - } - if (isset($def->info[$namespace][$name])) { - trigger_error('Cannot define alias over directive', - E_USER_ERROR); - return; - } - if (!isset($def->info[$new_namespace][$new_name])) { - trigger_error('Cannot define alias to undefined directive', - E_USER_ERROR); - return; - } - if ($def->info[$new_namespace][$new_name]->class == 'alias') { - trigger_error('Cannot define alias to alias', - E_USER_ERROR); - return; + public function addAlias($key, $new_key) { + $obj = new stdclass; + $obj->key = $new_key; + $obj->isAlias = true; + $this->info[$key] = $obj; + } + + /** + * Replaces any stdclass that only has the type property with type integer. + */ + public function postProcess() { + foreach ($this->info as $key => $v) { + if (count((array) $v) == 1) { + $this->info[$key] = $v->type; + } elseif (count((array) $v) == 2 && isset($v->allow_null)) { + $this->info[$key] = -$v->type; } } - $def->info[$namespace][$name] = - new HTMLPurifier_ConfigDef_DirectiveAlias( - $new_namespace, $new_name); - $def->info[$new_namespace][$new_name]->directiveAliases[] = "$namespace.$name"; - } - - /** - * Validate a variable according to type. Return null if invalid. - */ - function validate($var, $type, $allow_null = false) { - if (!isset($this->types[$type])) { - trigger_error('Invalid type', E_USER_ERROR); - return; - } - if ($allow_null && $var === null) return null; - switch ($type) { - case 'mixed': - //if (is_string($var)) $var = unserialize($var); - return $var; - case 'istring': - case 'string': - case 'text': // no difference, just is longer/multiple line string - case 'itext': - if (!is_string($var)) break; - if ($type === 'istring' || $type === 'itext') $var = strtolower($var); - return $var; - case 'int': - if (is_string($var) && ctype_digit($var)) $var = (int) $var; - elseif (!is_int($var)) break; - return $var; - case 'float': - if (is_string($var) && is_numeric($var)) $var = (float) $var; - elseif (!is_float($var)) break; - return $var; - case 'bool': - if (is_int($var) && ($var === 0 || $var === 1)) { - $var = (bool) $var; - } elseif (is_string($var)) { - if ($var == 'on' || $var == 'true' || $var == '1') { - $var = true; - } elseif ($var == 'off' || $var == 'false' || $var == '0') { - $var = false; - } else { - break; - } - } elseif (!is_bool($var)) break; - return $var; - case 'list': - case 'hash': - case 'lookup': - if (is_string($var)) { - // special case: technically, this is an array with - // a single empty string item, but having an empty - // array is more intuitive - if ($var == '') return array(); - if (strpos($var, "\n") === false && strpos($var, "\r") === false) { - // simplistic string to array method that only works - // for simple lists of tag names or alphanumeric characters - $var = explode(',',$var); - } else { - $var = preg_split('/(,|[\n\r]+)/', $var); - } - // remove spaces - foreach ($var as $i => $j) $var[$i] = trim($j); - if ($type === 'hash') { - // key:value,key2:value2 - $nvar = array(); - foreach ($var as $keypair) { - $c = explode(':', $keypair, 2); - if (!isset($c[1])) continue; - $nvar[$c[0]] = $c[1]; - } - $var = $nvar; - } - } - if (!is_array($var)) break; - $keys = array_keys($var); - if ($keys === array_keys($keys)) { - if ($type == 'list') return $var; - elseif ($type == 'lookup') { - $new = array(); - foreach ($var as $key) { - $new[$key] = true; - } - return $new; - } else break; - } - if ($type === 'lookup') { - foreach ($var as $key => $value) { - $var[$key] = true; - } - } - return $var; - } - $error = new HTMLPurifier_Error(); - return $error; - } - - /** - * Takes an absolute path and munges it into a more manageable relative path - */ - function mungeFilename($filename) { - if (!HTMLPURIFIER_SCHEMA_STRICT) return $filename; - $offset = strrpos($filename, 'HTMLPurifier'); - $filename = substr($filename, $offset); - $filename = str_replace('\\', '/', $filename); - return $filename; - } - - /** - * Checks if var is an HTMLPurifier_Error object - */ - function isError($var) { - if (!is_object($var)) return false; - if (!is_a($var, 'HTMLPurifier_Error')) return false; - return true; } + } - +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php new file mode 100644 index 00000000000..c05668a7060 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php @@ -0,0 +1,44 @@ +directives as $d) { + $schema->add( + $d->id->key, + $d->default, + $d->type, + $d->typeAllowsNull + ); + if ($d->allowed !== null) { + $schema->addAllowedValues( + $d->id->key, + $d->allowed + ); + } + foreach ($d->aliases as $alias) { + $schema->addAlias( + $alias->key, + $d->id->key + ); + } + if ($d->valueAliases !== null) { + $schema->addValueAliases( + $d->id->key, + $d->valueAliases + ); + } + } + $schema->postProcess(); + return $schema; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php new file mode 100644 index 00000000000..244561a3726 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php @@ -0,0 +1,106 @@ +startElement('div'); + + $purifier = HTMLPurifier::getInstance(); + $html = $purifier->purify($html); + $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $this->writeRaw($html); + + $this->endElement(); // div + } + + protected function export($var) { + if ($var === array()) return 'array()'; + return var_export($var, true); + } + + public function build($interchange) { + // global access, only use as last resort + $this->interchange = $interchange; + + $this->setIndent(true); + $this->startDocument('1.0', 'UTF-8'); + $this->startElement('configdoc'); + $this->writeElement('title', $interchange->name); + + foreach ($interchange->directives as $directive) { + $this->buildDirective($directive); + } + + if ($this->namespace) $this->endElement(); // namespace + + $this->endElement(); // configdoc + $this->flush(); + } + + public function buildDirective($directive) { + + // Kludge, although I suppose having a notion of a "root namespace" + // certainly makes things look nicer when documentation is built. + // Depends on things being sorted. + if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { + if ($this->namespace) $this->endElement(); // namespace + $this->namespace = $directive->id->getRootNamespace(); + $this->startElement('namespace'); + $this->writeAttribute('id', $this->namespace); + $this->writeElement('name', $this->namespace); + } + + $this->startElement('directive'); + $this->writeAttribute('id', $directive->id->toString()); + + $this->writeElement('name', $directive->id->getDirective()); + + $this->startElement('aliases'); + foreach ($directive->aliases as $alias) $this->writeElement('alias', $alias->toString()); + $this->endElement(); // aliases + + $this->startElement('constraints'); + if ($directive->version) $this->writeElement('version', $directive->version); + $this->startElement('type'); + if ($directive->typeAllowsNull) $this->writeAttribute('allow-null', 'yes'); + $this->text($directive->type); + $this->endElement(); // type + if ($directive->allowed) { + $this->startElement('allowed'); + foreach ($directive->allowed as $value => $x) $this->writeElement('value', $value); + $this->endElement(); // allowed + } + $this->writeElement('default', $this->export($directive->default)); + $this->writeAttribute('xml:space', 'preserve'); + if ($directive->external) { + $this->startElement('external'); + foreach ($directive->external as $project) $this->writeElement('project', $project); + $this->endElement(); + } + $this->endElement(); // constraints + + if ($directive->deprecatedVersion) { + $this->startElement('deprecated'); + $this->writeElement('version', $directive->deprecatedVersion); + $this->writeElement('use', $directive->deprecatedUse->toString()); + $this->endElement(); // deprecated + } + + $this->startElement('description'); + $this->writeHTMLDiv($directive->description); + $this->endElement(); // description + + $this->endElement(); // directive + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Exception.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Exception.php new file mode 100644 index 00000000000..2671516c588 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Exception.php @@ -0,0 +1,11 @@ + array(directive info) + */ + public $directives = array(); + + /** + * Adds a directive array to $directives + */ + public function addDirective($directive) { + if (isset($this->directives[$i = $directive->id->toString()])) { + throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); + } + $this->directives[$i] = $directive; + } + + /** + * Convenience function to perform standard validation. Throws exception + * on failed validation. + */ + public function validate() { + $validator = new HTMLPurifier_ConfigSchema_Validator(); + return $validator->validate($this); + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php new file mode 100644 index 00000000000..ac8be0d9707 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php @@ -0,0 +1,77 @@ + true). + * Null if all values are allowed. + */ + public $allowed; + + /** + * List of aliases for the directive, + * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). + */ + public $aliases = array(); + + /** + * Hash of value aliases, e.g. array('alt' => 'real'). Null if value + * aliasing is disabled (necessary for non-scalar types). + */ + public $valueAliases; + + /** + * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. + * Null if the directive has always existed. + */ + public $version; + + /** + * ID of directive that supercedes this old directive, is an instance + * of HTMLPurifier_ConfigSchema_Interchange_Id. Null if not deprecated. + */ + public $deprecatedUse; + + /** + * Version of HTML Purifier this directive was deprecated. Null if not + * deprecated. + */ + public $deprecatedVersion; + + /** + * List of external projects this directive depends on, e.g. array('CSSTidy'). + */ + public $external = array(); + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php new file mode 100644 index 00000000000..b9b3c6f5cfb --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php @@ -0,0 +1,37 @@ +key = $key; + } + + /** + * @warning This is NOT magic, to ensure that people don't abuse SPL and + * cause problems for PHP 5.0 support. + */ + public function toString() { + return $this->key; + } + + public function getRootNamespace() { + return substr($this->key, 0, strpos($this->key, ".")); + } + + public function getDirective() { + return substr($this->key, strpos($this->key, ".") + 1); + } + + public static function make($id) { + return new HTMLPurifier_ConfigSchema_Interchange_Id($id); + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php new file mode 100644 index 00000000000..785b72ce8e9 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php @@ -0,0 +1,180 @@ +varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); + } + + public static function buildFromDirectory($dir = null) { + $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); + $interchange = new HTMLPurifier_ConfigSchema_Interchange(); + return $builder->buildDir($interchange, $dir); + } + + public function buildDir($interchange, $dir = null) { + if (!$dir) $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; + if (file_exists($dir . '/info.ini')) { + $info = parse_ini_file($dir . '/info.ini'); + $interchange->name = $info['name']; + } + + $files = array(); + $dh = opendir($dir); + while (false !== ($file = readdir($dh))) { + if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { + continue; + } + $files[] = $file; + } + closedir($dh); + + sort($files); + foreach ($files as $file) { + $this->buildFile($interchange, $dir . '/' . $file); + } + + return $interchange; + } + + public function buildFile($interchange, $file) { + $parser = new HTMLPurifier_StringHashParser(); + $this->build( + $interchange, + new HTMLPurifier_StringHash( $parser->parseFile($file) ) + ); + } + + /** + * Builds an interchange object based on a hash. + * @param $interchange HTMLPurifier_ConfigSchema_Interchange object to build + * @param $hash HTMLPurifier_ConfigSchema_StringHash source data + */ + public function build($interchange, $hash) { + if (!$hash instanceof HTMLPurifier_StringHash) { + $hash = new HTMLPurifier_StringHash($hash); + } + if (!isset($hash['ID'])) { + throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); + } + if (strpos($hash['ID'], '.') === false) { + if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { + $hash->offsetGet('DESCRIPTION'); // prevent complaining + } else { + throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); + } + } else { + $this->buildDirective($interchange, $hash); + } + $this->_findUnused($hash); + } + + public function buildDirective($interchange, $hash) { + $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); + + // These are required elements: + $directive->id = $this->id($hash->offsetGet('ID')); + $id = $directive->id->toString(); // convenience + + if (isset($hash['TYPE'])) { + $type = explode('/', $hash->offsetGet('TYPE')); + if (isset($type[1])) $directive->typeAllowsNull = true; + $directive->type = $type[0]; + } else { + throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); + } + + if (isset($hash['DEFAULT'])) { + try { + $directive->default = $this->varParser->parse($hash->offsetGet('DEFAULT'), $directive->type, $directive->typeAllowsNull); + } catch (HTMLPurifier_VarParserException $e) { + throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); + } + } + + if (isset($hash['DESCRIPTION'])) { + $directive->description = $hash->offsetGet('DESCRIPTION'); + } + + if (isset($hash['ALLOWED'])) { + $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); + } + + if (isset($hash['VALUE-ALIASES'])) { + $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); + } + + if (isset($hash['ALIASES'])) { + $raw_aliases = trim($hash->offsetGet('ALIASES')); + $aliases = preg_split('/\s*,\s*/', $raw_aliases); + foreach ($aliases as $alias) { + $directive->aliases[] = $this->id($alias); + } + } + + if (isset($hash['VERSION'])) { + $directive->version = $hash->offsetGet('VERSION'); + } + + if (isset($hash['DEPRECATED-USE'])) { + $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); + } + + if (isset($hash['DEPRECATED-VERSION'])) { + $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); + } + + if (isset($hash['EXTERNAL'])) { + $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); + } + + $interchange->addDirective($directive); + } + + /** + * Evaluates an array PHP code string without array() wrapper + */ + protected function evalArray($contents) { + return eval('return array('. $contents .');'); + } + + /** + * Converts an array list into a lookup array. + */ + protected function lookup($array) { + $ret = array(); + foreach ($array as $val) $ret[$val] = true; + return $ret; + } + + /** + * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id + * object based on a string Id. + */ + protected function id($id) { + return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); + } + + /** + * Triggers errors for any unused keys passed in the hash; such keys + * may indicate typos, missing values, etc. + * @param $hash Instance of ConfigSchema_StringHash to check. + */ + protected function _findUnused($hash) { + $accessed = $hash->getAccessed(); + foreach ($hash as $k => $v) { + if (!isset($accessed[$k])) { + trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); + } + } + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Validator.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Validator.php new file mode 100644 index 00000000000..f374f6a0225 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/Validator.php @@ -0,0 +1,206 @@ +parser = new HTMLPurifier_VarParser(); + } + + /** + * Validates a fully-formed interchange object. Throws an + * HTMLPurifier_ConfigSchema_Exception if there's a problem. + */ + public function validate($interchange) { + $this->interchange = $interchange; + $this->aliases = array(); + // PHP is a bit lax with integer <=> string conversions in + // arrays, so we don't use the identical !== comparison + foreach ($interchange->directives as $i => $directive) { + $id = $directive->id->toString(); + if ($i != $id) $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); + $this->validateDirective($directive); + } + return true; + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. + */ + public function validateId($id) { + $id_string = $id->toString(); + $this->context[] = "id '$id_string'"; + if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { + // handled by InterchangeBuilder + $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); + } + // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) + // we probably should check that it has at least one namespace + $this->with($id, 'key') + ->assertNotEmpty() + ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder + array_pop($this->context); + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. + */ + public function validateDirective($d) { + $id = $d->id->toString(); + $this->context[] = "directive '$id'"; + $this->validateId($d->id); + + $this->with($d, 'description') + ->assertNotEmpty(); + + // BEGIN - handled by InterchangeBuilder + $this->with($d, 'type') + ->assertNotEmpty(); + $this->with($d, 'typeAllowsNull') + ->assertIsBool(); + try { + // This also tests validity of $d->type + $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); + } catch (HTMLPurifier_VarParserException $e) { + $this->error('default', 'had error: ' . $e->getMessage()); + } + // END - handled by InterchangeBuilder + + if (!is_null($d->allowed) || !empty($d->valueAliases)) { + // allowed and valueAliases require that we be dealing with + // strings, so check for that early. + $d_int = HTMLPurifier_VarParser::$types[$d->type]; + if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { + $this->error('type', 'must be a string type when used with allowed or value aliases'); + } + } + + $this->validateDirectiveAllowed($d); + $this->validateDirectiveValueAliases($d); + $this->validateDirectiveAliases($d); + + array_pop($this->context); + } + + /** + * Extra validation if $allowed member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + */ + public function validateDirectiveAllowed($d) { + if (is_null($d->allowed)) return; + $this->with($d, 'allowed') + ->assertNotEmpty() + ->assertIsLookup(); // handled by InterchangeBuilder + if (is_string($d->default) && !isset($d->allowed[$d->default])) { + $this->error('default', 'must be an allowed value'); + } + $this->context[] = 'allowed'; + foreach ($d->allowed as $val => $x) { + if (!is_string($val)) $this->error("value $val", 'must be a string'); + } + array_pop($this->context); + } + + /** + * Extra validation if $valueAliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + */ + public function validateDirectiveValueAliases($d) { + if (is_null($d->valueAliases)) return; + $this->with($d, 'valueAliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'valueAliases'; + foreach ($d->valueAliases as $alias => $real) { + if (!is_string($alias)) $this->error("alias $alias", 'must be a string'); + if (!is_string($real)) $this->error("alias target $real from alias '$alias'", 'must be a string'); + if ($alias === $real) { + $this->error("alias '$alias'", "must not be an alias to itself"); + } + } + if (!is_null($d->allowed)) { + foreach ($d->valueAliases as $alias => $real) { + if (isset($d->allowed[$alias])) { + $this->error("alias '$alias'", 'must not be an allowed value'); + } elseif (!isset($d->allowed[$real])) { + $this->error("alias '$alias'", 'must be an alias to an allowed value'); + } + } + } + array_pop($this->context); + } + + /** + * Extra validation if $aliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + */ + public function validateDirectiveAliases($d) { + $this->with($d, 'aliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'aliases'; + foreach ($d->aliases as $alias) { + $this->validateId($alias); + $s = $alias->toString(); + if (isset($this->interchange->directives[$s])) { + $this->error("alias '$s'", 'collides with another directive'); + } + if (isset($this->aliases[$s])) { + $other_directive = $this->aliases[$s]; + $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); + } + $this->aliases[$s] = $d->id->toString(); + } + array_pop($this->context); + } + + // protected helper functions + + /** + * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom + * for validating simple member variables of objects. + */ + protected function with($obj, $member) { + return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); + } + + /** + * Emits an error, providing helpful context. + */ + protected function error($target, $msg) { + if ($target !== false) $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); + else $prefix = ucfirst($this->getFormattedContext()); + throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); + } + + /** + * Returns a formatted context string. + */ + protected function getFormattedContext() { + return implode(' in ', array_reverse($this->context)); + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php new file mode 100644 index 00000000000..b95aea18cc7 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php @@ -0,0 +1,66 @@ +context = $context; + $this->obj = $obj; + $this->member = $member; + $this->contents =& $obj->$member; + } + + public function assertIsString() { + if (!is_string($this->contents)) $this->error('must be a string'); + return $this; + } + + public function assertIsBool() { + if (!is_bool($this->contents)) $this->error('must be a boolean'); + return $this; + } + + public function assertIsArray() { + if (!is_array($this->contents)) $this->error('must be an array'); + return $this; + } + + public function assertNotNull() { + if ($this->contents === null) $this->error('must not be null'); + return $this; + } + + public function assertAlnum() { + $this->assertIsString(); + if (!ctype_alnum($this->contents)) $this->error('must be alphanumeric'); + return $this; + } + + public function assertNotEmpty() { + if (empty($this->contents)) $this->error('must not be empty'); + return $this; + } + + public function assertIsLookup() { + $this->assertIsArray(); + foreach ($this->contents as $v) { + if ($v !== true) $this->error('must be a lookup array'); + } + return $this; + } + + protected function error($msg) { + throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema.ser b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema.ser new file mode 100644 index 00000000000..22b8d54a59f Binary files /dev/null and b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema.ser differ diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt new file mode 100644 index 00000000000..0517fed0a11 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt @@ -0,0 +1,8 @@ +Attr.AllowedClasses +TYPE: lookup/null +VERSION: 4.0.0 +DEFAULT: null +--DESCRIPTION-- +List of allowed class values in the class attribute. By default, this is null, +which means all classes are allowed. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt new file mode 100644 index 00000000000..249edd647b0 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt @@ -0,0 +1,12 @@ +Attr.AllowedFrameTargets +TYPE: lookup +DEFAULT: array() +--DESCRIPTION-- +Lookup table of all allowed link frame targets. Some commonly used link +targets include _blank, _self, _parent and _top. Values should be +lowercase, as validation will be done in a case-sensitive manner despite +W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute +so this directive will have no effect in that doctype. XHTML 1.1 does not +enable the Target module by default, you will have to manually enable it +(see the module documentation for more details.) +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt new file mode 100644 index 00000000000..9a8fa6a2e20 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt @@ -0,0 +1,9 @@ +Attr.AllowedRel +TYPE: lookup +VERSION: 1.6.0 +DEFAULT: array() +--DESCRIPTION-- +List of allowed forward document relationships in the rel attribute. Common +values may be nofollow or print. By default, this is empty, meaning that no +document relationships are allowed. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt new file mode 100644 index 00000000000..b0178834859 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt @@ -0,0 +1,9 @@ +Attr.AllowedRev +TYPE: lookup +VERSION: 1.6.0 +DEFAULT: array() +--DESCRIPTION-- +List of allowed reverse document relationships in the rev attribute. This +attribute is a bit of an edge-case; if you don't know what it is for, stay +away. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt new file mode 100644 index 00000000000..e774b823b1b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt @@ -0,0 +1,19 @@ +Attr.ClassUseCDATA +TYPE: bool/null +DEFAULT: null +VERSION: 4.0.0 +--DESCRIPTION-- +If null, class will auto-detect the doctype and, if matching XHTML 1.1 or +XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise, +it will use a relaxed CDATA definition. If true, the relaxed CDATA definition +is forced; if false, the NMTOKENS definition is forced. To get behavior +of HTML Purifier prior to 4.0.0, set this directive to false. + +Some rational behind the auto-detection: +in previous versions of HTML Purifier, it was assumed that the form of +class was NMTOKENS, as specified by the XHTML Modularization (representing +XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however +specify class as CDATA. HTML 5 effectively defines it as CDATA, but +with the additional constraint that each name should be unique (this is not +explicitly outlined in previous specifications). +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt new file mode 100644 index 00000000000..533165e1759 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt @@ -0,0 +1,11 @@ +Attr.DefaultImageAlt +TYPE: string/null +DEFAULT: null +VERSION: 3.2.0 +--DESCRIPTION-- +This is the content of the alt tag of an image if the user had not +previously specified an alt attribute. This applies to all images without +a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which +only applies to invalid images, and overrides in the case of an invalid image. +Default behavior with null is to use the basename of the src tag for the alt. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt new file mode 100644 index 00000000000..9eb7e38469f --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt @@ -0,0 +1,9 @@ +Attr.DefaultInvalidImage +TYPE: string +DEFAULT: '' +--DESCRIPTION-- +This is the default image an img tag will be pointed to if it does not have +a valid src attribute. In future versions, we may allow the image tag to +be removed completely, but due to design issues, this is not possible right +now. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt new file mode 100644 index 00000000000..2f17bf477a3 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt @@ -0,0 +1,8 @@ +Attr.DefaultInvalidImageAlt +TYPE: string +DEFAULT: 'Invalid image' +--DESCRIPTION-- +This is the content of the alt tag of an invalid image if the user had not +previously specified an alt attribute. It has no effect when the image is +valid but there was no alt attribute present. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt new file mode 100644 index 00000000000..52654b53ae3 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt @@ -0,0 +1,10 @@ +Attr.DefaultTextDir +TYPE: string +DEFAULT: 'ltr' +--DESCRIPTION-- +Defines the default text direction (ltr or rtl) of the document being +parsed. This generally is the same as the value of the dir attribute in +HTML, or ltr if that is not specified. +--ALLOWED-- +'ltr', 'rtl' +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt new file mode 100644 index 00000000000..6440d210321 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt @@ -0,0 +1,16 @@ +Attr.EnableID +TYPE: bool +DEFAULT: false +VERSION: 1.2.0 +--DESCRIPTION-- +Allows the ID attribute in HTML. This is disabled by default due to the +fact that without proper configuration user input can easily break the +validation of a webpage by specifying an ID that is already on the +surrounding HTML. If you don't mind throwing caution to the wind, enable +this directive, but I strongly recommend you also consider blacklisting IDs +you use (%Attr.IDBlacklist) or prefixing all user supplied IDs +(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of +pre-1.2.0 versions. +--ALIASES-- +HTML.EnableAttrID +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt new file mode 100644 index 00000000000..f31d226f58b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt @@ -0,0 +1,8 @@ +Attr.ForbiddenClasses +TYPE: lookup +VERSION: 4.0.0 +DEFAULT: array() +--DESCRIPTION-- +List of forbidden class values in the class attribute. By default, this is +empty, which means that no classes are forbidden. See also %Attr.AllowedClasses. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt new file mode 100644 index 00000000000..5f2b5e3d2cd --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt @@ -0,0 +1,5 @@ +Attr.IDBlacklist +TYPE: list +DEFAULT: array() +DESCRIPTION: Array of IDs not allowed in the document. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt new file mode 100644 index 00000000000..6f5824586ec --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt @@ -0,0 +1,9 @@ +Attr.IDBlacklistRegexp +TYPE: string/null +VERSION: 1.6.0 +DEFAULT: NULL +--DESCRIPTION-- +PCRE regular expression to be matched against all IDs. If the expression is +matches, the ID is rejected. Use this with care: may cause significant +degradation. ID matching is done after all other validation. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt new file mode 100644 index 00000000000..cc49d43fd0c --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt @@ -0,0 +1,12 @@ +Attr.IDPrefix +TYPE: string +VERSION: 1.2.0 +DEFAULT: '' +--DESCRIPTION-- +String to prefix to IDs. If you have no idea what IDs your pages may use, +you may opt to simply add a prefix to all user-submitted ID attributes so +that they are still usable, but will not conflict with core page IDs. +Example: setting the directive to 'user_' will result in a user submitted +'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true +before using this. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt new file mode 100644 index 00000000000..2c5924a7ade --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt @@ -0,0 +1,14 @@ +Attr.IDPrefixLocal +TYPE: string +VERSION: 1.2.0 +DEFAULT: '' +--DESCRIPTION-- +Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you +need to allow multiple sets of user content on web page, you may need to +have a seperate prefix that changes with each iteration. This way, +seperately submitted user content displayed on the same page doesn't +clobber each other. Ideal values are unique identifiers for the content it +represents (i.e. the id of the row in the database). Be sure to add a +seperator (like an underscore) at the end. Warning: this directive will +not work unless %Attr.IDPrefix is set to a non-empty value! +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt new file mode 100644 index 00000000000..d5caa1bb978 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt @@ -0,0 +1,31 @@ +AutoFormat.AutoParagraph +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ This directive turns on auto-paragraphing, where double newlines are + converted in to paragraphs whenever possible. Auto-paragraphing: +

+
    +
  • Always applies to inline elements or text in the root node,
  • +
  • Applies to inline elements or text with double newlines in nodes + that allow paragraph tags,
  • +
  • Applies to double newlines in paragraph tags
  • +
+

+ p tags must be allowed for this directive to take effect. + We do not use br tags for paragraphing, as that is + semantically incorrect. +

+

+ To prevent auto-paragraphing as a content-producer, refrain from using + double-newlines except to specify a new paragraph or in contexts where + it has special meaning (whitespace usually has no meaning except in + tags like pre, so this should not be difficult.) To prevent + the paragraphing of inline text adjacent to block elements, wrap them + in div tags (the behavior is slightly different outside of + the root node.) +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt new file mode 100644 index 00000000000..2a476481af6 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt @@ -0,0 +1,12 @@ +AutoFormat.Custom +TYPE: list +VERSION: 2.0.1 +DEFAULT: array() +--DESCRIPTION-- + +

+ This directive can be used to add custom auto-format injectors. + Specify an array of injector names (class name minus the prefix) + or concrete implementations. Injector class must exist. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt new file mode 100644 index 00000000000..663064a3447 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt @@ -0,0 +1,11 @@ +AutoFormat.DisplayLinkURI +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ This directive turns on the in-text display of URIs in <a> tags, and disables + those links. For example, example becomes + example (http://example.com). +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt new file mode 100644 index 00000000000..3a48ba960e9 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt @@ -0,0 +1,12 @@ +AutoFormat.Linkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ This directive turns on linkification, auto-linking http, ftp and + https URLs. a tags with the href attribute + must be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt new file mode 100644 index 00000000000..db58b134646 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt @@ -0,0 +1,12 @@ +AutoFormat.PurifierLinkify.DocURL +TYPE: string +VERSION: 2.0.1 +DEFAULT: '#%s' +ALIASES: AutoFormatParam.PurifierLinkifyDocURL +--DESCRIPTION-- +

+ Location of configuration documentation to link to, let %s substitute + into the configuration's namespace and directive names sans the percent + sign. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt new file mode 100644 index 00000000000..7996488be0d --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt @@ -0,0 +1,12 @@ +AutoFormat.PurifierLinkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ Internal auto-formatter that converts configuration directives in + syntax %Namespace.Directive to links. a tags + with the href attribute must be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt new file mode 100644 index 00000000000..35c393b4e63 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt @@ -0,0 +1,11 @@ +AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions +TYPE: lookup +VERSION: 4.0.0 +DEFAULT: array('td' => true, 'th' => true) +--DESCRIPTION-- +

+ When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp + are enabled, this directive defines what HTML elements should not be + removede if they have only a non-breaking space in them. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt new file mode 100644 index 00000000000..ca17eb1dc48 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt @@ -0,0 +1,15 @@ +AutoFormat.RemoveEmpty.RemoveNbsp +TYPE: bool +VERSION: 4.0.0 +DEFAULT: false +--DESCRIPTION-- +

+ When enabled, HTML Purifier will treat any elements that contain only + non-breaking spaces as well as regular whitespace as empty, and remove + them when %AutoForamt.RemoveEmpty is enabled. +

+

+ See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements + that don't have this behavior applied to them. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt new file mode 100644 index 00000000000..34657ba47b1 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt @@ -0,0 +1,46 @@ +AutoFormat.RemoveEmpty +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ When enabled, HTML Purifier will attempt to remove empty elements that + contribute no semantic information to the document. The following types + of nodes will be removed: +

+
  • + Tags with no attributes and no content, and that are not empty + elements (remove <a></a> but not + <br />), and +
  • +
  • + Tags with no content, except for:
      +
    • The colgroup element, or
    • +
    • + Elements with the id or name attribute, + when those attributes are permitted on those elements. +
    • +
  • +
+

+ Please be very careful when using this functionality; while it may not + seem that empty elements contain useful information, they can alter the + layout of a document given appropriate styling. This directive is most + useful when you are processing machine-generated HTML, please avoid using + it on regular user HTML. +

+

+ Elements that contain only whitespace will be treated as empty. Non-breaking + spaces, however, do not count as whitespace. See + %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior. +

+

+ This algorithm is not perfect; you may still notice some empty tags, + particularly if a node had elements, but those elements were later removed + because they were not permitted in that context, or tags that, after + being auto-closed by another tag, where empty. This is for safety reasons + to prevent clever code from breaking validation. The general rule of thumb: + if a tag looked empty on the way in, it will get removed; if HTML Purifier + made it empty, it will stay. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt new file mode 100755 index 00000000000..dde990ab260 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt @@ -0,0 +1,11 @@ +AutoFormat.RemoveSpansWithoutAttributes +TYPE: bool +VERSION: 4.0.1 +DEFAULT: false +--DESCRIPTION-- +

+ This directive causes span tags without any attributes + to be removed. It will also remove spans that had all attributes + removed during processing. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt new file mode 100644 index 00000000000..b324608f761 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt @@ -0,0 +1,8 @@ +CSS.AllowImportant +TYPE: bool +DEFAULT: false +VERSION: 3.1.0 +--DESCRIPTION-- +This parameter determines whether or not !important cascade modifiers should +be allowed in user CSS. If false, !important will stripped. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt new file mode 100644 index 00000000000..748be0eec8b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt @@ -0,0 +1,11 @@ +CSS.AllowTricky +TYPE: bool +DEFAULT: false +VERSION: 3.1.0 +--DESCRIPTION-- +This parameter determines whether or not to allow "tricky" CSS properties and +values. Tricky CSS properties/values can drastically modify page layout or +be used for deceptive practices but do not directly constitute a security risk. +For example, display:none; is considered a tricky property that +will only be allowed if this directive is set to true. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt new file mode 100644 index 00000000000..460112ebe0a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt @@ -0,0 +1,18 @@ +CSS.AllowedProperties +TYPE: lookup/null +VERSION: 3.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If HTML Purifier's style attributes set is unsatisfactory for your needs, + you can overload it with your own list of tags to allow. Note that this + method is subtractive: it does its job by taking away from HTML Purifier + usual feature set, so you cannot add an attribute that HTML Purifier never + supported in the first place. +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt new file mode 100644 index 00000000000..5cb7dda3bae --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt @@ -0,0 +1,11 @@ +CSS.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt new file mode 100644 index 00000000000..7a3291470cc --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt @@ -0,0 +1,16 @@ +CSS.MaxImgLength +TYPE: string/null +DEFAULT: '1200px' +VERSION: 3.1.1 +--DESCRIPTION-- +

+ 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). +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt new file mode 100644 index 00000000000..148eedb8be2 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt @@ -0,0 +1,10 @@ +CSS.Proprietary +TYPE: bool +VERSION: 3.0.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Whether or not to allow safe, proprietary CSS values. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt new file mode 100644 index 00000000000..c486724c88a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt @@ -0,0 +1,14 @@ +Cache.DefinitionImpl +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: 'Serializer' +--DESCRIPTION-- + +This directive defines which method to use when caching definitions, +the complex data-type that makes HTML Purifier tick. Set to null +to disable caching (not recommended, as you will see a definite +performance degradation). + +--ALIASES-- +Core.DefinitionCache +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt new file mode 100644 index 00000000000..54036507d6c --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt @@ -0,0 +1,13 @@ +Cache.SerializerPath +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Absolute path with no trailing slash to store serialized definitions in. + Default is within the + HTML Purifier library inside DefinitionCache/Serializer. This + path must be writable by the webserver. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt new file mode 100644 index 00000000000..568cbf3b328 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt @@ -0,0 +1,18 @@ +Core.AggressivelyFixLt +TYPE: bool +VERSION: 2.1.0 +DEFAULT: true +--DESCRIPTION-- +

+ This directive enables aggressive pre-filter fixes HTML Purifier can + perform in order to ensure that open angled-brackets do not get killed + during parsing stage. Enabling this will result in two preg_replace_callback + calls and at least two preg_replace calls for every HTML document parsed; + if your users make very well-formed HTML, you can set this directive false. + This has no effect when DirectLex is used. +

+

+ Notice: This directive's default turned from false to true + in HTML Purifier 3.2.0. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt new file mode 100644 index 00000000000..d7317911fa3 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt @@ -0,0 +1,12 @@ +Core.CollectErrors +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- + +Whether or not to collect errors found while filtering the document. This +is a useful way to give feedback to your users. Warning: +Currently this feature is very patchy and experimental, with lots of +possible error messages not yet implemented. It will not cause any +problems, but it may not help your users either. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt new file mode 100644 index 00000000000..08b381d34c1 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt @@ -0,0 +1,28 @@ +Core.ColorKeywords +TYPE: hash +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'maroon' => '#800000', + 'red' => '#FF0000', + 'orange' => '#FFA500', + 'yellow' => '#FFFF00', + 'olive' => '#808000', + 'purple' => '#800080', + 'fuchsia' => '#FF00FF', + 'white' => '#FFFFFF', + 'lime' => '#00FF00', + 'green' => '#008000', + 'navy' => '#000080', + 'blue' => '#0000FF', + 'aqua' => '#00FFFF', + 'teal' => '#008080', + 'black' => '#000000', + 'silver' => '#C0C0C0', + 'gray' => '#808080', +) +--DESCRIPTION-- + +Lookup array of color names to six digit hexadecimal number corresponding +to color, with preceding hash mark. Used when parsing colors. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt new file mode 100644 index 00000000000..64b114fce2b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt @@ -0,0 +1,14 @@ +Core.ConvertDocumentToFragment +TYPE: bool +DEFAULT: true +--DESCRIPTION-- + +This parameter determines whether or not the filter should convert +input that is a full document with html and body tags to a fragment +of just the contents of a body tag. This parameter is simply something +HTML Purifier can do during an edge-case: for most inputs, this +processing is not necessary. + +--ALIASES-- +Core.AcceptFullDocuments +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt new file mode 100644 index 00000000000..36f16e07eae --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt @@ -0,0 +1,17 @@ +Core.DirectLexLineNumberSyncInterval +TYPE: int +VERSION: 2.0.0 +DEFAULT: 0 +--DESCRIPTION-- + +

+ Specifies the number of tokens the DirectLex line number tracking + implementations should process before attempting to resyncronize the + current line count by manually counting all previous new-lines. When + at 0, this functionality is disabled. Lower values will decrease + performance, and this is only strictly necessary if the counting + algorithm is buggy (in which case you should report it as a bug). + This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is + not being used. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt new file mode 100644 index 00000000000..8bfb47c3ac1 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt @@ -0,0 +1,15 @@ +Core.Encoding +TYPE: istring +DEFAULT: 'utf-8' +--DESCRIPTION-- +If for some reason you are unable to convert all webpages to UTF-8, you can +use this directive as a stop-gap compatibility change to let HTML Purifier +deal with non UTF-8 input. This technique has notable deficiencies: +absolutely no characters outside of the selected character encoding will be +preserved, not even the ones that have been ampersand escaped (this is due +to a UTF-8 specific feature that automatically resolves all +entities), making it pretty useless for anything except the most I18N-blind +applications, although %Core.EscapeNonASCIICharacters offers fixes this +trouble with another tradeoff. This directive only accepts ISO-8859-1 if +iconv is not enabled. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt new file mode 100644 index 00000000000..4d5b5055cd3 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt @@ -0,0 +1,10 @@ +Core.EscapeInvalidChildren +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When true, a child is found that is not allowed in the context of the +parent element will be transformed into text as if it were ASCII. When +false, that element and all internal tags will be dropped, though text will +be preserved. There is no option for dropping the element but preserving +child nodes. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt new file mode 100644 index 00000000000..a7a5b249bb7 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt @@ -0,0 +1,7 @@ +Core.EscapeInvalidTags +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When true, invalid tags will be written back to the document as plain text. +Otherwise, they are silently dropped. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt new file mode 100644 index 00000000000..abb499948ac --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt @@ -0,0 +1,13 @@ +Core.EscapeNonASCIICharacters +TYPE: bool +VERSION: 1.4.0 +DEFAULT: false +--DESCRIPTION-- +This directive overcomes a deficiency in %Core.Encoding by blindly +converting all non-ASCII characters into decimal numeric entities before +converting it to its native encoding. This means that even characters that +can be expressed in the non-UTF-8 encoding will be entity-ized, which can +be a real downer for encodings like Big5. It also assumes that the ASCII +repetoire is available, although this is the case for almost all encodings. +Anyway, use UTF-8! +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt new file mode 100644 index 00000000000..915391edb73 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt @@ -0,0 +1,19 @@ +Core.HiddenElements +TYPE: lookup +--DEFAULT-- +array ( + 'script' => true, + 'style' => true, +) +--DESCRIPTION-- + +

+ This directive is a lookup array of elements which should have their + contents removed when they are not allowed by the HTML definition. + For example, the contents of a script tag are not + normally shown in a document, so if script tags are to be removed, + their contents should be removed to. This is opposed to a b + tag, which defines some presentational changes but does not hide its + contents. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt new file mode 100644 index 00000000000..233fca14f84 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt @@ -0,0 +1,10 @@ +Core.Language +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'en' +--DESCRIPTION-- + +ISO 639 language code for localizable things in HTML Purifier to use, +which is mainly error reporting. There is currently only an English (en) +translation, so this directive is currently useless. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt new file mode 100644 index 00000000000..8983e2cca9b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt @@ -0,0 +1,34 @@ +Core.LexerImpl +TYPE: mixed/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ This parameter determines what lexer implementation can be used. The + valid values are: +

+
+
null
+
+ Recommended, the lexer implementation will be auto-detected based on + your PHP-version and configuration. +
+
string lexer identifier
+
+ This is a slim way of manually overridding the implementation. + Currently recognized values are: DOMLex (the default PHP5 +implementation) + and DirectLex (the default PHP4 implementation). Only use this if + you know what you are doing: usually, the auto-detection will + manage things for cases you aren't even aware of. +
+
object lexer instance
+
+ Super-advanced: you can specify your own, custom, implementation that + implements the interface defined by HTMLPurifier_Lexer. + I may remove this option simply because I don't expect anyone + to use it. +
+
+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt new file mode 100644 index 00000000000..eb841a7597a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt @@ -0,0 +1,16 @@ +Core.MaintainLineNumbers +TYPE: bool/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If true, HTML Purifier will add line number information to all tokens. + This is useful when error reporting is turned on, but can result in + significant performance degradation and should not be used when + unnecessary. This directive must be used with the DirectLex lexer, + as the DOMLex lexer does not (yet) support this functionality. + If the value is null, an appropriate value will be selected based + on other configuration. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt new file mode 100644 index 00000000000..4070c2a0de2 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt @@ -0,0 +1,12 @@ +Core.RemoveInvalidImg +TYPE: bool +DEFAULT: true +VERSION: 1.3.0 +--DESCRIPTION-- + +

+ This directive enables pre-emptive URI checking in img + tags, as the attribute validation strategy is not authorized to + remove elements from the document. Revert to pre-1.3.0 behavior by setting to false. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt new file mode 100644 index 00000000000..a4cd966df89 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt @@ -0,0 +1,12 @@ +Core.RemoveScriptContents +TYPE: bool/null +DEFAULT: NULL +VERSION: 2.0.0 +DEPRECATED-VERSION: 2.1.0 +DEPRECATED-USE: Core.HiddenElements +--DESCRIPTION-- +

+ This directive enables HTML Purifier to remove not only script tags + but all of their contents. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt new file mode 100644 index 00000000000..3db50ef204b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt @@ -0,0 +1,11 @@ +Filter.Custom +TYPE: list +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ This directive can be used to add custom filters; it is nearly the + equivalent of the now deprecated HTMLPurifier->addFilter() + method. Specify an array of concrete implementations. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt new file mode 100644 index 00000000000..16829bcda03 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt @@ -0,0 +1,14 @@ +Filter.ExtractStyleBlocks.Escaping +TYPE: bool +VERSION: 3.0.0 +DEFAULT: true +ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping +--DESCRIPTION-- + +

+ Whether or not to escape the dangerous characters <, > and & + as \3C, \3E and \26, respectively. This is can be safely set to false + if the contents of StyleBlocks will be placed in an external stylesheet, + where there is no risk of it being interpreted as HTML. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt new file mode 100644 index 00000000000..7f95f54d125 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt @@ -0,0 +1,29 @@ +Filter.ExtractStyleBlocks.Scope +TYPE: string/null +VERSION: 3.0.0 +DEFAULT: NULL +ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope +--DESCRIPTION-- + +

+ If you would like users to be able to define external stylesheets, but + only allow them to specify CSS declarations for a specific node and + prevent them from fiddling with other elements, use this directive. + It accepts any valid CSS selector, and will prepend this to any + CSS declaration extracted from the document. For example, if this + directive is set to #user-content and a user uses the + selector a:hover, the final selector will be + #user-content a:hover. +

+

+ The comma shorthand may be used; consider the above example, with + #user-content, #user-content2, the final selector will + be #user-content a:hover, #user-content2 a:hover. +

+

+ Warning: It is possible for users to bypass this measure + using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML + Purifier, and I am working to get it fixed. Until then, HTML Purifier + performs a basic check to prevent this. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt new file mode 100644 index 00000000000..6c231b2d7f7 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt @@ -0,0 +1,16 @@ +Filter.ExtractStyleBlocks.TidyImpl +TYPE: mixed/null +VERSION: 3.1.0 +DEFAULT: NULL +ALIASES: FilterParam.ExtractStyleBlocksTidyImpl +--DESCRIPTION-- +

+ If left NULL, HTML Purifier will attempt to instantiate a csstidy + class to use for internal cleaning. This will usually be good enough. +

+

+ However, for trusted user input, you can set this to false to + disable cleaning. In addition, you can supply your own concrete implementation + of Tidy's interface to use, although I don't know why you'd want to do that. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt new file mode 100644 index 00000000000..078d087417e --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt @@ -0,0 +1,74 @@ +Filter.ExtractStyleBlocks +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +EXTERNAL: CSSTidy +--DESCRIPTION-- +

+ This directive turns on the style block extraction filter, which removes + style blocks from input HTML, cleans them up with CSSTidy, + and places them in the StyleBlocks context variable, for further + use by you, usually to be placed in an external stylesheet, or a + style block in the head of your document. +

+

+ Sample usage: +

+
';
+?>
+
+
+
+  Filter.ExtractStyleBlocks
+body {color:#F00;} Some text';
+
+    $config = HTMLPurifier_Config::createDefault();
+    $config->set('Filter', 'ExtractStyleBlocks', true);
+    $purifier = new HTMLPurifier($config);
+
+    $html = $purifier->purify($dirty);
+
+    // This implementation writes the stylesheets to the styles/ directory.
+    // You can also echo the styles inside the document, but it's a bit
+    // more difficult to make sure they get interpreted properly by
+    // browsers; try the usual CSS armoring techniques.
+    $styles = $purifier->context->get('StyleBlocks');
+    $dir = 'styles/';
+    if (!is_dir($dir)) mkdir($dir);
+    $hash = sha1($_GET['html']);
+    foreach ($styles as $i => $style) {
+        file_put_contents($name = $dir . $hash . "_$i");
+        echo '';
+    }
+?>
+
+
+  
+ +
+ + +]]>
+

+ Warning: It is possible for a user to mount an + imagecrash attack using this CSS. Counter-measures are difficult; + it is not simply enough to limit the range of CSS lengths (using + relative lengths with many nesting levels allows for large values + to be attained without actually specifying them in the stylesheet), + and the flexible nature of selectors makes it difficult to selectively + disable lengths on image tags (HTML Purifier, however, does disable + CSS width and height in inline styling). There are probably two effective + counter measures: an explicit width and height set to auto in all + images in your document (unlikely) or the disabling of width and + height (somewhat reasonable). Whether or not these measures should be + used is left to the reader. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt new file mode 100644 index 00000000000..7fa6536b2c7 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt @@ -0,0 +1,11 @@ +Filter.YouTube +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ This directive enables YouTube video embedding in HTML Purifier. Check + this document + on embedding videos for more information on what this filter does. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt new file mode 100644 index 00000000000..3e231d2d16a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt @@ -0,0 +1,22 @@ +HTML.Allowed +TYPE: itext/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ This is a convenience directive that rolls the functionality of + %HTML.AllowedElements and %HTML.AllowedAttributes into one directive. + Specify elements and attributes that are allowed using: + element1[attr1|attr2],element2.... You can also use + newlines instead of commas to separate elements. +

+

+ Warning: + All of the constraints on the component directives are still enforced. + The syntax is a subset of TinyMCE's valid_elements + whitelist: directly copy-pasting it here will probably result in + broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes + are set, this directive has no effect. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt new file mode 100644 index 00000000000..fcf093f17d1 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt @@ -0,0 +1,19 @@ +HTML.AllowedAttributes +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If HTML Purifier's attribute set is unsatisfactory, overload it! + The syntax is "tag.attr" or "*.attr" for the global attributes + (style, id, class, dir, lang, xml:lang). +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. For + example, %HTML.EnableAttrID will take precedence over *.id in this + directive. You must set that directive to true before you can use + IDs at all. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt new file mode 100644 index 00000000000..888d5581969 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt @@ -0,0 +1,18 @@ +HTML.AllowedElements +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

+ If HTML Purifier's tag set is unsatisfactory for your needs, you + can overload it with your own list of tags to allow. Note that this + method is subtractive: it does its job by taking away from HTML Purifier + usual feature set, so you cannot add a tag that HTML Purifier never + supported in the first place (like embed, form or head). If you + change this, you probably also want to change %HTML.AllowedAttributes. +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt new file mode 100644 index 00000000000..5a59a55c083 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt @@ -0,0 +1,20 @@ +HTML.AllowedModules +TYPE: lookup/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ A doctype comes with a set of usual modules to use. Without having + to mucking about with the doctypes, you can quickly activate or + disable these modules by specifying which modules you wish to allow + with this directive. This is most useful for unit testing specific + modules, although end users may find it useful for their own ends. +

+

+ If you specify a module that does not exist, the manager will silently + fail to use it, so be careful! User-defined modules are not affected + by this directive. Modules defined in %HTML.CoreModules are not + affected by this directive. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt new file mode 100644 index 00000000000..151fb7b8264 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt @@ -0,0 +1,11 @@ +HTML.Attr.Name.UseCDATA +TYPE: bool +DEFAULT: false +VERSION: 4.0.0 +--DESCRIPTION-- +The W3C specification DTD defines the name attribute to be CDATA, not ID, due +to limitations of DTD. In certain documents, this relaxed behavior is desired, +whether it is to specify duplicate names, or to specify names that would be +illegal IDs (for example, names that begin with a digit.) Set this configuration +directive to true to use the relaxed parsing rules. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt new file mode 100644 index 00000000000..45ae469ec98 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt @@ -0,0 +1,18 @@ +HTML.BlockWrapper +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'p' +--DESCRIPTION-- + +

+ String name of element to wrap inline elements that are inside a block + context. This only occurs in the children of blockquote in strict mode. +

+

+ Example: by default value, + <blockquote>Foo</blockquote> would become + <blockquote><p>Foo</p></blockquote>. + The <p> tags can be replaced with whatever you desire, + as long as it is a block level element. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt new file mode 100644 index 00000000000..52461887955 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt @@ -0,0 +1,23 @@ +HTML.CoreModules +TYPE: lookup +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'Structure' => true, + 'Text' => true, + 'Hypertext' => true, + 'List' => true, + 'NonXMLCommonAttributes' => true, + 'XMLCommonAttributes' => true, + 'CommonAttributes' => true, +) +--DESCRIPTION-- + +

+ Certain modularized doctypes (XHTML, namely), have certain modules + that must be included for the doctype to be an conforming document + type: put those modules here. By default, XHTML's core modules + are used. You can set this to a blank array to disable core module + protection, but this is not recommended. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt new file mode 100644 index 00000000000..a64e3d7c363 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt @@ -0,0 +1,9 @@ +HTML.CustomDoctype +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +A custom doctype for power-users who defined there own document +type. This directive only applies when %HTML.Doctype is blank. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt new file mode 100644 index 00000000000..103db754a28 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt @@ -0,0 +1,33 @@ +HTML.DefinitionID +TYPE: string/null +DEFAULT: NULL +VERSION: 2.0.0 +--DESCRIPTION-- + +

+ Unique identifier for a custom-built HTML definition. If you edit + the raw version of the HTMLDefinition, introducing changes that the + configuration object does not reflect, you must specify this variable. + If you change your custom edits, you should change this directive, or + clear your cache. Example: +

+
+$config = HTMLPurifier_Config::createDefault();
+$config->set('HTML', 'DefinitionID', '1');
+$def = $config->getHTMLDefinition();
+$def->addAttribute('a', 'tabindex', 'Number');
+
+

+ In the above example, the configuration is still at the defaults, but + using the advanced API, an extra attribute has been added. The + configuration object normally has no way of knowing that this change + has taken place, so it needs an extra directive: %HTML.DefinitionID. + If someone else attempts to use the default configuration, these two + pieces of code will not clobber each other in the cache, since one has + an extra directive attached to it. +

+

+ You must specify a value to this directive to use the + advanced API features. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt new file mode 100644 index 00000000000..229ae0267a6 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt @@ -0,0 +1,16 @@ +HTML.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition specified in + %HTML.DefinitionID. This serves the same purpose: uniquely identifying + your custom definition, but this one does so in a chronological + context: revision 3 is more up-to-date then revision 2. Thus, when + this gets incremented, the cache handling is smart enough to clean + up any older revisions of your definition as well as flush the + cache. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt new file mode 100644 index 00000000000..9dab497f2f3 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt @@ -0,0 +1,11 @@ +HTML.Doctype +TYPE: string/null +DEFAULT: NULL +--DESCRIPTION-- +Doctype to use during filtering. Technically speaking this is not actually +a doctype (as it does not identify a corresponding DTD), but we are using +this name for sake of simplicity. When non-blank, this will override any +older directives like %HTML.XHTML or %HTML.Strict. +--ALLOWED-- +'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1' +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt new file mode 100644 index 00000000000..57358f9bad2 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt @@ -0,0 +1,21 @@ +HTML.ForbiddenAttributes +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ While this directive is similar to %HTML.AllowedAttributes, for + forwards-compatibility with XML, this attribute has a different syntax. Instead of + tag.attr, use tag@attr. To disallow href + attributes in a tags, set this directive to + a@href. You can also disallow an attribute globally with + attr or *@attr (either syntax is fine; the latter + is provided for consistency with %HTML.AllowedAttributes). +

+

+ Warning: This directive complements %HTML.ForbiddenElements, + accordingly, check + out that directive for a discussion of why you + should think twice before using this directive. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt new file mode 100644 index 00000000000..93a53e14fb4 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt @@ -0,0 +1,20 @@ +HTML.ForbiddenElements +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ This was, perhaps, the most requested feature ever in HTML + Purifier. Please don't abuse it! This is the logical inverse of + %HTML.AllowedElements, and it will override that directive, or any + other directive. +

+

+ If possible, %HTML.Allowed is recommended over this directive, because it + can sometimes be difficult to tell whether or not you've forbidden all of + the behavior you would like to disallow. If you forbid img + with the expectation of preventing images on your site, you'll be in for + a nasty surprise when people start using the background-image + CSS property. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt new file mode 100644 index 00000000000..e424c386ec3 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt @@ -0,0 +1,14 @@ +HTML.MaxImgLength +TYPE: int/null +DEFAULT: 1200 +VERSION: 3.1.1 +--DESCRIPTION-- +

+ 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). +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt new file mode 100644 index 00000000000..62e8e160c73 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt @@ -0,0 +1,12 @@ +HTML.Parent +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'div' +--DESCRIPTION-- + +

+ String name of element that HTML fragment passed to library will be + inserted in. An interesting variation would be using span as the + parent element, meaning that only inline tags would be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt new file mode 100644 index 00000000000..dfb720496da --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt @@ -0,0 +1,12 @@ +HTML.Proprietary +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to allow proprietary elements and attributes in your + documents, as per HTMLPurifier_HTMLModule_Proprietary. + Warning: This can cause your documents to stop + validating! +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt new file mode 100644 index 00000000000..cdda09a4c5f --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt @@ -0,0 +1,13 @@ +HTML.SafeEmbed +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ 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 should + see if you can use %Output.FlashCompat with %HTML.SafeObject instead + first.

+--# vim: et sw=4 sts=4 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..ceb342e22b7 --- /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 should also enable + %Output.FlashCompat in order to generate Internet Explorer + compatibility code for your object tags. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt new file mode 100644 index 00000000000..a8b1de56bef --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt @@ -0,0 +1,9 @@ +HTML.Strict +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not to use Transitional (loose) or Strict rulesets. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt new file mode 100644 index 00000000000..b4c271b7fac --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt @@ -0,0 +1,8 @@ +HTML.TidyAdd +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to add to the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt new file mode 100644 index 00000000000..4186ccd0d19 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt @@ -0,0 +1,24 @@ +HTML.TidyLevel +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'medium' +--DESCRIPTION-- + +

General level of cleanliness the Tidy module should enforce. +There are four allowed values:

+
+
none
+
No extra tidying should be done
+
light
+
Only fix elements that would be discarded otherwise due to + lack of support in doctype
+
medium
+
Enforce best practices
+
heavy
+
Transform all deprecated elements and attributes to standards + compliant equivalents
+
+ +--ALLOWED-- +'none', 'light', 'medium', 'heavy' +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt new file mode 100644 index 00000000000..996762bd1d6 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt @@ -0,0 +1,8 @@ +HTML.TidyRemove +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to remove from the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt new file mode 100644 index 00000000000..89133b1a385 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt @@ -0,0 +1,8 @@ +HTML.Trusted +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user input is trusted or not. If the input is +trusted, a more expansive set of allowed tags and attributes will be used. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt new file mode 100644 index 00000000000..2a47e384f41 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt @@ -0,0 +1,11 @@ +HTML.XHTML +TYPE: bool +DEFAULT: true +VERSION: 1.1.0 +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. +--ALIASES-- +Core.XHTML +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt new file mode 100644 index 00000000000..08921fde70a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt @@ -0,0 +1,10 @@ +Output.CommentScriptContents +TYPE: bool +VERSION: 2.0.0 +DEFAULT: true +--DESCRIPTION-- +Determines whether or not HTML Purifier should attempt to fix up the +contents of script tags for legacy browsers with comments. +--ALIASES-- +Core.CommentScriptContents +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt new file mode 100644 index 00000000000..93398e85980 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt @@ -0,0 +1,11 @@ +Output.FlashCompat +TYPE: bool +VERSION: 4.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ If true, HTML Purifier will generate Internet Explorer compatibility + code for all object code. This is highly recommended if you enable + %HTML.SafeObject. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt new file mode 100644 index 00000000000..79f8ad82cfc --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt @@ -0,0 +1,13 @@ +Output.Newline +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Newline string to format final output with. If left null, HTML Purifier + will auto-detect the default newline type of the system and use that; + you can manually override it here. Remember, \r\n is Windows, \r + is Mac, and \n is Unix. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt new file mode 100644 index 00000000000..232b02362a4 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt @@ -0,0 +1,14 @@ +Output.SortAttr +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ If true, HTML Purifier will sort attributes by name before writing them back + to the document, converting a tag like: <el b="" a="" c="" /> + to <el a="" b="" c="" />. This is a workaround for + a bug in FCKeditor which causes it to swap attributes order, adding noise + to text diffs. If you're not seeing this bug, chances are, you don't need + this directive. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt new file mode 100644 index 00000000000..06bab00a0a2 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt @@ -0,0 +1,25 @@ +Output.TidyFormat +TYPE: bool +VERSION: 1.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ Determines whether or not to run Tidy on the final output for pretty + formatting reasons, such as indentation and wrap. +

+

+ This can greatly improve readability for editors who are hand-editing + the HTML, but is by no means necessary as HTML Purifier has already + fixed all major errors the HTML may have had. Tidy is a non-default + extension, and this directive will silently fail if Tidy is not + available. +

+

+ If you are looking to make the overall look of your page's source + better, I recommend running Tidy on the entire page rather than just + user-content (after all, the indentation relative to the containing + blocks will be incorrect). +

+--ALIASES-- +Core.TidyFormat +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt new file mode 100644 index 00000000000..071bc0295df --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt @@ -0,0 +1,7 @@ +Test.ForceNoIconv +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When set to true, HTMLPurifier_Encoder will act as if iconv does not exist +and use only pure PHP implementations. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt new file mode 100644 index 00000000000..ae3a913f242 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt @@ -0,0 +1,17 @@ +URI.AllowedSchemes +TYPE: lookup +--DEFAULT-- +array ( + 'http' => true, + 'https' => true, + 'mailto' => true, + 'ftp' => true, + 'nntp' => true, + 'news' => true, +) +--DESCRIPTION-- +Whitelist that defines the schemes that a URI is allowed to have. This +prevents XSS attacks from using pseudo-schemes like javascript or mocha. +There is also support for the data URI scheme, but it is not +enabled by default. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt new file mode 100644 index 00000000000..876f0680cf2 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt @@ -0,0 +1,17 @@ +URI.Base +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ The base URI is the URI of the document this purified HTML will be + inserted into. This information is important if HTML Purifier needs + to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute + is on. You may use a non-absolute URI for this value, but behavior + may vary (%URI.MakeAbsolute deals nicely with both absolute and + relative paths, but forwards-compatibility is not guaranteed). + Warning: If set, the scheme on this URI + overrides the one specified by %URI.DefaultScheme. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt new file mode 100644 index 00000000000..728e378cbeb --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt @@ -0,0 +1,10 @@ +URI.DefaultScheme +TYPE: string +DEFAULT: 'http' +--DESCRIPTION-- + +

+ Defines through what scheme the output will be served, in order to + select the proper object validator when no scheme information is present. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt new file mode 100644 index 00000000000..f05312ba862 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt @@ -0,0 +1,11 @@ +URI.DefinitionID +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Unique identifier for a custom-built URI definition. If you want + to add custom URIFilters, you must specify this value. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt new file mode 100644 index 00000000000..80cfea93f72 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt @@ -0,0 +1,11 @@ +URI.DefinitionRev +TYPE: int +VERSION: 2.1.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt new file mode 100644 index 00000000000..71ce025a2dc --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt @@ -0,0 +1,14 @@ +URI.Disable +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Disables all URIs in all forms. Not sure why you'd want to do that + (after all, the Internet's founded on the notion of a hyperlink). +

+ +--ALIASES-- +Attr.DisableURI +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt new file mode 100644 index 00000000000..13c122c8cec --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt @@ -0,0 +1,11 @@ +URI.DisableExternal +TYPE: bool +VERSION: 1.2.0 +DEFAULT: false +--DESCRIPTION-- +Disables links to external websites. This is a highly effective anti-spam +and anti-pagerank-leech measure, but comes at a hefty price: nolinks or +images outside of your domain will be allowed. Non-linkified URIs will +still be preserved. If you want to be able to link to subdomains or use +absolute URIs, specify %URI.Host for your website. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt new file mode 100644 index 00000000000..abcc1efd613 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt @@ -0,0 +1,13 @@ +URI.DisableExternalResources +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- +Disables the embedding of external resources, preventing users from +embedding things like images from other hosts. This prevents access +tracking (good for email viewers), bandwidth leeching, cross-site request +forging, goatse.cx posting, and other nasties, but also results in a loss +of end-user functionality (they can't directly post a pic they posted from +Flickr anymore). Use it if you don't have a robust user-content moderation +team. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt new file mode 100644 index 00000000000..51e6ea91f79 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt @@ -0,0 +1,12 @@ +URI.DisableResources +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Disables embedding resources, essentially meaning no pictures. You can + still link to them though. See %URI.DisableExternalResources for why + this might be a good idea. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt new file mode 100644 index 00000000000..ee83b121dec --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt @@ -0,0 +1,19 @@ +URI.Host +TYPE: string/null +VERSION: 1.2.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Defines the domain name of the server, so we can determine whether or + an absolute URI is from your website or not. Not strictly necessary, + as users should be using relative URIs to reference resources on your + website. It will, however, let you use absolute URIs to link to + subdomains of the domain you post here: i.e. example.com will allow + sub.example.com. However, higher up domains will still be excluded: + if you set %URI.Host to sub.example.com, example.com will be blocked. + Note: This directive overrides %URI.Base because + a given page may be on a sub-domain, but you wish HTML Purifier to be + more relaxed and allow some of the parent domains too. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt new file mode 100644 index 00000000000..0b6df7625dc --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt @@ -0,0 +1,9 @@ +URI.HostBlacklist +TYPE: list +VERSION: 1.3.0 +DEFAULT: array() +--DESCRIPTION-- +List of strings that are forbidden in the host of any URI. Use it to kill +domain names of spam, etc. Note that it will catch anything in the domain, +so moo.com will catch moo.com.example.com. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt new file mode 100644 index 00000000000..4214900a592 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt @@ -0,0 +1,13 @@ +URI.MakeAbsolute +TYPE: bool +VERSION: 2.1.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Converts all URIs into absolute forms. This is useful when the HTML + being filtered assumes a specific base path, but will actually be + viewed in a different context (and setting an alternate base URI is + not possible). %URI.Base must be set for this directive to work. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt new file mode 100644 index 00000000000..58c81dcc441 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -0,0 +1,83 @@ +URI.Munge +TYPE: string/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Munges all browsable (usually http, https and ftp) + 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). +

+

+ Uses for this directive: +

+
    +
  • + 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. +
  • +
  • + Redirect users to a splash page telling them they are leaving your + website. While this is poor usability practice, it is often mandated + in corporate environments. +
  • +
+

+ 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: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyDescriptionExample <a href="">
%r1 - The URI embeds a resource
(blank) - The URI is merely a link
%nThe name of the tag this URI came froma
%mThe name of the attribute this URI came fromhref
%pThe 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. +

+--# vim: et sw=4 sts=4 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..6fce0fdc373 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt @@ -0,0 +1,17 @@ +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. +

+--# vim: et sw=4 sts=4 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..0d00f62ea8a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt @@ -0,0 +1,30 @@ +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. +

+--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt new file mode 100644 index 00000000000..23331a4e790 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt @@ -0,0 +1,9 @@ +URI.OverrideAllowedSchemes +TYPE: bool +DEFAULT: true +--DESCRIPTION-- +If this is set to true (which it is by default), you can override +%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the +registry. If false, you will also have to update that directive in order +to add more schemes. +--# vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/info.ini b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/info.ini new file mode 100644 index 00000000000..5de4505e1b0 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/info.ini @@ -0,0 +1,3 @@ +name = "HTML Purifier" + +; vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ContentSets.php b/lib/htmlpurifier/HTMLPurifier/ContentSets.php index 7baf7a3101d..3b6e96f5f56 100644 --- a/lib/htmlpurifier/HTMLPurifier/ContentSets.php +++ b/lib/htmlpurifier/HTMLPurifier/ContentSets.php @@ -1,94 +1,98 @@ true) indexed by name. * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets - * @public */ - var $lookup = array(); - + public $lookup = array(); + /** * Synchronized list of defined content sets (keys of info) */ - var $keys = array(); + protected $keys = array(); /** * Synchronized list of defined content values (values of info) */ - var $values = array(); - + protected $values = array(); + /** * Merges in module's content sets, expands identifiers in the content * sets and populates the keys, values and lookup member variables. * @param $modules List of HTMLPurifier_HTMLModule */ - function HTMLPurifier_ContentSets($modules) { + public function __construct($modules) { if (!is_array($modules)) $modules = array($modules); // populate content_sets based on module hints // sorry, no way of overloading foreach ($modules as $module_i => $module) { foreach ($module->content_sets as $key => $value) { - if (isset($this->info[$key])) { + $temp = $this->convertToLookup($value); + if (isset($this->lookup[$key])) { // add it into the existing content set - $this->info[$key] = $this->info[$key] . ' | ' . $value; + $this->lookup[$key] = array_merge($this->lookup[$key], $temp); } else { - $this->info[$key] = $value; + $this->lookup[$key] = $temp; } } } - // perform content_set expansions - $this->keys = array_keys($this->info); - foreach ($this->info as $i => $set) { - // only performed once, so infinite recursion is not - // a problem - $this->info[$i] = - str_replace( - $this->keys, - // must be recalculated each time due to - // changing substitutions - array_values($this->info), - $set); + $old_lookup = false; + while ($old_lookup !== $this->lookup) { + $old_lookup = $this->lookup; + foreach ($this->lookup as $i => $set) { + $add = array(); + foreach ($set as $element => $x) { + if (isset($this->lookup[$element])) { + $add += $this->lookup[$element]; + unset($this->lookup[$i][$element]); + } + } + $this->lookup[$i] += $add; + } } + + foreach ($this->lookup as $key => $lookup) { + $this->info[$key] = implode(' | ', array_keys($lookup)); + } + $this->keys = array_keys($this->info); $this->values = array_values($this->info); - - // generate lookup tables - foreach ($this->info as $name => $set) { - $this->lookup[$name] = $this->convertToLookup($set); - } } - + /** * Accepts a definition; generates and assigns a ChildDef for it * @param $def HTMLPurifier_ElementDef reference * @param $module Module that defined the ElementDef */ - function generateChildDef(&$def, $module) { + public function generateChildDef(&$def, $module) { if (!empty($def->child)) return; // already done! $content_model = $def->content_model; if (is_string($content_model)) { - $def->content_model = str_replace( - $this->keys, $this->values, $content_model); + // Assume that $this->keys is alphanumeric + $def->content_model = preg_replace_callback( + '/\b(' . implode('|', $this->keys) . ')\b/', + array($this, 'generateChildDefCallback'), + $content_model + ); + //$def->content_model = str_replace( + // $this->keys, $this->values, $content_model); } $def->child = $this->getChildDef($def, $module); } - + + public function generateChildDefCallback($matches) { + return $this->info[$matches[0]]; + } + /** * Instantiates a ChildDef based on content_model and content_model_type * member variables in HTMLPurifier_ElementDef @@ -97,7 +101,7 @@ class HTMLPurifier_ContentSets * @param $def HTMLPurifier_ElementDef to have ChildDef extracted * @return HTMLPurifier_ChildDef corresponding to ElementDef */ - function getChildDef($def, $module) { + public function getChildDef($def, $module) { $value = $def->content_model; if (is_object($value)) { trigger_error( @@ -130,14 +134,14 @@ class HTMLPurifier_ContentSets ); return false; } - + /** * Converts a string list of elements separated by pipes into * a lookup array. * @param $string List of elements * @return Lookup array of elements */ - function convertToLookup($string) { + protected function convertToLookup($string) { $array = explode('|', str_replace(' ', '', $string)); $ret = array(); foreach ($array as $i => $k) { @@ -145,6 +149,7 @@ class HTMLPurifier_ContentSets } return $ret; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Context.php b/lib/htmlpurifier/HTMLPurifier/Context.php index a78a6fb6f63..9ddf0c5476a 100644 --- a/lib/htmlpurifier/HTMLPurifier/Context.php +++ b/lib/htmlpurifier/HTMLPurifier/Context.php @@ -4,22 +4,23 @@ * Registry object that contains information about the current context. * @warning Is a bit buggy when variables are set to null: it thinks * they don't exist! So use false instead, please. + * @note Since the variables Context deals with may not be objects, + * references are very important here! Do not remove! */ class HTMLPurifier_Context { - + /** * Private array that stores the references. - * @private */ - var $_storage = array(); - + private $_storage = array(); + /** * Registers a variable into the context. * @param $name String name - * @param $ref Variable to be registered + * @param $ref Reference to variable to be registered */ - function register($name, &$ref) { + public function register($name, &$ref) { if (isset($this->_storage[$name])) { trigger_error("Name $name produces collision, cannot re-register", E_USER_ERROR); @@ -27,13 +28,13 @@ class HTMLPurifier_Context } $this->_storage[$name] =& $ref; } - + /** * Retrieves a variable reference from the context. * @param $name String name * @param $ignore_error Boolean whether or not to ignore error */ - function &get($name, $ignore_error = false) { + public function &get($name, $ignore_error = false) { if (!isset($this->_storage[$name])) { if (!$ignore_error) { trigger_error("Attempted to retrieve non-existent variable $name", @@ -44,12 +45,12 @@ class HTMLPurifier_Context } return $this->_storage[$name]; } - + /** * Destorys a variable in the context. * @param $name String name */ - function destroy($name) { + public function destroy($name) { if (!isset($this->_storage[$name])) { trigger_error("Attempted to destroy non-existent variable $name", E_USER_ERROR); @@ -57,24 +58,25 @@ class HTMLPurifier_Context } unset($this->_storage[$name]); } - + /** * Checks whether or not the variable exists. * @param $name String name */ - function exists($name) { + public function exists($name) { return isset($this->_storage[$name]); } - + /** * Loads a series of variables from an associative array * @param $context_array Assoc array of variables to load */ - function loadArray(&$context_array) { + public function loadArray($context_array) { foreach ($context_array as $key => $discard) { $this->register($key, $context_array[$key]); } } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Definition.php b/lib/htmlpurifier/HTMLPurifier/Definition.php index 8f958e4798d..a7408c97498 100644 --- a/lib/htmlpurifier/HTMLPurifier/Definition.php +++ b/lib/htmlpurifier/HTMLPurifier/Definition.php @@ -4,37 +4,36 @@ * Super-class for definition datatype objects, implements serialization * functions for the class. */ -class HTMLPurifier_Definition +abstract class HTMLPurifier_Definition { - + /** * Has setup() been called yet? */ - var $setup = false; - + public $setup = false; + /** * What type of definition is it? */ - var $type; - + public $type; + /** * Sets up the definition object into the final form, something * not done by the constructor * @param $config HTMLPurifier_Config instance */ - function doSetup($config) { - trigger_error('Cannot call abstract method', E_USER_ERROR); - } - + abstract protected function doSetup($config); + /** * Setup function that aborts if already setup * @param $config HTMLPurifier_Config instance */ - function setup($config) { + public function setup($config) { if ($this->setup) return; $this->setup = true; $this->doSetup($config); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/DefinitionCache.php b/lib/htmlpurifier/HTMLPurifier/DefinitionCache.php index 5b14fdfe4dd..c6e1e388c65 100644 --- a/lib/htmlpurifier/HTMLPurifier/DefinitionCache.php +++ b/lib/htmlpurifier/HTMLPurifier/DefinitionCache.php @@ -1,131 +1,108 @@ type = $type; } - + /** * Generates a unique identifier for a particular configuration * @param Instance of HTMLPurifier_Config */ - function generateKey($config) { - return $config->version . '-' . // possibly replace with function calls - $config->getBatchSerial($this->type) . '-' . - $config->get($this->type, 'DefinitionRev'); + public function generateKey($config) { + return $config->version . ',' . // possibly replace with function calls + $config->getBatchSerial($this->type) . ',' . + $config->get($this->type . '.DefinitionRev'); } - + /** * Tests whether or not a key is old with respect to the configuration's * version and revision number. * @param $key Key to test * @param $config Instance of HTMLPurifier_Config to test against */ - function isOld($key, $config) { - if (substr_count($key, '-') < 2) return true; - list($version, $hash, $revision) = explode('-', $key, 3); + public function isOld($key, $config) { + if (substr_count($key, ',') < 2) return true; + list($version, $hash, $revision) = explode(',', $key, 3); $compare = version_compare($version, $config->version); // version mismatch, is always old if ($compare != 0) return true; // versions match, ids match, check revision number if ( $hash == $config->getBatchSerial($this->type) && - $revision < $config->get($this->type, 'DefinitionRev') + $revision < $config->get($this->type . '.DefinitionRev') ) return true; return false; } - + /** * Checks if a definition's type jives with the cache's type * @note Throws an error on failure * @param $def Definition object to check * @return Boolean true if good, false if not */ - function checkDefType($def) { + public function checkDefType($def) { if ($def->type !== $this->type) { trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); return false; } return true; } - + /** * Adds a definition object to the cache */ - function add($def, $config) { - trigger_error('Cannot call abstract method', E_USER_ERROR); - } - + abstract public function add($def, $config); + /** * Unconditionally saves a definition object to the cache */ - function set($def, $config) { - trigger_error('Cannot call abstract method', E_USER_ERROR); - } - + abstract public function set($def, $config); + /** * Replace an object in the cache */ - function replace($def, $config) { - trigger_error('Cannot call abstract method', E_USER_ERROR); - } - + abstract public function replace($def, $config); + /** * Retrieves a definition object from the cache */ - function get($config) { - trigger_error('Cannot call abstract method', E_USER_ERROR); - } - + abstract public function get($config); + /** * Removes a definition object to the cache */ - function remove($config) { - trigger_error('Cannot call abstract method', E_USER_ERROR); - } - + abstract public function remove($config); + /** * Clears all objects from cache */ - function flush($config) { - trigger_error('Cannot call abstract method', E_USER_ERROR); - } - + abstract public function flush($config); + /** * Clears all expired (older version or revision) objects from cache * @note Be carefuly implementing this method as flush. Flush must * not interfere with other Definition types, and cleanup() * should not be repeatedly called by userland code. */ - function cleanup($config) { - trigger_error('Cannot call abstract method', E_USER_ERROR); - } + abstract public function cleanup($config); + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator.php b/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator.php index 14fca859744..b0fb6d0cd67 100644 --- a/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator.php +++ b/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator.php @@ -1,59 +1,62 @@ copy(); // reference is necessary for mocks in PHP 4 $decorator->cache =& $cache; $decorator->type = $cache->type; return $decorator; } - + /** * Cross-compatible clone substitute */ - function copy() { + public function copy() { return new HTMLPurifier_DefinitionCache_Decorator(); } - - function add($def, $config) { + + public function add($def, $config) { return $this->cache->add($def, $config); } - - function set($def, $config) { + + public function set($def, $config) { return $this->cache->set($def, $config); } - - function replace($def, $config) { + + public function replace($def, $config) { return $this->cache->replace($def, $config); } - - function get($config) { + + public function get($config) { return $this->cache->get($config); } - - function flush($config) { + + public function remove($config) { + return $this->cache->remove($config); + } + + public function flush($config) { return $this->cache->flush($config); } - - function cleanup($config) { + + public function cleanup($config) { return $this->cache->cleanup($config); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php index eb47c433fbb..d4cc35c4bc1 100644 --- a/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php +++ b/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php @@ -1,7 +1,5 @@ definitions[$this->generateKey($config)] = $def; return $status; } - - function set($def, $config) { + + public function set($def, $config) { $status = parent::set($def, $config); if ($status) $this->definitions[$this->generateKey($config)] = $def; return $status; } - - function replace($def, $config) { + + public function replace($def, $config) { $status = parent::replace($def, $config); if ($status) $this->definitions[$this->generateKey($config)] = $def; return $status; } - - function get($config) { + + public function get($config) { $key = $this->generateKey($config); if (isset($this->definitions[$key])) return $this->definitions[$key]; $this->definitions[$key] = parent::get($config); return $this->definitions[$key]; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in index 62235e225db..21a8fcfda24 100644 --- a/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in +++ b/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in @@ -1,46 +1,47 @@ - - Absolute path with no trailing slash to store serialized definitions in. - Default is within the - HTML Purifier library inside DefinitionCache/Serializer. This - path must be writable by the webserver. This directive has been - available since 2.0.0. -

-'); - class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCache { - - function add($def, $config) { + + public function add($def, $config) { if (!$this->checkDefType($def)) return; $file = $this->generateFilePath($config); if (file_exists($file)) return false; if (!$this->_prepareDir($config)) return false; return $this->_write($file, serialize($def)); } - - function set($def, $config) { + + public function set($def, $config) { if (!$this->checkDefType($def)) return; $file = $this->generateFilePath($config); if (!$this->_prepareDir($config)) return false; return $this->_write($file, serialize($def)); } - - function replace($def, $config) { + + public function replace($def, $config) { if (!$this->checkDefType($def)) return; $file = $this->generateFilePath($config); if (!file_exists($file)) return false; if (!$this->_prepareDir($config)) return false; return $this->_write($file, serialize($def)); } - - function get($config) { + + public function get($config) { $file = $this->generateFilePath($config); if (!file_exists($file)) return false; return unserialize(file_get_contents($file)); } - - function remove($config) { + + public function remove($config) { $file = $this->generateFilePath($config); if (!file_exists($file)) return false; return unlink($file); } - - function flush($config) { + + public function flush($config) { if (!$this->_prepareDir($config)) return false; $dir = $this->generateDirectoryPath($config); $dh = opendir($dir); @@ -62,8 +49,8 @@ class HTMLPurifier_DefinitionCache_Serializer extends unlink($dir . '/' . $filename); } } - - function cleanup($config) { + + public function cleanup($config) { if (!$this->_prepareDir($config)) return false; $dir = $this->generateDirectoryPath($config); $dh = opendir($dir); @@ -74,91 +61,85 @@ class HTMLPurifier_DefinitionCache_Serializer extends if ($this->isOld($key, $config)) unlink($dir . '/' . $filename); } } - + /** * Generates the file path to the serial file corresponding to * the configuration and definition name + * @todo Make protected */ - function generateFilePath($config) { + public function generateFilePath($config) { $key = $this->generateKey($config); return $this->generateDirectoryPath($config) . '/' . $key . '.ser'; } - + /** * Generates the path to the directory contain this cache's serial files * @note No trailing slash + * @todo Make protected */ - function generateDirectoryPath($config) { + public function generateDirectoryPath($config) { $base = $this->generateBaseDirectoryPath($config); return $base . '/' . $this->type; } - + /** * Generates path to base directory that contains all definition type * serials + * @todo Make protected */ - function generateBaseDirectoryPath($config) { - $base = $config->get('Cache', 'SerializerPath'); + public function generateBaseDirectoryPath($config) { + $base = $config->get('Cache.SerializerPath'); $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base; return $base; } - + /** * Convenience wrapper function for file_put_contents * @param $file File name to write to * @param $data Data to write into file * @return Number of bytes written if success, or false if failure. */ - function _write($file, $data) { - static $file_put_contents; - if ($file_put_contents === null) { - $file_put_contents = function_exists('file_put_contents'); - } - if ($file_put_contents) { - return file_put_contents($file, $data); - } - $fh = fopen($file, 'w'); - if (!$fh) return false; - $status = fwrite($fh, $data); - fclose($fh); - return $status; + private function _write($file, $data) { + return file_put_contents($file, $data); } - + /** * Prepares the directory that this type stores the serials in * @return True if successful */ - function _prepareDir($config) { + private function _prepareDir($config) { $directory = $this->generateDirectoryPath($config); if (!is_dir($directory)) { $base = $this->generateBaseDirectoryPath($config); if (!is_dir($base)) { trigger_error('Base directory '.$base.' does not exist, please create or change using %Cache.SerializerPath', - E_USER_ERROR); + E_USER_WARNING); return false; } elseif (!$this->_testPermissions($base)) { return false; } + $old = umask(0022); // disable group and world writes mkdir($directory); + umask($old); } elseif (!$this->_testPermissions($directory)) { return false; } return true; } - + /** * Tests permissions on a directory and throws out friendly * error messages and attempts to chmod it itself if possible */ - function _testPermissions($dir) { + private function _testPermissions($dir) { // early abort, if it is writable, everything is hunky-dory if (is_writable($dir)) return true; if (!is_dir($dir)) { // generally, you'll want to handle this beforehand // so a more specific error message can be given trigger_error('Directory '.$dir.' does not exist', - E_USER_ERROR); + E_USER_WARNING); return false; } if (function_exists('posix_getuid')) { @@ -176,15 +157,16 @@ class HTMLPurifier_DefinitionCache_Serializer extends } trigger_error('Directory '.$dir.' not writable, '. 'please chmod to ' . $chmod, - E_USER_ERROR); + E_USER_WARNING); } else { // generic error message trigger_error('Directory '.$dir.' not writable, '. 'please alter file permissions', - E_USER_ERROR); + E_USER_WARNING); } return false; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/README b/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/README new file mode 100644 index 00000000000..2e35c1c3d06 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/README @@ -0,0 +1,3 @@ +This is a dummy file to prevent Git from ignoring this empty directory. + + vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/DefinitionCacheFactory.php b/lib/htmlpurifier/HTMLPurifier/DefinitionCacheFactory.php index dead92a32e9..a6ead628187 100644 --- a/lib/htmlpurifier/HTMLPurifier/DefinitionCacheFactory.php +++ b/lib/htmlpurifier/HTMLPurifier/DefinitionCacheFactory.php @@ -1,44 +1,26 @@ array()); - var $implementations = array(); - var $decorators = array(); - + + protected $caches = array('Serializer' => array()); + protected $implementations = array(); + protected $decorators = array(); + /** * Initialize default decorators */ - function setup() { + public function setup() { $this->addDecorator('Cleanup'); } - + /** * Retrieves an instance of global definition cache factory. - * @static */ - function &instance($prototype = null) { + public static function instance($prototype = null) { static $instance; if ($prototype !== null) { $instance = $prototype; @@ -48,33 +30,32 @@ class HTMLPurifier_DefinitionCacheFactory } return $instance; } - + /** * Registers a new definition cache object * @param $short Short name of cache object, for reference - * @param $long Full class name of cache object, for construction + * @param $long Full class name of cache object, for construction */ - function register($short, $long) { + public function register($short, $long) { $this->implementations[$short] = $long; } - + /** * Factory method that creates a cache object based on configuration * @param $name Name of definitions handled by cache * @param $config Instance of HTMLPurifier_Config */ - function &create($type, $config) { - $method = $config->get('Cache', 'DefinitionImpl'); + public function create($type, $config) { + $method = $config->get('Cache.DefinitionImpl'); if ($method === null) { - $null = new HTMLPurifier_DefinitionCache_Null($type); - return $null; + return new HTMLPurifier_DefinitionCache_Null($type); } if (!empty($this->caches[$method][$type])) { return $this->caches[$method][$type]; } if ( isset($this->implementations[$method]) && - class_exists($class = $this->implementations[$method]) + class_exists($class = $this->implementations[$method], false) ) { $cache = new $class($type); } else { @@ -92,18 +73,19 @@ class HTMLPurifier_DefinitionCacheFactory $this->caches[$method][$type] = $cache; return $this->caches[$method][$type]; } - + /** * Registers a decorator to add to all new cache objects - * @param + * @param */ - function addDecorator($decorator) { + public function addDecorator($decorator) { if (is_string($decorator)) { $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; $decorator = new $class; } $this->decorators[$decorator->name] = $decorator; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Doctype.php b/lib/htmlpurifier/HTMLPurifier/Doctype.php index 7afdcd74a2a..1e3c574c065 100644 --- a/lib/htmlpurifier/HTMLPurifier/Doctype.php +++ b/lib/htmlpurifier/HTMLPurifier/Doctype.php @@ -11,40 +11,40 @@ class HTMLPurifier_Doctype /** * Full name of doctype */ - var $name; - + public $name; + /** * List of standard modules (string identifiers or literal objects) * that this doctype uses */ - var $modules = array(); - + public $modules = array(); + /** * List of modules to use for tidying up code */ - var $tidyModules = array(); - + public $tidyModules = array(); + /** * Is the language derived from XML (i.e. XHTML)? */ - var $xml = true; - + public $xml = true; + /** * List of aliases for this doctype */ - var $aliases = array(); - + public $aliases = array(); + /** * Public DTD identifier */ - var $dtdPublic; - + public $dtdPublic; + /** * System DTD identifier */ - var $dtdSystem; - - function HTMLPurifier_Doctype($name = null, $xml = true, $modules = array(), + public $dtdSystem; + + public function __construct($name = null, $xml = true, $modules = array(), $tidyModules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null ) { $this->name = $name; @@ -55,12 +55,6 @@ class HTMLPurifier_Doctype $this->dtdPublic = $dtd_public; $this->dtdSystem = $dtd_system; } - - /** - * Clones the doctype, use before resolving modes and the like - */ - function copy() { - return unserialize(serialize($this)); - } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/DoctypeRegistry.php b/lib/htmlpurifier/HTMLPurifier/DoctypeRegistry.php index e657b3da4b1..86049e9391b 100644 --- a/lib/htmlpurifier/HTMLPurifier/DoctypeRegistry.php +++ b/lib/htmlpurifier/HTMLPurifier/DoctypeRegistry.php @@ -1,38 +1,18 @@ doctypes[$doctype->name] =& $doctype; + $this->doctypes[$doctype->name] = $doctype; $name = $doctype->name; // hookup aliases foreach ($doctype->aliases as $alias) { @@ -65,15 +45,15 @@ class HTMLPurifier_DoctypeRegistry if (isset($this->aliases[$name])) unset($this->aliases[$name]); return $doctype; } - + /** * Retrieves reference to a doctype of a certain name * @note This function resolves aliases * @note When possible, use the more fully-featured make() * @param $doctype Name of doctype - * @return Reference to doctype object + * @return Editable doctype object */ - function &get($doctype) { + public function get($doctype) { if (isset($this->aliases[$doctype])) $doctype = $this->aliases[$doctype]; if (!isset($this->doctypes[$doctype])) { trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); @@ -82,7 +62,7 @@ class HTMLPurifier_DoctypeRegistry } return $this->doctypes[$doctype]; } - + /** * Creates a doctype based on a configuration object, * will perform initialization on the doctype @@ -91,34 +71,33 @@ class HTMLPurifier_DoctypeRegistry * Generator whether or not the current document is XML * based or not). */ - function make($config) { - $original_doctype = $this->get($this->getDoctypeFromConfig($config)); - $doctype = $original_doctype->copy(); - return $doctype; + public function make($config) { + return clone $this->get($this->getDoctypeFromConfig($config)); } - + /** * Retrieves the doctype from the configuration object */ - function getDoctypeFromConfig($config) { + public function getDoctypeFromConfig($config) { // recommended test - $doctype = $config->get('HTML', 'Doctype'); + $doctype = $config->get('HTML.Doctype'); if (!empty($doctype)) return $doctype; - $doctype = $config->get('HTML', 'CustomDoctype'); + $doctype = $config->get('HTML.CustomDoctype'); if (!empty($doctype)) return $doctype; // backwards-compatibility - if ($config->get('HTML', 'XHTML')) { + if ($config->get('HTML.XHTML')) { $doctype = 'XHTML 1.0'; } else { $doctype = 'HTML 4.01'; } - if ($config->get('HTML', 'Strict')) { + if ($config->get('HTML.Strict')) { $doctype .= ' Strict'; } else { $doctype .= ' Transitional'; } return $doctype; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ElementDef.php b/lib/htmlpurifier/HTMLPurifier/ElementDef.php index b6439d1a5b6..5498d956704 100644 --- a/lib/htmlpurifier/HTMLPurifier/ElementDef.php +++ b/lib/htmlpurifier/HTMLPurifier/ElementDef.php @@ -5,16 +5,18 @@ * HTMLPurifier_HTMLDefinition and HTMLPurifier_HTMLModule. * @note This class is inspected by HTMLPurifier_Printer_HTMLDefinition. * Please update that class too. + * @warning If you add new properties to this class, you MUST update + * the mergeIn() method. */ class HTMLPurifier_ElementDef { - + /** * Does the definition work by itself, or is it created solely * for the purpose of merging into another definition? */ - var $standalone = true; - + public $standalone = true; + /** * Associative array of attribute name to HTMLPurifier_AttrDef * @note Before being processed by HTMLPurifier_AttrCollections @@ -25,68 +27,58 @@ class HTMLPurifier_ElementDef * contain string indentifiers in lieu of HTMLPurifier_AttrDef, * see HTMLPurifier_AttrTypes on how they are expanded during * HTMLPurifier_HTMLDefinition->setup() processing. - * @public */ - var $attr = array(); - + public $attr = array(); + /** * Indexed list of tag's HTMLPurifier_AttrTransform to be done before validation - * @public */ - var $attr_transform_pre = array(); - + public $attr_transform_pre = array(); + /** * Indexed list of tag's HTMLPurifier_AttrTransform to be done after validation - * @public */ - var $attr_transform_post = array(); - - - + public $attr_transform_post = array(); + /** * HTMLPurifier_ChildDef of this tag. - * @public */ - var $child; - + public $child; + /** * Abstract string representation of internal ChildDef rules. See * HTMLPurifier_ContentSets for how this is parsed and then transformed * into an HTMLPurifier_ChildDef. * @warning This is a temporary variable that is not available after * being processed by HTMLDefinition - * @public */ - var $content_model; - + public $content_model; + /** * Value of $child->type, used to determine which ChildDef to use, * used in combination with $content_model. * @warning This must be lowercase * @warning This is a temporary variable that is not available after * being processed by HTMLDefinition - * @public */ - var $content_model_type; - - - + public $content_model_type; + + + /** * Does the element have a content model (#PCDATA | Inline)*? This - * is important for chameleon ins and del processing in + * is important for chameleon ins and del processing in * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't * have to worry about this one. - * @public */ - var $descendants_are_inline = false; - + public $descendants_are_inline = false; + /** * List of the names of required attributes this element has. Dynamically * populated by HTMLPurifier_HTMLDefinition::getElement - * @public */ - var $required_attr = array(); - + public $required_attr = array(); + /** * Lookup table of tags excluded from all descendants of this tag. * @note SGML permits exclusions for all descendants, but this is @@ -97,35 +89,45 @@ class HTMLPurifier_ElementDef * all descendants and not just children. Note that the XHTML * Modularization Abstract Modules are blithely unaware of such * distinctions. - * @public */ - var $excludes = array(); - + public $excludes = array(); + /** - * Is this element safe for untrusted users to use? + * This tag is explicitly auto-closed by the following tags. */ - var $safe; - + public $autoclose = array(); + + /** + * If a foreign element is found in this element, test if it is + * allowed by this sub-element; if it is, instead of closing the + * current element, place it inside this element. + */ + public $wrap; + + /** + * Whether or not this is a formatting element affected by the + * "Active Formatting Elements" algorithm. + */ + public $formatting; + /** * Low-level factory constructor for creating new standalone element defs - * @static */ - function create($safe, $content_model, $content_model_type, $attr) { + public static function create($content_model, $content_model_type, $attr) { $def = new HTMLPurifier_ElementDef(); - $def->safe = (bool) $safe; $def->content_model = $content_model; $def->content_model_type = $content_model_type; $def->attr = $attr; return $def; } - + /** * Merges the values of another element definition into this one. * Values from the new element def take precedence if a value is * not mergeable. */ - function mergeIn($def) { - + public function mergeIn($def) { + // later keys takes precedence foreach($def->attr as $k => $v) { if ($k === 0) { @@ -145,9 +147,10 @@ class HTMLPurifier_ElementDef $this->_mergeAssocArray($this->attr_transform_pre, $def->attr_transform_pre); $this->_mergeAssocArray($this->attr_transform_post, $def->attr_transform_post); $this->_mergeAssocArray($this->excludes, $def->excludes); - + if(!empty($def->content_model)) { - $this->content_model .= ' | ' . $def->content_model; + $this->content_model = + str_replace("#SUPER", $this->content_model, $def->content_model); $this->child = false; } if(!empty($def->content_model_type)) { @@ -155,17 +158,17 @@ class HTMLPurifier_ElementDef $this->child = false; } if(!is_null($def->child)) $this->child = $def->child; + if(!is_null($def->formatting)) $this->formatting = $def->formatting; if($def->descendants_are_inline) $this->descendants_are_inline = $def->descendants_are_inline; - if(!is_null($def->safe)) $this->safe = $def->safe; - + } - + /** * Merges one array into another, removes values which equal false * @param $a1 Array by reference that is merged into * @param $a2 Array that merges into $a1 */ - function _mergeAssocArray(&$a1, $a2) { + private function _mergeAssocArray(&$a1, $a2) { foreach ($a2 as $k => $v) { if ($v === false) { if (isset($a1[$k])) unset($a1[$k]); @@ -174,14 +177,7 @@ class HTMLPurifier_ElementDef $a1[$k] = $v; } } - - /** - * Retrieves a copy of the element definition - */ - function copy() { - return unserialize(serialize($this)); - } - + } - +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Encoder.php b/lib/htmlpurifier/HTMLPurifier/Encoder.php index 4ec736066c7..2b3140caaf5 100644 --- a/lib/htmlpurifier/HTMLPurifier/Encoder.php +++ b/lib/htmlpurifier/HTMLPurifier/Encoder.php @@ -1,86 +1,36 @@ feature '. - 'that automatically resolves all entities), making it pretty useless '. - 'for anything except the most I18N-blind applications, although '. - '%Core.EscapeNonASCIICharacters offers fixes this trouble with '. - 'another tradeoff. This directive '. - 'only accepts ISO-8859-1 if iconv is not enabled.' -); - -HTMLPurifier_ConfigSchema::define( - 'Core', 'EscapeNonASCIICharacters', false, 'bool', - 'This directive overcomes a deficiency in %Core.Encoding by blindly '. - 'converting all non-ASCII characters into decimal numeric entities before '. - 'converting it to its native encoding. This means that even '. - 'characters that can be expressed in the non-UTF-8 encoding will '. - 'be entity-ized, which can be a real downer for encodings like Big5. '. - 'It also assumes that the ASCII repetoire is available, although '. - 'this is the case for almost all encodings. Anyway, use UTF-8! This '. - 'directive has been available since 1.4.0.' -); - -if ( !function_exists('iconv') ) { - // only encodings with native PHP support - HTMLPurifier_ConfigSchema::defineAllowedValues( - 'Core', 'Encoding', array( - 'utf-8', - 'iso-8859-1' - ) - ); - HTMLPurifier_ConfigSchema::defineValueAliases( - 'Core', 'Encoding', array( - 'iso8859-1' => 'iso-8859-1' - ) - ); -} - -HTMLPurifier_ConfigSchema::define( - 'Test', 'ForceNoIconv', false, 'bool', - 'When set to true, HTMLPurifier_Encoder will act as if iconv does not '. - 'exist and use only pure PHP implementations.' -); - /** * A UTF-8 specific character encoder that handles cleaning and transforming. * @note All functions in this class should be static. */ class HTMLPurifier_Encoder { - + /** * Constructor throws fatal error if you attempt to instantiate class */ - function HTMLPurifier_Encoder() { + private function __construct() { trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR); } - + /** * Error-handler that mutes errors, alternative to shut-up operator. */ - function muteErrorHandler() {} - - /** + public static function muteErrorHandler() {} + /** * Cleans a UTF-8 string for well-formedness and SGML validity - * + * * It will parse according to UTF-8 and return a valid UTF8 string, with * non-SGML codepoints excluded. - * - * @static + * * @note Just for reference, the non-SGML code points are 0 to 31 and * 127 to 159, inclusive. However, we allow code points 9, 10 * and 13, which are the tab, line feed and carriage return * respectively. 128 and above the code points map to multibyte * UTF-8 representations. - * + * * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and * hsivonen@iki.fi at under the * LGPL license. Notes on what changed are inside, but in general, @@ -94,8 +44,8 @@ class HTMLPurifier_Encoder * would need that, and I'm probably not going to implement them. * Once again, PHP 6 should solve all our problems. */ - function cleanUTF8($str, $force_php = false) { - + public static function cleanUTF8($str, $force_php = false) { + // 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 PHP stuff. 99% of the time, this will be the case. @@ -104,31 +54,31 @@ class HTMLPurifier_Encoder 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 // until the beginning of the next UTF8 character sequence $mUcs4 = 0; // cached Unicode character $mBytes = 1; // cached expected number of octets in the current sequence - + // original code involved an $out that was an array of Unicode // codepoints. Instead of having to convert back into UTF-8, we've // decided to directly append valid UTF-8 characters onto a string // $out once they're done. $char accumulates raw bytes, while $mUcs4 // turns into the Unicode code point, so there's some redundancy. - + $out = ''; $char = ''; - + $len = strlen($str); for($i = 0; $i < $len; $i++) { $in = ord($str{$i}); $char .= $str[$i]; // append byte to char if (0 == $mState) { - // When mState is zero we expect either a US-ASCII character + // When mState is zero we expect either a US-ASCII character // or a multi-octet sequence. if (0 == (0x80 & ($in))) { // US-ASCII, pass straight through. - if (($in <= 31 || $in == 127) && + if (($in <= 31 || $in == 127) && !($in == 9 || $in == 13 || $in == 10) // save \r\t\n ) { // control characters, remove @@ -158,12 +108,12 @@ class HTMLPurifier_Encoder $mBytes = 4; } elseif (0xF8 == (0xFC & ($in))) { // First octet of 5 octet sequence. - // - // This is illegal because the encoded codepoint must be + // + // This is illegal because the encoded codepoint must be // either: // (a) not the shortest form or // (b) outside the Unicode range of 0-0x10FFFF. - // Rather than trying to resynchronize, we will carry on + // Rather than trying to resynchronize, we will carry on // until the end of the sequence and let the later error // handling code catch it. $mUcs4 = ($in); @@ -178,7 +128,7 @@ class HTMLPurifier_Encoder $mState = 5; $mBytes = 6; } else { - // Current octet is neither in the US-ASCII range nor a + // Current octet is neither in the US-ASCII range nor a // legal first octet of a multi-octet sequence. $mState = 0; $mUcs4 = 0; @@ -194,13 +144,13 @@ class HTMLPurifier_Encoder $tmp = $in; $tmp = ($tmp & 0x0000003F) << $shift; $mUcs4 |= $tmp; - + if (0 == --$mState) { // End of the multi-octet sequence. mUcs4 now contains // the final Unicode codepoint to be output - + // Check for illegal sequences and codepoints. - + // From Unicode 3.1, non-shortest form is illegal if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || ((3 == $mBytes) && ($mUcs4 < 0x0800)) || @@ -211,7 +161,7 @@ class HTMLPurifier_Encoder // Codepoints outside the Unicode range are illegal ($mUcs4 > 0x10FFFF) ) { - + } elseif (0xFEFF != $mUcs4 && // omit BOM // check for valid Char unicode codepoints ( @@ -246,10 +196,9 @@ class HTMLPurifier_Encoder } return $out; } - + /** * Translates a Unicode codepoint into its corresponding UTF-8 character. - * @static * @note Based on Feyd's function at * , * which is in public domain. @@ -260,7 +209,7 @@ class HTMLPurifier_Encoder * maintenance/generate-entity-file.php (although this is superior, * due to its sanity checks). */ - + // +----------+----------+----------+----------+ // | 33222222 | 22221111 | 111111 | | // | 10987654 | 32109876 | 54321098 | 76543210 | bit @@ -272,17 +221,17 @@ class HTMLPurifier_Encoder // +----------+----------+----------+----------+ // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes - // +----------+----------+----------+----------+ - - function unichr($code) { + // +----------+----------+----------+----------+ + + public static function unichr($code) { if($code > 1114111 or $code < 0 or ($code >= 55296 and $code <= 57343) ) { // bits are set outside the "valid" range as defined - // by UNICODE 4.1.0 + // by UNICODE 4.1.0 return ''; } - - $x = $y = $z = $w = 0; + + $x = $y = $z = $w = 0; if ($code < 128) { // regular ASCII character $x = $code; @@ -299,30 +248,35 @@ class HTMLPurifier_Encoder $z = (($code >> 12) & 63) | 128; $w = (($code >> 18) & 7) | 240; } - } + } } // set up the actual character $ret = ''; if($w) $ret .= chr($w); if($z) $ret .= chr($z); if($y) $ret .= chr($y); - $ret .= chr($x); - + $ret .= chr($x); + return $ret; } - + /** * Converts a string to UTF-8 based on configuration. - * @static */ - function convertToUTF8($str, $config, &$context) { - $encoding = $config->get('Core', 'Encoding'); + public static function convertToUTF8($str, $config, $context) { + $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')) { + if ($iconv && !$config->get('Test.ForceNoIconv')) { $str = iconv($encoding, 'utf-8//IGNORE', $str); + if ($str === false) { + // $encoding is not a valid encoding + restore_error_handler(); + trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); + return ''; + } // 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. @@ -334,25 +288,24 @@ class HTMLPurifier_Encoder restore_error_handler(); return $str; } - trigger_error('Encoding not supported', E_USER_ERROR); + trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); } - + /** * Converts a string from UTF-8 based on configuration. - * @static * @note Currently, this is a lossy conversion, with unexpressable * characters being omitted. */ - function convertFromUTF8($str, $config, &$context) { - $encoding = $config->get('Core', 'Encoding'); + public static function convertFromUTF8($str, $config, $context) { + $encoding = $config->get('Core.Encoding'); if ($encoding === 'utf-8') return $str; static $iconv = null; if ($iconv === null) $iconv = function_exists('iconv'); - if ($escape = $config->get('Core', 'EscapeNonASCIICharacters')) { + if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { $str = HTMLPurifier_Encoder::convertToASCIIDumbLossless($str); } set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler')); - if ($iconv && !$config->get('Test', 'ForceNoIconv')) { + if ($iconv && !$config->get('Test.ForceNoIconv')) { // Undo our previous fix in convertToUTF8, otherwise iconv will barf $ascii_fix = HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding); if (!$escape && !empty($ascii_fix)) { @@ -372,10 +325,9 @@ class HTMLPurifier_Encoder } trigger_error('Encoding not supported', E_USER_ERROR); } - + /** * Lossless (character-wise) conversion of HTML to ASCII - * @static * @param $str UTF-8 string to be converted to ASCII * @returns ASCII encoded string with non-ASCII character entity-ized * @warning Adapted from MediaWiki, claiming fair use: this is a common @@ -390,7 +342,7 @@ class HTMLPurifier_Encoder * @note Sort of with cleanUTF8() but it assumes that $str is * well-formed UTF-8 */ - function convertToASCIIDumbLossless($str) { + public static function convertToASCIIDumbLossless($str) { $bytesleft = 0; $result = ''; $working = 0; @@ -420,19 +372,19 @@ 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. */ - function testEncodingSupportsASCII($encoding, $bypass = false) { + public static function testEncodingSupportsASCII($encoding, $bypass = false) { static $encodings = array(); if (!$bypass) { if (isset($encodings[$encoding])) return $encodings[$encoding]; @@ -449,8 +401,14 @@ class HTMLPurifier_Encoder 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) === '') { + $c = chr($i); // UTF-8 char + $r = iconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion + if ( + $r === '' || + // This line is needed for iconv implementations that do not + // omit characters that do not exist in the target character set + ($r === $c && iconv($encoding, 'UTF-8//IGNORE', $r) !== $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. @@ -461,7 +419,8 @@ class HTMLPurifier_Encoder $encodings[$encoding] = $ret; return $ret; } - - + + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/EntityLookup.php b/lib/htmlpurifier/HTMLPurifier/EntityLookup.php index 8204867be3c..b4dfce94c38 100644 --- a/lib/htmlpurifier/HTMLPurifier/EntityLookup.php +++ b/lib/htmlpurifier/HTMLPurifier/EntityLookup.php @@ -4,32 +4,30 @@ * Object that provides entity lookup table from entity name to character */ class HTMLPurifier_EntityLookup { - + /** * Assoc array of entity name to character represented. - * @public */ - var $table; - + public $table; + /** * Sets up the entity lookup table from the serialized file contents. * @note The serialized contents are versioned, but were generated * using the maintenance script generate_entity_file.php * @warning This is not in constructor to help enforce the Singleton */ - function setup($file = false) { + public function setup($file = false) { if (!$file) { $file = HTMLPURIFIER_PREFIX . '/HTMLPurifier/EntityLookup/entities.ser'; } $this->table = unserialize(file_get_contents($file)); } - + /** * Retrieves sole instance of the object. - * @static * @param Optional prototype of custom lookup table to overload with. */ - function instance($prototype = false) { + public static function instance($prototype = false) { // no references, since PHP doesn't copy unless modified static $instance = null; if ($prototype) { @@ -40,6 +38,7 @@ class HTMLPurifier_EntityLookup { } return $instance; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/EntityParser.php b/lib/htmlpurifier/HTMLPurifier/EntityParser.php index 25472413504..8c384472dc6 100644 --- a/lib/htmlpurifier/HTMLPurifier/EntityParser.php +++ b/lib/htmlpurifier/HTMLPurifier/EntityParser.php @@ -1,8 +1,5 @@ '"', 38 => '&', @@ -40,29 +34,27 @@ class HTMLPurifier_EntityParser 60 => '<', 62 => '>' ); - + /** * Stripped entity names to decimal conversion table for special entities. - * @protected */ - var $_special_ent2dec = + protected $_special_ent2dec = array( 'quot' => 34, 'amp' => 38, 'lt' => 60, 'gt' => 62 ); - + /** * Substitutes non-special entities with their parsed equivalents. Since * running this whenever you have parsed character is t3h 5uck, we run * it before everything else. - * - * @protected + * * @param $string String to have non-special entities parsed. * @returns Parsed string. */ - function substituteNonSpecialEntities($string) { + public function substituteNonSpecialEntities($string) { // it will try to detect missing semicolons, but don't rely on it return preg_replace_callback( $this->_substituteEntitiesRegex, @@ -70,29 +62,27 @@ class HTMLPurifier_EntityParser $string ); } - + /** * Callback function for substituteNonSpecialEntities() that does the work. - * - * @warning Though this is public in order to let the callback happen, - * calling it directly is not recommended. + * * @param $matches PCRE matches array, with 0 the entire match, and * either index 1, 2 or 3 set with a hex value, dec value, * or string (respectively). * @returns Replacement string. */ - - function nonSpecialEntityCallback($matches) { + + protected function nonSpecialEntityCallback($matches) { // replaces all but big five $entity = $matches[0]; $is_num = (@$matches[0][1] === '#'); if ($is_num) { $is_hex = (@$entity[2] === 'x'); $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; - + // abort for special characters if (isset($this->_special_dec2str[$code])) return $entity; - + return HTMLPurifier_Encoder::unichr($code); } else { if (isset($this->_special_ent2dec[$matches[3]])) return $entity; @@ -106,37 +96,34 @@ class HTMLPurifier_EntityParser } } } - + /** * Substitutes only special entities with their parsed equivalents. - * + * * @notice We try to avoid calling this function because otherwise, it * would have to be called a lot (for every parsed section). - * - * @protected + * * @param $string String to have non-special entities parsed. * @returns Parsed string. */ - function substituteSpecialEntities($string) { + public function substituteSpecialEntities($string) { return preg_replace_callback( $this->_substituteEntitiesRegex, array($this, 'specialEntityCallback'), $string); } - + /** * Callback function for substituteSpecialEntities() that does the work. - * + * * This callback has same syntax as nonSpecialEntityCallback(). - * - * @warning Though this is public in order to let the callback happen, - * calling it directly is not recommended. + * * @param $matches PCRE-style matches array, with 0 the entire match, and * either index 1, 2 or 3 set with a hex value, dec value, * or string (respectively). * @returns Replacement string. */ - function specialEntityCallback($matches) { + protected function specialEntityCallback($matches) { $entity = $matches[0]; $is_num = (@$matches[0][1] === '#'); if ($is_num) { @@ -151,6 +138,7 @@ class HTMLPurifier_EntityParser $entity; } } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Error.php b/lib/htmlpurifier/HTMLPurifier/Error.php deleted file mode 100644 index 2ca4d7323c9..00000000000 --- a/lib/htmlpurifier/HTMLPurifier/Error.php +++ /dev/null @@ -1,7 +0,0 @@ -locale =& $context->get('Locale'); - $this->generator =& $context->get('Generator'); - $this->context =& $context; + + /** + * Identifiers for the returned error array. These are purposely numeric + * so list() can be used. + */ + const LINENO = 0; + const SEVERITY = 1; + const MESSAGE = 2; + const CHILDREN = 3; + + protected $errors; + protected $_current; + protected $_stacks = array(array()); + protected $locale; + protected $generator; + protected $context; + + protected $lines = array(); + + public function __construct($context) { + $this->locale =& $context->get('Locale'); + $this->context = $context; + $this->_current =& $this->_stacks[0]; + $this->errors =& $this->_stacks[0]; } - + /** * Sends an error message to the collector for later use - * @param $line Integer line number, or HTMLPurifier_Token that caused error * @param $severity int Error severity, PHP error style (don't use E_USER_) * @param $msg string Error message text + * @param $subst1 string First substitution for $msg + * @param $subst2 string ... */ - function send($severity, $msg) { - + public function send($severity, $msg) { + $args = array(); if (func_num_args() > 2) { $args = func_get_args(); array_shift($args); unset($args[0]); } - + $token = $this->context->get('CurrentToken', true); $line = $token ? $token->line : $this->context->get('CurrentLine', true); + $col = $token ? $token->col : $this->context->get('CurrentCol', true); $attr = $this->context->get('CurrentAttr', true); - + // perform special substitutions, also add custom parameters $subst = array(); if (!is_null($token)) { @@ -48,71 +62,148 @@ class HTMLPurifier_ErrorCollector $subst['$CurrentAttr.Name'] = $attr; if (isset($token->attr[$attr])) $subst['$CurrentAttr.Value'] = $token->attr[$attr]; } - + if (empty($args)) { $msg = $this->locale->getMessage($msg); } else { $msg = $this->locale->formatMessage($msg, $args); } - + if (!empty($subst)) $msg = strtr($msg, $subst); - - $this->errors[] = array($line, $severity, $msg); + + // (numerically indexed) + $error = array( + self::LINENO => $line, + self::SEVERITY => $severity, + self::MESSAGE => $msg, + self::CHILDREN => array() + ); + $this->_current[] = $error; + + + // NEW CODE BELOW ... + + $struct = null; + // Top-level errors are either: + // TOKEN type, if $value is set appropriately, or + // "syntax" type, if $value is null + $new_struct = new HTMLPurifier_ErrorStruct(); + $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; + if ($token) $new_struct->value = clone $token; + if (is_int($line) && is_int($col)) { + if (isset($this->lines[$line][$col])) { + $struct = $this->lines[$line][$col]; + } else { + $struct = $this->lines[$line][$col] = $new_struct; + } + // These ksorts may present a performance problem + ksort($this->lines[$line], SORT_NUMERIC); + } else { + if (isset($this->lines[-1])) { + $struct = $this->lines[-1]; + } else { + $struct = $this->lines[-1] = $new_struct; + } + } + ksort($this->lines, SORT_NUMERIC); + + // Now, check if we need to operate on a lower structure + if (!empty($attr)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); + if (!$struct->value) { + $struct->value = array($attr, 'PUT VALUE HERE'); + } + } + if (!empty($cssprop)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); + if (!$struct->value) { + // if we tokenize CSS this might be a little more difficult to do + $struct->value = array($cssprop, 'PUT VALUE HERE'); + } + } + + // Ok, structs are all setup, now time to register the error + $struct->addError($severity, $msg); } - + /** * Retrieves raw error data for custom formatter to use - * @param List of arrays in format of array(Error message text, - * token that caused error, tokens surrounding token) + * @param List of arrays in format of array(line of error, + * error severity, error message, + * recursive sub-errors array) */ - function getRaw() { + public function getRaw() { return $this->errors; } - + /** * Default HTML formatting implementation for error messages * @param $config Configuration array, vital for HTML output nature + * @param $errors Errors array to display; used for recursion. */ - function getHTMLFormatted($config) { + public function getHTMLFormatted($config, $errors = null) { $ret = array(); - - $errors = $this->errors; - - // sort error array by line - // line numbers are enabled if they aren't explicitly disabled - if ($config->get('Core', 'MaintainLineNumbers') !== false) { - $has_line = array(); - $lines = array(); - $original_order = array(); - foreach ($errors as $i => $error) { - $has_line[] = (int) (bool) $error[0]; - $lines[] = $error[0]; - $original_order[] = $i; + + $this->generator = new HTMLPurifier_Generator($config, $this->context); + if ($errors === null) $errors = $this->errors; + + // 'At line' message needs to be removed + + // generation code for new structure goes here. It needs to be recursive. + foreach ($this->lines as $line => $col_array) { + if ($line == -1) continue; + foreach ($col_array as $col => $struct) { + $this->_renderStruct($ret, $struct, $line, $col); } - array_multisort($has_line, SORT_DESC, $lines, SORT_ASC, $original_order, SORT_ASC, $errors); } - - foreach ($errors as $error) { - list($line, $severity, $msg) = $error; - $string = ''; - $string .= '' . $this->locale->getErrorName($severity) . ': '; - $string .= $this->generator->escape($msg); - if ($line) { - // have javascript link generation that causes - // textarea to skip to the specified line - $string .= $this->locale->formatMessage( - 'ErrorCollector: At line', array('line' => $line)); - } - $ret[] = $string; + if (isset($this->lines[-1])) { + $this->_renderStruct($ret, $this->lines[-1]); } - + if (empty($errors)) { return '

' . $this->locale->getMessage('ErrorCollector: No errors') . '

'; } else { return '
  • ' . implode('
  • ', $ret) . '
'; } - + } - + + private function _renderStruct(&$ret, $struct, $line = null, $col = null) { + $stack = array($struct); + $context_stack = array(array()); + while ($current = array_pop($stack)) { + $context = array_pop($context_stack); + foreach ($current->errors as $error) { + list($severity, $msg) = $error; + $string = ''; + $string .= '
'; + // W3C uses an icon to indicate the severity of the error. + $error = $this->locale->getErrorName($severity); + $string .= "$error "; + if (!is_null($line) && !is_null($col)) { + $string .= "Line $line, Column $col: "; + } else { + $string .= 'End of Document: '; + } + $string .= '' . $this->generator->escape($msg) . ' '; + $string .= '
'; + // Here, have a marker for the character on the column appropriate. + // Be sure to clip extremely long lines. + //$string .= '
';
+                //$string .= '';
+                //$string .= '
'; + $ret[] = $string; + } + foreach ($current->children as $type => $array) { + $context[] = $current; + $stack = array_merge($stack, array_reverse($array, true)); + for ($i = count($array); $i > 0; $i--) { + $context_stack[] = $context; + } + } + } + } + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/ErrorStruct.php b/lib/htmlpurifier/HTMLPurifier/ErrorStruct.php new file mode 100644 index 00000000000..9bc8996ec19 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/ErrorStruct.php @@ -0,0 +1,60 @@ +children[$type][$id])) { + $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); + $this->children[$type][$id]->type = $type; + } + return $this->children[$type][$id]; + } + + public function addError($severity, $message) { + $this->errors[] = array($severity, $message); + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Exception.php b/lib/htmlpurifier/HTMLPurifier/Exception.php new file mode 100644 index 00000000000..be85b4c560e --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/Exception.php @@ -0,0 +1,12 @@ +preFilter, * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, * 1->postFilter. + * + * @note Methods are not declared abstract as it is perfectly legitimate + * for an implementation not to want anything to happen on a step */ class HTMLPurifier_Filter { - + /** * Name of the filter for identification purposes */ - var $name; - + public $name; + /** - * Pre-processor function, handles HTML before HTML Purifier + * Pre-processor function, handles HTML before HTML Purifier */ - function preFilter($html, $config, &$context) {} - + public function preFilter($html, $config, $context) { + return $html; + } + /** * Post-processor function, handles HTML after HTML Purifier */ - function postFilter($html, $config, &$context) {} - + public function postFilter($html, $config, $context) { + return $html; + } + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php b/lib/htmlpurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php new file mode 100644 index 00000000000..bbf78a6630a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -0,0 +1,135 @@ + blocks from input HTML, cleans them up + * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') + * so they can be used elsewhere in the document. + * + * @note + * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for + * sample usage. + * + * @note + * This filter can also be used on stylesheets not included in the + * document--something purists would probably prefer. Just directly + * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() + */ +class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter +{ + + public $name = 'ExtractStyleBlocks'; + private $_styleMatches = array(); + private $_tidy; + + public function __construct() { + $this->_tidy = new csstidy(); + } + + /** + * Save the contents of CSS blocks to style matches + * @param $matches preg_replace style $matches array + */ + protected function styleCallback($matches) { + $this->_styleMatches[] = $matches[1]; + } + + /** + * Removes inline #isU', array($this, 'styleCallback'), $html); + $style_blocks = $this->_styleMatches; + $this->_styleMatches = array(); // reset + $context->register('StyleBlocks', $style_blocks); // $context must not be reused + if ($this->_tidy) { + foreach ($style_blocks as &$style) { + $style = $this->cleanCSS($style, $config, $context); + } + } + return $html; + } + + /** + * Takes CSS (the stuff found in in a font-family prop). + if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { + $css = str_replace( + array('<', '>', '&'), + array('\3C ', '\3E ', '\26 '), + $css + ); + } + return $css; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Filter/YouTube.php b/lib/htmlpurifier/HTMLPurifier/Filter/YouTube.php index 4f63ad6d560..23df221eaa3 100644 --- a/lib/htmlpurifier/HTMLPurifier/Filter/YouTube.php +++ b/lib/htmlpurifier/HTMLPurifier/Filter/YouTube.php @@ -1,33 +1,39 @@ ]+>.+?'. - 'http://www.youtube.com/v/([A-Za-z0-9\-_]+).+?#s'; + 'http://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; $pre_replace = '\1'; return preg_replace($pre_regex, $pre_replace, $html); } - - function postFilter($html, $config, &$context) { - $post_regex = '#([A-Za-z0-9\-_]+)#'; - $post_replace = ''. - ''. - ''. + + public function postFilter($html, $config, $context) { + $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; + return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); + } + + protected function armorUrl($url) { + return str_replace('--', '--', $url); + } + + protected function postFilterCallback($matches) { + $url = $this->armorUrl($matches[1]); + return ''. + ''. ''. ''; - return preg_replace($post_regex, $post_replace, $html); + } - } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Generator.php b/lib/htmlpurifier/HTMLPurifier/Generator.php index 5322b8c20b5..4a62417271e 100644 --- a/lib/htmlpurifier/HTMLPurifier/Generator.php +++ b/lib/htmlpurifier/HTMLPurifier/Generator.php @@ -1,191 +1,201 @@ - Determines whether or not to run Tidy on the final output for pretty - formatting reasons, such as indentation and wrap. -

-

- This can greatly improve readability for editors who are hand-editing - the HTML, but is by no means necessary as HTML Purifier has already - fixed all major errors the HTML may have had. Tidy is a non-default - extension, and this directive will silently fail if Tidy is not - available. -

-

- If you are looking to make the overall look of your page's source - better, I recommend running Tidy on the entire page rather than just - user-content (after all, the indentation relative to the containing - blocks will be incorrect). -

-

- This directive was available since 1.1.1. -

-HTML -); -HTMLPurifier_ConfigSchema::defineAlias('Core', 'TidyFormat', 'Output', 'TidyFormat'); - -HTMLPurifier_ConfigSchema::define('Output', 'Newline', null, 'string/null', ' -

- Newline string to format final output with. If left null, HTML Purifier - will auto-detect the default newline type of the system and use that; - you can manually override it here. Remember, \r\n is Windows, \r - is Mac, and \n is Unix. This directive was available since 2.0.1. -

-'); - /** * Generates HTML from tokens. * @todo Refactor interface so that configuration/context is determined - * upon instantiation, no need for messy generateFromTokens() calls + * upon instantiation, no need for messy generateFromTokens() calls + * @todo Make some of the more internal functions protected, and have + * unit tests work around that */ class HTMLPurifier_Generator { - + /** - * Bool cache of %HTML.XHTML - * @private + * Whether or not generator should produce XML output */ - var $_xhtml = true; - + private $_xhtml = true; + /** - * Bool cache of %Output.CommentScriptContents - * @private + * :HACK: Whether or not generator should comment the insides of )#si', - array('HTMLPurifier_Lexer_DirectLex', 'scriptCallback'), $html); + array($this, 'scriptCallback'), $html); } - + $html = $this->normalize($html, $config, $context); - + $cursor = 0; // our location in the text $inside_tag = false; // whether or not we're parsing the inside of a tag $array = array(); // result array - - $maintain_line_numbers = $config->get('Core', 'MaintainLineNumbers'); - + + // This is also treated to mean maintain *column* numbers too + $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); + if ($maintain_line_numbers === null) { // automatically determine line numbering by checking // if error collection is on - $maintain_line_numbers = $config->get('Core', 'CollectErrors'); + $maintain_line_numbers = $config->get('Core.CollectErrors'); + } + + if ($maintain_line_numbers) { + $current_line = 1; + $current_col = 0; + $length = strlen($html); + } else { + $current_line = false; + $current_col = false; + $length = false; } - - if ($maintain_line_numbers) $current_line = 1; - else $current_line = false; $context->register('CurrentLine', $current_line); + $context->register('CurrentCol', $current_col); $nl = "\n"; // how often to manually recalculate. This will ALWAYS be right, // but it's pretty wasteful. Set to 0 to turn off - $synchronize_interval = $config->get('Core', 'DirectLexLineNumberSyncInterval'); - + $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); + $e = false; - if ($config->get('Core', 'CollectErrors')) { + if ($config->get('Core.CollectErrors')) { $e =& $context->get('ErrorCollector'); } - - // infinite loop protection - // has to be pretty big, since html docs can be big - // we're allow two hundred thousand tags... more than enough? - // NOTE: this is also used for synchronization, so watch out + + // for testing synchronization $loops = 0; - - while(true) { - - // infinite loop protection - if (++$loops > 200000) return array(); - - // recalculate lines - if ( - $maintain_line_numbers && // line number tracking is on - $synchronize_interval && // synchronization is on - $cursor > 0 && // cursor is further than zero - $loops % $synchronize_interval === 0 // time to synchronize! - ) { - $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); + + while(++$loops) { + + // $cursor is either at the start of a token, or inside of + // a tag (i.e. there was a < immediately before it), as indicated + // by $inside_tag + + if ($maintain_line_numbers) { + + // $rcursor, however, is always at the start of a token. + $rcursor = $cursor - (int) $inside_tag; + + // Column number is cheap, so we calculate it every round. + // We're interested at the *end* of the newline string, so + // we need to add strlen($nl) == 1 to $nl_pos before subtracting it + // from our "rcursor" position. + $nl_pos = strrpos($html, $nl, $rcursor - $length); + $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); + + // recalculate lines + if ( + $synchronize_interval && // synchronization is on + $cursor > 0 && // cursor is further than zero + $loops % $synchronize_interval === 0 // time to synchronize! + ) { + $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); + } + } - + $position_next_lt = strpos($html, '<', $cursor); $position_next_gt = strpos($html, '>', $cursor); - + // triggers on "asdf" but not "asdf " // special case to set up context if ($position_next_lt === $cursor) { $inside_tag = true; $cursor++; } - + if (!$inside_tag && $position_next_lt !== false) { // We are not inside tag and there still is another tag to parse $token = new @@ -123,7 +127,7 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer ) ); if ($maintain_line_numbers) { - $token->line = $current_line; + $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); } $array[] = $token; @@ -143,32 +147,32 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer ) ) ); - if ($maintain_line_numbers) $token->line = $current_line; + if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col); $array[] = $token; break; } elseif ($inside_tag && $position_next_gt !== false) { // We are in tag and it is well formed // Grab the internals of the tag $strlen_segment = $position_next_gt - $cursor; - + if ($strlen_segment < 1) { // there's nothing to process! $token = new HTMLPurifier_Token_Text('<'); $cursor++; continue; } - + $segment = substr($html, $cursor, $strlen_segment); - + if ($segment === false) { // somehow, we attempted to access beyond the end of // the string, defense-in-depth, reported by Nate Abele break; } - + // Check if it's a comment if ( - strncmp('!--', $segment, 3) === 0 + substr($segment, 0, 3) === '!--' ) { // re-determine segment length, looking for --> $position_comment_end = strpos($html, '-->', $cursor); @@ -184,9 +188,14 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer } $strlen_segment = $position_comment_end - $cursor; $segment = substr($html, $cursor, $strlen_segment); - $token = new HTMLPurifier_Token_Comment(substr($segment, 3)); + $token = new + HTMLPurifier_Token_Comment( + substr( + $segment, 3, $strlen_segment - 3 + ) + ); if ($maintain_line_numbers) { - $token->line = $current_line; + $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); } $array[] = $token; @@ -194,14 +203,14 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer $inside_tag = false; continue; } - + // Check if it's an end tag $is_end_tag = (strpos($segment,'/') === 0); if ($is_end_tag) { $type = substr($segment, 1); $token = new HTMLPurifier_Token_End($type); if ($maintain_line_numbers) { - $token->line = $current_line; + $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; @@ -209,31 +218,23 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer $cursor = $position_next_gt + 1; continue; } - + // Check leading character is alnum, if not, we may // have accidently grabbed an emoticon. Translate into // text and go our merry way if (!ctype_alpha($segment[0])) { // XML: $segment[0] !== '_' && $segment[0] !== ':' if ($e) $e->send(E_NOTICE, 'Lexer: Unescaped lt'); - $token = new - HTMLPurifier_Token_Text( - '<' . - $this->parseData( - $segment - ) . - '>' - ); + $token = new HTMLPurifier_Token_Text('<'); if ($maintain_line_numbers) { - $token->line = $current_line; + $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; - $cursor = $position_next_gt + 1; $inside_tag = false; continue; } - + // Check if it is explicitly self closing, if so, remove // trailing slash. Remember, we could have a tag like
, so // any later token processing scripts must convert improperly @@ -243,10 +244,10 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer $strlen_segment--; $segment = substr($segment, 0, $strlen_segment); } - + // Check if there are any attributes $position_first_space = strcspn($segment, $this->_whitespace); - + if ($position_first_space >= $strlen_segment) { if ($is_self_closing) { $token = new HTMLPurifier_Token_Empty($segment); @@ -254,7 +255,7 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer $token = new HTMLPurifier_Token_Start($segment); } if ($maintain_line_numbers) { - $token->line = $current_line; + $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; @@ -262,7 +263,7 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer $cursor = $position_next_gt + 1; continue; } - + // Grab out all the data $type = substr($segment, 0, $position_first_space); $attribute_string = @@ -279,14 +280,14 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer } else { $attr = array(); } - + if ($is_self_closing) { $token = new HTMLPurifier_Token_Empty($type, $attr); } else { $token = new HTMLPurifier_Token_Start($type, $attr); } if ($maintain_line_numbers) { - $token->line = $current_line; + $token->rawPosition($current_line, $current_col); $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; @@ -303,22 +304,23 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer substr($html, $cursor) ) ); - if ($maintain_line_numbers) $token->line = $current_line; + if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col); // no cursor scroll? Hmm... $array[] = $token; break; } break; } - + $context->destroy('CurrentLine'); + $context->destroy('CurrentCol'); return $array; } - + /** - * PHP 4 compatible substr_count that implements offset and length + * PHP 5.0.x compatible substr_count that implements offset and length */ - function substrCount($haystack, $needle, $offset, $length) { + protected function substrCount($haystack, $needle, $offset, $length) { static $oldVersion; if ($oldVersion === null) { $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); @@ -330,23 +332,23 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer return substr_count($haystack, $needle, $offset, $length); } } - + /** * Takes the inside of an HTML tag and makes an assoc array of attributes. - * + * * @param $string Inside of tag excluding name. * @returns Assoc array of attributes. */ - function parseAttributeString($string, $config, &$context) { + public function parseAttributeString($string, $config, $context) { $string = (string) $string; // quick typecast - + if ($string == '') return array(); // no attributes - + $e = false; - if ($config->get('Core', 'CollectErrors')) { + if ($config->get('Core.CollectErrors')) { $e =& $context->get('ErrorCollector'); } - + // let's see if we can abort as quickly as possible // one equal sign, no spaces => one attribute $num_equal = substr_count($string, '='); @@ -365,10 +367,10 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer if (!$quoted_value) return array($key => ''); $first_char = @$quoted_value[0]; $last_char = @$quoted_value[strlen($quoted_value)-1]; - + $same_quote = ($first_char == $last_char); $open_quote = ($first_char == '"' || $first_char == "'"); - + if ( $same_quote && $open_quote) { // well behaved $value = substr($quoted_value, 1, strlen($quoted_value) - 2); @@ -384,75 +386,67 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer if ($value === false) $value = ''; return array($key => $value); } - + // setup loop environment $array = array(); // return assoc array of attributes $cursor = 0; // current position in string (moves forward) $size = strlen($string); // size of the string (stays the same) - + // if we have unquoted attributes, the parser expects a terminating // space, so let's guarantee that there's always a terminating space. $string .= ' '; - - // infinite loop protection - $loops = 0; + while(true) { - - // infinite loop protection - if (++$loops > 1000) { - trigger_error('Infinite loop detected in attribute parsing', E_USER_WARNING); - return array(); - } - + if ($cursor >= $size) { break; } - + $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); // grab the key - + $key_begin = $cursor; //we're currently at the start of the key - + // scroll past all characters that are the key (not whitespace or =) $cursor += strcspn($string, $this->_whitespace . '=', $cursor); - + $key_end = $cursor; // now at the end of the key - + $key = substr($string, $key_begin, $key_end - $key_begin); - + if (!$key) { if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key'); $cursor += strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop continue; // empty key } - + // scroll past all whitespace $cursor += strspn($string, $this->_whitespace, $cursor); - + if ($cursor >= $size) { $array[$key] = $key; break; } - + // if the next character is an equal sign, we've got a regular // pair, otherwise, it's a bool attribute $first_char = @$string[$cursor]; - + if ($first_char == '=') { // key="value" - + $cursor++; $cursor += strspn($string, $this->_whitespace, $cursor); - + if ($cursor === false) { $array[$key] = ''; break; } - + // we might be in front of a quote right now - + $char = @$string[$cursor]; - + if ($char == '"' || $char == "'") { // it's quoted, end bound is $char $cursor++; @@ -465,18 +459,18 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer $cursor += strcspn($string, $this->_whitespace, $cursor); $value_end = $cursor; } - + // we reached a premature end if ($cursor === false) { $cursor = $size; $value_end = $cursor; } - + $value = substr($string, $value_begin, $value_end - $value_begin); if ($value === false) $value = ''; $array[$key] = $this->parseData($value); $cursor++; - + } else { // boolattr if ($key !== '') { @@ -485,11 +479,12 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer // purely theoretical if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key'); } - + } } return $array; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Lexer/PEARSax3.php b/lib/htmlpurifier/HTMLPurifier/Lexer/PEARSax3.php index 3888229b075..1d358c7b6be 100644 --- a/lib/htmlpurifier/HTMLPurifier/Lexer/PEARSax3.php +++ b/lib/htmlpurifier/HTMLPurifier/Lexer/PEARSax3.php @@ -1,101 +1,122 @@ tokens = array(); - + $this->last_token_was_empty = false; + $string = $this->normalize($string, $config, $context); - + + $this->parent_handler = set_error_handler(array($this, 'muteStrictErrorHandler')); + $parser = new XML_HTMLSax3(); $parser->set_object($this); $parser->set_element_handler('openHandler','closeHandler'); $parser->set_data_handler('dataHandler'); $parser->set_escape_handler('escapeHandler'); - + // doesn't seem to work correctly for attributes $parser->set_option('XML_OPTION_ENTITIES_PARSED', 1); - + $parser->parse($string); - + + restore_error_handler(); + return $this->tokens; - + } - + /** * Open tag event handler, interface is defined by PEAR package. */ - function openHandler(&$parser, $name, $attrs, $closed) { + public function openHandler(&$parser, $name, $attrs, $closed) { // entities are not resolved in attrs foreach ($attrs as $key => $attr) { $attrs[$key] = $this->parseData($attr); } if ($closed) { $this->tokens[] = new HTMLPurifier_Token_Empty($name, $attrs); + $this->last_token_was_empty = true; } else { $this->tokens[] = new HTMLPurifier_Token_Start($name, $attrs); } + $this->stack[] = $name; return true; } - + /** * Close tag event handler, interface is defined by PEAR package. */ - function closeHandler(&$parser, $name) { + public function closeHandler(&$parser, $name) { // HTMLSax3 seems to always send empty tags an extra close tag // check and ignore if you see it: // [TESTME] to make sure it doesn't overreach - if ($this->tokens[count($this->tokens)-1]->type == 'empty') { + if ($this->last_token_was_empty) { + $this->last_token_was_empty = false; return true; } $this->tokens[] = new HTMLPurifier_Token_End($name); + if (!empty($this->stack)) array_pop($this->stack); return true; } - + /** * Data event handler, interface is defined by PEAR package. */ - function dataHandler(&$parser, $data) { + public function dataHandler(&$parser, $data) { + $this->last_token_was_empty = false; $this->tokens[] = new HTMLPurifier_Token_Text($data); return true; } - + /** * Escaped text handler, interface is defined by PEAR package. */ - function escapeHandler(&$parser, $data) { + public function escapeHandler(&$parser, $data) { if (strpos($data, '--') === 0) { - $this->tokens[] = new HTMLPurifier_Token_Comment($data); + // remove trailing and leading double-dashes + $data = substr($data, 2); + if (strlen($data) >= 2 && substr($data, -2) == "--") { + $data = substr($data, 0, -2); + } + if (isset($this->stack[sizeof($this->stack) - 1]) && + $this->stack[sizeof($this->stack) - 1] == "style") { + $this->tokens[] = new HTMLPurifier_Token_Text($data); + } else { + $this->tokens[] = new HTMLPurifier_Token_Comment($data); + } + $this->last_token_was_empty = false; } // CDATA is handled elsewhere, but if it was handled here: //if (strpos($data, '[CDATA[') === 0) { @@ -104,6 +125,15 @@ class HTMLPurifier_Lexer_PEARSax3 extends HTMLPurifier_Lexer //} return true; } - + + /** + * An error handler that mutes strict errors + */ + public function muteStrictErrorHandler($errno, $errstr, $errfile=null, $errline=null, $errcontext=null) { + if ($errno == E_STRICT) return; + return call_user_func($this->parent_handler, $errno, $errstr, $errfile, $errline, $errcontext); + } + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Lexer/PH5P.php b/lib/htmlpurifier/HTMLPurifier/Lexer/PH5P.php index b6762379141..fa1bf973e0b 100644 --- a/lib/htmlpurifier/HTMLPurifier/Lexer/PH5P.php +++ b/lib/htmlpurifier/HTMLPurifier/Lexer/PH5P.php @@ -1,3886 +1,3906 @@ -normalize($html, $config, $context); - $html = $this->wrapHTML( $html, $config, $context); - $parser = new HTML5($html); - $doc = $parser->save(); - $tokens = array(); - $this->tokenizeDOM( - $doc->getElementsByTagName('html')->item(0)-> // - getElementsByTagName('body')->item(0)-> // - getElementsByTagName('div')->item(0) //
- , $tokens); - return $tokens; - } - -} - -/* - -Copyright 2007 Jeroen van der Meer - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -class HTML5 { - private $data; - private $char; - private $EOF; - private $state; - private $tree; - private $token; - private $content_model; - private $escape = false; - private $entities = array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute', - 'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;', - 'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;', - 'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;', - 'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;', - 'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;', - 'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;', - 'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;', - 'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;', - 'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN', - 'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;', - 'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;', - 'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig', - 'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;', - 'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;', - 'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil', - 'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;', - 'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;', - 'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;', - 'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth', - 'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12', - 'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt', - 'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc', - 'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;', - 'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;', - 'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;', - 'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro', - 'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;', - 'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;', - 'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;', - 'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash', - 'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;', - 'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;', - 'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;', - 'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;', - 'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;', - 'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;', - 'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;', - 'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;', - 'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc', - 'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;', - 'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;'); - - const PCDATA = 0; - const RCDATA = 1; - const CDATA = 2; - const PLAINTEXT = 3; - - const DOCTYPE = 0; - const STARTTAG = 1; - const ENDTAG = 2; - const COMMENT = 3; - const CHARACTR = 4; - const EOF = 5; - - public function __construct($data) { - $data = str_replace("\r\n", "\n", $data); - $date = str_replace("\r", null, $data); - - $this->data = $data; - $this->char = -1; - $this->EOF = strlen($data); - $this->tree = new HTML5TreeConstructer; - $this->content_model = self::PCDATA; - - $this->state = 'data'; - - while($this->state !== null) { - $this->{$this->state.'State'}(); - } - } - - public function save() { - return $this->tree->save(); - } - - private function char() { - return ($this->char < $this->EOF) - ? $this->data[$this->char] - : false; - } - - private function character($s, $l = 0) { - if($s + $l < $this->EOF) { - if($l === 0) { - return $this->data[$s]; - } else { - return substr($this->data, $s, $l); - } - } - } - - private function characters($char_class, $start) { - return preg_replace('#^(['.$char_class.']+).*#s', '\\1', substr($this->data, $start)); - } - - private function dataState() { - // Consume the next input character - $this->char++; - $char = $this->char(); - - if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { - /* U+0026 AMPERSAND (&) - When the content model flag is set to one of the PCDATA or RCDATA - states: switch to the entity data state. Otherwise: treat it as per - the "anything else" entry below. */ - $this->state = 'entityData'; - - } elseif($char === '-') { - /* If the content model flag is set to either the RCDATA state or - the CDATA state, and the escape flag is false, and there are at - least three characters before this one in the input stream, and the - last four characters in the input stream, including this one, are - U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, - and U+002D HYPHEN-MINUS (""), - set the escape flag to false. */ - if(($this->content_model === self::RCDATA || - $this->content_model === self::CDATA) && $this->escape === true && - $this->character($this->char, 3) === '-->') { - $this->escape = false; - } - - /* In any case, emit the input character as a character token. - Stay in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - } elseif($this->char === $this->EOF) { - /* EOF - Emit an end-of-file token. */ - $this->EOF(); - - } elseif($this->content_model === self::PLAINTEXT) { - /* When the content model flag is set to the PLAINTEXT state - THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of - the text and emit it as a character token. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => substr($this->data, $this->char) - )); - - $this->EOF(); - - } else { - /* Anything else - THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that - otherwise would also be treated as a character token and emit it - as a single character token. Stay in the data state. */ - $len = strcspn($this->data, '<&', $this->char); - $char = substr($this->data, $this->char, $len); - $this->char += $len - 1; - - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - $this->state = 'data'; - } - } - - private function entityDataState() { - // Attempt to consume an entity. - $entity = $this->entity(); - - // If nothing is returned, emit a U+0026 AMPERSAND character token. - // Otherwise, emit the character token that was returned. - $char = (!$entity) ? '&' : $entity; - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - // Finally, switch to the data state. - $this->state = 'data'; - } - - private function tagOpenState() { - switch($this->content_model) { - case self::RCDATA: - case self::CDATA: - /* If the next input character is a U+002F SOLIDUS (/) character, - consume it and switch to the close tag open state. If the next - input character is not a U+002F SOLIDUS (/) character, emit a - U+003C LESS-THAN SIGN character token and switch to the data - state to process the next input character. */ - if($this->character($this->char + 1) === '/') { - $this->char++; - $this->state = 'closeTagOpen'; - - } else { - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<' - )); - - $this->state = 'data'; - } - break; - - case self::PCDATA: - // If the content model flag is set to the PCDATA state - // Consume the next input character: - $this->char++; - $char = $this->char(); - - if($char === '!') { - /* U+0021 EXCLAMATION MARK (!) - Switch to the markup declaration open state. */ - $this->state = 'markupDeclarationOpen'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Switch to the close tag open state. */ - $this->state = 'closeTagOpen'; - - } elseif(preg_match('/^[A-Za-z]$/', $char)) { - /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z - Create a new start tag token, set its tag name to the lowercase - version of the input character (add 0x0020 to the character's code - point), then switch to the tag name state. (Don't emit the token - yet; further details will be filled in before it is emitted.) */ - $this->token = array( - 'name' => strtolower($char), - 'type' => self::STARTTAG, - 'attr' => array() - ); - - $this->state = 'tagName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Parse error. Emit a U+003C LESS-THAN SIGN character token and a - U+003E GREATER-THAN SIGN character token. Switch to the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<>' - )); - - $this->state = 'data'; - - } elseif($char === '?') { - /* U+003F QUESTION MARK (?) - Parse error. Switch to the bogus comment state. */ - $this->state = 'bogusComment'; - - } else { - /* Anything else - Parse error. Emit a U+003C LESS-THAN SIGN character token and - reconsume the current input character in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<' - )); - - $this->char--; - $this->state = 'data'; - } - break; - } - } - - private function closeTagOpenState() { - $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); - $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; - - if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && - (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/', - $this->character($this->char + 1 + strlen($next_node))) || $this->EOF === $this->char)))) { - /* If the content model flag is set to the RCDATA or CDATA states then - examine the next few characters. If they do not match the tag name of - the last start tag token emitted (case insensitively), or if they do but - they are not immediately followed by one of the following characters: - * U+0009 CHARACTER TABULATION - * U+000A LINE FEED (LF) - * U+000B LINE TABULATION - * U+000C FORM FEED (FF) - * U+0020 SPACE - * U+003E GREATER-THAN SIGN (>) - * U+002F SOLIDUS (/) - * EOF - ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character - token, a U+002F SOLIDUS character token, and switch to the data state - to process the next input character. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => 'state = 'data'; - - } else { - /* Otherwise, if the content model flag is set to the PCDATA state, - or if the next few characters do match that tag name, consume the - next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[A-Za-z]$/', $char)) { - /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z - Create a new end tag token, set its tag name to the lowercase version - of the input character (add 0x0020 to the character's code point), then - switch to the tag name state. (Don't emit the token yet; further details - will be filled in before it is emitted.) */ - $this->token = array( - 'name' => strtolower($char), - 'type' => self::ENDTAG - ); - - $this->state = 'tagName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Parse error. Switch to the data state. */ - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F - SOLIDUS character token. Reconsume the EOF character in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => 'char--; - $this->state = 'data'; - - } else { - /* Parse error. Switch to the bogus comment state. */ - $this->state = 'bogusComment'; - } - } - } - - private function tagNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } else { - /* Anything else - Append the current input character to the current tag token's tag name. - Stay in the tag name state. */ - $this->token['name'] .= strtolower($char); - $this->state = 'tagName'; - } - } - - private function beforeAttributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Stay in the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Start a new attribute in the current tag token. Set that attribute's - name to the current input character, and its value to the empty string. - Switch to the attribute name state. */ - $this->token['attr'][] = array( - 'name' => strtolower($char), - 'value' => null - ); - - $this->state = 'attributeName'; - } - } - - private function attributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute name state. */ - $this->state = 'afterAttributeName'; - - } elseif($char === '=') { - /* U+003D EQUALS SIGN (=) - Switch to the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/' && $this->character($this->char + 1) !== '>') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's name. - Stay in the attribute name state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['name'] .= strtolower($char); - - $this->state = 'attributeName'; - } - } - - private function afterAttributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the after attribute name state. */ - $this->state = 'afterAttributeName'; - - } elseif($char === '=') { - /* U+003D EQUALS SIGN (=) - Switch to the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/' && $this->character($this->char + 1) !== '>') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the - before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Start a new attribute in the current tag token. Set that attribute's - name to the current input character, and its value to the empty string. - Switch to the attribute name state. */ - $this->token['attr'][] = array( - 'name' => strtolower($char), - 'value' => null - ); - - $this->state = 'attributeName'; - } - } - - private function beforeAttributeValueState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '"') { - /* U+0022 QUOTATION MARK (") - Switch to the attribute value (double-quoted) state. */ - $this->state = 'attributeValueDoubleQuoted'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the attribute value (unquoted) state and reconsume - this input character. */ - $this->char--; - $this->state = 'attributeValueUnquoted'; - - } elseif($char === '\'') { - /* U+0027 APOSTROPHE (') - Switch to the attribute value (single-quoted) state. */ - $this->state = 'attributeValueSingleQuoted'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Switch to the attribute value (unquoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueUnquoted'; - } - } - - private function attributeValueDoubleQuotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if($char === '"') { - /* U+0022 QUOTATION MARK (") - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState('double'); - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the character - in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (double-quoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueDoubleQuoted'; - } - } - - private function attributeValueSingleQuotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if($char === '\'') { - /* U+0022 QUOTATION MARK (') - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState('single'); - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the character - in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (single-quoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueSingleQuoted'; - } - } - - private function attributeValueUnquotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState(); - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (unquoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueUnquoted'; - } - } - - private function entityInAttributeValueState() { - // Attempt to consume an entity. - $entity = $this->entity(); - - // If nothing is returned, append a U+0026 AMPERSAND character to the - // current attribute's value. Otherwise, emit the character token that - // was returned. - $char = (!$entity) - ? '&' - : $entity; - - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - } - - private function bogusCommentState() { - /* Consume every character up to the first U+003E GREATER-THAN SIGN - character (>) or the end of the file (EOF), whichever comes first. Emit - a comment token whose data is the concatenation of all the characters - starting from and including the character that caused the state machine - to switch into the bogus comment state, up to and including the last - consumed character before the U+003E character, if any, or up to the - end of the file otherwise. (If the comment was started by the end of - the file (EOF), the token is empty.) */ - $data = $this->characters('^>', $this->char); - $this->emitToken(array( - 'data' => $data, - 'type' => self::COMMENT - )); - - $this->char += strlen($data); - - /* Switch to the data state. */ - $this->state = 'data'; - - /* If the end of the file was reached, reconsume the EOF character. */ - if($this->char === $this->EOF) { - $this->char = $this->EOF - 1; - } - } - - private function markupDeclarationOpenState() { - /* If the next two characters are both U+002D HYPHEN-MINUS (-) - characters, consume those two characters, create a comment token whose - data is the empty string, and switch to the comment state. */ - if($this->character($this->char + 1, 2) === '--') { - $this->char += 2; - $this->state = 'comment'; - $this->token = array( - 'data' => null, - 'type' => self::COMMENT - ); - - /* Otherwise if the next seven chacacters are a case-insensitive match - for the word "DOCTYPE", then consume those characters and switch to the - DOCTYPE state. */ - } elseif(strtolower($this->character($this->char + 1, 7)) === 'doctype') { - $this->char += 7; - $this->state = 'doctype'; - - /* Otherwise, is is a parse error. Switch to the bogus comment state. - The next character that is consumed, if any, is the first character - that will be in the comment. */ - } else { - $this->char++; - $this->state = 'bogusComment'; - } - } - - private function commentState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - /* U+002D HYPHEN-MINUS (-) */ - if($char === '-') { - /* Switch to the comment dash state */ - $this->state = 'commentDash'; - - /* EOF */ - } elseif($this->char === $this->EOF) { - /* Parse error. Emit the comment token. Reconsume the EOF character - in the data state. */ - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - /* Anything else */ - } else { - /* Append the input character to the comment token's data. Stay in - the comment state. */ - $this->token['data'] .= $char; - } - } - - private function commentDashState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - /* U+002D HYPHEN-MINUS (-) */ - if($char === '-') { - /* Switch to the comment end state */ - $this->state = 'commentEnd'; - - /* EOF */ - } elseif($this->char === $this->EOF) { - /* Parse error. Emit the comment token. Reconsume the EOF character - in the data state. */ - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - /* Anything else */ - } else { - /* Append a U+002D HYPHEN-MINUS (-) character and the input - character to the comment token's data. Switch to the comment state. */ - $this->token['data'] .= '-'.$char; - $this->state = 'comment'; - } - } - - private function commentEndState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '-') { - $this->token['data'] .= '-'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['data'] .= '--'.$char; - $this->state = 'comment'; - } - } - - private function doctypeState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - $this->state = 'beforeDoctypeName'; - - } else { - $this->char--; - $this->state = 'beforeDoctypeName'; - } - } - - private function beforeDoctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - // Stay in the before DOCTYPE name state. - - } elseif(preg_match('/^[a-z]$/', $char)) { - $this->token = array( - 'name' => strtoupper($char), - 'type' => self::DOCTYPE, - 'error' => true - ); - - $this->state = 'doctypeName'; - - } elseif($char === '>') { - $this->emitToken(array( - 'name' => null, - 'type' => self::DOCTYPE, - 'error' => true - )); - - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken(array( - 'name' => null, - 'type' => self::DOCTYPE, - 'error' => true - )); - - $this->char--; - $this->state = 'data'; - - } else { - $this->token = array( - 'name' => $char, - 'type' => self::DOCTYPE, - 'error' => true - ); - - $this->state = 'doctypeName'; - } - } - - private function doctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - $this->state = 'AfterDoctypeName'; - - } elseif($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif(preg_match('/^[a-z]$/', $char)) { - $this->token['name'] .= strtoupper($char); - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['name'] .= $char; - } - - $this->token['error'] = ($this->token['name'] === 'HTML') - ? false - : true; - } - - private function afterDoctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - // Stay in the DOCTYPE name state. - - } elseif($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['error'] = true; - $this->state = 'bogusDoctype'; - } - } - - private function bogusDoctypeState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - // Stay in the bogus DOCTYPE state. - } - } - - private function entity() { - $start = $this->char; - - // This section defines how to consume an entity. This definition is - // used when parsing entities in text and in attributes. - - // The behaviour depends on the identity of the next character (the - // one immediately after the U+0026 AMPERSAND character): - - switch($this->character($this->char + 1)) { - // U+0023 NUMBER SIGN (#) - case '#': - - // The behaviour further depends on the character after the - // U+0023 NUMBER SIGN: - switch($this->character($this->char + 1)) { - // U+0078 LATIN SMALL LETTER X - // U+0058 LATIN CAPITAL LETTER X - case 'x': - case 'X': - // Follow the steps below, but using the range of - // characters U+0030 DIGIT ZERO through to U+0039 DIGIT - // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 - // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER - // A, through to U+0046 LATIN CAPITAL LETTER F (in other - // words, 0-9, A-F, a-f). - $char = 1; - $char_class = '0-9A-Fa-f'; - break; - - // Anything else - default: - // Follow the steps below, but using the range of - // characters U+0030 DIGIT ZERO through to U+0039 DIGIT - // NINE (i.e. just 0-9). - $char = 0; - $char_class = '0-9'; - break; - } - - // Consume as many characters as match the range of characters - // given above. - $this->char++; - $e_name = $this->characters($char_class, $this->char + $char + 1); - $entity = $this->character($start, $this->char); - $cond = strlen($e_name) > 0; - - // The rest of the parsing happens bellow. - break; - - // Anything else - default: - // Consume the maximum number of characters possible, with the - // consumed characters case-sensitively matching one of the - // identifiers in the first column of the entities table. - $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); - $len = strlen($e_name); - - for($c = 1; $c <= $len; $c++) { - $id = substr($e_name, 0, $c); - $this->char++; - - if(in_array($id, $this->entities)) { - if ($e_name[$c-1] !== ';') { - if ($c < $len && $e_name[$c] == ';') { - $this->char++; // consume extra semicolon - } - } - $entity = $id; - break; - } - } - - $cond = isset($entity); - // The rest of the parsing happens bellow. - break; - } - - if(!$cond) { - // If no match can be made, then this is a parse error. No - // characters are consumed, and nothing is returned. - $this->char = $start; - return false; - } - - // Return a character token for the character corresponding to the - // entity name (as given by the second column of the entities table). - return html_entity_decode('&'.$entity.';', ENT_QUOTES, 'UTF-8'); - } - - private function emitToken($token) { - $emit = $this->tree->emitToken($token); - - if(is_int($emit)) { - $this->content_model = $emit; - - } elseif($token['type'] === self::ENDTAG) { - $this->content_model = self::PCDATA; - } - } - - private function EOF() { - $this->state = null; - $this->tree->emitToken(array( - 'type' => self::EOF - )); - } -} - -class HTML5TreeConstructer { - public $stack = array(); - - private $phase; - private $mode; - private $dom; - private $foster_parent = null; - private $a_formatting = array(); - - private $head_pointer = null; - private $form_pointer = null; - - private $scoping = array('button','caption','html','marquee','object','table','td','th'); - private $formatting = array('a','b','big','em','font','i','nobr','s','small','strike','strong','tt','u'); - private $special = array('address','area','base','basefont','bgsound', - 'blockquote','body','br','center','col','colgroup','dd','dir','div','dl', - 'dt','embed','fieldset','form','frame','frameset','h1','h2','h3','h4','h5', - 'h6','head','hr','iframe','image','img','input','isindex','li','link', - 'listing','menu','meta','noembed','noframes','noscript','ol','optgroup', - 'option','p','param','plaintext','pre','script','select','spacer','style', - 'tbody','textarea','tfoot','thead','title','tr','ul','wbr'); - - // The different phases. - const INIT_PHASE = 0; - const ROOT_PHASE = 1; - const MAIN_PHASE = 2; - const END_PHASE = 3; - - // The different insertion modes for the main phase. - const BEFOR_HEAD = 0; - const IN_HEAD = 1; - const AFTER_HEAD = 2; - const IN_BODY = 3; - const IN_TABLE = 4; - const IN_CAPTION = 5; - const IN_CGROUP = 6; - const IN_TBODY = 7; - const IN_ROW = 8; - const IN_CELL = 9; - const IN_SELECT = 10; - const AFTER_BODY = 11; - const IN_FRAME = 12; - const AFTR_FRAME = 13; - - // The different types of elements. - const SPECIAL = 0; - const SCOPING = 1; - const FORMATTING = 2; - const PHRASING = 3; - - const MARKER = 0; - - public function __construct() { - $this->phase = self::INIT_PHASE; - $this->mode = self::BEFOR_HEAD; - $this->dom = new DOMDocument; - - $this->dom->encoding = 'UTF-8'; - $this->dom->preserveWhiteSpace = true; - $this->dom->substituteEntities = true; - $this->dom->strictErrorChecking = false; - } - - // Process tag tokens - public function emitToken($token) { - switch($this->phase) { - case self::INIT_PHASE: return $this->initPhase($token); break; - case self::ROOT_PHASE: return $this->rootElementPhase($token); break; - case self::MAIN_PHASE: return $this->mainPhase($token); break; - case self::END_PHASE : return $this->trailingEndPhase($token); break; - } - } - - private function initPhase($token) { - /* Initially, the tree construction stage must handle each token - emitted from the tokenisation stage as follows: */ - - /* A DOCTYPE token that is marked as being in error - A comment token - A start tag token - An end tag token - A character token that is not one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE - An end-of-file token */ - if((isset($token['error']) && $token['error']) || - $token['type'] === HTML5::COMMENT || - $token['type'] === HTML5::STARTTAG || - $token['type'] === HTML5::ENDTAG || - $token['type'] === HTML5::EOF || - ($token['type'] === HTML5::CHARACTR && isset($token['data']) && - !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))) { - /* This specification does not define how to handle this case. In - particular, user agents may ignore the entirety of this specification - altogether for such documents, and instead invoke special parse modes - with a greater emphasis on backwards compatibility. */ - - $this->phase = self::ROOT_PHASE; - return $this->rootElementPhase($token); - - /* A DOCTYPE token marked as being correct */ - } elseif(isset($token['error']) && !$token['error']) { - /* Append a DocumentType node to the Document node, with the name - attribute set to the name given in the DOCTYPE token (which will be - "HTML"), and the other attributes specific to DocumentType objects - set to null, empty lists, or the empty string as appropriate. */ - $doctype = new DOMDocumentType(null, null, 'HTML'); - - /* Then, switch to the root element phase of the tree construction - stage. */ - $this->phase = self::ROOT_PHASE; - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif(isset($token['data']) && preg_match('/^[\t\n\x0b\x0c ]+$/', - $token['data'])) { - /* Append that character to the Document node. */ - $text = $this->dom->createTextNode($token['data']); - $this->dom->appendChild($text); - } - } - - private function rootElementPhase($token) { - /* After the initial phase, as each token is emitted from the tokenisation - stage, it must be processed as described in this section. */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the Document object with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->dom->appendChild($comment); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append that character to the Document node. */ - $text = $this->dom->createTextNode($token['data']); - $this->dom->appendChild($text); - - /* A character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED - (FF), or U+0020 SPACE - A start tag token - An end tag token - An end-of-file token */ - } elseif(($token['type'] === HTML5::CHARACTR && - !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || - $token['type'] === HTML5::STARTTAG || - $token['type'] === HTML5::ENDTAG || - $token['type'] === HTML5::EOF) { - /* Create an HTMLElement node with the tag name html, in the HTML - namespace. Append it to the Document object. Switch to the main - phase and reprocess the current token. */ - $html = $this->dom->createElement('html'); - $this->dom->appendChild($html); - $this->stack[] = $html; - - $this->phase = self::MAIN_PHASE; - return $this->mainPhase($token); - } - } - - private function mainPhase($token) { - /* Tokens in the main phase must be handled as follows: */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A start tag token with the tag name "html" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { - /* If this start tag token was not the first start tag token, then - it is a parse error. */ - - /* For each attribute on the token, check to see if the attribute - is already present on the top element of the stack of open elements. - If it is not, add the attribute and its corresponding value to that - element. */ - foreach($token['attr'] as $attr) { - if(!$this->stack[0]->hasAttribute($attr['name'])) { - $this->stack[0]->setAttribute($attr['name'], $attr['value']); - } - } - - /* An end-of-file token */ - } elseif($token['type'] === HTML5::EOF) { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Anything else. */ - } else { - /* Depends on the insertion mode: */ - switch($this->mode) { - case self::BEFOR_HEAD: return $this->beforeHead($token); break; - case self::IN_HEAD: return $this->inHead($token); break; - case self::AFTER_HEAD: return $this->afterHead($token); break; - case self::IN_BODY: return $this->inBody($token); break; - case self::IN_TABLE: return $this->inTable($token); break; - case self::IN_CAPTION: return $this->inCaption($token); break; - case self::IN_CGROUP: return $this->inColumnGroup($token); break; - case self::IN_TBODY: return $this->inTableBody($token); break; - case self::IN_ROW: return $this->inRow($token); break; - case self::IN_CELL: return $this->inCell($token); break; - case self::IN_SELECT: return $this->inSelect($token); break; - case self::AFTER_BODY: return $this->afterBody($token); break; - case self::IN_FRAME: return $this->inFrameset($token); break; - case self::AFTR_FRAME: return $this->afterFrameset($token); break; - case self::END_PHASE: return $this->trailingEndPhase($token); break; - } - } - } - - private function beforeHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token with the tag name "head" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { - /* Create an element for the token, append the new element to the - current node and push it onto the stack of open elements. */ - $element = $this->insertElement($token); - - /* Set the head element pointer to this new element node. */ - $this->head_pointer = $element; - - /* Change the insertion mode to "in head". */ - $this->mode = self::IN_HEAD; - - /* A start tag token whose tag name is one of: "base", "link", "meta", - "script", "style", "title". Or an end tag with the tag name "html". - Or a character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. Or any other start tag token */ - } elseif($token['type'] === HTML5::STARTTAG || - ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || - ($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]$/', - $token['data']))) { - /* Act as if a start tag token with the tag name "head" and no - attributes had been seen, then reprocess the current token. */ - $this->beforeHead(array( - 'name' => 'head', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inHead($token); - - /* Any other end tag */ - } elseif($token['type'] === HTML5::ENDTAG) { - /* Parse error. Ignore the token. */ - } - } - - private function inHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. - - THIS DIFFERS FROM THE SPEC: If the current node is either a title, style - or script element, append the character to the current node regardless - of its content. */ - if(($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( - $token['type'] === HTML5::CHARACTR && in_array(end($this->stack)->nodeName, - array('title', 'style', 'script')))) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('title', 'style', 'script'))) { - array_pop($this->stack); - return HTML5::PCDATA; - - /* A start tag with the tag name "title" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - } else { - $element = $this->insertElement($token); - } - - /* Switch the tokeniser's content model flag to the RCDATA state. */ - return HTML5::RCDATA; - - /* A start tag with the tag name "style" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - } else { - $this->insertElement($token); - } - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - - /* A start tag with the tag name "script" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { - /* Create an element for the token. */ - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - - /* A start tag with the tag name "base", "link", or "meta" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('base', 'link', 'meta'))) { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - array_pop($this->stack); - - } else { - $this->insertElement($token); - } - - /* An end tag with the tag name "head" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { - /* If the current node is a head element, pop the current node off - the stack of open elements. */ - if($this->head_pointer->isSameNode(end($this->stack))) { - array_pop($this->stack); - - /* Otherwise, this is a parse error. */ - } else { - // k - } - - /* Change the insertion mode to "after head". */ - $this->mode = self::AFTER_HEAD; - - /* A start tag with the tag name "head" or an end tag except "html". */ - } elseif(($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || - ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* If the current node is a head element, act as if an end tag - token with the tag name "head" had been seen. */ - if($this->head_pointer->isSameNode(end($this->stack))) { - $this->inHead(array( - 'name' => 'head', - 'type' => HTML5::ENDTAG - )); - - /* Otherwise, change the insertion mode to "after head". */ - } else { - $this->mode = self::AFTER_HEAD; - } - - /* Then, reprocess the current token. */ - return $this->afterHead($token); - } - } - - private function afterHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token with the tag name "body" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { - /* Insert a body element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in body". */ - $this->mode = self::IN_BODY; - - /* A start tag token with the tag name "frameset" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { - /* Insert a frameset element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in frameset". */ - $this->mode = self::IN_FRAME; - - /* A start tag token whose tag name is one of: "base", "link", "meta", - "script", "style", "title" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('base', 'link', 'meta', 'script', 'style', 'title'))) { - /* Parse error. Switch the insertion mode back to "in head" and - reprocess the token. */ - $this->mode = self::IN_HEAD; - return $this->inHead($token); - - /* Anything else */ - } else { - /* Act as if a start tag token with the tag name "body" and no - attributes had been seen, and then reprocess the current token. */ - $this->afterHead(array( - 'name' => 'body', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inBody($token); - } - } - - private function inBody($token) { - /* Handle the token as follows: */ - - switch($token['type']) { - /* A character token */ - case HTML5::CHARACTR: - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Append the token's character to the current node. */ - $this->insertText($token['data']); - break; - - /* A comment token */ - case HTML5::COMMENT: - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - break; - - case HTML5::STARTTAG: - switch($token['name']) { - /* A start tag token whose tag name is one of: "script", - "style" */ - case 'script': case 'style': - /* Process the token as if the insertion mode had been "in - head". */ - return $this->inHead($token); - break; - - /* A start tag token whose tag name is one of: "base", "link", - "meta", "title" */ - case 'base': case 'link': case 'meta': case 'title': - /* Parse error. Process the token as if the insertion mode - had been "in head". */ - return $this->inHead($token); - break; - - /* A start tag token with the tag name "body" */ - case 'body': - /* Parse error. If the second element on the stack of open - elements is not a body element, or, if the stack of open - elements has only one node on it, then ignore the token. - (innerHTML case) */ - if(count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { - // Ignore - - /* Otherwise, for each attribute on the token, check to see - if the attribute is already present on the body element (the - second element) on the stack of open elements. If it is not, - add the attribute and its corresponding value to that - element. */ - } else { - foreach($token['attr'] as $attr) { - if(!$this->stack[1]->hasAttribute($attr['name'])) { - $this->stack[1]->setAttribute($attr['name'], $attr['value']); - } - } - } - break; - - /* A start tag whose tag name is one of: "address", - "blockquote", "center", "dir", "div", "dl", "fieldset", - "listing", "menu", "ol", "p", "ul" */ - case 'address': case 'blockquote': case 'center': case 'dir': - case 'div': case 'dl': case 'fieldset': case 'listing': - case 'menu': case 'ol': case 'p': case 'ul': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - break; - - /* A start tag whose tag name is "form" */ - case 'form': - /* If the form element pointer is not null, ignore the - token with a parse error. */ - if($this->form_pointer !== null) { - // Ignore. - - /* Otherwise: */ - } else { - /* If the stack of open elements has a p element in - scope, then act as if an end tag with the tag name p - had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token, and set the - form element pointer to point to the element created. */ - $element = $this->insertElement($token); - $this->form_pointer = $element; - } - break; - - /* A start tag whose tag name is "li", "dd" or "dt" */ - case 'li': case 'dd': case 'dt': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - $stack_length = count($this->stack) - 1; - - for($n = $stack_length; 0 <= $n; $n--) { - /* 1. Initialise node to be the current node (the - bottommost node of the stack). */ - $stop = false; - $node = $this->stack[$n]; - $cat = $this->getElementCategory($node->tagName); - - /* 2. If node is an li, dd or dt element, then pop all - the nodes from the current node up to node, including - node, then stop this algorithm. */ - if($token['name'] === $node->tagName || ($token['name'] !== 'li' - && ($node->tagName === 'dd' || $node->tagName === 'dt'))) { - for($x = $stack_length; $x >= $n ; $x--) { - array_pop($this->stack); - } - - break; - } - - /* 3. If node is not in the formatting category, and is - not in the phrasing category, and is not an address or - div element, then stop this algorithm. */ - if($cat !== self::FORMATTING && $cat !== self::PHRASING && - $node->tagName !== 'address' && $node->tagName !== 'div') { - break; - } - } - - /* Finally, insert an HTML element with the same tag - name as the token's. */ - $this->insertElement($token); - break; - - /* A start tag token whose tag name is "plaintext" */ - case 'plaintext': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - return HTML5::PLAINTEXT; - break; - - /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", - "h5", "h6" */ - case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* If the stack of open elements has in scope an element whose - tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then - this is a parse error; pop elements from the stack until an - element with one of those tag names has been popped from the - stack. */ - while($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { - array_pop($this->stack); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - break; - - /* A start tag whose tag name is "a" */ - case 'a': - /* If the list of active formatting elements contains - an element whose tag name is "a" between the end of the - list and the last marker on the list (or the start of - the list if there is no marker on the list), then this - is a parse error; act as if an end tag with the tag name - "a" had been seen, then remove that element from the list - of active formatting elements and the stack of open - elements if the end tag didn't already remove it (it - might not have if the element is not in table scope). */ - $leng = count($this->a_formatting); - - for($n = $leng - 1; $n >= 0; $n--) { - if($this->a_formatting[$n] === self::MARKER) { - break; - - } elseif($this->a_formatting[$n]->nodeName === 'a') { - $this->emitToken(array( - 'name' => 'a', - 'type' => HTML5::ENDTAG - )); - break; - } - } - - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $el = $this->insertElement($token); - - /* Add that element to the list of active formatting - elements. */ - $this->a_formatting[] = $el; - break; - - /* A start tag whose tag name is one of: "b", "big", "em", "font", - "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ - case 'b': case 'big': case 'em': case 'font': case 'i': - case 'nobr': case 's': case 'small': case 'strike': - case 'strong': case 'tt': case 'u': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $el = $this->insertElement($token); - - /* Add that element to the list of active formatting - elements. */ - $this->a_formatting[] = $el; - break; - - /* A start tag token whose tag name is "button" */ - case 'button': - /* If the stack of open elements has a button element in scope, - then this is a parse error; act as if an end tag with the tag - name "button" had been seen, then reprocess the token. (We don't - do that. Unnecessary.) */ - if($this->elementInScope('button')) { - $this->inBody(array( - 'name' => 'button', - 'type' => HTML5::ENDTAG - )); - } - - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - break; - - /* A start tag token whose tag name is one of: "marquee", "object" */ - case 'marquee': case 'object': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - break; - - /* A start tag token whose tag name is "xmp" */ - case 'xmp': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Switch the content model flag to the CDATA state. */ - return HTML5::CDATA; - break; - - /* A start tag whose tag name is "table" */ - case 'table': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in table". */ - $this->mode = self::IN_TABLE; - break; - - /* A start tag whose tag name is one of: "area", "basefont", - "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ - case 'area': case 'basefont': case 'bgsound': case 'br': - case 'embed': case 'img': case 'param': case 'spacer': - case 'wbr': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "hr" */ - case 'hr': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "image" */ - case 'image': - /* Parse error. Change the token's tag name to "img" and - reprocess it. (Don't ask.) */ - $token['name'] = 'img'; - return $this->inBody($token); - break; - - /* A start tag whose tag name is "input" */ - case 'input': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an input element for the token. */ - $element = $this->insertElement($token, false); - - /* If the form element pointer is not null, then associate the - input element with the form element pointed to by the form - element pointer. */ - $this->form_pointer !== null - ? $this->form_pointer->appendChild($element) - : end($this->stack)->appendChild($element); - - /* Pop that input element off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "isindex" */ - case 'isindex': - /* Parse error. */ - // w/e - - /* If the form element pointer is not null, - then ignore the token. */ - if($this->form_pointer === null) { - /* Act as if a start tag token with the tag name "form" had - been seen. */ - $this->inBody(array( - 'name' => 'body', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "hr" had - been seen. */ - $this->inBody(array( - 'name' => 'hr', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "p" had - been seen. */ - $this->inBody(array( - 'name' => 'p', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "label" - had been seen. */ - $this->inBody(array( - 'name' => 'label', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a stream of character tokens had been seen. */ - $this->insertText('This is a searchable index. '. - 'Insert your search keywords here: '); - - /* Act as if a start tag token with the tag name "input" - had been seen, with all the attributes from the "isindex" - token, except with the "name" attribute set to the value - "isindex" (ignoring any explicit "name" attribute). */ - $attr = $token['attr']; - $attr[] = array('name' => 'name', 'value' => 'isindex'); - - $this->inBody(array( - 'name' => 'input', - 'type' => HTML5::STARTTAG, - 'attr' => $attr - )); - - /* Act as if a stream of character tokens had been seen - (see below for what they should say). */ - $this->insertText('This is a searchable index. '. - 'Insert your search keywords here: '); - - /* Act as if an end tag token with the tag name "label" - had been seen. */ - $this->inBody(array( - 'name' => 'label', - 'type' => HTML5::ENDTAG - )); - - /* Act as if an end tag token with the tag name "p" had - been seen. */ - $this->inBody(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - - /* Act as if a start tag token with the tag name "hr" had - been seen. */ - $this->inBody(array( - 'name' => 'hr', - 'type' => HTML5::ENDTAG - )); - - /* Act as if an end tag token with the tag name "form" had - been seen. */ - $this->inBody(array( - 'name' => 'form', - 'type' => HTML5::ENDTAG - )); - } - break; - - /* A start tag whose tag name is "textarea" */ - case 'textarea': - $this->insertElement($token); - - /* Switch the tokeniser's content model flag to the - RCDATA state. */ - return HTML5::RCDATA; - break; - - /* A start tag whose tag name is one of: "iframe", "noembed", - "noframes" */ - case 'iframe': case 'noembed': case 'noframes': - $this->insertElement($token); - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - break; - - /* A start tag whose tag name is "select" */ - case 'select': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in select". */ - $this->mode = self::IN_SELECT; - break; - - /* A start or end tag whose tag name is one of: "caption", "col", - "colgroup", "frame", "frameset", "head", "option", "optgroup", - "tbody", "td", "tfoot", "th", "thead", "tr". */ - case 'caption': case 'col': case 'colgroup': case 'frame': - case 'frameset': case 'head': case 'option': case 'optgroup': - case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': - case 'tr': - // Parse error. Ignore the token. - break; - - /* A start or end tag whose tag name is one of: "event-source", - "section", "nav", "article", "aside", "header", "footer", - "datagrid", "command" */ - case 'event-source': case 'section': case 'nav': case 'article': - case 'aside': case 'header': case 'footer': case 'datagrid': - case 'command': - // Work in progress! - break; - - /* A start tag token not covered by the previous entries */ - default: - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - $this->insertElement($token); - break; - } - break; - - case HTML5::ENDTAG: - switch($token['name']) { - /* An end tag with the tag name "body" */ - case 'body': - /* If the second element in the stack of open elements is - not a body element, this is a parse error. Ignore the token. - (innerHTML case) */ - if(count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { - // Ignore. - - /* If the current node is not the body element, then this - is a parse error. */ - } elseif(end($this->stack)->nodeName !== 'body') { - // Parse error. - } - - /* Change the insertion mode to "after body". */ - $this->mode = self::AFTER_BODY; - break; - - /* An end tag with the tag name "html" */ - case 'html': - /* Act as if an end tag with tag name "body" had been seen, - then, if that token wasn't ignored, reprocess the current - token. */ - $this->inBody(array( - 'name' => 'body', - 'type' => HTML5::ENDTAG - )); - - return $this->afterBody($token); - break; - - /* An end tag whose tag name is one of: "address", "blockquote", - "center", "dir", "div", "dl", "fieldset", "listing", "menu", - "ol", "pre", "ul" */ - case 'address': case 'blockquote': case 'center': case 'dir': - case 'div': case 'dl': case 'fieldset': case 'listing': - case 'menu': case 'ol': case 'pre': case 'ul': - /* If the stack of open elements has an element in scope - with the same tag name as that of the token, then generate - implied end tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with - the same tag name as that of the token, then this - is a parse error. */ - // w/e - - /* If the stack of open elements has an element in - scope with the same tag name as that of the token, - then pop elements from this stack until an element - with that tag name has been popped from the stack. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is "form" */ - case 'form': - /* If the stack of open elements has an element in scope - with the same tag name as that of the token, then generate - implied end tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - } - - if(end($this->stack)->nodeName !== $token['name']) { - /* Now, if the current node is not an element with the - same tag name as that of the token, then this is a parse - error. */ - // w/e - - } else { - /* Otherwise, if the current node is an element with - the same tag name as that of the token pop that element - from the stack. */ - array_pop($this->stack); - } - - /* In any case, set the form element pointer to null. */ - $this->form_pointer = null; - break; - - /* An end tag whose tag name is "p" */ - case 'p': - /* If the stack of open elements has a p element in scope, - then generate implied end tags, except for p elements. */ - if($this->elementInScope('p')) { - $this->generateImpliedEndTags(array('p')); - - /* If the current node is not a p element, then this is - a parse error. */ - // k - - /* If the stack of open elements has a p element in - scope, then pop elements from this stack until the stack - no longer has a p element in scope. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->elementInScope('p')) { - array_pop($this->stack); - - } else { - break; - } - } - } - break; - - /* An end tag whose tag name is "dd", "dt", or "li" */ - case 'dd': case 'dt': case 'li': - /* If the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then - generate implied end tags, except for elements with the - same tag name as the token. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(array($token['name'])); - - /* If the current node is not an element with the same - tag name as the token, then this is a parse error. */ - // w/e - - /* If the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then - pop elements from this stack until an element with that - tag name has been popped from the stack. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", - "h5", "h6" */ - case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); - - /* If the stack of open elements has in scope an element whose - tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then - generate implied end tags. */ - if($this->elementInScope($elements)) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with the same - tag name as that of the token, then this is a parse error. */ - // w/e - - /* If the stack of open elements has in scope an element - whose tag name is one of "h1", "h2", "h3", "h4", "h5", or - "h6", then pop elements from the stack until an element - with one of those tag names has been popped from the stack. */ - while($this->elementInScope($elements)) { - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is one of: "a", "b", "big", "em", - "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ - case 'a': case 'b': case 'big': case 'em': case 'font': - case 'i': case 'nobr': case 's': case 'small': case 'strike': - case 'strong': case 'tt': case 'u': - /* 1. Let the formatting element be the last element in - the list of active formatting elements that: - * is between the end of the list and the last scope - marker in the list, if any, or the start of the list - otherwise, and - * has the same tag name as the token. - */ - while(true) { - for($a = count($this->a_formatting) - 1; $a >= 0; $a--) { - if($this->a_formatting[$a] === self::MARKER) { - break; - - } elseif($this->a_formatting[$a]->tagName === $token['name']) { - $formatting_element = $this->a_formatting[$a]; - $in_stack = in_array($formatting_element, $this->stack, true); - $fe_af_pos = $a; - break; - } - } - - /* If there is no such node, or, if that node is - also in the stack of open elements but the element - is not in scope, then this is a parse error. Abort - these steps. The token is ignored. */ - if(!isset($formatting_element) || ($in_stack && - !$this->elementInScope($token['name']))) { - break; - - /* Otherwise, if there is such a node, but that node - is not in the stack of open elements, then this is a - parse error; remove the element from the list, and - abort these steps. */ - } elseif(isset($formatting_element) && !$in_stack) { - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - break; - } - - /* 2. Let the furthest block be the topmost node in the - stack of open elements that is lower in the stack - than the formatting element, and is not an element in - the phrasing or formatting categories. There might - not be one. */ - $fe_s_pos = array_search($formatting_element, $this->stack, true); - $length = count($this->stack); - - for($s = $fe_s_pos + 1; $s < $length; $s++) { - $category = $this->getElementCategory($this->stack[$s]->nodeName); - - if($category !== self::PHRASING && $category !== self::FORMATTING) { - $furthest_block = $this->stack[$s]; - } - } - - /* 3. If there is no furthest block, then the UA must - skip the subsequent steps and instead just pop all - the nodes from the bottom of the stack of open - elements, from the current node up to the formatting - element, and remove the formatting element from the - list of active formatting elements. */ - if(!isset($furthest_block)) { - for($n = $length - 1; $n >= $fe_s_pos; $n--) { - array_pop($this->stack); - } - - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - break; - } - - /* 4. Let the common ancestor be the element - immediately above the formatting element in the stack - of open elements. */ - $common_ancestor = $this->stack[$fe_s_pos - 1]; - - /* 5. If the furthest block has a parent node, then - remove the furthest block from its parent node. */ - if($furthest_block->parentNode !== null) { - $furthest_block->parentNode->removeChild($furthest_block); - } - - /* 6. Let a bookmark note the position of the - formatting element in the list of active formatting - elements relative to the elements on either side - of it in the list. */ - $bookmark = $fe_af_pos; - - /* 7. Let node and last node be the furthest block. - Follow these steps: */ - $node = $furthest_block; - $last_node = $furthest_block; - - while(true) { - for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { - /* 7.1 Let node be the element immediately - prior to node in the stack of open elements. */ - $node = $this->stack[$n]; - - /* 7.2 If node is not in the list of active - formatting elements, then remove node from - the stack of open elements and then go back - to step 1. */ - if(!in_array($node, $this->a_formatting, true)) { - unset($this->stack[$n]); - $this->stack = array_merge($this->stack); - - } else { - break; - } - } - - /* 7.3 Otherwise, if node is the formatting - element, then go to the next step in the overall - algorithm. */ - if($node === $formatting_element) { - break; - - /* 7.4 Otherwise, if last node is the furthest - block, then move the aforementioned bookmark to - be immediately after the node in the list of - active formatting elements. */ - } elseif($last_node === $furthest_block) { - $bookmark = array_search($node, $this->a_formatting, true) + 1; - } - - /* 7.5 If node has any children, perform a - shallow clone of node, replace the entry for - node in the list of active formatting elements - with an entry for the clone, replace the entry - for node in the stack of open elements with an - entry for the clone, and let node be the clone. */ - if($node->hasChildNodes()) { - $clone = $node->cloneNode(); - $s_pos = array_search($node, $this->stack, true); - $a_pos = array_search($node, $this->a_formatting, true); - - $this->stack[$s_pos] = $clone; - $this->a_formatting[$a_pos] = $clone; - $node = $clone; - } - - /* 7.6 Insert last node into node, first removing - it from its previous parent node if any. */ - if($last_node->parentNode !== null) { - $last_node->parentNode->removeChild($last_node); - } - - $node->appendChild($last_node); - - /* 7.7 Let last node be node. */ - $last_node = $node; - } - - /* 8. Insert whatever last node ended up being in - the previous step into the common ancestor node, - first removing it from its previous parent node if - any. */ - if($last_node->parentNode !== null) { - $last_node->parentNode->removeChild($last_node); - } - - $common_ancestor->appendChild($last_node); - - /* 9. Perform a shallow clone of the formatting - element. */ - $clone = $formatting_element->cloneNode(); - - /* 10. Take all of the child nodes of the furthest - block and append them to the clone created in the - last step. */ - while($furthest_block->hasChildNodes()) { - $child = $furthest_block->firstChild; - $furthest_block->removeChild($child); - $clone->appendChild($child); - } - - /* 11. Append that clone to the furthest block. */ - $furthest_block->appendChild($clone); - - /* 12. Remove the formatting element from the list - of active formatting elements, and insert the clone - into the list of active formatting elements at the - position of the aforementioned bookmark. */ - $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - - $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); - $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); - $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); - - /* 13. Remove the formatting element from the stack - of open elements, and insert the clone into the stack - of open elements immediately after (i.e. in a more - deeply nested position than) the position of the - furthest block in that stack. */ - $fe_s_pos = array_search($formatting_element, $this->stack, true); - $fb_s_pos = array_search($furthest_block, $this->stack, true); - unset($this->stack[$fe_s_pos]); - - $s_part1 = array_slice($this->stack, 0, $fb_s_pos); - $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); - $this->stack = array_merge($s_part1, array($clone), $s_part2); - - /* 14. Jump back to step 1 in this series of steps. */ - unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); - } - break; - - /* An end tag token whose tag name is one of: "button", - "marquee", "object" */ - case 'button': case 'marquee': case 'object': - /* If the stack of open elements has an element in scope whose - tag name matches the tag name of the token, then generate implied - tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with the same - tag name as the token, then this is a parse error. */ - // k - - /* Now, if the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then pop - elements from the stack until that element has been popped from - the stack, and clear the list of active formatting elements up - to the last marker. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - - $marker = end(array_keys($this->a_formatting, self::MARKER, true)); - - for($n = count($this->a_formatting) - 1; $n > $marker; $n--) { - array_pop($this->a_formatting); - } - } - break; - - /* Or an end tag whose tag name is one of: "area", "basefont", - "bgsound", "br", "embed", "hr", "iframe", "image", "img", - "input", "isindex", "noembed", "noframes", "param", "select", - "spacer", "table", "textarea", "wbr" */ - case 'area': case 'basefont': case 'bgsound': case 'br': - case 'embed': case 'hr': case 'iframe': case 'image': - case 'img': case 'input': case 'isindex': case 'noembed': - case 'noframes': case 'param': case 'select': case 'spacer': - case 'table': case 'textarea': case 'wbr': - // Parse error. Ignore the token. - break; - - /* An end tag token not covered by the previous entries */ - default: - for($n = count($this->stack) - 1; $n >= 0; $n--) { - /* Initialise node to be the current node (the bottommost - node of the stack). */ - $node = end($this->stack); - - /* If node has the same tag name as the end tag token, - then: */ - if($token['name'] === $node->nodeName) { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* If the tag name of the end tag token does not - match the tag name of the current node, this is a - parse error. */ - // k - - /* Pop all the nodes from the current node up to - node, including node, then stop this algorithm. */ - for($x = count($this->stack) - $n; $x >= $n; $x--) { - array_pop($this->stack); - } - - } else { - $category = $this->getElementCategory($node); - - if($category !== self::SPECIAL && $category !== self::SCOPING) { - /* Otherwise, if node is in neither the formatting - category nor the phrasing category, then this is a - parse error. Stop this algorithm. The end tag token - is ignored. */ - return false; - } - } - } - break; - } - break; - } - } - - private function inTable($token) { - $clear = array('html', 'table'); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $text = $this->dom->createTextNode($token['data']); - end($this->stack)->appendChild($text); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - end($this->stack)->appendChild($comment); - - /* A start tag whose tag name is "caption" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'caption') { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - - /* Insert an HTML element for the token, then switch the - insertion mode to "in caption". */ - $this->insertElement($token); - $this->mode = self::IN_CAPTION; - - /* A start tag whose tag name is "colgroup" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'colgroup') { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the - insertion mode to "in column group". */ - $this->insertElement($token); - $this->mode = self::IN_CGROUP; - - /* A start tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'col') { - $this->inTable(array( - 'name' => 'colgroup', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - $this->inColumnGroup($token); - - /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('tbody', 'tfoot', 'thead'))) { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the insertion - mode to "in table body". */ - $this->insertElement($token); - $this->mode = self::IN_TBODY; - - /* A start tag whose tag name is one of: "td", "th", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && - in_array($token['name'], array('td', 'th', 'tr'))) { - /* Act as if a start tag token with the tag name "tbody" had been - seen, then reprocess the current token. */ - $this->inTable(array( - 'name' => 'tbody', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inTableBody($token); - - /* A start tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'table') { - /* Parse error. Act as if an end tag token with the tag name "table" - had been seen, then, if that token wasn't ignored, reprocess the - current token. */ - $this->inTable(array( - 'name' => 'table', - 'type' => HTML5::ENDTAG - )); - - return $this->mainPhase($token); - - /* An end tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'table') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - return false; - - /* Otherwise: */ - } else { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Now, if the current node is not a table element, then this - is a parse error. */ - // w/e - - /* Pop elements from this stack until a table element has been - popped from the stack. */ - while(true) { - $current = end($this->stack)->nodeName; - array_pop($this->stack); - - if($current === 'table') { - break; - } - } - - /* Reset the insertion mode appropriately. */ - $this->resetInsertionMode(); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td', - 'tfoot', 'th', 'thead', 'tr'))) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* Parse error. Process the token as if the insertion mode was "in - body", with the following exception: */ - - /* If the current node is a table, tbody, tfoot, thead, or tr - element, then, whenever a node would be inserted into the current - node, it must instead be inserted into the foster parent element. */ - if(in_array(end($this->stack)->nodeName, - array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* The foster parent element is the parent element of the last - table element in the stack of open elements, if there is a - table element and it has such a parent element. If there is no - table element in the stack of open elements (innerHTML case), - then the foster parent element is the first element in the - stack of open elements (the html element). Otherwise, if there - is a table element in the stack of open elements, but the last - table element in the stack of open elements has no parent, or - its parent node is not an element, then the foster parent - element is the element before the last table element in the - stack of open elements. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === 'table') { - $table = $this->stack[$n]; - break; - } - } - - if(isset($table) && $table->parentNode !== null) { - $this->foster_parent = $table->parentNode; - - } elseif(!isset($table)) { - $this->foster_parent = $this->stack[0]; - - } elseif(isset($table) && ($table->parentNode === null || - $table->parentNode->nodeType !== XML_ELEMENT_NODE)) { - $this->foster_parent = $this->stack[$n - 1]; - } - } - - $this->inBody($token); - } - } - - private function inCaption($token) { - /* An end tag whose tag name is "caption" */ - if($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore - - /* Otherwise: */ - } else { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Now, if the current node is not a caption element, then this - is a parse error. */ - // w/e - - /* Pop elements from this stack until a caption element has - been popped from the stack. */ - while(true) { - $node = end($this->stack)->nodeName; - array_pop($this->stack); - - if($node === 'caption') { - break; - } - } - - /* Clear the list of active formatting elements up to the last - marker. */ - $this->clearTheActiveFormattingElementsUpToTheLastMarker(); - - /* Switch the insertion mode to "in table". */ - $this->mode = self::IN_TABLE; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag - name is "table" */ - } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) || ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'table')) { - /* Parse error. Act as if an end tag with the tag name "caption" - had been seen, then, if that token wasn't ignored, reprocess the - current token. */ - $this->inCaption(array( - 'name' => 'caption', - 'type' => HTML5::ENDTAG - )); - - return $this->inTable($token); - - /* An end tag whose tag name is one of: "body", "col", "colgroup", - "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th', - 'thead', 'tr'))) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in body". */ - $this->inBody($token); - } - } - - private function inColumnGroup($token) { - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $text = $this->dom->createTextNode($token['data']); - end($this->stack)->appendChild($text); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - end($this->stack)->appendChild($comment); - - /* A start tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { - /* Insert a col element for the token. Immediately pop the current - node off the stack of open elements. */ - $this->insertElement($token); - array_pop($this->stack); - - /* An end tag whose tag name is "colgroup" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'colgroup') { - /* If the current node is the root html element, then this is a - parse error, ignore the token. (innerHTML case) */ - if(end($this->stack)->nodeName === 'html') { - // Ignore - - /* Otherwise, pop the current node (which will be a colgroup - element) from the stack of open elements. Switch the insertion - mode to "in table". */ - } else { - array_pop($this->stack); - $this->mode = self::IN_TABLE; - } - - /* An end tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Act as if an end tag with the tag name "colgroup" had been seen, - and then, if that token wasn't ignored, reprocess the current token. */ - $this->inColumnGroup(array( - 'name' => 'colgroup', - 'type' => HTML5::ENDTAG - )); - - return $this->inTable($token); - } - } - - private function inTableBody($token) { - $clear = array('tbody', 'tfoot', 'thead', 'html'); - - /* A start tag whose tag name is "tr" */ - if($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Insert a tr element for the token, then switch the insertion - mode to "in row". */ - $this->insertElement($token); - $this->mode = self::IN_ROW; - - /* A start tag whose tag name is one of: "th", "td" */ - } elseif($token['type'] === HTML5::STARTTAG && - ($token['name'] === 'th' || $token['name'] === 'td')) { - /* Parse error. Act as if a start tag with the tag name "tr" had - been seen, then reprocess the current token. */ - $this->inTableBody(array( - 'name' => 'tr', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inRow($token); - - /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore - - /* Otherwise: */ - } else { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Pop the current node from the stack of open elements. Switch - the insertion mode to "in table". */ - array_pop($this->stack); - $this->mode = self::IN_TABLE; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ - } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead'))) || - ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')) { - /* If the stack of open elements does not have a tbody, thead, or - tfoot element in table scope, this is a parse error. Ignore the - token. (innerHTML case) */ - if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Act as if an end tag with the same tag name as the current - node ("tbody", "tfoot", or "thead") had been seen, then - reprocess the current token. */ - $this->inTableBody(array( - 'name' => end($this->stack)->nodeName, - 'type' => HTML5::ENDTAG - )); - - return $this->mainPhase($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "td", "th", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in table". */ - $this->inTable($token); - } - } - - private function inRow($token) { - $clear = array('tr', 'html'); - - /* A start tag whose tag name is one of: "th", "td" */ - if($token['type'] === HTML5::STARTTAG && - ($token['name'] === 'th' || $token['name'] === 'td')) { - /* Clear the stack back to a table row context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the insertion - mode to "in cell". */ - $this->insertElement($token); - $this->mode = self::IN_CELL; - - /* Insert a marker at the end of the list of active formatting - elements. */ - $this->a_formatting[] = self::MARKER; - - /* An end tag whose tag name is "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Clear the stack back to a table row context. */ - $this->clearStackToTableContext($clear); - - /* Pop the current node (which will be a tr element) from the - stack of open elements. Switch the insertion mode to "in table - body". */ - array_pop($this->stack); - $this->mode = self::IN_TBODY; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* Act as if an end tag with the tag name "tr" had been seen, then, - if that token wasn't ignored, reprocess the current token. */ - $this->inRow(array( - 'name' => 'tr', - 'type' => HTML5::ENDTAG - )); - - return $this->inCell($token); - - /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Otherwise, act as if an end tag with the tag name "tr" had - been seen, then reprocess the current token. */ - $this->inRow(array( - 'name' => 'tr', - 'type' => HTML5::ENDTAG - )); - - return $this->inCell($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "td", "th" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in table". */ - $this->inTable($token); - } - } - - private function inCell($token) { - /* An end tag whose tag name is one of: "td", "th" */ - if($token['type'] === HTML5::ENDTAG && - ($token['name'] === 'td' || $token['name'] === 'th')) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as that of the token, then this is a - parse error and the token must be ignored. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Generate implied end tags, except for elements with the same - tag name as the token. */ - $this->generateImpliedEndTags(array($token['name'])); - - /* Now, if the current node is not an element with the same tag - name as the token, then this is a parse error. */ - // k - - /* Pop elements from this stack until an element with the same - tag name as the token has been popped from the stack. */ - while(true) { - $node = end($this->stack)->nodeName; - array_pop($this->stack); - - if($node === $token['name']) { - break; - } - } - - /* Clear the list of active formatting elements up to the last - marker. */ - $this->clearTheActiveFormattingElementsUpToTheLastMarker(); - - /* Switch the insertion mode to "in row". (The current node - will be a tr element at this point.) */ - $this->mode = self::IN_ROW; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) { - /* If the stack of open elements does not have a td or th element - in table scope, then this is a parse error; ignore the token. - (innerHTML case) */ - if(!$this->elementInScope(array('td', 'th'), true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) { - /* If the stack of open elements does not have a td or th element - in table scope, then this is a parse error; ignore the token. - (innerHTML case) */ - if(!$this->elementInScope(array('td', 'th'), true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html'))) { - /* Parse error. Ignore the token. */ - - /* An end tag whose tag name is one of: "table", "tbody", "tfoot", - "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as that of the token (which can only - happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), - then this is a parse error and the token must be ignored. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in body". */ - $this->inBody($token); - } - } - - private function inSelect($token) { - /* Handle the token as follows: */ - - /* A character token */ - if($token['type'] === HTML5::CHARACTR) { - /* Append the token's character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token whose tag name is "option" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'option') { - /* If the current node is an option element, act as if an end tag - with the tag name "option" had been seen. */ - if(end($this->stack)->nodeName === 'option') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* A start tag token whose tag name is "optgroup" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'optgroup') { - /* If the current node is an option element, act as if an end tag - with the tag name "option" had been seen. */ - if(end($this->stack)->nodeName === 'option') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* If the current node is an optgroup element, act as if an end tag - with the tag name "optgroup" had been seen. */ - if(end($this->stack)->nodeName === 'optgroup') { - $this->inSelect(array( - 'name' => 'optgroup', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* An end tag token whose tag name is "optgroup" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'optgroup') { - /* First, if the current node is an option element, and the node - immediately before it in the stack of open elements is an optgroup - element, then act as if an end tag with the tag name "option" had - been seen. */ - $elements_in_stack = count($this->stack); - - if($this->stack[$elements_in_stack - 1]->nodeName === 'option' && - $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* If the current node is an optgroup element, then pop that node - from the stack of open elements. Otherwise, this is a parse error, - ignore the token. */ - if($this->stack[$elements_in_stack - 1] === 'optgroup') { - array_pop($this->stack); - } - - /* An end tag token whose tag name is "option" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'option') { - /* If the current node is an option element, then pop that node - from the stack of open elements. Otherwise, this is a parse error, - ignore the token. */ - if(end($this->stack)->nodeName === 'option') { - array_pop($this->stack); - } - - /* An end tag whose tag name is "select" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'select') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // w/e - - /* Otherwise: */ - } else { - /* Pop elements from the stack of open elements until a select - element has been popped from the stack. */ - while(true) { - $current = end($this->stack)->nodeName; - array_pop($this->stack); - - if($current === 'select') { - break; - } - } - - /* Reset the insertion mode appropriately. */ - $this->resetInsertionMode(); - } - - /* A start tag whose tag name is "select" */ - } elseif($token['name'] === 'select' && - $token['type'] === HTML5::STARTTAG) { - /* Parse error. Act as if the token had been an end tag with the - tag name "select" instead. */ - $this->inSelect(array( - 'name' => 'select', - 'type' => HTML5::ENDTAG - )); - - /* An end tag whose tag name is one of: "caption", "table", "tbody", - "tfoot", "thead", "tr", "td", "th" */ - } elseif(in_array($token['name'], array('caption', 'table', 'tbody', - 'tfoot', 'thead', 'tr', 'td', 'th')) && $token['type'] === HTML5::ENDTAG) { - /* Parse error. */ - // w/e - - /* If the stack of open elements has an element in table scope with - the same tag name as that of the token, then act as if an end tag - with the tag name "select" had been seen, and reprocess the token. - Otherwise, ignore the token. */ - if($this->elementInScope($token['name'], true)) { - $this->inSelect(array( - 'name' => 'select', - 'type' => HTML5::ENDTAG - )); - - $this->mainPhase($token); - } - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function afterBody($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Process the token as it would be processed if the insertion mode - was "in body". */ - $this->inBody($token); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the first element in the stack of open - elements (the html element), with the data attribute set to the - data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->stack[0]->appendChild($comment); - - /* An end tag with the tag name "html" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { - /* If the parser was originally created in order to handle the - setting of an element's innerHTML attribute, this is a parse error; - ignore the token. (The element will be an html element in this - case.) (innerHTML case) */ - - /* Otherwise, switch to the trailing end phase. */ - $this->phase = self::END_PHASE; - - /* Anything else */ - } else { - /* Parse error. Set the insertion mode to "in body" and reprocess - the token. */ - $this->mode = self::IN_BODY; - return $this->inBody($token); - } - } - - private function inFrameset($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag with the tag name "frameset" */ - } elseif($token['name'] === 'frameset' && - $token['type'] === HTML5::STARTTAG) { - $this->insertElement($token); - - /* An end tag with the tag name "frameset" */ - } elseif($token['name'] === 'frameset' && - $token['type'] === HTML5::ENDTAG) { - /* If the current node is the root html element, then this is a - parse error; ignore the token. (innerHTML case) */ - if(end($this->stack)->nodeName === 'html') { - // Ignore - - } else { - /* Otherwise, pop the current node from the stack of open - elements. */ - array_pop($this->stack); - - /* If the parser was not originally created in order to handle - the setting of an element's innerHTML attribute (innerHTML case), - and the current node is no longer a frameset element, then change - the insertion mode to "after frameset". */ - $this->mode = self::AFTR_FRAME; - } - - /* A start tag with the tag name "frame" */ - } elseif($token['name'] === 'frame' && - $token['type'] === HTML5::STARTTAG) { - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - - /* A start tag with the tag name "noframes" */ - } elseif($token['name'] === 'noframes' && - $token['type'] === HTML5::STARTTAG) { - /* Process the token as if the insertion mode had been "in body". */ - $this->inBody($token); - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function afterFrameset($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* An end tag with the tag name "html" */ - } elseif($token['name'] === 'html' && - $token['type'] === HTML5::ENDTAG) { - /* Switch to the trailing end phase. */ - $this->phase = self::END_PHASE; - - /* A start tag with the tag name "noframes" */ - } elseif($token['name'] === 'noframes' && - $token['type'] === HTML5::STARTTAG) { - /* Process the token as if the insertion mode had been "in body". */ - $this->inBody($token); - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function trailingEndPhase($token) { - /* After the main phase, as each token is emitted from the tokenisation - stage, it must be processed as described in this section. */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the Document object with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->dom->appendChild($comment); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Process the token as it would be processed in the main phase. */ - $this->mainPhase($token); - - /* A character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. Or a start tag token. Or an end tag token. */ - } elseif(($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || - $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG) { - /* Parse error. Switch back to the main phase and reprocess the - token. */ - $this->phase = self::MAIN_PHASE; - return $this->mainPhase($token); - - /* An end-of-file token */ - } elseif($token['type'] === HTML5::EOF) { - /* OMG DONE!! */ - } - } - - private function insertElement($token, $append = true) { - $el = $this->dom->createElement($token['name']); - - foreach($token['attr'] as $attr) { - if(!$el->hasAttribute($attr['name'])) { - $el->setAttribute($attr['name'], $attr['value']); - } - } - - $this->appendToRealParent($el); - $this->stack[] = $el; - - return $el; - } - - private function insertText($data) { - $text = $this->dom->createTextNode($data); - $this->appendToRealParent($text); - } - - private function insertComment($data) { - $comment = $this->dom->createComment($data); - $this->appendToRealParent($comment); - } - - private function appendToRealParent($node) { - if($this->foster_parent === null) { - end($this->stack)->appendChild($node); - - } elseif($this->foster_parent !== null) { - /* If the foster parent element is the parent element of the - last table element in the stack of open elements, then the new - node must be inserted immediately before the last table element - in the stack of open elements in the foster parent element; - otherwise, the new node must be appended to the foster parent - element. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === 'table' && - $this->stack[$n]->parentNode !== null) { - $table = $this->stack[$n]; - break; - } - } - - if(isset($table) && $this->foster_parent->isSameNode($table->parentNode)) - $this->foster_parent->insertBefore($node, $table); - else - $this->foster_parent->appendChild($node); - - $this->foster_parent = null; - } - } - - private function elementInScope($el, $table = false) { - if(is_array($el)) { - foreach($el as $element) { - if($this->elementInScope($element, $table)) { - return true; - } - } - - return false; - } - - $leng = count($this->stack); - - for($n = 0; $n < $leng; $n++) { - /* 1. Initialise node to be the current node (the bottommost node of - the stack). */ - $node = $this->stack[$leng - 1 - $n]; - - if($node->tagName === $el) { - /* 2. If node is the target node, terminate in a match state. */ - return true; - - } elseif($node->tagName === 'table') { - /* 3. Otherwise, if node is a table element, terminate in a failure - state. */ - return false; - - } elseif($table === true && in_array($node->tagName, array('caption', 'td', - 'th', 'button', 'marquee', 'object'))) { - /* 4. Otherwise, if the algorithm is the "has an element in scope" - variant (rather than the "has an element in table scope" variant), - and node is one of the following, terminate in a failure state. */ - return false; - - } elseif($node === $node->ownerDocument->documentElement) { - /* 5. Otherwise, if node is an html element (root element), terminate - in a failure state. (This can only happen if the node is the topmost - node of the stack of open elements, and prevents the next step from - being invoked if there are no more elements in the stack.) */ - return false; - } - - /* Otherwise, set node to the previous entry in the stack of open - elements and return to step 2. (This will never fail, since the loop - will always terminate in the previous step if the top of the stack - is reached.) */ - } - } - - private function reconstructActiveFormattingElements() { - /* 1. If there are no entries in the list of active formatting elements, - then there is nothing to reconstruct; stop this algorithm. */ - $formatting_elements = count($this->a_formatting); - - if($formatting_elements === 0) { - return false; - } - - /* 3. Let entry be the last (most recently added) element in the list - of active formatting elements. */ - $entry = end($this->a_formatting); - - /* 2. If the last (most recently added) entry in the list of active - formatting elements is a marker, or if it is an element that is in the - stack of open elements, then there is nothing to reconstruct; stop this - algorithm. */ - if($entry === self::MARKER || in_array($entry, $this->stack, true)) { - return false; - } - - for($a = $formatting_elements - 1; $a >= 0; true) { - /* 4. If there are no entries before entry in the list of active - formatting elements, then jump to step 8. */ - if($a === 0) { - $step_seven = false; - break; - } - - /* 5. Let entry be the entry one earlier than entry in the list of - active formatting elements. */ - $a--; - $entry = $this->a_formatting[$a]; - - /* 6. If entry is neither a marker nor an element that is also in - thetack of open elements, go to step 4. */ - if($entry === self::MARKER || in_array($entry, $this->stack, true)) { - break; - } - } - - while(true) { - /* 7. Let entry be the element one later than entry in the list of - active formatting elements. */ - if(isset($step_seven) && $step_seven === true) { - $a++; - $entry = $this->a_formatting[$a]; - } - - /* 8. Perform a shallow clone of the element entry to obtain clone. */ - $clone = $entry->cloneNode(); - - /* 9. Append clone to the current node and push it onto the stack - of open elements so that it is the new current node. */ - end($this->stack)->appendChild($clone); - $this->stack[] = $clone; - - /* 10. Replace the entry for entry in the list with an entry for - clone. */ - $this->a_formatting[$a] = $clone; - - /* 11. If the entry for clone in the list of active formatting - elements is not the last entry in the list, return to step 7. */ - if(end($this->a_formatting) !== $clone) { - $step_seven = true; - } else { - break; - } - } - } - - private function clearTheActiveFormattingElementsUpToTheLastMarker() { - /* When the steps below require the UA to clear the list of active - formatting elements up to the last marker, the UA must perform the - following steps: */ - - while(true) { - /* 1. Let entry be the last (most recently added) entry in the list - of active formatting elements. */ - $entry = end($this->a_formatting); - - /* 2. Remove entry from the list of active formatting elements. */ - array_pop($this->a_formatting); - - /* 3. If entry was a marker, then stop the algorithm at this point. - The list has been cleared up to the last marker. */ - if($entry === self::MARKER) { - break; - } - } - } - - private function generateImpliedEndTags($exclude = array()) { - /* When the steps below require the UA to generate implied end tags, - then, if the current node is a dd element, a dt element, an li element, - a p element, a td element, a th element, or a tr element, the UA must - act as if an end tag with the respective tag name had been seen and - then generate implied end tags again. */ - $node = end($this->stack); - $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); - - while(in_array(end($this->stack)->nodeName, $elements)) { - array_pop($this->stack); - } - } - - private function getElementCategory($node) { - $name = $node->tagName; - if(in_array($name, $this->special)) - return self::SPECIAL; - - elseif(in_array($name, $this->scoping)) - return self::SCOPING; - - elseif(in_array($name, $this->formatting)) - return self::FORMATTING; - - else - return self::PHRASING; - } - - private function clearStackToTableContext($elements) { - /* When the steps above require the UA to clear the stack back to a - table context, it means that the UA must, while the current node is not - a table element or an html element, pop elements from the stack of open - elements. If this causes any elements to be popped from the stack, then - this is a parse error. */ - while(true) { - $node = end($this->stack)->nodeName; - - if(in_array($node, $elements)) { - break; - } else { - array_pop($this->stack); - } - } - } - - private function resetInsertionMode() { - /* 1. Let last be false. */ - $last = false; - $leng = count($this->stack); - - for($n = $leng - 1; $n >= 0; $n--) { - /* 2. Let node be the last node in the stack of open elements. */ - $node = $this->stack[$n]; - - /* 3. If node is the first node in the stack of open elements, then - set last to true. If the element whose innerHTML attribute is being - set is neither a td element nor a th element, then set node to the - element whose innerHTML attribute is being set. (innerHTML case) */ - if($this->stack[0]->isSameNode($node)) { - $last = true; - } - - /* 4. If node is a select element, then switch the insertion mode to - "in select" and abort these steps. (innerHTML case) */ - if($node->nodeName === 'select') { - $this->mode = self::IN_SELECT; - break; - - /* 5. If node is a td or th element, then switch the insertion mode - to "in cell" and abort these steps. */ - } elseif($node->nodeName === 'td' || $node->nodeName === 'th') { - $this->mode = self::IN_CELL; - break; - - /* 6. If node is a tr element, then switch the insertion mode to - "in row" and abort these steps. */ - } elseif($node->nodeName === 'tr') { - $this->mode = self::IN_ROW; - break; - - /* 7. If node is a tbody, thead, or tfoot element, then switch the - insertion mode to "in table body" and abort these steps. */ - } elseif(in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { - $this->mode = self::IN_TBODY; - break; - - /* 8. If node is a caption element, then switch the insertion mode - to "in caption" and abort these steps. */ - } elseif($node->nodeName === 'caption') { - $this->mode = self::IN_CAPTION; - break; - - /* 9. If node is a colgroup element, then switch the insertion mode - to "in column group" and abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'colgroup') { - $this->mode = self::IN_CGROUP; - break; - - /* 10. If node is a table element, then switch the insertion mode - to "in table" and abort these steps. */ - } elseif($node->nodeName === 'table') { - $this->mode = self::IN_TABLE; - break; - - /* 11. If node is a head element, then switch the insertion mode - to "in body" ("in body"! not "in head"!) and abort these steps. - (innerHTML case) */ - } elseif($node->nodeName === 'head') { - $this->mode = self::IN_BODY; - break; - - /* 12. If node is a body element, then switch the insertion mode to - "in body" and abort these steps. */ - } elseif($node->nodeName === 'body') { - $this->mode = self::IN_BODY; - break; - - /* 13. If node is a frameset element, then switch the insertion - mode to "in frameset" and abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'frameset') { - $this->mode = self::IN_FRAME; - break; - - /* 14. If node is an html element, then: if the head element - pointer is null, switch the insertion mode to "before head", - otherwise, switch the insertion mode to "after head". In either - case, abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'html') { - $this->mode = ($this->head_pointer === null) - ? self::BEFOR_HEAD - : self::AFTER_HEAD; - - break; - - /* 15. If last is true, then set the insertion mode to "in body" - and abort these steps. (innerHTML case) */ - } elseif($last) { - $this->mode = self::IN_BODY; - break; - } - } - } - - private function closeCell() { - /* If the stack of open elements has a td or th element in table scope, - then act as if an end tag token with that tag name had been seen. */ - foreach(array('td', 'th') as $cell) { - if($this->elementInScope($cell, true)) { - $this->inCell(array( - 'name' => $cell, - 'type' => HTML5::ENDTAG - )); - - break; - } - } - } - - public function save() { - return $this->dom; - } -} -?> +normalize($html, $config, $context); + $new_html = $this->wrapHTML($new_html, $config, $context); + try { + $parser = new HTML5($new_html); + $doc = $parser->save(); + } catch (DOMException $e) { + // Uh oh, it failed. Punt to DirectLex. + $lexer = new HTMLPurifier_Lexer_DirectLex(); + $context->register('PH5PError', $e); // save the error, so we can detect it + return $lexer->tokenizeHTML($html, $config, $context); // use original HTML + } + $tokens = array(); + $this->tokenizeDOM( + $doc->getElementsByTagName('html')->item(0)-> // + getElementsByTagName('body')->item(0)-> // + getElementsByTagName('div')->item(0) //
+ , $tokens); + return $tokens; + } + +} + +/* + +Copyright 2007 Jeroen van der Meer + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +class HTML5 { + private $data; + private $char; + private $EOF; + private $state; + private $tree; + private $token; + private $content_model; + private $escape = false; + private $entities = array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute', + 'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;', + 'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;', + 'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;', + 'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;', + 'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;', + 'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;', + 'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;', + 'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;', + 'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN', + 'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;', + 'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;', + 'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig', + 'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;', + 'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;', + 'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil', + 'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;', + 'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;', + 'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;', + 'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth', + 'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12', + 'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt', + 'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc', + 'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;', + 'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;', + 'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;', + 'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro', + 'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;', + 'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;', + 'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;', + 'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash', + 'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;', + 'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;', + 'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;', + 'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;', + 'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;', + 'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;', + 'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;', + 'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;', + 'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc', + 'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;', + 'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;'); + + const PCDATA = 0; + const RCDATA = 1; + const CDATA = 2; + const PLAINTEXT = 3; + + const DOCTYPE = 0; + const STARTTAG = 1; + const ENDTAG = 2; + const COMMENT = 3; + const CHARACTR = 4; + const EOF = 5; + + public function __construct($data) { + $data = str_replace("\r\n", "\n", $data); + $data = str_replace("\r", null, $data); + + $this->data = $data; + $this->char = -1; + $this->EOF = strlen($data); + $this->tree = new HTML5TreeConstructer; + $this->content_model = self::PCDATA; + + $this->state = 'data'; + + while($this->state !== null) { + $this->{$this->state.'State'}(); + } + } + + public function save() { + return $this->tree->save(); + } + + private function char() { + return ($this->char < $this->EOF) + ? $this->data[$this->char] + : false; + } + + private function character($s, $l = 0) { + if($s + $l < $this->EOF) { + if($l === 0) { + return $this->data[$s]; + } else { + return substr($this->data, $s, $l); + } + } + } + + private function characters($char_class, $start) { + return preg_replace('#^(['.$char_class.']+).*#s', '\\1', substr($this->data, $start)); + } + + private function dataState() { + // Consume the next input character + $this->char++; + $char = $this->char(); + + if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { + /* U+0026 AMPERSAND (&) + When the content model flag is set to one of the PCDATA or RCDATA + states: switch to the entity data state. Otherwise: treat it as per + the "anything else" entry below. */ + $this->state = 'entityData'; + + } elseif($char === '-') { + /* If the content model flag is set to either the RCDATA state or + the CDATA state, and the escape flag is false, and there are at + least three characters before this one in the input stream, and the + last four characters in the input stream, including this one, are + U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, + and U+002D HYPHEN-MINUS (""), + set the escape flag to false. */ + if(($this->content_model === self::RCDATA || + $this->content_model === self::CDATA) && $this->escape === true && + $this->character($this->char, 3) === '-->') { + $this->escape = false; + } + + /* In any case, emit the input character as a character token. + Stay in the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => $char + )); + + } elseif($this->char === $this->EOF) { + /* EOF + Emit an end-of-file token. */ + $this->EOF(); + + } elseif($this->content_model === self::PLAINTEXT) { + /* When the content model flag is set to the PLAINTEXT state + THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of + the text and emit it as a character token. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => substr($this->data, $this->char) + )); + + $this->EOF(); + + } else { + /* Anything else + THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that + otherwise would also be treated as a character token and emit it + as a single character token. Stay in the data state. */ + $len = strcspn($this->data, '<&', $this->char); + $char = substr($this->data, $this->char, $len); + $this->char += $len - 1; + + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => $char + )); + + $this->state = 'data'; + } + } + + private function entityDataState() { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, emit a U+0026 AMPERSAND character token. + // Otherwise, emit the character token that was returned. + $char = (!$entity) ? '&' : $entity; + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => $char + )); + + // Finally, switch to the data state. + $this->state = 'data'; + } + + private function tagOpenState() { + switch($this->content_model) { + case self::RCDATA: + case self::CDATA: + /* If the next input character is a U+002F SOLIDUS (/) character, + consume it and switch to the close tag open state. If the next + input character is not a U+002F SOLIDUS (/) character, emit a + U+003C LESS-THAN SIGN character token and switch to the data + state to process the next input character. */ + if($this->character($this->char + 1) === '/') { + $this->char++; + $this->state = 'closeTagOpen'; + + } else { + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => '<' + )); + + $this->state = 'data'; + } + break; + + case self::PCDATA: + // If the content model flag is set to the PCDATA state + // Consume the next input character: + $this->char++; + $char = $this->char(); + + if($char === '!') { + /* U+0021 EXCLAMATION MARK (!) + Switch to the markup declaration open state. */ + $this->state = 'markupDeclarationOpen'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the close tag open state. */ + $this->state = 'closeTagOpen'; + + } elseif(preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new start tag token, set its tag name to the lowercase + version of the input character (add 0x0020 to the character's code + point), then switch to the tag name state. (Don't emit the token + yet; further details will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::STARTTAG, + 'attr' => array() + ); + + $this->state = 'tagName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit a U+003C LESS-THAN SIGN character token and a + U+003E GREATER-THAN SIGN character token. Switch to the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => '<>' + )); + + $this->state = 'data'; + + } elseif($char === '?') { + /* U+003F QUESTION MARK (?) + Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + + } else { + /* Anything else + Parse error. Emit a U+003C LESS-THAN SIGN character token and + reconsume the current input character in the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => '<' + )); + + $this->char--; + $this->state = 'data'; + } + break; + } + } + + private function closeTagOpenState() { + $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); + $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; + + if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && + (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/', + $this->character($this->char + 1 + strlen($next_node))) || $this->EOF === $this->char)))) { + /* If the content model flag is set to the RCDATA or CDATA states then + examine the next few characters. If they do not match the tag name of + the last start tag token emitted (case insensitively), or if they do but + they are not immediately followed by one of the following characters: + * U+0009 CHARACTER TABULATION + * U+000A LINE FEED (LF) + * U+000B LINE TABULATION + * U+000C FORM FEED (FF) + * U+0020 SPACE + * U+003E GREATER-THAN SIGN (>) + * U+002F SOLIDUS (/) + * EOF + ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character + token, a U+002F SOLIDUS character token, and switch to the data state + to process the next input character. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => 'state = 'data'; + + } else { + /* Otherwise, if the content model flag is set to the PCDATA state, + or if the next few characters do match that tag name, consume the + next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new end tag token, set its tag name to the lowercase version + of the input character (add 0x0020 to the character's code point), then + switch to the tag name state. (Don't emit the token yet; further details + will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::ENDTAG + ); + + $this->state = 'tagName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Switch to the data state. */ + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F + SOLIDUS character token. Reconsume the EOF character in the data state. */ + $this->emitToken(array( + 'type' => self::CHARACTR, + 'data' => 'char--; + $this->state = 'data'; + + } else { + /* Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + } + } + } + + private function tagNameState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } else { + /* Anything else + Append the current input character to the current tag token's tag name. + Stay in the tag name state. */ + $this->token['name'] .= strtolower($char); + $this->state = 'tagName'; + } + } + + private function beforeAttributeNameState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Stay in the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function attributeNameState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's name. + Stay in the attribute name state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['name'] .= strtolower($char); + + $this->state = 'attributeName'; + } + } + + private function afterAttributeNameState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the + before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function beforeAttributeValueState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the attribute value (double-quoted) state. */ + $this->state = 'attributeValueDoubleQuoted'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the attribute value (unquoted) state and reconsume + this input character. */ + $this->char--; + $this->state = 'attributeValueUnquoted'; + + } elseif($char === '\'') { + /* U+0027 APOSTROPHE (') + Switch to the attribute value (single-quoted) state. */ + $this->state = 'attributeValueSingleQuoted'; + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Switch to the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function attributeValueDoubleQuotedState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('double'); + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (double-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueDoubleQuoted'; + } + } + + private function attributeValueSingleQuotedState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if($char === '\'') { + /* U+0022 QUOTATION MARK (') + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('single'); + + } elseif($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (single-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueSingleQuoted'; + } + } + + private function attributeValueUnquotedState() { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState(); + + } elseif($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function entityInAttributeValueState() { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, append a U+0026 AMPERSAND character to the + // current attribute's value. Otherwise, emit the character token that + // was returned. + $char = (!$entity) + ? '&' + : $entity; + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + } + + private function bogusCommentState() { + /* Consume every character up to the first U+003E GREATER-THAN SIGN + character (>) or the end of the file (EOF), whichever comes first. Emit + a comment token whose data is the concatenation of all the characters + starting from and including the character that caused the state machine + to switch into the bogus comment state, up to and including the last + consumed character before the U+003E character, if any, or up to the + end of the file otherwise. (If the comment was started by the end of + the file (EOF), the token is empty.) */ + $data = $this->characters('^>', $this->char); + $this->emitToken(array( + 'data' => $data, + 'type' => self::COMMENT + )); + + $this->char += strlen($data); + + /* Switch to the data state. */ + $this->state = 'data'; + + /* If the end of the file was reached, reconsume the EOF character. */ + if($this->char === $this->EOF) { + $this->char = $this->EOF - 1; + } + } + + private function markupDeclarationOpenState() { + /* If the next two characters are both U+002D HYPHEN-MINUS (-) + characters, consume those two characters, create a comment token whose + data is the empty string, and switch to the comment state. */ + if($this->character($this->char + 1, 2) === '--') { + $this->char += 2; + $this->state = 'comment'; + $this->token = array( + 'data' => null, + 'type' => self::COMMENT + ); + + /* Otherwise if the next seven chacacters are a case-insensitive match + for the word "DOCTYPE", then consume those characters and switch to the + DOCTYPE state. */ + } elseif(strtolower($this->character($this->char + 1, 7)) === 'doctype') { + $this->char += 7; + $this->state = 'doctype'; + + /* Otherwise, is is a parse error. Switch to the bogus comment state. + The next character that is consumed, if any, is the first character + that will be in the comment. */ + } else { + $this->char++; + $this->state = 'bogusComment'; + } + } + + private function commentState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if($char === '-') { + /* Switch to the comment dash state */ + $this->state = 'commentDash'; + + /* EOF */ + } elseif($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append the input character to the comment token's data. Stay in + the comment state. */ + $this->token['data'] .= $char; + } + } + + private function commentDashState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if($char === '-') { + /* Switch to the comment end state */ + $this->state = 'commentEnd'; + + /* EOF */ + } elseif($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append a U+002D HYPHEN-MINUS (-) character and the input + character to the comment token's data. Switch to the comment state. */ + $this->token['data'] .= '-'.$char; + $this->state = 'comment'; + } + } + + private function commentEndState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($char === '-') { + $this->token['data'] .= '-'; + + } elseif($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['data'] .= '--'.$char; + $this->state = 'comment'; + } + } + + private function doctypeState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'beforeDoctypeName'; + + } else { + $this->char--; + $this->state = 'beforeDoctypeName'; + } + } + + private function beforeDoctypeNameState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the before DOCTYPE name state. + + } elseif(preg_match('/^[a-z]$/', $char)) { + $this->token = array( + 'name' => strtoupper($char), + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + + } elseif($char === '>') { + $this->emitToken(array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + )); + + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + $this->emitToken(array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + )); + + $this->char--; + $this->state = 'data'; + + } else { + $this->token = array( + 'name' => $char, + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + } + } + + private function doctypeNameState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'AfterDoctypeName'; + + } elseif($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif(preg_match('/^[a-z]$/', $char)) { + $this->token['name'] .= strtoupper($char); + + } elseif($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['name'] .= $char; + } + + $this->token['error'] = ($this->token['name'] === 'HTML') + ? false + : true; + } + + private function afterDoctypeNameState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the DOCTYPE name state. + + } elseif($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['error'] = true; + $this->state = 'bogusDoctype'; + } + } + + private function bogusDoctypeState() { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + // Stay in the bogus DOCTYPE state. + } + } + + private function entity() { + $start = $this->char; + + // This section defines how to consume an entity. This definition is + // used when parsing entities in text and in attributes. + + // The behaviour depends on the identity of the next character (the + // one immediately after the U+0026 AMPERSAND character): + + switch($this->character($this->char + 1)) { + // U+0023 NUMBER SIGN (#) + case '#': + + // The behaviour further depends on the character after the + // U+0023 NUMBER SIGN: + switch($this->character($this->char + 1)) { + // U+0078 LATIN SMALL LETTER X + // U+0058 LATIN CAPITAL LETTER X + case 'x': + case 'X': + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 + // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER + // A, through to U+0046 LATIN CAPITAL LETTER F (in other + // words, 0-9, A-F, a-f). + $char = 1; + $char_class = '0-9A-Fa-f'; + break; + + // Anything else + default: + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE (i.e. just 0-9). + $char = 0; + $char_class = '0-9'; + break; + } + + // Consume as many characters as match the range of characters + // given above. + $this->char++; + $e_name = $this->characters($char_class, $this->char + $char + 1); + $entity = $this->character($start, $this->char); + $cond = strlen($e_name) > 0; + + // The rest of the parsing happens bellow. + break; + + // Anything else + default: + // Consume the maximum number of characters possible, with the + // consumed characters case-sensitively matching one of the + // identifiers in the first column of the entities table. + $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); + $len = strlen($e_name); + + for($c = 1; $c <= $len; $c++) { + $id = substr($e_name, 0, $c); + $this->char++; + + if(in_array($id, $this->entities)) { + if ($e_name[$c-1] !== ';') { + if ($c < $len && $e_name[$c] == ';') { + $this->char++; // consume extra semicolon + } + } + $entity = $id; + break; + } + } + + $cond = isset($entity); + // The rest of the parsing happens bellow. + break; + } + + if(!$cond) { + // If no match can be made, then this is a parse error. No + // characters are consumed, and nothing is returned. + $this->char = $start; + return false; + } + + // Return a character token for the character corresponding to the + // entity name (as given by the second column of the entities table). + return html_entity_decode('&'.$entity.';', ENT_QUOTES, 'UTF-8'); + } + + private function emitToken($token) { + $emit = $this->tree->emitToken($token); + + if(is_int($emit)) { + $this->content_model = $emit; + + } elseif($token['type'] === self::ENDTAG) { + $this->content_model = self::PCDATA; + } + } + + private function EOF() { + $this->state = null; + $this->tree->emitToken(array( + 'type' => self::EOF + )); + } +} + +class HTML5TreeConstructer { + public $stack = array(); + + private $phase; + private $mode; + private $dom; + private $foster_parent = null; + private $a_formatting = array(); + + private $head_pointer = null; + private $form_pointer = null; + + private $scoping = array('button','caption','html','marquee','object','table','td','th'); + private $formatting = array('a','b','big','em','font','i','nobr','s','small','strike','strong','tt','u'); + private $special = array('address','area','base','basefont','bgsound', + 'blockquote','body','br','center','col','colgroup','dd','dir','div','dl', + 'dt','embed','fieldset','form','frame','frameset','h1','h2','h3','h4','h5', + 'h6','head','hr','iframe','image','img','input','isindex','li','link', + 'listing','menu','meta','noembed','noframes','noscript','ol','optgroup', + 'option','p','param','plaintext','pre','script','select','spacer','style', + 'tbody','textarea','tfoot','thead','title','tr','ul','wbr'); + + // The different phases. + const INIT_PHASE = 0; + const ROOT_PHASE = 1; + const MAIN_PHASE = 2; + const END_PHASE = 3; + + // The different insertion modes for the main phase. + const BEFOR_HEAD = 0; + const IN_HEAD = 1; + const AFTER_HEAD = 2; + const IN_BODY = 3; + const IN_TABLE = 4; + const IN_CAPTION = 5; + const IN_CGROUP = 6; + const IN_TBODY = 7; + const IN_ROW = 8; + const IN_CELL = 9; + const IN_SELECT = 10; + const AFTER_BODY = 11; + const IN_FRAME = 12; + const AFTR_FRAME = 13; + + // The different types of elements. + const SPECIAL = 0; + const SCOPING = 1; + const FORMATTING = 2; + const PHRASING = 3; + + const MARKER = 0; + + public function __construct() { + $this->phase = self::INIT_PHASE; + $this->mode = self::BEFOR_HEAD; + $this->dom = new DOMDocument; + + $this->dom->encoding = 'UTF-8'; + $this->dom->preserveWhiteSpace = true; + $this->dom->substituteEntities = true; + $this->dom->strictErrorChecking = false; + } + + // Process tag tokens + public function emitToken($token) { + switch($this->phase) { + case self::INIT_PHASE: return $this->initPhase($token); break; + case self::ROOT_PHASE: return $this->rootElementPhase($token); break; + case self::MAIN_PHASE: return $this->mainPhase($token); break; + case self::END_PHASE : return $this->trailingEndPhase($token); break; + } + } + + private function initPhase($token) { + /* Initially, the tree construction stage must handle each token + emitted from the tokenisation stage as follows: */ + + /* A DOCTYPE token that is marked as being in error + A comment token + A start tag token + An end tag token + A character token that is not one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE + An end-of-file token */ + if((isset($token['error']) && $token['error']) || + $token['type'] === HTML5::COMMENT || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF || + ($token['type'] === HTML5::CHARACTR && isset($token['data']) && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))) { + /* This specification does not define how to handle this case. In + particular, user agents may ignore the entirety of this specification + altogether for such documents, and instead invoke special parse modes + with a greater emphasis on backwards compatibility. */ + + $this->phase = self::ROOT_PHASE; + return $this->rootElementPhase($token); + + /* A DOCTYPE token marked as being correct */ + } elseif(isset($token['error']) && !$token['error']) { + /* Append a DocumentType node to the Document node, with the name + attribute set to the name given in the DOCTYPE token (which will be + "HTML"), and the other attributes specific to DocumentType objects + set to null, empty lists, or the empty string as appropriate. */ + $doctype = new DOMDocumentType(null, null, 'HTML'); + + /* Then, switch to the root element phase of the tree construction + stage. */ + $this->phase = self::ROOT_PHASE; + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif(isset($token['data']) && preg_match('/^[\t\n\x0b\x0c ]+$/', + $token['data'])) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + } + } + + private function rootElementPhase($token) { + /* After the initial phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED + (FF), or U+0020 SPACE + A start tag token + An end tag token + An end-of-file token */ + } elseif(($token['type'] === HTML5::CHARACTR && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF) { + /* Create an HTMLElement node with the tag name html, in the HTML + namespace. Append it to the Document object. Switch to the main + phase and reprocess the current token. */ + $html = $this->dom->createElement('html'); + $this->dom->appendChild($html); + $this->stack[] = $html; + + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + } + } + + private function mainPhase($token) { + /* Tokens in the main phase must be handled as follows: */ + + /* A DOCTYPE token */ + if($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A start tag token with the tag name "html" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { + /* If this start tag token was not the first start tag token, then + it is a parse error. */ + + /* For each attribute on the token, check to see if the attribute + is already present on the top element of the stack of open elements. + If it is not, add the attribute and its corresponding value to that + element. */ + foreach($token['attr'] as $attr) { + if(!$this->stack[0]->hasAttribute($attr['name'])) { + $this->stack[0]->setAttribute($attr['name'], $attr['value']); + } + } + + /* An end-of-file token */ + } elseif($token['type'] === HTML5::EOF) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Anything else. */ + } else { + /* Depends on the insertion mode: */ + switch($this->mode) { + case self::BEFOR_HEAD: return $this->beforeHead($token); break; + case self::IN_HEAD: return $this->inHead($token); break; + case self::AFTER_HEAD: return $this->afterHead($token); break; + case self::IN_BODY: return $this->inBody($token); break; + case self::IN_TABLE: return $this->inTable($token); break; + case self::IN_CAPTION: return $this->inCaption($token); break; + case self::IN_CGROUP: return $this->inColumnGroup($token); break; + case self::IN_TBODY: return $this->inTableBody($token); break; + case self::IN_ROW: return $this->inRow($token); break; + case self::IN_CELL: return $this->inCell($token); break; + case self::IN_SELECT: return $this->inSelect($token); break; + case self::AFTER_BODY: return $this->afterBody($token); break; + case self::IN_FRAME: return $this->inFrameset($token); break; + case self::AFTR_FRAME: return $this->afterFrameset($token); break; + case self::END_PHASE: return $this->trailingEndPhase($token); break; + } + } + } + + private function beforeHead($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "head" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { + /* Create an element for the token, append the new element to the + current node and push it onto the stack of open elements. */ + $element = $this->insertElement($token); + + /* Set the head element pointer to this new element node. */ + $this->head_pointer = $element; + + /* Change the insertion mode to "in head". */ + $this->mode = self::IN_HEAD; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title". Or an end tag with the tag name "html". + Or a character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or any other start tag token */ + } elseif($token['type'] === HTML5::STARTTAG || + ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || + ($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]$/', + $token['data']))) { + /* Act as if a start tag token with the tag name "head" and no + attributes had been seen, then reprocess the current token. */ + $this->beforeHead(array( + 'name' => 'head', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + return $this->inHead($token); + + /* Any other end tag */ + } elseif($token['type'] === HTML5::ENDTAG) { + /* Parse error. Ignore the token. */ + } + } + + private function inHead($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. + + THIS DIFFERS FROM THE SPEC: If the current node is either a title, style + or script element, append the character to the current node regardless + of its content. */ + if(($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( + $token['type'] === HTML5::CHARACTR && in_array(end($this->stack)->nodeName, + array('title', 'style', 'script')))) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + } elseif($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('title', 'style', 'script'))) { + array_pop($this->stack); + return HTML5::PCDATA; + + /* A start tag with the tag name "title" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $element = $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the RCDATA state. */ + return HTML5::RCDATA; + + /* A start tag with the tag name "style" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "script" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { + /* Create an element for the token. */ + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "base", "link", or "meta" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('base', 'link', 'meta'))) { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + array_pop($this->stack); + + } else { + $this->insertElement($token); + } + + /* An end tag with the tag name "head" */ + } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { + /* If the current node is a head element, pop the current node off + the stack of open elements. */ + if($this->head_pointer->isSameNode(end($this->stack))) { + array_pop($this->stack); + + /* Otherwise, this is a parse error. */ + } else { + // k + } + + /* Change the insertion mode to "after head". */ + $this->mode = self::AFTER_HEAD; + + /* A start tag with the tag name "head" or an end tag except "html". */ + } elseif(($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || + ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* If the current node is a head element, act as if an end tag + token with the tag name "head" had been seen. */ + if($this->head_pointer->isSameNode(end($this->stack))) { + $this->inHead(array( + 'name' => 'head', + 'type' => HTML5::ENDTAG + )); + + /* Otherwise, change the insertion mode to "after head". */ + } else { + $this->mode = self::AFTER_HEAD; + } + + /* Then, reprocess the current token. */ + return $this->afterHead($token); + } + } + + private function afterHead($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "body" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { + /* Insert a body element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in body". */ + $this->mode = self::IN_BODY; + + /* A start tag token with the tag name "frameset" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { + /* Insert a frameset element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in frameset". */ + $this->mode = self::IN_FRAME; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('base', 'link', 'meta', 'script', 'style', 'title'))) { + /* Parse error. Switch the insertion mode back to "in head" and + reprocess the token. */ + $this->mode = self::IN_HEAD; + return $this->inHead($token); + + /* Anything else */ + } else { + /* Act as if a start tag token with the tag name "body" and no + attributes had been seen, and then reprocess the current token. */ + $this->afterHead(array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + return $this->inBody($token); + } + } + + private function inBody($token) { + /* Handle the token as follows: */ + + switch($token['type']) { + /* A character token */ + case HTML5::CHARACTR: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + break; + + /* A comment token */ + case HTML5::COMMENT: + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + break; + + case HTML5::STARTTAG: + switch($token['name']) { + /* A start tag token whose tag name is one of: "script", + "style" */ + case 'script': case 'style': + /* Process the token as if the insertion mode had been "in + head". */ + return $this->inHead($token); + break; + + /* A start tag token whose tag name is one of: "base", "link", + "meta", "title" */ + case 'base': case 'link': case 'meta': case 'title': + /* Parse error. Process the token as if the insertion mode + had been "in head". */ + return $this->inHead($token); + break; + + /* A start tag token with the tag name "body" */ + case 'body': + /* Parse error. If the second element on the stack of open + elements is not a body element, or, if the stack of open + elements has only one node on it, then ignore the token. + (innerHTML case) */ + if(count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { + // Ignore + + /* Otherwise, for each attribute on the token, check to see + if the attribute is already present on the body element (the + second element) on the stack of open elements. If it is not, + add the attribute and its corresponding value to that + element. */ + } else { + foreach($token['attr'] as $attr) { + if(!$this->stack[1]->hasAttribute($attr['name'])) { + $this->stack[1]->setAttribute($attr['name'], $attr['value']); + } + } + } + break; + + /* A start tag whose tag name is one of: "address", + "blockquote", "center", "dir", "div", "dl", "fieldset", + "listing", "menu", "ol", "p", "ul" */ + case 'address': case 'blockquote': case 'center': case 'dir': + case 'div': case 'dl': case 'fieldset': case 'listing': + case 'menu': case 'ol': case 'p': case 'ul': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "form" */ + case 'form': + /* If the form element pointer is not null, ignore the + token with a parse error. */ + if($this->form_pointer !== null) { + // Ignore. + + /* Otherwise: */ + } else { + /* If the stack of open elements has a p element in + scope, then act as if an end tag with the tag name p + had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token, and set the + form element pointer to point to the element created. */ + $element = $this->insertElement($token); + $this->form_pointer = $element; + } + break; + + /* A start tag whose tag name is "li", "dd" or "dt" */ + case 'li': case 'dd': case 'dt': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + $stack_length = count($this->stack) - 1; + + for($n = $stack_length; 0 <= $n; $n--) { + /* 1. Initialise node to be the current node (the + bottommost node of the stack). */ + $stop = false; + $node = $this->stack[$n]; + $cat = $this->getElementCategory($node->tagName); + + /* 2. If node is an li, dd or dt element, then pop all + the nodes from the current node up to node, including + node, then stop this algorithm. */ + if($token['name'] === $node->tagName || ($token['name'] !== 'li' + && ($node->tagName === 'dd' || $node->tagName === 'dt'))) { + for($x = $stack_length; $x >= $n ; $x--) { + array_pop($this->stack); + } + + break; + } + + /* 3. If node is not in the formatting category, and is + not in the phrasing category, and is not an address or + div element, then stop this algorithm. */ + if($cat !== self::FORMATTING && $cat !== self::PHRASING && + $node->tagName !== 'address' && $node->tagName !== 'div') { + break; + } + } + + /* Finally, insert an HTML element with the same tag + name as the token's. */ + $this->insertElement($token); + break; + + /* A start tag token whose tag name is "plaintext" */ + case 'plaintext': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + return HTML5::PLAINTEXT; + break; + + /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + this is a parse error; pop elements from the stack until an + element with one of those tag names has been popped from the + stack. */ + while($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { + array_pop($this->stack); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "a" */ + case 'a': + /* If the list of active formatting elements contains + an element whose tag name is "a" between the end of the + list and the last marker on the list (or the start of + the list if there is no marker on the list), then this + is a parse error; act as if an end tag with the tag name + "a" had been seen, then remove that element from the list + of active formatting elements and the stack of open + elements if the end tag didn't already remove it (it + might not have if the element is not in table scope). */ + $leng = count($this->a_formatting); + + for($n = $leng - 1; $n >= 0; $n--) { + if($this->a_formatting[$n] === self::MARKER) { + break; + + } elseif($this->a_formatting[$n]->nodeName === 'a') { + $this->emitToken(array( + 'name' => 'a', + 'type' => HTML5::ENDTAG + )); + break; + } + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag whose tag name is one of: "b", "big", "em", "font", + "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'b': case 'big': case 'em': case 'font': case 'i': + case 'nobr': case 's': case 'small': case 'strike': + case 'strong': case 'tt': case 'u': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag token whose tag name is "button" */ + case 'button': + /* If the stack of open elements has a button element in scope, + then this is a parse error; act as if an end tag with the tag + name "button" had been seen, then reprocess the token. (We don't + do that. Unnecessary.) */ + if($this->elementInScope('button')) { + $this->inBody(array( + 'name' => 'button', + 'type' => HTML5::ENDTAG + )); + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is one of: "marquee", "object" */ + case 'marquee': case 'object': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is "xmp" */ + case 'xmp': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Switch the content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "table" */ + case 'table': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + break; + + /* A start tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ + case 'area': case 'basefont': case 'bgsound': case 'br': + case 'embed': case 'img': case 'param': case 'spacer': + case 'wbr': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "hr" */ + case 'hr': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if($this->elementInScope('p')) { + $this->emitToken(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "image" */ + case 'image': + /* Parse error. Change the token's tag name to "img" and + reprocess it. (Don't ask.) */ + $token['name'] = 'img'; + return $this->inBody($token); + break; + + /* A start tag whose tag name is "input" */ + case 'input': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an input element for the token. */ + $element = $this->insertElement($token, false); + + /* If the form element pointer is not null, then associate the + input element with the form element pointed to by the form + element pointer. */ + $this->form_pointer !== null + ? $this->form_pointer->appendChild($element) + : end($this->stack)->appendChild($element); + + /* Pop that input element off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "isindex" */ + case 'isindex': + /* Parse error. */ + // w/e + + /* If the form element pointer is not null, + then ignore the token. */ + if($this->form_pointer === null) { + /* Act as if a start tag token with the tag name "form" had + been seen. */ + $this->inBody(array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody(array( + 'name' => 'hr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + /* Act as if a start tag token with the tag name "p" had + been seen. */ + $this->inBody(array( + 'name' => 'p', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + /* Act as if a start tag token with the tag name "label" + had been seen. */ + $this->inBody(array( + 'name' => 'label', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + /* Act as if a stream of character tokens had been seen. */ + $this->insertText('This is a searchable index. '. + 'Insert your search keywords here: '); + + /* Act as if a start tag token with the tag name "input" + had been seen, with all the attributes from the "isindex" + token, except with the "name" attribute set to the value + "isindex" (ignoring any explicit "name" attribute). */ + $attr = $token['attr']; + $attr[] = array('name' => 'name', 'value' => 'isindex'); + + $this->inBody(array( + 'name' => 'input', + 'type' => HTML5::STARTTAG, + 'attr' => $attr + )); + + /* Act as if a stream of character tokens had been seen + (see below for what they should say). */ + $this->insertText('This is a searchable index. '. + 'Insert your search keywords here: '); + + /* Act as if an end tag token with the tag name "label" + had been seen. */ + $this->inBody(array( + 'name' => 'label', + 'type' => HTML5::ENDTAG + )); + + /* Act as if an end tag token with the tag name "p" had + been seen. */ + $this->inBody(array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + )); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody(array( + 'name' => 'hr', + 'type' => HTML5::ENDTAG + )); + + /* Act as if an end tag token with the tag name "form" had + been seen. */ + $this->inBody(array( + 'name' => 'form', + 'type' => HTML5::ENDTAG + )); + } + break; + + /* A start tag whose tag name is "textarea" */ + case 'textarea': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the + RCDATA state. */ + return HTML5::RCDATA; + break; + + /* A start tag whose tag name is one of: "iframe", "noembed", + "noframes" */ + case 'iframe': case 'noembed': case 'noframes': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "select" */ + case 'select': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in select". */ + $this->mode = self::IN_SELECT; + break; + + /* A start or end tag whose tag name is one of: "caption", "col", + "colgroup", "frame", "frameset", "head", "option", "optgroup", + "tbody", "td", "tfoot", "th", "thead", "tr". */ + case 'caption': case 'col': case 'colgroup': case 'frame': + case 'frameset': case 'head': case 'option': case 'optgroup': + case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': + case 'tr': + // Parse error. Ignore the token. + break; + + /* A start or end tag whose tag name is one of: "event-source", + "section", "nav", "article", "aside", "header", "footer", + "datagrid", "command" */ + case 'event-source': case 'section': case 'nav': case 'article': + case 'aside': case 'header': case 'footer': case 'datagrid': + case 'command': + // Work in progress! + break; + + /* A start tag token not covered by the previous entries */ + default: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + $this->insertElement($token, true, true); + break; + } + break; + + case HTML5::ENDTAG: + switch($token['name']) { + /* An end tag with the tag name "body" */ + case 'body': + /* If the second element in the stack of open elements is + not a body element, this is a parse error. Ignore the token. + (innerHTML case) */ + if(count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { + // Ignore. + + /* If the current node is not the body element, then this + is a parse error. */ + } elseif(end($this->stack)->nodeName !== 'body') { + // Parse error. + } + + /* Change the insertion mode to "after body". */ + $this->mode = self::AFTER_BODY; + break; + + /* An end tag with the tag name "html" */ + case 'html': + /* Act as if an end tag with tag name "body" had been seen, + then, if that token wasn't ignored, reprocess the current + token. */ + $this->inBody(array( + 'name' => 'body', + 'type' => HTML5::ENDTAG + )); + + return $this->afterBody($token); + break; + + /* An end tag whose tag name is one of: "address", "blockquote", + "center", "dir", "div", "dl", "fieldset", "listing", "menu", + "ol", "pre", "ul" */ + case 'address': case 'blockquote': case 'center': case 'dir': + case 'div': case 'dl': case 'fieldset': case 'listing': + case 'menu': case 'ol': case 'pre': case 'ul': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with + the same tag name as that of the token, then this + is a parse error. */ + // w/e + + /* If the stack of open elements has an element in + scope with the same tag name as that of the token, + then pop elements from this stack until an element + with that tag name has been popped from the stack. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is "form" */ + case 'form': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + } + + if(end($this->stack)->nodeName !== $token['name']) { + /* Now, if the current node is not an element with the + same tag name as that of the token, then this is a parse + error. */ + // w/e + + } else { + /* Otherwise, if the current node is an element with + the same tag name as that of the token pop that element + from the stack. */ + array_pop($this->stack); + } + + /* In any case, set the form element pointer to null. */ + $this->form_pointer = null; + break; + + /* An end tag whose tag name is "p" */ + case 'p': + /* If the stack of open elements has a p element in scope, + then generate implied end tags, except for p elements. */ + if($this->elementInScope('p')) { + $this->generateImpliedEndTags(array('p')); + + /* If the current node is not a p element, then this is + a parse error. */ + // k + + /* If the stack of open elements has a p element in + scope, then pop elements from this stack until the stack + no longer has a p element in scope. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->elementInScope('p')) { + array_pop($this->stack); + + } else { + break; + } + } + } + break; + + /* An end tag whose tag name is "dd", "dt", or "li" */ + case 'dd': case 'dt': case 'li': + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + generate implied end tags, except for elements with the + same tag name as the token. */ + if($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(array($token['name'])); + + /* If the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + pop elements from this stack until an element with that + tag name has been popped from the stack. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': + $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + generate implied end tags. */ + if($this->elementInScope($elements)) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as that of the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has in scope an element + whose tag name is one of "h1", "h2", "h3", "h4", "h5", or + "h6", then pop elements from the stack until an element + with one of those tag names has been popped from the stack. */ + while($this->elementInScope($elements)) { + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "a", "b", "big", "em", + "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'a': case 'b': case 'big': case 'em': case 'font': + case 'i': case 'nobr': case 's': case 'small': case 'strike': + case 'strong': case 'tt': case 'u': + /* 1. Let the formatting element be the last element in + the list of active formatting elements that: + * is between the end of the list and the last scope + marker in the list, if any, or the start of the list + otherwise, and + * has the same tag name as the token. + */ + while(true) { + for($a = count($this->a_formatting) - 1; $a >= 0; $a--) { + if($this->a_formatting[$a] === self::MARKER) { + break; + + } elseif($this->a_formatting[$a]->tagName === $token['name']) { + $formatting_element = $this->a_formatting[$a]; + $in_stack = in_array($formatting_element, $this->stack, true); + $fe_af_pos = $a; + break; + } + } + + /* If there is no such node, or, if that node is + also in the stack of open elements but the element + is not in scope, then this is a parse error. Abort + these steps. The token is ignored. */ + if(!isset($formatting_element) || ($in_stack && + !$this->elementInScope($token['name']))) { + break; + + /* Otherwise, if there is such a node, but that node + is not in the stack of open elements, then this is a + parse error; remove the element from the list, and + abort these steps. */ + } elseif(isset($formatting_element) && !$in_stack) { + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 2. Let the furthest block be the topmost node in the + stack of open elements that is lower in the stack + than the formatting element, and is not an element in + the phrasing or formatting categories. There might + not be one. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $length = count($this->stack); + + for($s = $fe_s_pos + 1; $s < $length; $s++) { + $category = $this->getElementCategory($this->stack[$s]->nodeName); + + if($category !== self::PHRASING && $category !== self::FORMATTING) { + $furthest_block = $this->stack[$s]; + } + } + + /* 3. If there is no furthest block, then the UA must + skip the subsequent steps and instead just pop all + the nodes from the bottom of the stack of open + elements, from the current node up to the formatting + element, and remove the formatting element from the + list of active formatting elements. */ + if(!isset($furthest_block)) { + for($n = $length - 1; $n >= $fe_s_pos; $n--) { + array_pop($this->stack); + } + + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 4. Let the common ancestor be the element + immediately above the formatting element in the stack + of open elements. */ + $common_ancestor = $this->stack[$fe_s_pos - 1]; + + /* 5. If the furthest block has a parent node, then + remove the furthest block from its parent node. */ + if($furthest_block->parentNode !== null) { + $furthest_block->parentNode->removeChild($furthest_block); + } + + /* 6. Let a bookmark note the position of the + formatting element in the list of active formatting + elements relative to the elements on either side + of it in the list. */ + $bookmark = $fe_af_pos; + + /* 7. Let node and last node be the furthest block. + Follow these steps: */ + $node = $furthest_block; + $last_node = $furthest_block; + + while(true) { + for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { + /* 7.1 Let node be the element immediately + prior to node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 7.2 If node is not in the list of active + formatting elements, then remove node from + the stack of open elements and then go back + to step 1. */ + if(!in_array($node, $this->a_formatting, true)) { + unset($this->stack[$n]); + $this->stack = array_merge($this->stack); + + } else { + break; + } + } + + /* 7.3 Otherwise, if node is the formatting + element, then go to the next step in the overall + algorithm. */ + if($node === $formatting_element) { + break; + + /* 7.4 Otherwise, if last node is the furthest + block, then move the aforementioned bookmark to + be immediately after the node in the list of + active formatting elements. */ + } elseif($last_node === $furthest_block) { + $bookmark = array_search($node, $this->a_formatting, true) + 1; + } + + /* 7.5 If node has any children, perform a + shallow clone of node, replace the entry for + node in the list of active formatting elements + with an entry for the clone, replace the entry + for node in the stack of open elements with an + entry for the clone, and let node be the clone. */ + if($node->hasChildNodes()) { + $clone = $node->cloneNode(); + $s_pos = array_search($node, $this->stack, true); + $a_pos = array_search($node, $this->a_formatting, true); + + $this->stack[$s_pos] = $clone; + $this->a_formatting[$a_pos] = $clone; + $node = $clone; + } + + /* 7.6 Insert last node into node, first removing + it from its previous parent node if any. */ + if($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $node->appendChild($last_node); + + /* 7.7 Let last node be node. */ + $last_node = $node; + } + + /* 8. Insert whatever last node ended up being in + the previous step into the common ancestor node, + first removing it from its previous parent node if + any. */ + if($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $common_ancestor->appendChild($last_node); + + /* 9. Perform a shallow clone of the formatting + element. */ + $clone = $formatting_element->cloneNode(); + + /* 10. Take all of the child nodes of the furthest + block and append them to the clone created in the + last step. */ + while($furthest_block->hasChildNodes()) { + $child = $furthest_block->firstChild; + $furthest_block->removeChild($child); + $clone->appendChild($child); + } + + /* 11. Append that clone to the furthest block. */ + $furthest_block->appendChild($clone); + + /* 12. Remove the formatting element from the list + of active formatting elements, and insert the clone + into the list of active formatting elements at the + position of the aforementioned bookmark. */ + $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + + $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); + $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); + $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); + + /* 13. Remove the formatting element from the stack + of open elements, and insert the clone into the stack + of open elements immediately after (i.e. in a more + deeply nested position than) the position of the + furthest block in that stack. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $fb_s_pos = array_search($furthest_block, $this->stack, true); + unset($this->stack[$fe_s_pos]); + + $s_part1 = array_slice($this->stack, 0, $fb_s_pos); + $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); + $this->stack = array_merge($s_part1, array($clone), $s_part2); + + /* 14. Jump back to step 1 in this series of steps. */ + unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); + } + break; + + /* An end tag token whose tag name is one of: "button", + "marquee", "object" */ + case 'button': case 'marquee': case 'object': + /* If the stack of open elements has an element in scope whose + tag name matches the tag name of the token, then generate implied + tags. */ + if($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // k + + /* Now, if the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then pop + elements from the stack until that element has been popped from + the stack, and clear the list of active formatting elements up + to the last marker. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + + $marker = end(array_keys($this->a_formatting, self::MARKER, true)); + + for($n = count($this->a_formatting) - 1; $n > $marker; $n--) { + array_pop($this->a_formatting); + } + } + break; + + /* Or an end tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "hr", "iframe", "image", "img", + "input", "isindex", "noembed", "noframes", "param", "select", + "spacer", "table", "textarea", "wbr" */ + case 'area': case 'basefont': case 'bgsound': case 'br': + case 'embed': case 'hr': case 'iframe': case 'image': + case 'img': case 'input': case 'isindex': case 'noembed': + case 'noframes': case 'param': case 'select': case 'spacer': + case 'table': case 'textarea': case 'wbr': + // Parse error. Ignore the token. + break; + + /* An end tag token not covered by the previous entries */ + default: + for($n = count($this->stack) - 1; $n >= 0; $n--) { + /* Initialise node to be the current node (the bottommost + node of the stack). */ + $node = end($this->stack); + + /* If node has the same tag name as the end tag token, + then: */ + if($token['name'] === $node->nodeName) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* If the tag name of the end tag token does not + match the tag name of the current node, this is a + parse error. */ + // k + + /* Pop all the nodes from the current node up to + node, including node, then stop this algorithm. */ + for($x = count($this->stack) - $n; $x >= $n; $x--) { + array_pop($this->stack); + } + + } else { + $category = $this->getElementCategory($node); + + if($category !== self::SPECIAL && $category !== self::SCOPING) { + /* Otherwise, if node is in neither the formatting + category nor the phrasing category, then this is a + parse error. Stop this algorithm. The end tag token + is ignored. */ + return false; + } + } + } + break; + } + break; + } + } + + private function inTable($token) { + $clear = array('html', 'table'); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "caption" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'caption') { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + + /* Insert an HTML element for the token, then switch the + insertion mode to "in caption". */ + $this->insertElement($token); + $this->mode = self::IN_CAPTION; + + /* A start tag whose tag name is "colgroup" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'colgroup') { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the + insertion mode to "in column group". */ + $this->insertElement($token); + $this->mode = self::IN_CGROUP; + + /* A start tag whose tag name is "col" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'col') { + $this->inTable(array( + 'name' => 'colgroup', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + $this->inColumnGroup($token); + + /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('tbody', 'tfoot', 'thead'))) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in table body". */ + $this->insertElement($token); + $this->mode = self::IN_TBODY; + + /* A start tag whose tag name is one of: "td", "th", "tr" */ + } elseif($token['type'] === HTML5::STARTTAG && + in_array($token['name'], array('td', 'th', 'tr'))) { + /* Act as if a start tag token with the tag name "tbody" had been + seen, then reprocess the current token. */ + $this->inTable(array( + 'name' => 'tbody', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + return $this->inTableBody($token); + + /* A start tag whose tag name is "table" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'table') { + /* Parse error. Act as if an end tag token with the tag name "table" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inTable(array( + 'name' => 'table', + 'type' => HTML5::ENDTAG + )); + + return $this->mainPhase($token); + + /* An end tag whose tag name is "table" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if(!$this->elementInScope($token['name'], true)) { + return false; + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a table element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a table element has been + popped from the stack. */ + while(true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if($current === 'table') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td', + 'tfoot', 'th', 'thead', 'tr'))) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Parse error. Process the token as if the insertion mode was "in + body", with the following exception: */ + + /* If the current node is a table, tbody, tfoot, thead, or tr + element, then, whenever a node would be inserted into the current + node, it must instead be inserted into the foster parent element. */ + if(in_array(end($this->stack)->nodeName, + array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { + /* The foster parent element is the parent element of the last + table element in the stack of open elements, if there is a + table element and it has such a parent element. If there is no + table element in the stack of open elements (innerHTML case), + then the foster parent element is the first element in the + stack of open elements (the html element). Otherwise, if there + is a table element in the stack of open elements, but the last + table element in the stack of open elements has no parent, or + its parent node is not an element, then the foster parent + element is the element before the last table element in the + stack of open elements. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === 'table') { + $table = $this->stack[$n]; + break; + } + } + + if(isset($table) && $table->parentNode !== null) { + $this->foster_parent = $table->parentNode; + + } elseif(!isset($table)) { + $this->foster_parent = $this->stack[0]; + + } elseif(isset($table) && ($table->parentNode === null || + $table->parentNode->nodeType !== XML_ELEMENT_NODE)) { + $this->foster_parent = $this->stack[$n - 1]; + } + } + + $this->inBody($token); + } + } + + private function inCaption($token) { + /* An end tag whose tag name is "caption" */ + if($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a caption element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a caption element has + been popped from the stack. */ + while(true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if($node === 'caption') { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag + name is "table" */ + } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', + 'thead', 'tr'))) || ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table')) { + /* Parse error. Act as if an end tag with the tag name "caption" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inCaption(array( + 'name' => 'caption', + 'type' => HTML5::ENDTAG + )); + + return $this->inTable($token); + + /* An end tag whose tag name is one of: "body", "col", "colgroup", + "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th', + 'thead', 'tr'))) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inColumnGroup($token) { + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "col" */ + } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { + /* Insert a col element for the token. Immediately pop the current + node off the stack of open elements. */ + $this->insertElement($token); + array_pop($this->stack); + + /* An end tag whose tag name is "colgroup" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'colgroup') { + /* If the current node is the root html element, then this is a + parse error, ignore the token. (innerHTML case) */ + if(end($this->stack)->nodeName === 'html') { + // Ignore + + /* Otherwise, pop the current node (which will be a colgroup + element) from the stack of open elements. Switch the insertion + mode to "in table". */ + } else { + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* An end tag whose tag name is "col" */ + } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Act as if an end tag with the tag name "colgroup" had been seen, + and then, if that token wasn't ignored, reprocess the current token. */ + $this->inColumnGroup(array( + 'name' => 'colgroup', + 'type' => HTML5::ENDTAG + )); + + return $this->inTable($token); + } + } + + private function inTableBody($token) { + $clear = array('tbody', 'tfoot', 'thead', 'html'); + + /* A start tag whose tag name is "tr" */ + if($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Insert a tr element for the token, then switch the insertion + mode to "in row". */ + $this->insertElement($token); + $this->mode = self::IN_ROW; + + /* A start tag whose tag name is one of: "th", "td" */ + } elseif($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td')) { + /* Parse error. Act as if a start tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inTableBody(array( + 'name' => 'tr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + )); + + return $this->inRow($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node from the stack of open elements. Switch + the insertion mode to "in table". */ + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ + } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead'))) || + ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')) { + /* If the stack of open elements does not have a tbody, thead, or + tfoot element in table scope, this is a parse error. Ignore the + token. (innerHTML case) */ + if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Act as if an end tag with the same tag name as the current + node ("tbody", "tfoot", or "thead") had been seen, then + reprocess the current token. */ + $this->inTableBody(array( + 'name' => end($this->stack)->nodeName, + 'type' => HTML5::ENDTAG + )); + + return $this->mainPhase($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th", "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inRow($token) { + $clear = array('tr', 'html'); + + /* A start tag whose tag name is one of: "th", "td" */ + if($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td')) { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in cell". */ + $this->insertElement($token); + $this->mode = self::IN_CELL; + + /* Insert a marker at the end of the list of active formatting + elements. */ + $this->a_formatting[] = self::MARKER; + + /* An end tag whose tag name is "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node (which will be a tr element) from the + stack of open elements. Switch the insertion mode to "in table + body". */ + array_pop($this->stack); + $this->mode = self::IN_TBODY; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) { + /* Act as if an end tag with the tag name "tr" had been seen, then, + if that token wasn't ignored, reprocess the current token. */ + $this->inRow(array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + )); + + return $this->inCell($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Otherwise, act as if an end tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inRow(array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + )); + + return $this->inCell($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inCell($token) { + /* An end tag whose tag name is one of: "td", "th" */ + if($token['type'] === HTML5::ENDTAG && + ($token['name'] === 'td' || $token['name'] === 'th')) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token, then this is a + parse error and the token must be ignored. */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Generate implied end tags, except for elements with the same + tag name as the token. */ + $this->generateImpliedEndTags(array($token['name'])); + + /* Now, if the current node is not an element with the same tag + name as the token, then this is a parse error. */ + // k + + /* Pop elements from this stack until an element with the same + tag name as the token has been popped from the stack. */ + while(true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if($node === $token['name']) { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in row". (The current node + will be a tr element at this point.) */ + $this->mode = self::IN_ROW; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', + 'thead', 'tr'))) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if(!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', + 'thead', 'tr'))) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if(!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('body', 'caption', 'col', 'colgroup', 'html'))) { + /* Parse error. Ignore the token. */ + + /* An end tag whose tag name is one of: "table", "tbody", "tfoot", + "thead", "tr" */ + } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], + array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token (which can only + happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), + then this is a parse error and the token must be ignored. */ + if(!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inSelect($token) { + /* Handle the token as follows: */ + + /* A character token */ + if($token['type'] === HTML5::CHARACTR) { + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token whose tag name is "option" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'option') { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if(end($this->stack)->nodeName === 'option') { + $this->inSelect(array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* A start tag token whose tag name is "optgroup" */ + } elseif($token['type'] === HTML5::STARTTAG && + $token['name'] === 'optgroup') { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if(end($this->stack)->nodeName === 'option') { + $this->inSelect(array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + )); + } + + /* If the current node is an optgroup element, act as if an end tag + with the tag name "optgroup" had been seen. */ + if(end($this->stack)->nodeName === 'optgroup') { + $this->inSelect(array( + 'name' => 'optgroup', + 'type' => HTML5::ENDTAG + )); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* An end tag token whose tag name is "optgroup" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'optgroup') { + /* First, if the current node is an option element, and the node + immediately before it in the stack of open elements is an optgroup + element, then act as if an end tag with the tag name "option" had + been seen. */ + $elements_in_stack = count($this->stack); + + if($this->stack[$elements_in_stack - 1]->nodeName === 'option' && + $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup') { + $this->inSelect(array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + )); + } + + /* If the current node is an optgroup element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if($this->stack[$elements_in_stack - 1] === 'optgroup') { + array_pop($this->stack); + } + + /* An end tag token whose tag name is "option" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'option') { + /* If the current node is an option element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if(end($this->stack)->nodeName === 'option') { + array_pop($this->stack); + } + + /* An end tag whose tag name is "select" */ + } elseif($token['type'] === HTML5::ENDTAG && + $token['name'] === 'select') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if(!$this->elementInScope($token['name'], true)) { + // w/e + + /* Otherwise: */ + } else { + /* Pop elements from the stack of open elements until a select + element has been popped from the stack. */ + while(true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if($current === 'select') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* A start tag whose tag name is "select" */ + } elseif($token['name'] === 'select' && + $token['type'] === HTML5::STARTTAG) { + /* Parse error. Act as if the token had been an end tag with the + tag name "select" instead. */ + $this->inSelect(array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + )); + + /* An end tag whose tag name is one of: "caption", "table", "tbody", + "tfoot", "thead", "tr", "td", "th" */ + } elseif(in_array($token['name'], array('caption', 'table', 'tbody', + 'tfoot', 'thead', 'tr', 'td', 'th')) && $token['type'] === HTML5::ENDTAG) { + /* Parse error. */ + // w/e + + /* If the stack of open elements has an element in table scope with + the same tag name as that of the token, then act as if an end tag + with the tag name "select" had been seen, and reprocess the token. + Otherwise, ignore the token. */ + if($this->elementInScope($token['name'], true)) { + $this->inSelect(array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + )); + + $this->mainPhase($token); + } + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterBody($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Process the token as it would be processed if the insertion mode + was "in body". */ + $this->inBody($token); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the first element in the stack of open + elements (the html element), with the data attribute set to the + data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->stack[0]->appendChild($comment); + + /* An end tag with the tag name "html" */ + } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { + /* If the parser was originally created in order to handle the + setting of an element's innerHTML attribute, this is a parse error; + ignore the token. (The element will be an html element in this + case.) (innerHTML case) */ + + /* Otherwise, switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* Anything else */ + } else { + /* Parse error. Set the insertion mode to "in body" and reprocess + the token. */ + $this->mode = self::IN_BODY; + return $this->inBody($token); + } + } + + private function inFrameset($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag with the tag name "frameset" */ + } elseif($token['name'] === 'frameset' && + $token['type'] === HTML5::STARTTAG) { + $this->insertElement($token); + + /* An end tag with the tag name "frameset" */ + } elseif($token['name'] === 'frameset' && + $token['type'] === HTML5::ENDTAG) { + /* If the current node is the root html element, then this is a + parse error; ignore the token. (innerHTML case) */ + if(end($this->stack)->nodeName === 'html') { + // Ignore + + } else { + /* Otherwise, pop the current node from the stack of open + elements. */ + array_pop($this->stack); + + /* If the parser was not originally created in order to handle + the setting of an element's innerHTML attribute (innerHTML case), + and the current node is no longer a frameset element, then change + the insertion mode to "after frameset". */ + $this->mode = self::AFTR_FRAME; + } + + /* A start tag with the tag name "frame" */ + } elseif($token['name'] === 'frame' && + $token['type'] === HTML5::STARTTAG) { + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + /* A start tag with the tag name "noframes" */ + } elseif($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterFrameset($token) { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* An end tag with the tag name "html" */ + } elseif($token['name'] === 'html' && + $token['type'] === HTML5::ENDTAG) { + /* Switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* A start tag with the tag name "noframes" */ + } elseif($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function trailingEndPhase($token) { + /* After the main phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { + /* Process the token as it would be processed in the main phase. */ + $this->mainPhase($token); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or a start tag token. Or an end tag token. */ + } elseif(($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG) { + /* Parse error. Switch back to the main phase and reprocess the + token. */ + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + + /* An end-of-file token */ + } elseif($token['type'] === HTML5::EOF) { + /* OMG DONE!! */ + } + } + + private function insertElement($token, $append = true, $check = false) { + // Proprietary workaround for libxml2's limitations with tag names + if ($check) { + // Slightly modified HTML5 tag-name modification, + // removing anything that's not an ASCII letter, digit, or hyphen + $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); + // Remove leading hyphens and numbers + $token['name'] = ltrim($token['name'], '-0..9'); + // In theory, this should ever be needed, but just in case + if ($token['name'] === '') $token['name'] = 'span'; // arbitrary generic choice + } + + $el = $this->dom->createElement($token['name']); + + foreach($token['attr'] as $attr) { + if(!$el->hasAttribute($attr['name'])) { + $el->setAttribute($attr['name'], $attr['value']); + } + } + + $this->appendToRealParent($el); + $this->stack[] = $el; + + return $el; + } + + private function insertText($data) { + $text = $this->dom->createTextNode($data); + $this->appendToRealParent($text); + } + + private function insertComment($data) { + $comment = $this->dom->createComment($data); + $this->appendToRealParent($comment); + } + + private function appendToRealParent($node) { + if($this->foster_parent === null) { + end($this->stack)->appendChild($node); + + } elseif($this->foster_parent !== null) { + /* If the foster parent element is the parent element of the + last table element in the stack of open elements, then the new + node must be inserted immediately before the last table element + in the stack of open elements in the foster parent element; + otherwise, the new node must be appended to the foster parent + element. */ + for($n = count($this->stack) - 1; $n >= 0; $n--) { + if($this->stack[$n]->nodeName === 'table' && + $this->stack[$n]->parentNode !== null) { + $table = $this->stack[$n]; + break; + } + } + + if(isset($table) && $this->foster_parent->isSameNode($table->parentNode)) + $this->foster_parent->insertBefore($node, $table); + else + $this->foster_parent->appendChild($node); + + $this->foster_parent = null; + } + } + + private function elementInScope($el, $table = false) { + if(is_array($el)) { + foreach($el as $element) { + if($this->elementInScope($element, $table)) { + return true; + } + } + + return false; + } + + $leng = count($this->stack); + + for($n = 0; $n < $leng; $n++) { + /* 1. Initialise node to be the current node (the bottommost node of + the stack). */ + $node = $this->stack[$leng - 1 - $n]; + + if($node->tagName === $el) { + /* 2. If node is the target node, terminate in a match state. */ + return true; + + } elseif($node->tagName === 'table') { + /* 3. Otherwise, if node is a table element, terminate in a failure + state. */ + return false; + + } elseif($table === true && in_array($node->tagName, array('caption', 'td', + 'th', 'button', 'marquee', 'object'))) { + /* 4. Otherwise, if the algorithm is the "has an element in scope" + variant (rather than the "has an element in table scope" variant), + and node is one of the following, terminate in a failure state. */ + return false; + + } elseif($node === $node->ownerDocument->documentElement) { + /* 5. Otherwise, if node is an html element (root element), terminate + in a failure state. (This can only happen if the node is the topmost + node of the stack of open elements, and prevents the next step from + being invoked if there are no more elements in the stack.) */ + return false; + } + + /* Otherwise, set node to the previous entry in the stack of open + elements and return to step 2. (This will never fail, since the loop + will always terminate in the previous step if the top of the stack + is reached.) */ + } + } + + private function reconstructActiveFormattingElements() { + /* 1. If there are no entries in the list of active formatting elements, + then there is nothing to reconstruct; stop this algorithm. */ + $formatting_elements = count($this->a_formatting); + + if($formatting_elements === 0) { + return false; + } + + /* 3. Let entry be the last (most recently added) element in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. If the last (most recently added) entry in the list of active + formatting elements is a marker, or if it is an element that is in the + stack of open elements, then there is nothing to reconstruct; stop this + algorithm. */ + if($entry === self::MARKER || in_array($entry, $this->stack, true)) { + return false; + } + + for($a = $formatting_elements - 1; $a >= 0; true) { + /* 4. If there are no entries before entry in the list of active + formatting elements, then jump to step 8. */ + if($a === 0) { + $step_seven = false; + break; + } + + /* 5. Let entry be the entry one earlier than entry in the list of + active formatting elements. */ + $a--; + $entry = $this->a_formatting[$a]; + + /* 6. If entry is neither a marker nor an element that is also in + thetack of open elements, go to step 4. */ + if($entry === self::MARKER || in_array($entry, $this->stack, true)) { + break; + } + } + + while(true) { + /* 7. Let entry be the element one later than entry in the list of + active formatting elements. */ + if(isset($step_seven) && $step_seven === true) { + $a++; + $entry = $this->a_formatting[$a]; + } + + /* 8. Perform a shallow clone of the element entry to obtain clone. */ + $clone = $entry->cloneNode(); + + /* 9. Append clone to the current node and push it onto the stack + of open elements so that it is the new current node. */ + end($this->stack)->appendChild($clone); + $this->stack[] = $clone; + + /* 10. Replace the entry for entry in the list with an entry for + clone. */ + $this->a_formatting[$a] = $clone; + + /* 11. If the entry for clone in the list of active formatting + elements is not the last entry in the list, return to step 7. */ + if(end($this->a_formatting) !== $clone) { + $step_seven = true; + } else { + break; + } + } + } + + private function clearTheActiveFormattingElementsUpToTheLastMarker() { + /* When the steps below require the UA to clear the list of active + formatting elements up to the last marker, the UA must perform the + following steps: */ + + while(true) { + /* 1. Let entry be the last (most recently added) entry in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. Remove entry from the list of active formatting elements. */ + array_pop($this->a_formatting); + + /* 3. If entry was a marker, then stop the algorithm at this point. + The list has been cleared up to the last marker. */ + if($entry === self::MARKER) { + break; + } + } + } + + private function generateImpliedEndTags($exclude = array()) { + /* When the steps below require the UA to generate implied end tags, + then, if the current node is a dd element, a dt element, an li element, + a p element, a td element, a th element, or a tr element, the UA must + act as if an end tag with the respective tag name had been seen and + then generate implied end tags again. */ + $node = end($this->stack); + $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); + + while(in_array(end($this->stack)->nodeName, $elements)) { + array_pop($this->stack); + } + } + + private function getElementCategory($node) { + $name = $node->tagName; + if(in_array($name, $this->special)) + return self::SPECIAL; + + elseif(in_array($name, $this->scoping)) + return self::SCOPING; + + elseif(in_array($name, $this->formatting)) + return self::FORMATTING; + + else + return self::PHRASING; + } + + private function clearStackToTableContext($elements) { + /* When the steps above require the UA to clear the stack back to a + table context, it means that the UA must, while the current node is not + a table element or an html element, pop elements from the stack of open + elements. If this causes any elements to be popped from the stack, then + this is a parse error. */ + while(true) { + $node = end($this->stack)->nodeName; + + if(in_array($node, $elements)) { + break; + } else { + array_pop($this->stack); + } + } + } + + private function resetInsertionMode() { + /* 1. Let last be false. */ + $last = false; + $leng = count($this->stack); + + for($n = $leng - 1; $n >= 0; $n--) { + /* 2. Let node be the last node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 3. If node is the first node in the stack of open elements, then + set last to true. If the element whose innerHTML attribute is being + set is neither a td element nor a th element, then set node to the + element whose innerHTML attribute is being set. (innerHTML case) */ + if($this->stack[0]->isSameNode($node)) { + $last = true; + } + + /* 4. If node is a select element, then switch the insertion mode to + "in select" and abort these steps. (innerHTML case) */ + if($node->nodeName === 'select') { + $this->mode = self::IN_SELECT; + break; + + /* 5. If node is a td or th element, then switch the insertion mode + to "in cell" and abort these steps. */ + } elseif($node->nodeName === 'td' || $node->nodeName === 'th') { + $this->mode = self::IN_CELL; + break; + + /* 6. If node is a tr element, then switch the insertion mode to + "in row" and abort these steps. */ + } elseif($node->nodeName === 'tr') { + $this->mode = self::IN_ROW; + break; + + /* 7. If node is a tbody, thead, or tfoot element, then switch the + insertion mode to "in table body" and abort these steps. */ + } elseif(in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { + $this->mode = self::IN_TBODY; + break; + + /* 8. If node is a caption element, then switch the insertion mode + to "in caption" and abort these steps. */ + } elseif($node->nodeName === 'caption') { + $this->mode = self::IN_CAPTION; + break; + + /* 9. If node is a colgroup element, then switch the insertion mode + to "in column group" and abort these steps. (innerHTML case) */ + } elseif($node->nodeName === 'colgroup') { + $this->mode = self::IN_CGROUP; + break; + + /* 10. If node is a table element, then switch the insertion mode + to "in table" and abort these steps. */ + } elseif($node->nodeName === 'table') { + $this->mode = self::IN_TABLE; + break; + + /* 11. If node is a head element, then switch the insertion mode + to "in body" ("in body"! not "in head"!) and abort these steps. + (innerHTML case) */ + } elseif($node->nodeName === 'head') { + $this->mode = self::IN_BODY; + break; + + /* 12. If node is a body element, then switch the insertion mode to + "in body" and abort these steps. */ + } elseif($node->nodeName === 'body') { + $this->mode = self::IN_BODY; + break; + + /* 13. If node is a frameset element, then switch the insertion + mode to "in frameset" and abort these steps. (innerHTML case) */ + } elseif($node->nodeName === 'frameset') { + $this->mode = self::IN_FRAME; + break; + + /* 14. If node is an html element, then: if the head element + pointer is null, switch the insertion mode to "before head", + otherwise, switch the insertion mode to "after head". In either + case, abort these steps. (innerHTML case) */ + } elseif($node->nodeName === 'html') { + $this->mode = ($this->head_pointer === null) + ? self::BEFOR_HEAD + : self::AFTER_HEAD; + + break; + + /* 15. If last is true, then set the insertion mode to "in body" + and abort these steps. (innerHTML case) */ + } elseif($last) { + $this->mode = self::IN_BODY; + break; + } + } + } + + private function closeCell() { + /* If the stack of open elements has a td or th element in table scope, + then act as if an end tag token with that tag name had been seen. */ + foreach(array('td', 'th') as $cell) { + if($this->elementInScope($cell, true)) { + $this->inCell(array( + 'name' => $cell, + 'type' => HTML5::ENDTAG + )); + + break; + } + } + } + + public function save() { + return $this->dom; + } +} +?> diff --git a/lib/htmlpurifier/HTMLPurifier/PercentEncoder.php b/lib/htmlpurifier/HTMLPurifier/PercentEncoder.php index c3b6c8e6794..a43c44f4c58 100644 --- a/lib/htmlpurifier/HTMLPurifier/PercentEncoder.php +++ b/lib/htmlpurifier/HTMLPurifier/PercentEncoder.php @@ -10,16 +10,16 @@ */ class HTMLPurifier_PercentEncoder { - + /** * Reserved characters to preserve when using encode(). */ - var $preserve = array(); - + protected $preserve = array(); + /** * String of characters that should be preserved while using encode(). */ - function HTMLPurifier_PercentEncoder($preserve = false) { + public function __construct($preserve = false) { // unreserved letters, ought to const-ify for ($i = 48; $i <= 57; $i++) $this->preserve[$i] = true; // digits for ($i = 65; $i <= 90; $i++) $this->preserve[$i] = true; // upper-case @@ -28,7 +28,7 @@ class HTMLPurifier_PercentEncoder $this->preserve[46] = true; // Period . $this->preserve[95] = true; // Underscore _ $this->preserve[126]= true; // Tilde ~ - + // extra letters not to escape if ($preserve !== false) { for ($i = 0, $c = strlen($preserve); $i < $c; $i++) { @@ -36,7 +36,7 @@ class HTMLPurifier_PercentEncoder } } } - + /** * Our replacement for urlencode, it encodes all non-reserved characters, * as well as any extra characters that were instructed to be preserved. @@ -47,7 +47,7 @@ class HTMLPurifier_PercentEncoder * @param $string String to be encoded * @return Encoded string. */ - function encode($string) { + public function encode($string) { $ret = ''; for ($i = 0, $c = strlen($string); $i < $c; $i++) { if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])]) ) { @@ -58,7 +58,7 @@ class HTMLPurifier_PercentEncoder } return $ret; } - + /** * Fix up percent-encoding by decoding unreserved characters and normalizing. * @warning This function is affected by $preserve, even though the @@ -66,7 +66,7 @@ class HTMLPurifier_PercentEncoder * characters. Be careful when reusing instances of PercentEncoder! * @param $string String to normalize */ - function normalize($string) { + public function normalize($string) { if ($string == '') return ''; $parts = explode('%', $string); $ret = array_shift($parts); @@ -92,6 +92,7 @@ class HTMLPurifier_PercentEncoder } return $ret; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Printer.php b/lib/htmlpurifier/HTMLPurifier/Printer.php index 7e20daafe35..e7eb82e83e4 100644 --- a/lib/htmlpurifier/HTMLPurifier/Printer.php +++ b/lib/htmlpurifier/HTMLPurifier/Printer.php @@ -1,68 +1,63 @@ generator = new HTMLPurifier_Generator(); + public function __construct() { } - + /** * Give generator necessary configuration if possible */ - function prepareGenerator($config) { - // hack for smoketests/configForm.php - if (empty($config->conf['HTML'])) return; + public function prepareGenerator($config) { + $all = $config->getAll(); $context = new HTMLPurifier_Context(); - $this->generator->generateFromTokens(array(), $config, $context); + $this->generator = new HTMLPurifier_Generator($config, $context); } - + /** * Main function that renders object or aspect of that object * @note Parameters vary depending on printer */ // function render() {} - + /** * Returns a start tag * @param $tag Tag name * @param $attr Attribute array */ - function start($tag, $attr = array()) { + protected function start($tag, $attr = array()) { return $this->generator->generateFromToken( new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) ); } - + /** * Returns an end teg * @param $tag Tag name */ - function end($tag) { + protected function end($tag) { return $this->generator->generateFromToken( new HTMLPurifier_Token_End($tag) ); } - + /** * Prints a complete element with content inside * @param $tag Tag name @@ -70,30 +65,30 @@ class HTMLPurifier_Printer * @param $attr Tag attributes * @param $escape Bool whether or not to escape contents */ - function element($tag, $contents, $attr = array(), $escape = true) { + protected function element($tag, $contents, $attr = array(), $escape = true) { return $this->start($tag, $attr) . ($escape ? $this->escape($contents) : $contents) . $this->end($tag); } - - function elementEmpty($tag, $attr = array()) { + + protected function elementEmpty($tag, $attr = array()) { return $this->generator->generateFromToken( new HTMLPurifier_Token_Empty($tag, $attr) ); } - - function text($text) { + + protected function text($text) { return $this->generator->generateFromToken( new HTMLPurifier_Token_Text($text) ); } - + /** * Prints a simple key/value row in a table. * @param $name Key * @param $value Value */ - function row($name, $value) { + protected function row($name, $value) { if (is_bool($value)) $value = $value ? 'On' : 'Off'; return $this->start('tr') . "\n" . @@ -102,23 +97,23 @@ class HTMLPurifier_Printer $this->end('tr') ; } - + /** * Escapes a string for HTML output. * @param $string String to escape */ - function escape($string) { + protected function escape($string) { $string = HTMLPurifier_Encoder::cleanUTF8($string); $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); return $string; } - + /** * Takes a list of strings and turns them into a single list * @param $array List of strings * @param $polite Bool whether or not to add an end before the last */ - function listify($array, $polite = false) { + protected function listify($array, $polite = false) { if (empty($array)) return 'None'; $ret = ''; $i = count($array); @@ -130,13 +125,13 @@ class HTMLPurifier_Printer } return $ret; } - + /** * Retrieves the class of an object without prefixes, as well as metadata * @param $obj Object to determine class of * @param $prefix Further prefix to remove */ - function getClass($obj, $sec_prefix = '') { + protected function getClass($obj, $sec_prefix = '') { static $five = null; if ($five === null) $five = version_compare(PHP_VERSION, '5', '>='); $prefix = 'HTMLPurifier_' . $sec_prefix; @@ -152,21 +147,30 @@ class HTMLPurifier_Printer } $class .= implode(', ', $values); break; - case 'composite': + case 'css_composite': $values = array(); foreach ($obj->defs as $def) { $values[] = $this->getClass($def, $sec_prefix); } $class .= implode(', ', $values); break; - case 'multiple': + case 'css_multiple': $class .= $this->getClass($obj->single, $sec_prefix) . ', '; $class .= $obj->max; break; + case 'css_denyelementdecorator': + $class .= $this->getClass($obj->def, $sec_prefix) . ', '; + $class .= $obj->element; + break; + case 'css_importantdecorator': + $class .= $this->getClass($obj->def, $sec_prefix); + if ($obj->allow) $class .= ', !important'; + break; } $class .= ')'; return $class; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php b/lib/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php index 7d3ad61e98e..81f9865901d 100644 --- a/lib/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php +++ b/lib/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php @@ -1,39 +1,38 @@ def = $config->getCSSDefinition(); $ret = ''; - + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); $ret .= $this->start('table'); - + $ret .= $this->element('caption', 'Properties ($info)'); - + $ret .= $this->start('thead'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Property', array('class' => 'heavy')); $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); $ret .= $this->end('tr'); $ret .= $this->end('thead'); - + ksort($this->def->info); foreach ($this->def->info as $property => $obj) { $name = $this->getClass($obj, 'AttrDef_'); $ret .= $this->row($property, $name); } - + $ret .= $this->end('table'); $ret .= $this->end('div'); - + return $ret; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css index 0653bbb0c0c..3ff1a88aa42 100644 --- a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css +++ b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css @@ -6,3 +6,5 @@ .hp-config .namespace th {text-align:center;} .hp-config .verbose {display:none;} .hp-config .controls {text-align:center;} + +/* vim: et sw=4 sts=4 */ diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js index 119ca4a04d9..cba00c9b80b 100644 --- a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js +++ b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js @@ -1,3 +1,5 @@ function toggleWriteability(id_of_patient, checked) { document.getElementById(id_of_patient).disabled = checked; -} \ No newline at end of file +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php index 21b8314bc52..02aa6568940 100644 --- a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php +++ b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php @@ -1,99 +1,105 @@ docURL = $doc_url; $this->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(); } - + /** * Sets default column and row size for textareas in sub-printers * @param $cols Integer columns of textarea, null to use default * @param $rows Integer rows of textarea, null to use default */ - function setTextareaDimensions($cols = null, $rows = null) { + public function setTextareaDimensions($cols = null, $rows = null) { if ($cols) $this->fields['default']->cols = $cols; if ($rows) $this->fields['default']->rows = $rows; } - + /** * Retrieves styling, in case it is not accessible by webserver */ - function getCSS() { + public static function getCSS() { return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); } - + /** * Retrieves JavaScript, in case it is not accessible by webserver */ - function getJavaScript() { + public static function getJavaScript() { return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); } - + /** * 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. */ - function render($config, $allowed = true, $render_controls = true) { + public function render($config, $allowed = true, $render_controls = true) { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->config = $config; - $this->prepareGenerator($config); - - $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed); + $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; - $all[$ns][$directive] = $config->get($ns, $directive); + $all[$ns][$directive] = $config->get($ns .'.'. $directive); } - + $ret = ''; $ret .= $this->start('table', array('class' => 'hp-config')); $ret .= $this->start('thead'); $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Directive'); - $ret .= $this->element('th', 'Value'); + $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); + $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); $ret .= $this->end('tr'); $ret .= $this->end('thead'); foreach ($all as $ns => $directives) { @@ -112,14 +118,13 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer $ret .= $this->end('table'); return $ret; } - + /** * Renders a single namespace * @param $ns String namespace name * @param $directive Associative array of directives to values - * @protected */ - function renderNamespace($ns, $directives) { + protected function renderNamespace($ns, $directives) { $ret = ''; $ret .= $this->start('tbody', array('class' => 'namespace')); $ret .= $this->start('tr'); @@ -135,7 +140,7 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer $ret .= $this->start('a', array('href' => $url)); } $attr = array('for' => "{$this->name}:$ns.$directive"); - + // crop directive name if it's too long if (!$this->compress || (strlen($directive) < $this->compress)) { $directive_disp = $directive; @@ -143,7 +148,7 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; $attr['title'] = $directive; } - + $ret .= $this->element( 'label', $directive_disp, @@ -152,23 +157,29 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer ); if ($this->docURL) $ret .= $this->end('a'); $ret .= $this->end('th'); - + $ret .= $this->start('td'); - $def = $this->config->def->info[$ns][$directive]; - $type = $def->type; - if (!isset($this->fields[$type])) $type = 'default'; + $def = $this->config->def->info["$ns.$directive"]; + 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'); } $ret .= $this->end('tbody'); return $ret; } - + } /** @@ -178,16 +189,23 @@ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer /** * Printer being decorated */ - var $obj; + protected $obj; /** * @param $obj Printer to decorate */ - function HTMLPurifier_Printer_ConfigForm_NullDecorator($obj) { - parent::HTMLPurifier_Printer(); + public function __construct($obj) { + parent::__construct(); $this->obj = $obj; } - function render($ns, $directive, $value, $name, $config) { - $this->prepareGenerator($config); + public function render($ns, $directive, $value, $name, $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')); @@ -201,11 +219,15 @@ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer 'id' => "$name:Null_$ns.$directive", 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! ); + if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { + // modify inline javascript slightly + $attr['onclick'] = "toggleWriteability('$name:Yes_$ns.$directive',checked);toggleWriteability('$name:No_$ns.$directive',checked)"; + } if ($value === null) $attr['checked'] = 'checked'; $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,25 +236,36 @@ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer * Swiss-army knife configuration form field printer */ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { - var $cols = 18; - var $rows = 5; - function render($ns, $directive, $value, $name, $config) { - $this->prepareGenerator($config); + public $cols = 18; + public $rows = 5; + public function render($ns, $directive, $value, $name, $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]; + $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; @@ -243,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); } @@ -252,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(); @@ -261,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; @@ -282,42 +318,51 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { * Bool form field printer */ class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer { - function render($ns, $directive, $value, $name, $config) { - $this->prepareGenerator($config); + public function render($ns, $directive, $value, $name, $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")); - + $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); $ret .= $this->text(' Yes'); $ret .= $this->end('label'); - + $attr = array( 'type' => 'radio', 'name' => "$name"."[$ns.$directive]", 'id' => "$name:Yes_$ns.$directive", 'value' => '1' ); - if ($value) $attr['checked'] = 'checked'; + if ($value === true) $attr['checked'] = 'checked'; + if ($value === null) $attr['disabled'] = 'disabled'; $ret .= $this->elementEmpty('input', $attr); - + $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); $ret .= $this->text(' No'); $ret .= $this->end('label'); - + $attr = array( 'type' => 'radio', 'name' => "$name"."[$ns.$directive]", 'id' => "$name:No_$ns.$directive", 'value' => '0' ); - if (!$value) $attr['checked'] = 'checked'; + if ($value === false) $attr['checked'] = 'checked'; + if ($value === null) $attr['disabled'] = 'disabled'; $ret .= $this->elementEmpty('input', $attr); - + $ret .= $this->end('div'); - + return $ret; } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php b/lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php index 4a0a90f1525..8a8f126b81f 100644 --- a/lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php +++ b/lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php @@ -1,37 +1,35 @@ config =& $config; - + $this->def = $config->getHTMLDefinition(); - + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); - + $ret .= $this->renderDoctype(); $ret .= $this->renderEnvironment(); $ret .= $this->renderContentSets(); $ret .= $this->renderInfo(); - + $ret .= $this->end('div'); - + return $ret; } - + /** * Renders the Doctype table */ - function renderDoctype() { + protected function renderDoctype() { $doctype = $this->def->doctype; $ret = ''; $ret .= $this->start('table'); @@ -43,28 +41,28 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer $ret .= $this->end('table'); return $ret; } - - + + /** * Renders environment table, which is miscellaneous info */ - function renderEnvironment() { + protected function renderEnvironment() { $def = $this->def; - + $ret = ''; - + $ret .= $this->start('table'); $ret .= $this->element('caption', 'Environment'); - + $ret .= $this->row('Parent of fragment', $def->info_parent); $ret .= $this->renderChildren($def->info_parent_def->child); $ret .= $this->row('Block wrap name', $def->info_block_wrapper); - + $ret .= $this->start('tr'); $ret .= $this->element('th', 'Global attributes'); $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr),0,0); $ret .= $this->end('tr'); - + $ret .= $this->start('tr'); $ret .= $this->element('th', 'Tag transforms'); $list = array(); @@ -74,25 +72,25 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer } $ret .= $this->element('td', $this->listify($list)); $ret .= $this->end('tr'); - + $ret .= $this->start('tr'); $ret .= $this->element('th', 'Pre-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); $ret .= $this->end('tr'); - + $ret .= $this->start('tr'); $ret .= $this->element('th', 'Post-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); $ret .= $this->end('tr'); - + $ret .= $this->end('table'); return $ret; } - + /** * Renders the Content Sets table */ - function renderContentSets() { + protected function renderContentSets() { $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Content Sets'); @@ -105,11 +103,11 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer $ret .= $this->end('table'); return $ret; } - + /** * Renders the Elements ($info) table */ - function renderInfo() { + protected function renderInfo() { $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Elements ($info)'); @@ -120,7 +118,7 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer $ret .= $this->end('tr'); foreach ($this->def->info as $name => $def) { $ret .= $this->start('tr'); - $ret .= $this->element('th', "<$name>" . ($def->safe ? '' : ' (unsafe)'), array('class'=>'heavy' . ($def->safe ? '' : ' unsafe'), 'colspan' => 2)); + $ret .= $this->element('th', "<$name>", array('class'=>'heavy', 'colspan' => 2)); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Inline content'); @@ -154,22 +152,22 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer $ret .= $this->element('th', 'Allowed attributes'); $ret .= $this->element('td',$this->listifyAttr($def->attr), array(), 0); $ret .= $this->end('tr'); - + if (!empty($def->required_attr)) { $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); } - + $ret .= $this->renderChildren($def->child); } $ret .= $this->end('table'); return $ret; } - - /** + + /** * Renders a row describing the allowed children of an element * @param $def HTMLPurifier_ChildDef of pertinent element */ - function renderChildren($def) { + protected function renderChildren($def) { $context = new HTMLPurifier_Context(); $ret = ''; $ret .= $this->start('tr'); @@ -190,9 +188,9 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer 'tfoot', 'tbody', 'tr')); } $ret .= $this->element('th', 'Allowed children', $attr); - + if ($def->type == 'chameleon') { - + $ret .= $this->element('td', 'Block: ' . $this->escape($this->listifyTagLookup($def->block->elements)),0,0); @@ -201,12 +199,12 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer $ret .= $this->element('td', 'Inline: ' . $this->escape($this->listifyTagLookup($def->inline->elements)),0,0); - + } elseif ($def->type == 'custom') { - + $ret .= $this->element('td', ''.ucfirst($def->type).': ' . $def->dtd_regex); - + } else { $ret .= $this->element('td', ''.ucfirst($def->type).': ' . @@ -215,12 +213,12 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer $ret .= $this->end('tr'); return $ret; } - - /** + + /** * Listifies a tag lookup table. * @param $array Tag lookup array in form of array('tagname' => true) */ - function listifyTagLookup($array) { + protected function listifyTagLookup($array) { ksort($array); $list = array(); foreach ($array as $name => $discard) { @@ -229,13 +227,13 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer } return $this->listify($list); } - + /** * Listifies a list of objects by retrieving class names and internal state * @param $array List of objects * @todo Also add information about internal state */ - function listifyObjectList($array) { + protected function listifyObjectList($array) { ksort($array); $list = array(); foreach ($array as $discard => $obj) { @@ -243,12 +241,12 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer } return $this->listify($list); } - + /** * Listifies a hash of attributes to AttrDef classes * @param $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) */ - function listifyAttr($array) { + protected function listifyAttr($array) { ksort($array); $list = array(); foreach ($array as $name => $obj) { @@ -257,17 +255,18 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer } return $this->listify($list); } - + /** * Creates a heavy header row */ - function heavyHeader($text, $num = 1) { + protected function heavyHeader($text, $num = 1) { $ret = ''; $ret .= $this->start('tr'); $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); $ret .= $this->end('tr'); return $ret; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/PropertyList.php b/lib/htmlpurifier/HTMLPurifier/PropertyList.php new file mode 100755 index 00000000000..2b99fb7bc3c --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/PropertyList.php @@ -0,0 +1,86 @@ +parent = $parent; + } + + /** + * Recursively retrieves the value for a key + */ + public function get($name) { + if ($this->has($name)) return $this->data[$name]; + // possible performance bottleneck, convert to iterative if necessary + if ($this->parent) return $this->parent->get($name); + throw new HTMLPurifier_Exception("Key '$name' not found"); + } + + /** + * Sets the value of a key, for this plist + */ + public function set($name, $value) { + $this->data[$name] = $value; + } + + /** + * Returns true if a given key exists + */ + public function has($name) { + return array_key_exists($name, $this->data); + } + + /** + * Resets a value to the value of it's parent, usually the default. If + * no value is specified, the entire plist is reset. + */ + public function reset($name = null) { + if ($name == null) $this->data = array(); + else unset($this->data[$name]); + } + + /** + * Squashes this property list and all of its property lists into a single + * array, and returns the array. This value is cached by default. + * @param $force If true, ignores the cache and regenerates the array. + */ + public function squash($force = false) { + if ($this->cache !== null && !$force) return $this->cache; + if ($this->parent) { + return $this->cache = array_merge($this->parent->squash($force), $this->data); + } else { + return $this->cache = $this->data; + } + } + + /** + * Returns the parent plist. + */ + public function getParent() { + return $this->parent; + } + + /** + * Sets the parent plist. + */ + public function setParent($plist) { + $this->parent = $plist; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/PropertyListIterator.php b/lib/htmlpurifier/HTMLPurifier/PropertyListIterator.php new file mode 100755 index 00000000000..8f250443e41 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/PropertyListIterator.php @@ -0,0 +1,32 @@ +l = strlen($filter); + $this->filter = $filter; + } + + public function accept() { + $key = $this->getInnerIterator()->key(); + if( strncmp($key, $this->filter, $this->l) !== 0 ) { + return false; + } + return true; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy.php b/lib/htmlpurifier/HTMLPurifier/Strategy.php index a6ab7e8bcab..2462865210c 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy.php @@ -2,31 +2,25 @@ /** * Supertype for classes that define a strategy for modifying/purifying tokens. - * - * While HTMLPurifier's core purpose is fixing HTML into something proper, + * + * While HTMLPurifier's core purpose is fixing HTML into something proper, * strategies provide plug points for extra configuration or even extra * features, such as custom tags, custom parsing of text, etc. */ -HTMLPurifier_ConfigSchema::define( - 'Core', 'EscapeInvalidTags', false, 'bool', - 'When true, invalid tags will be written back to the document as plain '. - 'text. Otherwise, they are silently dropped.' -); - -class HTMLPurifier_Strategy + +abstract class HTMLPurifier_Strategy { - + /** * Executes the strategy on the tokens. - * + * * @param $tokens Array of HTMLPurifier_Token objects to be operated on. * @param $config Configuration options * @returns Processed array of token objects. */ - function execute($tokens, $config, &$context) { - trigger_error('Cannot call abstract function', E_USER_ERROR); - } - + abstract public function execute($tokens, $config, $context); + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/Composite.php b/lib/htmlpurifier/HTMLPurifier/Strategy/Composite.php index fcd230f4723..816490b7996 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy/Composite.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy/Composite.php @@ -1,29 +1,25 @@ strategies as $strategy) { $tokens = $strategy->execute($tokens, $config, $context); } return $tokens; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/Core.php b/lib/htmlpurifier/HTMLPurifier/Strategy/Core.php index 93d051046a0..d90e1586064 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy/Core.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy/Core.php @@ -1,24 +1,18 @@ strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php b/lib/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php index 25e9f8acbca..f81802391bf 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php @@ -1,102 +1,99 @@ getHTMLDefinition(); - + // insert implicit "parent" node, will be removed at end. // DEFINITION CALL $parent_name = $definition->info_parent; array_unshift($tokens, new HTMLPurifier_Token_Start($parent_name)); $tokens[] = new HTMLPurifier_Token_End($parent_name); - + // setup the context variable 'IsInline', for chameleon processing // is 'false' when we are not inline, 'true' when it must always // be inline, and an integer when it is inline for a certain // branch of the document tree $is_inline = $definition->info_parent_def->descendants_are_inline; $context->register('IsInline', $is_inline); - + // setup error collector $e =& $context->get('ErrorCollector', true); - + //####################################################################// // Loop initialization - + // stack that contains the indexes of all parents, // $stack[count($stack)-1] being the current parent $stack = array(); - + // stack that contains all elements that are excluded - // it is organized by parent elements, similar to $stack, + // it is organized by parent elements, similar to $stack, // but it is only populated when an element with exclusions is // processed, i.e. there won't be empty exclusions. $exclude_stack = array(); - + // variable that contains the start token while we are processing // nodes. This enables error reporting to do its job $start_token = false; $context->register('CurrentToken', $start_token); - + //####################################################################// // Loop - + // iterate through all start nodes. Determining the start node // is complicated so it has been omitted from the loop construct for ($i = 0, $size = count($tokens) ; $i < $size; ) { - + //################################################################// // Gather information on children - + // child token accumulator $child_tokens = array(); - + // scroll to the end of this node, report number, and collect // all children for ($j = $i, $depth = 0; ; $j++) { - if ($tokens[$j]->type == 'start') { + if ($tokens[$j] instanceof HTMLPurifier_Token_Start) { $depth++; // skip token assignment on first iteration, this is the // token we currently are on if ($depth == 1) continue; - } elseif ($tokens[$j]->type == 'end') { + } elseif ($tokens[$j] instanceof HTMLPurifier_Token_End) { $depth--; // skip token assignment on last iteration, this is the // end token of the token we're currently on @@ -104,15 +101,15 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy } $child_tokens[] = $tokens[$j]; } - + // $i is index of start token // $j is index of end token - + $start_token = $tokens[$i]; // to make token available via CurrentToken - + //################################################################// // Gather information on parent - + // calculate parent information if ($count = count($stack)) { $parent_index = $stack[$count-1]; @@ -125,11 +122,11 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy } else { // processing as if the parent were the "root" node // unknown info, it won't be used anyway, in the future, - // we may want to enforce one element only (this is + // we may want to enforce one element only (this is // necessary for HTML Purifier to clean entire documents $parent_index = $parent_name = $parent_def = null; } - + // calculate context if ($is_inline === false) { // check if conditions make it inline @@ -142,10 +139,10 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy $is_inline = false; } } - + //################################################################// // Determine whether element is explicitly excluded SGML-style - + // determine whether or not element is excluded by checking all // parent exclusions. The array should not be very large, two // elements at most. @@ -159,10 +156,10 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy } } } - + //################################################################// // Perform child validation - + if ($excluded) { // there is an exclusion, remove the entire node $result = false; @@ -174,9 +171,9 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy $def = $definition->info_parent_def; } else { $def = $definition->info[$tokens[$i]->name]; - + } - + if (!empty($def->child)) { // have DTD child def validate children $result = $def->child->validateChildren( @@ -185,31 +182,31 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy // weird, no child definition, get rid of everything $result = false; } - + // determine whether or not this element has any exclusions $excludes = $def->excludes; } - + // $result is now a bool or array - + //################################################################// // Process result by interpreting $result - + if ($result === true || $child_tokens === $result) { // leave the node as is - + // register start token as a parental node start $stack[] = $i; - + // register exclusions if there are any if (!empty($excludes)) $exclude_stack[] = $excludes; - + // move cursor to next possible start node $i++; - + } elseif($result === false) { // remove entire node - + if ($e) { if ($excluded) { $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); @@ -217,20 +214,20 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); } } - + // calculate length of inner tokens and current tokens $length = $j - $i + 1; - + // perform removal array_splice($tokens, $i, $length); - + // update size $size -= $length; - + // there is no start token to register, // current node is now the next possible start node // unless it turns out that we need to do a double-check - + // this is a rought heuristic that covers 100% of HTML's // cases and 99% of all other cases. A child definition // that would be tricked by this would be something like: @@ -242,16 +239,16 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy $i = $parent_index; array_pop($stack); } - + // PROJECTED OPTIMIZATION: Process all children elements before // reprocessing parent node. - + } else { // replace node with $result - + // calculate length of inner tokens $length = $j - $i - 1; - + if ($e) { if (empty($result) && $length) { $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); @@ -259,36 +256,36 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); } } - + // perform replacement array_splice($tokens, $i + 1, $length, $result); - + // update size $size -= $length; $size += count($result); - + // register start token as a parental node start $stack[] = $i; - + // register exclusions if there are any if (!empty($excludes)) $exclude_stack[] = $excludes; - + // move cursor to next possible start node $i++; - + } - + //################################################################// // Scroll to next start node - + // We assume, at this point, that $i is the index of the token // that is the first possible new start point for a node. - + // Test if the token indeed is a start tag, if not, move forward // and test again. $size = count($tokens); - while ($i < $size and $tokens[$i]->type != 'start') { - if ($tokens[$i]->type == 'end') { + while ($i < $size and !$tokens[$i] instanceof HTMLPurifier_Token_Start) { + if ($tokens[$i] instanceof HTMLPurifier_Token_End) { // pop a token index off the stack if we ended a node array_pop($stack); // pop an exclusion lookup off exclusion stack if @@ -305,27 +302,27 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy } $i++; } - + } - + //####################################################################// // Post-processing - + // remove implicit parent tokens at the beginning and end array_shift($tokens); array_pop($tokens); - + // remove context variables $context->destroy('IsInline'); $context->destroy('CurrentToken'); - + //####################################################################// // Return - + return $tokens; - + } - + } - +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php b/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php index 30208ba1479..316b2386ac8 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php @@ -1,316 +1,471 @@ - This directive can be used to add custom auto-format injectors. - Specify an array of injector names (class name minus the prefix) - or concrete implementations. Injector class must exist. This directive - has been available since 2.0.1. -

-' -); - /** * Takes tokens makes them well-formed (balance end tags, etc.) */ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy { - + /** - * Locally shared variable references - * @private + * Array stream of tokens being processed. */ - var $inputTokens, $inputIndex, $outputTokens, $currentNesting, - $currentInjector, $injectors; - - function execute($tokens, $config, &$context) { - + protected $tokens; + + /** + * Current index in $tokens. + */ + protected $t; + + /** + * Current nesting of elements. + */ + protected $stack; + + /** + * Injectors active in this stream processing. + */ + protected $injectors; + + /** + * Current instance of HTMLPurifier_Config. + */ + protected $config; + + /** + * Current instance of HTMLPurifier_Context. + */ + protected $context; + + public function execute($tokens, $config, $context) { + $definition = $config->getHTMLDefinition(); - + // local variables - $result = array(); - $generator = new HTMLPurifier_Generator(); - $escape_invalid_tags = $config->get('Core', 'EscapeInvalidTags'); - $e =& $context->get('ErrorCollector', true); - + $generator = new HTMLPurifier_Generator($config, $context); + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + $e = $context->get('ErrorCollector', true); + $t = false; // token index + $i = false; // injector index + $token = false; // the current token + $reprocess = false; // whether or not to reprocess the same token + $stack = array(); + // member variables - $this->currentNesting = array(); - $this->inputIndex = false; - $this->inputTokens =& $tokens; - $this->outputTokens =& $result; - + $this->stack =& $stack; + $this->t =& $t; + $this->tokens =& $tokens; + $this->config = $config; + $this->context = $context; + // context variables - $context->register('CurrentNesting', $this->currentNesting); - $context->register('InputIndex', $this->inputIndex); - $context->register('InputTokens', $tokens); - + $context->register('CurrentNesting', $stack); + $context->register('InputIndex', $t); + $context->register('InputTokens', $tokens); + $context->register('CurrentToken', $token); + // -- begin INJECTOR -- - + $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) { + // XXX: Fix with a legitimate lookup table of enabled filters + if (strpos($injector, '.') !== false) continue; $injector = "HTMLPurifier_Injector_$injector"; 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 (!$injector) continue; if (is_string($injector)) { $injector = "HTMLPurifier_Injector_$injector"; $injector = new $injector; } $this->injectors[] = $injector; } - - // array index of the injector that resulted in an array - // substitution. This enables processTokens() to know which - // injectors are affected by the added tokens and which are - // not (namely, the ones after the current injector are not - // affected) - $this->currentInjector = false; - + // give the injectors references to the definition and context // variables for performance reasons - foreach ($this->injectors as $i => $x) { - $error = $this->injectors[$i]->prepare($config, $context); + foreach ($this->injectors as $ix => $injector) { + $error = $injector->prepare($config, $context); if (!$error) continue; - list($injector) = array_splice($this->injectors, $i, 1); - $name = $injector->name; - trigger_error("Cannot enable $name injector because $error is not allowed", E_USER_WARNING); + array_splice($this->injectors, $ix, 1); // rm the injector + trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); } - - // warning: most foreach loops follow the convention $i => $x. - // be sure, for PHP4 compatibility, to only perform write operations - // directly referencing the object using $i: $x is only safe for reads - + // -- end INJECTOR -- - - $token = false; - $context->register('CurrentToken', $token); - - for ($this->inputIndex = 0; isset($tokens[$this->inputIndex]); $this->inputIndex++) { - - // if all goes well, this token will be passed through unharmed - $token = $tokens[$this->inputIndex]; - - //printTokens($tokens, $this->inputIndex); - - foreach ($this->injectors as $i => $x) { - if ($x->skip > 0) $this->injectors[$i]->skip--; - } - - // quick-check: if it's not a tag, no need to process - if (empty( $token->is_tag )) { - if ($token->type === 'text') { - // injector handler code; duplicated for performance reasons - foreach ($this->injectors as $i => $x) { - if (!$x->skip) $this->injectors[$i]->handleText($token); - if (is_array($token)) { - $this->currentInjector = $i; - break; - } - } + + // a note on punting: + // In order to reduce code duplication, whenever some code needs + // to make HTML changes in order to make things "correct", the + // new HTML gets sent through the purifier, regardless of its + // status. This means that if we add a start token, because it + // was totally necessary, we don't have to update nesting; we just + // punt ($reprocess = true; continue;) and it does that for us. + + // isset is in loop because $tokens size changes during loop exec + for ( + $t = 0; + $t == 0 || isset($tokens[$t - 1]); + // only increment if we don't need to reprocess + $reprocess ? $reprocess = false : $t++ + ) { + + // check for a rewind + if (is_int($i) && $i >= 0) { + // possibility: disable rewinding if the current token has a + // rewind set on it already. This would offer protection from + // infinite loop, but might hinder some advanced rewinding. + $rewind_to = $this->injectors[$i]->getRewind(); + if (is_int($rewind_to) && $rewind_to < $t) { + if ($rewind_to < 0) $rewind_to = 0; + while ($t > $rewind_to) { + $t--; + $prev = $tokens[$t]; + // indicate that other injectors should not process this token, + // but we need to reprocess it + unset($prev->skip[$i]); + $prev->rewind = $i; + if ($prev instanceof HTMLPurifier_Token_Start) array_pop($this->stack); + elseif ($prev instanceof HTMLPurifier_Token_End) $this->stack[] = $prev->start; + } } - $this->processToken($token, $config, $context); + $i = false; + } + + // handle case of document end + if (!isset($tokens[$t])) { + // kill processing if stack is empty + if (empty($this->stack)) break; + + // peek + $top_nesting = array_pop($this->stack); + $this->stack[] = $top_nesting; + + // send error + if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); + } + + // append, don't splice, since this is the end + $tokens[] = new HTMLPurifier_Token_End($top_nesting->name); + + // punt! + $reprocess = true; continue; } - - $info = $definition->info[$token->name]->child; - - // quick tag checks: anything that's *not* an end tag - $ok = false; - if ($info->type == 'empty' && $token->type == 'start') { - // test if it claims to be a start tag but is empty - $token = new HTMLPurifier_Token_Empty($token->name, $token->attr); - $ok = true; - } elseif ($info->type != 'empty' && $token->type == 'empty' ) { - // claims to be empty but really is a start tag - $token = array( - new HTMLPurifier_Token_Start($token->name, $token->attr), - new HTMLPurifier_Token_End($token->name) - ); - $ok = true; - } elseif ($token->type == 'empty') { - // real empty token - $ok = true; - } elseif ($token->type == 'start') { - // start tag - - // ...unless they also have to close their parent - if (!empty($this->currentNesting)) { - - $parent = array_pop($this->currentNesting); - $parent_info = $definition->info[$parent->name]; - - // this can be replaced with a more general algorithm: - // if the token is not allowed by the parent, auto-close - // the parent - if (!isset($parent_info->child->elements[$token->name])) { - if ($e) $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent); - // close the parent, then re-loop to reprocess token - $result[] = new HTMLPurifier_Token_End($parent->name); - $this->inputIndex--; - continue; - } - - $this->currentNesting[] = $parent; // undo the pop - } - $ok = true; - } - - // injector handler code; duplicated for performance reasons - if ($ok) { - foreach ($this->injectors as $i => $x) { - if (!$x->skip) $this->injectors[$i]->handleElement($token); - if (is_array($token)) { - $this->currentInjector = $i; + + $token = $tokens[$t]; + + //echo '
'; printTokens($tokens, $t); printTokens($this->stack); + + // quick-check: if it's not a tag, no need to process + if (empty($token->is_tag)) { + if ($token instanceof HTMLPurifier_Token_Text) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) continue; + if ($token->rewind !== null && $token->rewind !== $i) continue; + $injector->handleText($token); + $this->processToken($token, $i); + $reprocess = true; break; } } - $this->processToken($token, $config, $context); + // another possibility is a comment continue; } - + + if (isset($definition->info[$token->name])) { + $type = $definition->info[$token->name]->child->type; + } else { + $type = false; // Type is unknown, treat accordingly + } + + // quick tag checks: anything that's *not* an end tag + $ok = false; + if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { + // claims to be a start tag but is empty + $token = new HTMLPurifier_Token_Empty($token->name, $token->attr); + $ok = true; + } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { + // claims to be empty but really is a start tag + $this->swap(new HTMLPurifier_Token_End($token->name)); + $this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr)); + // punt (since we had to modify the input stream in a non-trivial way) + $reprocess = true; + continue; + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + // real empty token + $ok = true; + } elseif ($token instanceof HTMLPurifier_Token_Start) { + // start tag + + // ...unless they also have to close their parent + if (!empty($this->stack)) { + + $parent = array_pop($this->stack); + $this->stack[] = $parent; + + if (isset($definition->info[$parent->name])) { + $elements = $definition->info[$parent->name]->child->getAllowedElements($config); + $autoclose = !isset($elements[$token->name]); + } else { + $autoclose = false; + } + + if ($autoclose && $definition->info[$token->name]->wrap) { + // check if this is actually a wrap (mmm wraps!) + $wrapname = $definition->info[$token->name]->wrap; + $wrapdef = $definition->info[$wrapname]; + $elements = $wrapdef->child->getAllowedElements($config); + if (isset($elements[$token->name])) { + $newtoken = new HTMLPurifier_Token_Start($wrapname); + $this->insertBefore($newtoken); + $reprocess = true; + continue; + } + } + + $carryover = false; + if ($autoclose && $definition->info[$parent->name]->formatting) { + $carryover = true; + } + + if ($autoclose) { + // errors need to be updated + $new_token = new HTMLPurifier_Token_End($parent->name); + $new_token->start = $parent; + if ($carryover) { + $element = clone $parent; + $element->armor['MakeWellFormed_TagClosedError'] = true; + $element->carryover = true; + $this->processToken(array($new_token, $token, $element)); + } else { + $this->insertBefore($new_token); + } + if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) { + if (!$carryover) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent); + } else { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent); + } + } + $reprocess = true; + continue; + } + + } + $ok = true; + } + + if ($ok) { + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) continue; + if ($token->rewind !== null && $token->rewind !== $i) continue; + $injector->handleElement($token); + $this->processToken($token, $i); + $reprocess = true; + break; + } + if (!$reprocess) { + // ah, nothing interesting happened; do normal processing + $this->swap($token); + if ($token instanceof HTMLPurifier_Token_Start) { + $this->stack[] = $token; + } elseif ($token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception('Improper handling of end tag in start code; possible error in MakeWellFormed'); + } + } + continue; + } + // sanity check: we should be dealing with a closing tag - if ($token->type != 'end') continue; - + if (!$token instanceof HTMLPurifier_Token_End) { + throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier'); + } + // make sure that we have something open - if (empty($this->currentNesting)) { + if (empty($this->stack)) { 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) - ); - } elseif ($e) { - $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); + $this->swap(new HTMLPurifier_Token_Text( + $generator->generateFromToken($token) + )); + } else { + $this->remove(); + if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); } + $reprocess = true; continue; } - - // first, check for the simplest case: everything closes neatly - $current_parent = array_pop($this->currentNesting); + + // first, check for the simplest case: everything closes neatly. + // Eventually, everything passes through here; if there are problems + // we modify the input stream accordingly and then punt, so that + // the tokens get processed again. + $current_parent = array_pop($this->stack); if ($current_parent->name == $token->name) { - $result[] = $token; - foreach ($this->injectors as $i => $x) { - $this->injectors[$i]->notifyEnd($token); + $token->start = $current_parent; + foreach ($this->injectors as $i => $injector) { + if (isset($token->skip[$i])) continue; + if ($token->rewind !== null && $token->rewind !== $i) continue; + $injector->handleEnd($token); + $this->processToken($token, $i); + $this->stack[] = $current_parent; + $reprocess = true; + break; } continue; } - + // okay, so we're trying to close the wrong tag - + // undo the pop previous pop - $this->currentNesting[] = $current_parent; - + $this->stack[] = $current_parent; + // scroll back the entire nest, trying to find our tag. // (feature could be to specify how far you'd like to go) - $size = count($this->currentNesting); + $size = count($this->stack); // -2 because -1 is the last element, but we already checked that $skipped_tags = false; - for ($i = $size - 2; $i >= 0; $i--) { - if ($this->currentNesting[$i]->name == $token->name) { - // current nesting is modified - $skipped_tags = array_splice($this->currentNesting, $i); + for ($j = $size - 2; $j >= 0; $j--) { + if ($this->stack[$j]->name == $token->name) { + $skipped_tags = array_slice($this->stack, $j); break; } } - - // we still didn't find the tag, so remove + + // we didn't find the tag, so remove if ($skipped_tags === false) { if ($escape_invalid_tags) { - $result[] = new HTMLPurifier_Token_Text( - $generator->generateFromToken($token, $config, $context) - ); + $this->swap(new HTMLPurifier_Token_Text( + $generator->generateFromToken($token) + )); if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text'); - } elseif ($e) { - $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed'); + } else { + $this->remove(); + if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed'); } + $reprocess = true; continue; } - - // okay, we found it, close all the skipped tags - // note that skipped tags contains the element we need closed - for ($i = count($skipped_tags) - 1; $i >= 0; $i--) { - if ($i && $e && !isset($skipped_tags[$i]->armor['MakeWellFormed_TagClosedError'])) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$i]); - } - $result[] = $new_token = new HTMLPurifier_Token_End($skipped_tags[$i]->name); - foreach ($this->injectors as $j => $x) { // $j, not $i!!! - $this->injectors[$j]->notifyEnd($new_token); + + // do errors, in REVERSE $j order: a,b,c with + $c = count($skipped_tags); + if ($e) { + for ($j = $c - 1; $j > 0; $j--) { + // notice we exclude $j == 0, i.e. the current ending tag, from + // the errors... + if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) { + $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]); + } } } - + + // insert tags, in FORWARD $j order: c,b,a with + $replace = array($token); + for ($j = 1; $j < $c; $j++) { + // ...as well as from the insertions + $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name); + $new_token->start = $skipped_tags[$j]; + array_unshift($replace, $new_token); + if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) { + $element = clone $skipped_tags[$j]; + $element->carryover = true; + $element->armor['MakeWellFormed_TagClosedError'] = true; + $replace[] = $element; + } + } + $this->processToken($replace); + $reprocess = true; + continue; } - + $context->destroy('CurrentNesting'); $context->destroy('InputTokens'); $context->destroy('InputIndex'); $context->destroy('CurrentToken'); - - // we're at the end now, fix all still unclosed tags (this is - // duplicated from the end of the loop with some slight modifications) - // not using $skipped_tags since it would invariably be all of them - if (!empty($this->currentNesting)) { - for ($i = count($this->currentNesting) - 1; $i >= 0; $i--) { - if ($e && !isset($this->currentNesting[$i]->armor['MakeWellFormed_TagClosedError'])) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $this->currentNesting[$i]); - } - $result[] = $new_token = new HTMLPurifier_Token_End($this->currentNesting[$i]->name); - foreach ($this->injectors as $j => $x) { // $j, not $i!!! - $this->injectors[$j]->notifyEnd($new_token); - } + + unset($this->injectors, $this->stack, $this->tokens, $this->t); + return $tokens; + } + + /** + * Processes arbitrary token values for complicated substitution patterns. + * In general: + * + * If $token is an array, it is a list of tokens to substitute for the + * current token. These tokens then get individually processed. If there + * is a leading integer in the list, that integer determines how many + * tokens from the stream should be removed. + * + * If $token is a regular token, it is swapped with the current token. + * + * If $token is false, the current token is deleted. + * + * If $token is an integer, that number of tokens (with the first token + * being the current one) will be deleted. + * + * @param $token Token substitution value + * @param $injector Injector that performed the substitution; default is if + * this is not an injector related operation. + */ + protected function processToken($token, $injector = -1) { + + // normalize forms of token + if (is_object($token)) $token = array(1, $token); + if (is_int($token)) $token = array($token); + if ($token === false) $token = array(1); + if (!is_array($token)) throw new HTMLPurifier_Exception('Invalid token type from injector'); + if (!is_int($token[0])) array_unshift($token, 1); + if ($token[0] === 0) throw new HTMLPurifier_Exception('Deleting zero tokens is not valid'); + + // $token is now an array with the following form: + // array(number nodes to delete, new node 1, new node 2, ...) + + $delete = array_shift($token); + $old = array_splice($this->tokens, $this->t, $delete, $token); + + if ($injector > -1) { + // determine appropriate skips + $oldskip = isset($old[0]) ? $old[0]->skip : array(); + foreach ($token as $object) { + $object->skip = $oldskip; + $object->skip[$injector] = true; } } - - unset($this->outputTokens, $this->injectors, $this->currentInjector, - $this->currentNesting, $this->inputTokens, $this->inputIndex); - - return $result; + } - - function processToken($token, $config, &$context) { - if (is_array($token)) { - // the original token was overloaded by an injector, time - // to some fancy acrobatics - - // $this->inputIndex is decremented so that the entire set gets - // re-processed - array_splice($this->inputTokens, $this->inputIndex--, 1, $token); - - // adjust the injector skips based on the array substitution - if ($this->injectors) { - $offset = count($token); - for ($i = 0; $i <= $this->currentInjector; $i++) { - // because of the skip back, we need to add one more - // for uninitialized injectors. I'm not exactly - // sure why this is the case, but I think it has to - // do with the fact that we're decrementing skips - // before re-checking text - if (!$this->injectors[$i]->skip) $this->injectors[$i]->skip++; - $this->injectors[$i]->skip += $offset; - } - } - } elseif ($token) { - // regular case - $this->outputTokens[] = $token; - if ($token->type == 'start') { - $this->currentNesting[] = $token; - } elseif ($token->type == 'end') { - array_pop($this->currentNesting); // not actually used - } - } + + /** + * Inserts a token before the current token. Cursor now points to this token + */ + private function insertBefore($token) { + array_splice($this->tokens, $this->t, 0, array($token)); } - + + /** + * Removes current token. Cursor now points to new token occupying previously + * occupied space. + */ + private function remove() { + array_splice($this->tokens, $this->t, 1); + } + + /** + * Swap current token with new token. Cursor points to new token (no change). + */ + private function swap($token) { + $this->tokens[$this->t] = $token; + } + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php b/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php index 5d26e4f5705..cf3a33e406e 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php @@ -1,52 +1,8 @@ - This directive enables pre-emptive URI checking in img - tags, as the attribute validation strategy is not authorized to - remove elements from the document. This directive has been available - since 1.3.0, revert to pre-1.3.0 behavior by setting to false. -

-' -); - -HTMLPurifier_ConfigSchema::define( - 'Core', 'RemoveScriptContents', null, 'bool/null', ' -

- This directive enables HTML Purifier to remove not only script tags - but all of their contents. This directive has been deprecated since 2.1.0, - and when not set the value of %Core.HiddenElements will take - precedence. This directive has been available since 2.0.0, and can be used to - revert to pre-2.0.0 behavior by setting it to false. -

-' -); - -HTMLPurifier_ConfigSchema::define( - 'Core', 'HiddenElements', array('script' => true, 'style' => true), 'lookup', ' -

- This directive is a lookup array of elements which should have their - contents removed when they are not allowed by the HTML definition. - For example, the contents of a script tag are not - normally shown in a document, so if script tags are to be removed, - their contents should be removed to. This is opposed to a b - tag, which defines some presentational changes but does not hide its - contents. -

-' -); - /** * Removes all unrecognized tags from the list of tokens. - * + * * This strategy iterates through all the tokens and removes unrecognized * tokens. If a token is not recognized but a TagTransform is defined for * that element, the element will be transformed accordingly. @@ -54,41 +10,44 @@ HTMLPurifier_ConfigSchema::define( class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy { - - function execute($tokens, $config, &$context) { + + public function execute($tokens, $config, $context) { $definition = $config->getHTMLDefinition(); - $generator = new HTMLPurifier_Generator(); + $generator = new HTMLPurifier_Generator($config, $context); $result = array(); - - $escape_invalid_tags = $config->get('Core', 'EscapeInvalidTags'); - $remove_invalid_img = $config->get('Core', 'RemoveInvalidImg'); - - $remove_script_contents = $config->get('Core', 'RemoveScriptContents'); - $hidden_elements = $config->get('Core', 'HiddenElements'); - + + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + $remove_invalid_img = $config->get('Core.RemoveInvalidImg'); + + // currently only used to determine if comments should be kept + $trusted = $config->get('HTML.Trusted'); + + $remove_script_contents = $config->get('Core.RemoveScriptContents'); + $hidden_elements = $config->get('Core.HiddenElements'); + // remove script contents compatibility if ($remove_script_contents === true) { $hidden_elements['script'] = true; } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) { unset($hidden_elements['script']); } - + $attr_validator = new HTMLPurifier_AttrValidator(); - + // removes tokens until it reaches a closing tag with its value $remove_until = false; - + // converts comments into text tokens when this is equal to a tag name $textify_comments = false; - + $token = false; $context->register('CurrentToken', $token); - + $e = false; - if ($config->get('Core', 'CollectErrors')) { + if ($config->get('Core.CollectErrors')) { $e =& $context->get('ErrorCollector'); } - + foreach($tokens as $token) { if ($remove_until) { if (empty($token->is_tag) || $token->name !== $remove_until) { @@ -97,7 +56,7 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy } if (!empty( $token->is_tag )) { // DEFINITION CALL - + // before any processing, try to transform the element if ( isset($definition->info_tag_transform[$token->name]) @@ -110,13 +69,13 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy transform($token, $config, $context); if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name); } - + if (isset($definition->info[$token->name])) { - + // mostly everything's good, but // we need to make sure required attributes are in order if ( - ($token->type === 'start' || $token->type === 'empty') && + ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) && $definition->info[$token->name]->required_attr && ($token->name != 'img' || $remove_invalid_img) // ensure config option still works ) { @@ -134,26 +93,26 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy } $token->armor['ValidateAttributes'] = true; } - - if (isset($hidden_elements[$token->name]) && $token->type == 'start') { + + if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) { $textify_comments = $token->name; - } elseif ($token->name === $textify_comments && $token->type == 'end') { + } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) { $textify_comments = false; } - + } elseif ($escape_invalid_tags) { // 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 // CAN BE GENERICIZED if (isset($hidden_elements[$token->name])) { - if ($token->type == 'start') { + if ($token instanceof HTMLPurifier_Token_Start) { $remove_until = $token->name; - } elseif ($token->type == 'empty') { + } elseif ($token instanceof HTMLPurifier_Token_Empty) { // do nothing: we're still looking } else { $remove_until = false; @@ -164,17 +123,34 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy } continue; } - } elseif ($token->type == 'comment') { + } elseif ($token instanceof HTMLPurifier_Token_Comment) { // textify comments in script tags when they are allowed if ($textify_comments !== false) { $data = $token->data; $token = new HTMLPurifier_Token_Text($data); + } elseif ($trusted) { + // keep, but perform comment cleaning + if ($e) { + // perform check whether or not there's a trailing hyphen + if (substr($token->data, -1) == '-') { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed'); + } + } + $token->data = rtrim($token->data, '-'); + $found_double_hyphen = false; + while (strpos($token->data, '--') !== false) { + if ($e && !$found_double_hyphen) { + $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed'); + } + $found_double_hyphen = true; // prevent double-erroring + $token->data = str_replace('--', '-', $token->data); + } } else { // strip comments if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); continue; } - } elseif ($token->type == 'text') { + } elseif ($token instanceof HTMLPurifier_Token_Text) { } else { continue; } @@ -184,11 +160,12 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy // we removed tokens until the end, throw error $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until); } - + $context->destroy('CurrentToken'); - + return $result; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php b/lib/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php index 6debcc336bd..c3328a9d441 100644 --- a/lib/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php +++ b/lib/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php @@ -1,44 +1,39 @@ register('CurrentToken', $token); - + foreach ($tokens as $key => $token) { - + // only process tokens that have attributes, // namely start and empty tags - if ($token->type !== 'start' && $token->type !== 'empty') continue; - + if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) continue; + // skip tokens that are armored if (!empty($token->armor['ValidateAttributes'])) continue; - + // note that we have no facilities here for removing tokens $validator->validateToken($token, $config, $context); - + $tokens[$key] = $token; // for PHP 4 } $context->destroy('CurrentToken'); - + return $tokens; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/StringHash.php b/lib/htmlpurifier/HTMLPurifier/StringHash.php new file mode 100644 index 00000000000..62085c5c2ff --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/StringHash.php @@ -0,0 +1,39 @@ +accessed[$index] = true; + return parent::offsetGet($index); + } + + /** + * Returns a lookup array of all array indexes that have been accessed. + * @return Array in form array($index => true). + */ + public function getAccessed() { + return $this->accessed; + } + + /** + * Resets the access array. + */ + public function resetAccessed() { + $this->accessed = array(); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/StringHashParser.php b/lib/htmlpurifier/HTMLPurifier/StringHashParser.php new file mode 100644 index 00000000000..f3e70c712f0 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/StringHashParser.php @@ -0,0 +1,110 @@ + 'DefaultKeyValue', + * 'KEY' => 'Value', + * 'KEY2' => 'Value2', + * 'MULTILINE-KEY' => "Multiline\nvalue.\n", + * ) + * + * We use this as an easy to use file-format for configuration schema + * files, but the class itself is usage agnostic. + * + * You can use ---- to forcibly terminate parsing of a single string-hash; + * this marker is used in multi string-hashes to delimit boundaries. + */ +class HTMLPurifier_StringHashParser +{ + + public $default = 'ID'; + + /** + * Parses a file that contains a single string-hash. + */ + public function parseFile($file) { + if (!file_exists($file)) return false; + $fh = fopen($file, 'r'); + if (!$fh) return false; + $ret = $this->parseHandle($fh); + fclose($fh); + return $ret; + } + + /** + * Parses a file that contains multiple string-hashes delimited by '----' + */ + public function parseMultiFile($file) { + if (!file_exists($file)) return false; + $ret = array(); + $fh = fopen($file, 'r'); + if (!$fh) return false; + while (!feof($fh)) { + $ret[] = $this->parseHandle($fh); + } + fclose($fh); + return $ret; + } + + /** + * Internal parser that acepts a file handle. + * @note While it's possible to simulate in-memory parsing by using + * custom stream wrappers, if such a use-case arises we should + * factor out the file handle into its own class. + * @param $fh File handle with pointer at start of valid string-hash + * block. + */ + protected function parseHandle($fh) { + $state = false; + $single = false; + $ret = array(); + do { + $line = fgets($fh); + if ($line === false) break; + $line = rtrim($line, "\n\r"); + if (!$state && $line === '') continue; + if ($line === '----') break; + if (strncmp('--#', $line, 3) === 0) { + // Comment + continue; + } elseif (strncmp('--', $line, 2) === 0) { + // Multiline declaration + $state = trim($line, '- '); + if (!isset($ret[$state])) $ret[$state] = ''; + continue; + } elseif (!$state) { + $single = true; + if (strpos($line, ':') !== false) { + // Single-line declaration + list($state, $line) = explode(':', $line, 2); + $line = trim($line); + } else { + // Use default declaration + $state = $this->default; + } + } + if ($single) { + $ret[$state] = $line; + $single = false; + $state = false; + } else { + $ret[$state] .= "$line\n"; + } + } while (!feof($fh)); + return $ret; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/TagTransform.php b/lib/htmlpurifier/HTMLPurifier/TagTransform.php index f5de99ce49c..210a4472176 100644 --- a/lib/htmlpurifier/HTMLPurifier/TagTransform.php +++ b/lib/htmlpurifier/HTMLPurifier/TagTransform.php @@ -1,29 +1,24 @@ 'xx-small', '1' => 'xx-small', '2' => 'small', @@ -35,30 +36,30 @@ class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform '+3' => '200%', '+4' => '300%' ); - - function transform($tag, $config, &$context) { - - if ($tag->type == 'end') { - $new_tag = $tag->copy(); + + public function transform($tag, $config, $context) { + + if ($tag instanceof HTMLPurifier_Token_End) { + $new_tag = clone $tag; $new_tag->name = $this->transform_to; return $new_tag; } - + $attr = $tag->attr; $prepend_style = ''; - + // handle color transform if (isset($attr['color'])) { $prepend_style .= 'color:' . $attr['color'] . ';'; unset($attr['color']); } - + // handle face transform if (isset($attr['face'])) { $prepend_style .= 'font-family:' . $attr['face'] . ';'; unset($attr['face']); } - + // handle size transform if (isset($attr['size'])) { // normalize large numbers @@ -76,19 +77,20 @@ class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform } unset($attr['size']); } - + if ($prepend_style) { $attr['style'] = isset($attr['style']) ? $prepend_style . $attr['style'] : $prepend_style; } - - $new_tag = $tag->copy(); + + $new_tag = clone $tag; $new_tag->name = $this->transform_to; $new_tag->attr = $attr; - + return $new_tag; - + } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/TagTransform/Simple.php b/lib/htmlpurifier/HTMLPurifier/TagTransform/Simple.php index 0b5a84d480d..0e36130f253 100644 --- a/lib/htmlpurifier/HTMLPurifier/TagTransform/Simple.php +++ b/lib/htmlpurifier/HTMLPurifier/TagTransform/Simple.php @@ -1,7 +1,5 @@ transform_to = $transform_to; $this->style = $style; } - - function transform($tag, $config, &$context) { - $new_tag = $tag->copy(); + + public function transform($tag, $config, $context) { + $new_tag = clone $tag; $new_tag->name = $this->transform_to; if (!is_null($this->style) && - ($new_tag->type == 'start' || $new_tag->type == 'empty') + ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty) ) { $this->prependCSS($new_tag->attr, $this->style); } return $new_tag; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Token.php b/lib/htmlpurifier/HTMLPurifier/Token.php index bd859d7f470..7900e6cb101 100644 --- a/lib/htmlpurifier/HTMLPurifier/Token.php +++ b/lib/htmlpurifier/HTMLPurifier/Token.php @@ -1,163 +1,57 @@ is_a(). @public */ - var $line; /**< Line number node was on in source document. Null if unknown. @public */ - + public $line; /**< Line number node was on in source document. Null if unknown. */ + public $col; /**< Column of line node was on in source document. Null if unknown. */ + /** * Lookup array of processing that this token is exempt from. * Currently, valid values are "ValidateAttributes" and * "MakeWellFormed_TagClosedError" */ - var $armor = array(); - - /** - * Copies the tag into a new one (clone substitute). - * @return Copied token - */ - function copy() { - return unserialize(serialize($this)); - } -} + public $armor = array(); -/** - * Abstract class of a tag token (start, end or empty), and its behavior. - */ -class HTMLPurifier_Token_Tag extends HTMLPurifier_Token // abstract -{ /** - * Static bool marker that indicates the class is a tag. - * - * This allows us to check objects with !empty($obj->is_tag) - * without having to use a function call is_a(). - * - * @public + * Used during MakeWellFormed. */ - var $is_tag = true; - - /** - * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. - * - * @note Strictly speaking, XML tags are case sensitive, so we shouldn't - * be lower-casing them, but these tokens cater to HTML tags, which are - * insensitive. - * - * @public - */ - var $name; - - /** - * Associative array of the tag's attributes. - */ - var $attr = array(); - - /** - * Non-overloaded constructor, which lower-cases passed tag name. - * - * @param $name String name. - * @param $attr Associative array of attributes. - */ - function HTMLPurifier_Token_Tag($name, $attr = array(), $line = null) { - $this->name = ctype_lower($name) ? $name : strtolower($name); - foreach ($attr as $key => $value) { - // normalization only necessary when key is not lowercase - if (!ctype_lower($key)) { - $new_key = strtolower($key); - if (!isset($attr[$new_key])) { - $attr[$new_key] = $attr[$key]; - } - if ($new_key !== $key) { - unset($attr[$key]); - } - } + public $skip; + public $rewind; + public $carryover; + + public function __get($n) { + if ($n === 'type') { + trigger_error('Deprecated type property called; use instanceof', E_USER_NOTICE); + switch (get_class($this)) { + case 'HTMLPurifier_Token_Start': return 'start'; + case 'HTMLPurifier_Token_Empty': return 'empty'; + case 'HTMLPurifier_Token_End': return 'end'; + case 'HTMLPurifier_Token_Text': return 'text'; + case 'HTMLPurifier_Token_Comment': return 'comment'; + default: return null; } - $this->attr = $attr; - $this->line = $line; + } } -} -/** - * Concrete start token class. - */ -class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag -{ - var $type = 'start'; -} - -/** - * Concrete empty token class. - */ -class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag -{ - var $type = 'empty'; -} - -/** - * Concrete end token class. - * - * @warning This class accepts attributes even though end tags cannot. This - * is for optimization reasons, as under normal circumstances, the Lexers - * do not pass attributes. - */ -class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag -{ - var $type = 'end'; -} - -/** - * Concrete text token class. - * - * Text tokens comprise of regular parsed character data (PCDATA) and raw - * character data (from the CDATA sections). Internally, their - * data is parsed with all entities expanded. Surprisingly, the text token - * does have a "tag name" called #PCDATA, which is how the DTD represents it - * in permissible child nodes. - */ -class HTMLPurifier_Token_Text extends HTMLPurifier_Token -{ - - var $name = '#PCDATA'; /**< PCDATA tag name compatible with DTD. @public */ - var $type = 'text'; - var $data; /**< Parsed character data of text. @public */ - var $is_whitespace; /**< Bool indicating if node is whitespace. @public */ - /** - * Constructor, accepts data and determines if it is whitespace. - * - * @param $data String parsed character data. + * Sets the position of the token in the source document. */ - function HTMLPurifier_Token_Text($data, $line = null) { - $this->data = $data; - $this->is_whitespace = ctype_space($data); - $this->line = $line; + public function position($l = null, $c = null) { + $this->line = $l; + $this->col = $c; } - -} -/** - * Concrete comment token class. Generally will be ignored. - */ -class HTMLPurifier_Token_Comment extends HTMLPurifier_Token -{ - var $data; /**< Character data within comment. @public */ - var $type = 'comment'; /** - * Transparent constructor. - * - * @param $data String comment data. + * Convenience function for DirectLex settings line/col position. */ - function HTMLPurifier_Token_Comment($data, $line = null) { - $this->data = $data; - $this->line = $line; + public function rawPosition($l, $c) { + if ($c === -1) $l++; + $this->line = $l; + $this->col = $c; } + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Token/Comment.php b/lib/htmlpurifier/HTMLPurifier/Token/Comment.php new file mode 100644 index 00000000000..dc6bdcabb85 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/Token/Comment.php @@ -0,0 +1,22 @@ +data = $data; + $this->line = $line; + $this->col = $col; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Token/Empty.php b/lib/htmlpurifier/HTMLPurifier/Token/Empty.php new file mode 100644 index 00000000000..2a82b47ad12 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/Token/Empty.php @@ -0,0 +1,11 @@ +!empty($obj->is_tag) + * without having to use a function call is_a(). + */ + public $is_tag = true; + + /** + * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. + * + * @note Strictly speaking, XML tags are case sensitive, so we shouldn't + * be lower-casing them, but these tokens cater to HTML tags, which are + * insensitive. + */ + public $name; + + /** + * Associative array of the tag's attributes. + */ + public $attr = array(); + + /** + * Non-overloaded constructor, which lower-cases passed tag name. + * + * @param $name String name. + * @param $attr Associative array of attributes. + */ + public function __construct($name, $attr = array(), $line = null, $col = null) { + $this->name = ctype_lower($name) ? $name : strtolower($name); + foreach ($attr as $key => $value) { + // normalization only necessary when key is not lowercase + if (!ctype_lower($key)) { + $new_key = strtolower($key); + if (!isset($attr[$new_key])) { + $attr[$new_key] = $attr[$key]; + } + if ($new_key !== $key) { + unset($attr[$key]); + } + } + } + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/Token/Text.php b/lib/htmlpurifier/HTMLPurifier/Token/Text.php new file mode 100644 index 00000000000..82efd823d6b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/Token/Text.php @@ -0,0 +1,33 @@ +data = $data; + $this->is_whitespace = ctype_space($data); + $this->line = $line; + $this->col = $col; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/TokenFactory.php b/lib/htmlpurifier/HTMLPurifier/TokenFactory.php index d15ee1a9e12..7cf48fb41c1 100644 --- a/lib/htmlpurifier/HTMLPurifier/TokenFactory.php +++ b/lib/htmlpurifier/HTMLPurifier/TokenFactory.php @@ -1,28 +1,26 @@ p_text = new HTMLPurifier_Token_Text(''); $this->p_comment= new HTMLPurifier_Token_Comment(''); } - + /** * Creates a HTMLPurifier_Token_Start. * @param $name Tag name @@ -42,10 +40,10 @@ class HTMLPurifier_TokenFactory */ public function createStart($name, $attr = array()) { $p = clone $this->p_start; - $p->HTMLPurifier_Token_Tag($name, $attr); + $p->__construct($name, $attr); return $p; } - + /** * Creates a HTMLPurifier_Token_End. * @param $name Tag name @@ -53,10 +51,10 @@ class HTMLPurifier_TokenFactory */ public function createEnd($name) { $p = clone $this->p_end; - $p->HTMLPurifier_Token_Tag($name); + $p->__construct($name); return $p; } - + /** * Creates a HTMLPurifier_Token_Empty. * @param $name Tag name @@ -65,10 +63,10 @@ class HTMLPurifier_TokenFactory */ public function createEmpty($name, $attr = array()) { $p = clone $this->p_empty; - $p->HTMLPurifier_Token_Tag($name, $attr); + $p->__construct($name, $attr); return $p; } - + /** * Creates a HTMLPurifier_Token_Text. * @param $data Data of text token @@ -76,10 +74,10 @@ class HTMLPurifier_TokenFactory */ public function createText($data) { $p = clone $this->p_text; - $p->HTMLPurifier_Token_Text($data); + $p->__construct($data); return $p; } - + /** * Creates a HTMLPurifier_Token_Comment. * @param $data Data of comment token @@ -87,9 +85,10 @@ class HTMLPurifier_TokenFactory */ public function createComment($data) { $p = clone $this->p_comment; - $p->HTMLPurifier_Token_Comment($data); + $p->__construct($data); return $p; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URI.php b/lib/htmlpurifier/HTMLPurifier/URI.php index be42ccb8690..8b50d0d18d8 100644 --- a/lib/htmlpurifier/HTMLPurifier/URI.php +++ b/lib/htmlpurifier/HTMLPurifier/URI.php @@ -1,8 +1,5 @@ scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme); $this->userinfo = $userinfo; $this->host = $host; @@ -28,15 +25,15 @@ class HTMLPurifier_URI $this->query = $query; $this->fragment = $fragment; } - + /** * Retrieves a scheme object corresponding to the URI's scheme/default * @param $config Instance of HTMLPurifier_Config * @param $context Instance of HTMLPurifier_Context * @return Scheme object appropriate for validating this URI */ - function getSchemeObj($config, &$context) { - $registry =& HTMLPurifier_URISchemeRegistry::instance(); + public function getSchemeObj($config, $context) { + $registry = HTMLPurifier_URISchemeRegistry::instance(); if ($this->scheme !== null) { $scheme_obj = $registry->getScheme($this->scheme, $config, $context); if (!$scheme_obj) return false; // invalid scheme, clean it out @@ -55,7 +52,7 @@ class HTMLPurifier_URI } return $scheme_obj; } - + /** * Generic validation method applicable for all schemes. May modify * this URI in order to get it into a compliant form. @@ -63,13 +60,13 @@ class HTMLPurifier_URI * @param $context Instance of HTMLPurifier_Context * @return True if validation/filtering succeeds, false if failure */ - function validate($config, &$context) { - + public function validate($config, $context) { + // ABNF definitions from RFC 3986 $chars_sub_delims = '!$&\'()*+,;='; $chars_gen_delims = ':/?#[]@'; $chars_pchar = $chars_sub_delims . ':@'; - + // validate scheme (MUST BE FIRST!) if (!is_null($this->scheme) && is_null($this->host)) { $def = $config->getDefinition('URI'); @@ -77,25 +74,25 @@ class HTMLPurifier_URI $this->scheme = null; } } - + // validate host if (!is_null($this->host)) { $host_def = new HTMLPurifier_AttrDef_URI_Host(); $this->host = $host_def->validate($this->host, $config, $context); if ($this->host === false) $this->host = null; } - + // validate username if (!is_null($this->userinfo)) { $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':'); $this->userinfo = $encoder->encode($this->userinfo); } - + // validate port if (!is_null($this->port)) { if ($this->port < 1 || $this->port > 65535) $this->port = null; } - + // validate path $path_parts = array(); $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/'); @@ -120,7 +117,7 @@ class HTMLPurifier_URI $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@'); $c = strpos($this->path, '/'); if ($c !== false) { - $this->path = + $this->path = $segment_nc_encoder->encode(substr($this->path, 0, $c)) . $segments_encoder->encode(substr($this->path, $c)); } else { @@ -130,27 +127,27 @@ class HTMLPurifier_URI // path-empty (hier and relative) $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; - + } - + /** * Convert URI back to string * @return String URI appropriate for output */ - function toString() { + public function toString() { // reconstruct authority $authority = null; if (!is_null($this->host)) { @@ -159,7 +156,7 @@ class HTMLPurifier_URI $authority .= $this->host; if(!is_null($this->port)) $authority .= ':' . $this->port; } - + // reconstruct the result $result = ''; if (!is_null($this->scheme)) $result .= $this->scheme . ':'; @@ -167,16 +164,10 @@ class HTMLPurifier_URI $result .= $this->path; if (!is_null($this->query)) $result .= '?' . $this->query; if (!is_null($this->fragment)) $result .= '#' . $this->fragment; - + return $result; } - - /** - * Returns a copy of the URI object - */ - function copy() { - return unserialize(serialize($this)); - } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIDefinition.php b/lib/htmlpurifier/HTMLPurifier/URIDefinition.php index 45c505edb03..ea2b8fe2458 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIDefinition.php +++ b/lib/htmlpurifier/HTMLPurifier/URIDefinition.php @@ -1,145 +1,93 @@ - Unique identifier for a custom-built URI definition. If you want - to add custom URIFilters, you must specify this value. - This directive has been available since 2.1.0. -

-'); - -HTMLPurifier_ConfigSchema::define( - 'URI', 'DefinitionRev', 1, 'int', ' -

- Revision identifier for your custom definition. See - %HTML.DefinitionRev for details. This directive has been available - since 2.1.0. -

-'); - -// informative URI directives - -HTMLPurifier_ConfigSchema::define( - 'URI', 'DefaultScheme', 'http', 'string', ' -

- Defines through what scheme the output will be served, in order to - select the proper object validator when no scheme information is present. -

-'); - -HTMLPurifier_ConfigSchema::define( - 'URI', 'Host', null, 'string/null', ' -

- Defines the domain name of the server, so we can determine whether or - an absolute URI is from your website or not. Not strictly necessary, - as users should be using relative URIs to reference resources on your - website. It will, however, let you use absolute URIs to link to - subdomains of the domain you post here: i.e. example.com will allow - sub.example.com. However, higher up domains will still be excluded: - if you set %URI.Host to sub.example.com, example.com will be blocked. - Note: This directive overrides %URI.Base because - a given page may be on a sub-domain, but you wish HTML Purifier to be - more relaxed and allow some of the parent domains too. - This directive has been available since 1.2.0. -

-'); - -HTMLPurifier_ConfigSchema::define( - 'URI', 'Base', null, 'string/null', ' -

- The base URI is the URI of the document this purified HTML will be - inserted into. This information is important if HTML Purifier needs - to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute - is on. You may use a non-absolute URI for this value, but behavior - may vary (%URI.MakeAbsolute deals nicely with both absolute and - relative paths, but forwards-compatibility is not guaranteed). - Warning: If set, the scheme on this URI - overrides the one specified by %URI.DefaultScheme. This directive has - been available since 2.1.0. -

-'); - class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition { - - var $type = 'URI'; - var $filters = array(); - var $registeredFilters = array(); - + + public $type = 'URI'; + protected $filters = array(); + protected $postFilters = array(); + protected $registeredFilters = array(); + /** * HTMLPurifier_URI object of the base specified at %URI.Base */ - var $base; - + public $base; + /** - * String host to consider "home" base + * String host to consider "home" base, derived off of $base */ - var $host; - + public $host; + /** * Name of default scheme based on %URI.DefaultScheme and %URI.Base */ - var $defaultScheme; - - function HTMLPurifier_URIDefinition() { + public $defaultScheme; + + public function __construct() { $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternal()); $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources()); $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist()); $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute()); + $this->registerFilter(new HTMLPurifier_URIFilter_Munge()); } - - function registerFilter($filter) { + + public function registerFilter($filter) { $this->registeredFilters[$filter->name] = $filter; } - - function addFilter($filter, $config) { - $filter->prepare($config); - $this->filters[$filter->name] = $filter; + + public function addFilter($filter, $config) { + $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; + } } - - function doSetup($config) { + + protected function doSetup($config) { $this->setupMemberVariables($config); $this->setupFilters($config); } - - function setupFilters($config) { + + protected function setupFilters($config) { foreach ($this->registeredFilters as $name => $filter) { - $conf = $config->get('URI', $name); + $conf = $config->get('URI.' . $name); if ($conf !== false && $conf !== null) { $this->addFilter($filter, $config); } } unset($this->registeredFilters); } - - function setupMemberVariables($config) { - $this->host = $config->get('URI', 'Host'); - $base_uri = $config->get('URI', 'Base'); + + protected function setupMemberVariables($config) { + $this->host = $config->get('URI.Host'); + $base_uri = $config->get('URI.Base'); if (!is_null($base_uri)) { $parser = new HTMLPurifier_URIParser(); $this->base = $parser->parse($base_uri); $this->defaultScheme = $this->base->scheme; if (is_null($this->host)) $this->host = $this->base->host; } - if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI', 'DefaultScheme'); + if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme'); } - - function filter(&$uri, $config, &$context) { - foreach ($this->filters as $name => $x) { - $result = $this->filters[$name]->filter($uri, $config, $context); + + public function 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; + } + } + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter.php b/lib/htmlpurifier/HTMLPurifier/URIFilter.php index ca000ea5a2a..c116f93dffc 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIFilter.php +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter.php @@ -2,39 +2,44 @@ /** * Chainable filters for custom URI processing. - * + * * These filters can perform custom actions on a URI filter object, * including transformation or blacklisting. - * + * * @warning This filter is called before scheme object validation occurs. * Make sure, if you require a specific scheme object, you * you check that it exists. This allows filters to convert * proprietary URI schemes into regular ones. */ -class HTMLPurifier_URIFilter +abstract class HTMLPurifier_URIFilter { - + /** * Unique identifier of filter */ - var $name; - + public $name; + + /** + * True if this filter should be run after scheme validation. + */ + public $post = false; + /** * Performs initialization for the filter */ - function prepare($config) {} - + public function prepare($config) {return true;} + /** * Filter a URI object - * @param &$uri Reference to URI object + * @param $uri Reference to URI object variable * @param $config Instance of HTMLPurifier_Config - * @param &$context Instance of HTMLPurifier_Context + * @param $context Instance of HTMLPurifier_Context * @return bool Whether or not to continue processing: false indicates * URL is no good, true indicates continue processing. Note that * all changes are committed directly on the URI object */ - function filter(&$uri, $config, &$context) { - trigger_error('Cannot call abstract function', E_USER_ERROR); - } - + abstract public function filter(&$uri, $config, $context); + } + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternal.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternal.php index 4e6dc187c8e..d8a39a50111 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternal.php +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternal.php @@ -1,26 +1,14 @@ get('URI', 'Host'); + public $name = 'DisableExternal'; + protected $ourHostParts = false; + public function prepare($config) { + $our_host = $config->getDefinition('URI')->host; if ($our_host !== null) $this->ourHostParts = array_reverse(explode('.', $our_host)); } - function filter(&$uri, $config, &$context) { + public function filter(&$uri, $config, $context) { if (is_null($uri->host)) return true; if ($this->ourHostParts === false) return false; $host_parts = array_reverse(explode('.', $uri->host)); @@ -32,3 +20,4 @@ class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php index dc00e741105..881abc43cfa 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php @@ -1,26 +1,12 @@ get('EmbeddedURI', true)) return true; return parent::filter($uri, $config, $context); } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php index d3429d5cbf9..045aa0992c6 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php @@ -1,23 +1,14 @@ moo.com will catch moo.com.example.com. '. - 'This directive has been available since 1.3.0.' -); - class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter { - var $name = 'HostBlacklist'; - var $blacklist = array(); - function prepare($config) { - $this->blacklist = $config->get('URI', 'HostBlacklist'); + public $name = 'HostBlacklist'; + protected $blacklist = array(); + public function prepare($config) { + $this->blacklist = $config->get('URI.HostBlacklist'); + return true; } - function filter(&$uri, $config, &$context) { + public function filter(&$uri, $config, $context) { foreach($this->blacklist as $blacklisted_host_fragment) { if (strpos($uri->host, $blacklisted_host_fragment) !== false) { return false; @@ -26,3 +17,5 @@ class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter return true; } } + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php index 8fe4f73e616..f46ab2630d4 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php @@ -2,45 +2,33 @@ // does not support network paths -require_once 'HTMLPurifier/URIFilter.php'; - -HTMLPurifier_ConfigSchema::define( - 'URI', 'MakeAbsolute', false, 'bool', ' -

- Converts all URIs into absolute forms. This is useful when the HTML - being filtered assumes a specific base path, but will actually be - viewed in a different context (and setting an alternate base URI is - not possible). %URI.Base must be set for this directive to work. - This directive has been available since 2.1.0. -

-'); - class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter { - var $name = 'MakeAbsolute'; - var $base; - var $basePathStack = array(); - function prepare($config) { + public $name = 'MakeAbsolute'; + protected $base; + protected $basePathStack = array(); + public function prepare($config) { $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; } - function filter(&$uri, $config, &$context) { + public function filter(&$uri, $config, $context) { if (is_null($this->base)) return true; // abort early if ( $uri->path === '' && is_null($uri->scheme) && is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment) ) { // reference to current document - $uri = $this->base->copy(); + $uri = clone $this->base; return true; } if (!is_null($uri->scheme)) { @@ -63,12 +51,18 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter } if ($uri->path === '') { $uri->path = $this->base->path; - }elseif ($uri->path[0] !== '/') { + } elseif ($uri->path[0] !== '/') { // relative path, needs more complicated processing $stack = explode('/', $uri->path); $new_stack = array_merge($this->basePathStack, $stack); + if ($new_stack[0] !== '' && !is_null($this->base->host)) { + array_unshift($new_stack, ''); + } $new_stack = $this->_collapseStack($new_stack); $uri->path = implode('/', $new_stack); + } else { + // absolute path, but still we should collapse + $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path))); } // re-combine $uri->scheme = $this->base->scheme; @@ -77,13 +71,13 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter if (is_null($uri->port)) $uri->port = $this->base->port; return true; } - + /** * Resolve dots and double-dots in a path stack - * @private */ - function _collapseStack($stack) { + private function _collapseStack($stack) { $result = array(); + $is_folder = false; for ($i = 0; isset($stack[$i]); $i++) { $is_folder = false; // absorb an internally duplicated slash @@ -117,3 +111,4 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter } } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php new file mode 100644 index 00000000000..efa10a6458a --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php @@ -0,0 +1,58 @@ +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; + } + // don't redirect if target host is our host + if ($uri->host === $config->getDefinition('URI')->host) { + return true; + } + + $this->makeReplace($uri, $config, $context); + $this->replace = array_map('rawurlencode', $this->replace); + + $new_uri = strtr($this->target, $this->replace); + $new_uri = $this->parser->parse($new_uri); + // don't redirect if the target host is the same as the + // starting host + if ($uri->host === $new_uri->host) return true; + $uri = $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); + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIParser.php b/lib/htmlpurifier/HTMLPurifier/URIParser.php index 8ba485cf351..7179e4ab899 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIParser.php +++ b/lib/htmlpurifier/HTMLPurifier/URIParser.php @@ -1,35 +1,33 @@ percentEncoder = new HTMLPurifier_PercentEncoder(); } - + /** * Parses a URI. * @param $uri string URI to parse * @return HTMLPurifier_URI representation of URI. This representation has * not been validated yet and may not conform to RFC. */ - function parse($uri) { - + public function parse($uri) { + $uri = $this->percentEncoder->normalize($uri); - + // Regexp is as per Appendix B. - // Note that ["<>] are an addition to the RFC's recommended + // Note that ["<>] are an addition to the RFC's recommended // characters, because they represent external delimeters. $r_URI = '!'. '(([^:/?#"<>]+):)?'. // 2. Scheme @@ -38,19 +36,19 @@ class HTMLPurifier_URIParser '(\?([^#"<>]*))?'. // 7. Query '(#([^"<>]*))?'. // 8. Fragment '!'; - + $matches = array(); $result = preg_match($r_URI, $uri, $matches); - + if (!$result) return false; // *really* invalid URI - + // seperate out parts $scheme = !empty($matches[1]) ? $matches[2] : null; $authority = !empty($matches[3]) ? $matches[4] : null; $path = $matches[5]; // always present, can be empty $query = !empty($matches[6]) ? $matches[7] : null; $fragment = !empty($matches[8]) ? $matches[9] : null; - + // further parse authority if ($authority !== null) { $r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/"; @@ -62,10 +60,11 @@ class HTMLPurifier_URIParser } else { $port = $host = $userinfo = null; } - + return new HTMLPurifier_URI( $scheme, $userinfo, $host, $port, $path, $query, $fragment); } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme.php b/lib/htmlpurifier/HTMLPurifier/URIScheme.php index 41c02f70d2d..039710fd158 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIScheme.php +++ b/lib/htmlpurifier/HTMLPurifier/URIScheme.php @@ -5,26 +5,24 @@ */ class HTMLPurifier_URIScheme { - + /** * Scheme's default port (integer) - * @public */ - var $default_port = null; - + public $default_port = null; + /** * Whether or not URIs of this schem are locatable by a browser * http and ftp are accessible, while mailto and news are not. - * @public */ - var $browsable = false; - + public $browsable = false; + /** * Whether or not the URI always uses , resolves edge cases * with making relative URIs absolute */ - var $hierarchical = false; - + public $hierarchical = false; + /** * Validates the components of a URI * @note This implementation should be called by children if they define @@ -34,10 +32,11 @@ class HTMLPurifier_URIScheme * @param $context HTMLPurifier_Context object * @return Bool success or failure */ - function validate(&$uri, $config, &$context) { + public function validate(&$uri, $config, $context) { if ($this->default_port == $uri->port) $uri->port = null; return true; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/data.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/data.php new file mode 100644 index 00000000000..b7f1989cbf7 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/data.php @@ -0,0 +1,93 @@ + true, + 'image/gif' => true, + 'image/png' => true, + ); + + public function validate(&$uri, $config, $context) { + $result = explode(',', $uri->path, 2); + $is_base64 = false; + $charset = null; + $content_type = null; + if (count($result) == 2) { + list($metadata, $data) = $result; + // do some legwork on the metadata + $metas = explode(';', $metadata); + while(!empty($metas)) { + $cur = array_shift($metas); + if ($cur == 'base64') { + $is_base64 = true; + break; + } + if (substr($cur, 0, 8) == 'charset=') { + // doesn't match if there are arbitrary spaces, but + // whatever dude + if ($charset !== null) continue; // garbage + $charset = substr($cur, 8); // not used + } else { + if ($content_type !== null) continue; // garbage + $content_type = $cur; + } + } + } else { + $data = $result[0]; + } + if ($content_type !== null && empty($this->allowed_types[$content_type])) { + return false; + } + if ($charset !== null) { + // error; we don't allow plaintext stuff + $charset = null; + } + $data = rawurldecode($data); + if ($is_base64) { + $raw_data = base64_decode($data); + } else { + $raw_data = $data; + } + // XXX probably want to refactor this into a general mechanism + // for filtering arbitrary content types + $file = tempnam("/tmp", ""); + file_put_contents($file, $raw_data); + if (function_exists('exif_imagetype')) { + $image_code = exif_imagetype($file); + } elseif (function_exists('getimagesize')) { + set_error_handler(array($this, 'muteErrorHandler')); + $info = getimagesize($file); + restore_error_handler(); + if ($info == false) return false; + $image_code = $info[2]; + } else { + trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR); + } + $real_content_type = image_type_to_mime_type($image_code); + if ($real_content_type != $content_type) { + // we're nice guys; if the content type is something else we + // support, change it over + if (empty($this->allowed_types[$real_content_type])) return false; + $content_type = $real_content_type; + } + // ok, it's kosher, rewrite what we need + $uri->userinfo = null; + $uri->host = null; + $uri->port = null; + $uri->fragment = null; + $uri->query = null; + $uri->path = "$content_type;base64," . base64_encode($raw_data); + return true; + } + + public function muteErrorHandler($errno, $errstr) {} + +} + diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/ftp.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/ftp.php index 5555ef33a17..5849bf7ff05 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIScheme/ftp.php +++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/ftp.php @@ -1,20 +1,18 @@ query = null; - + // typecode check $semicolon_pos = strrpos($uri->path, ';'); // reverse if ($semicolon_pos !== false) { @@ -36,9 +34,10 @@ class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme { $uri->path = str_replace(';', '%3B', $uri->path); $uri->path .= $type_ret; } - + return true; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/http.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/http.php index 7abc6680fdc..b097a31d6a3 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIScheme/http.php +++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/http.php @@ -1,21 +1,20 @@ userinfo = null; return true; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/https.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/https.php index bbd69b9c42f..29e380919f0 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIScheme/https.php +++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/https.php @@ -1,13 +1,12 @@ userinfo = null; $uri->host = null; @@ -23,6 +21,7 @@ class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme { // we need to validate path against RFC 2368's addr-spec return true; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/news.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/news.php index 87bda63c7f6..f5f54f4f56f 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIScheme/news.php +++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/news.php @@ -1,15 +1,13 @@ userinfo = null; $uri->host = null; @@ -18,6 +16,7 @@ class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme { // typecode check needed on path return true; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/nntp.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/nntp.php index caa85b260fb..5bf93ea7849 100644 --- a/lib/htmlpurifier/HTMLPurifier/URIScheme/nntp.php +++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/nntp.php @@ -1,21 +1,20 @@ userinfo = null; $uri->query = null; return true; } - + } +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/URISchemeRegistry.php b/lib/htmlpurifier/HTMLPurifier/URISchemeRegistry.php index 6837fadaafc..576bf7b6d1a 100644 --- a/lib/htmlpurifier/HTMLPurifier/URISchemeRegistry.php +++ b/lib/htmlpurifier/HTMLPurifier/URISchemeRegistry.php @@ -1,50 +1,19 @@ true, // "Hypertext Transfer Protocol", nuf' said - 'https' => true, // HTTP over SSL (Secure Socket Layer) - // quite useful, but not necessary - 'mailto' => true,// Email - 'ftp' => true, // "File Transfer Protocol" - // for Usenet, these two are similar, but distinct - 'nntp' => true, // individual Netnews articles - 'news' => true // newsgroup or individual Netnews articles - ), 'lookup', - 'Whitelist that defines the schemes that a URI is allowed to have. This '. - 'prevents XSS attacks from using pseudo-schemes like javascript or mocha.' -); - -HTMLPurifier_ConfigSchema::define( - 'URI', 'OverrideAllowedSchemes', true, 'bool', - 'If this is set to true (which it is by default), you can override '. - '%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme '. - 'to the registry. If false, you will also have to update that directive '. - 'in order to add more schemes.' -); - /** * Registry for retrieving specific URI scheme validator objects. */ class HTMLPurifier_URISchemeRegistry { - + /** * Retrieve sole instance of the registry. - * @static * @param $prototype Optional prototype to overload sole instance with, * or bool true to reset to default registry. * @note Pass a registry object $prototype with a compatible interface and * the function will copy it and return it all further times. */ - function &instance($prototype = null) { + public static function instance($prototype = null) { static $instance = null; if ($prototype !== null) { $instance = $prototype; @@ -53,49 +22,47 @@ class HTMLPurifier_URISchemeRegistry } return $instance; } - + /** * Cache of retrieved schemes. - * @protected */ - var $schemes = array(); - + protected $schemes = array(); + /** * Retrieves a scheme validator object * @param $scheme String scheme name like http or mailto * @param $config HTMLPurifier_Config object * @param $config HTMLPurifier_Context object */ - function &getScheme($scheme, $config, &$context) { + public function getScheme($scheme, $config, $context) { if (!$config) $config = HTMLPurifier_Config::createDefault(); - $null = null; // for the sake of passing by reference - + // important, otherwise attacker could include arbitrary file - $allowed_schemes = $config->get('URI', 'AllowedSchemes'); - if (!$config->get('URI', 'OverrideAllowedSchemes') && + $allowed_schemes = $config->get('URI.AllowedSchemes'); + if (!$config->get('URI.OverrideAllowedSchemes') && !isset($allowed_schemes[$scheme]) ) { - return $null; + return; } - + if (isset($this->schemes[$scheme])) return $this->schemes[$scheme]; - if (!isset($allowed_schemes[$scheme])) return $null; - + if (!isset($allowed_schemes[$scheme])) return; + $class = 'HTMLPurifier_URIScheme_' . $scheme; - if (!class_exists($class)) return $null; + if (!class_exists($class)) return; $this->schemes[$scheme] = new $class(); return $this->schemes[$scheme]; } - + /** * Registers a custom scheme to the cache, bypassing reflection. * @param $scheme Scheme name * @param $scheme_obj HTMLPurifier_URIScheme object */ - function register($scheme, &$scheme_obj) { - $this->schemes[$scheme] =& $scheme_obj; + public function register($scheme, $scheme_obj) { + $this->schemes[$scheme] = $scheme_obj; } - + } - +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/UnitConverter.php b/lib/htmlpurifier/HTMLPurifier/UnitConverter.php index d8791ccaa8d..545d4262206 100644 --- a/lib/htmlpurifier/HTMLPurifier/UnitConverter.php +++ b/lib/htmlpurifier/HTMLPurifier/UnitConverter.php @@ -6,28 +6,56 @@ */ class HTMLPurifier_UnitConverter { - + + const ENGLISH = 1; + const METRIC = 2; + const DIGITAL = 3; + + /** + * Units information array. Units are grouped into measuring systems + * (English, Metric), and are assigned an integer representing + * the conversion factor between that unit and the smallest unit in + * the system. Numeric indexes are actually magical constants that + * encode conversion data from one system to the next, with a O(n^2) + * constraint on memory (this is generally not a problem, since + * the number of measuring systems is small.) + */ + protected static $units = array( + self::ENGLISH => 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. */ - var $outputPrecision; - + protected $outputPrecision; + /** * Bcmath precision for internal calculations. */ - var $internalPrecision; - + protected $internalPrecision; + /** * Whether or not BCMath is available */ - var $bcmath; - - function HTMLPurifier_UnitConverter($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) { + 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 @@ -46,132 +74,103 @@ class HTMLPurifier_UnitConverter * and this causes some decimals to be excluded, those * decimals will be added on. */ - function convert($length, $to_unit) { - - /** - * Units information array. Units are grouped into measuring systems - * (English, Metric), and are assigned an integer representing - * the conversion factor between that unit and the smallest unit in - * the system. Numeric indexes are actually magical constants that - * encode conversion data from one system to the next, with a O(n^2) - * constraint on memory (this is generally not a problem, since - * the number of measuring systems is small.) - */ - static $units = array( - 1 => array( - 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary - 'pt' => 4, - 'pc' => 48, - 'in' => 288, - 2 => array('pt', '0.352777778', 'mm'), - ), - 2 => array( - 'mm' => 1, - 'cm' => 10, - 1 => array('mm', '2.83464567', 'pt'), - ), - ); - + 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 ($units as $k => $x) { + 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; - - // Cleanup $n for PHP 4.3.9 and 4.3.10. See http://bugs.php.net/bug.php?id=30726 - if (strncmp($n, '-.', 2) === 0) { - $n = '-0.' . substr($n, 2); - } - + // 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 = $units[$state][$dest_state][0]; + $dest_unit = self::$units[$state][$dest_state][0]; } - + // Do the conversion if necessary if ($dest_unit !== $unit) { - $factor = $this->div($units[$state][$unit], $units[$state][$dest_unit], $cp); + $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, $units[$state][$dest_state][1], $cp); - $unit = $units[$state][$dest_state][2]; + $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 */ - function getSigFigs($n) { + public function getSigFigs($n) { $n = ltrim($n, '0+-'); $dp = strpos($n, '.'); // decimal position if ($dp === false) { @@ -182,36 +181,36 @@ class HTMLPurifier_UnitConverter } return $sigfigs; } - + /** * Adds two numbers, using arbitrary precision when available. */ - function add($s1, $s2, $scale) { + 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. */ - function mul($s1, $s2, $scale) { + 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. */ - function div($s1, $s2, $scale) { + 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. */ - function round($n, $sigfigs) { + 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 @@ -230,12 +229,26 @@ class HTMLPurifier_UnitConverter return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1); } } - + /** * Scales a float to $scale digits right of decimal point, like BCMath. */ - function scale($r, $scale) { + private function scale($r, $scale) { + if ($scale < 0) { + // The f sprintf type doesn't support negative numbers, so we + // need to cludge things manually. First get the string. + $r = sprintf('%.0f', (float) $r); + // Due to floating point precision loss, $r will more than likely + // look something like 4652999999999.9234. We grab one more digit + // than we need to precise from $r and then use that to round + // appropriately. + $precise = (string) round(substr($r, 0, strlen($r) + $scale), -1); + // Now we return it, truncating the zero that was rounded off. + return substr($precise, 0, -1) . str_repeat('0', -$scale + 1); + } return sprintf('%.' . $scale . 'f', (float) $r); } - + } + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/VarParser.php b/lib/htmlpurifier/HTMLPurifier/VarParser.php new file mode 100644 index 00000000000..68e72ae8695 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/VarParser.php @@ -0,0 +1,154 @@ + 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 + ); + + /** + * Lookup table of types that are string, and can have aliases or + * allowed value lists. + */ + static public $stringTypes = array( + self::STRING => true, + self::ISTRING => true, + self::TEXT => true, + self::ITEXT => true, + ); + + /** + * Validate a variable according to type. Throws + * HTMLPurifier_VarParserException if invalid. + * It may return NULL as a valid type if $allow_null is true. + * + * @param $var Variable to validate + * @param $type Type of variable, see HTMLPurifier_VarParser->types + * @param $allow_null Whether or not to permit null as a value + * @return Validated and type-coerced variable + */ + final public function parse($var, $type, $allow_null = false) { + 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 (self::STRING): + case (self::ISTRING): + case (self::TEXT): + case (self::ITEXT): + if (!is_string($var)) break; + if ($type == self::ISTRING || $type == self::ITEXT) $var = strtolower($var); + return $var; + case (self::INT): + if (!is_int($var)) break; + return $var; + case (self::FLOAT): + if (!is_float($var)) break; + return $var; + case (self::BOOL): + if (!is_bool($var)) break; + return $var; + case (self::LOOKUP): + case (self::ALIST): + case (self::HASH): + if (!is_array($var)) break; + if ($type === self::LOOKUP) { + foreach ($var as $k) if ($k !== true) $this->error('Lookup table contains value other than true'); + } elseif ($type === self::ALIST) { + $keys = array_keys($var); + if (array_keys($keys) !== $keys) $this->error('Indices for list are not uniform'); + } + return $var; + case (self::MIXED): + return $var; + default: + $this->errorInconsistent(get_class($this), $type); + } + $this->errorGeneric($var, $type); + } + + /** + * Actually implements the parsing. Base implementation is to not + * do anything to $var. Subclasses should overload this! + */ + protected function parseImplementation($var, $type, $allow_null) { + return $var; + } + + /** + * Throws an exception. + */ + protected function error($msg) { + throw new HTMLPurifier_VarParserException($msg); + } + + /** + * Throws an inconsistency exception. + * @note This should not ever be called. It would be called if we + * extend the allowed values of HTMLPurifier_VarParser without + * updating subclasses. + */ + protected function errorInconsistent($class, $type) { + throw new HTMLPurifier_Exception("Inconsistency in $class: ".HTMLPurifier_VarParser::getTypeName($type)." not implemented"); + } + + /** + * Generic error for if a type didn't work. + */ + protected function errorGeneric($var, $type) { + $vtype = gettype($var); + $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]; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php b/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php new file mode 100644 index 00000000000..c954250e9f0 --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php @@ -0,0 +1,96 @@ + $j) $var[$i] = trim($j); + if ($type === self::HASH) { + // key:value,key2:value2 + $nvar = array(); + foreach ($var as $keypair) { + $c = explode(':', $keypair, 2); + if (!isset($c[1])) continue; + $nvar[$c[0]] = $c[1]; + } + $var = $nvar; + } + } + if (!is_array($var)) break; + $keys = array_keys($var); + if ($keys === array_keys($keys)) { + if ($type == self::ALIST) return $var; + elseif ($type == self::LOOKUP) { + $new = array(); + foreach ($var as $key) { + $new[$key] = true; + } + return $new; + } else break; + } + if ($type === self::LOOKUP) { + foreach ($var as $key => $value) { + $var[$key] = true; + } + } + return $var; + default: + $this->errorInconsistent(__CLASS__, $type); + } + $this->errorGeneric($var, $type); + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/VarParser/Native.php b/lib/htmlpurifier/HTMLPurifier/VarParser/Native.php new file mode 100644 index 00000000000..b02a6de54ca --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/VarParser/Native.php @@ -0,0 +1,26 @@ +evalExpression($var); + } + + protected function evalExpression($expr) { + $var = null; + $result = eval("\$var = $expr;"); + if ($result === false) { + throw new HTMLPurifier_VarParserException("Fatal error in evaluated code"); + } + return $var; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/htmlpurifier/HTMLPurifier/VarParserException.php b/lib/htmlpurifier/HTMLPurifier/VarParserException.php new file mode 100644 index 00000000000..5df3414959b --- /dev/null +++ b/lib/htmlpurifier/HTMLPurifier/VarParserException.php @@ -0,0 +1,11 @@ +, , and tags @@ -6,5 +6,3 @@ Changes: * AttrDef/Lang.php - relax lang check - needed for multilang skodak - -$Id$ diff --git a/lib/setup.php b/lib/setup.php index 4dee4a24af2..9a71075c2c5 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -92,6 +92,10 @@ global $HTTPSPAGEREQUIRED; /// sometimes default PHP settings are borked on shared hosting servers, I wonder why they have to do that?? @ini_set('precision', 14); // needed for upgrades and gradebook +/// New versions of HTML Purifier are not compatible with PHP 4 + if (version_compare(phpversion(), "5.0.5") < 0) { + $CFG->enablehtmlpurifier = 0; + } /// store settings from config.php in array in $CFG - we can use it later to detect problems and overrides $CFG->config_php_settings = (array)$CFG; @@ -256,7 +260,6 @@ global $HTTPSPAGEREQUIRED; $originaldatabasedebug = -1; } - /// For now, only needed under apache (and probably unstable in other contexts) if (function_exists('register_shutdown_function')) { register_shutdown_function('moodle_request_shutdown'); diff --git a/lib/weblib.php b/lib/weblib.php index 6ec0acf7889..16ff9cb432d 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -2019,6 +2019,7 @@ function clean_text($text, $format=FORMAT_MOODLE) { default: if (!empty($CFG->enablehtmlpurifier)) { + //this is PHP5 only, the lib/setup.php contains a disabler for PHP4 $text = purify_html($text); } else { /// Fix non standard entity notations @@ -2046,24 +2047,28 @@ function clean_text($text, $format=FORMAT_MOODLE) { /** * KSES replacement cleaning function - uses HTML Purifier. + * + * @global object + * @param string $text The (X)HTML string to purify */ function purify_html($text) { global $CFG; // this can not be done only once because we sometimes need to reset the cache - $cachedir = $CFG->dataroot.'/cache/htmlpurifier/'; + $cachedir = $CFG->dataroot.'/cache/htmlpurifier'; $status = check_dir_exists($cachedir, true, true); static $purifier = false; + static $config; if ($purifier === false) { - require_once $CFG->libdir.'/htmlpurifier/HTMLPurifier.auto.php'; + require_once $CFG->libdir.'/htmlpurifier/HTMLPurifier.safe-includes.php'; $config = HTMLPurifier_Config::createDefault(); - $config->set('Core', 'AcceptFullDocuments', false); - $config->set('Core', 'Encoding', 'UTF-8'); - $config->set('HTML', 'Doctype', 'XHTML 1.0 Transitional'); - $config->set('Cache', 'SerializerPath', $cachedir); - $config->set('URI', 'AllowedSchemes', array('http'=>1, 'https'=>1, 'ftp'=>1, 'irc'=>1, 'nntp'=>1, 'news'=>1, 'rtsp'=>1, 'teamspeak'=>1, 'gopher'=>1, 'mms'=>1)); - $config->set('Attr', 'AllowedFrameTargets', array('_blank')); + $config->set('Core.ConvertDocumentToFragment', true); + $config->set('Core.Encoding', 'UTF-8'); + $config->set('HTML.Doctype', 'XHTML 1.0 Transitional'); + $config->set('Cache.SerializerPath', $cachedir); + $config->set('URI.AllowedSchemes', array('http'=>1, 'https'=>1, 'ftp'=>1, 'irc'=>1, 'nntp'=>1, 'news'=>1, 'rtsp'=>1, 'teamspeak'=>1, 'gopher'=>1, 'mms'=>1)); + $config->set('Attr.AllowedFrameTargets', array('_blank')); $purifier = new HTMLPurifier($config); } return $purifier->purify($text);