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) {
$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 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!
*/
protected function mungeRgb($string) {
return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string);
}
/**
* Parses a possibly escaped CSS string and returns the "pure"
* version of it.
*/
protected function expandCSSEscape($string) {
// flexibly parse it
$ret = '';
for ($i = 0, $c = strlen($string); $i < $c; $i++) {
if ($string[$i] === '\\') {
$i++;
if ($i >= $c) {
$ret .= '\\';
break;
}
if (ctype_xdigit($string[$i])) {
$code = $string[$i];
for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) {
if (!ctype_xdigit($string[$i])) break;
$code .= $string[$i];
}
// We have to be extremely careful when adding
// new characters, to make sure we're not breaking
// the encoding.
$char = HTMLPurifier_Encoder::unichr(hexdec($code));
if (HTMLPurifier_Encoder::cleanUTF8($char) === '') continue;
$ret .= $char;
if ($i < $c && trim($string[$i]) !== '') $i--;
continue;
}
if ($string[$i] === "\n") continue;
}
$ret .= $string[$i];
}
return $ret;
}
}
/**
* 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.
*/
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.
*/
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
*/
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
*/
public function confiscateAttr(&$attr, $key) {
if (!isset($attr[$key])) return null;
$value = $attr[$key];
unset($attr[$key]);
return $value;
}
}
/**
* Provides lookup array of attribute types to HTMLPurifier_AttrDef objects
*/
class HTMLPurifier_AttrTypes
{
/**
* Lookup array of attribute string identifiers to concrete implementations
*/
protected $info = array();
/**
* Constructs the info array, supplying default implementations for attribute
* types.
*/
public function __construct() {
// XXX This is kind of poor, since we don't actually /clone/
// instances; instead, we use the supplied make() attribute. So,
// the underlying class must know how to deal with arguments.
// With the old implementation of Enum, that ignored its
// arguments when handling a make dispatch, the IAlign
// definition wouldn't work.
// pseudo-types, must be instantiated via shorthand
$this->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();
$this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength();
$this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens();
$this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels();
$this->info['Text'] = new HTMLPurifier_AttrDef_Text();
$this->info['URI'] = new HTMLPurifier_AttrDef_URI();
$this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang();
$this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color();
$this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right');
$this->info['LAlign'] = self::makeEnum('top,bottom,left,right');
$this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget();
// 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);
}
private static function makeEnum($in) {
return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in)));
}
/**
* Retrieves a type
* @param $type String type name
* @return Object AttrDef for 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
*/
public function set($type, $impl) {
$this->info[$type] = $impl;
}
}
/**
* Validates the attributes of a token. Doesn't manage required attributes
* very well. The only reason we factored this out was because RemoveForeignElements
* also needed it besides ValidateAttributes.
*/
class HTMLPurifier_AttrValidator
{
/**
* Validates the attributes of a token, returning a modified token
* that has valid tokens
* @param $token Reference to token to validate. We require a reference
* because the operation this class performs on the token are
* not atomic, so the context CurrentToken to be updated
* throughout
* @param $config Instance of HTMLPurifier_Config
* @param $context Instance of HTMLPurifier_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 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) {
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) {
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
if ($defs[$attr_key] === false) {
// We've explicitly been told not to allow this element.
// This is usually when there's a global definition
// that must be overridden.
// Theoretically speaking, we could have a
// AttrDef_DenyAll, but this is faster!
$result = false;
} else {
// validate according to the element's definition
$result = $defs[$attr_key]->validate(
$value, $config, $context
);
}
} elseif ( isset($d_defs[$attr_key]) ) {
// there is a global definition defined, validate according
// to the global definition
$result = $d_defs[$attr_key]->validate(
$value, $config, $context
);
} else {
// 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) {
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) {
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');
}
}
// constants are slow, so we use as few as possible
if (!defined('HTMLPURIFIER_PREFIX')) {
define('HTMLPURIFIER_PREFIX', dirname(__FILE__) . '/standalone');
set_include_path(HTMLPURIFIER_PREFIX . PATH_SEPARATOR . get_include_path());
}
// accomodations for versions earlier than 5.0.2
// borrowed from PHP_Compat, LGPL licensed, by Aidan Lister
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;
// Technically speaking, it should be ok and more efficient to
// just do 'require', but Antonio Parraga reports that with
// Zend extensions such as Zend debugger and APC, this invariant
// may be broken. Since we have efficient alternatives, pay
// the cost here and avoid the bug.
require_once 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')) {
if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
// prepend flag exists, no need for shenanigans
spl_autoload_register($autoload, true, true);
} else {
$buggy = version_compare(PHP_VERSION, '5.2.11', '<');
$compat = version_compare(PHP_VERSION, '5.1.2', '<=') &&
version_compare(PHP_VERSION, '5.1.0', '>=');
foreach ($funcs as $func) {
if ($buggy && 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);
}
}
}
}
/**
* Super-class for definition datatype objects, implements serialization
* functions for the class.
*/
abstract class HTMLPurifier_Definition
{
/**
* Has setup() been called yet?
*/
public $setup = false;
/**
* If true, write out the final definition object to the cache after
* setup. This will be true only if all invocations to get a raw
* definition object are also optimized. This does not cause file
* system thrashing because on subsequent calls the cached object
* is used and any writes to the raw definition object are short
* circuited. See enduser-customize.html for the high-level
* picture.
*/
public $optimized = null;
/**
* What type of definition is it?
*/
public $type;
/**
* Sets up the definition object into the final form, something
* not done by the constructor
* @param $config HTMLPurifier_Config instance
*/
abstract protected function doSetup($config);
/**
* Setup function that aborts if already setup
* @param $config HTMLPurifier_Config instance
*/
public function setup($config) {
if ($this->setup) return;
$this->setup = true;
$this->doSetup($config);
}
}
/**
* Defines allowed CSS attributes and what their values are.
* @see HTMLPurifier_HTMLDefinition
*/
class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
{
public $type = 'CSS';
/**
* Assoc array of attribute name to definition object.
*/
public $info = array();
/**
* Constructs the info array. The meat of this class.
*/
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-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(
array('none', 'left', 'right'), false);
$this->info['font-style'] = new HTMLPurifier_AttrDef_Enum(
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')
);
$this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum(
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'] =
$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'] =
$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',
'larger', 'smaller')),
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-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-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');
$this->info['width'] =
$this->info['height'] =
$max === null ?
$trusted_wh :
new HTMLPurifier_AttrDef_Switch('img',
// For img tags:
new HTMLPurifier_AttrDef_CSS_Composite(array(
new HTMLPurifier_AttrDef_CSS_Length('0', $max),
new HTMLPurifier_AttrDef_Enum(array('auto'))
)),
// 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-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);
// These CSS properties don't work on many browsers, but we live
// in THE FUTURE!
$this->info['white-space'] = new HTMLPurifier_AttrDef_Enum(array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line'));
if ($config->get('CSS.Proprietary')) {
$this->doSetupProprietary($config);
}
if ($config->get('CSS.AllowTricky')) {
$this->doSetupTricky($config);
}
if ($config->get('CSS.Trusted')) {
$this->doSetupTrusted($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();
// more CSS3
$this->info['page-break-after'] =
$this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum(array('auto','always','avoid','left','right'));
$this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto','avoid'));
}
protected function doSetupTricky($config) {
$this->info['display'] = new HTMLPurifier_AttrDef_Enum(array(
'inline', 'block', 'list-item', 'run-in', 'compact',
'marker', 'table', 'inline-block', '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'));
}
protected function doSetupTrusted($config) {
$this->info['position'] = new HTMLPurifier_AttrDef_Enum(array(
'static', 'relative', 'absolute', 'fixed'
));
$this->info['top'] =
$this->info['left'] =
$this->info['right'] =
$this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
new HTMLPurifier_AttrDef_CSS_Length(),
new HTMLPurifier_AttrDef_CSS_Percentage(),
new HTMLPurifier_AttrDef_Enum(array('auto')),
));
$this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
new HTMLPurifier_AttrDef_Integer(),
new HTMLPurifier_AttrDef_Enum(array('auto')),
));
}
/**
* 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_properties = $config->get('CSS.AllowedProperties');
if ($allowed_properties !== null) {
foreach ($this->info as $name => $d) {
if(!isset($allowed_properties[$name])) unset($this->info[$name]);
unset($allowed_properties[$name]);
}
// emit errors
foreach ($allowed_properties 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);
}
}
$forbidden_properties = $config->get('CSS.ForbiddenProperties');
if ($forbidden_properties !== null) {
foreach ($this->info as $name => $d) {
if (isset($forbidden_properties[$name])) {
unset($this->info[$name]);
}
}
}
}
}
/**
* Defines allowed child nodes and validates tokens against it.
*/
abstract class HTMLPurifier_ChildDef
{
/**
* Type of child definition, usually right-most part of class name lowercase.
* Used occasionally in terms of context.
*/
public $type;
/**
* Bool that indicates whether or not an empty array of children is okay
*
* This is necessary for redundant checking when changes affecting
* a child node may cause a parent node to now be disallowed.
*/
public $allow_empty;
/**
* Lookup array of all elements that this definition could possibly allow
*/
public $elements = array();
/**
* Get lookup of tag names that should not close this element automatically.
* All other elements will do so.
*/
public function getAllowedElements($config) {
return $this->elements;
}
/**
* Validates nodes according to definition and returns modification.
*
* @param $tokens_of_children Array of HTMLPurifier_Token
* @param $config HTMLPurifier_Config object
* @param $context HTMLPurifier_Context object
* @return bool true to leave nodes as is
* @return bool false to remove parent node
* @return array of replacement child tokens
*/
abstract public function validateChildren($tokens_of_children, $config, $context);
}
/**
* 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
*/
public $version = '4.5.0';
/**
* Bool indicator whether or not to automatically finalize
* the object if a read operation is done
*/
public $autoFinalize = true;
// protected member variables
/**
* Namespace indexed array of serials for specific namespaces (see
* getSerial() for more info).
*/
protected $serials = array();
/**
* Serial for entire configuration object
*/
protected $serial;
/**
* Parser for variables
*/
protected $parser = null;
/**
* 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). This will also compress some errors
* and exceptions.
*/
public $chatty = true;
/**
* Current lock; only gets to this namespace are allowed.
*/
private $lock;
/**
* @param $definition HTMLPurifier_ConfigSchema that defines what directives
* are allowed.
*/
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
* @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
*/
public static function create($config, $schema = null) {
if ($config instanceof HTMLPurifier_Config) {
// pass-through
return $config;
}
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.
* @return Default HTMLPurifier_Config object.
*/
public static function createDefault() {
$definition = HTMLPurifier_ConfigSchema::instance();
$config = new HTMLPurifier_Config($definition);
return $config;
}
/**
* Retreives a value from the configuration.
* @param $key String 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
$this->triggerError('Cannot retrieve value of undefined directive ' . htmlspecialchars($key),
E_USER_WARNING);
return;
}
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;
}
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
*/
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 $full[$namespace];
}
/**
* Returns a SHA-1 signature of a segment of the configuration object
* that uniquely identifies that particular configuration
* @note Revision is handled specially and is removed from the batch
* before processing!
* @param $namespace Namespace to get serial for
*/
public function getBatchSerial($namespace) {
if (empty($this->serials[$namespace])) {
$batch = $this->getBatch($namespace);
unset($batch['DefinitionRev']);
$this->serials[$namespace] = sha1(serialize($batch));
}
return $this->serials[$namespace];
}
/**
* Returns a SHA-1 signature for the entire configuration object
* that uniquely identifies that particular configuration
*/
public function getSerial() {
if (empty($this->serial)) {
$this->serial = sha1(serialize($this->getAll()));
}
return $this->serial;
}
/**
* Retrieves all directives, organized by namespace
* @warning This is a pretty inefficient function, avoid if you can
*/
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 $key String key
* @param $value Mixed value
*/
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[$key])) {
$this->triggerError('Cannot set undefined directive ' . htmlspecialchars($key) . ' to value',
E_USER_WARNING);
return;
}
$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->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;
}
// 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($def->aliases[$value])) {
$value = $def->aliases[$value];
}
// 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;
}
}
$this->plist->set($key, $value);
// reset definitions if the directives they depend on changed
// this is a very costly process, so it's discouraged
// with finalization
if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') {
$this->definitions[$namespace] = null;
}
$this->serials[$namespace] = false;
}
/**
* Convenience function for error reporting
*/
private function _listify($lookup) {
$list = array();
foreach ($lookup as $name => $b) $list[] = $name;
return implode(', ', $list);
}
/**
* 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.
* @param $optimized If true, this method may return null, to
* indicate that a cached version of the modified
* definition object is available and no further edits
* are necessary. Consider using
* maybeGetRawHTMLDefinition, which is more explicitly
* named, instead.
*/
public function getHTMLDefinition($raw = false, $optimized = false) {
return $this->getDefinition('HTML', $raw, $optimized);
}
/**
* 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.
* @param $optimized If true, this method may return null, to
* indicate that a cached version of the modified
* definition object is available and no further edits
* are necessary. Consider using
* maybeGetRawCSSDefinition, which is more explicitly
* named, instead.
*/
public function getCSSDefinition($raw = false, $optimized = false) {
return $this->getDefinition('CSS', $raw, $optimized);
}
/**
* Retrieves object reference to the URI definition
* @param $raw Return a copy that has not been setup yet. Must be
* called before it's been setup, otherwise won't work.
* @param $optimized If true, this method may return null, to
* indicate that a cached version of the modified
* definition object is available and no further edits
* are necessary. Consider using
* maybeGetRawURIDefinition, which is more explicitly
* named, instead.
*/
public function getURIDefinition($raw = false, $optimized = false) {
return $this->getDefinition('URI', $raw, $optimized);
}
/**
* Retrieves a definition
* @param $type Type of definition: HTML, CSS, etc
* @param $raw Whether or not definition should be returned raw
* @param $optimized Only has an effect when $raw is true. Whether
* or not to return null if the result is already present in
* the cache. This is off by default for backwards
* compatibility reasons, but you need to do things this
* way in order to ensure that caching is done properly.
* Check out enduser-customize.html for more details.
* We probably won't ever change this default, as much as the
* maybe semantics is the "right thing to do."
*/
public function getDefinition($type, $raw = false, $optimized = false) {
if ($optimized && !$raw) {
throw new HTMLPurifier_Exception("Cannot set optimized = true when 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) {
// full definition
// ---------------
// check if definition is in memory
if (!empty($this->definitions[$type])) {
$def = $this->definitions[$type];
// check if the definition is setup
if ($def->setup) {
return $def;
} else {
$def->setup($this);
if ($def->optimized) $cache->add($def, $this);
return $def;
}
}
// check if definition is in cache
$def = $cache->get($this);
if ($def) {
// definition in cache, save to memory and return it
$this->definitions[$type] = $def;
return $def;
}
// initialize it
$def = $this->initDefinition($type);
// set it up
$this->lock = $type;
$def->setup($this);
$this->lock = null;
// save in cache
$cache->add($def, $this);
// return it
return $def;
} else {
// raw definition
// --------------
// check preconditions
$def = null;
if ($optimized) {
if (is_null($this->get($type . '.DefinitionID'))) {
// fatally error out if definition ID not set
throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID");
}
}
if (!empty($this->definitions[$type])) {
$def = $this->definitions[$type];
if ($def->setup && !$optimized) {
$extra = $this->chatty ? " (try moving this code block earlier in your initialization)" : "";
throw new HTMLPurifier_Exception("Cannot retrieve raw definition after it has already been setup" . $extra);
}
if ($def->optimized === null) {
$extra = $this->chatty ? " (try flushing your cache)" : "";
throw new HTMLPurifier_Exception("Optimization status of definition is unknown" . $extra);
}
if ($def->optimized !== $optimized) {
$msg = $optimized ? "optimized" : "unoptimized";
$extra = $this->chatty ? " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" : "";
throw new HTMLPurifier_Exception("Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra);
}
}
// check if definition was in memory
if ($def) {
if ($def->setup) {
// invariant: $optimized === true (checked above)
return null;
} else {
return $def;
}
}
// if optimized, check if definition was in cache
// (because we do the memory check first, this formulation
// is prone to cache slamming, but I think
// guaranteeing that either /all/ of the raw
// setup code or /none/ of it is run is more important.)
if ($optimized) {
// This code path only gets run once; once we put
// something in $definitions (which is guaranteed by the
// trailing code), we always short-circuit above.
$def = $cache->get($this);
if ($def) {
// save the full definition for later, but don't
// return it yet
$this->definitions[$type] = $def;
return null;
}
}
// check invariants for creation
if (!$optimized) {
if (!is_null($this->get($type . '.DefinitionID'))) {
if ($this->chatty) {
$this->triggerError("Due to a documentation error in previous version of HTML Purifier, your definitions are not being cached. If this is OK, you can remove the %$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, modify your code to use maybeGetRawDefinition, and test if the returned value is null before making any edits (if it is null, that means that a cached version is available, and no raw operations are necessary). See Customize for more details", E_USER_WARNING);
} else {
$this->triggerError("Useless DefinitionID declaration", E_USER_WARNING);
}
}
}
// initialize it
$def = $this->initDefinition($type);
$def->optimized = $optimized;
return $def;
}
throw new HTMLPurifier_Exception("The impossible happened!");
}
private function initDefinition($type) {
// quick checks failed, let's create the object
if ($type == 'HTML') {
$def = new HTMLPurifier_HTMLDefinition();
} elseif ($type == 'CSS') {
$def = new HTMLPurifier_CSSDefinition();
} elseif ($type == 'URI') {
$def = new HTMLPurifier_URIDefinition();
} else {
throw new HTMLPurifier_Exception("Definition of $type type not supported");
}
$this->definitions[$type] = $def;
return $def;
}
public function maybeGetRawDefinition($name) {
return $this->getDefinition($name, true, true);
}
public function maybeGetRawHTMLDefinition() {
return $this->getDefinition('HTML', true, true);
}
public function maybeGetRawCSSDefinition() {
return $this->getDefinition('CSS', true, true);
}
public function maybeGetRawURIDefinition() {
return $this->getDefinition('URI', true, true);
}
/**
* Loads configuration values from an array with the following structure:
* Namespace.Directive => Value
* @param $config_array Configuration associative 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) {
$this->set($key, $value);
} else {
$namespace = $key;
$namespace_values = $value;
foreach ($namespace_values as $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
*/
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();
$allowed_directives = array();
$blacklisted_directives = array();
foreach ($allowed as $ns_or_directive) {
if (strpos($ns_or_directive, '.') !== false) {
// directive
if ($ns_or_directive[0] == '-') {
$blacklisted_directives[substr($ns_or_directive, 1)] = true;
} else {
$allowed_directives[$ns_or_directive] = true;
}
} else {
// namespace
$allowed_ns[$ns_or_directive] = true;
}
}
}
$ret = array();
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 $mq_fix Boolean whether or not to enable magic quotes fix
* @param $schema Instance of HTMLPurifier_ConfigSchema to use, if not global copy
*/
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
*/
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
*/
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;
$skey = "$ns.$directive";
if (!empty($array["Null_$skey"])) {
$ret[$ns][$directive] = null;
continue;
}
if (!isset($array[$skey])) continue;
$value = $mq ? stripslashes($array[$skey]) : $array[$skey];
$ret[$ns][$directive] = $value;
}
return $ret;
}
/**
* Loads configuration values from an ini file
* @param $filename Name of ini file
*/
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
*/
public function isFinalized($error = false) {
if ($this->finalized && $error) {
$this->triggerError($error, E_USER_ERROR);
}
return $this->finalized;
}
/**
* Finalizes configuration only if auto finalize is on and not
* already finalized
*/
public function autoFinalize() {
if ($this->autoFinalize) {
$this->finalize();
} else {
$this->plist->squash(true);
}
}
/**
* Finalizes a configuration object, prohibiting further change
*/
public function finalize() {
$this->finalized = true;
$this->parser = null;
}
/**
* Produces a nicely formatted error message by supplying the
* stack frame information OUTSIDE of HTMLPurifier_Config.
*/
protected function triggerError($msg, $no) {
// determine previous stack frame
$extra = '';
if ($this->chatty) {
$trace = debug_backtrace();
// zip(tail(trace), trace) -- but PHP is not Haskell har har
for ($i = 0, $c = count($trace); $i < $c - 1; $i++) {
// XXX this is not correct on some versions of HTML Purifier
if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') {
continue;
}
$frame = $trace[$i];
$extra = " invoked on line {$frame['line']} in file {$frame['file']}";
break;
}
}
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);
}
}
/**
* Configuration definition, defines directives and their defaults.
*/
class HTMLPurifier_ConfigSchema {
/**
* Defaults of the directives and namespaces.
* @note This shares the exact same structure as HTMLPurifier_Config::$conf
*/
public $defaults = array();
/**
* The default property list. Do not edit this property list.
*/
public $defaultPlist;
/**
* Definition of the directives. The structure of this is:
*
* array(
* 'Namespace' => 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.
*/
public $info = array();
/**
* Application-wide singleton
*/
static protected $singleton;
public function __construct() {
$this->defaultPlist = new HTMLPurifier_PropertyList();
}
/**
* Unserializes the default ConfigSchema.
*/
public static function makeFromSerial() {
$contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser');
$r = unserialize($contents);
if (!$r) {
$hash = sha1($contents);
trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR);
}
return $r;
}
/**
* Retrieves an instance of the application-wide configuration definition.
*/
public static function instance($prototype = null) {
if ($prototype !== null) {
HTMLPurifier_ConfigSchema::$singleton = $prototype;
} elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) {
HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial();
}
return HTMLPurifier_ConfigSchema::$singleton;
}
/**
* Defines a directive for configuration
* @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 $allow_null Whether or not to allow null values
*/
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 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.
* @param $namespace Directive's namespace
* @param $name Name of Directive
* @param $aliases Hash of aliased values to the real alias
*/
public function addValueAliases($key, $aliases) {
if (!isset($this->info[$key]->aliases)) {
$this->info[$key]->aliases = array();
}
foreach ($aliases as $alias => $real) {
$this->info[$key]->aliases[$alias] = $real;
}
}
/**
* Defines a set of allowed values for a directive.
* @warning This is slightly different from the corresponding static
* method definition.
* @param $namespace Namespace of directive
* @param $name Name of directive
* @param $allowed Lookup array of allowed values
*/
public function addAllowedValues($key, $allowed) {
$this->info[$key]->allowed = $allowed;
}
/**
* Defines a directive alias for backwards compatibility
* @param $namespace
* @param $name Directive that will be aliased
* @param $new_namespace
* @param $new_name Directive that the alias will be to
*/
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;
}
}
}
}
/**
* @todo Unit test
*/
class HTMLPurifier_ContentSets
{
/**
* List of content set strings (pipe seperators) indexed by name.
*/
public $info = array();
/**
* List of content set lookups (element => true) indexed by name.
* @note This is in HTMLPurifier_HTMLDefinition->info_content_sets
*/
public $lookup = array();
/**
* Synchronized list of defined content sets (keys of info)
*/
protected $keys = array();
/**
* Synchronized list of defined content values (values of info)
*/
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
*/
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) {
$temp = $this->convertToLookup($value);
if (isset($this->lookup[$key])) {
// add it into the existing content set
$this->lookup[$key] = array_merge($this->lookup[$key], $temp);
} else {
$this->lookup[$key] = $temp;
}
}
}
$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);
}
/**
* Accepts a definition; generates and assigns a ChildDef for it
* @param $def HTMLPurifier_ElementDef reference
* @param $module Module that defined the ElementDef
*/
public function generateChildDef(&$def, $module) {
if (!empty($def->child)) return; // already done!
$content_model = $def->content_model;
if (is_string($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
* @note This will also defer to modules for custom HTMLPurifier_ChildDef
* subclasses that need content set expansion
* @param $def HTMLPurifier_ElementDef to have ChildDef extracted
* @return HTMLPurifier_ChildDef corresponding to ElementDef
*/
public function getChildDef($def, $module) {
$value = $def->content_model;
if (is_object($value)) {
trigger_error(
'Literal object child definitions should be stored in '.
'ElementDef->child not ElementDef->content_model',
E_USER_NOTICE
);
return $value;
}
switch ($def->content_model_type) {
case 'required':
return new HTMLPurifier_ChildDef_Required($value);
case 'optional':
return new HTMLPurifier_ChildDef_Optional($value);
case 'empty':
return new HTMLPurifier_ChildDef_Empty();
case 'custom':
return new HTMLPurifier_ChildDef_Custom($value);
}
// defer to its module
$return = false;
if ($module->defines_child_def) { // save a func call
$return = $module->getChildDef($def);
}
if ($return !== false) return $return;
// error-out
trigger_error(
'Could not determine which ChildDef class to instantiate',
E_USER_ERROR
);
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
*/
protected function convertToLookup($string) {
$array = explode('|', str_replace(' ', '', $string));
$ret = array();
foreach ($array as $i => $k) {
$ret[$k] = true;
}
return $ret;
}
}
/**
* 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 $_storage = array();
/**
* Registers a variable into the context.
* @param $name String name
* @param $ref Reference to variable to be registered
*/
public function register($name, &$ref) {
if (isset($this->_storage[$name])) {
trigger_error("Name $name produces collision, cannot re-register",
E_USER_ERROR);
return;
}
$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
*/
public function &get($name, $ignore_error = false) {
if (!isset($this->_storage[$name])) {
if (!$ignore_error) {
trigger_error("Attempted to retrieve non-existent variable $name",
E_USER_ERROR);
}
$var = null; // so we can return by reference
return $var;
}
return $this->_storage[$name];
}
/**
* Destorys a variable in the context.
* @param $name String name
*/
public function destroy($name) {
if (!isset($this->_storage[$name])) {
trigger_error("Attempted to destroy non-existent variable $name",
E_USER_ERROR);
return;
}
unset($this->_storage[$name]);
}
/**
* Checks whether or not the variable exists.
* @param $name String 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
*/
public function loadArray($context_array) {
foreach ($context_array as $key => $discard) {
$this->register($key, $context_array[$key]);
}
}
}
/**
* Abstract class representing Definition cache managers that implements
* useful common methods and is a factory.
* @todo Create a separate maintenance file advanced users can use to
* cache their custom HTMLDefinition, which can be loaded
* via a configuration directive
* @todo Implement memcached
*/
abstract class HTMLPurifier_DefinitionCache
{
public $type;
/**
* @param $name Type of definition objects this instance of the
* cache will handle.
*/
public function __construct($type) {
$this->type = $type;
}
/**
* Generates a unique identifier for a particular configuration
* @param Instance of HTMLPurifier_Config
*/
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
*/
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')
) 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
*/
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
*/
abstract public function add($def, $config);
/**
* Unconditionally saves a definition object to the cache
*/
abstract public function set($def, $config);
/**
* Replace an object in the cache
*/
abstract public function replace($def, $config);
/**
* Retrieves a definition object from the cache
*/
abstract public function get($config);
/**
* Removes a definition object to the cache
*/
abstract public function remove($config);
/**
* Clears all objects from cache
*/
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.
*/
abstract public function cleanup($config);
}
/**
* Responsible for creating definition caches.
*/
class HTMLPurifier_DefinitionCacheFactory
{
protected $caches = array('Serializer' => array());
protected $implementations = array();
protected $decorators = array();
/**
* Initialize default decorators
*/
public function setup() {
$this->addDecorator('Cleanup');
}
/**
* Retrieves an instance of global definition cache factory.
*/
public static function instance($prototype = null) {
static $instance;
if ($prototype !== null) {
$instance = $prototype;
} elseif ($instance === null || $prototype === true) {
$instance = new HTMLPurifier_DefinitionCacheFactory();
$instance->setup();
}
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
*/
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
*/
public function create($type, $config) {
$method = $config->get('Cache.DefinitionImpl');
if ($method === 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], false)
) {
$cache = new $class($type);
} else {
if ($method != 'Serializer') {
trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING);
}
$cache = new HTMLPurifier_DefinitionCache_Serializer($type);
}
foreach ($this->decorators as $decorator) {
$new_cache = $decorator->decorate($cache);
// prevent infinite recursion in PHP 4
unset($cache);
$cache = $new_cache;
}
$this->caches[$method][$type] = $cache;
return $this->caches[$method][$type];
}
/**
* Registers a decorator to add to all new cache objects
* @param
*/
public function addDecorator($decorator) {
if (is_string($decorator)) {
$class = "HTMLPurifier_DefinitionCache_Decorator_$decorator";
$decorator = new $class;
}
$this->decorators[$decorator->name] = $decorator;
}
}
/**
* Represents a document type, contains information on which modules
* need to be loaded.
* @note This class is inspected by Printer_HTMLDefinition->renderDoctype.
* If structure changes, please update that function.
*/
class HTMLPurifier_Doctype
{
/**
* Full name of doctype
*/
public $name;
/**
* List of standard modules (string identifiers or literal objects)
* that this doctype uses
*/
public $modules = array();
/**
* List of modules to use for tidying up code
*/
public $tidyModules = array();
/**
* Is the language derived from XML (i.e. XHTML)?
*/
public $xml = true;
/**
* List of aliases for this doctype
*/
public $aliases = array();
/**
* Public DTD identifier
*/
public $dtdPublic;
/**
* System DTD identifier
*/
public $dtdSystem;
public function __construct($name = null, $xml = true, $modules = array(),
$tidyModules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
) {
$this->name = $name;
$this->xml = $xml;
$this->modules = $modules;
$this->tidyModules = $tidyModules;
$this->aliases = $aliases;
$this->dtdPublic = $dtd_public;
$this->dtdSystem = $dtd_system;
}
}
class HTMLPurifier_DoctypeRegistry
{
/**
* Hash of doctype names to doctype objects
*/
protected $doctypes;
/**
* Lookup table of aliases to real doctype names
*/
protected $aliases;
/**
* Registers a doctype to the registry
* @note Accepts a fully-formed doctype object, or the
* parameters for constructing a doctype object
* @param $doctype Name of doctype or literal doctype object
* @param $modules Modules doctype will load
* @param $modules_for_modes Modules doctype will load for certain modes
* @param $aliases Alias names for doctype
* @return Editable registered doctype
*/
public function register($doctype, $xml = true, $modules = array(),
$tidy_modules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
) {
if (!is_array($modules)) $modules = array($modules);
if (!is_array($tidy_modules)) $tidy_modules = array($tidy_modules);
if (!is_array($aliases)) $aliases = array($aliases);
if (!is_object($doctype)) {
$doctype = new HTMLPurifier_Doctype(
$doctype, $xml, $modules, $tidy_modules, $aliases, $dtd_public, $dtd_system
);
}
$this->doctypes[$doctype->name] = $doctype;
$name = $doctype->name;
// hookup aliases
foreach ($doctype->aliases as $alias) {
if (isset($this->doctypes[$alias])) continue;
$this->aliases[$alias] = $name;
}
// remove old aliases
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 Editable doctype object
*/
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);
$anon = new HTMLPurifier_Doctype($doctype);
return $anon;
}
return $this->doctypes[$doctype];
}
/**
* Creates a doctype based on a configuration object,
* will perform initialization on the doctype
* @note Use this function to get a copy of doctype that config
* can hold on to (this is necessary in order to tell
* Generator whether or not the current document is XML
* based or not).
*/
public function make($config) {
return clone $this->get($this->getDoctypeFromConfig($config));
}
/**
* Retrieves the doctype from the configuration object
*/
public function getDoctypeFromConfig($config) {
// recommended test
$doctype = $config->get('HTML.Doctype');
if (!empty($doctype)) return $doctype;
$doctype = $config->get('HTML.CustomDoctype');
if (!empty($doctype)) return $doctype;
// backwards-compatibility
if ($config->get('HTML.XHTML')) {
$doctype = 'XHTML 1.0';
} else {
$doctype = 'HTML 4.01';
}
if ($config->get('HTML.Strict')) {
$doctype .= ' Strict';
} else {
$doctype .= ' Transitional';
}
return $doctype;
}
}
/**
* Structure that stores an HTML element definition. Used by
* 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?
*/
public $standalone = true;
/**
* Associative array of attribute name to HTMLPurifier_AttrDef
* @note Before being processed by HTMLPurifier_AttrCollections
* when modules are finalized during
* HTMLPurifier_HTMLDefinition->setup(), this array may also
* contain an array at index 0 that indicates which attribute
* collections to load into the full array. It may also
* contain string indentifiers in lieu of HTMLPurifier_AttrDef,
* see HTMLPurifier_AttrTypes on how they are expanded during
* HTMLPurifier_HTMLDefinition->setup() processing.
*/
public $attr = array();
// XXX: Design note: currently, it's not possible to override
// previously defined AttrTransforms without messing around with
// the final generated config. This is by design; a previous version
// used an associated list of attr_transform, but it was extremely
// easy to accidentally override other attribute transforms by
// forgetting to specify an index (and just using 0.) While we
// could check this by checking the index number and complaining,
// there is a second problem which is that it is not at all easy to
// tell when something is getting overridden. Combine this with a
// codebase where this isn't really being used, and it's perfect for
// nuking.
/**
* List of tags HTMLPurifier_AttrTransform to be done before validation
*/
public $attr_transform_pre = array();
/**
* List of tags HTMLPurifier_AttrTransform to be done after validation
*/
public $attr_transform_post = array();
/**
* HTMLPurifier_ChildDef of this tag.
*/
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 $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 $content_model_type;
/**
* Does the element have a content model (#PCDATA | Inline)*? This
* is important for chameleon ins and del processing in
* HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't
* have to worry about this one.
*/
public $descendants_are_inline = false;
/**
* List of the names of required attributes this element has. Dynamically
* populated by HTMLPurifier_HTMLDefinition::getElement
*/
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
* not possible with DTDs or XML Schemas. W3C has elected to
* use complicated compositions of content_models to simulate
* exclusion for children, but we go the simpler, SGML-style
* route of flat-out exclusions, which correctly apply to
* all descendants and not just children. Note that the XHTML
* Modularization Abstract Modules are blithely unaware of such
* distinctions.
*/
public $excludes = array();
/**
* This tag is explicitly auto-closed by the following tags.
*/
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
*/
public static function create($content_model, $content_model_type, $attr) {
$def = new HTMLPurifier_ElementDef();
$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.
*/
public function mergeIn($def) {
// later keys takes precedence
foreach($def->attr as $k => $v) {
if ($k === 0) {
// merge in the includes
// sorry, no way to override an include
foreach ($v as $v2) {
$this->attr[0][] = $v2;
}
continue;
}
if ($v === false) {
if (isset($this->attr[$k])) unset($this->attr[$k]);
continue;
}
$this->attr[$k] = $v;
}
$this->_mergeAssocArray($this->excludes, $def->excludes);
$this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre);
$this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post);
if(!empty($def->content_model)) {
$this->content_model =
str_replace("#SUPER", $this->content_model, $def->content_model);
$this->child = false;
}
if(!empty($def->content_model_type)) {
$this->content_model_type = $def->content_model_type;
$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;
}
/**
* 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
*/
private function _mergeAssocArray(&$a1, $a2) {
foreach ($a2 as $k => $v) {
if ($v === false) {
if (isset($a1[$k])) unset($a1[$k]);
continue;
}
$a1[$k] = $v;
}
}
}
/**
* 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
*/
private function __construct() {
trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR);
}
/**
* Error-handler that mutes errors, alternative to shut-up operator.
*/
public static function muteErrorHandler() {}
/**
* iconv wrapper which mutes errors, but doesn't work around bugs.
*/
public static function unsafeIconv($in, $out, $text) {
set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
$r = iconv($in, $out, $text);
restore_error_handler();
return $r;
}
/**
* iconv wrapper which mutes errors and works around bugs.
*/
public static function iconv($in, $out, $text, $max_chunk_size = 8000) {
$code = self::testIconvTruncateBug();
if ($code == self::ICONV_OK) {
return self::unsafeIconv($in, $out, $text);
} elseif ($code == self::ICONV_TRUNCATES) {
// we can only work around this if the input character set
// is utf-8
if ($in == 'utf-8') {
if ($max_chunk_size < 4) {
trigger_error('max_chunk_size is too small', E_USER_WARNING);
return false;
}
// split into 8000 byte chunks, but be careful to handle
// multibyte boundaries properly
if (($c = strlen($text)) <= $max_chunk_size) {
return self::unsafeIconv($in, $out, $text);
}
$r = '';
$i = 0;
while (true) {
if ($i + $max_chunk_size >= $c) {
$r .= self::unsafeIconv($in, $out, substr($text, $i));
break;
}
// wibble the boundary
if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) {
$chunk_size = $max_chunk_size;
} elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) {
$chunk_size = $max_chunk_size - 1;
} elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) {
$chunk_size = $max_chunk_size - 2;
} elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) {
$chunk_size = $max_chunk_size - 3;
} else {
return false; // rather confusing UTF-8...
}
$chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths
$r .= self::unsafeIconv($in, $out, $chunk);
$i += $chunk_size;
}
return $r;
} else {
return false;
}
} else {
return false;
}
}
/**
* 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.
*
* @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,
* the original code transformed UTF-8 text into an array of integer
* Unicode codepoints. Understandably, transforming that back to
* a string would be somewhat expensive, so the function was modded to
* directly operate on the string. However, this discourages code
* reuse, and the logic enumerated here would be useful for any
* function that needs to be able to understand UTF-8 characters.
* As of right now, only smart lossless character encoding converters
* would need that, and I'm probably not going to implement them.
* Once again, PHP 6 should solve all our problems.
*/
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.
// The regexp matches the XML char production, as well as well as excluding
// non-SGML codepoints U+007F to U+009F
if (preg_match('/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', $str)) {
return $str;
}
$mState = 0; // cached expected number of octets after the current octet
// 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
// or a multi-octet sequence.
if (0 == (0x80 & ($in))) {
// US-ASCII, pass straight through.
if (($in <= 31 || $in == 127) &&
!($in == 9 || $in == 13 || $in == 10) // save \r\t\n
) {
// control characters, remove
} else {
$out .= $char;
}
// reset
$char = '';
$mBytes = 1;
} elseif (0xC0 == (0xE0 & ($in))) {
// First octet of 2 octet sequence
$mUcs4 = ($in);
$mUcs4 = ($mUcs4 & 0x1F) << 6;
$mState = 1;
$mBytes = 2;
} elseif (0xE0 == (0xF0 & ($in))) {
// First octet of 3 octet sequence
$mUcs4 = ($in);
$mUcs4 = ($mUcs4 & 0x0F) << 12;
$mState = 2;
$mBytes = 3;
} elseif (0xF0 == (0xF8 & ($in))) {
// First octet of 4 octet sequence
$mUcs4 = ($in);
$mUcs4 = ($mUcs4 & 0x07) << 18;
$mState = 3;
$mBytes = 4;
} elseif (0xF8 == (0xFC & ($in))) {
// First octet of 5 octet sequence.
//
// 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
// until the end of the sequence and let the later error
// handling code catch it.
$mUcs4 = ($in);
$mUcs4 = ($mUcs4 & 0x03) << 24;
$mState = 4;
$mBytes = 5;
} elseif (0xFC == (0xFE & ($in))) {
// First octet of 6 octet sequence, see comments for 5
// octet sequence.
$mUcs4 = ($in);
$mUcs4 = ($mUcs4 & 1) << 30;
$mState = 5;
$mBytes = 6;
} else {
// Current octet is neither in the US-ASCII range nor a
// legal first octet of a multi-octet sequence.
$mState = 0;
$mUcs4 = 0;
$mBytes = 1;
$char = '';
}
} else {
// When mState is non-zero, we expect a continuation of the
// multi-octet sequence
if (0x80 == (0xC0 & ($in))) {
// Legal continuation.
$shift = ($mState - 1) * 6;
$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)) ||
((4 == $mBytes) && ($mUcs4 < 0x10000)) ||
(4 < $mBytes) ||
// From Unicode 3.2, surrogate characters = illegal
(($mUcs4 & 0xFFFFF800) == 0xD800) ||
// Codepoints outside the Unicode range are illegal
($mUcs4 > 0x10FFFF)
) {
} elseif (0xFEFF != $mUcs4 && // omit BOM
// check for valid Char unicode codepoints
(
0x9 == $mUcs4 ||
0xA == $mUcs4 ||
0xD == $mUcs4 ||
(0x20 <= $mUcs4 && 0x7E >= $mUcs4) ||
// 7F-9F is not strictly prohibited by XML,
// but it is non-SGML, and thus we don't allow it
(0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) ||
(0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4)
)
) {
$out .= $char;
}
// initialize UTF8 cache (reset)
$mState = 0;
$mUcs4 = 0;
$mBytes = 1;
$char = '';
}
} else {
// ((0xC0 & (*in) != 0x80) && (mState != 0))
// Incomplete multi-octet sequence.
// used to result in complete fail, but we'll reset
$mState = 0;
$mUcs4 = 0;
$mBytes = 1;
$char ='';
}
}
}
return $out;
}
/**
* Translates a Unicode codepoint into its corresponding UTF-8 character.
* @note Based on Feyd's function at
* ,
* which is in public domain.
* @note While we're going to do code point parsing anyway, a good
* optimization would be to refuse to translate code points that
* are non-SGML characters. However, this could lead to duplication.
* @note This is very similar to the unichr function in
* maintenance/generate-entity-file.php (although this is superior,
* due to its sanity checks).
*/
// +----------+----------+----------+----------+
// | 33222222 | 22221111 | 111111 | |
// | 10987654 | 32109876 | 54321098 | 76543210 | bit
// +----------+----------+----------+----------+
// | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F
// | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF
// | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF
// | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF
// +----------+----------+----------+----------+
// | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF)
// | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes
// +----------+----------+----------+----------+
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
return '';
}
$x = $y = $z = $w = 0;
if ($code < 128) {
// regular ASCII character
$x = $code;
} else {
// set up bits for UTF-8
$x = ($code & 63) | 128;
if ($code < 2048) {
$y = (($code & 2047) >> 6) | 192;
} else {
$y = (($code & 4032) >> 6) | 128;
if($code < 65536) {
$z = (($code >> 12) & 15) | 224;
} else {
$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);
return $ret;
}
public static function iconvAvailable() {
static $iconv = null;
if ($iconv === null) {
$iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE;
}
return $iconv;
}
/**
* Converts a string to UTF-8 based on configuration.
*/
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 = self::iconvAvailable();
if ($iconv && !$config->get('Test.ForceNoIconv')) {
// unaffected by bugs, since UTF-8 support all characters
$str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str);
if ($str === false) {
// $encoding is not a valid encoding
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.
$str = strtr($str, self::testEncodingSupportsASCII($encoding));
return $str;
} elseif ($encoding === 'iso-8859-1') {
$str = utf8_encode($str);
return $str;
}
$bug = HTMLPurifier_Encoder::testIconvTruncateBug();
if ($bug == self::ICONV_OK) {
trigger_error('Encoding not supported, please install iconv', E_USER_ERROR);
} else {
trigger_error('You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', E_USER_ERROR);
}
}
/**
* Converts a string from UTF-8 based on configuration.
* @note Currently, this is a lossy conversion, with unexpressable
* characters being omitted.
*/
public static function convertFromUTF8($str, $config, $context) {
$encoding = $config->get('Core.Encoding');
if ($escape = $config->get('Core.EscapeNonASCIICharacters')) {
$str = self::convertToASCIIDumbLossless($str);
}
if ($encoding === 'utf-8') return $str;
static $iconv = null;
if ($iconv === null) $iconv = self::iconvAvailable();
if ($iconv && !$config->get('Test.ForceNoIconv')) {
// Undo our previous fix in convertToUTF8, otherwise iconv will barf
$ascii_fix = self::testEncodingSupportsASCII($encoding);
if (!$escape && !empty($ascii_fix)) {
$clear_fix = array();
foreach ($ascii_fix as $utf8 => $native) $clear_fix[$utf8] = '';
$str = strtr($str, $clear_fix);
}
$str = strtr($str, array_flip($ascii_fix));
// Normal stuff
$str = self::iconv('utf-8', $encoding . '//IGNORE', $str);
return $str;
} elseif ($encoding === 'iso-8859-1') {
$str = utf8_decode($str);
return $str;
}
trigger_error('Encoding not supported', E_USER_ERROR);
// You might be tempted to assume that the ASCII representation
// might be OK, however, this is *not* universally true over all
// encodings. So we take the conservative route here, rather
// than forcibly turn on %Core.EscapeNonASCIICharacters
}
/**
* Lossless (character-wise) conversion of HTML to ASCII
* @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
* algorithm. If you disagree with this license fudgery,
* implement it yourself.
* @note Uses decimal numeric entities since they are best supported.
* @note This is a DUMB function: it has no concept of keeping
* character entities that the projected character encoding
* can allow. We could possibly implement a smart version
* but that would require it to also know which Unicode
* codepoints the charset supported (not an easy task).
* @note Sort of with cleanUTF8() but it assumes that $str is
* well-formed UTF-8
*/
public static function convertToASCIIDumbLossless($str) {
$bytesleft = 0;
$result = '';
$working = 0;
$len = strlen($str);
for( $i = 0; $i < $len; $i++ ) {
$bytevalue = ord( $str[$i] );
if( $bytevalue <= 0x7F ) { //0xxx xxxx
$result .= chr( $bytevalue );
$bytesleft = 0;
} elseif( $bytevalue <= 0xBF ) { //10xx xxxx
$working = $working << 6;
$working += ($bytevalue & 0x3F);
$bytesleft--;
if( $bytesleft <= 0 ) {
$result .= "" . $working . ";";
}
} elseif( $bytevalue <= 0xDF ) { //110x xxxx
$working = $bytevalue & 0x1F;
$bytesleft = 1;
} elseif( $bytevalue <= 0xEF ) { //1110 xxxx
$working = $bytevalue & 0x0F;
$bytesleft = 2;
} else { //1111 0xxx
$working = $bytevalue & 0x07;
$bytesleft = 3;
}
}
return $result;
}
/** No bugs detected in iconv. */
const ICONV_OK = 0;
/** Iconv truncates output if converting from UTF-8 to another
* character set with //IGNORE, and a non-encodable character is found */
const ICONV_TRUNCATES = 1;
/** Iconv does not support //IGNORE, making it unusable for
* transcoding purposes */
const ICONV_UNUSABLE = 2;
/**
* glibc iconv has a known bug where it doesn't handle the magic
* //IGNORE stanza correctly. In particular, rather than ignore
* characters, it will return an EILSEQ after consuming some number
* of characters, and expect you to restart iconv as if it were
* an E2BIG. Old versions of PHP did not respect the errno, and
* returned the fragment, so as a result you would see iconv
* mysteriously truncating output. We can work around this by
* manually chopping our input into segments of about 8000
* characters, as long as PHP ignores the error code. If PHP starts
* paying attention to the error code, iconv becomes unusable.
*
* @returns Error code indicating severity of bug.
*/
public static function testIconvTruncateBug() {
static $code = null;
if ($code === null) {
// better not use iconv, otherwise infinite loop!
$r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000));
if ($r === false) {
$code = self::ICONV_UNUSABLE;
} elseif (($c = strlen($r)) < 9000) {
$code = self::ICONV_TRUNCATES;
} elseif ($c > 9000) {
trigger_error('Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: include your iconv version as per phpversion()', E_USER_ERROR);
} else {
$code = self::ICONV_OK;
}
}
return $code;
}
/**
* This expensive function tests whether or not a given character
* encoding supports ASCII. 7/8-bit encodings like Shift_JIS will
* fail this test, and require special processing. Variable width
* encodings shouldn't ever fail.
*
* @param string $encoding Encoding name to test, as per iconv format
* @param bool $bypass Whether or not to bypass the precompiled arrays.
* @return Array of UTF-8 characters to their corresponding ASCII,
* which can be used to "undo" any overzealous iconv action.
*/
public static function testEncodingSupportsASCII($encoding, $bypass = false) {
// All calls to iconv here are unsafe, proof by case analysis:
// If ICONV_OK, no difference.
// If ICONV_TRUNCATE, all calls involve one character inputs,
// so bug is not triggered.
// If ICONV_UNUSABLE, this call is irrelevant
static $encodings = array();
if (!$bypass) {
if (isset($encodings[$encoding])) return $encodings[$encoding];
$lenc = strtolower($encoding);
switch ($lenc) {
case 'shift_jis':
return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~');
case 'johab':
return array("\xE2\x82\xA9" => '\\');
}
if (strpos($lenc, 'iso-8859-') === 0) return array();
}
$ret = array();
if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) return false;
for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars
$c = chr($i); // UTF-8 char
$r = self::unsafeIconv('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 && self::unsafeIconv($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.
$ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c;
}
}
$encodings[$encoding] = $ret;
return $ret;
}
}
/**
* Object that provides entity lookup table from entity name to character
*/
class HTMLPurifier_EntityLookup {
/**
* Assoc array of entity name to character represented.
*/
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
*/
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.
* @param Optional prototype of custom lookup table to overload with.
*/
public static function instance($prototype = false) {
// no references, since PHP doesn't copy unless modified
static $instance = null;
if ($prototype) {
$instance = $prototype;
} elseif (!$instance) {
$instance = new HTMLPurifier_EntityLookup();
$instance->setup();
}
return $instance;
}
}
// if want to implement error collecting here, we'll need to use some sort
// of global data (probably trigger_error) because it's impossible to pass
// $config or $context to the callback functions.
/**
* Handles referencing and derefencing character entities
*/
class HTMLPurifier_EntityParser
{
/**
* Reference to entity lookup table.
*/
protected $_entity_lookup;
/**
* Callback regex string for parsing entities.
*/
protected $_substituteEntitiesRegex =
'/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/';
// 1. hex 2. dec 3. string (XML style)
/**
* Decimal to parsed string conversion table for special entities.
*/
protected $_special_dec2str =
array(
34 => '"',
38 => '&',
39 => "'",
60 => '<',
62 => '>'
);
/**
* Stripped entity names to decimal conversion table for special entities.
*/
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.
*
* @param $string String to have non-special entities parsed.
* @returns Parsed string.
*/
public function substituteNonSpecialEntities($string) {
// it will try to detect missing semicolons, but don't rely on it
return preg_replace_callback(
$this->_substituteEntitiesRegex,
array($this, 'nonSpecialEntityCallback'),
$string
);
}
/**
* Callback function for substituteNonSpecialEntities() that does the work.
*
* @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.
*/
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;
if (!$this->_entity_lookup) {
$this->_entity_lookup = HTMLPurifier_EntityLookup::instance();
}
if (isset($this->_entity_lookup->table[$matches[3]])) {
return $this->_entity_lookup->table[$matches[3]];
} else {
return $entity;
}
}
}
/**
* 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).
*
* @param $string String to have non-special entities parsed.
* @returns Parsed 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().
*
* @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.
*/
protected function specialEntityCallback($matches) {
$entity = $matches[0];
$is_num = (@$matches[0][1] === '#');
if ($is_num) {
$is_hex = (@$entity[2] === 'x');
$int = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
return isset($this->_special_dec2str[$int]) ?
$this->_special_dec2str[$int] :
$entity;
} else {
return isset($this->_special_ent2dec[$matches[3]]) ?
$this->_special_ent2dec[$matches[3]] :
$entity;
}
}
}
/**
* Error collection class that enables HTML Purifier to report HTML
* problems back to the user
*/
class HTMLPurifier_ErrorCollector
{
/**
* 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 $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 ...
*/
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)) {
$args['CurrentToken'] = $token;
}
if (!is_null($attr)) {
$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);
// (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(line of error,
* error severity, error message,
* recursive sub-errors array)
*/
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.
*/
public function getHTMLFormatted($config, $errors = null) {
$ret = array();
$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);
}
}
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;
}
}
}
}
}
/**
* Records errors for particular segments of an HTML document such as tokens,
* attributes or CSS properties. They can contain error structs (which apply
* to components of what they represent), but their main purpose is to hold
* errors applying to whatever struct is being used.
*/
class HTMLPurifier_ErrorStruct
{
/**
* Possible values for $children first-key. Note that top-level structures
* are automatically token-level.
*/
const TOKEN = 0;
const ATTR = 1;
const CSSPROP = 2;
/**
* Type of this struct.
*/
public $type;
/**
* Value of the struct we are recording errors for. There are various
* values for this:
* - TOKEN: Instance of HTMLPurifier_Token
* - ATTR: array('attr-name', 'value')
* - CSSPROP: array('prop-name', 'value')
*/
public $value;
/**
* Errors registered for this structure.
*/
public $errors = array();
/**
* Child ErrorStructs that are from this structure. For example, a TOKEN
* ErrorStruct would contain ATTR ErrorStructs. This is a multi-dimensional
* array in structure: [TYPE]['identifier']
*/
public $children = array();
public function getChild($type, $id) {
if (!isset($this->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);
}
}
/**
* Global exception class for HTML Purifier; any exceptions we throw
* are from here.
*/
class HTMLPurifier_Exception extends Exception
{
}
/**
* Represents a pre or post processing filter on HTML Purifier's output
*
* Sometimes, a little ad-hoc fixing of HTML has to be done before
* it gets sent through HTML Purifier: you can use filters to acheive
* this effect. For instance, YouTube videos can be preserved using
* this manner. You could have used a decorator for this task, but
* PHP's support for them is not terribly robust, so we're going
* to just loop through the filters.
*
* Filters should be exited first in, last out. If there are three filters,
* named 1, 2 and 3, the order of execution should go 1->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
*/
public $name;
/**
* Pre-processor function, handles HTML before HTML Purifier
*/
public function preFilter($html, $config, $context) {
return $html;
}
/**
* Post-processor function, handles HTML after HTML Purifier
*/
public function postFilter($html, $config, $context) {
return $html;
}
}
/**
* Generates HTML from tokens.
* @todo Refactor interface so that configuration/context is determined
* 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
{
/**
* Whether or not generator should produce XML output
*/
private $_xhtml = true;
/**
* :HACK: Whether or not generator should comment the insides of )#si',
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
// 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');
}
if ($maintain_line_numbers) {
$current_line = 1;
$current_col = 0;
$length = strlen($html);
} else {
$current_line = false;
$current_col = false;
$length = 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');
$e = false;
if ($config->get('Core.CollectErrors')) {
$e =& $context->get('ErrorCollector');
}
// for testing synchronization
$loops = 0;
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
HTMLPurifier_Token_Text(
$this->parseData(
substr(
$html, $cursor, $position_next_lt - $cursor
)
)
);
if ($maintain_line_numbers) {
$token->rawPosition($current_line, $current_col);
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor);
}
$array[] = $token;
$cursor = $position_next_lt + 1;
$inside_tag = true;
continue;
} elseif (!$inside_tag) {
// We are not inside tag but there are no more tags
// If we're already at the end, break
if ($cursor === strlen($html)) break;
// Create Text of rest of string
$token = new
HTMLPurifier_Token_Text(
$this->parseData(
substr(
$html, $cursor
)
)
);
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 (
substr($segment, 0, 3) === '!--'
) {
// re-determine segment length, looking for -->
$position_comment_end = strpos($html, '-->', $cursor);
if ($position_comment_end === false) {
// uh oh, we have a comment that extends to
// infinity. Can't be helped: set comment
// end position to end of string
if ($e) $e->send(E_WARNING, 'Lexer: Unclosed comment');
$position_comment_end = strlen($html);
$end = true;
} else {
$end = false;
}
$strlen_segment = $position_comment_end - $cursor;
$segment = substr($html, $cursor, $strlen_segment);
$token = new
HTMLPurifier_Token_Comment(
substr(
$segment, 3, $strlen_segment - 3
)
);
if ($maintain_line_numbers) {
$token->rawPosition($current_line, $current_col);
$current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment);
}
$array[] = $token;
$cursor = $end ? $position_comment_end : $position_comment_end + 3;
$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->rawPosition($current_line, $current_col);
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
}
$array[] = $token;
$inside_tag = false;
$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('<');
if ($maintain_line_numbers) {
$token->rawPosition($current_line, $current_col);
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
}
$array[] = $token;
$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
// classified EmptyTags from StartTags.
$is_self_closing = (strrpos($segment,'/') === $strlen_segment-1);
if ($is_self_closing) {
$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);
} else {
$token = new HTMLPurifier_Token_Start($segment);
}
if ($maintain_line_numbers) {
$token->rawPosition($current_line, $current_col);
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
}
$array[] = $token;
$inside_tag = false;
$cursor = $position_next_gt + 1;
continue;
}
// Grab out all the data
$type = substr($segment, 0, $position_first_space);
$attribute_string =
trim(
substr(
$segment, $position_first_space
)
);
if ($attribute_string) {
$attr = $this->parseAttributeString(
$attribute_string
, $config, $context
);
} 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->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;
} else {
// inside tag, but there's no ending > sign
if ($e) $e->send(E_WARNING, 'Lexer: Missing gt');
$token = new
HTMLPurifier_Token_Text(
'<' .
$this->parseData(
substr($html, $cursor)
)
);
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 5.0.x compatible substr_count that implements offset and length
*/
protected function substrCount($haystack, $needle, $offset, $length) {
static $oldVersion;
if ($oldVersion === null) {
$oldVersion = version_compare(PHP_VERSION, '5.1', '<');
}
if ($oldVersion) {
$haystack = substr($haystack, $offset, $length);
return substr_count($haystack, $needle);
} else {
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.
*/
public function parseAttributeString($string, $config, $context) {
$string = (string) $string; // quick typecast
if ($string == '') return array(); // no attributes
$e = false;
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, '=');
$has_space = strpos($string, ' ');
if ($num_equal === 0 && !$has_space) {
// bool attribute
return array($string => $string);
} elseif ($num_equal === 1 && !$has_space) {
// only one attribute
list($key, $quoted_value) = explode('=', $string);
$quoted_value = trim($quoted_value);
if (!$key) {
if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
return array();
}
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);
} else {
// not well behaved
if ($open_quote) {
if ($e) $e->send(E_ERROR, 'Lexer: Missing end quote');
$value = substr($quoted_value, 1);
} else {
$value = $quoted_value;
}
}
if ($value === false) $value = '';
return array($key => $this->parseData($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 .= ' ';
while(true) {
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++;
$value_begin = $cursor;
$cursor = strpos($string, $char, $cursor);
$value_end = $cursor;
} else {
// it's not quoted, end bound is whitespace
$value_begin = $cursor;
$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 !== '') {
$array[$key] = $key;
} else {
// purely theoretical
if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
}
}
}
return $array;
}
}
/**
* Composite strategy that runs multiple strategies on tokens.
*/
abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy
{
/**
* List of strategies to run tokens through.
*/
protected $strategies = array();
public function execute($tokens, $config, $context) {
foreach ($this->strategies as $strategy) {
$tokens = $strategy->execute($tokens, $config, $context);
}
return $tokens;
}
}
/**
* Core strategy composed of the big four strategies.
*/
class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite
{
public function __construct() {
$this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements();
$this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed();
$this->strategies[] = new HTMLPurifier_Strategy_FixNesting();
$this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes();
}
}
/**
* Takes a well formed list of tokens and fixes their nesting.
*
* HTML elements dictate which elements are allowed to be their children,
* for example, you can't have a p tag in a span tag. Other elements have
* much more rigorous definitions: tables, for instance, require a specific
* order for their elements. There are also constraints not expressible by
* document type definitions, such as the chameleon nature of ins/del
* tags and global child exclusions.
*
* The first major objective of this strategy is to iterate through all the
* nodes (not tokens) of the list of tokens and determine whether or not
* their children conform to the element's definition. If they do not, the
* child definition may optionally supply an amended list of elements that
* is valid or require that the entire node be deleted (and the previous
* node rescanned).
*
* The second objective is to ensure that explicitly excluded elements of
* an element do not appear in its children. Code that accomplishes this
* task is pervasive through the strategy, though the two are distinct tasks
* and could, theoretically, be seperated (although it's not recommended).
*
* @note Whether or not unrecognized children are silently dropped or
* translated into text depends on the child definitions.
*
* @todo Enable nodes to be bubbled out of the structure.
*
* @warning This algorithm (though it may be hard to see) proceeds from
* a top-down fashion. Thus, parents are processed before
* children. This is easy to implement and has a nice effiency
* benefit, in that if a node is removed, we never waste any
* time processing it, but it also means that if a child
* changes in a non-encapsulated way (e.g. it is removed), we
* need to go back and reprocess the parent to see if those
* changes resulted in problems for the parent. See
* [BACKTRACK] for an example of this. In the current
* implementation, this backtracking can only be triggered when
* a node is removed and if that node was the sole node, the
* parent would need to be removed. As such, it is easy to see
* that backtracking only incurs constant overhead. If more
* sophisticated backtracking is implemented, care must be
* taken to avoid nontermination or exponential blowup.
*/
class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
{
public function execute($tokens, $config, $context) {
//####################################################################//
// Pre-processing
// get a copy of the HTML definition
$definition = $config->getHTMLDefinition();
$excludes_enabled = !$config->get('Core.DisableExcludes');
// 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,
// 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] 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] instanceof HTMLPurifier_Token_End) {
$depth--;
// skip token assignment on last iteration, this is the
// end token of the token we're currently on
if ($depth == 0) break;
}
$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];
$parent_name = $tokens[$parent_index]->name;
if ($parent_index == 0) {
$parent_def = $definition->info_parent_def;
} else {
$parent_def = $definition->info[$parent_name];
}
} 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
// 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
if (!empty($parent_def) && $parent_def->descendants_are_inline) {
$is_inline = $count - 1;
}
} else {
// check if we're out of inline
if ($count === $is_inline) {
$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.
$excluded = false;
if (!empty($exclude_stack) && $excludes_enabled) {
foreach ($exclude_stack as $lookup) {
if (isset($lookup[$tokens[$i]->name])) {
$excluded = true;
// no need to continue processing
break;
}
}
}
//################################################################//
// Perform child validation
if ($excluded) {
// there is an exclusion, remove the entire node
$result = false;
$excludes = array(); // not used, but good to initialize anyway
} else {
// DEFINITION CALL
if ($i === 0) {
// special processing for the first node
$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(
$child_tokens, $config, $context);
} else {
// 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');
} else {
$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:
// ( | a b c) where it's all or nothing. Fortunately,
// our current implementation claims that that case would
// not allow empty, even if it did
if (!$parent_def->child->allow_empty) {
// we need to do a double-check [BACKTRACK]
$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');
} else {
$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] 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
// we ended node and that node had exclusions
if ($i == 0 || $i == $size - 1) {
// use specialized var if it's the super-parent
$s_excludes = $definition->info_parent_def->excludes;
} else {
$s_excludes = $definition->info[$tokens[$i]->name]->excludes;
}
if ($s_excludes) {
array_pop($exclude_stack);
}
}
$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;
}
}
/**
* Takes tokens makes them well-formed (balance end tags, etc.)
*
* Specification of the armor attributes this strategy uses:
*
* - MakeWellFormed_TagClosedError: This armor field is used to
* suppress tag closed errors for certain tokens [TagClosedSuppress],
* in particular, if a tag was generated automatically by HTML
* Purifier, we may rely on our infrastructure to close it for us
* and shouldn't report an error to the user [TagClosedAuto].
*/
class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
{
/**
* Array stream of tokens being processed.
*/
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
$generator = new HTMLPurifier_Generator($config, $context);
$escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
// used for autoclose early abortion
$global_parent_allowed_elements = array();
if (isset($definition->info[$definition->info_parent])) {
// may be unset under testing circumstances
$global_parent_allowed_elements = $definition->info[$definition->info_parent]->child->getAllowedElements($config);
}
$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->stack =& $stack;
$this->t =& $t;
$this->tokens =& $tokens;
$this->config = $config;
$this->context = $context;
// context variables
$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;
}
// give the injectors references to the definition and context
// variables for performance reasons
foreach ($this->injectors as $ix => $injector) {
$error = $injector->prepare($config, $context);
if (!$error) continue;
array_splice($this->injectors, $ix, 1); // rm the injector
trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
}
// -- end INJECTOR --
// a note on reprocessing:
// 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;
}
}
$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 [TagClosedSuppress]
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;
}
$token = $tokens[$t];
//echo '
'; printTokens($tokens, $t); printTokens($this->stack);
//flush();
// 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;
}
}
// 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, $token->line, $token->col, $token->armor);
$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, $token->line, $token->col, $token->armor));
// 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)) {
// Performance note: you might think that it's rather
// inefficient, recalculating the autoclose information
// for every tag that a token closes (since when we
// do an autoclose, we push a new token into the
// stream and then /process/ that, before
// re-processing this token.) But this is
// necessary, because an injector can make an
// arbitrary transformations to the autoclosing
// tokens we introduce, so things may have changed
// in the meantime. Also, doing the inefficient thing is
// "easy" to reason about (for certain perverse definitions
// of "easy")
$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 an element can be wrapped by another
// element to make it valid in a context (for
// example, needs a - in between)
$wrapname = $definition->info[$token->name]->wrap;
$wrapdef = $definition->info[$wrapname];
$elements = $wrapdef->child->getAllowedElements($config);
$parent_elements = $definition->info[$parent->name]->child->getAllowedElements($config);
if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) {
$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) {
// check if this autoclose is doomed to fail
// (this rechecks $parent, which his harmless)
$autoclose_ok = isset($global_parent_allowed_elements[$token->name]);
if (!$autoclose_ok) {
foreach ($this->stack as $ancestor) {
$elements = $definition->info[$ancestor->name]->child->getAllowedElements($config);
if (isset($elements[$token->name])) {
$autoclose_ok = true;
break;
}
if ($definition->info[$token->name]->wrap) {
$wrapname = $definition->info[$token->name]->wrap;
$wrapdef = $definition->info[$wrapname];
$wrap_elements = $wrapdef->child->getAllowedElements($config);
if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) {
$autoclose_ok = true;
break;
}
}
}
}
if ($autoclose_ok) {
// errors need to be updated
$new_token = new HTMLPurifier_Token_End($parent->name);
$new_token->start = $parent;
if ($carryover) {
$element = clone $parent;
// [TagClosedAuto]
$element->armor['MakeWellFormed_TagClosedError'] = true;
$element->carryover = true;
$this->processToken(array($new_token, $token, $element));
} else {
$this->insertBefore($new_token);
}
// [TagClosedSuppress]
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);
}
}
} else {
$this->remove();
}
$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 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->stack)) {
if ($escape_invalid_tags) {
if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
$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.
// 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) {
$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->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->stack);
// -2 because -1 is the last element, but we already checked that
$skipped_tags = false;
for ($j = $size - 2; $j >= 0; $j--) {
if ($this->stack[$j]->name == $token->name) {
$skipped_tags = array_slice($this->stack, $j);
break;
}
}
// we didn't find the tag, so remove
if ($skipped_tags === false) {
if ($escape_invalid_tags) {
$this->swap(new HTMLPurifier_Token_Text(
$generator->generateFromToken($token)
));
if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
} else {
$this->remove();
if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
}
$reprocess = true;
continue;
}
// 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... [TagClosedSuppress]
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) {
// [TagClosedAuto]
$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');
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;
}
}
}
/**
* Inserts a token before the current token. Cursor now points to
* this token. You must reprocess after this.
*/
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. You must reprocess after this.
*/
private function remove() {
array_splice($this->tokens, $this->t, 1);
}
/**
* Swap current token with new token. Cursor points to new token (no
* change). You must reprocess after this.
*/
private function swap($token) {
$this->tokens[$this->t] = $token;
}
}
/**
* 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.
*/
class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
{
public function execute($tokens, $config, $context) {
$definition = $config->getHTMLDefinition();
$generator = new HTMLPurifier_Generator($config, $context);
$result = array();
$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');
$comment_lookup = $config->get('HTML.AllowedComments');
$comment_regexp = $config->get('HTML.AllowedCommentsRegexp');
$check_comments = $comment_lookup !== array() || $comment_regexp !== null;
$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')) {
$e =& $context->get('ErrorCollector');
}
foreach($tokens as $token) {
if ($remove_until) {
if (empty($token->is_tag) || $token->name !== $remove_until) {
continue;
}
}
if (!empty( $token->is_tag )) {
// DEFINITION CALL
// before any processing, try to transform the element
if (
isset($definition->info_tag_transform[$token->name])
) {
$original_name = $token->name;
// there is a transformation for this tag
// DEFINITION CALL
$token = $definition->
info_tag_transform[$token->name]->
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 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
) {
$attr_validator->validateToken($token, $config, $context);
$ok = true;
foreach ($definition->info[$token->name]->required_attr as $name) {
if (!isset($token->attr[$name])) {
$ok = false;
break;
}
}
if (!$ok) {
if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Missing required attribute', $name);
continue;
}
$token->armor['ValidateAttributes'] = true;
}
if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) {
$textify_comments = $token->name;
} 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)
);
} else {
// check if we need to destroy all of the tag's children
// CAN BE GENERICIZED
if (isset($hidden_elements[$token->name])) {
if ($token instanceof HTMLPurifier_Token_Start) {
$remove_until = $token->name;
} elseif ($token instanceof HTMLPurifier_Token_Empty) {
// do nothing: we're still looking
} else {
$remove_until = false;
}
if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed');
} else {
if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed');
}
continue;
}
} 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 || $check_comments) {
// always cleanup comments
$trailing_hyphen = false;
if ($e) {
// perform check whether or not there's a trailing hyphen
if (substr($token->data, -1) == '-') {
$trailing_hyphen = true;
}
}
$token->data = rtrim($token->data, '-');
$found_double_hyphen = false;
while (strpos($token->data, '--') !== false) {
$found_double_hyphen = true;
$token->data = str_replace('--', '-', $token->data);
}
if ($trusted || !empty($comment_lookup[trim($token->data)]) || ($comment_regexp !== NULL && preg_match($comment_regexp, trim($token->data)))) {
// OK good
if ($e) {
if ($trailing_hyphen) {
$e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed');
}
if ($found_double_hyphen) {
$e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed');
}
}
} else {
if ($e) {
$e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed');
}
continue;
}
} else {
// strip comments
if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed');
continue;
}
} elseif ($token instanceof HTMLPurifier_Token_Text) {
} else {
continue;
}
$result[] = $token;
}
if ($remove_until && $e) {
// 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;
}
}
/**
* Validate all attributes in the tokens.
*/
class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
{
public function execute($tokens, $config, $context) {
// setup validator
$validator = new HTMLPurifier_AttrValidator();
$token = false;
$context->register('CurrentToken', $token);
foreach ($tokens as $key => $token) {
// only process tokens that have attributes,
// namely start and empty tags
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;
}
}
/**
* Transforms FONT tags to the proper form (SPAN with CSS styling)
*
* This transformation takes the three proprietary attributes of FONT and
* transforms them into their corresponding CSS attributes. These are color,
* face, and size.
*
* @note Size is an interesting case because it doesn't map cleanly to CSS.
* Thanks to
* http://style.cleverchimp.com/font_size_intervals/altintervals.html
* for reasonable mappings.
* @warning This doesn't work completely correctly; specifically, this
* TagTransform operates before well-formedness is enforced, so
* the "active formatting elements" algorithm doesn't get applied.
*/
class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform
{
public $transform_to = 'span';
protected $_size_lookup = array(
'0' => 'xx-small',
'1' => 'xx-small',
'2' => 'small',
'3' => 'medium',
'4' => 'large',
'5' => 'x-large',
'6' => 'xx-large',
'7' => '300%',
'-1' => 'smaller',
'-2' => '60%',
'+1' => 'larger',
'+2' => '150%',
'+3' => '200%',
'+4' => '300%'
);
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
if ($attr['size'] !== '') {
if ($attr['size']{0} == '+' || $attr['size']{0} == '-') {
$size = (int) $attr['size'];
if ($size < -2) $attr['size'] = '-2';
if ($size > 4) $attr['size'] = '+4';
} else {
$size = (int) $attr['size'];
if ($size > 7) $attr['size'] = '7';
}
}
if (isset($this->_size_lookup[$attr['size']])) {
$prepend_style .= 'font-size:' .
$this->_size_lookup[$attr['size']] . ';';
}
unset($attr['size']);
}
if ($prepend_style) {
$attr['style'] = isset($attr['style']) ?
$prepend_style . $attr['style'] :
$prepend_style;
}
$new_tag = clone $tag;
$new_tag->name = $this->transform_to;
$new_tag->attr = $attr;
return $new_tag;
}
}
/**
* Simple transformation, just change tag name to something else,
* and possibly add some styling. This will cover most of the deprecated
* tag cases.
*/
class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform
{
protected $style;
/**
* @param $transform_to Tag name to transform to.
* @param $style CSS style to add to the tag
*/
public function __construct($transform_to, $style = null) {
$this->transform_to = $transform_to;
$this->style = $style;
}
public function transform($tag, $config, $context) {
$new_tag = clone $tag;
$new_tag->name = $this->transform_to;
if (!is_null($this->style) &&
($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty)
) {
$this->prependCSS($new_tag->attr, $this->style);
}
return $new_tag;
}
}
/**
* Concrete comment token class. Generally will be ignored.
*/
class HTMLPurifier_Token_Comment extends HTMLPurifier_Token
{
public $data; /**< Character data within comment. */
public $is_whitespace = true;
/**
* Transparent constructor.
*
* @param $data String comment data.
*/
public function __construct($data, $line = null, $col = null) {
$this->data = $data;
$this->line = $line;
$this->col = $col;
}
}
/**
* Abstract class of a tag token (start, end or empty), and its behavior.
*/
class HTMLPurifier_Token_Tag extends HTMLPurifier_Token
{
/**
* 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 $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, $armor = array()) {
$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;
$this->armor = $armor;
}
}
/**
* Concrete empty token class.
*/
class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag
{
}
/**
* 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
{
/**
* Token that started this node. Added by MakeWellFormed. Please
* do not edit this!
*/
public $start;
}
/**
* Concrete start token class.
*/
class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag
{
}
/**
* 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
{
public $name = '#PCDATA'; /**< PCDATA tag name compatible with DTD. */
public $data; /**< Parsed character data of text. */
public $is_whitespace; /**< Bool indicating if node is whitespace. */
/**
* Constructor, accepts data and determines if it is whitespace.
*
* @param $data String parsed character data.
*/
public function __construct($data, $line = null, $col = null) {
$this->data = $data;
$this->is_whitespace = ctype_space($data);
$this->line = $line;
$this->col = $col;
}
}
class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter
{
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));
}
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));
foreach ($this->ourHostParts as $i => $x) {
if (!isset($host_parts[$i])) return false;
if ($host_parts[$i] != $this->ourHostParts[$i]) return false;
}
return true;
}
}
class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal
{
public $name = 'DisableExternalResources';
public function filter(&$uri, $config, $context) {
if (!$context->get('EmbeddedURI', true)) return true;
return parent::filter($uri, $config, $context);
}
}
class HTMLPurifier_URIFilter_DisableResources extends HTMLPurifier_URIFilter
{
public $name = 'DisableResources';
public function filter(&$uri, $config, $context) {
return !$context->get('EmbeddedURI', true);
}
}
// It's not clear to me whether or not Punycode means that hostnames
// do not have canonical forms anymore. As far as I can tell, it's
// not a problem (punycoding should be identity when no Unicode
// points are involved), but I'm not 100% sure
class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
{
public $name = 'HostBlacklist';
protected $blacklist = array();
public function prepare($config) {
$this->blacklist = $config->get('URI.HostBlacklist');
return true;
}
public function filter(&$uri, $config, $context) {
foreach($this->blacklist as $blacklisted_host_fragment) {
if (strpos($uri->host, $blacklisted_host_fragment) !== false) {
return false;
}
}
return true;
}
}
// does not support network paths
class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
{
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_WARNING);
return false;
}
$this->base->fragment = null; // fragment is invalid for base URI
$stack = explode('/', $this->base->path);
array_pop($stack); // discard last segment
$stack = $this->_collapseStack($stack); // do pre-parsing
$this->basePathStack = $stack;
return true;
}
public function filter(&$uri, $config, $context) {
if (is_null($this->base)) return true; // abort early
if (
$uri->path === '' && is_null($uri->scheme) &&
is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)
) {
// reference to current document
$uri = clone $this->base;
return true;
}
if (!is_null($uri->scheme)) {
// absolute URI already: don't change
if (!is_null($uri->host)) return true;
$scheme_obj = $uri->getSchemeObj($config, $context);
if (!$scheme_obj) {
// scheme not recognized
return false;
}
if (!$scheme_obj->hierarchical) {
// non-hierarchal URI with explicit scheme, don't change
return true;
}
// special case: had a scheme but always is hierarchical and had no authority
}
if (!is_null($uri->host)) {
// network path, don't bother
return true;
}
if ($uri->path === '') {
$uri->path = $this->base->path;
} 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;
if (is_null($uri->userinfo)) $uri->userinfo = $this->base->userinfo;
if (is_null($uri->host)) $uri->host = $this->base->host;
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) {
$result = array();
$is_folder = false;
for ($i = 0; isset($stack[$i]); $i++) {
$is_folder = false;
// absorb an internally duplicated slash
if ($stack[$i] == '' && $i && isset($stack[$i+1])) continue;
if ($stack[$i] == '..') {
if (!empty($result)) {
$segment = array_pop($result);
if ($segment === '' && empty($result)) {
// error case: attempted to back out too far:
// restore the leading slash
$result[] = '';
} elseif ($segment === '..') {
$result[] = '..'; // cannot remove .. with ..
}
} else {
// relative path, preserve the double-dots
$result[] = '..';
}
$is_folder = true;
continue;
}
if ($stack[$i] == '.') {
// silently absorb
$is_folder = true;
continue;
}
$result[] = $stack[$i];
}
if ($is_folder) $result[] = '';
return $result;
}
}
class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter
{
public $name = 'Munge';
public $post = true;
private $target, $parser, $doEmbed, $secretKey;
protected $replace = array();
public function prepare($config) {
$this->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 (!$scheme_obj->browsable) return true; // ignore non-browseable schemes, since we can't munge those in a reasonable way
if ($uri->isBenign($config, $context)) return true; // don't redirect if a benign URL
$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);
}
}
/**
* Implements safety checks for safe iframes.
*
* @warning This filter is *critical* for ensuring that %HTML.SafeIframe
* works safely.
*/
class HTMLPurifier_URIFilter_SafeIframe extends HTMLPurifier_URIFilter
{
public $name = 'SafeIframe';
public $always_load = true;
protected $regexp = NULL;
// XXX: The not so good bit about how this is all setup now is we
// can't check HTML.SafeIframe in the 'prepare' step: we have to
// defer till the actual filtering.
public function prepare($config) {
$this->regexp = $config->get('URI.SafeIframeRegexp');
return true;
}
public function filter(&$uri, $config, $context) {
// check if filter not applicable
if (!$config->get('HTML.SafeIframe')) return true;
// check if the filter should actually trigger
if (!$context->get('EmbeddedURI', true)) return true;
$token = $context->get('CurrentToken', true);
if (!($token && $token->name == 'iframe')) return true;
// check if we actually have some whitelists enabled
if ($this->regexp === null) return false;
// actually check the whitelists
return preg_match($this->regexp, $uri->toString());
}
}
/**
* Implements data: URI for base64 encoded images supported by GD.
*/
class HTMLPurifier_URIScheme_data extends HTMLPurifier_URIScheme {
public $browsable = true;
public $allowed_types = array(
// you better write validation code for other types if you
// decide to allow them
'image/jpeg' => true,
'image/gif' => true,
'image/png' => true,
);
// this is actually irrelevant since we only write out the path
// component
public $may_omit_host = true;
public function doValidate(&$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);
unlink($file);
} elseif (function_exists('getimagesize')) {
set_error_handler(array($this, 'muteErrorHandler'));
$info = getimagesize($file);
restore_error_handler();
unlink($file);
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) {}
}
/**
* Validates file as defined by RFC 1630 and RFC 1738.
*/
class HTMLPurifier_URIScheme_file extends HTMLPurifier_URIScheme {
// Generally file:// URLs are not accessible from most
// machines, so placing them as an img src is incorrect.
public $browsable = false;
// Basically the *only* URI scheme for which this is true, since
// accessing files on the local machine is very common. In fact,
// browsers on some operating systems don't understand the
// authority, though I hear it is used on Windows to refer to
// network shares.
public $may_omit_host = true;
public function doValidate(&$uri, $config, $context) {
// Authentication method is not supported
$uri->userinfo = null;
// file:// makes no provisions for accessing the resource
$uri->port = null;
// While it seems to work on Firefox, the querystring has
// no possible effect and is thus stripped.
$uri->query = null;
return true;
}
}
/**
* Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738.
*/
class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme {
public $default_port = 21;
public $browsable = true; // usually
public $hierarchical = true;
public function doValidate(&$uri, $config, $context) {
$uri->query = null;
// typecode check
$semicolon_pos = strrpos($uri->path, ';'); // reverse
if ($semicolon_pos !== false) {
$type = substr($uri->path, $semicolon_pos + 1); // no semicolon
$uri->path = substr($uri->path, 0, $semicolon_pos);
$type_ret = '';
if (strpos($type, '=') !== false) {
// figure out whether or not the declaration is correct
list($key, $typecode) = explode('=', $type, 2);
if ($key !== 'type') {
// invalid key, tack it back on encoded
$uri->path .= '%3B' . $type;
} elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') {
$type_ret = ";type=$typecode";
}
} else {
$uri->path .= '%3B' . $type;
}
$uri->path = str_replace(';', '%3B', $uri->path);
$uri->path .= $type_ret;
}
return true;
}
}
/**
* Validates http (HyperText Transfer Protocol) as defined by RFC 2616
*/
class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme {
public $default_port = 80;
public $browsable = true;
public $hierarchical = true;
public function doValidate(&$uri, $config, $context) {
$uri->userinfo = null;
return true;
}
}
/**
* Validates https (Secure HTTP) according to http scheme.
*/
class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http {
public $default_port = 443;
public $secure = true;
}
// VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the
// email is valid, but be careful!
/**
* Validates mailto (for E-mail) according to RFC 2368
* @todo Validate the email address
* @todo Filter allowed query parameters
*/
class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme {
public $browsable = false;
public $may_omit_host = true;
public function doValidate(&$uri, $config, $context) {
$uri->userinfo = null;
$uri->host = null;
$uri->port = null;
// we need to validate path against RFC 2368's addr-spec
return true;
}
}
/**
* Validates news (Usenet) as defined by generic RFC 1738
*/
class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme {
public $browsable = false;
public $may_omit_host = true;
public function doValidate(&$uri, $config, $context) {
$uri->userinfo = null;
$uri->host = null;
$uri->port = null;
$uri->query = null;
// typecode check needed on path
return true;
}
}
/**
* Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738
*/
class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme {
public $default_port = 119;
public $browsable = false;
public function doValidate(&$uri, $config, $context) {
$uri->userinfo = null;
$uri->query = null;
return true;
}
}
/**
* Performs safe variable parsing based on types which can be used by
* users. This may not be able to represent all possible data inputs,
* however.
*/
class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser
{
protected function parseImplementation($var, $type, $allow_null) {
if ($allow_null && $var === null) return null;
switch ($type) {
// Note: if code "breaks" from the switch, it triggers a generic
// exception to be thrown. Specific errors can be specifically
// done here.
case self::MIXED :
case self::ISTRING :
case self::STRING :
case self::TEXT :
case self::ITEXT :
return $var;
case self::INT :
if (is_string($var) && ctype_digit($var)) $var = (int) $var;
return $var;
case self::FLOAT :
if ((is_string($var) && is_numeric($var)) || is_int($var)) $var = (float) $var;
return $var;
case self::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 {
throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type");
}
}
return $var;
case self::ALIST :
case self::HASH :
case self::LOOKUP :
if (is_string($var)) {
// special case: technically, this is an array with
// a single empty string item, but having an empty
// 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 === self::HASH) {
// key:value,key2:value2
$nvar = array();
foreach ($var as $keypair) {
$c = explode(':', $keypair, 2);
if (!isset($c[1])) continue;
$nvar[trim($c[0])] = trim($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::ALIST) {
trigger_error("Array list did not have consecutive integer indexes", E_USER_WARNING);
return array_values($var);
}
if ($type === self::LOOKUP) {
foreach ($var as $key => $value) {
if ($value !== true) {
trigger_error("Lookup array has non-true value at key '$key'; maybe your input array was not indexed numerically", E_USER_WARNING);
}
$var[$key] = true;
}
}
return $var;
default:
$this->errorInconsistent(__CLASS__, $type);
}
$this->errorGeneric($var, $type);
}
}
/**
* This variable parser uses PHP's internal code engine. Because it does
* this, it can represent all inputs; however, it is dangerous and cannot
* be used by users.
*/
class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser
{
protected function parseImplementation($var, $type, $allow_null) {
return $this->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;
}
}