Вход Регистрация
Файл: framework/vendors/htmlpurifier/HTMLPurifier.standalone.php
Строк: 23912
<?php

/**
 * @file
 * This file was auto-generated by generate-includes.php and includes all of
 * the core files required by HTML Purifier. Use this if performance is a
 * primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS
 * FILE, changes will be overwritten the next time the script is run.
 *
 * @version 4.9.2
 *
 * @warning
 *      You must *not* include any other HTML Purifier files before this file,
 *      because 'require' not 'require_once' is used.
 *
 * @warning
 *      This file requires that the include path contains the HTML Purifier
 *      library directory; this is not auto-set.
 */



/*! @mainpage
 *
 * HTML Purifier is an HTML filter that will take an arbitrary snippet of
 * HTML and rigorously test, validate and filter it into a version that
 * is safe for output onto webpages. It achieves this by:
 *
 *  -# Lexing (parsing into tokens) the document,
 *  -# Executing various strategies on the tokens:
 *      -# Removing all elements not in the whitelist,
 *      -# Making the tokens well-formed,
 *      -# Fixing the nesting of the nodes, and
 *      -# Validating attributes of the nodes; and
 *  -# Generating HTML from the purified tokens.
 *
 * However, most users will only need to interface with the HTMLPurifier
 * and HTMLPurifier_Config.
 */

/*
    HTML Purifier 4.9.2 - Standards Compliant HTML Filtering
    Copyright (C) 2006-2008 Edward Z. Yang

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

/**
 * Facade that coordinates HTML Purifier's subsystems in order to purify HTML.
 *
 * @note There are several points in which configuration can be specified
 *       for HTML Purifier.  The precedence of these (from lowest to
 *       highest) is as follows:
 *          -# Instance: new HTMLPurifier($config)
 *          -# Invocation: purify($html, $config)
 *       These configurations are entirely independent of each other and
 *       are *not* merged (this behavior may change in the future).
 *
 * @todo We need an easier way to inject strategies using the configuration
 *       object.
 */
class HTMLPurifier
{

    
/**
     * Version of HTML Purifier.
     * @type string
     */
    
public $version '4.9.2';

    
/**
     * Constant with version of HTML Purifier.
     */
    
const VERSION '4.9.2';

    
/**
     * Global configuration object.
     * @type HTMLPurifier_Config
     */
    
public $config;

    
/**
     * Array of extra filter objects to run on HTML,
     * for backwards compatibility.
     * @type HTMLPurifier_Filter[]
     */
    
private $filters = array();

    
/**
     * Single instance of HTML Purifier.
     * @type HTMLPurifier
     */
    
private static $instance;

    
/**
     * @type HTMLPurifier_Strategy_Core
     */
    
protected $strategy;

    
/**
     * @type HTMLPurifier_Generator
     */
    
protected $generator;

    
/**
     * Resultant context of last run purification.
     * Is an array of contexts if the last called method was purifyArray().
     * @type HTMLPurifier_Context
     */
    
public $context;

    
/**
     * Initializes the purifier.
     *
     * @param HTMLPurifier_Config|mixed $config Optional HTMLPurifier_Config object
     *                for all instances of the purifier, if omitted, a default
     *                configuration is supplied (which can be overridden on a
     *                per-use basis).
     *                The parameter can also be any type that
     *                HTMLPurifier_Config::create() supports.
     */
    
public function __construct($config null)
    {
        
$this->config HTMLPurifier_Config::create($config);
        
$this->strategy = new HTMLPurifier_Strategy_Core();
    }

    
/**
     * Adds a filter to process the output. First come first serve
     *
     * @param HTMLPurifier_Filter $filter HTMLPurifier_Filter object
     */
    
public function addFilter($filter)
    {
        
trigger_error(
            
'HTMLPurifier->addFilter() is deprecated, use configuration directives' .
            
' in the Filter namespace or Filter.Custom',
            
E_USER_WARNING
        
);
        
$this->filters[] = $filter;
    }

    
/**
     * Filters an HTML snippet/document to be XSS-free and standards-compliant.
     *
     * @param string $html String of HTML to purify
     * @param HTMLPurifier_Config $config Config object for this operation,
     *                if omitted, defaults to the config object specified during this
     *                object's construction. The parameter can also be any type
     *                that HTMLPurifier_Config::create() supports.
     *
     * @return string Purified HTML
     */
    
public function purify($html$config null)
    {
        
// :TODO: make the config merge in, instead of replace
        
$config $config HTMLPurifier_Config::create($config) : $this->config;

        
// implementation is partially environment dependant, partially
        // configuration dependant
        
$lexer HTMLPurifier_Lexer::create($config);

        
$context = new HTMLPurifier_Context();

        
// setup HTML generator
        
$this->generator = new HTMLPurifier_Generator($config$context);
        
$context->register('Generator'$this->generator);

        
// set up global context variables
        
if ($config->get('Core.CollectErrors')) {
            
// may get moved out if other facilities use it
            
$language_factory HTMLPurifier_LanguageFactory::instance();
            
$language $language_factory->create($config$context);
            
$context->register('Locale'$language);

            
$error_collector = new HTMLPurifier_ErrorCollector($context);
            
$context->register('ErrorCollector'$error_collector);
        }

        
// setup id_accumulator context, necessary due to the fact that
        // AttrValidator can be called from many places
        
$id_accumulator HTMLPurifier_IDAccumulator::build($config$context);
        
$context->register('IDAccumulator'$id_accumulator);

        
$html HTMLPurifier_Encoder::convertToUTF8($html$config$context);

        
// setup filters
        
$filter_flags $config->getBatch('Filter');
        
$custom_filters $filter_flags['Custom'];
        unset(
$filter_flags['Custom']);
        
$filters = array();
        foreach (
$filter_flags as $filter => $flag) {
            if (!
$flag) {
                continue;
            }
            if (
strpos($filter'.') !== false) {
                continue;
            }
            
$class "HTMLPurifier_Filter_$filter";
            
$filters[] = new $class;
        }
        foreach (
$custom_filters as $filter) {
            
// maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat
            
$filters[] = $filter;
        }
        
$filters array_merge($filters$this->filters);
        
// maybe prepare(), but later

        
for ($i 0$filter_size count($filters); $i $filter_size$i++) {
            
$html $filters[$i]->preFilter($html$config$context);
        }

        
// purified HTML
        
$html =
            
$this->generator->generateFromTokens(
                
// list of tokens
                
$this->strategy->execute(
                    
// list of un-purified tokens
                    
$lexer->tokenizeHTML(
                        
// un-purified HTML
                        
$html,
                        
$config,
                        
$context
                    
),
                    
$config,
                    
$context
                
)
            );

        for (
$i $filter_size 1$i >= 0$i--) {
            
$html $filters[$i]->postFilter($html$config$context);
        }

        
$html HTMLPurifier_Encoder::convertFromUTF8($html$config$context);
        
$this->context =& $context;
        return 
$html;
    }

    
/**
     * Filters an array of HTML snippets
     *
     * @param string[] $array_of_html Array of html snippets
     * @param HTMLPurifier_Config $config Optional config object for this operation.
     *                See HTMLPurifier::purify() for more details.
     *
     * @return string[] Array of purified HTML
     */
    
public function purifyArray($array_of_html$config null)
    {
        
$context_array = array();
        foreach (
$array_of_html as $key => $html) {
            
$array_of_html[$key] = $this->purify($html$config);
            
$context_array[$key] = $this->context;
        }
        
$this->context $context_array;
        return 
$array_of_html;
    }

    
/**
     * Singleton for enforcing just one HTML Purifier in your system
     *
     * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype
     *                   HTMLPurifier instance to overload singleton with,
     *                   or HTMLPurifier_Config instance to configure the
     *                   generated version with.
     *
     * @return HTMLPurifier
     */
    
public static function instance($prototype null)
    {
        if (!
self::$instance || $prototype) {
            if (
$prototype instanceof HTMLPurifier) {
                
self::$instance $prototype;
            } elseif (
$prototype) {
                
self::$instance = new HTMLPurifier($prototype);
            } else {
                
self::$instance = new HTMLPurifier();
            }
        }
        return 
self::$instance;
    }

    
/**
     * Singleton for enforcing just one HTML Purifier in your system
     *
     * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype
     *                   HTMLPurifier instance to overload singleton with,
     *                   or HTMLPurifier_Config instance to configure the
     *                   generated version with.
     *
     * @return HTMLPurifier
     * @note Backwards compatibility, see instance()
     */
    
public static function getInstance($prototype null)
    {
        return 
HTMLPurifier::instance($prototype);
    }
}





/**
 * Converts a stream of HTMLPurifier_Token into an HTMLPurifier_Node,
 * and back again.
 *
 * @note This transformation is not an equivalence.  We mutate the input
 * token stream to make it so; see all [MUT] markers in code.
 */
class HTMLPurifier_Arborize
{
    public static function 
arborize($tokens$config$context) {
        
$definition $config->getHTMLDefinition();
        
$parent = new HTMLPurifier_Token_Start($definition->info_parent);
        
$stack = array($parent->toNode());
        foreach (
$tokens as $token) {
            
$token->skip null// [MUT]
            
$token->carryover null// [MUT]
            
if ($token instanceof HTMLPurifier_Token_End) {
                
$token->start null// [MUT]
                
$r array_pop($stack);
                
//assert($r->name === $token->name);
                //assert(empty($token->attr));
                
$r->endCol $token->col;
                
$r->endLine $token->line;
                
$r->endArmor $token->armor;
                continue;
            }
            
$node $token->toNode();
            
$stack[count($stack)-1]->children[] = $node;
            if (
$token instanceof HTMLPurifier_Token_Start) {
                
$stack[] = $node;
            }
        }
        
//assert(count($stack) == 1);
        
return $stack[0];
    }

    public static function 
flatten($node$config$context) {
        
$level 0;
        
$nodes = array($level => new HTMLPurifier_Queue(array($node)));
        
$closingTokens = array();
        
$tokens = array();
        do {
            while (!
$nodes[$level]->isEmpty()) {
                
$node $nodes[$level]->shift(); // FIFO
                
list($start$end) = $node->toTokenPair();
                if (
$level 0) {
                    
$tokens[] = $start;
                }
                if (
$end !== NULL) {
                    
$closingTokens[$level][] = $end;
                }
                if (
$node instanceof HTMLPurifier_Node_Element) {
                    
$level++;
                    
$nodes[$level] = new HTMLPurifier_Queue();
                    foreach (
$node->children as $childNode) {
                        
$nodes[$level]->push($childNode);
                    }
                }
            }
            
$level--;
            if (
$level && isset($closingTokens[$level])) {
                while (
$token array_pop($closingTokens[$level])) {
                    
$tokens[] = $token;
                }
            }
        } while (
$level 0);
        return 
$tokens;
    }
}



/**
 * Defines common attribute collections that modules reference
 */

class HTMLPurifier_AttrCollections
{

    
/**
     * Associative array of attribute collections, indexed by name.
     * @type array
     */
    
public $info = array();

    
/**
     * Performs all expansions on internal data for use by other inclusions
     * It also collects all attribute collection extensions from
     * modules
     * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance
     * @param HTMLPurifier_HTMLModule[] $modules Hash array of HTMLPurifier_HTMLModule members
     */
    
public function __construct($attr_types$modules)
    {
        
$this->doConstruct($attr_types$modules);
    }

    public function 
doConstruct($attr_types$modules)
    {
        
// load extensions from the modules
        
foreach ($modules as $module) {
            foreach (
$module->attr_collections as $coll_i => $coll) {
                if (!isset(
$this->info[$coll_i])) {
                    
$this->info[$coll_i] = array();
                }
                foreach (
$coll as $attr_i => $attr) {
                    if (
$attr_i === && isset($this->info[$coll_i][$attr_i])) {
                        
// merge in includes
                        
$this->info[$coll_i][$attr_i] = array_merge(
                            
$this->info[$coll_i][$attr_i],
                            
$attr
                        
);
                        continue;
                    }
                    
$this->info[$coll_i][$attr_i] = $attr;
                }
            }
        }
        
// perform internal expansions and inclusions
        
foreach ($this->info as $name => $attr) {
            
// merge attribute collections that include others
            
$this->performInclusions($this->info[$name]);
            
// replace string identifiers with actual attribute objects
            
$this->expandIdentifiers($this->info[$name], $attr_types);
        }
    }

    
/**
     * Takes a reference to an attribute associative array and performs
     * all inclusions specified by the zero index.
     * @param array &$attr Reference to attribute array
     */
    
public function performInclusions(&$attr)
    {
        if (!isset(
$attr[0])) {
            return;
        }
        
$merge $attr[0];
        
$seen  = array(); // recursion guard
        // loop through all the inclusions
        
for ($i 0; isset($merge[$i]); $i++) {
            if (isset(
$seen[$merge[$i]])) {
                continue;
            }
            
$seen[$merge[$i]] = true;
            
// foreach attribute of the inclusion, copy it over
            
if (!isset($this->info[$merge[$i]])) {
                continue;
            }
            foreach (
$this->info[$merge[$i]] as $key => $value) {
                if (isset(
$attr[$key])) {
                    continue;
                } 
// also catches more inclusions
                
$attr[$key] = $value;
            }
            if (isset(
$this->info[$merge[$i]][0])) {
                
// recursion
                
$merge array_merge($merge$this->info[$merge[$i]][0]);
            }
        }
        unset(
$attr[0]);
    }

    
/**
     * Expands all string identifiers in an attribute array by replacing
     * them with the appropriate values inside HTMLPurifier_AttrTypes
     * @param array &$attr Reference to attribute array
     * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance
     */
    
public function expandIdentifiers(&$attr$attr_types)
    {
        
// because foreach will process new elements we add, make sure we
        // skip duplicates
        
$processed = array();

        foreach (
$attr as $def_i => $def) {
            
// skip inclusions
            
if ($def_i === 0) {
                continue;
            }

            if (isset(
$processed[$def_i])) {
                continue;
            }

            
// determine whether or not attribute is required
            
if ($required = (strpos($def_i'*') !== false)) {
                
// rename the definition
                
unset($attr[$def_i]);
                
$def_i trim($def_i'*');
                
$attr[$def_i] = $def;
            }

            
$processed[$def_i] = true;

            
// if we've already got a literal object, move on
            
if (is_object($def)) {
                
// preserve previous required
                
$attr[$def_i]->required = ($required || $attr[$def_i]->required);
                continue;
            }

            if (
$def === false) {
                unset(
$attr[$def_i]);
                continue;
            }

            if (
$t $attr_types->get($def)) {
                
$attr[$def_i] = $t;
                
$attr[$def_i]->required $required;
            } else {
                unset(
$attr[$def_i]);
            }
        }
    }
}





/**
 * Base class for all validating attribute definitions.
 *
 * This family of classes forms the core for not only HTML attribute validation,
 * but also any sort of string that needs to be validated or cleaned (which
 * means CSS properties and composite definitions are defined here too).
 * Besides defining (through code) what precisely makes the string valid,
 * subclasses are also responsible for cleaning the code if possible.
 */

abstract class HTMLPurifier_AttrDef
{

    
/**
     * Tells us whether or not an HTML attribute is minimized.
     * Has no meaning in other contexts.
     * @type bool
     */
    
public $minimized false;

    
/**
     * Tells us whether or not an HTML attribute is required.
     * Has no meaning in other contexts
     * @type bool
     */
    
public $required false;

    
/**
     * Validates and cleans passed string according to a definition.
     *
     * @param string $string String to be validated and cleaned.
     * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object.
     * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object.
     */
    
abstract public function validate($string$config$context);

    
/**
     * Convenience method that parses a string as if it were CDATA.
     *
     * This method process a string in the manner specified at
     * <http://www.w3.org/TR/html4/types.html#h-6.2> 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 String construction info
     * @return HTMLPurifier_AttrDef 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!
     * @param string $string a CSS colour definition
     * @return string
     */
    
protected function mungeRgb($string)
    {
        
$p 's*(d+(.d+)?([%]?))s*';

        if (
preg_match('/(rgba|hsla)(/'$string)) {
            return 
preg_replace('/(rgba|hsla)('.$p.','.$p.','.$p.','.$p.')/''1(2,5,8,11)'$string);
        }

        return 
preg_replace('/(rgb|hsl)('.$p.','.$p.','.$p.')/''1(2,5,8)'$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 array $attr Assoc array of attributes, usually from
     *              HTMLPurifier_Token_Tag::$attr
     * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object.
     * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object
     * @return array 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 array &$attr Attribute array to process (passed by reference)
     * @param string $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 array &$attr Attribute array to process (passed by reference)
     * @param mixed $key Key of attribute to confiscate
     * @return mixed
     */
    
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.
     * @type HTMLPurifier_AttrDef[]
     */
    
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(falsefalsetrue);
    }

    private static function 
makeEnum($in)
    {
        return new 
HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(','$in)));
    }

    
/**
     * Retrieves a type
     * @param string $type String type name
     * @return HTMLPurifier_AttrDef 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('#'$type2);
        } else {
            
$string '';
        }

        if (!isset(
$this->info[$type])) {
            
trigger_error('Cannot retrieve undefined attribute type ' $typeE_USER_ERROR);
            return;
        }
        return 
$this->info[$type]->make($string);
    }

    
/**
     * Sets a new implementation for a type
     * @param string $type String type name
     * @param HTMLPurifier_AttrDef $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, mutating it as necessary.
     * that has valid tokens
     * @param HTMLPurifier_Token $token Token to validate.
     * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config
     * @param HTMLPurifier_Context $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;
        }

        
// 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. <p align="right"> to <p style="text-align:right;">
        
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 <aidan@php.net>
if (!defined('PHP_EOL')) {
    switch (
strtoupper(substr(PHP_OS03))) {
        case 
'WIN':
            
define('PHP_EOL'"rn");
            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 string $class Class to load
     * @return bool
     */
    
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.
     * @param string $class Class path to get
     * @return string
     */
    
public static function getPath($class)
    {
        if (
strncmp('HTMLPurifier'$class12) !== 0) {
            return 
false;
        }
        
// Custom implementations
        
if (strncmp('HTMLPurifier_Language_'$class22) === 0) {
            
$code str_replace('_''-'substr($class22));
            
$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($autoloadtruetrue);
            } 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?
     * @type bool
     */
    
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.
     * @type bool
     */
    
public $optimized null;

    
/**
     * What type of definition is it?
     * @type string
     */
    
public $type;

    
/**
     * Sets up the definition object into the final form, something
     * not done by the constructor
     * @param HTMLPurifier_Config $config
     */
    
abstract protected function doSetup($config);

    
/**
     * Setup function that aborts if already setup
     * @param HTMLPurifier_Config $config
     */
    
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.
     * @type HTMLPurifier_AttrDef[]
     */
    
public $info = array();

    
/**
     * Constructs the info array.  The meat of this class.
     * @param HTMLPurifier_Config $config
     */
    
protected function doSetup($config)
    {
        
$this->info['text-align'] = new HTMLPurifier_AttrDef_Enum(
            array(
'left''right''center''justify'),
            
false
        
);

        
$border_style =
            
$this->info['border-bottom-style'] =
            
$this->info['border-right-style'] =
            
$this->info['border-left-style'] =
            
$this->info['border-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['min-width'] =
        
$this->info['max-width'] =
        
$this->info['min-height'] =
        
$this->info['max-height'] =
        
$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);
    }

    
/**
     * @param HTMLPurifier_Config $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();

        
// vendor specific prefixes of opacity
        
$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'));

        
$border_radius = new HTMLPurifier_AttrDef_CSS_Composite(
            array(
                new 
HTMLPurifier_AttrDef_CSS_Percentage(true), // disallow negative
                
new HTMLPurifier_AttrDef_CSS_Length('0'// disallow negative
            
));

        
$this->info['border-top-left-radius'] =
        
$this->info['border-top-right-radius'] =
        
$this->info['border-bottom-right-radius'] =
        
$this->info['border-bottom-left-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius2);
        
// TODO: support SLASH syntax
        
$this->info['border-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius4);

    }

    
/**
     * @param HTMLPurifier_Config $config
     */
    
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'));
        
$this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
    }

    
/**
     * @param HTMLPurifier_Config $config
     */
    
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.
     * @param HTMLPurifier_Config $config
     * @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 nodes against it.
 */
abstract class HTMLPurifier_ChildDef
{
    
/**
     * Type of child definition, usually right-most part of class name lowercase.
     * Used occasionally in terms of context.
     * @type string
     */
    
public $type;

    
/**
     * 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.
     * @type bool
     */
    
public $allow_empty;

    
/**
     * Lookup array of all elements that this definition could possibly allow.
     * @type array
     */
    
public $elements = array();

    
/**
     * Get lookup of tag names that should not close this element automatically.
     * All other elements will do so.
     * @param HTMLPurifier_Config $config HTMLPurifier_Config object
     * @return array
     */
    
public function getAllowedElements($config)
    {
        return 
$this->elements;
    }

    
/**
     * Validates nodes according to definition and returns modification.
     *
     * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node
     * @param HTMLPurifier_Config $config HTMLPurifier_Config object
     * @param HTMLPurifier_Context $context HTMLPurifier_Context object
     * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children
     */
    
abstract public function validateChildren($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
     * @type string
     */
    
public $version '4.9.2';

    
/**
     * Whether or not to automatically finalize
     * the object if a read operation is done.
     * @type bool
     */
    
public $autoFinalize true;

    
// protected member variables

    /**
     * Namespace indexed array of serials for specific namespaces.
     * @see getSerial() for more info.
     * @type string[]
     */
    
protected $serials = array();

    
/**
     * Serial for entire configuration object.
     * @type string
     */
    
protected $serial;

    
/**
     * Parser for variables.
     * @type HTMLPurifier_VarParser_Flexible
     */
    
protected $parser null;

    
/**
     * Reference HTMLPurifier_ConfigSchema for value checking.
     * @type HTMLPurifier_ConfigSchema
     * @note This is public for introspective purposes. Please don't
     *       abuse!
     */
    
public $def;

    
/**
     * Indexed array of definitions.
     * @type HTMLPurifier_Definition[]
     */
    
protected $definitions;

    
/**
     * Whether or not config is finalized.
     * @type bool
     */
    
protected $finalized false;

    
/**
     * Property list containing configuration directives.
     * @type array
     */
    
protected $plist;

    
/**
     * Whether or not a set is taking place due to an alias lookup.
     * @type bool
     */
    
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.
     * @type bool
     */
    
public $chatty true;

    
/**
     * Current lock; only gets to this namespace are allowed.
     * @type string
     */
    
private $lock;

    
/**
     * Constructor
     * @param HTMLPurifier_ConfigSchema $definition ConfigSchema that defines
     * what directives are allowed.
     * @param HTMLPurifier_PropertyList $parent
     */
    
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 Schema object
     * @return HTMLPurifier_Config Configured 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 HTMLPurifier_Config default object.
     */
    
public static function createDefault()
    {
        
$definition HTMLPurifier_ConfigSchema::instance();
        
$config = new HTMLPurifier_Config($definition);
        return 
$config;
    }

    
/**
     * Retrieves a value from the configuration.
     *
     * @param string $key String key
     * @param mixed $a
     *
     * @return mixed
     */
    
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);
    }

    
/**
     * Retrieves an array of directives to values from a given namespace
     *
     * @param string $namespace String namespace
     *
     * @return array
     */
    
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
     *
     * @param string $namespace Namespace to get serial for
     *
     * @return string
     * @note Revision is handled specially and is removed from the batch
     *       before processing!
     */
    
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
     *
     * @return string
     */
    
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('.'$name2);
            
$ret[$ns][$key] = $value;
        }
        return 
$ret;
    }

    
/**
     * Sets a value to configuration.
     *
     * @param string $key key
     * @param mixed $value value
     * @param mixed $a
     */
    
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
     *
     * @param array $lookup
     *
     * @return string
     */
    
private function _listify($lookup)
    {
        
$list = array();
        foreach (
$lookup as $name => $b) {
            
$list[] = $name;
        }
        return 
implode(', '$list);
    }

    
/**
     * Retrieves object reference to the HTML definition.
     *
     * @param bool $raw Return a copy that has not been setup yet. Must be
     *             called before it's been setup, otherwise won't work.
     * @param bool $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.
     *
     * @return HTMLPurifier_HTMLDefinition
     */
    
public function getHTMLDefinition($raw false$optimized false)
    {
        return 
$this->getDefinition('HTML'$raw$optimized);
    }

    
/**
     * Retrieves object reference to the CSS definition
     *
     * @param bool $raw Return a copy that has not been setup yet. Must be
     *             called before it's been setup, otherwise won't work.
     * @param bool $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.
     *
     * @return HTMLPurifier_CSSDefinition
     */
    
public function getCSSDefinition($raw false$optimized false)
    {
        return 
$this->getDefinition('CSS'$raw$optimized);
    }

    
/**
     * Retrieves object reference to the URI definition
     *
     * @param bool $raw Return a copy that has not been setup yet. Must be
     *             called before it's been setup, otherwise won't work.
     * @param bool $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.
     *
     * @return HTMLPurifier_URIDefinition
     */
    
public function getURIDefinition($raw false$optimized false)
    {
        return 
$this->getDefinition('URI'$raw$optimized);
    }

    
/**
     * Retrieves a definition
     *
     * @param string $type Type of definition: HTML, CSS, etc
     * @param bool $raw Whether or not definition should be returned raw
     * @param bool $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."
     *
     * @throws HTMLPurifier_Exception
     * @return HTMLPurifier_Definition
     */
    
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 ' .
                            
'<a href="http://htmlpurifier.org/docs/enduser-customize.html#optimized">' .
                            
'Customize</a> 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!");
    }

    
/**
     * Initialise definition
     *
     * @param string $type What type of definition to create
     *
     * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition
     * @throws HTMLPurifier_Exception
     */
    
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($nametruetrue);
    }

    
/**
     * @return HTMLPurifier_HTMLDefinition
     */
    
public function maybeGetRawHTMLDefinition()
    {
        return 
$this->getDefinition('HTML'truetrue);
    }
    
    
/**
     * @return HTMLPurifier_CSSDefinition
     */
    
public function maybeGetRawCSSDefinition()
    {
        return 
$this->getDefinition('CSS'truetrue);
    }
    
    
/**
     * @return HTMLPurifier_URIDefinition
     */
    
public function maybeGetRawURIDefinition()
    {
        return 
$this->getDefinition('URI'truetrue);
    }

    
/**
     * Loads configuration values from an array with the following structure:
     * Namespace.Directive => Value
     *
     * @param array $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 => $value2) {
                    
$this->set($namespace .'.'$directive$value2);
                }
            }
        }
    }

    
/**
     * 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 array $allowed List of allowed namespaces/directives
     * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy
     *
     * @return array
     */
    
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_directive1)] = 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('.'$key2);
            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 $array $_GET or $_POST array to import
     * @param string|bool $index Index/name that the config variables are in
     * @param array|bool $allowed List of allowed namespaces/directives
     * @param bool $mq_fix Boolean whether or not to enable magic quotes fix
     * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy
     *
     * @return mixed
     */
    
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.
     *
     * @param array $array $_GET or $_POST array to import
     * @param string|bool $index Index/name that the config variables are in
     * @param array|bool $allowed List of allowed namespaces/directives
     * @param bool $mq_fix Boolean whether or not to enable magic quotes fix
     */
    
public function mergeArrayFromForm($array$index false$allowed true$mq_fix true)
    {
         
$ret HTMLPurifier_Config::prepareArrayFromForm($array$index$allowed$mq_fix$this->def);
         
$this->loadArray($ret);
    }

    
/**
     * Prepares an array from a form into something usable for the more
     * strict parts of HTMLPurifier_Config
     *
     * @param array $array $_GET or $_POST array to import
     * @param string|bool $index Index/name that the config variables are in
     * @param array|bool $allowed List of allowed namespaces/directives
     * @param bool $mq_fix Boolean whether or not to enable magic quotes fix
     * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy
     *
     * @return array
     */
    
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 string $filename Name of ini file
     */
    
public function loadIni($filename)
    {
        if (
$this->isFinalized('Cannot load directives after finalization')) {
            return;
        }
        
$array parse_ini_file($filenametrue);
        
$this->loadArray($array);
    }

    
/**
     * Checks whether or not the configuration object is finalized.
     *
     * @param string|bool $error String error message, or false for no error
     *
     * @return bool
     */
    
public function isFinalized($error false)
    {
        if (
$this->finalized && $error) {
            
$this->triggerError($errorE_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.
     *
     * @param string $msg An error message
     * @param int $no An error number
     */
    
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.
     *
     * @return string
     */
    
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.
     * @type array
     * @note This shares the exact same structure as HTMLPurifier_Config::$conf
     */
    
public $defaults = array();

    
/**
     * The default property list. Do not edit this property list.
     * @type array
     */
    
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.
     * @type array
     */
    
public $info = array();

    
/**
     * Application-wide singleton
     * @type HTMLPurifier_ConfigSchema
     */
    
protected static $singleton;

    public function 
__construct()
    {
        
$this->defaultPlist = new HTMLPurifier_PropertyList();
    }

    
/**
     * Unserializes the default ConfigSchema.
     * @return HTMLPurifier_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.
     * @param HTMLPurifier_ConfigSchema $prototype
     * @return HTMLPurifier_ConfigSchema
     */
    
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 string $key Name of directive
     * @param mixed $default Default value of directive
     * @param string $type Allowed type of the directive. See
     *      HTMLPurifier_DirectiveDef::$type for allowed values
     * @param bool $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 string $key Name of Directive
     * @param array $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 string $key Name of directive
     * @param array $allowed Lookup array of allowed values
     */
    
public function addAllowedValues($key$allowed)
    {
        
$this->info[$key]->allowed $allowed;
    }

    
/**
     * Defines a directive alias for backwards compatibility
     * @param string $key Directive that will be aliased
     * @param string $new_key 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) == && isset($v->allow_null)) {
                
$this->info[$key] = -$v->type;
            }
        }
    }
}





/**
 * @todo Unit test
 */
class HTMLPurifier_ContentSets
{

    
/**
     * List of content set strings (pipe separators) indexed by name.
     * @type array
     */
    
public $info = array();

    
/**
     * List of content set lookups (element => true) indexed by name.
     * @type array
     * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets
     */
    
public $lookup = array();

    
/**
     * Synchronized list of defined content sets (keys of info).
     * @type array
     */
    
protected $keys = array();
    
/**
     * Synchronized list of defined content values (values of info).
     * @type array
     */
    
protected $values = array();

    
/**
     * Merges in module's content sets, expands identifiers in the content
     * sets and populates the keys, values and lookup member variables.
     * @param HTMLPurifier_HTMLModule[] $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) {
            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 HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference
     * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef
     */
    
public function generateChildDef(&$def$module)
    {
        if (!empty(
$def->child)) { // already done!
            
return;
        }
        
$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 HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted
     * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef
     * @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 $string List of elements
     * @return array Lookup array of elements
     */
    
protected function convertToLookup($string)
    {
        
$array explode('|'str_replace(' '''$string));
        
$ret = array();
        foreach (
$array as $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.
     * @type array
     */
    
private $_storage = array();

    
/**
     * Registers a variable into the context.
     * @param string $name String name
     * @param mixed $ref Reference to variable to be registered
     */
    
public function register($name, &$ref)
    {
        if (
array_key_exists($name$this->_storage)) {
            
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 string $name String name
     * @param bool $ignore_error Boolean whether or not to ignore error
     * @return mixed
     */
    
public function &get($name$ignore_error false)
    {
        if (!
array_key_exists($name$this->_storage)) {
            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];
    }

    
/**
     * Destroys a variable in the context.
     * @param string $name String name
     */
    
public function destroy($name)
    {
        if (!
array_key_exists($name$this->_storage)) {
            
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 string $name String name
     * @return bool
     */
    
public function exists($name)
    {
        return 
array_key_exists($name$this->_storage);
    }

    
/**
     * Loads a series of variables from an associative array
     * @param array $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
{
    
/**
     * @type string
     */
    
public $type;

    
/**
     * @param string $type 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 HTMLPurifier_Config $config Instance of HTMLPurifier_Config
     * @return string
     */
    
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 string $key Key to test
     * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against
     * @return bool
     */
    
public function isOld($key$config)
    {
        if (
substr_count($key',') < 2) {
            return 
true;
        }
        list(
$version$hash$revision) = explode(','$key3);
        
$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 HTMLPurifier_Definition $def Definition object to check
     * @return bool 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
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     */
    
abstract public function add($def$config);

    
/**
     * Unconditionally saves a definition object to the cache
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     */
    
abstract public function set($def$config);

    
/**
     * Replace an object in the cache
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     */
    
abstract public function replace($def$config);

    
/**
     * Retrieves a definition object from the cache
     * @param HTMLPurifier_Config $config
     */
    
abstract public function get($config);

    
/**
     * Removes a definition object to the cache
     * @param HTMLPurifier_Config $config
     */
    
abstract public function remove($config);

    
/**
     * Clears all objects from cache
     * @param HTMLPurifier_Config $config
     */
    
abstract public function flush($config);

    
/**
     * Clears all expired (older version or revision) objects from cache
     * @note Be careful implementing this method as flush. Flush must
     *       not interfere with other Definition types, and cleanup()
     *       should not be repeatedly called by userland code.
     * @param HTMLPurifier_Config $config
     */
    
abstract public function cleanup($config);
}





/**
 * Responsible for creating definition caches.
 */
class HTMLPurifier_DefinitionCacheFactory
{
    
/**
     * @type array
     */
    
protected $caches = array('Serializer' => array());

    
/**
     * @type array
     */
    
protected $implementations = array();

    
/**
     * @type HTMLPurifier_DefinitionCache_Decorator[]
     */
    
protected $decorators = array();

    
/**
     * Initialize default decorators
     */
    
public function setup()
    {
        
$this->addDecorator('Cleanup');
    }

    
/**
     * Retrieves an instance of global definition cache factory.
     * @param HTMLPurifier_DefinitionCacheFactory $prototype
     * @return HTMLPurifier_DefinitionCacheFactory
     */
    
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 string $short Short name of cache object, for reference
     * @param string $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 string $type Name of definitions handled by cache
     * @param HTMLPurifier_Config $config Config instance
     * @return mixed
     */
    
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 HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator
     */
    
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
     * @type string
     */
    
public $name;

    
/**
     * List of standard modules (string identifiers or literal objects)
     * that this doctype uses
     * @type array
     */
    
public $modules = array();

    
/**
     * List of modules to use for tidying up code
     * @type array
     */
    
public $tidyModules = array();

    
/**
     * Is the language derived from XML (i.e. XHTML)?
     * @type bool
     */
    
public $xml true;

    
/**
     * List of aliases for this doctype
     * @type array
     */
    
public $aliases = array();

    
/**
     * Public DTD identifier
     * @type string
     */
    
public $dtdPublic;

    
/**
     * System DTD identifier
     * @type string
     */
    
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.
     * @type array
     */
    
protected $doctypes;

    
/**
     * Lookup table of aliases to real doctype names.
     * @type array
     */
    
protected $aliases;

    
/**
     * Registers a doctype to the registry
     * @note Accepts a fully-formed doctype object, or the
     *       parameters for constructing a doctype object
     * @param string $doctype Name of doctype or literal doctype object
     * @param bool $xml
     * @param array $modules Modules doctype will load
     * @param array $tidy_modules Modules doctype will load for certain modes
     * @param array $aliases Alias names for doctype
     * @param string $dtd_public
     * @param string $dtd_system
     * @return HTMLPurifier_Doctype 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 string $doctype Name of doctype
     * @return HTMLPurifier_Doctype 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).
     * @param HTMLPurifier_Config $config
     * @return HTMLPurifier_Doctype
     */
    
public function make($config)
    {
        return clone 
$this->get($this->getDoctypeFromConfig($config));
    }

    
/**
     * Retrieves the doctype from the configuration object
     * @param HTMLPurifier_Config $config
     * @return string
     */
    
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?
     * @type bool
     */
    
public $standalone true;

    
/**
     * Associative array of attribute name to HTMLPurifier_AttrDef.
     * @type array
     * @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.
     * @type array
     */
    
public $attr_transform_pre = array();

    
/**
     * List of tags HTMLPurifier_AttrTransform to be done after validation.
     * @type array
     */
    
public $attr_transform_post = array();

    
/**
     * HTMLPurifier_ChildDef of this tag.
     * @type HTMLPurifier_ChildDef
     */
    
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
     * @type string
     */
    
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
     * @type string
     */
    
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.
     * @type bool
     */
    
public $descendants_are_inline false;

    
/**
     * List of the names of required attributes this element has.
     * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement()
     * @type array
     */
    
public $required_attr = array();

    
/**
     * Lookup table of tags excluded from all descendants of this tag.
     * @type array
     * @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.
     * @type array
     */
    
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.
     * @type string
     */
    
public $wrap;

    
/**
     * Whether or not this is a formatting element affected by the
     * "Active Formatting Elements" algorithm.
     * @type bool
     */
    
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.
     * @param HTMLPurifier_ElementDef $def
     */
    
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.
     * @param string $in Input encoding
     * @param string $out Output encoding
     * @param string $text The text to convert
     * @return string
     */
    
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.
     * @param string $in Input encoding
     * @param string $out Output encoding
     * @param string $text The text to convert
     * @param int $max_chunk_size
     * @return string
     */
    
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$outsubstr($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.
     *
     * Specifically, it will permit:
     * x{9}x{A}x{D}x{20}-x{7E}x{A0}-x{D7FF}x{E000}-x{FFFD}x{10000}-x{10FFFF}
     * Source: https://www.w3.org/TR/REC-xml/#NT-Char
     * Arguably this function should be modernized to the HTML5 set
     * of allowed characters:
     * https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream
     * which simultaneously expand and restrict the set of allowed characters.
     *
     * @param string $str The string to clean
     * @param bool $force_php
     * @return string
     *
     * @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 <http://iki.fi/hsivonen/php-utf8/> 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.
     */
    
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.
        
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 (== $mState) {
                
// When mState is zero we expect either a US-ASCII character
                // or a multi-octet sequence.
                
if (== (0x80 & ($in))) {
                    
// US-ASCII, pass straight through.
                    
if (($in <= 31 || $in == 127) &&
                        !(
$in == || $in == 13 || $in == 10// save rtn
                    
) {
                        
// 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 (
== --$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 (((== $mBytes) && ($mUcs4 0x0080)) ||
                            ((
== $mBytes) && ($mUcs4 0x0800)) ||
                            ((
== $mBytes) && ($mUcs4 0x10000)) ||
                            (
$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) ||
                                (
0xE000 <= $mUcs4 && 0xFFFD >= $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
     *       <http://forums.devnetwork.net/viewtopic.php?p=191404#191404>,
     *       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 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;
    }

    
/**
     * @return bool
     */
    
public static function iconvAvailable()
    {
        static 
$iconv null;
        if (
$iconv === null) {
            
$iconv function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE;
        }
        return 
$iconv;
    }

    
/**
     * Convert a string to UTF-8 based on configuration.
     * @param string $str The string to convert
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return string
     */
    
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 ' $encodingE_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($strself::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.
     * @param string $str The string to convert
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return string
     * @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($strarray_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 string $str UTF-8 string to be converted to ASCII
     * @return string 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.
     *
     * @return int 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'"xCExB1" 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(
"xC2xA5" => '\', "xE2x80xBE" => '~');
                case '
johab':
                    return array("xE2x82xA9" => '
\');
            }
            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.
     * @type array
     */
    
public $table;

    
/**
     * Sets up the entity lookup table from the serialized file contents.
     * @param bool $file
     * @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 bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with.
     * @return HTMLPurifier_EntityLookup
     */
    
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.
     * @type HTMLPurifier_EntityLookup
     */
    
protected $_entity_lookup;

    
/**
     * Callback regex string for entities in text.
     * @type string
     */
    
protected $_textEntitiesRegex;

    
/**
     * Callback regex string for entities in attributes.
     * @type string
     */
    
protected $_attrEntitiesRegex;

    
/**
     * Tests if the beginning of a string is a semi-optional regex
     */
    
protected $_semiOptionalPrefixRegex;

    public function 
__construct() {
        
// From
        // http://stackoverflow.com/questions/15532252/why-is-reg-being-rendered-as-without-the-bounding-semicolon
        
$semi_optional "quot|QUOT|lt|LT|gt|GT|amp|AMP|AElig|Aacute|Acirc|Agrave|Aring|Atilde|Auml|COPY|Ccedil|ETH|Eacute|Ecirc|Egrave|Euml|Iacute|Icirc|Igrave|Iuml|Ntilde|Oacute|Ocirc|Ograve|Oslash|Otilde|Ouml|REG|THORN|Uacute|Ucirc|Ugrave|Uuml|Yacute|aacute|acirc|acute|aelig|agrave|aring|atilde|auml|brvbar|ccedil|cedil|cent|copy|curren|deg|divide|eacute|ecirc|egrave|eth|euml|frac12|frac14|frac34|iacute|icirc|iexcl|igrave|iquest|iuml|laquo|macr|micro|middot|nbsp|not|ntilde|oacute|ocirc|ograve|ordf|ordm|oslash|otilde|ouml|para|plusmn|pound|raquo|reg|sect|shy|sup1|sup2|sup3|szlig|thorn|times|uacute|ucirc|ugrave|uml|uuml|yacute|yen|yuml";

        
// NB: three empty captures to put the fourth match in the right
        // place
        
$this->_semiOptionalPrefixRegex "/&()()()($semi_optional)/";

        
$this->_textEntitiesRegex =
            
'/&(?:'.
            
// hex
            
'[#]x([a-fA-F0-9]+);?|'.
            
// dec
            
'[#]0*(d+);?|'.
            
// string (mandatory semicolon)
            // NB: order matters: match semicolon preferentially
            
'([A-Za-z_:][A-Za-z0-9.-_:]*);|'.
            
// string (optional semicolon)
            
"($semi_optional)".
            
')/';

        
$this->_attrEntitiesRegex =
            
'/&(?:'.
            
// hex
            
'[#]x([a-fA-F0-9]+);?|'.
            
// dec
            
'[#]0*(d+);?|'.
            
// string (mandatory semicolon)
            // NB: order matters: match semicolon preferentially
            
'([A-Za-z_:][A-Za-z0-9.-_:]*);|'.
            
// string (optional semicolon)
            // don't match if trailing is equals or alphanumeric (URL
            // like)
            
"($semi_optional)(?![=;A-Za-z0-9])".
            
')/';

    }

    
/**
     * Substitute entities with the parsed equivalents.  Use this on
     * textual data in an HTML document (as opposed to attributes.)
     *
     * @param string $string String to have entities parsed.
     * @return string Parsed string.
     */
    
public function substituteTextEntities($string)
    {
        return 
preg_replace_callback(
            
$this->_textEntitiesRegex,
            array(
$this'entityCallback'),
            
$string
        
);
    }

    
/**
     * Substitute entities with the parsed equivalents.  Use this on
     * attribute contents in documents.
     *
     * @param string $string String to have entities parsed.
     * @return string Parsed string.
     */
    
public function substituteAttrEntities($string)
    {
        return 
preg_replace_callback(
            
$this->_attrEntitiesRegex,
            array(
$this'entityCallback'),
            
$string
        
);
    }

    
/**
     * Callback function for substituteNonSpecialEntities() that does the work.
     *
     * @param array $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).
     * @return string Replacement string.
     */

    
protected function entityCallback($matches)
    {
        
$entity $matches[0];
        
$hex_part = @$matches[1];
        
$dec_part = @$matches[2];
        
$named_part = empty($matches[3]) ? @$matches[4] : $matches[3];
        if (
$hex_part !== NULL && $hex_part !== "") {
            return 
HTMLPurifier_Encoder::unichr(hexdec($hex_part));
        } elseif (
$dec_part !== NULL && $dec_part !== "") {
            return 
HTMLPurifier_Encoder::unichr((int) $dec_part);
        } else {
            if (!
$this->_entity_lookup) {
                
$this->_entity_lookup HTMLPurifier_EntityLookup::instance();
            }
            if (isset(
$this->_entity_lookup->table[$named_part])) {
                return 
$this->_entity_lookup->table[$named_part];
            } else {
                
// exact match didn't match anything, so test if
                // any of the semicolon optional match the prefix.
                // Test that this is an EXACT match is important to
                // prevent infinite loop
                
if (!empty($matches[3])) {
                    return 
preg_replace_callback(
                        
$this->_semiOptionalPrefixRegex,
                        array(
$this'entityCallback'),
                        
$entity
                    
);
                }
                return 
$entity;
            }
        }
    }

    
// LEGACY CODE BELOW

    /**
     * Callback regex string for parsing entities.
     * @type string
     */
    
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.
     * @type array
     */
    
protected $_special_dec2str =
            array(
                    
34 => '"',
                    
38 => '&',
                    
39 => "'",
                    
60 => '<',
                    
62 => '>'
            
);

    
/**
     * Stripped entity names to decimal conversion table for special entities.
     * @type array
     */
    
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 String to have non-special entities parsed.
     * @return string 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 array $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).
     * @return string 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 String to have non-special entities parsed.
     * @return string 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 array $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).
     * @return string 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_dec2str[$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;

    
/**
     * @type array
     */
    
protected $errors;

    
/**
     * @type array
     */
    
protected $_current;

    
/**
     * @type array
     */
    
protected $_stacks = array(array());

    
/**
     * @type HTMLPurifier_Language
     */
    
protected $locale;

    
/**
     * @type HTMLPurifier_Generator
     */
    
protected $generator;

    
/**
     * @type HTMLPurifier_Context
     */
    
protected $context;

    
/**
     * @type array
     */
    
protected $lines = array();

    
/**
     * @param HTMLPurifier_Context $context
     */
    
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 int $severity Error severity, PHP error style (don't use E_USER_)
     * @param string $msg Error message text
     */
    
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 ...
        // 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->linesSORT_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
     */
    
public function getRaw()
    {
        return 
$this->errors;
    }

    
/**
     * Default HTML formatting implementation for error messages
     * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature
     * @param array $errors Errors array to display; used for recursion.
     * @return string
     */
    
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 
'<p>' $this->locale->getMessage('ErrorCollector: No errors') . '</p>';
        } else {
            return 
'<ul><li>' implode('</li><li>'$ret) . '</li></ul>';
        }

    }

    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 .= '<div>';
                
// W3C uses an icon to indicate the severity of the error.
                
$error $this->locale->getErrorName($severity);
                
$string .= "<span class="error e$severity"><strong>$error</strong></span> ";
                if (!
is_null($line) && !is_null($col)) {
                    
$string .= "<em class="location">Line $line, Column $col: </em> ";
                } else {
                    
$string .= '<em class="location">End of Document: </em> ';
                }
                
$string .= '<strong class="description">' $this->generator->escape($msg) . '</strong> ';
                
$string .= '</div>';
                
// Here, have a marker for the character on the column appropriate.
                // Be sure to clip extremely long lines.
                //$string .= '<pre>';
                //$string .= '';
                //$string .= '</pre>';
                
$ret[] = $string;
            }
            foreach (
$current->children as $array) {
                
$context[] = $current;
                
$stack array_merge($stackarray_reverse($arraytrue));
                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.
     * @type string
     */
    
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')
     * @type mixed
     */
    
public $value;

    
/**
     * Errors registered for this structure.
     * @type array
     */
    
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']
     * @type array
     */
    
public $children = array();

    
/**
     * @param string $type
     * @param string $id
     * @return mixed
     */
    
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];
    }

    
/**
     * @param int $severity
     * @param string $message
     */
    
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.
     * @type string
     */
    
public $name;

    
/**
     * Pre-processor function, handles HTML before HTML Purifier
     * @param string $html
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return string
     */
    
public function preFilter($html$config$context)
    {
        return 
$html;
    }

    
/**
     * Post-processor function, handles HTML after HTML Purifier
     * @param string $html
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return string
     */
    
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.
     * @type bool
     */
    
private $_xhtml true;

    
/**
     * :HACK: Whether or not generator should comment the insides of <script> tags.
     * @type bool
     */
    
private $_scriptFix false;

    
/**
     * Cache of HTMLDefinition during HTML output to determine whether or
     * not attributes should be minimized.
     * @type HTMLPurifier_HTMLDefinition
     */
    
private $_def;

    
/**
     * Cache of %Output.SortAttr.
     * @type bool
     */
    
private $_sortAttr;

    
/**
     * Cache of %Output.FlashCompat.
     * @type bool
     */
    
private $_flashCompat;

    
/**
     * Cache of %Output.FixInnerHTML.
     * @type bool
     */
    
private $_innerHTMLFix;

    
/**
     * Stack for keeping track of object information when outputting IE
     * compatibility code.
     * @type array
     */
    
private $_flashStack = array();

    
/**
     * Configuration for the generator
     * @type HTMLPurifier_Config
     */
    
protected $config;

    
/**
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     */
    
public function __construct($config$context)
    {
        
$this->config $config;
        
$this->_scriptFix $config->get('Output.CommentScriptContents');
        
$this->_innerHTMLFix $config->get('Output.FixInnerHTML');
        
$this->_sortAttr $config->get('Output.SortAttr');
        
$this->_flashCompat $config->get('Output.FlashCompat');
        
$this->_def $config->getHTMLDefinition();
        
$this->_xhtml $this->_def->doctype->xml;
    }

    
/**
     * Generates HTML from an array of tokens.
     * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token
     * @return string Generated HTML
     */
    
public function generateFromTokens($tokens)
    {
        if (!
$tokens) {
            return 
'';
        }

        
// Basic algorithm
        
$html '';
        for (
$i 0$size count($tokens); $i $size$i++) {
            if (
$this->_scriptFix && $tokens[$i]->name === 'script'
                
&& $i $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) {
                
// script special case
                // the contents of the script block must be ONE token
                // for this to work.
                
$html .= $this->generateFromToken($tokens[$i++]);
                
$html .= $this->generateScriptFromToken($tokens[$i++]);
            }
            
$html .= $this->generateFromToken($tokens[$i]);
        }

        
// Tidy cleanup
        
if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) {
            
$tidy = new Tidy;
            
$tidy->parseString(
                
$html,
                array(
                   
'indent'=> true,
                   
'output-xhtml' => $this->_xhtml,
                   
'show-body-only' => true,
                   
'indent-spaces' => 2,
                   
'wrap' => 68,
                ),
                
'utf8'
            
);
            
$tidy->cleanRepair();
            
$html = (string) $tidy// explicit cast necessary
        
}

        
// Normalize newlines to system defined value
        
if ($this->config->get('Core.NormalizeNewlines')) {
            
$nl $this->config->get('Output.Newline');
            if (
$nl === null) {
                
$nl PHP_EOL;
            }
            if (
$nl !== "n") {
                
$html str_replace("n"$nl$html);
            }
        }
        return 
$html;
    }

    
/**
     * Generates HTML from a single token.
     * @param HTMLPurifier_Token $token HTMLPurifier_Token object.
     * @return string Generated HTML
     */
    
public function generateFromToken($token)
    {
        if (!
$token instanceof HTMLPurifier_Token) {
            
trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object'E_USER_WARNING);
            return 
'';

        } elseif (
$token instanceof HTMLPurifier_Token_Start) {
            
$attr $this->generateAttributes($token->attr$token->name);
            if (
$this->_flashCompat) {
                if (
$token->name == "object") {
                    
$flash = new stdclass();
                    
$flash->attr $token->attr;
                    
$flash->param = array();
                    
$this->_flashStack[] = $flash;
                }
            }
            return 
'<' $token->name . ($attr ' ' '') . $attr '>';

        } elseif (
$token instanceof HTMLPurifier_Token_End) {
            
$_extra '';
            if (
$this->_flashCompat) {
                if (
$token->name == "object" && !empty($this->_flashStack)) {
                    
// doesn't do anything for now
                
}
            }
            return 
$_extra '</' $token->name '>';

        } elseif (
$token instanceof HTMLPurifier_Token_Empty) {
            if (
$this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) {
                
$this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value'];
            }
            
$attr $this->generateAttributes($token->attr$token->name);
             return 
'<' $token->name . ($attr ' ' '') . $attr .
                ( 
$this->_xhtml ' /''' // <br /> v. <br>
                
'>';

        } elseif (
$token instanceof HTMLPurifier_Token_Text) {
            return 
$this->escape($token->dataENT_NOQUOTES);

        } elseif (
$token instanceof HTMLPurifier_Token_Comment) {
            return 
'<!--' $token->data '-->';
        } else {
            return 
'';

        }
    }

    
/**
     * Special case processor for the contents of script tags
     * @param HTMLPurifier_Token $token HTMLPurifier_Token object.
     * @return string
     * @warning This runs into problems if there's already a literal
     *          --> somewhere inside the script contents.
     */
    
public function generateScriptFromToken($token)
    {
        if (!
$token instanceof HTMLPurifier_Token_Text) {
            return 
$this->generateFromToken($token);
        }
        
// Thanks <http://lachy.id.au/log/2005/05/script-comments>
        
$data preg_replace('#//s*$#'''$token->data);
        return 
'<!--//--><![CDATA[//><!--' "n" trim($data) . "n" '//--><!]]>';
    }

    
/**
     * Generates attribute declarations from attribute array.
     * @note This does not include the leading or trailing space.
     * @param array $assoc_array_of_attributes Attribute array
     * @param string $element Name of element attributes are for, used to check
     *        attribute minimization.
     * @return string Generated HTML fragment for insertion.
     */
    
public function generateAttributes($assoc_array_of_attributes$element '')
    {
        
$html '';
        if (
$this->_sortAttr) {
            
ksort($assoc_array_of_attributes);
        }
        foreach (
$assoc_array_of_attributes as $key => $value) {
            if (!
$this->_xhtml) {
                
// Remove namespaced attributes
                
if (strpos($key':') !== false) {
                    continue;
                }
                
// Check if we should minimize the attribute: val="val" -> val
                
if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) {
                    
$html .= $key ' ';
                    continue;
                }
            }
            
// Workaround for Internet Explorer innerHTML bug.
            // Essentially, Internet Explorer, when calculating
            // innerHTML, omits quotes if there are no instances of
            // angled brackets, quotes or spaces.  However, when parsing
            // HTML (for example, when you assign to innerHTML), it
            // treats backticks as quotes.  Thus,
            //      <img alt="``" />
            // becomes
            //      <img alt=`` />
            // becomes
            //      <img alt='' />
            // Fortunately, all we need to do is trigger an appropriate
            // quoting style, which we do by adding an extra space.
            // This also is consistent with the W3C spec, which states
            // that user agents may ignore leading or trailing
            // whitespace (in fact, most don't, at least for attributes
            // like alt, but an extra space at the end is barely
            // noticeable).  Still, we have a configuration knob for
            // this, since this transformation is not necesary if you
            // don't process user input with innerHTML or you don't plan
            // on supporting Internet Explorer.
            
if ($this->_innerHTMLFix) {
                if (
strpos($value'`') !== false) {
                    
// check if correct quoting style would not already be
                    // triggered
                    
if (strcspn($value'"' <>') === strlen($value)) {
                        // protect!
                        $value .= ' ';
                    }
                }
            }
            $html .= $key.'
="'.$this->escape($value).'" ';
        }
        return rtrim($html);
    }

    /**
     * Escapes raw text data.
     * @todo This really ought to be protected, but until we have a facility
     *       for properly generating HTML here w/o using tokens, it stays
     *       public.
     * @param string $string String data to escape for HTML.
     * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is
     *               permissible for non-attribute output.
     * @return string escaped data.
     */
    public function escape($string, $quote = null)
    {
        // Workaround for APC bug on Mac Leopard reported by sidepodcast
        // http://htmlpurifier.org/phorum/read.php?3,4823,4846
        if ($quote === null) {
            $quote = ENT_COMPAT;
        }
        return htmlspecialchars($string, $quote, '
UTF-8');
    }
}





/**
 * Definition of the purified HTML that describes allowed children,
 * attributes, and many other things.
 *
 * Conventions:
 *
 * All member variables that are prefixed with info
 * (including the main $info array) are used by HTML Purifier internals
 * and should not be directly edited when customizing the HTMLDefinition.
 * They can usually be set via configuration directives or custom
 * modules.
 *
 * On the other hand, member variables without the info prefix are used
 * internally by the HTMLDefinition and MUST NOT be used by other HTML
 * Purifier internals. Many of them, however, are public, and may be
 * edited by userspace code to tweak the behavior of HTMLDefinition.
 *
 * @note This class is inspected by Printer_HTMLDefinition; please
 *       update that class if things here change.
 *
 * @warning Directives that change this object'
s structure must be in
 
*          the HTML or Attr namespace!
 */
class 
HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition
{

    
// FULLY-PUBLIC VARIABLES ---------------------------------------------

    /**
     * Associative array of element names to HTMLPurifier_ElementDef.
     * @type HTMLPurifier_ElementDef[]
     */
    
public $info = array();

    
/**
     * Associative array of global attribute name to attribute definition.
     * @type array
     */
    
public $info_global_attr = array();

    
/**
     * String name of parent element HTML will be going into.
     * @type string
     */
    
public $info_parent 'div';

    
/**
     * Definition for parent element, allows parent element to be a
     * tag that's not allowed inside the HTML fragment.
     * @type HTMLPurifier_ElementDef
     */
    
public $info_parent_def;

    
/**
     * String name of element used to wrap inline elements in block context.
     * @type string
     * @note This is rarely used except for BLOCKQUOTEs in strict mode
     */
    
public $info_block_wrapper 'p';

    
/**
     * Associative array of deprecated tag name to HTMLPurifier_TagTransform.
     * @type array
     */
    
public $info_tag_transform = array();

    
/**
     * Indexed list of HTMLPurifier_AttrTransform to be performed before validation.
     * @type HTMLPurifier_AttrTransform[]
     */
    
public $info_attr_transform_pre = array();

    
/**
     * Indexed list of HTMLPurifier_AttrTransform to be performed after validation.
     * @type HTMLPurifier_AttrTransform[]
     */
    
public $info_attr_transform_post = array();

    
/**
     * Nested lookup array of content set name (Block, Inline) to
     * element name to whether or not it belongs in that content set.
     * @type array
     */
    
public $info_content_sets = array();

    
/**
     * Indexed list of HTMLPurifier_Injector to be used.
     * @type HTMLPurifier_Injector[]
     */
    
public $info_injector = array();

    
/**
     * Doctype object
     * @type HTMLPurifier_Doctype
     */
    
public $doctype;



    
// RAW CUSTOMIZATION STUFF --------------------------------------------

    /**
     * Adds a custom attribute to a pre-existing element
     * @note This is strictly convenience, and does not have a corresponding
     *       method in HTMLPurifier_HTMLModule
     * @param string $element_name Element name to add attribute to
     * @param string $attr_name Name of attribute
     * @param mixed $def Attribute definition, can be string or object, see
     *             HTMLPurifier_AttrTypes for details
     */
    
public function addAttribute($element_name$attr_name$def)
    {
        
$module $this->getAnonymousModule();
        if (!isset(
$module->info[$element_name])) {
            
$element $module->addBlankElement($element_name);
        } else {
            
$element $module->info[$element_name];
        }
        
$element->attr[$attr_name] = $def;
    }

    
/**
     * Adds a custom element to your HTML definition
     * @see HTMLPurifier_HTMLModule::addElement() for detailed
     *       parameter and return value descriptions.
     */
    
public function addElement($element_name$type$contents$attr_collections$attributes = array())
    {
        
$module $this->getAnonymousModule();
        
// assume that if the user is calling this, the element
        // is safe. This may not be a good idea
        
$element $module->addElement($element_name$type$contents$attr_collections$attributes);
        return 
$element;
    }

    
/**
     * Adds a blank element to your HTML definition, for overriding
     * existing behavior
     * @param string $element_name
     * @return HTMLPurifier_ElementDef
     * @see HTMLPurifier_HTMLModule::addBlankElement() for detailed
     *       parameter and return value descriptions.
     */
    
public function addBlankElement($element_name)
    {
        
$module  $this->getAnonymousModule();
        
$element $module->addBlankElement($element_name);
        return 
$element;
    }

    
/**
     * Retrieves a reference to the anonymous module, so you can
     * bust out advanced features without having to make your own
     * module.
     * @return HTMLPurifier_HTMLModule
     */
    
public function getAnonymousModule()
    {
        if (!
$this->_anonModule) {
            
$this->_anonModule = new HTMLPurifier_HTMLModule();
            
$this->_anonModule->name 'Anonymous';
        }
        return 
$this->_anonModule;
    }

    private 
$_anonModule null;

    
// PUBLIC BUT INTERNAL VARIABLES --------------------------------------

    /**
     * @type string
     */
    
public $type 'HTML';

    
/**
     * @type HTMLPurifier_HTMLModuleManager
     */
    
public $manager;

    
/**
     * Performs low-cost, preliminary initialization.
     */
    
public function __construct()
    {
        
$this->manager = new HTMLPurifier_HTMLModuleManager();
    }

    
/**
     * @param HTMLPurifier_Config $config
     */
    
protected function doSetup($config)
    {
        
$this->processModules($config);
        
$this->setupConfigStuff($config);
        unset(
$this->manager);

        
// cleanup some of the element definitions
        
foreach ($this->info as $k => $v) {
            unset(
$this->info[$k]->content_model);
            unset(
$this->info[$k]->content_model_type);
        }
    }

    
/**
     * Extract out the information from the manager
     * @param HTMLPurifier_Config $config
     */
    
protected function processModules($config)
    {
        if (
$this->_anonModule) {
            
// for user specific changes
            // this is late-loaded so we don't have to deal with PHP4
            // reference wonky-ness
            
$this->manager->addModule($this->_anonModule);
            unset(
$this->_anonModule);
        }

        
$this->manager->setup($config);
        
$this->doctype $this->manager->doctype;

        foreach (
$this->manager->modules as $module) {
            foreach (
$module->info_tag_transform as $k => $v) {
                if (
$v === false) {
                    unset(
$this->info_tag_transform[$k]);
                } else {
                    
$this->info_tag_transform[$k] = $v;
                }
            }
            foreach (
$module->info_attr_transform_pre as $k => $v) {
                if (
$v === false) {
                    unset(
$this->info_attr_transform_pre[$k]);
                } else {
                    
$this->info_attr_transform_pre[$k] = $v;
                }
            }
            foreach (
$module->info_attr_transform_post as $k => $v) {
                if (
$v === false) {
                    unset(
$this->info_attr_transform_post[$k]);
                } else {
                    
$this->info_attr_transform_post[$k] = $v;
                }
            }
            foreach (
$module->info_injector as $k => $v) {
                if (
$v === false) {
                    unset(
$this->info_injector[$k]);
                } else {
                    
$this->info_injector[$k] = $v;
                }
            }
        }
        
$this->info $this->manager->getElements();
        
$this->info_content_sets $this->manager->contentSets->lookup;
    }

    
/**
     * Sets up stuff based on config. We need a better way of doing this.
     * @param HTMLPurifier_Config $config
     */
    
protected function setupConfigStuff($config)
    {
        
$block_wrapper $config->get('HTML.BlockWrapper');
        if (isset(
$this->info_content_sets['Block'][$block_wrapper])) {
            
$this->info_block_wrapper $block_wrapper;
        } else {
            
trigger_error(
                
'Cannot use non-block element as block wrapper',
                
E_USER_ERROR
            
);
        }

        
$parent $config->get('HTML.Parent');
        
$def $this->manager->getElement($parenttrue);
        if (
$def) {
            
$this->info_parent $parent;
            
$this->info_parent_def $def;
        } else {
            
trigger_error(
                
'Cannot use unrecognized element as parent',
                
E_USER_ERROR
            
);
            
$this->info_parent_def $this->manager->getElement($this->info_parenttrue);
        }

        
// support template text
        
$support "(for information on implementing this, see the support forums) ";

        
// setup allowed elements -----------------------------------------

        
$allowed_elements $config->get('HTML.AllowedElements');
        
$allowed_attributes $config->get('HTML.AllowedAttributes'); // retrieve early

        
if (!is_array($allowed_elements) && !is_array($allowed_attributes)) {
            
$allowed $config->get('HTML.Allowed');
            if (
is_string($allowed)) {
                list(
$allowed_elements$allowed_attributes) = $this->parseTinyMCEAllowedList($allowed);
            }
        }

        if (
is_array($allowed_elements)) {
            foreach (
$this->info as $name => $d) {
                if (!isset(
$allowed_elements[$name])) {
                    unset(
$this->info[$name]);
                }
                unset(
$allowed_elements[$name]);
            }
            
// emit errors
            
foreach ($allowed_elements as $element => $d) {
                
$element htmlspecialchars($element); // PHP doesn't escape errors, be careful!
                
trigger_error("Element '$element' is not supported $support"E_USER_WARNING);
            }
        }

        
// setup allowed attributes ---------------------------------------

        
$allowed_attributes_mutable $allowed_attributes// by copy!
        
if (is_array($allowed_attributes)) {
            
// This actually doesn't do anything, since we went away from
            // global attributes. It's possible that userland code uses
            // it, but HTMLModuleManager doesn't!
            
foreach ($this->info_global_attr as $attr => $x) {
                
$keys = array($attr"*@$attr""*.$attr");
                
$delete true;
                foreach (
$keys as $key) {
                    if (
$delete && isset($allowed_attributes[$key])) {
                        
$delete false;
                    }
                    if (isset(
$allowed_attributes_mutable[$key])) {
                        unset(
$allowed_attributes_mutable[$key]);
                    }
                }
                if (
$delete) {
                    unset(
$this->info_global_attr[$attr]);
                }
            }

            foreach (
$this->info as $tag => $info) {
                foreach (
$info->attr as $attr => $x) {
                    
$keys = array("$tag@$attr"$attr"*@$attr""$tag.$attr""*.$attr");
                    
$delete true;
                    foreach (
$keys as $key) {
                        if (
$delete && isset($allowed_attributes[$key])) {
                            
$delete false;
                        }
                        if (isset(
$allowed_attributes_mutable[$key])) {
                            unset(
$allowed_attributes_mutable[$key]);
                        }
                    }
                    if (
$delete) {
                        if (
$this->info[$tag]->attr[$attr]->required) {
                            
trigger_error(
                                
"Required attribute '$attr' in element '$tag' " .
                                
"was not allowed, which means '$tag' will not be allowed either",
                                
E_USER_WARNING
                            
);
                        }
                        unset(
$this->info[$tag]->attr[$attr]);
                    }
                }
            }
            
// emit errors
            
foreach ($allowed_attributes_mutable as $elattr => $d) {
                
$bits preg_split('/[.@]/'$elattr2);
                
$c count($bits);
                switch (
$c) {
                    case 
2:
                        if (
$bits[0] !== '*') {
                            
$element htmlspecialchars($bits[0]);
                            
$attribute htmlspecialchars($bits[1]);
                            if (!isset(
$this->info[$element])) {
                                
trigger_error(
                                    
"Cannot allow attribute '$attribute' if element " .
                                    
"'$element' is not allowed/supported $support"
                                
);
                            } else {
                                
trigger_error(
                                    
"Attribute '$attribute' in element '$element' not supported $support",
                                    
E_USER_WARNING
                                
);
                            }
                            break;
                        }
                        
// otherwise fall through
                    
case 1:
                        
$attribute htmlspecialchars($bits[0]);
                        
trigger_error(
                            
"Global attribute '$attribute' is not ".
                            
"supported in any elements $support",
                            
E_USER_WARNING
                        
);
                        break;
                }
            }
        }

        
// setup forbidden elements ---------------------------------------

        
$forbidden_elements   $config->get('HTML.ForbiddenElements');
        
$forbidden_attributes $config->get('HTML.ForbiddenAttributes');

        foreach (
$this->info as $tag => $info) {
            if (isset(
$forbidden_elements[$tag])) {
                unset(
$this->info[$tag]);
                continue;
            }
            foreach (
$info->attr as $attr => $x) {
                if (isset(
$forbidden_attributes["$tag@$attr"]) ||
                    isset(
$forbidden_attributes["*@$attr"]) ||
                    isset(
$forbidden_attributes[$attr])
                ) {
                    unset(
$this->info[$tag]->attr[$attr]);
                    continue;
                } elseif (isset(
$forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually
                    // $tag.$attr are not user supplied, so no worries!
                    
trigger_error(
                        
"Error with $tag.$attr: tag.attr syntax not supported for " .
                        
"HTML.ForbiddenAttributes; use tag@attr instead",
                        
E_USER_WARNING
                    
);
                }
            }
        }
        foreach (
$forbidden_attributes as $key => $v) {
            if (
strlen($key) < 2) {
                continue;
            }
            if (
$key[0] != '*') {
                continue;
            }
            if (
$key[1] == '.') {
                
trigger_error(
                    
"Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead",
                    
E_USER_WARNING
                
);
            }
        }

        
// setup injectors -----------------------------------------------------
        
foreach ($this->info_injector as $i => $injector) {
            if (
$injector->checkNeeded($config) !== false) {
                
// remove injector that does not have it's required
                // elements/attributes present, and is thus not needed.
                
unset($this->info_injector[$i]);
            }
        }
    }

    
/**
     * Parses a TinyMCE-flavored Allowed Elements and Attributes list into
     * separate lists for processing. Format is element[attr1|attr2],element2...
     * @warning Although it's largely drawn from TinyMCE's implementation,
     *      it is different, and you'll probably have to modify your lists
     * @param array $list String list to parse
     * @return array
     * @todo Give this its own class, probably static interface
     */
    
public function parseTinyMCEAllowedList($list)
    {
        
$list str_replace(array(' '"t"), ''$list);

        
$elements = array();
        
$attributes = array();

        
$chunks preg_split('/(,|[nr]+)/'$list);
        foreach (
$chunks as $chunk) {
            if (empty(
$chunk)) {
                continue;
            }
            
// remove TinyMCE element control characters
            
if (!strpos($chunk'[')) {
                
$element $chunk;
                
$attr false;
            } else {
                list(
$element$attr) = explode('['$chunk);
            }
            if (
$element !== '*') {
                
$elements[$element] = true;
            }
            if (!
$attr) {
                continue;
            }
            
$attr substr($attr0strlen($attr) - 1); // remove trailing ]
            
$attr explode('|'$attr);
            foreach (
$attr as $key) {
                
$attributes["$element.$key"] = true;
            }
        }
        return array(
$elements$attributes);
    }
}





/**
 * Represents an XHTML 1.1 module, with information on elements, tags
 * and attributes.
 * @note Even though this is technically XHTML 1.1, it is also used for
 *       regular HTML parsing. We are using modulization as a convenient
 *       way to represent the internals of HTMLDefinition, and our
 *       implementation is by no means conforming and does not directly
 *       use the normative DTDs or XML schemas.
 * @note The public variables in a module should almost directly
 *       correspond to the variables in HTMLPurifier_HTMLDefinition.
 *       However, the prefix info carries no special meaning in these
 *       objects (include it anyway if that's the correspondence though).
 * @todo Consider making some member functions protected
 */

class HTMLPurifier_HTMLModule
{

    
// -- Overloadable ----------------------------------------------------

    /**
     * Short unique string identifier of the module.
     * @type string
     */
    
public $name;

    
/**
     * Informally, a list of elements this module changes.
     * Not used in any significant way.
     * @type array
     */
    
public $elements = array();

    
/**
     * Associative array of element names to element definitions.
     * Some definitions may be incomplete, to be merged in later
     * with the full definition.
     * @type array
     */
    
public $info = array();

    
/**
     * Associative array of content set names to content set additions.
     * This is commonly used to, say, add an A element to the Inline
     * content set. This corresponds to an internal variable $content_sets
     * and NOT info_content_sets member variable of HTMLDefinition.
     * @type array
     */
    
public $content_sets = array();

    
/**
     * Associative array of attribute collection names to attribute
     * collection additions. More rarely used for adding attributes to
     * the global collections. Example is the StyleAttribute module adding
     * the style attribute to the Core. Corresponds to HTMLDefinition's
     * attr_collections->info, since the object's data is only info,
     * with extra behavior associated with it.
     * @type array
     */
    
public $attr_collections = array();

    
/**
     * Associative array of deprecated tag name to HTMLPurifier_TagTransform.
     * @type array
     */
    
public $info_tag_transform = array();

    
/**
     * List of HTMLPurifier_AttrTransform to be performed before validation.
     * @type array
     */
    
public $info_attr_transform_pre = array();

    
/**
     * List of HTMLPurifier_AttrTransform to be performed after validation.
     * @type array
     */
    
public $info_attr_transform_post = array();

    
/**
     * List of HTMLPurifier_Injector to be performed during well-formedness fixing.
     * An injector will only be invoked if all of it's pre-requisites are met;
     * if an injector fails setup, there will be no error; it will simply be
     * silently disabled.
     * @type array
     */
    
public $info_injector = array();

    
/**
     * Boolean flag that indicates whether or not getChildDef is implemented.
     * For optimization reasons: may save a call to a function. Be sure
     * to set it if you do implement getChildDef(), otherwise it will have
     * no effect!
     * @type bool
     */
    
public $defines_child_def false;

    
/**
     * Boolean flag whether or not this module is safe. If it is not safe, all
     * of its members are unsafe. Modules are safe by default (this might be
     * slightly dangerous, but it doesn't make much sense to force HTML Purifier,
     * which is based off of safe HTML, to explicitly say, "This is safe," even
     * though there are modules which are "unsafe")
     *
     * @type bool
     * @note Previously, safety could be applied at an element level granularity.
     *       We've removed this ability, so in order to add "unsafe" elements
     *       or attributes, a dedicated module with this property set to false
     *       must be used.
     */
    
public $safe true;

    
/**
     * Retrieves a proper HTMLPurifier_ChildDef subclass based on
     * content_model and content_model_type member variables of
     * the HTMLPurifier_ElementDef class. There is a similar function
     * in HTMLPurifier_HTMLDefinition.
     * @param HTMLPurifier_ElementDef $def
     * @return HTMLPurifier_ChildDef subclass
     */
    
public function getChildDef($def)
    {
        return 
false;
    }

    
// -- Convenience -----------------------------------------------------

    /**
     * Convenience function that sets up a new element
     * @param string $element Name of element to add
     * @param string|bool $type What content set should element be registered to?
     *              Set as false to skip this step.
     * @param string $contents Allowed children in form of:
     *              "$content_model_type: $content_model"
     * @param array $attr_includes What attribute collections to register to
     *              element?
     * @param array $attr What unique attributes does the element define?
     * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters.
     * @return HTMLPurifier_ElementDef Created element definition object, so you
     *         can set advanced parameters
     */
    
public function addElement($element$type$contents$attr_includes = array(), $attr = array())
    {
        
$this->elements[] = $element;
        
// parse content_model
        
list($content_model_type$content_model) = $this->parseContents($contents);
        
// merge in attribute inclusions
        
$this->mergeInAttrIncludes($attr$attr_includes);
        
// add element to content sets
        
if ($type) {
            
$this->addElementToContentSet($element$type);
        }
        
// create element
        
$this->info[$element] = HTMLPurifier_ElementDef::create(
            
$content_model,
            
$content_model_type,
            
$attr
        
);
        
// literal object $contents means direct child manipulation
        
if (!is_string($contents)) {
            
$this->info[$element]->child $contents;
        }
        return 
$this->info[$element];
    }

    
/**
     * Convenience function that creates a totally blank, non-standalone
     * element.
     * @param string $element Name of element to create
     * @return HTMLPurifier_ElementDef Created element
     */
    
public function addBlankElement($element)
    {
        if (!isset(
$this->info[$element])) {
            
$this->elements[] = $element;
            
$this->info[$element] = new HTMLPurifier_ElementDef();
            
$this->info[$element]->standalone false;
        } else {
            
trigger_error("Definition for $element already exists in module, cannot redefine");
        }
        return 
$this->info[$element];
    }

    
/**
     * Convenience function that registers an element to a content set
     * @param string $element Element to register
     * @param string $type Name content set (warning: case sensitive, usually upper-case
     *        first letter)
     */
    
public function addElementToContentSet($element$type)
    {
        if (!isset(
$this->content_sets[$type])) {
            
$this->content_sets[$type] = '';
        } else {
            
$this->content_sets[$type] .= ' | ';
        }
        
$this->content_sets[$type] .= $element;
    }

    
/**
     * Convenience function that transforms single-string contents
     * into separate content model and content model type
     * @param string $contents Allowed children in form of:
     *                  "$content_model_type: $content_model"
     * @return array
     * @note If contents is an object, an array of two nulls will be
     *       returned, and the callee needs to take the original $contents
     *       and use it directly.
     */
    
public function parseContents($contents)
    {
        if (!
is_string($contents)) {
            return array(
nullnull);
        } 
// defer
        
switch ($contents) {
            
// check for shorthand content model forms
            
case 'Empty':
                return array(
'empty''');
            case 
'Inline':
                return array(
'optional''Inline | #PCDATA');
            case 
'Flow':
                return array(
'optional''Flow | #PCDATA');
        }
        list(
$content_model_type$content_model) = explode(':'$contents);
        
$content_model_type strtolower(trim($content_model_type));
        
$content_model trim($content_model);
        return array(
$content_model_type$content_model);
    }

    
/**
     * Convenience function that merges a list of attribute includes into
     * an attribute array.
     * @param array $attr Reference to attr array to modify
     * @param array $attr_includes Array of includes / string include to merge in
     */
    
public function mergeInAttrIncludes(&$attr$attr_includes)
    {
        if (!
is_array($attr_includes)) {
            if (empty(
$attr_includes)) {
                
$attr_includes = array();
            } else {
                
$attr_includes = array($attr_includes);
            }
        }
        
$attr[0] = $attr_includes;
    }

    
/**
     * Convenience function that generates a lookup table with boolean
     * true as value.
     * @param string $list List of values to turn into a lookup
     * @note You can also pass an arbitrary number of arguments in
     *       place of the regular argument
     * @return array array equivalent of list
     */
    
public function makeLookup($list)
    {
        if (
is_string($list)) {
            
$list func_get_args();
        }
        
$ret = array();
        foreach (
$list as $value) {
            if (
is_null($value)) {
                continue;
            }
            
$ret[$value] = true;
        }
        return 
$ret;
    }

    
/**
     * Lazy load construction of the module after determining whether
     * or not it's needed, and also when a finalized configuration object
     * is available.
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
    }
}





class 
HTMLPurifier_HTMLModuleManager
{

    
/**
     * @type HTMLPurifier_DoctypeRegistry
     */
    
public $doctypes;

    
/**
     * Instance of current doctype.
     * @type string
     */
    
public $doctype;

    
/**
     * @type HTMLPurifier_AttrTypes
     */
    
public $attrTypes;

    
/**
     * Active instances of modules for the specified doctype are
     * indexed, by name, in this array.
     * @type HTMLPurifier_HTMLModule[]
     */
    
public $modules = array();

    
/**
     * Array of recognized HTMLPurifier_HTMLModule instances,
     * indexed by module's class name. This array is usually lazy loaded, but a
     * user can overload a module by pre-emptively registering it.
     * @type HTMLPurifier_HTMLModule[]
     */
    
public $registeredModules = array();

    
/**
     * List of extra modules that were added by the user
     * using addModule(). These get unconditionally merged into the current doctype, whatever
     * it may be.
     * @type HTMLPurifier_HTMLModule[]
     */
    
public $userModules = array();

    
/**
     * Associative array of element name to list of modules that have
     * definitions for the element; this array is dynamically filled.
     * @type array
     */
    
public $elementLookup = array();

    
/**
     * List of prefixes we should use for registering small names.
     * @type array
     */
    
public $prefixes = array('HTMLPurifier_HTMLModule_');

    
/**
     * @type HTMLPurifier_ContentSets
     */
    
public $contentSets;

    
/**
     * @type HTMLPurifier_AttrCollections
     */
    
public $attrCollections;

    
/**
     * If set to true, unsafe elements and attributes will be allowed.
     * @type bool
     */
    
public $trusted false;

    public function 
__construct()
    {
        
// editable internal objects
        
$this->attrTypes = new HTMLPurifier_AttrTypes();
        
$this->doctypes  = new HTMLPurifier_DoctypeRegistry();

        
// setup basic modules
        
$common = array(
            
'CommonAttributes''Text''Hypertext''List',
            
'Presentation''Edit''Bdo''Tables''Image',
            
'StyleAttribute',
            
// Unsafe:
            
'Scripting''Object''Forms',
            
// Sorta legacy, but present in strict:
            
'Name',
        );
        
$transitional = array('Legacy''Target''Iframe');
        
$xml = array('XMLCommonAttributes');
        
$non_xml = array('NonXMLCommonAttributes');

        
// setup basic doctypes
        
$this->doctypes->register(
            
'HTML 4.01 Transitional',
            
false,
            
array_merge($common$transitional$non_xml),
            array(
'Tidy_Transitional''Tidy_Proprietary'),
            array(),
            
'-//W3C//DTD HTML 4.01 Transitional//EN',
            
'http://www.w3.org/TR/html4/loose.dtd'
        
);

        
$this->doctypes->register(
            
'HTML 4.01 Strict',
            
false,
            
array_merge($common$non_xml),
            array(
'Tidy_Strict''Tidy_Proprietary''Tidy_Name'),
            array(),
            
'-//W3C//DTD HTML 4.01//EN',
            
'http://www.w3.org/TR/html4/strict.dtd'
        
);

        
$this->doctypes->register(
            
'XHTML 1.0 Transitional',
            
true,
            
array_merge($common$transitional$xml$non_xml),
            array(
'Tidy_Transitional''Tidy_XHTML''Tidy_Proprietary''Tidy_Name'),
            array(),
            
'-//W3C//DTD XHTML 1.0 Transitional//EN',
            
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
        
);

        
$this->doctypes->register(
            
'XHTML 1.0 Strict',
            
true,
            
array_merge($common$xml$non_xml),
            array(
'Tidy_Strict''Tidy_XHTML''Tidy_Strict''Tidy_Proprietary''Tidy_Name'),
            array(),
            
'-//W3C//DTD XHTML 1.0 Strict//EN',
            
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
        
);

        
$this->doctypes->register(
            
'XHTML 1.1',
            
true,
            
// Iframe is a real XHTML 1.1 module, despite being
            // "transitional"!
            
array_merge($common$xml, array('Ruby''Iframe')),
            array(
'Tidy_Strict''Tidy_XHTML''Tidy_Proprietary''Tidy_Strict''Tidy_Name'), // Tidy_XHTML1_1
            
array(),
            
'-//W3C//DTD XHTML 1.1//EN',
            
'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
        
);

    }

    
/**
     * Registers a module to the recognized module list, useful for
     * overloading pre-existing modules.
     * @param $module Mixed: string module name, with or without
     *                HTMLPurifier_HTMLModule prefix, or instance of
     *                subclass of HTMLPurifier_HTMLModule.
     * @param $overload Boolean whether or not to overload previous modules.
     *                  If this is not set, and you do overload a module,
     *                  HTML Purifier will complain with a warning.
     * @note This function will not call autoload, you must instantiate
     *       (and thus invoke) autoload outside the method.
     * @note If a string is passed as a module name, different variants
     *       will be tested in this order:
     *          - Check for HTMLPurifier_HTMLModule_$name
     *          - Check all prefixes with $name in order they were added
     *          - Check for literal object name
     *          - Throw fatal error
     *       If your object name collides with an internal class, specify
     *       your module manually. All modules must have been included
     *       externally: registerModule will not perform inclusions for you!
     */
    
public function registerModule($module$overload false)
    {
        if (
is_string($module)) {
            
// attempt to load the module
            
$original_module $module;
            
$ok false;
            foreach (
$this->prefixes as $prefix) {
                
$module $prefix $original_module;
                if (
class_exists($module)) {
                    
$ok true;
                    break;
                }
            }
            if (!
$ok) {
                
$module $original_module;
                if (!
class_exists($module)) {
                    
trigger_error(
                        
$original_module ' module does not exist',
                        
E_USER_ERROR
                    
);
                    return;
                }
            }
            
$module = new $module();
        }
        if (empty(
$module->name)) {
            
trigger_error('Module instance of ' get_class($module) . ' must have name');
            return;
        }
        if (!
$overload && isset($this->registeredModules[$module->name])) {
            
trigger_error('Overloading ' $module->name ' without explicit overload parameter'E_USER_WARNING);
        }
        
$this->registeredModules[$module->name] = $module;
    }

    
/**
     * Adds a module to the current doctype by first registering it,
     * and then tacking it on to the active doctype
     */
    
public function addModule($module)
    {
        
$this->registerModule($module);
        if (
is_object($module)) {
            
$module $module->name;
        }
        
$this->userModules[] = $module;
    }

    
/**
     * Adds a class prefix that registerModule() will use to resolve a
     * string name to a concrete class
     */
    
public function addPrefix($prefix)
    {
        
$this->prefixes[] = $prefix;
    }

    
/**
     * Performs processing on modules, after being called you may
     * use getElement() and getElements()
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$this->trusted $config->get('HTML.Trusted');

        
// generate
        
$this->doctype $this->doctypes->make($config);
        
$modules $this->doctype->modules;

        
// take out the default modules that aren't allowed
        
$lookup $config->get('HTML.AllowedModules');
        
$special_cases $config->get('HTML.CoreModules');

        if (
is_array($lookup)) {
            foreach (
$modules as $k => $m) {
                if (isset(
$special_cases[$m])) {
                    continue;
                }
                if (!isset(
$lookup[$m])) {
                    unset(
$modules[$k]);
                }
            }
        }

        
// custom modules
        
if ($config->get('HTML.Proprietary')) {
            
$modules[] = 'Proprietary';
        }
        if (
$config->get('HTML.SafeObject')) {
            
$modules[] = 'SafeObject';
        }
        if (
$config->get('HTML.SafeEmbed')) {
            
$modules[] = 'SafeEmbed';
        }
        if (
$config->get('HTML.SafeScripting') !== array()) {
            
$modules[] = 'SafeScripting';
        }
        if (
$config->get('HTML.Nofollow')) {
            
$modules[] = 'Nofollow';
        }
        if (
$config->get('HTML.TargetBlank')) {
            
$modules[] = 'TargetBlank';
        }
        
// NB: HTML.TargetNoreferrer and HTML.TargetNoopener must be AFTER HTML.TargetBlank
        // so that its post-attr-transform gets run afterwards.
        
if ($config->get('HTML.TargetNoreferrer')) {
            
$modules[] = 'TargetNoreferrer';
        }
        if (
$config->get('HTML.TargetNoopener')) {
            
$modules[] = 'TargetNoopener';
        }

        
// merge in custom modules
        
$modules array_merge($modules$this->userModules);

        foreach (
$modules as $module) {
            
$this->processModule($module);
            
$this->modules[$module]->setup($config);
        }

        foreach (
$this->doctype->tidyModules as $module) {
            
$this->processModule($module);
            
$this->modules[$module]->setup($config);
        }

        
// prepare any injectors
        
foreach ($this->modules as $module) {
            
$n = array();
            foreach (
$module->info_injector as $injector) {
                if (!
is_object($injector)) {
                    
$class "HTMLPurifier_Injector_$injector";
                    
$injector = new $class;
                }
                
$n[$injector->name] = $injector;
            }
            
$module->info_injector $n;
        }

        
// setup lookup table based on all valid modules
        
foreach ($this->modules as $module) {
            foreach (
$module->info as $name => $def) {
                if (!isset(
$this->elementLookup[$name])) {
                    
$this->elementLookup[$name] = array();
                }
                
$this->elementLookup[$name][] = $module->name;
            }
        }

        
// note the different choice
        
$this->contentSets = new HTMLPurifier_ContentSets(
            
// content set assembly deals with all possible modules,
            // not just ones deemed to be "safe"
            
$this->modules
        
);
        
$this->attrCollections = new HTMLPurifier_AttrCollections(
            
$this->attrTypes,
            
// there is no way to directly disable a global attribute,
            // but using AllowedAttributes or simply not including
            // the module in your custom doctype should be sufficient
            
$this->modules
        
);
    }

    
/**
     * Takes a module and adds it to the active module collection,
     * registering it if necessary.
     */
    
public function processModule($module)
    {
        if (!isset(
$this->registeredModules[$module]) || is_object($module)) {
            
$this->registerModule($module);
        }
        
$this->modules[$module] = $this->registeredModules[$module];
    }

    
/**
     * Retrieves merged element definitions.
     * @return Array of HTMLPurifier_ElementDef
     */
    
public function getElements()
    {
        
$elements = array();
        foreach (
$this->modules as $module) {
            if (!
$this->trusted && !$module->safe) {
                continue;
            }
            foreach (
$module->info as $name => $v) {
                if (isset(
$elements[$name])) {
                    continue;
                }
                
$elements[$name] = $this->getElement($name);
            }
        }

        
// remove dud elements, this happens when an element that
        // appeared to be safe actually wasn't
        
foreach ($elements as $n => $v) {
            if (
$v === false) {
                unset(
$elements[$n]);
            }
        }

        return 
$elements;

    }

    
/**
     * Retrieves a single merged element definition
     * @param string $name Name of element
     * @param bool $trusted Boolean trusted overriding parameter: set to true
     *                 if you want the full version of an element
     * @return HTMLPurifier_ElementDef Merged HTMLPurifier_ElementDef
     * @note You may notice that modules are getting iterated over twice (once
     *       in getElements() and once here). This
     *       is because
     */
    
public function getElement($name$trusted null)
    {
        if (!isset(
$this->elementLookup[$name])) {
            return 
false;
        }

        
// setup global state variables
        
$def false;
        if (
$trusted === null) {
            
$trusted $this->trusted;
        }

        
// iterate through each module that has registered itself to this
        // element
        
foreach ($this->elementLookup[$name] as $module_name) {
            
$module $this->modules[$module_name];

            
// refuse to create/merge from a module that is deemed unsafe--
            // pretend the module doesn't exist--when trusted mode is not on.
            
if (!$trusted && !$module->safe) {
                continue;
            }

            
// clone is used because, ideally speaking, the original
            // definition should not be modified. Usually, this will
            // make no difference, but for consistency's sake
            
$new_def = clone $module->info[$name];

            if (!
$def && $new_def->standalone) {
                
$def $new_def;
            } elseif (
$def) {
                
// This will occur even if $new_def is standalone. In practice,
                // this will usually result in a full replacement.
                
$def->mergeIn($new_def);
            } else {
                
// :TODO:
                // non-standalone definitions that don't have a standalone
                // to merge into could be deferred to the end
                // HOWEVER, it is perfectly valid for a non-standalone
                // definition to lack a standalone definition, even
                // after all processing: this allows us to safely
                // specify extra attributes for elements that may not be
                // enabled all in one place.  In particular, this might
                // be the case for trusted elements.  WARNING: care must
                // be taken that the /extra/ definitions are all safe.
                
continue;
            }

            
// attribute value expansions
            
$this->attrCollections->performInclusions($def->attr);
            
$this->attrCollections->expandIdentifiers($def->attr$this->attrTypes);

            
// descendants_are_inline, for ChildDef_Chameleon
            
if (is_string($def->content_model) &&
                
strpos($def->content_model'Inline') !== false) {
                if (
$name != 'del' && $name != 'ins') {
                    
// this is for you, ins/del
                    
$def->descendants_are_inline true;
                }
            }

            
$this->contentSets->generateChildDef($def$module);
        }

        
// This can occur if there is a blank definition, but no base to
        // mix it in with
        
if (!$def) {
            return 
false;
        }

        
// add information on required attributes
        
foreach ($def->attr as $attr_name => $attr_def) {
            if (
$attr_def->required) {
                
$def->required_attr[] = $attr_name;
            }
        }
        return 
$def;
    }
}





/**
 * Component of HTMLPurifier_AttrContext that accumulates IDs to prevent dupes
 * @note In Slashdot-speak, dupe means duplicate.
 * @note The default constructor does not accept $config or $context objects:
 *       use must use the static build() factory method to perform initialization.
 */
class HTMLPurifier_IDAccumulator
{

    
/**
     * Lookup table of IDs we've accumulated.
     * @public
     */
    
public $ids = array();

    
/**
     * Builds an IDAccumulator, also initializing the default blacklist
     * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config
     * @param HTMLPurifier_Context $context Instance of HTMLPurifier_Context
     * @return HTMLPurifier_IDAccumulator Fully initialized HTMLPurifier_IDAccumulator
     */
    
public static function build($config$context)
    {
        
$id_accumulator = new HTMLPurifier_IDAccumulator();
        
$id_accumulator->load($config->get('Attr.IDBlacklist'));
        return 
$id_accumulator;
    }

    
/**
     * Add an ID to the lookup table.
     * @param string $id ID to be added.
     * @return bool status, true if success, false if there's a dupe
     */
    
public function add($id)
    {
        if (isset(
$this->ids[$id])) {
            return 
false;
        }
        return 
$this->ids[$id] = true;
    }

    
/**
     * Load a list of IDs into the lookup table
     * @param $array_of_ids Array of IDs to load
     * @note This function doesn't care about duplicates
     */
    
public function load($array_of_ids)
    {
        foreach (
$array_of_ids as $id) {
            
$this->ids[$id] = true;
        }
    }
}





/**
 * Injects tokens into the document while parsing for well-formedness.
 * This enables "formatter-like" functionality such as auto-paragraphing,
 * smiley-ification and linkification to take place.
 *
 * A note on how handlers create changes; this is done by assigning a new
 * value to the $token reference. These values can take a variety of forms and
 * are best described HTMLPurifier_Strategy_MakeWellFormed->processToken()
 * documentation.
 *
 * @todo Allow injectors to request a re-run on their output. This
 *       would help if an operation is recursive.
 */
abstract class HTMLPurifier_Injector
{

    
/**
     * Advisory name of injector, this is for friendly error messages.
     * @type string
     */
    
public $name;

    
/**
     * @type HTMLPurifier_HTMLDefinition
     */
    
protected $htmlDefinition;

    
/**
     * Reference to CurrentNesting variable in Context. This is an array
     * list of tokens that we are currently "inside"
     * @type array
     */
    
protected $currentNesting;

    
/**
     * Reference to current token.
     * @type HTMLPurifier_Token
     */
    
protected $currentToken;

    
/**
     * Reference to InputZipper variable in Context.
     * @type HTMLPurifier_Zipper
     */
    
protected $inputZipper;

    
/**
     * Array of elements and attributes this injector creates and therefore
     * need to be allowed by the definition. Takes form of
     * array('element' => array('attr', 'attr2'), 'element2')
     * @type array
     */
    
public $needed = array();

    
/**
     * Number of elements to rewind backwards (relative).
     * @type bool|int
     */
    
protected $rewindOffset false;

    
/**
     * Rewind to a spot to re-perform processing. This is useful if you
     * deleted a node, and now need to see if this change affected any
     * earlier nodes. Rewinding does not affect other injectors, and can
     * result in infinite loops if not used carefully.
     * @param bool|int $offset
     * @warning HTML Purifier will prevent you from fast-forwarding with this
     *          function.
     */
    
public function rewindOffset($offset)
    {
        
$this->rewindOffset $offset;
    }

    
/**
     * Retrieves rewind offset, and then unsets it.
     * @return bool|int
     */
    
public function getRewindOffset()
    {
        
$r $this->rewindOffset;
        
$this->rewindOffset false;
        return 
$r;
    }

    
/**
     * Prepares the injector by giving it the config and context objects:
     * this allows references to important variables to be made within
     * the injector. This function also checks if the HTML environment
     * will work with the Injector (see checkNeeded()).
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string Boolean false if success, string of missing needed element/attribute if failure
     */
    
public function prepare($config$context)
    {
        
$this->htmlDefinition $config->getHTMLDefinition();
        
// Even though this might fail, some unit tests ignore this and
        // still test checkNeeded, so be careful. Maybe get rid of that
        // dependency.
        
$result $this->checkNeeded($config);
        if (
$result !== false) {
            return 
$result;
        }
        
$this->currentNesting =& $context->get('CurrentNesting');
        
$this->currentToken   =& $context->get('CurrentToken');
        
$this->inputZipper    =& $context->get('InputZipper');
        return 
false;
    }

    
/**
     * This function checks if the HTML environment
     * will work with the Injector: if p tags are not allowed, the
     * Auto-Paragraphing injector should not be enabled.
     * @param HTMLPurifier_Config $config
     * @return bool|string Boolean false if success, string of missing needed element/attribute if failure
     */
    
public function checkNeeded($config)
    {
        
$def $config->getHTMLDefinition();
        foreach (
$this->needed as $element => $attributes) {
            if (
is_int($element)) {
                
$element $attributes;
            }
            if (!isset(
$def->info[$element])) {
                return 
$element;
            }
            if (!
is_array($attributes)) {
                continue;
            }
            foreach (
$attributes as $name) {
                if (!isset(
$def->info[$element]->attr[$name])) {
                    return 
"$element.$name";
                }
            }
        }
        return 
false;
    }

    
/**
     * Tests if the context node allows a certain element
     * @param string $name Name of element to test for
     * @return bool True if element is allowed, false if it is not
     */
    
public function allowsElement($name)
    {
        if (!empty(
$this->currentNesting)) {
            
$parent_token array_pop($this->currentNesting);
            
$this->currentNesting[] = $parent_token;
            
$parent $this->htmlDefinition->info[$parent_token->name];
        } else {
            
$parent $this->htmlDefinition->info_parent_def;
        }
        if (!isset(
$parent->child->elements[$name]) || isset($parent->excludes[$name])) {
            return 
false;
        }
        
// check for exclusion
        
for ($i count($this->currentNesting) - 2$i >= 0$i--) {
            
$node $this->currentNesting[$i];
            
$def  $this->htmlDefinition->info[$node->name];
            if (isset(
$def->excludes[$name])) {
                return 
false;
            }
        }
        return 
true;
    }

    
/**
     * Iterator function, which starts with the next token and continues until
     * you reach the end of the input tokens.
     * @warning Please prevent previous references from interfering with this
     *          functions by setting $i = null beforehand!
     * @param int $i Current integer index variable for inputTokens
     * @param HTMLPurifier_Token $current Current token variable.
     *          Do NOT use $token, as that variable is also a reference
     * @return bool
     */
    
protected function forward(&$i, &$current)
    {
        if (
$i === null) {
            
$i count($this->inputZipper->back) - 1;
        } else {
            
$i--;
        }
        if (
$i 0) {
            return 
false;
        }
        
$current $this->inputZipper->back[$i];
        return 
true;
    }

    
/**
     * Similar to _forward, but accepts a third parameter $nesting (which
     * should be initialized at 0) and stops when we hit the end tag
     * for the node $this->inputIndex starts in.
     * @param int $i Current integer index variable for inputTokens
     * @param HTMLPurifier_Token $current Current token variable.
     *          Do NOT use $token, as that variable is also a reference
     * @param int $nesting
     * @return bool
     */
    
protected function forwardUntilEndToken(&$i, &$current, &$nesting)
    {
        
$result $this->forward($i$current);
        if (!
$result) {
            return 
false;
        }
        if (
$nesting === null) {
            
$nesting 0;
        }
        if (
$current instanceof HTMLPurifier_Token_Start) {
            
$nesting++;
        } elseif (
$current instanceof HTMLPurifier_Token_End) {
            if (
$nesting <= 0) {
                return 
false;
            }
            
$nesting--;
        }
        return 
true;
    }

    
/**
     * Iterator function, starts with the previous token and continues until
     * you reach the beginning of input tokens.
     * @warning Please prevent previous references from interfering with this
     *          functions by setting $i = null beforehand!
     * @param int $i Current integer index variable for inputTokens
     * @param HTMLPurifier_Token $current Current token variable.
     *          Do NOT use $token, as that variable is also a reference
     * @return bool
     */
    
protected function backward(&$i, &$current)
    {
        if (
$i === null) {
            
$i count($this->inputZipper->front) - 1;
        } else {
            
$i--;
        }
        if (
$i 0) {
            return 
false;
        }
        
$current $this->inputZipper->front[$i];
        return 
true;
    }

    
/**
     * Handler that is called when a text token is processed
     */
    
public function handleText(&$token)
    {
    }

    
/**
     * Handler that is called when a start or empty token is processed
     */
    
public function handleElement(&$token)
    {
    }

    
/**
     * Handler that is called when an end token is processed
     */
    
public function handleEnd(&$token)
    {
        
$this->notifyEnd($token);
    }

    
/**
     * Notifier that is called when an end token is processed
     * @param HTMLPurifier_Token $token Current token variable.
     * @note This differs from handlers in that the token is read-only
     * @deprecated
     */
    
public function notifyEnd($token)
    {
    }
}





/**
 * Represents a language and defines localizable string formatting and
 * other functions, as well as the localized messages for HTML Purifier.
 */
class HTMLPurifier_Language
{

    
/**
     * ISO 639 language code of language. Prefers shortest possible version.
     * @type string
     */
    
public $code 'en';

    
/**
     * Fallback language code.
     * @type bool|string
     */
    
public $fallback false;

    
/**
     * Array of localizable messages.
     * @type array
     */
    
public $messages = array();

    
/**
     * Array of localizable error codes.
     * @type array
     */
    
public $errorNames = array();

    
/**
     * True if no message file was found for this language, so English
     * is being used instead. Check this if you'd like to notify the
     * user that they've used a non-supported language.
     * @type bool
     */
    
public $error false;

    
/**
     * Has the language object been loaded yet?
     * @type bool
     * @todo Make it private, fix usage in HTMLPurifier_LanguageTest
     */
    
public $_loaded false;

    
/**
     * @type HTMLPurifier_Config
     */
    
protected $config;

    
/**
     * @type HTMLPurifier_Context
     */
    
protected $context;

    
/**
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     */
    
public function __construct($config$context)
    {
        
$this->config  $config;
        
$this->context $context;
    }

    
/**
     * Loads language object with necessary info from factory cache
     * @note This is a lazy loader
     */
    
public function load()
    {
        if (
$this->_loaded) {
            return;
        }
        
$factory HTMLPurifier_LanguageFactory::instance();
        
$factory->loadLanguage($this->code);
        foreach (
$factory->keys as $key) {
            
$this->$key $factory->cache[$this->code][$key];
        }
        
$this->_loaded true;
    }

    
/**
     * Retrieves a localised message.
     * @param string $key string identifier of message
     * @return string localised message
     */
    
public function getMessage($key)
    {
        if (!
$this->_loaded) {
            
$this->load();
        }
        if (!isset(
$this->messages[$key])) {
            return 
"[$key]";
        }
        return 
$this->messages[$key];
    }

    
/**
     * Retrieves a localised error name.
     * @param int $int error number, corresponding to PHP's error reporting
     * @return string localised message
     */
    
public function getErrorName($int)
    {
        if (!
$this->_loaded) {
            
$this->load();
        }
        if (!isset(
$this->errorNames[$int])) {
            return 
"[Error: $int]";
        }
        return 
$this->errorNames[$int];
    }

    
/**
     * Converts an array list into a string readable representation
     * @param array $array
     * @return string
     */
    
public function listify($array)
    {
        
$sep      $this->getMessage('Item separator');
        
$sep_last $this->getMessage('Item separator last');
        
$ret '';
        for (
$i 0$c count($array); $i $c$i++) {
            if (
$i == 0) {
            } elseif (
$i $c) {
                
$ret .= $sep;
            } else {
                
$ret .= $sep_last;
            }
            
$ret .= $array[$i];
        }
        return 
$ret;
    }

    
/**
     * Formats a localised message with passed parameters
     * @param string $key string identifier of message
     * @param array $args Parameters to substitute in
     * @return string localised message
     * @todo Implement conditionals? Right now, some messages make
     *     reference to line numbers, but those aren't always available
     */
    
public function formatMessage($key$args = array())
    {
        if (!
$this->_loaded) {
            
$this->load();
        }
        if (!isset(
$this->messages[$key])) {
            return 
"[$key]";
        }
        
$raw $this->messages[$key];
        
$subst = array();
        
$generator false;
        foreach (
$args as $i => $value) {
            if (
is_object($value)) {
                if (
$value instanceof HTMLPurifier_Token) {
                    
// factor this out some time
                    
if (!$generator) {
                        
$generator $this->context->get('Generator');
                    }
                    if (isset(
$value->name)) {
                        
$subst['$'.$i.'.Name'] = $value->name;
                    }
                    if (isset(
$value->data)) {
                        
$subst['$'.$i.'.Data'] = $value->data;
                    }
                    
$subst['$'.$i.'.Compact'] =
                    
$subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value);
                    
// a more complex algorithm for compact representation
                    // could be introduced for all types of tokens. This
                    // may need to be factored out into a dedicated class
                    
if (!empty($value->attr)) {
                        
$stripped_token = clone $value;
                        
$stripped_token->attr = array();
                        
$subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token);
                    }
                    
$subst['$'.$i.'.Line'] = $value->line $value->line 'unknown';
                }
                continue;
            } elseif (
is_array($value)) {
                
$keys array_keys($value);
                if (
array_keys($keys) === $keys) {
                    
// list
                    
$subst['$'.$i] = $this->listify($value);
                } else {
                    
// associative array
                    // no $i implementation yet, sorry
                    
$subst['$'.$i.'.Keys'] = $this->listify($keys);
                    
$subst['$'.$i.'.Values'] = $this->listify(array_values($value));
                }
                continue;
            }
            
$subst['$' $i] = $value;
        }
        return 
strtr($raw$subst);
    }
}





/**
 * Class responsible for generating HTMLPurifier_Language objects, managing
 * caching and fallbacks.
 * @note Thanks to MediaWiki for the general logic, although this version
 *       has been entirely rewritten
 * @todo Serialized cache for languages
 */
class HTMLPurifier_LanguageFactory
{

    
/**
     * Cache of language code information used to load HTMLPurifier_Language objects.
     * Structure is: $factory->cache[$language_code][$key] = $value
     * @type array
     */
    
public $cache;

    
/**
     * Valid keys in the HTMLPurifier_Language object. Designates which
     * variables to slurp out of a message file.
     * @type array
     */
    
public $keys = array('fallback''messages''errorNames');

    
/**
     * Instance to validate language codes.
     * @type HTMLPurifier_AttrDef_Lang
     *
     */
    
protected $validator;

    
/**
     * Cached copy of dirname(__FILE__), directory of current file without
     * trailing slash.
     * @type string
     */
    
protected $dir;

    
/**
     * Keys whose contents are a hash map and can be merged.
     * @type array
     */
    
protected $mergeable_keys_map = array('messages' => true'errorNames' => true);

    
/**
     * Keys whose contents are a list and can be merged.
     * @value array lookup
     */
    
protected $mergeable_keys_list = array();

    
/**
     * Retrieve sole instance of the factory.
     * @param HTMLPurifier_LanguageFactory $prototype Optional prototype to overload sole instance with,
     *                   or bool true to reset to default factory.
     * @return HTMLPurifier_LanguageFactory
     */
    
public static function instance($prototype null)
    {
        static 
$instance null;
        if (
$prototype !== null) {
            
$instance $prototype;
        } elseif (
$instance === null || $prototype == true) {
            
$instance = new HTMLPurifier_LanguageFactory();
            
$instance->setup();
        }
        return 
$instance;
    }

    
/**
     * Sets up the singleton, much like a constructor
     * @note Prevents people from getting this outside of the singleton
     */
    
public function setup()
    {
        
$this->validator = new HTMLPurifier_AttrDef_Lang();
        
$this->dir HTMLPURIFIER_PREFIX '/HTMLPurifier';
    }

    
/**
     * Creates a language object, handles class fallbacks
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @param bool|string $code Code to override configuration with. Private parameter.
     * @return HTMLPurifier_Language
     */
    
public function create($config$context$code false)
    {
        
// validate language code
        
if ($code === false) {
            
$code $this->validator->validate(
                
$config->get('Core.Language'),
                
$config,
                
$context
            
);
        } else {
            
$code $this->validator->validate($code$config$context);
        }
        if (
$code === false) {
            
$code 'en'// malformed code becomes English
        
}

        
$pcode str_replace('-''_'$code); // make valid PHP classname
        
static $depth 0// recursion protection

        
if ($code == 'en') {
            
$lang = new HTMLPurifier_Language($config$context);
        } else {
            
$class 'HTMLPurifier_Language_' $pcode;
            
$file  $this->dir '/Language/classes/' $code '.php';
            if (
file_exists($file) || class_exists($classfalse)) {
                
$lang = new $class($config$context);
            } else {
                
// Go fallback
                
$raw_fallback $this->getFallbackFor($code);
                
$fallback $raw_fallback $raw_fallback 'en';
                
$depth++;
                
$lang $this->create($config$context$fallback);
                if (!
$raw_fallback) {
                    
$lang->error true;
                }
                
$depth--;
            }
        }
        
$lang->code $code;
        return 
$lang;
    }

    
/**
     * Returns the fallback language for language
     * @note Loads the original language into cache
     * @param string $code language code
     * @return string|bool
     */
    
public function getFallbackFor($code)
    {
        
$this->loadLanguage($code);
        return 
$this->cache[$code]['fallback'];
    }

    
/**
     * Loads language into the cache, handles message file and fallbacks
     * @param string $code language code
     */
    
public function loadLanguage($code)
    {
        static 
$languages_seen = array(); // recursion guard

        // abort if we've already loaded it
        
if (isset($this->cache[$code])) {
            return;
        }

        
// generate filename
        
$filename $this->dir '/Language/messages/' $code '.php';

        
// default fallback : may be overwritten by the ensuing include
        
$fallback = ($code != 'en') ? 'en' false;

        
// load primary localisation
        
if (!file_exists($filename)) {
            
// skip the include: will rely solely on fallback
            
$filename $this->dir '/Language/messages/en.php';
            
$cache = array();
        } else {
            include 
$filename;
            
$cache compact($this->keys);
        }

        
// load fallback localisation
        
if (!empty($fallback)) {

            
// infinite recursion guard
            
if (isset($languages_seen[$code])) {
                
trigger_error(
                    
'Circular fallback reference in language ' .
                    
$code,
                    
E_USER_ERROR
                
);
                
$fallback 'en';
            }
            
$language_seen[$code] = true;

            
// load the fallback recursively
            
$this->loadLanguage($fallback);
            
$fallback_cache $this->cache[$fallback];

            
// merge fallback with current language
            
foreach ($this->keys as $key) {
                if (isset(
$cache[$key]) && isset($fallback_cache[$key])) {
                    if (isset(
$this->mergeable_keys_map[$key])) {
                        
$cache[$key] = $cache[$key] + $fallback_cache[$key];
                    } elseif (isset(
$this->mergeable_keys_list[$key])) {
                        
$cache[$key] = array_merge($fallback_cache[$key], $cache[$key]);
                    }
                } else {
                    
$cache[$key] = $fallback_cache[$key];
                }
            }
        }

        
// save to cache for later retrieval
        
$this->cache[$code] = $cache;
        return;
    }
}





/**
 * Represents a measurable length, with a string numeric magnitude
 * and a unit. This object is immutable.
 */
class HTMLPurifier_Length
{

    
/**
     * String numeric magnitude.
     * @type string
     */
    
protected $n;

    
/**
     * String unit. False is permitted if $n = 0.
     * @type string|bool
     */
    
protected $unit;

    
/**
     * Whether or not this length is valid. Null if not calculated yet.
     * @type bool
     */
    
protected $isValid;

    
/**
     * Array Lookup array of units recognized by CSS 2.1
     * @type array
     */
    
protected static $allowedUnits = array(
        
'em' => true'ex' => true'px' => true'in' => true,
        
'cm' => true'mm' => true'pt' => true'pc' => true
    
);

    
/**
     * @param string $n Magnitude
     * @param bool|string $u Unit
     */
    
public function __construct($n '0'$u false)
    {
        
$this->= (string) $n;
        
$this->unit $u !== false ? (string) $u false;
    }

    
/**
     * @param string $s Unit string, like '2em' or '3.4in'
     * @return HTMLPurifier_Length
     * @warning Does not perform validation.
     */
    
public static function make($s)
    {
        if (
$s instanceof HTMLPurifier_Length) {
            return 
$s;
        }
        
$n_length strspn($s'1234567890.+-');
        
$n substr($s0$n_length);
        
$unit substr($s$n_length);
        if (
$unit === '') {
            
$unit false;
        }
        return new 
HTMLPurifier_Length($n$unit);
    }

    
/**
     * Validates the number and unit.
     * @return bool
     */
    
protected function validate()
    {
        
// Special case:
        
if ($this->=== '+0' || $this->=== '-0') {
            
$this->'0';
        }
        if (
$this->=== '0' && $this->unit === false) {
            return 
true;
        }
        if (!
ctype_lower($this->unit)) {
            
$this->unit strtolower($this->unit);
        }
        if (!isset(
HTMLPurifier_Length::$allowedUnits[$this->unit])) {
            return 
false;
        }
        
// Hack:
        
$def = new HTMLPurifier_AttrDef_CSS_Number();
        
$result $def->validate($this->nfalsefalse);
        if (
$result === false) {
            return 
false;
        }
        
$this->$result;
        return 
true;
    }

    
/**
     * Returns string representation of number.
     * @return string
     */
    
public function toString()
    {
        if (!
$this->isValid()) {
            return 
false;
        }
        return 
$this->$this->unit;
    }

    
/**
     * Retrieves string numeric magnitude.
     * @return string
     */
    
public function getN()
    {
        return 
$this->n;
    }

    
/**
     * Retrieves string unit.
     * @return string
     */
    
public function getUnit()
    {
        return 
$this->unit;
    }

    
/**
     * Returns true if this length unit is valid.
     * @return bool
     */
    
public function isValid()
    {
        if (
$this->isValid === null) {
            
$this->isValid $this->validate();
        }
        return 
$this->isValid;
    }

    
/**
     * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal.
     * @param HTMLPurifier_Length $l
     * @return int
     * @warning If both values are too large or small, this calculation will
     *          not work properly
     */
    
public function compareTo($l)
    {
        if (
$l === false) {
            return 
false;
        }
        if (
$l->unit !== $this->unit) {
            
$converter = new HTMLPurifier_UnitConverter();
            
$l $converter->convert($l$this->unit);
            if (
$l === false) {
                return 
false;
            }
        }
        return 
$this->$l->n;
    }
}





/**
 * Forgivingly lexes HTML (SGML-style) markup into tokens.
 *
 * A lexer parses a string of SGML-style markup and converts them into
 * corresponding tokens.  It doesn't check for well-formedness, although its
 * internal mechanism may make this automatic (such as the case of
 * HTMLPurifier_Lexer_DOMLex).  There are several implementations to choose
 * from.
 *
 * A lexer is HTML-oriented: it might work with XML, but it's not
 * recommended, as we adhere to a subset of the specification for optimization
 * reasons. This might change in the future. Also, most tokenizers are not
 * expected to handle DTDs or PIs.
 *
 * This class should not be directly instantiated, but you may use create() to
 * retrieve a default copy of the lexer.  Being a supertype, this class
 * does not actually define any implementation, but offers commonly used
 * convenience functions for subclasses.
 *
 * @note The unit tests will instantiate this class for testing purposes, as
 *       many of the utility functions require a class to be instantiated.
 *       This means that, even though this class is not runnable, it will
 *       not be declared abstract.
 *
 * @par
 *
 * @note
 * We use tokens rather than create a DOM representation because DOM would:
 *
 * @par
 *  -# Require more processing and memory to create,
 *  -# Is not streamable, and
 *  -# Has the entire document structure (html and body not needed).
 *
 * @par
 * However, DOM is helpful in that it makes it easy to move around nodes
 * without a lot of lookaheads to see when a tag is closed. This is a
 * limitation of the token system and some workarounds would be nice.
 */
class HTMLPurifier_Lexer
{

    
/**
     * Whether or not this lexer implements line-number/column-number tracking.
     * If it does, set to true.
     */
    
public $tracksLineNumbers false;

    
// -- STATIC ----------------------------------------------------------

    /**
     * Retrieves or sets the default Lexer as a Prototype Factory.
     *
     * By default HTMLPurifier_Lexer_DOMLex will be returned. There are
     * a few exceptions involving special features that only DirectLex
     * implements.
     *
     * @note The behavior of this class has changed, rather than accepting
     *       a prototype object, it now accepts a configuration object.
     *       To specify your own prototype, set %Core.LexerImpl to it.
     *       This change in behavior de-singletonizes the lexer object.
     *
     * @param HTMLPurifier_Config $config
     * @return HTMLPurifier_Lexer
     * @throws HTMLPurifier_Exception
     */
    
public static function create($config)
    {
        if (!(
$config instanceof HTMLPurifier_Config)) {
            
$lexer $config;
            
trigger_error(
                
"Passing a prototype to
                HTMLPurifier_Lexer::create() is deprecated, please instead
                use %Core.LexerImpl"
,
                
E_USER_WARNING
            
);
        } else {
            
$lexer $config->get('Core.LexerImpl');
        }

        
$needs_tracking =
            
$config->get('Core.MaintainLineNumbers') ||
            
$config->get('Core.CollectErrors');

        
$inst null;
        if (
is_object($lexer)) {
            
$inst $lexer;
        } else {
            if (
is_null($lexer)) {
                do {
                    
// auto-detection algorithm
                    
if ($needs_tracking) {
                        
$lexer 'DirectLex';
                        break;
                    }

                    if (
class_exists('DOMDocument') &&
                        
method_exists('DOMDocument''loadHTML') &&
                        !
extension_loaded('domxml')
                    ) {
                        
// check for DOM support, because while it's part of the
                        // core, it can be disabled compile time. Also, the PECL
                        // domxml extension overrides the default DOM, and is evil
                        // and nasty and we shan't bother to support it
                        
$lexer 'DOMLex';
                    } else {
                        
$lexer 'DirectLex';
                    }
                } while (
0);
            } 
// do..while so we can break

            // instantiate recognized string names
            
switch ($lexer) {
                case 
'DOMLex':
                    
$inst = new HTMLPurifier_Lexer_DOMLex();
                    break;
                case 
'DirectLex':
                    
$inst = new HTMLPurifier_Lexer_DirectLex();
                    break;
                case 
'PH5P':
                    
$inst = new HTMLPurifier_Lexer_PH5P();
                    break;
                default:
                    throw new 
HTMLPurifier_Exception(
                        
"Cannot instantiate unrecognized Lexer type " .
                        
htmlspecialchars($lexer)
                    );
            }
        }

        if (!
$inst) {
            throw new 
HTMLPurifier_Exception('No lexer was instantiated');
        }

        
// once PHP DOM implements native line numbers, or we
        // hack out something using XSLT, remove this stipulation
        
if ($needs_tracking && !$inst->tracksLineNumbers) {
            throw new 
HTMLPurifier_Exception(
                
'Cannot use lexer that does not support line numbers with ' .
                
'Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)'
            
);
        }

        return 
$inst;

    }

    
// -- CONVENIENCE MEMBERS ---------------------------------------------

    
public function __construct()
    {
        
$this->_entity_parser = new HTMLPurifier_EntityParser();
    }

    
/**
     * Most common entity to raw value conversion table for special entities.
     * @type array
     */
    
protected $_special_entity2str =
        array(
            
'&quot;' => '"',
            
'&amp;' => '&',
            
'&lt;' => '<',
            
'&gt;' => '>',
            
'&#39;' => "'",
            
'&#039;' => "'",
            
'&#x27;' => "'"
        
);

    public function 
parseText($string$config) {
        return 
$this->parseData($stringfalse$config);
    }

    public function 
parseAttr($string$config) {
        return 
$this->parseData($stringtrue$config);
    }

    
/**
     * Parses special entities into the proper characters.
     *
     * This string will translate escaped versions of the special characters
     * into the correct ones.
     *
     * @param string $string String character data to be parsed.
     * @return string Parsed character data.
     */
    
public function parseData($string$is_attr$config)
    {
        
// following functions require at least one character
        
if ($string === '') {
            return 
'';
        }

        
// subtracts amps that cannot possibly be escaped
        
$num_amp substr_count($string'&') - substr_count($string'& ') -
            (
$string[strlen($string) - 1] === '&' 0);

        if (!
$num_amp) {
            return 
$string;
        } 
// abort if no entities
        
$num_esc_amp substr_count($string'&amp;');
        
$string strtr($string$this->_special_entity2str);

        
// code duplication for sake of optimization, see above
        
$num_amp_2 substr_count($string'&') - substr_count($string'& ') -
            (
$string[strlen($string) - 1] === '&' 0);

        if (
$num_amp_2 <= $num_esc_amp) {
            return 
$string;
        }

        
// hmm... now we have some uncommon entities. Use the callback.
        
if ($config->get('Core.LegacyEntityDecoder')) {
            
$string $this->_entity_parser->substituteSpecialEntities($string);
        } else {
            if (
$is_attr) {
                
$string $this->_entity_parser->substituteAttrEntities($string);
            } else {
                
$string $this->_entity_parser->substituteTextEntities($string);
            }
        }
        return 
$string;
    }

    
/**
     * Lexes an HTML string into tokens.
     * @param $string String HTML.
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return HTMLPurifier_Token[] array representation of HTML.
     */
    
public function tokenizeHTML($string$config$context)
    {
        
trigger_error('Call to abstract class'E_USER_ERROR);
    }

    
/**
     * Translates CDATA sections into regular sections (through escaping).
     * @param string $string HTML string to process.
     * @return string HTML with CDATA sections escaped.
     */
    
protected static function escapeCDATA($string)
    {
        return 
preg_replace_callback(
            
'/<![CDATA[(.+?)]]>/s',
            array(
'HTMLPurifier_Lexer''CDATACallback'),
            
$string
        
);
    }

    
/**
     * Special CDATA case that is especially convoluted for <script>
     * @param string $string HTML string to process.
     * @return string HTML with CDATA sections escaped.
     */
    
protected static function escapeCommentedCDATA($string)
    {
        return 
preg_replace_callback(
            
'#<!--//--><![CDATA[//><!--(.+?)//--><!]]>#s',
            array(
'HTMLPurifier_Lexer''CDATACallback'),
            
$string
        
);
    }

    
/**
     * Special Internet Explorer conditional comments should be removed.
     * @param string $string HTML string to process.
     * @return string HTML with conditional comments removed.
     */
    
protected static function removeIEConditional($string)
    {
        return 
preg_replace(
            
'#<!--[if [^>]+]>.*?<![endif]-->#si'// probably should generalize for all strings
            
'',
            
$string
        
);
    }

    
/**
     * Callback function for escapeCDATA() that does the work.
     *
     * @warning Though this is public in order to let the callback happen,
     *          calling it directly is not recommended.
     * @param array $matches PCRE matches array, with index 0 the entire match
     *                  and 1 the inside of the CDATA section.
     * @return string Escaped internals of the CDATA section.
     */
    
protected static function CDATACallback($matches)
    {
        
// not exactly sure why the character set is needed, but whatever
        
return htmlspecialchars($matches[1], ENT_COMPAT'UTF-8');
    }

    
/**
     * Takes a piece of HTML and normalizes it by converting entities, fixing
     * encoding, extracting bits, and other good stuff.
     * @param string $html HTML.
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return string
     * @todo Consider making protected
     */
    
public function normalize($html$config$context)
    {
        
// normalize newlines to n
        
if ($config->get('Core.NormalizeNewlines')) {
            
$html str_replace("rn""n"$html);
            
$html str_replace("r""n"$html);
        }

        if (
$config->get('HTML.Trusted')) {
            
// escape convoluted CDATA
            
$html $this->escapeCommentedCDATA($html);
        }

        
// escape CDATA
        
$html $this->escapeCDATA($html);

        
$html $this->removeIEConditional($html);

        
// extract body from document if applicable
        
if ($config->get('Core.ConvertDocumentToFragment')) {
            
$e false;
            if (
$config->get('Core.CollectErrors')) {
                
$e =& $context->get('ErrorCollector');
            }
            
$new_html $this->extractBody($html);
            if (
$e && $new_html != $html) {
                
$e->send(E_WARNING'Lexer: Extracted body');
            }
            
$html $new_html;
        }

        
// expand entities that aren't the big five
        
if ($config->get('Core.LegacyEntityDecoder')) {
            
$html $this->_entity_parser->substituteNonSpecialEntities($html);
        }

        
// clean into wellformed UTF-8 string for an SGML context: this has
        // to be done after entity expansion because the entities sometimes
        // represent non-SGML characters (horror, horror!)
        
$html HTMLPurifier_Encoder::cleanUTF8($html);

        
// if processing instructions are to removed, remove them now
        
if ($config->get('Core.RemoveProcessingInstructions')) {
            
$html preg_replace('#<?.+??>#s'''$html);
        }

        
$hidden_elements $config->get('Core.HiddenElements');
        if (
$config->get('Core.AggressivelyRemoveScript') &&
            !(
$config->get('HTML.Trusted') || !$config->get('Core.RemoveScriptContents')
            || empty(
$hidden_elements["script"]))) {
            
$html preg_replace('#<script[^>]*>.*?</script>#i'''$html);
        }

        return 
$html;
    }

    
/**
     * Takes a string of HTML (fragment or document) and returns the content
     * @todo Consider making protected
     */
    
public function extractBody($html)
    {
        
$matches = array();
        
$result preg_match('|(.*?)<body[^>]*>(.*)</body>|is'$html$matches);
        if (
$result) {
            
// Make sure it's not in a comment
            
$comment_start strrpos($matches[1], '<!--');
            
$comment_end   strrpos($matches[1], '-->');
            if (
$comment_start === false ||
                (
$comment_end !== false && $comment_end $comment_start)) {
                return 
$matches[2];
            }
        }
        return 
$html;
    }
}





/**
 * Abstract base node class that all others inherit from.
 *
 * Why do we not use the DOM extension?  (1) It is not always available,
 * (2) it has funny constraints on the data it can represent,
 * whereas we want a maximally flexible representation, and (3) its
 * interface is a bit cumbersome.
 */
abstract class HTMLPurifier_Node
{
    
/**
     * Line number of the start token in the source document
     * @type int
     */
    
public $line;

    
/**
     * Column number of the start token in the source document. Null if unknown.
     * @type int
     */
    
public $col;

    
/**
     * Lookup array of processing that this token is exempt from.
     * Currently, valid values are "ValidateAttributes".
     * @type array
     */
    
public $armor = array();

    
/**
     * When true, this node should be ignored as non-existent.
     *
     * Who is responsible for ignoring dead nodes?  FixNesting is
     * responsible for removing them before passing on to child
     * validators.
     */
    
public $dead false;

    
/**
     * Returns a pair of start and end tokens, where the end token
     * is null if it is not necessary. Does not include children.
     * @type array
     */
    
abstract public function toTokenPair();
}





/**
 * Class that handles operations involving percent-encoding in URIs.
 *
 * @warning
 *      Be careful when reusing instances of PercentEncoder. The object
 *      you use for normalize() SHOULD NOT be used for encode(), or
 *      vice-versa.
 */
class HTMLPurifier_PercentEncoder
{

    
/**
     * Reserved characters to preserve when using encode().
     * @type array
     */
    
protected $preserve = array();

    
/**
     * String of characters that should be preserved while using encode().
     * @param bool $preserve
     */
    
public function __construct($preserve false)
    {
        
// unreserved letters, ought to const-ify
        
for ($i 48$i <= 57$i++) { // digits
            
$this->preserve[$i] = true;
        }
        for (
$i 65$i <= 90$i++) { // upper-case
            
$this->preserve[$i] = true;
        }
        for (
$i 97$i <= 122$i++) { // lower-case
            
$this->preserve[$i] = true;
        }
        
$this->preserve[45] = true// Dash         -
        
$this->preserve[46] = true// Period       .
        
$this->preserve[95] = true// Underscore   _
        
$this->preserve[126]= true// Tilde        ~

        // extra letters not to escape
        
if ($preserve !== false) {
            for (
$i 0$c strlen($preserve); $i $c$i++) {
                
$this->preserve[ord($preserve[$i])] = true;
            }
        }
    }

    
/**
     * Our replacement for urlencode, it encodes all non-reserved characters,
     * as well as any extra characters that were instructed to be preserved.
     * @note
     *      Assumes that the string has already been normalized, making any
     *      and all percent escape sequences valid. Percents will not be
     *      re-escaped, regardless of their status in $preserve
     * @param string $string String to be encoded
     * @return string Encoded string.
     */
    
public function encode($string)
    {
        
$ret '';
        for (
$i 0$c strlen($string); $i $c$i++) {
            if (
$string[$i] !== '%' && !isset($this->preserve[$int ord($string[$i])])) {
                
$ret .= '%' sprintf('%02X'$int);
            } else {
                
$ret .= $string[$i];
            }
        }
        return 
$ret;
    }

    
/**
     * Fix up percent-encoding by decoding unreserved characters and normalizing.
     * @warning This function is affected by $preserve, even though the
     *          usual desired behavior is for this not to preserve those
     *          characters. Be careful when reusing instances of PercentEncoder!
     * @param string $string String to normalize
     * @return string
     */
    
public function normalize($string)
    {
        if (
$string == '') {
            return 
'';
        }
        
$parts explode('%'$string);
        
$ret array_shift($parts);
        foreach (
$parts as $part) {
            
$length strlen($part);
            if (
$length 2) {
                
$ret .= '%25' $part;
                continue;
            }
            
$encoding substr($part02);
            
$text     substr($part2);
            if (!
ctype_xdigit($encoding)) {
                
$ret .= '%25' $part;
                continue;
            }
            
$int hexdec($encoding);
            if (isset(
$this->preserve[$int])) {
                
$ret .= chr($int) . $text;
                continue;
            }
            
$encoding strtoupper($encoding);
            
$ret .= '%' $encoding $text;
        }
        return 
$ret;
    }
}





/**
 * Generic property list implementation
 */
class HTMLPurifier_PropertyList
{
    
/**
     * Internal data-structure for properties.
     * @type array
     */
    
protected $data = array();

    
/**
     * Parent plist.
     * @type HTMLPurifier_PropertyList
     */
    
protected $parent;

    
/**
     * Cache.
     * @type array
     */
    
protected $cache;

    
/**
     * @param HTMLPurifier_PropertyList $parent Parent plist
     */
    
public function __construct($parent null)
    {
        
$this->parent $parent;
    }

    
/**
     * Recursively retrieves the value for a key
     * @param string $name
     * @throws HTMLPurifier_Exception
     */
    
public function get($name)
    {
        if (
$this->has($name)) {
            return 
$this->data[$name];
        }
        
// possible performance bottleneck, convert to iterative if necessary
        
if ($this->parent) {
            return 
$this->parent->get($name);
        }
        throw new 
HTMLPurifier_Exception("Key '$name' not found");
    }

    
/**
     * Sets the value of a key, for this plist
     * @param string $name
     * @param mixed $value
     */
    
public function set($name$value)
    {
        
$this->data[$name] = $value;
    }

    
/**
     * Returns true if a given key exists
     * @param string $name
     * @return bool
     */
    
public function has($name)
    {
        return 
array_key_exists($name$this->data);
    }

    
/**
     * Resets a value to the value of it's parent, usually the default. If
     * no value is specified, the entire plist is reset.
     * @param string $name
     */
    
public function reset($name null)
    {
        if (
$name == null) {
            
$this->data = array();
        } else {
            unset(
$this->data[$name]);
        }
    }

    
/**
     * Squashes this property list and all of its property lists into a single
     * array, and returns the array. This value is cached by default.
     * @param bool $force If true, ignores the cache and regenerates the array.
     * @return array
     */
    
public function squash($force false)
    {
        if (
$this->cache !== null && !$force) {
            return 
$this->cache;
        }
        if (
$this->parent) {
            return 
$this->cache array_merge($this->parent->squash($force), $this->data);
        } else {
            return 
$this->cache $this->data;
        }
    }

    
/**
     * Returns the parent plist.
     * @return HTMLPurifier_PropertyList
     */
    
public function getParent()
    {
        return 
$this->parent;
    }

    
/**
     * Sets the parent plist.
     * @param HTMLPurifier_PropertyList $plist Parent plist
     */
    
public function setParent($plist)
    {
        
$this->parent $plist;
    }
}





/**
 * Property list iterator. Do not instantiate this class directly.
 */
class HTMLPurifier_PropertyListIterator extends FilterIterator
{

    
/**
     * @type int
     */
    
protected $l;
    
/**
     * @type string
     */
    
protected $filter;

    
/**
     * @param Iterator $iterator Array of data to iterate over
     * @param string $filter Optional prefix to only allow values of
     */
    
public function __construct(Iterator $iterator$filter null)
    {
        
parent::__construct($iterator);
        
$this->strlen($filter);
        
$this->filter $filter;
    }

    
/**
     * @return bool
     */
    
public function accept()
    {
        
$key $this->getInnerIterator()->key();
        if (
strncmp($key$this->filter$this->l) !== 0) {
            return 
false;
        }
        return 
true;
    }
}





/**
 * A simple array-backed queue, based off of the classic Okasaki
 * persistent amortized queue.  The basic idea is to maintain two
 * stacks: an input stack and an output stack.  When the output
 * stack runs out, reverse the input stack and use it as the output
 * stack.
 *
 * We don't use the SPL implementation because it's only supported
 * on PHP 5.3 and later.
 *
 * Exercise: Prove that push/pop on this queue take amortized O(1) time.
 *
 * Exercise: Extend this queue to be a deque, while preserving amortized
 * O(1) time.  Some care must be taken on rebalancing to avoid quadratic
 * behaviour caused by repeatedly shuffling data from the input stack
 * to the output stack and back.
 */
class HTMLPurifier_Queue {
    private 
$input;
    private 
$output;

    public function 
__construct($input = array()) {
        
$this->input $input;
        
$this->output = array();
    }

    
/**
     * Shifts an element off the front of the queue.
     */
    
public function shift() {
        if (empty(
$this->output)) {
            
$this->output array_reverse($this->input);
            
$this->input = array();
        }
        if (empty(
$this->output)) {
            return 
NULL;
        }
        return 
array_pop($this->output);
    }

    
/**
     * Pushes an element onto the front of the queue.
     */
    
public function push($x) {
        
array_push($this->input$x);
    }

    
/**
     * Checks if it's empty.
     */
    
public function isEmpty() {
        return empty(
$this->input) && empty($this->output);
    }
}



/**
 * Supertype for classes that define a strategy for modifying/purifying tokens.
 *
 * While HTMLPurifier's core purpose is fixing HTML into something proper,
 * strategies provide plug points for extra configuration or even extra
 * features, such as custom tags, custom parsing of text, etc.
 */


abstract class HTMLPurifier_Strategy
{

    
/**
     * Executes the strategy on the tokens.
     *
     * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token objects to be operated on.
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return HTMLPurifier_Token[] Processed array of token objects.
     */
    
abstract public function execute($tokens$config$context);
}





/**
 * This is in almost every respect equivalent to an array except
 * that it keeps track of which keys were accessed.
 *
 * @warning For the sake of backwards compatibility with early versions
 *     of PHP 5, you must not use the $hash[$key] syntax; if you do
 *     our version of offsetGet is never called.
 */
class HTMLPurifier_StringHash extends ArrayObject
{
    
/**
     * @type array
     */
    
protected $accessed = array();

    
/**
     * Retrieves a value, and logs the access.
     * @param mixed $index
     * @return mixed
     */
    
public function offsetGet($index)
    {
        
$this->accessed[$index] = true;
        return 
parent::offsetGet($index);
    }

    
/**
     * Returns a lookup array of all array indexes that have been accessed.
     * @return array in form array($index => true).
     */
    
public function getAccessed()
    {
        return 
$this->accessed;
    }

    
/**
     * Resets the access array.
     */
    
public function resetAccessed()
    {
        
$this->accessed = array();
    }
}





/**
 * Parses string hash files. File format is as such:
 *
 *      DefaultKeyValue
 *      KEY: Value
 *      KEY2: Value2
 *      --MULTILINE-KEY--
 *      Multiline
 *      value.
 *
 * Which would output something similar to:
 *
 *      array(
 *          'ID' => 'DefaultKeyValue',
 *          'KEY' => 'Value',
 *          'KEY2' => 'Value2',
 *          'MULTILINE-KEY' => "Multilinenvalue.n",
 *      )
 *
 * We use this as an easy to use file-format for configuration schema
 * files, but the class itself is usage agnostic.
 *
 * You can use ---- to forcibly terminate parsing of a single string-hash;
 * this marker is used in multi string-hashes to delimit boundaries.
 */
class HTMLPurifier_StringHashParser
{

    
/**
     * @type string
     */
    
public $default 'ID';

    
/**
     * Parses a file that contains a single string-hash.
     * @param string $file
     * @return array
     */
    
public function parseFile($file)
    {
        if (!
file_exists($file)) {
            return 
false;
        }
        
$fh fopen($file'r');
        if (!
$fh) {
            return 
false;
        }
        
$ret $this->parseHandle($fh);
        
fclose($fh);
        return 
$ret;
    }

    
/**
     * Parses a file that contains multiple string-hashes delimited by '----'
     * @param string $file
     * @return array
     */
    
public function parseMultiFile($file)
    {
        if (!
file_exists($file)) {
            return 
false;
        }
        
$ret = array();
        
$fh fopen($file'r');
        if (!
$fh) {
            return 
false;
        }
        while (!
feof($fh)) {
            
$ret[] = $this->parseHandle($fh);
        }
        
fclose($fh);
        return 
$ret;
    }

    
/**
     * Internal parser that acepts a file handle.
     * @note While it's possible to simulate in-memory parsing by using
     *       custom stream wrappers, if such a use-case arises we should
     *       factor out the file handle into its own class.
     * @param resource $fh File handle with pointer at start of valid string-hash
     *            block.
     * @return array
     */
    
protected function parseHandle($fh)
    {
        
$state   false;
        
$single  false;
        
$ret     = array();
        do {
            
$line fgets($fh);
            if (
$line === false) {
                break;
            }
            
$line rtrim($line"nr");
            if (!
$state && $line === '') {
                continue;
            }
            if (
$line === '----') {
                break;
            }
            if (
strncmp('--#'$line3) === 0) {
                
// Comment
                
continue;
            } elseif (
strncmp('--'$line2) === 0) {
                
// Multiline declaration
                
$state trim($line'- ');
                if (!isset(
$ret[$state])) {
                    
$ret[$state] = '';
                }
                continue;
            } elseif (!
$state) {
                
$single true;
                if (
strpos($line':') !== false) {
                    
// Single-line declaration
                    
list($state$line) = explode(':'$line2);
                    
$line trim($line);
                } else {
                    
// Use default declaration
                    
$state  $this->default;
                }
            }
            if (
$single) {
                
$ret[$state] = $line;
                
$single false;
                
$state  false;
            } else {
                
$ret[$state] .= "$linen";
            }
        } while (!
feof($fh));
        return 
$ret;
    }
}





/**
 * Defines a mutation of an obsolete tag into a valid tag.
 */
abstract class HTMLPurifier_TagTransform
{

    
/**
     * Tag name to transform the tag to.
     * @type string
     */
    
public $transform_to;

    
/**
     * Transforms the obsolete tag into the valid tag.
     * @param HTMLPurifier_Token_Tag $tag Tag to be transformed.
     * @param HTMLPurifier_Config $config Mandatory HTMLPurifier_Config object
     * @param HTMLPurifier_Context $context Mandatory HTMLPurifier_Context object
     */
    
abstract public function transform($tag$config$context);

    
/**
     * Prepends CSS properties to the style attribute, creating the
     * attribute if it doesn't exist.
     * @warning Copied over from AttrTransform, be sure to keep in sync
     * @param array $attr Attribute array to process (passed by reference)
     * @param string $css CSS to prepend
     */
    
protected function prependCSS(&$attr$css)
    {
        
$attr['style'] = isset($attr['style']) ? $attr['style'] : '';
        
$attr['style'] = $css $attr['style'];
    }
}





/**
 * Abstract base token class that all others inherit from.
 */
abstract class HTMLPurifier_Token
{
    
/**
     * Line number node was on in source document. Null if unknown.
     * @type int
     */
    
public $line;

    
/**
     * Column of line node was on in source document. Null if unknown.
     * @type int
     */
    
public $col;

    
/**
     * Lookup array of processing that this token is exempt from.
     * Currently, valid values are "ValidateAttributes" and
     * "MakeWellFormed_TagClosedError"
     * @type array
     */
    
public $armor = array();

    
/**
     * Used during MakeWellFormed.  See Note [Injector skips]
     * @type
     */
    
public $skip;

    
/**
     * @type
     */
    
public $rewind;

    
/**
     * @type
     */
    
public $carryover;

    
/**
     * @param string $n
     * @return null|string
     */
    
public function __get($n)
    {
        if (
$n === 'type') {
            
trigger_error('Deprecated type property called; use instanceof'E_USER_NOTICE);
            switch (
get_class($this)) {
                case 
'HTMLPurifier_Token_Start':
                    return 
'start';
                case 
'HTMLPurifier_Token_Empty':
                    return 
'empty';
                case 
'HTMLPurifier_Token_End':
                    return 
'end';
                case 
'HTMLPurifier_Token_Text':
                    return 
'text';
                case 
'HTMLPurifier_Token_Comment':
                    return 
'comment';
                default:
                    return 
null;
            }
        }
    }

    
/**
     * Sets the position of the token in the source document.
     * @param int $l
     * @param int $c
     */
    
public function position($l null$c null)
    {
        
$this->line $l;
        
$this->col $c;
    }

    
/**
     * Convenience function for DirectLex settings line/col position.
     * @param int $l
     * @param int $c
     */
    
public function rawPosition($l$c)
    {
        if (
$c === -1) {
            
$l++;
        }
        
$this->line $l;
        
$this->col $c;
    }

    
/**
     * Converts a token into its corresponding node.
     */
    
abstract public function toNode();
}





/**
 * Factory for token generation.
 *
 * @note Doing some benchmarking indicates that the new operator is much
 *       slower than the clone operator (even discounting the cost of the
 *       constructor).  This class is for that optimization.
 *       Other then that, there's not much point as we don't
 *       maintain parallel HTMLPurifier_Token hierarchies (the main reason why
 *       you'd want to use an abstract factory).
 * @todo Port DirectLex to use this
 */
class HTMLPurifier_TokenFactory
{
    
// p stands for prototype

    /**
     * @type HTMLPurifier_Token_Start
     */
    
private $p_start;

    
/**
     * @type HTMLPurifier_Token_End
     */
    
private $p_end;

    
/**
     * @type HTMLPurifier_Token_Empty
     */
    
private $p_empty;

    
/**
     * @type HTMLPurifier_Token_Text
     */
    
private $p_text;

    
/**
     * @type HTMLPurifier_Token_Comment
     */
    
private $p_comment;

    
/**
     * Generates blank prototypes for cloning.
     */
    
public function __construct()
    {
        
$this->p_start = new HTMLPurifier_Token_Start('', array());
        
$this->p_end = new HTMLPurifier_Token_End('');
        
$this->p_empty = new HTMLPurifier_Token_Empty('', array());
        
$this->p_text = new HTMLPurifier_Token_Text('');
        
$this->p_comment = new HTMLPurifier_Token_Comment('');
    }

    
/**
     * Creates a HTMLPurifier_Token_Start.
     * @param string $name Tag name
     * @param array $attr Associative array of attributes
     * @return HTMLPurifier_Token_Start Generated HTMLPurifier_Token_Start
     */
    
public function createStart($name$attr = array())
    {
        
$p = clone $this->p_start;
        
$p->__construct($name$attr);
        return 
$p;
    }

    
/**
     * Creates a HTMLPurifier_Token_End.
     * @param string $name Tag name
     * @return HTMLPurifier_Token_End Generated HTMLPurifier_Token_End
     */
    
public function createEnd($name)
    {
        
$p = clone $this->p_end;
        
$p->__construct($name);
        return 
$p;
    }

    
/**
     * Creates a HTMLPurifier_Token_Empty.
     * @param string $name Tag name
     * @param array $attr Associative array of attributes
     * @return HTMLPurifier_Token_Empty Generated HTMLPurifier_Token_Empty
     */
    
public function createEmpty($name$attr = array())
    {
        
$p = clone $this->p_empty;
        
$p->__construct($name$attr);
        return 
$p;
    }

    
/**
     * Creates a HTMLPurifier_Token_Text.
     * @param string $data Data of text token
     * @return HTMLPurifier_Token_Text Generated HTMLPurifier_Token_Text
     */
    
public function createText($data)
    {
        
$p = clone $this->p_text;
        
$p->__construct($data);
        return 
$p;
    }

    
/**
     * Creates a HTMLPurifier_Token_Comment.
     * @param string $data Data of comment token
     * @return HTMLPurifier_Token_Comment Generated HTMLPurifier_Token_Comment
     */
    
public function createComment($data)
    {
        
$p = clone $this->p_comment;
        
$p->__construct($data);
        return 
$p;
    }
}





/**
 * HTML Purifier's internal representation of a URI.
 * @note
 *      Internal data-structures are completely escaped. If the data needs
 *      to be used in a non-URI context (which is very unlikely), be sure
 *      to decode it first. The URI may not necessarily be well-formed until
 *      validate() is called.
 */
class HTMLPurifier_URI
{
    
/**
     * @type string
     */
    
public $scheme;

    
/**
     * @type string
     */
    
public $userinfo;

    
/**
     * @type string
     */
    
public $host;

    
/**
     * @type int
     */
    
public $port;

    
/**
     * @type string
     */
    
public $path;

    
/**
     * @type string
     */
    
public $query;

    
/**
     * @type string
     */
    
public $fragment;

    
/**
     * @param string $scheme
     * @param string $userinfo
     * @param string $host
     * @param int $port
     * @param string $path
     * @param string $query
     * @param string $fragment
     * @note Automatically normalizes scheme and port
     */
    
public function __construct($scheme$userinfo$host$port$path$query$fragment)
    {
        
$this->scheme is_null($scheme) || ctype_lower($scheme) ? $scheme strtolower($scheme);
        
$this->userinfo $userinfo;
        
$this->host $host;
        
$this->port is_null($port) ? $port : (int)$port;
        
$this->path $path;
        
$this->query $query;
        
$this->fragment $fragment;
    }

    
/**
     * Retrieves a scheme object corresponding to the URI's scheme/default
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return HTMLPurifier_URIScheme Scheme object appropriate for validating this URI
     */
    
public function getSchemeObj($config$context)
    {
        
$registry HTMLPurifier_URISchemeRegistry::instance();
        if (
$this->scheme !== null) {
            
$scheme_obj $registry->getScheme($this->scheme$config$context);
            if (!
$scheme_obj) {
                return 
false;
            } 
// invalid scheme, clean it out
        
} else {
            
// no scheme: retrieve the default one
            
$def $config->getDefinition('URI');
            
$scheme_obj $def->getDefaultScheme($config$context);
            if (!
$scheme_obj) {
                if (
$def->defaultScheme !== null) {
                    
// something funky happened to the default scheme object
                    
trigger_error(
                        
'Default scheme object "' $def->defaultScheme '" was not readable',
                        
E_USER_WARNING
                    
);
                } 
// suppress error if it's null
                
return false;
            }
        }
        return 
$scheme_obj;
    }

    
/**
     * Generic validation method applicable for all schemes. May modify
     * this URI in order to get it into a compliant form.
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool True if validation/filtering succeeds, false if failure
     */
    
public function validate($config$context)
    {
        
// ABNF definitions from RFC 3986
        
$chars_sub_delims '!$&'()*+,;=';
        $chars_gen_delims = '
:/?#[]@';
        
$chars_pchar $chars_sub_delims ':@';

        
// validate host
        
if (!is_null($this->host)) {
            
$host_def = new HTMLPurifier_AttrDef_URI_Host();
            
$this->host $host_def->validate($this->host$config$context);
            if (
$this->host === false) {
                
$this->host null;
            }
        }

        
// validate scheme
        // NOTE: It's not appropriate to check whether or not this
        // scheme is in our registry, since a URIFilter may convert a
        // URI that we don't allow into one we do.  So instead, we just
        // check if the scheme can be dropped because there is no host
        // and it is our default scheme.
        
if (!is_null($this->scheme) && is_null($this->host) || $this->host === '') {
            
// support for relative paths is pretty abysmal when the
            // scheme is present, so axe it when possible
            
$def $config->getDefinition('URI');
            if (
$def->defaultScheme === $this->scheme) {
                
$this->scheme null;
            }
        }

        
// validate username
        
if (!is_null($this->userinfo)) {
            
$encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims ':');
            
$this->userinfo $encoder->encode($this->userinfo);
        }

        
// validate port
        
if (!is_null($this->port)) {
            if (
$this->port || $this->port 65535) {
                
$this->port null;
            }
        }

        
// validate path
        
$segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar '/');
        if (!
is_null($this->host)) { // this catches $this->host === ''
            // path-abempty (hier and relative)
            // http://www.example.com/my/path
            // //www.example.com/my/path (looks odd, but works, and
            //                            recognized by most browsers)
            // (this set is valid or invalid on a scheme by scheme
            // basis, so we'll deal with it later)
            // file:///my/path
            // ///my/path
            
$this->path $segments_encoder->encode($this->path);
        } elseif (
$this->path !== '') {
            if (
$this->path[0] === '/') {
                
// path-absolute (hier and relative)
                // http:/my/path
                // /my/path
                
if (strlen($this->path) >= && $this->path[1] === '/') {
                    
// This could happen if both the host gets stripped
                    // out
                    // http://my/path
                    // //my/path
                    
$this->path '';
                } else {
                    
$this->path $segments_encoder->encode($this->path);
                }
            } elseif (!
is_null($this->scheme)) {
                
// path-rootless (hier)
                // http:my/path
                // Short circuit evaluation means we don't need to check nz
                
$this->path $segments_encoder->encode($this->path);
            } else {
                
// path-noscheme (relative)
                // my/path
                // (once again, not checking nz)
                
$segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims '@');
                
$c strpos($this->path'/');
                if (
$c !== false) {
                    
$this->path =
                        
$segment_nc_encoder->encode(substr($this->path0$c)) .
                        
$segments_encoder->encode(substr($this->path$c));
                } else {
                    
$this->path $segment_nc_encoder->encode($this->path);
                }
            }
        } else {
            
// path-empty (hier and relative)
            
$this->path ''// just to be safe
        
}

        
// qf = query and fragment
        
$qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar '/?');

        if (!
is_null($this->query)) {
            
$this->query $qf_encoder->encode($this->query);
        }

        if (!
is_null($this->fragment)) {
            
$this->fragment $qf_encoder->encode($this->fragment);
        }
        return 
true;
    }

    
/**
     * Convert URI back to string
     * @return string URI appropriate for output
     */
    
public function toString()
    {
        
// reconstruct authority
        
$authority null;
        
// there is a rendering difference between a null authority
        // (http:foo-bar) and an empty string authority
        // (http:///foo-bar).
        
if (!is_null($this->host)) {
            
$authority '';
            if (!
is_null($this->userinfo)) {
                
$authority .= $this->userinfo '@';
            }
            
$authority .= $this->host;
            if (!
is_null($this->port)) {
                
$authority .= ':' $this->port;
            }
        }

        
// Reconstruct the result
        // One might wonder about parsing quirks from browsers after
        // this reconstruction.  Unfortunately, parsing behavior depends
        // on what *scheme* was employed (file:///foo is handled *very*
        // differently than http:///foo), so unfortunately we have to
        // defer to the schemes to do the right thing.
        
$result '';
        if (!
is_null($this->scheme)) {
            
$result .= $this->scheme ':';
        }
        if (!
is_null($authority)) {
            
$result .= '//' $authority;
        }
        
$result .= $this->path;
        if (!
is_null($this->query)) {
            
$result .= '?' $this->query;
        }
        if (!
is_null($this->fragment)) {
            
$result .= '#' $this->fragment;
        }

        return 
$result;
    }

    
/**
     * Returns true if this URL might be considered a 'local' URL given
     * the current context.  This is true when the host is null, or
     * when it matches the host supplied to the configuration.
     *
     * Note that this does not do any scheme checking, so it is mostly
     * only appropriate for metadata that doesn't care about protocol
     * security.  isBenign is probably what you actually want.
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool
     */
    
public function isLocal($config$context)
    {
        if (
$this->host === null) {
            return 
true;
        }
        
$uri_def $config->getDefinition('URI');
        if (
$uri_def->host === $this->host) {
            return 
true;
        }
        return 
false;
    }

    
/**
     * Returns true if this URL should be considered a 'benign' URL,
     * that is:
     *
     *      - It is a local URL (isLocal), and
     *      - It has a equal or better level of security
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool
     */
    
public function isBenign($config$context)
    {
        if (!
$this->isLocal($config$context)) {
            return 
false;
        }

        
$scheme_obj $this->getSchemeObj($config$context);
        if (!
$scheme_obj) {
            return 
false;
        } 
// conservative approach

        
$current_scheme_obj $config->getDefinition('URI')->getDefaultScheme($config$context);
        if (
$current_scheme_obj->secure) {
            if (!
$scheme_obj->secure) {
                return 
false;
            }
        }
        return 
true;
    }
}





class 
HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
{

    public 
$type 'URI';
    protected 
$filters = array();
    protected 
$postFilters = array();
    protected 
$registeredFilters = array();

    
/**
     * HTMLPurifier_URI object of the base specified at %URI.Base
     */
    
public $base;

    
/**
     * String host to consider "home" base, derived off of $base
     */
    
public $host;

    
/**
     * Name of default scheme based on %URI.DefaultScheme and %URI.Base
     */
    
public $defaultScheme;

    public function 
__construct()
    {
        
$this->registerFilter(new HTMLPurifier_URIFilter_DisableExternal());
        
$this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources());
        
$this->registerFilter(new HTMLPurifier_URIFilter_DisableResources());
        
$this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist());
        
$this->registerFilter(new HTMLPurifier_URIFilter_SafeIframe());
        
$this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute());
        
$this->registerFilter(new HTMLPurifier_URIFilter_Munge());
    }

    public function 
registerFilter($filter)
    {
        
$this->registeredFilters[$filter->name] = $filter;
    }

    public function 
addFilter($filter$config)
    {
        
$r $filter->prepare($config);
        if (
$r === false) return; // null is ok, for backwards compat
        
if ($filter->post) {
            
$this->postFilters[$filter->name] = $filter;
        } else {
            
$this->filters[$filter->name] = $filter;
        }
    }

    protected function 
doSetup($config)
    {
        
$this->setupMemberVariables($config);
        
$this->setupFilters($config);
    }

    protected function 
setupFilters($config)
    {
        foreach (
$this->registeredFilters as $name => $filter) {
            if (
$filter->always_load) {
                
$this->addFilter($filter$config);
            } else {
                
$conf $config->get('URI.' $name);
                if (
$conf !== false && $conf !== null) {
                    
$this->addFilter($filter$config);
                }
            }
        }
        unset(
$this->registeredFilters);
    }

    protected function 
setupMemberVariables($config)
    {
        
$this->host $config->get('URI.Host');
        
$base_uri $config->get('URI.Base');
        if (!
is_null($base_uri)) {
            
$parser = new HTMLPurifier_URIParser();
            
$this->base $parser->parse($base_uri);
            
$this->defaultScheme $this->base->scheme;
            if (
is_null($this->host)) $this->host $this->base->host;
        }
        if (
is_null($this->defaultScheme)) $this->defaultScheme $config->get('URI.DefaultScheme');
    }

    public function 
getDefaultScheme($config$context)
    {
        return 
HTMLPurifier_URISchemeRegistry::instance()->getScheme($this->defaultScheme$config$context);
    }

    public function 
filter(&$uri$config$context)
    {
        foreach (
$this->filters as $name => $f) {
            
$result $f->filter($uri$config$context);
            if (!
$result) return false;
        }
        return 
true;
    }

    public function 
postFilter(&$uri$config$context)
    {
        foreach (
$this->postFilters as $name => $f) {
            
$result $f->filter($uri$config$context);
            if (!
$result) return false;
        }
        return 
true;
    }

}





/**
 * Chainable filters for custom URI processing.
 *
 * These filters can perform custom actions on a URI filter object,
 * including transformation or blacklisting.  A filter named Foo
 * must have a corresponding configuration directive %URI.Foo,
 * unless always_load is specified to be true.
 *
 * The following contexts may be available while URIFilters are being
 * processed:
 *
 *      - EmbeddedURI: true if URI is an embedded resource that will
 *        be loaded automatically on page load
 *      - CurrentToken: a reference to the token that is currently
 *        being processed
 *      - CurrentAttr: the name of the attribute that is currently being
 *        processed
 *      - CurrentCSSProperty: the name of the CSS property that is
 *        currently being processed (if applicable)
 *
 * @warning This filter is called before scheme object validation occurs.
 *          Make sure, if you require a specific scheme object, you
 *          you check that it exists. This allows filters to convert
 *          proprietary URI schemes into regular ones.
 */
abstract class HTMLPurifier_URIFilter
{

    
/**
     * Unique identifier of filter.
     * @type string
     */
    
public $name;

    
/**
     * True if this filter should be run after scheme validation.
     * @type bool
     */
    
public $post false;

    
/**
     * True if this filter should always be loaded.
     * This permits a filter to be named Foo without the corresponding
     * %URI.Foo directive existing.
     * @type bool
     */
    
public $always_load false;

    
/**
     * Performs initialization for the filter.  If the filter returns
     * false, this means that it shouldn't be considered active.
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function prepare($config)
    {
        return 
true;
    }

    
/**
     * Filter a URI object
     * @param HTMLPurifier_URI $uri Reference to URI object variable
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool Whether or not to continue processing: false indicates
     *         URL is no good, true indicates continue processing. Note that
     *         all changes are committed directly on the URI object
     */
    
abstract public function filter(&$uri$config$context);
}





/**
 * Parses a URI into the components and fragment identifier as specified
 * by RFC 3986.
 */
class HTMLPurifier_URIParser
{

    
/**
     * Instance of HTMLPurifier_PercentEncoder to do normalization with.
     */
    
protected $percentEncoder;

    public function 
__construct()
    {
        
$this->percentEncoder = new HTMLPurifier_PercentEncoder();
    }

    
/**
     * Parses a URI.
     * @param $uri string URI to parse
     * @return HTMLPurifier_URI representation of URI. This representation has
     *         not been validated yet and may not conform to RFC.
     */
    
public function parse($uri)
    {
        
$uri $this->percentEncoder->normalize($uri);

        
// Regexp is as per Appendix B.
        // Note that ["<>] are an addition to the RFC's recommended
        // characters, because they represent external delimeters.
        
$r_URI '!'.
            
'(([a-zA-Z0-9.+-]+):)?'// 2. Scheme
            
'(//([^/?#"<>]*))?'// 4. Authority
            
'([^?#"<>]*)'.       // 5. Path
            
'(?([^#"<>]*))?'.   // 7. Query
            
'(#([^"<>]*))?'.     // 8. Fragment
            
'!';

        
$matches = array();
        
$result preg_match($r_URI$uri$matches);

        if (!
$result) return false// *really* invalid URI

        // seperate out parts
        
$scheme     = !empty($matches[1]) ? $matches[2] : null;
        
$authority  = !empty($matches[3]) ? $matches[4] : null;
        
$path       $matches[5]; // always present, can be empty
        
$query      = !empty($matches[6]) ? $matches[7] : null;
        
$fragment   = !empty($matches[8]) ? $matches[9] : null;

        
// further parse authority
        
if ($authority !== null) {
            
$r_authority "/^((.+?)@)?([[^]]+]|[^:]*)(:(d*))?/";
            
$matches = array();
            
preg_match($r_authority$authority$matches);
            
$userinfo   = !empty($matches[1]) ? $matches[2] : null;
            
$host       = !empty($matches[3]) ? $matches[3] : '';
            
$port       = !empty($matches[4]) ? (int) $matches[5] : null;
        } else {
            
$port $host $userinfo null;
        }

        return new 
HTMLPurifier_URI(
            
$scheme$userinfo$host$port$path$query$fragment);
    }

}





/**
 * Validator for the components of a URI for a specific scheme
 */
abstract class HTMLPurifier_URIScheme
{

    
/**
     * Scheme's default port (integer). If an explicit port number is
     * specified that coincides with the default port, it will be
     * elided.
     * @type int
     */
    
public $default_port null;

    
/**
     * Whether or not URIs of this scheme are locatable by a browser
     * http and ftp are accessible, while mailto and news are not.
     * @type bool
     */
    
public $browsable false;

    
/**
     * Whether or not data transmitted over this scheme is encrypted.
     * https is secure, http is not.
     * @type bool
     */
    
public $secure false;

    
/**
     * Whether or not the URI always uses <hier_part>, resolves edge cases
     * with making relative URIs absolute
     * @type bool
     */
    
public $hierarchical false;

    
/**
     * Whether or not the URI may omit a hostname when the scheme is
     * explicitly specified, ala file:///path/to/file. As of writing,
     * 'file' is the only scheme that browsers support his properly.
     * @type bool
     */
    
public $may_omit_host false;

    
/**
     * Validates the components of a URI for a specific scheme.
     * @param HTMLPurifier_URI $uri Reference to a HTMLPurifier_URI object
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool success or failure
     */
    
abstract public function doValidate(&$uri$config$context);

    
/**
     * Public interface for validating components of a URI.  Performs a
     * bunch of default actions. Don't overload this method.
     * @param HTMLPurifier_URI $uri Reference to a HTMLPurifier_URI object
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool success or failure
     */
    
public function validate(&$uri$config$context)
    {
        if (
$this->default_port == $uri->port) {
            
$uri->port null;
        }
        
// kludge: browsers do funny things when the scheme but not the
        // authority is set
        
if (!$this->may_omit_host &&
            
// if the scheme is present, a missing host is always in error
            
(!is_null($uri->scheme) && ($uri->host === '' || is_null($uri->host))) ||
            
// if the scheme is not present, a *blank* host is in error,
            // since this translates into '///path' which most browsers
            // interpret as being 'http://path'.
            
(is_null($uri->scheme) && $uri->host === '')
        ) {
            do {
                if (
is_null($uri->scheme)) {
                    if (
substr($uri->path02) != '//') {
                        
$uri->host null;
                        break;
                    }
                    
// URI is '////path', so we cannot nullify the
                    // host to preserve semantics.  Try expanding the
                    // hostname instead (fall through)
                
}
                
// first see if we can manually insert a hostname
                
$host $config->get('URI.Host');
                if (!
is_null($host)) {
                    
$uri->host $host;
                } else {
                    
// we can't do anything sensible, reject the URL.
                    
return false;
                }
            } while (
false);
        }
        return 
$this->doValidate($uri$config$context);
    }
}





/**
 * Registry for retrieving specific URI scheme validator objects.
 */
class HTMLPurifier_URISchemeRegistry
{

    
/**
     * Retrieve sole instance of the registry.
     * @param HTMLPurifier_URISchemeRegistry $prototype Optional prototype to overload sole instance with,
     *                   or bool true to reset to default registry.
     * @return HTMLPurifier_URISchemeRegistry
     * @note Pass a registry object $prototype with a compatible interface and
     *       the function will copy it and return it all further times.
     */
    
public static function instance($prototype null)
    {
        static 
$instance null;
        if (
$prototype !== null) {
            
$instance $prototype;
        } elseif (
$instance === null || $prototype == true) {
            
$instance = new HTMLPurifier_URISchemeRegistry();
        }
        return 
$instance;
    }

    
/**
     * Cache of retrieved schemes.
     * @type HTMLPurifier_URIScheme[]
     */
    
protected $schemes = array();

    
/**
     * Retrieves a scheme validator object
     * @param string $scheme String scheme name like http or mailto
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return HTMLPurifier_URIScheme
     */
    
public function getScheme($scheme$config$context)
    {
        if (!
$config) {
            
$config HTMLPurifier_Config::createDefault();
        }

        
// important, otherwise attacker could include arbitrary file
        
$allowed_schemes $config->get('URI.AllowedSchemes');
        if (!
$config->get('URI.OverrideAllowedSchemes') &&
            !isset(
$allowed_schemes[$scheme])
        ) {
            return;
        }

        if (isset(
$this->schemes[$scheme])) {
            return 
$this->schemes[$scheme];
        }
        if (!isset(
$allowed_schemes[$scheme])) {
            return;
        }

        
$class 'HTMLPurifier_URIScheme_' $scheme;
        if (!
class_exists($class)) {
            return;
        }
        
$this->schemes[$scheme] = new $class();
        return 
$this->schemes[$scheme];
    }

    
/**
     * Registers a custom scheme to the cache, bypassing reflection.
     * @param string $scheme Scheme name
     * @param HTMLPurifier_URIScheme $scheme_obj
     */
    
public function register($scheme$scheme_obj)
    {
        
$this->schemes[$scheme] = $scheme_obj;
    }
}





/**
 * Class for converting between different unit-lengths as specified by
 * CSS.
 */
class HTMLPurifier_UnitConverter
{

    const 
ENGLISH 1;
    const 
METRIC 2;
    const 
DIGITAL 3;

    
/**
     * Units information array. Units are grouped into measuring systems
     * (English, Metric), and are assigned an integer representing
     * the conversion factor between that unit and the smallest unit in
     * the system. Numeric indexes are actually magical constants that
     * encode conversion data from one system to the next, with a O(n^2)
     * constraint on memory (this is generally not a problem, since
     * the number of measuring systems is small.)
     */
    
protected static $units = array(
        
self::ENGLISH => array(
            
'px' => 3// This is as per CSS 2.1 and Firefox. Your mileage may vary
            
'pt' => 4,
            
'pc' => 48,
            
'in' => 288,
            
self::METRIC => array('pt''0.352777778''mm'),
        ),
        
self::METRIC => array(
            
'mm' => 1,
            
'cm' => 10,
            
self::ENGLISH => array('mm''2.83464567''pt'),
        ),
    );

    
/**
     * Minimum bcmath precision for output.
     * @type int
     */
    
protected $outputPrecision;

    
/**
     * Bcmath precision for internal calculations.
     * @type int
     */
    
protected $internalPrecision;

    
/**
     * Whether or not BCMath is available.
     * @type bool
     */
    
private $bcmath;

    public function 
__construct($output_precision 4$internal_precision 10$force_no_bcmath false)
    {
        
$this->outputPrecision $output_precision;
        
$this->internalPrecision $internal_precision;
        
$this->bcmath = !$force_no_bcmath && function_exists('bcmul');
    }

    
/**
     * Converts a length object of one unit into another unit.
     * @param HTMLPurifier_Length $length
     *      Instance of HTMLPurifier_Length to convert. You must validate()
     *      it before passing it here!
     * @param string $to_unit
     *      Unit to convert to.
     * @return HTMLPurifier_Length|bool
     * @note
     *      About precision: This conversion function pays very special
     *      attention to the incoming precision of values and attempts
     *      to maintain a number of significant figure. Results are
     *      fairly accurate up to nine digits. Some caveats:
     *          - If a number is zero-padded as a result of this significant
     *            figure tracking, the zeroes will be eliminated.
     *          - If a number contains less than four sigfigs ($outputPrecision)
     *            and this causes some decimals to be excluded, those
     *            decimals will be added on.
     */
    
public function convert($length$to_unit)
    {
        if (!
$length->isValid()) {
            return 
false;
        }

        
$n $length->getN();
        
$unit $length->getUnit();

        if (
$n === '0' || $unit === false) {
            return new 
HTMLPurifier_Length('0'false);
        }

        
$state $dest_state false;
        foreach (
self::$units as $k => $x) {
            if (isset(
$x[$unit])) {
                
$state $k;
            }
            if (isset(
$x[$to_unit])) {
                
$dest_state $k;
            }
        }
        if (!
$state || !$dest_state) {
            return 
false;
        }

        
// Some calculations about the initial precision of the number;
        // this will be useful when we need to do final rounding.
        
$sigfigs $this->getSigFigs($n);
        if (
$sigfigs $this->outputPrecision) {
            
$sigfigs $this->outputPrecision;
        }

        
// BCMath's internal precision deals only with decimals. Use
        // our default if the initial number has no decimals, or increase
        // it by how ever many decimals, thus, the number of guard digits
        // will always be greater than or equal to internalPrecision.
        
$log = (int)floor(log(abs($n), 10));
        
$cp = ($log 0) ? $this->internalPrecision $log $this->internalPrecision// internal precision

        
for ($i 0$i 2$i++) {

            
// Determine what unit IN THIS SYSTEM we need to convert to
            
if ($dest_state === $state) {
                
// Simple conversion
                
$dest_unit $to_unit;
            } else {
                
// Convert to the smallest unit, pending a system shift
                
$dest_unit self::$units[$state][$dest_state][0];
            }

            
// Do the conversion if necessary
            
if ($dest_unit !== $unit) {
                
$factor $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
                
$n $this->mul($n$factor$cp);
                
$unit $dest_unit;
            }

            
// Output was zero, so bail out early. Shouldn't ever happen.
            
if ($n === '') {
                
$n '0';
                
$unit $to_unit;
                break;
            }

            
// It was a simple conversion, so bail out
            
if ($dest_state === $state) {
                break;
            }

            if (
$i !== 0) {
                
// Conversion failed! Apparently, the system we forwarded
                // to didn't have this unit. This should never happen!
                
return false;
            }

            
// Pre-condition: $i == 0

            // Perform conversion to next system of units
            
$n $this->mul($nself::$units[$state][$dest_state][1], $cp);
            
$unit self::$units[$state][$dest_state][2];
            
$state $dest_state;

            
// One more loop around to convert the unit in the new system.

        
}

        
// Post-condition: $unit == $to_unit
        
if ($unit !== $to_unit) {
            return 
false;
        }

        
// Useful for debugging:
        //echo "<pre>n";
        //echo "$nnsigfigs = $sigfigsnnew_log = $new_lognlog = $lognrp = $rpn</pre>n";

        
$n $this->round($n$sigfigs);
        if (
strpos($n'.') !== false) {
            
$n rtrim($n'0');
        }
        
$n rtrim($n'.');

        return new 
HTMLPurifier_Length($n$unit);
    }

    
/**
     * Returns the number of significant figures in a string number.
     * @param string $n Decimal number
     * @return int number of sigfigs
     */
    
public function getSigFigs($n)
    {
        
$n ltrim($n'0+-');
        
$dp strpos($n'.'); // decimal position
        
if ($dp === false) {
            
$sigfigs strlen(rtrim($n'0'));
        } else {
            
$sigfigs strlen(ltrim($n'0.')); // eliminate extra decimal character
            
if ($dp !== 0) {
                
$sigfigs--;
            }
        }
        return 
$sigfigs;
    }

    
/**
     * Adds two numbers, using arbitrary precision when available.
     * @param string $s1
     * @param string $s2
     * @param int $scale
     * @return string
     */
    
private function add($s1$s2$scale)
    {
        if (
$this->bcmath) {
            return 
bcadd($s1$s2$scale);
        } else {
            return 
$this->scale((float)$s1 + (float)$s2$scale);
        }
    }

    
/**
     * Multiples two numbers, using arbitrary precision when available.
     * @param string $s1
     * @param string $s2
     * @param int $scale
     * @return string
     */
    
private function mul($s1$s2$scale)
    {
        if (
$this->bcmath) {
            return 
bcmul($s1$s2$scale);
        } else {
            return 
$this->scale((float)$s1 * (float)$s2$scale);
        }
    }

    
/**
     * Divides two numbers, using arbitrary precision when available.
     * @param string $s1
     * @param string $s2
     * @param int $scale
     * @return string
     */
    
private function div($s1$s2$scale)
    {
        if (
$this->bcmath) {
            return 
bcdiv($s1$s2$scale);
        } else {
            return 
$this->scale((float)$s1 / (float)$s2$scale);
        }
    }

    
/**
     * Rounds a number according to the number of sigfigs it should have,
     * using arbitrary precision when available.
     * @param float $n
     * @param int $sigfigs
     * @return string
     */
    
private function round($n$sigfigs)
    {
        
$new_log = (int)floor(log(abs($n), 10)); // Number of digits left of decimal - 1
        
$rp $sigfigs $new_log 1// Number of decimal places needed
        
$neg $n '-' ''// Negative sign
        
if ($this->bcmath) {
            if (
$rp >= 0) {
                
$n bcadd($n$neg '0.' str_repeat('0'$rp) . '5'$rp 1);
                
$n bcdiv($n'1'$rp);
            } else {
                
// This algorithm partially depends on the standardized
                // form of numbers that comes out of bcmath.
                
$n bcadd($n$neg '5' str_repeat('0'$new_log $sigfigs), 0);
                
$n substr($n0$sigfigs strlen($neg)) . str_repeat('0'$new_log $sigfigs 1);
            }
            return 
$n;
        } else {
            return 
$this->scale(round($n$sigfigs $new_log 1), $rp 1);
        }
    }

    
/**
     * Scales a float to $scale digits right of decimal point, like BCMath.
     * @param float $r
     * @param int $scale
     * @return string
     */
    
private function scale($r$scale)
    {
        if (
$scale 0) {
            
// The f sprintf type doesn't support negative numbers, so we
            // need to cludge things manually. First get the string.
            
$r sprintf('%.0f', (float)$r);
            
// Due to floating point precision loss, $r will more than likely
            // look something like 4652999999999.9234. We grab one more digit
            // than we need to precise from $r and then use that to round
            // appropriately.
            
$precise = (string)round(substr($r0strlen($r) + $scale), -1);
            
// Now we return it, truncating the zero that was rounded off.
            
return substr($precise0, -1) . str_repeat('0', -$scale 1);
        }
        return 
sprintf('%.' $scale 'f', (float)$r);
    }
}





/**
 * Parses string representations into their corresponding native PHP
 * variable type. The base implementation does a simple type-check.
 */
class HTMLPurifier_VarParser
{

    const 
STRING 1;
    const 
ISTRING 2;
    const 
TEXT 3;
    const 
ITEXT 4;
    const 
INT 5;
    const 
FLOAT 6;
    const 
BOOL 7;
    const 
LOOKUP 8;
    const 
ALIST 9;
    const 
HASH 10;
    const 
MIXED 11;

    
/**
     * Lookup table of allowed types. Mainly for backwards compatibility, but
     * also convenient for transforming string type names to the integer constants.
     */
    
public static $types = array(
        
'string' => self::STRING,
        
'istring' => self::ISTRING,
        
'text' => self::TEXT,
        
'itext' => self::ITEXT,
        
'int' => self::INT,
        
'float' => self::FLOAT,
        
'bool' => self::BOOL,
        
'lookup' => self::LOOKUP,
        
'list' => self::ALIST,
        
'hash' => self::HASH,
        
'mixed' => self::MIXED
    
);

    
/**
     * Lookup table of types that are string, and can have aliases or
     * allowed value lists.
     */
    
public static $stringTypes = array(
        
self::STRING => true,
        
self::ISTRING => true,
        
self::TEXT => true,
        
self::ITEXT => true,
    );

    
/**
     * Validate a variable according to type.
     * It may return NULL as a valid type if $allow_null is true.
     *
     * @param mixed $var Variable to validate
     * @param int $type Type of variable, see HTMLPurifier_VarParser->types
     * @param bool $allow_null Whether or not to permit null as a value
     * @return string Validated and type-coerced variable
     * @throws HTMLPurifier_VarParserException
     */
    
final public function parse($var$type$allow_null false)
    {
        if (
is_string($type)) {
            if (!isset(
HTMLPurifier_VarParser::$types[$type])) {
                throw new 
HTMLPurifier_VarParserException("Invalid type '$type'");
            } else {
                
$type HTMLPurifier_VarParser::$types[$type];
            }
        }
        
$var $this->parseImplementation($var$type$allow_null);
        if (
$allow_null && $var === null) {
            return 
null;
        }
        
// These are basic checks, to make sure nothing horribly wrong
        // happened in our implementations.
        
switch ($type) {
            case (
self::STRING):
            case (
self::ISTRING):
            case (
self::TEXT):
            case (
self::ITEXT):
                if (!
is_string($var)) {
                    break;
                }
                if (
$type == self::ISTRING || $type == self::ITEXT) {
                    
$var strtolower($var);
                }
                return 
$var;
            case (
self::INT):
                if (!
is_int($var)) {
                    break;
                }
                return 
$var;
            case (
self::FLOAT):
                if (!
is_float($var)) {
                    break;
                }
                return 
$var;
            case (
self::BOOL):
                if (!
is_bool($var)) {
                    break;
                }
                return 
$var;
            case (
self::LOOKUP):
            case (
self::ALIST):
            case (
self::HASH):
                if (!
is_array($var)) {
                    break;
                }
                if (
$type === self::LOOKUP) {
                    foreach (
$var as $k) {
                        if (
$k !== true) {
                            
$this->error('Lookup table contains value other than true');
                        }
                    }
                } elseif (
$type === self::ALIST) {
                    
$keys array_keys($var);
                    if (
array_keys($keys) !== $keys) {
                        
$this->error('Indices for list are not uniform');
                    }
                }
                return 
$var;
            case (
self::MIXED):
                return 
$var;
            default:
                
$this->errorInconsistent(get_class($this), $type);
        }
        
$this->errorGeneric($var$type);
    }

    
/**
     * Actually implements the parsing. Base implementation does not
     * do anything to $var. Subclasses should overload this!
     * @param mixed $var
     * @param int $type
     * @param bool $allow_null
     * @return string
     */
    
protected function parseImplementation($var$type$allow_null)
    {
        return 
$var;
    }

    
/**
     * Throws an exception.
     * @throws HTMLPurifier_VarParserException
     */
    
protected function error($msg)
    {
        throw new 
HTMLPurifier_VarParserException($msg);
    }

    
/**
     * Throws an inconsistency exception.
     * @note This should not ever be called. It would be called if we
     *       extend the allowed values of HTMLPurifier_VarParser without
     *       updating subclasses.
     * @param string $class
     * @param int $type
     * @throws HTMLPurifier_Exception
     */
    
protected function errorInconsistent($class$type)
    {
        throw new 
HTMLPurifier_Exception(
            
"Inconsistency in $class: " HTMLPurifier_VarParser::getTypeName($type) .
            
" not implemented"
        
);
    }

    
/**
     * Generic error for if a type didn't work.
     * @param mixed $var
     * @param int $type
     */
    
protected function errorGeneric($var$type)
    {
        
$vtype gettype($var);
        
$this->error("Expected type " HTMLPurifier_VarParser::getTypeName($type) . ", got $vtype");
    }

    
/**
     * @param int $type
     * @return string
     */
    
public static function getTypeName($type)
    {
        static 
$lookup;
        if (!
$lookup) {
            
// Lazy load the alternative lookup table
            
$lookup array_flip(HTMLPurifier_VarParser::$types);
        }
        if (!isset(
$lookup[$type])) {
            return 
'unknown';
        }
        return 
$lookup[$type];
    }
}





/**
 * Exception type for HTMLPurifier_VarParser
 */
class HTMLPurifier_VarParserException extends HTMLPurifier_Exception
{

}





/**
 * A zipper is a purely-functional data structure which contains
 * a focus that can be efficiently manipulated.  It is known as
 * a "one-hole context".  This mutable variant implements a zipper
 * for a list as a pair of two arrays, laid out as follows:
 *
 *      Base list: 1 2 3 4 [ ] 6 7 8 9
 *      Front list: 1 2 3 4
 *      Back list: 9 8 7 6
 *
 * User is expected to keep track of the "current element" and properly
 * fill it back in as necessary.  (ToDo: Maybe it's more user friendly
 * to implicitly track the current element?)
 *
 * Nota bene: the current class gets confused if you try to store NULLs
 * in the list.
 */

class HTMLPurifier_Zipper
{
    public 
$front$back;

    public function 
__construct($front$back) {
        
$this->front $front;
        
$this->back $back;
    }

    
/**
     * Creates a zipper from an array, with a hole in the
     * 0-index position.
     * @param Array to zipper-ify.
     * @return Tuple of zipper and element of first position.
     */
    
static public function fromArray($array) {
        
$z = new self(array(), array_reverse($array));
        
$t $z->delete(); // delete the "dummy hole"
        
return array($z$t);
    }

    
/**
     * Convert zipper back into a normal array, optionally filling in
     * the hole with a value. (Usually you should supply a $t, unless you
     * are at the end of the array.)
     */
    
public function toArray($t NULL) {
        
$a $this->front;
        if (
$t !== NULL$a[] = $t;
        for (
$i count($this->back)-1$i >= 0$i--) {
            
$a[] = $this->back[$i];
        }
        return 
$a;
    }

    
/**
     * Move hole to the next element.
     * @param $t Element to fill hole with
     * @return Original contents of new hole.
     */
    
public function next($t) {
        if (
$t !== NULLarray_push($this->front$t);
        return empty(
$this->back) ? NULL array_pop($this->back);
    }

    
/**
     * Iterated hole advancement.
     * @param $t Element to fill hole with
     * @param $i How many forward to advance hole
     * @return Original contents of new hole, i away
     */
    
public function advance($t$n) {
        for (
$i 0$i $n$i++) {
            
$t $this->next($t);
        }
        return 
$t;
    }

    
/**
     * Move hole to the previous element
     * @param $t Element to fill hole with
     * @return Original contents of new hole.
     */
    
public function prev($t) {
        if (
$t !== NULLarray_push($this->back$t);
        return empty(
$this->front) ? NULL array_pop($this->front);
    }

    
/**
     * Delete contents of current hole, shifting hole to
     * next element.
     * @return Original contents of new hole.
     */
    
public function delete() {
        return empty(
$this->back) ? NULL array_pop($this->back);
    }

    
/**
     * Returns true if we are at the end of the list.
     * @return bool
     */
    
public function done() {
        return empty(
$this->back);
    }

    
/**
     * Insert element before hole.
     * @param Element to insert
     */
    
public function insertBefore($t) {
        if (
$t !== NULLarray_push($this->front$t);
    }

    
/**
     * Insert element after hole.
     * @param Element to insert
     */
    
public function insertAfter($t) {
        if (
$t !== NULLarray_push($this->back$t);
    }

    
/**
     * Splice in multiple elements at hole.  Functional specification
     * in terms of array_splice:
     *
     *      $arr1 = $arr;
     *      $old1 = array_splice($arr1, $i, $delete, $replacement);
     *
     *      list($z, $t) = HTMLPurifier_Zipper::fromArray($arr);
     *      $t = $z->advance($t, $i);
     *      list($old2, $t) = $z->splice($t, $delete, $replacement);
     *      $arr2 = $z->toArray($t);
     *
     *      assert($old1 === $old2);
     *      assert($arr1 === $arr2);
     *
     * NB: the absolute index location after this operation is
     * *unchanged!*
     *
     * @param Current contents of hole.
     */
    
public function splice($t$delete$replacement) {
        
// delete
        
$old = array();
        
$r $t;
        for (
$i $delete$i 0$i--) {
            
$old[] = $r;
            
$r $this->delete();
        }
        
// insert
        
for ($i count($replacement)-1$i >= 0$i--) {
            
$this->insertAfter($r);
            
$r $replacement[$i];
        }
        return array(
$old$r);
    }
}



/**
 * Validates the HTML attribute style, otherwise known as CSS.
 * @note We don't implement the whole CSS specification, so it might be
 *       difficult to reuse this component in the context of validating
 *       actual stylesheet declarations.
 * @note If we were really serious about validating the CSS, we would
 *       tokenize the styles and then parse the tokens. Obviously, we
 *       are not doing that. Doing that could seriously harm performance,
 *       but would make these components a lot more viable for a CSS
 *       filtering solution.
 */
class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef
{

    
/**
     * @param string $css
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($css$config$context)
    {
        
$css $this->parseCDATA($css);

        
$definition $config->getCSSDefinition();
        
$allow_duplicates $config->get("CSS.AllowDuplicates");


        
// According to the CSS2.1 spec, the places where a
        // non-delimiting semicolon can appear are in strings
        // escape sequences.   So here is some dumb hack to
        // handle quotes.
        
$len strlen($css);
        
$accum "";
        
$declarations = array();
        
$quoted false;
        for (
$i 0$i $len$i++) {
            
$c strcspn($css";'"", $i);
            
$accum .= substr($css$i$c);
            
$i += $c;
            if (
$i == $len) break;
            
$d = $css[$i];
            if (
$quoted) {
                
$accum .= $d;
                if (
$d == $quoted) {
                    
$quoted = false;
                }
            } else {
                if (
$d == ";") {
                    
$declarations[] = $accum;
                    
$accum = "";
                } else {
                    
$accum .= $d;
                    
$quoted = $d;
                }
            }
        }
        if (
$accum != "") $declarations[] = $accum;

        
$propvalues = array();
        
$new_declarations = '';

        /**
         * Name of the current CSS property being validated.
         */
        
$property = false;
        
$context->register('CurrentCSSProperty', $property);

        foreach (
$declarations as $declaration) {
            if (!
$declaration) {
                continue;
            }
            if (!strpos(
$declaration, ':')) {
                continue;
            }
            list(
$property$value) = explode(':', $declaration, 2);
            
$property = trim($property);
            
$value = trim($value);
            
$ok = false;
            do {
                if (isset(
$definition->info[$property])) {
                    
$ok = true;
                    break;
                }
                if (ctype_lower(
$property)) {
                    break;
                }
                
$property = strtolower($property);
                if (isset(
$definition->info[$property])) {
                    
$ok = true;
                    break;
                }
            } while (0);
            if (!
$ok) {
                continue;
            }
            // inefficient call, since the validator will do this again
            if (strtolower(trim(
$value)) !== 'inherit') {
                // inherit works for everything (but only on the base property)
                
$result = $definition->info[$property]->validate(
                    
$value,
                    
$config,
                    
$context
                );
            } else {
                
$result = 'inherit';
            }
            if (
$result === false) {
                continue;
            }
            if (
$allow_duplicates) {
                
$new_declarations .= "$property:$result;";
            } else {
                
$propvalues[$property] = $result;
            }
        }

        
$context->destroy('CurrentCSSProperty');

        // procedure does not write the new CSS simultaneously, so it's
        // slightly inefficient, but it's the only way of getting rid of
        // duplicates. Perhaps config to optimize it, but not now.

        foreach (
$propvalues as $prop => $value) {
            
$new_declarations .= "$prop:$value;";
        }

        return 
$new_declarations ? $new_declarations : false;

    }

}





/**
 * Dummy AttrDef that mimics another AttrDef, BUT it generates clones
 * with make.
 */
class HTMLPurifier_AttrDef_Clone extends HTMLPurifier_AttrDef
{
    /**
     * What we're cloning.
     * @type HTMLPurifier_AttrDef
     */
    protected 
$clone;

    /**
     * @param HTMLPurifier_AttrDef 
$clone
     */
    public function __construct(
$clone)
    {
        
$this->clone = $clone;
    }

    /**
     * @param string 
$v
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$v$config$context)
    {
        return 
$this->clone->validate($v$config$context);
    }

    /**
     * @param string 
$string
     * @return HTMLPurifier_AttrDef
     */
    public function make(
$string)
    {
        return clone 
$this->clone;
    }
}





// Enum = Enumerated
/**
 * Validates a keyword against a list of valid values.
 * @warning The case-insensitive compare of this function uses PHP's
 *          built-in strtolower and ctype_lower functions, which may
 *          cause problems with international comparisons
 */
class HTMLPurifier_AttrDef_Enum extends HTMLPurifier_AttrDef
{

    /**
     * Lookup table of valid values.
     * @type array
     * @todo Make protected
     */
    public 
$valid_values = array();

    /**
     * Bool indicating whether or not enumeration is case sensitive.
     * @note In general this is always case insensitive.
     */
    protected 
$case_sensitive = false; // values according to W3C spec

    /**
     * @param array 
$valid_values List of valid values
     * @param bool 
$case_sensitive Whether or not case sensitive
     */
    public function __construct(
$valid_values = array(), $case_sensitive = false)
    {
        
$this->valid_values = array_flip($valid_values);
        
$this->case_sensitive = $case_sensitive;
    }

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        
$string = trim($string);
        if (!
$this->case_sensitive) {
            // we may want to do full case-insensitive libraries
            
$string = ctype_lower($string) ? $string : strtolower($string);
        }
        
$result = isset($this->valid_values[$string]);

        return 
$result ? $string : false;
    }

    /**
     * @param string 
$string In form of comma-delimited list of case-insensitive
     *      valid values. Example: "
foo,bar,baz". Prepend "s:" to make
     *      case sensitive
     * @return HTMLPurifier_AttrDef_Enum
     */
    public function make(
$string)
    {
        if (strlen(
$string) > 2 && $string[0] == 's' && $string[1] == ':') {
            
$string = substr($string, 2);
            
$sensitive = true;
        } else {
            
$sensitive = false;
        }
        
$values = explode(',', $string);
        return new HTMLPurifier_AttrDef_Enum(
$values$sensitive);
    }
}





/**
 * Validates an integer.
 * @note While this class was modeled off the CSS definition, no currently
 *       allowed CSS uses this type.  The properties that do are: widows,
 *       orphans, z-index, counter-increment, counter-reset.  Some of the
 *       HTML attributes, however, find use for a non-negative version of this.
 */
class HTMLPurifier_AttrDef_Integer extends HTMLPurifier_AttrDef
{

    /**
     * Whether or not negative values are allowed.
     * @type bool
     */
    protected 
$negative = true;

    /**
     * Whether or not zero is allowed.
     * @type bool
     */
    protected 
$zero = true;

    /**
     * Whether or not positive values are allowed.
     * @type bool
     */
    protected 
$positive = true;

    /**
     * @param 
$negative Bool indicating whether or not negative values are allowed
     * @param 
$zero Bool indicating whether or not zero is allowed
     * @param 
$positive Bool indicating whether or not positive values are allowed
     */
    public function __construct(
$negative = true, $zero = true, $positive = true)
    {
        
$this->negative = $negative;
        
$this->zero = $zero;
        
$this->positive = $positive;
    }

    /**
     * @param string 
$integer
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$integer$config$context)
    {
        
$integer = $this->parseCDATA($integer);
        if (
$integer === '') {
            return false;
        }

        // we could possibly simply typecast it to integer, but there are
        // certain fringe cases that must not return an integer.

        // clip leading sign
        if (
$this->negative && $integer[0] === '-') {
            
$digits = substr($integer, 1);
            if (
$digits === '0') {
                
$integer = '0';
            } // rm minus sign for zero
        } elseif (
$this->positive && $integer[0] === '+') {
            
$digits = $integer = substr($integer, 1); // rm unnecessary plus
        } else {
            
$digits = $integer;
        }

        // test if it's numeric
        if (!ctype_digit(
$digits)) {
            return false;
        }

        // perform scope tests
        if (!
$this->zero && $integer == 0) {
            return false;
        }
        if (!
$this->positive && $integer > 0) {
            return false;
        }
        if (!
$this->negative && $integer < 0) {
            return false;
        }

        return 
$integer;
    }
}





/**
 * Validates the HTML attribute lang, effectively a language code.
 * @note Built according to RFC 3066, which obsoleted RFC 1766
 */
class HTMLPurifier_AttrDef_Lang extends HTMLPurifier_AttrDef
{

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        
$string = trim($string);
        if (!
$string) {
            return false;
        }

        
$subtags = explode('-', $string);
        
$num_subtags = count($subtags);

        if (
$num_subtags == 0) { // sanity check
            return false;
        }

        // process primary subtag : 
$subtags[0]
        
$length = strlen($subtags[0]);
        switch (
$length) {
            case 0:
                return false;
            case 1:
                if (!(
$subtags[0] == 'x' || $subtags[0] == 'i')) {
                    return false;
                }
                break;
            case 2:
            case 3:
                if (!ctype_alpha(
$subtags[0])) {
                    return false;
                } elseif (!ctype_lower(
$subtags[0])) {
                    
$subtags[0] = strtolower($subtags[0]);
                }
                break;
            default:
                return false;
        }

        
$new_string = $subtags[0];
        if (
$num_subtags == 1) {
            return 
$new_string;
        }

        // process second subtag : 
$subtags[1]
        
$length = strlen($subtags[1]);
        if (
$length == 0 || ($length == 1 && $subtags[1] != 'x') || $length > 8 || !ctype_alnum($subtags[1])) {
            return 
$new_string;
        }
        if (!ctype_lower(
$subtags[1])) {
            
$subtags[1] = strtolower($subtags[1]);
        }

        
$new_string .= '-' . $subtags[1];
        if (
$num_subtags == 2) {
            return 
$new_string;
        }

        // process all other subtags, index 2 and up
        for (
$i = 2; $i < $num_subtags$i++) {
            
$length = strlen($subtags[$i]);
            if (
$length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) {
                return 
$new_string;
            }
            if (!ctype_lower(
$subtags[$i])) {
                
$subtags[$i] = strtolower($subtags[$i]);
            }
            
$new_string .= '-' . $subtags[$i];
        }
        return 
$new_string;
    }
}





/**
 * Decorator that, depending on a token, switches between two definitions.
 */
class HTMLPurifier_AttrDef_Switch
{

    /**
     * @type string
     */
    protected 
$tag;

    /**
     * @type HTMLPurifier_AttrDef
     */
    protected 
$withTag;

    /**
     * @type HTMLPurifier_AttrDef
     */
    protected 
$withoutTag;

    /**
     * @param string 
$tag Tag name to switch upon
     * @param HTMLPurifier_AttrDef 
$with_tag Call if token matches tag
     * @param HTMLPurifier_AttrDef 
$without_tag Call if token doesn't match, or there is no token
     */
    public function __construct(
$tag$with_tag$without_tag)
    {
        
$this->tag = $tag;
        
$this->withTag = $with_tag;
        
$this->withoutTag = $without_tag;
    }

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        
$token = $context->get('CurrentToken', true);
        if (!
$token || $token->name !== $this->tag) {
            return 
$this->withoutTag->validate($string$config$context);
        } else {
            return 
$this->withTag->validate($string$config$context);
        }
    }
}





/**
 * Validates arbitrary text according to the HTML spec.
 */
class HTMLPurifier_AttrDef_Text extends HTMLPurifier_AttrDef
{

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        return 
$this->parseCDATA($string);
    }
}





/**
 * Validates a URI as defined by RFC 3986.
 * @note Scheme-specific mechanics deferred to HTMLPurifier_URIScheme
 */
class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef
{

    /**
     * @type HTMLPurifier_URIParser
     */
    protected 
$parser;

    /**
     * @type bool
     */
    protected 
$embedsResource;

    /**
     * @param bool 
$embeds_resource Does the URI here result in an extra HTTP request?
     */
    public function __construct(
$embeds_resource = false)
    {
        
$this->parser = new HTMLPurifier_URIParser();
        
$this->embedsResource = (bool)$embeds_resource;
    }

    /**
     * @param string 
$string
     * @return HTMLPurifier_AttrDef_URI
     */
    public function make(
$string)
    {
        
$embeds = ($string === 'embedded');
        return new HTMLPurifier_AttrDef_URI(
$embeds);
    }

    /**
     * @param string 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$uri$config$context)
    {
        if (
$config->get('URI.Disable')) {
            return false;
        }

        
$uri = $this->parseCDATA($uri);

        // parse the URI
        
$uri = $this->parser->parse($uri);
        if (
$uri === false) {
            return false;
        }

        // add embedded flag to context for validators
        
$context->register('EmbeddedURI', $this->embedsResource);

        
$ok = false;
        do {

            // generic validation
            
$result = $uri->validate($config$context);
            if (!
$result) {
                break;
            }

            // chained filtering
            
$uri_def = $config->getDefinition('URI');
            
$result = $uri_def->filter($uri$config$context);
            if (!
$result) {
                break;
            }

            // scheme-specific validation
            
$scheme_obj = $uri->getSchemeObj($config$context);
            if (!
$scheme_obj) {
                break;
            }
            if (
$this->embedsResource && !$scheme_obj->browsable) {
                break;
            }
            
$result = $scheme_obj->validate($uri$config$context);
            if (!
$result) {
                break;
            }

            // Post chained filtering
            
$result = $uri_def->postFilter($uri$config$context);
            if (!
$result) {
                break;
            }

            // survived gauntlet
            
$ok = true;

        } while (false);

        
$context->destroy('EmbeddedURI');
        if (!
$ok) {
            return false;
        }
        // back to string
        return 
$uri->toString();
    }
}





/**
 * Validates a number as defined by the CSS spec.
 */
class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef
{

    /**
     * Indicates whether or not only positive values are allowed.
     * @type bool
     */
    protected 
$non_negative = false;

    /**
     * @param bool 
$non_negative indicates whether negatives are forbidden
     */
    public function __construct(
$non_negative = false)
    {
        
$this->non_negative = $non_negative;
    }

    /**
     * @param string 
$number
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return string|bool
     * @warning Some contexts do not pass 
$config$context. These
     *          variables should not be used without checking HTMLPurifier_Length
     */
    public function validate(
$number$config$context)
    {
        
$number = $this->parseCDATA($number);

        if (
$number === '') {
            return false;
        }
        if (
$number === '0') {
            return '0';
        }

        
$sign = '';
        switch (
$number[0]) {
            case '-':
                if (
$this->non_negative) {
                    return false;
                }
                
$sign = '-';
            case '+':
                
$number = substr($number, 1);
        }

        if (ctype_digit(
$number)) {
            
$number = ltrim($number, '0');
            return 
$number ? $sign . $number : '0';
        }

        // Period is the only non-numeric character allowed
        if (strpos(
$number, '.') === false) {
            return false;
        }

        list(
$left$right) = explode('.', $number, 2);

        if (
$left === '' && $right === '') {
            return false;
        }
        if (
$left !== '' && !ctype_digit($left)) {
            return false;
        }

        
$left = ltrim($left, '0');
        
$right = rtrim($right, '0');

        if (
$right === '') {
            return 
$left ? $sign . $left : '0';
        } elseif (!ctype_digit(
$right)) {
            return false;
        }
        return 
$sign . $left . '.' . $right;
    }
}





class HTMLPurifier_AttrDef_CSS_AlphaValue extends HTMLPurifier_AttrDef_CSS_Number
{

    public function __construct()
    {
        parent::__construct(false); // opacity is non-negative, but we will clamp it
    }

    /**
     * @param string 
$number
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return string
     */
    public function validate(
$number$config$context)
    {
        
$result = parent::validate($number$config$context);
        if (
$result === false) {
            return 
$result;
        }
        
$float = (float)$result;
        if (
$float < 0.0) {
            
$result = '0';
        }
        if (
$float > 1.0) {
            
$result = '1';
        }
        return 
$result;
    }
}





/**
 * Validates shorthand CSS property background.
 * @warning Does not support url tokens that have internal spaces.
 */
class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef
{

    /**
     * Local copy of component validators.
     * @type HTMLPurifier_AttrDef[]
     * @note See HTMLPurifier_AttrDef_Font::
$info for a similar impl.
     */
    protected 
$info;

    /**
     * @param HTMLPurifier_Config 
$config
     */
    public function __construct(
$config)
    {
        
$def = $config->getCSSDefinition();
        
$this->info['background-color'] = $def->info['background-color'];
        
$this->info['background-image'] = $def->info['background-image'];
        
$this->info['background-repeat'] = $def->info['background-repeat'];
        
$this->info['background-attachment'] = $def->info['background-attachment'];
        
$this->info['background-position'] = $def->info['background-position'];
    }

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        // regular pre-processing
        
$string = $this->parseCDATA($string);
        if (
$string === '') {
            return false;
        }

        // munge rgb() decl if necessary
        
$string = $this->mungeRgb($string);

        // assumes URI doesn't have spaces in it
        
$bits = explode(' ', $string); // bits to process

        
$caught = array();
        
$caught['color'] = false;
        
$caught['image'] = false;
        
$caught['repeat'] = false;
        
$caught['attachment'] = false;
        
$caught['position'] = false;

        
$i = 0; // number of catches

        foreach (
$bits as $bit) {
            if (
$bit === '') {
                continue;
            }
            foreach (
$caught as $key => $status) {
                if (
$key != 'position') {
                    if (
$status !== false) {
                        continue;
                    }
                    
$r = $this->info['background-' . $key]->validate($bit$config$context);
                } else {
                    
$r = $bit;
                }
                if (
$r === false) {
                    continue;
                }
                if (
$key == 'position') {
                    if (
$caught[$key] === false) {
                        
$caught[$key] = '';
                    }
                    
$caught[$key] .= $r . ' ';
                } else {
                    
$caught[$key] = $r;
                }
                
$i++;
                break;
            }
        }

        if (!
$i) {
            return false;
        }
        if (
$caught['position'] !== false) {
            
$caught['position'] = $this->info['background-position']->
                validate(
$caught['position'], $config$context);
        }

        
$ret = array();
        foreach (
$caught as $value) {
            if (
$value === false) {
                continue;
            }
            
$ret[] = $value;
        }

        if (empty(
$ret)) {
            return false;
        }
        return implode(' ', 
$ret);
    }
}





/* W3C says:
    [ // adjective and number must be in correct order, even if
      // you could switch them without introducing ambiguity.
      // some browsers support that syntax
        [
            <percentage> | <length> | left | center | right
        ]
        [
            <percentage> | <length> | top | center | bottom
        ]?
    ] |
    [ // this signifies that the vertical and horizontal adjectives
      // can be arbitrarily ordered, however, there can only be two,
      // one of each, or none at all
        [
            left | center | right
        ] ||
        [
            top | center | bottom
        ]
    ]
    top, left = 0%
    center, (none) = 50%
    bottom, right = 100%
*/

/* QuirksMode says:
    keyword + length/percentage must be ordered correctly, as per W3C

    Internet Explorer and Opera, however, support arbitrary ordering. We
    should fix it up.

    Minor issue though, not strictly necessary.
*/

// control freaks may appreciate the ability to convert these to
// percentages or something, but it's not necessary

/**
 * Validates the value of background-position.
 */
class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef
{

    /**
     * @type HTMLPurifier_AttrDef_CSS_Length
     */
    protected 
$length;

    /**
     * @type HTMLPurifier_AttrDef_CSS_Percentage
     */
    protected 
$percentage;

    public function __construct()
    {
        
$this->length = new HTMLPurifier_AttrDef_CSS_Length();
        
$this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage();
    }

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        
$string = $this->parseCDATA($string);
        
$bits = explode(' ', $string);

        
$keywords = array();
        
$keywords['h'] = false; // left, right
        
$keywords['v'] = false; // top, bottom
        
$keywords['ch'] = false; // center (first word)
        
$keywords['cv'] = false; // center (second word)
        
$measures = array();

        
$i = 0;

        
$lookup = array(
            'top' => 'v',
            'bottom' => 'v',
            'left' => 'h',
            'right' => 'h',
            'center' => 'c'
        );

        foreach (
$bits as $bit) {
            if (
$bit === '') {
                continue;
            }

            // test for keyword
            
$lbit = ctype_lower($bit) ? $bit : strtolower($bit);
            if (isset(
$lookup[$lbit])) {
                
$status = $lookup[$lbit];
                if (
$status == 'c') {
                    if (
$i == 0) {
                        
$status = 'ch';
                    } else {
                        
$status = 'cv';
                    }
                }
                
$keywords[$status] = $lbit;
                
$i++;
            }

            // test for length
            
$r = $this->length->validate($bit$config$context);
            if (
$r !== false) {
                
$measures[] = $r;
                
$i++;
            }

            // test for percentage
            
$r = $this->percentage->validate($bit$config$context);
            if (
$r !== false) {
                
$measures[] = $r;
                
$i++;
            }
        }

        if (!
$i) {
            return false;
        } // no valid values were caught

        
$ret = array();

        // first keyword
        if (
$keywords['h']) {
            
$ret[] = $keywords['h'];
        } elseif (
$keywords['ch']) {
            
$ret[] = $keywords['ch'];
            
$keywords['cv'] = false; // prevent re-use: center = center center
        } elseif (count(
$measures)) {
            
$ret[] = array_shift($measures);
        }

        if (
$keywords['v']) {
            
$ret[] = $keywords['v'];
        } elseif (
$keywords['cv']) {
            
$ret[] = $keywords['cv'];
        } elseif (count(
$measures)) {
            
$ret[] = array_shift($measures);
        }

        if (empty(
$ret)) {
            return false;
        }
        return implode(' ', 
$ret);
    }
}





/**
 * Validates the border property as defined by CSS.
 */
class HTMLPurifier_AttrDef_CSS_Border extends HTMLPurifier_AttrDef
{

    /**
     * Local copy of properties this property is shorthand for.
     * @type HTMLPurifier_AttrDef[]
     */
    protected 
$info = array();

    /**
     * @param HTMLPurifier_Config 
$config
     */
    public function __construct(
$config)
    {
        
$def = $config->getCSSDefinition();
        
$this->info['border-width'] = $def->info['border-width'];
        
$this->info['border-style'] = $def->info['border-style'];
        
$this->info['border-top-color'] = $def->info['border-top-color'];
    }

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        
$string = $this->parseCDATA($string);
        
$string = $this->mungeRgb($string);
        
$bits = explode(' ', $string);
        
$done = array(); // segments we've finished
        
$ret = ''; // return value
        foreach (
$bits as $bit) {
            foreach (
$this->info as $propname => $validator) {
                if (isset(
$done[$propname])) {
                    continue;
                }
                
$r = $validator->validate($bit$config$context);
                if (
$r !== false) {
                    
$ret .= $r . ' ';
                    
$done[$propname] = true;
                    break;
                }
            }
        }
        return rtrim(
$ret);
    }
}





/**
 * Validates Color as defined by CSS.
 */
class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
{

    /**
     * @type HTMLPurifier_AttrDef_CSS_AlphaValue
     */
    protected 
$alpha;

    public function __construct()
    {
        
$this->alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue();
    }

    /**
     * @param string 
$color
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$color$config$context)
    {
        static 
$colors = null;
        if (
$colors === null) {
            
$colors = $config->get('Core.ColorKeywords');
        }

        
$color = trim($color);
        if (
$color === '') {
            return false;
        }

        
$lower = strtolower($color);
        if (isset(
$colors[$lower])) {
            return 
$colors[$lower];
        }

        if (preg_match('#(rgb|rgba|hsl|hsla)(#', 
$color$matches) === 1) {
            
$length = strlen($color);
            if (strpos(
$color, ')') !== $length - 1) {
                return false;
            }

            // get used function : rgb, rgba, hsl or hsla
            
$function = $matches[1];

            
$parameters_size = 3;
            
$alpha_channel = false;
            if (substr(
$function, -1) === 'a') {
                
$parameters_size = 4;
                
$alpha_channel = true;
            }

            /*
             * Allowed types for values :
             * parameter_position => [type => max_value]
             */
            
$allowed_types = array(
                1 => array('percentage' => 100, 'integer' => 255),
                2 => array('percentage' => 100, 'integer' => 255),
                3 => array('percentage' => 100, 'integer' => 255),
            );
            
$allow_different_types = false;

            if (strpos(
$function, 'hsl') !== false) {
                
$allowed_types = array(
                    1 => array('integer' => 360),
                    2 => array('percentage' => 100),
                    3 => array('percentage' => 100),
                );
                
$allow_different_types = true;
            }

            
$values = trim(str_replace($function, '', $color), ' ()');

            
$parts = explode(',', $values);
            if (count(
$parts) !== $parameters_size) {
                return false;
            }

            
$type = false;
            
$new_parts = array();
            
$i = 0;

            foreach (
$parts as $part) {
                
$i++;
                
$part = trim($part);

                if (
$part === '') {
                    return false;
                }

                // different check for alpha channel
                if (
$alpha_channel === true && $i === count($parts)) {
                    
$result = $this->alpha->validate($part$config$context);

                    if (
$result === false) {
                        return false;
                    }

                    
$new_parts[] = (string)$result;
                    continue;
                }

                if (substr(
$part, -1) === '%') {
                    
$current_type = 'percentage';
                } else {
                    
$current_type = 'integer';
                }

                if (!array_key_exists(
$current_type$allowed_types[$i])) {
                    return false;
                }

                if (!
$type) {
                    
$type = $current_type;
                }

                if (
$allow_different_types === false && $type != $current_type) {
                    return false;
                }

                
$max_value = $allowed_types[$i][$current_type];

                if (
$current_type == 'integer') {
                    // Return value between range 0 -> 
$max_value
                    
$new_parts[] = (int)max(min($part$max_value), 0);
                } elseif (
$current_type == 'percentage') {
                    
$new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%';
                }
            }

            
$new_values = implode(',', $new_parts);

            
$color = $function . '(' . $new_values . ')';
        } else {
            // hexadecimal handling
            if (
$color[0] === '#') {
                
$hex = substr($color, 1);
            } else {
                
$hex = $color;
                
$color = '#' . $color;
            }
            
$length = strlen($hex);
            if (
$length !== 3 && $length !== 6) {
                return false;
            }
            if (!ctype_xdigit(
$hex)) {
                return false;
            }
        }
        return 
$color;
    }

}





/**
 * Allows multiple validators to attempt to validate attribute.
 *
 * Composite is just what it sounds like: a composite of many validators.
 * This means that multiple HTMLPurifier_AttrDef objects will have a whack
 * at the string.  If one of them passes, that's what is returned.  This is
 * especially useful for CSS values, which often are a choice between
 * an enumerated set of predefined values or a flexible data type.
 */
class HTMLPurifier_AttrDef_CSS_Composite extends HTMLPurifier_AttrDef
{

    /**
     * List of objects that may process strings.
     * @type HTMLPurifier_AttrDef[]
     * @todo Make protected
     */
    public 
$defs;

    /**
     * @param HTMLPurifier_AttrDef[] 
$defs List of HTMLPurifier_AttrDef objects
     */
    public function __construct(
$defs)
    {
        
$this->defs = $defs;
    }

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        foreach (
$this->defs as $i => $def) {
            
$result = $this->defs[$i]->validate($string$config$context);
            if (
$result !== false) {
                return 
$result;
            }
        }
        return false;
    }
}





/**
 * Decorator which enables CSS properties to be disabled for specific elements.
 */
class HTMLPurifier_AttrDef_CSS_DenyElementDecorator extends HTMLPurifier_AttrDef
{
    /**
     * @type HTMLPurifier_AttrDef
     */
    public 
$def;
    /**
     * @type string
     */
    public 
$element;

    /**
     * @param HTMLPurifier_AttrDef 
$def Definition to wrap
     * @param string 
$element Element to deny
     */
    public function __construct(
$def$element)
    {
        
$this->def = $def;
        
$this->element = $element;
    }

    /**
     * Checks if CurrentToken is set and equal to 
$this->element
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        
$token = $context->get('CurrentToken', true);
        if (
$token && $token->name == $this->element) {
            return false;
        }
        return 
$this->def->validate($string$config$context);
    }
}





/**
 * Microsoft's proprietary filter: CSS property
 * @note Currently supports the alpha filter. In the future, this will
 *       probably need an extensible framework
 */
class HTMLPurifier_AttrDef_CSS_Filter extends HTMLPurifier_AttrDef
{
    /**
     * @type HTMLPurifier_AttrDef_Integer
     */
    protected 
$intValidator;

    public function __construct()
    {
        
$this->intValidator = new HTMLPurifier_AttrDef_Integer();
    }

    /**
     * @param string 
$value
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$value$config$context)
    {
        
$value = $this->parseCDATA($value);
        if (
$value === 'none') {
            return 
$value;
        }
        // if we looped this we could support multiple filters
        
$function_length = strcspn($value, '(');
        
$function = trim(substr($value, 0, $function_length));
        if (
$function !== 'alpha' &&
            
$function !== 'Alpha' &&
            
$function !== 'progid:DXImageTransform.Microsoft.Alpha'
        ) {
            return false;
        }
        
$cursor = $function_length + 1;
        
$parameters_length = strcspn($value, ')', $cursor);
        
$parameters = substr($value$cursor$parameters_length);
        
$params = explode(',', $parameters);
        
$ret_params = array();
        
$lookup = array();
        foreach (
$params as $param) {
            list(
$key$value) = explode('=', $param);
            
$key = trim($key);
            
$value = trim($value);
            if (isset(
$lookup[$key])) {
                continue;
            }
            if (
$key !== 'opacity') {
                continue;
            }
            
$value = $this->intValidator->validate($value$config$context);
            if (
$value === false) {
                continue;
            }
            
$int = (int)$value;
            if (
$int > 100) {
                
$value = '100';
            }
            if (
$int < 0) {
                
$value = '0';
            }
            
$ret_params[] = "$key=$value";
            
$lookup[$key] = true;
        }
        
$ret_parameters = implode(',', $ret_params);
        
$ret_function = "$function($ret_parameters)";
        return 
$ret_function;
    }
}





/**
 * Validates shorthand CSS property font.
 */
class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef
{

    /**
     * Local copy of validators
     * @type HTMLPurifier_AttrDef[]
     * @note If we moved specific CSS property definitions to their own
     *       classes instead of having them be assembled at run time by
     *       CSSDefinition, this wouldn't be necessary.  We'd instantiate
     *       our own copies.
     */
    protected 
$info = array();

    /**
     * @param HTMLPurifier_Config 
$config
     */
    public function __construct(
$config)
    {
        
$def = $config->getCSSDefinition();
        
$this->info['font-style'] = $def->info['font-style'];
        
$this->info['font-variant'] = $def->info['font-variant'];
        
$this->info['font-weight'] = $def->info['font-weight'];
        
$this->info['font-size'] = $def->info['font-size'];
        
$this->info['line-height'] = $def->info['line-height'];
        
$this->info['font-family'] = $def->info['font-family'];
    }

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        static 
$system_fonts = array(
            'caption' => true,
            'icon' => true,
            'menu' => true,
            'message-box' => true,
            'small-caption' => true,
            'status-bar' => true
        );

        // regular pre-processing
        
$string = $this->parseCDATA($string);
        if (
$string === '') {
            return false;
        }

        // check if it's one of the keywords
        
$lowercase_string = strtolower($string);
        if (isset(
$system_fonts[$lowercase_string])) {
            return 
$lowercase_string;
        }

        
$bits = explode(' ', $string); // bits to process
        
$stage = 0; // this indicates what we're looking for
        
$caught = array(); // which stage 0 properties have we caught?
        
$stage_1 = array('font-style', 'font-variant', 'font-weight');
        
$final = ''; // output

        for (
$i = 0, $size = count($bits); $i < $size$i++) {
            if (
$bits[$i] === '') {
                continue;
            }
            switch (
$stage) {
                case 0: // attempting to catch font-style, font-variant or font-weight
                    foreach (
$stage_1 as $validator_name) {
                        if (isset(
$caught[$validator_name])) {
                            continue;
                        }
                        
$r = $this->info[$validator_name]->validate(
                            
$bits[$i],
                            
$config,
                            
$context
                        );
                        if (
$r !== false) {
                            
$final .= $r . ' ';
                            
$caught[$validator_name] = true;
                            break;
                        }
                    }
                    // all three caught, continue on
                    if (count(
$caught) >= 3) {
                        
$stage = 1;
                    }
                    if (
$r !== false) {
                        break;
                    }
                case 1: // attempting to catch font-size and perhaps line-height
                    
$found_slash = false;
                    if (strpos(
$bits[$i], '/') !== false) {
                        list(
$font_size$line_height) =
                            explode('/', 
$bits[$i]);
                        if (
$line_height === '') {
                            // ooh, there's a space after the slash!
                            
$line_height = false;
                            
$found_slash = true;
                        }
                    } else {
                        
$font_size = $bits[$i];
                        
$line_height = false;
                    }
                    
$r = $this->info['font-size']->validate(
                        
$font_size,
                        
$config,
                        
$context
                    );
                    if (
$r !== false) {
                        
$final .= $r;
                        // attempt to catch line-height
                        if (
$line_height === false) {
                            // we need to scroll forward
                            for (
$j = $i + 1; $j < $size$j++) {
                                if (
$bits[$j] === '') {
                                    continue;
                                }
                                if (
$bits[$j] === '/') {
                                    if (
$found_slash) {
                                        return false;
                                    } else {
                                        
$found_slash = true;
                                        continue;
                                    }
                                }
                                
$line_height = $bits[$j];
                                break;
                            }
                        } else {
                            // slash already found
                            
$found_slash = true;
                            
$j = $i;
                        }
                        if (
$found_slash) {
                            
$i = $j;
                            
$r = $this->info['line-height']->validate(
                                
$line_height,
                                
$config,
                                
$context
                            );
                            if (
$r !== false) {
                                
$final .= '/' . $r;
                            }
                        }
                        
$final .= ' ';
                        
$stage = 2;
                        break;
                    }
                    return false;
                case 2: // attempting to catch font-family
                    
$font_family =
                        implode(' ', array_slice(
$bits$i$size - $i));
                    
$r = $this->info['font-family']->validate(
                        
$font_family,
                        
$config,
                        
$context
                    );
                    if (
$r !== false) {
                        
$final .= $r . ' ';
                        // processing completed successfully
                        return rtrim(
$final);
                    }
                    return false;
            }
        }
        return false;
    }
}





/**
 * Validates a font family list according to CSS spec
 */
class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
{

    protected 
$mask = null;

    public function __construct()
    {
        
$this->mask = '_- ';
        for (
$c = 'a'; $c <= 'z'; $c++) {
            
$this->mask .= $c;
        }
        for (
$c = 'A'; $c <= 'Z'; $c++) {
            
$this->mask .= $c;
        }
        for (
$c = '0'; $c <= '9'; $c++) {
            
$this->mask .= $c;
        } // cast-y, but should be fine
        // special bytes used by UTF-8
        for (
$i = 0x80; $i <= 0xFF; $i++) {
            // We don't bother excluding invalid bytes in this range,
            // because the our restriction of well-formed UTF-8 will
            // prevent these from ever occurring.
            
$this->mask .= chr($i);
        }

        /*
            PHP's internal strcspn implementation is
            O(length of string * length of mask), making it inefficient
            for large masks.  However, it's still faster than
            preg_match 8)
          for (p = s1;;) {
            spanp = s2;
            do {
              if (*spanp == c || p == s1_end) {
                return p - s1;
              }
            } while (spanp++ < (s2_end - 1));
            c = *++p;
          }
         */
        // possible optimization: invert the mask.
    }

    /**
     * @param string 
$string
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool|string
     */
    public function validate(
$string$config$context)
    {
        static 
$generic_names = array(
            'serif' => true,
            'sans-serif' => true,
            'monospace' => true,
            'fantasy' => true,
            'cursive' => true
        );
        
$allowed_fonts = $config->get('CSS.AllowedFonts');

        // assume that no font names contain commas in them
        
$fonts = explode(',', $string);
        
$final = '';
        foreach (
$fonts as $font) {
            
$font = trim($font);
            if (
$font === '') {
                continue;
            }
            // match a generic name
            if (isset(
$generic_names[$font])) {
                if (
$allowed_fonts === null || isset($allowed_fonts[$font])) {
                    
$final .= $font . ', ';
                }
                continue;
            }
            // match a quoted name
            if (
$font[0] === '"' || $font[0] === "'") {
                
$length = strlen($font);
                if (
$length <= 2) {
                    continue;
                }
                
$quote = $font[0];
                if (
$font[$length - 1] !== $quote) {
                    continue;
                }
                
$font = substr($font, 1, $length - 2);
            }

            
$font = $this->expandCSSEscape($font);

            // 
$font is a pure representation of the font name

            if (
$allowed_fonts !== null && !isset($allowed_fonts[$font])) {
                continue;
            }

            if (ctype_alnum(
$font) && $font !== '') {
                // very simple font, allow it in unharmed
                
$final .= $font . ', ';
                continue;
            }

            // bugger out on whitespace.  form feed (0C) really
            // shouldn't show up regardless
            
$font = str_replace(array("n", "t", "r", "x0C"), ' ', $font);

            // Here, there are various classes of characters which need
            // to be treated differently:
            //  - Alphanumeric characters are essentially safe.  We
            //    handled these above.
            //  - Spaces require quoting, though most parsers will do
            //    the right thing if there aren't any characters that
            //    can be misinterpreted
            //  - Dashes rarely occur, but they fairly unproblematic
            //    for parsing/rendering purposes.
            //  The above characters cover the majority of Western font
            //  names.
            //  - Arbitrary Unicode characters not in ASCII.  Because
            //    most parsers give little thought to Unicode, treatment
            //    of these codepoints is basically uniform, even for
            //    punctuation-like codepoints.  These characters can
            //    show up in non-Western pages and are supported by most
            //    major browsers, for example: "
MS 明朝" is a
            //    legitimate font-name
            //    <http://ja.wikipedia.org/wiki/MS_明朝>.  See
            //    the CSS3 spec for more examples:
            //    <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png>
            //    You can see live samples of these on the Internet:
            //    <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック>
            //    However, most of these fonts have ASCII equivalents:
            //    for example, 'MS Mincho', and it's considered
            //    professional to use ASCII font names instead of
            //    Unicode font names.  Thanks Takeshi Terada for
            //    providing this information.
            //  The following characters, to my knowledge, have not been
            //  used to name font names.
            //  - Single quote.  While theoretically you might find a
            //    font name that has a single quote in its name (serving
            //    as an apostrophe, e.g. Dave's Scribble), I haven't
            //    been able to find any actual examples of this.
            //    Internet Explorer's cssText translation (which I
            //    believe is invoked by innerHTML) normalizes any
            //    quoting to single quotes, and fails to escape single
            //    quotes.  (Note that this is not IE's behavior for all
            //    CSS properties, just some sort of special casing for
            //    font-family).  So a single quote *cannot* be used
            //    safely in the font-family context if there will be an
            //    innerHTML/cssText translation.  Note that Firefox 3.x
            //    does this too.
            //  - Double quote.  In IE, these get normalized to
            //    single-quotes, no matter what the encoding.  (Fun
            //    fact, in IE8, the 'content' CSS property gained
            //    support, where they special cased to preserve encoded
            //    double quotes, but still translate unadorned double
            //    quotes into single quotes.)  So, because their
            //    fixpoint behavior is identical to single quotes, they
            //    cannot be allowed either.  Firefox 3.x displays
            //    single-quote style behavior.
            //  - Backslashes are reduced by one (so \ -> ) every
            //    iteration, so they cannot be used safely.  This shows
            //    up in IE7, IE8 and FF3
            //  - Semicolons, commas and backticks are handled properly.
            //  - The rest of the ASCII punctuation is handled properly.
            // We haven't checked what browsers do to unadorned
            // versions, but this is not important as long as the
            // browser doesn't /remove/ surrounding quotes (as IE does
            // for HTML).
            //
            // With these results in hand, we conclude that there are
            // various levels of safety:
            //  - Paranoid: alphanumeric, spaces and dashes(?)
            //  - International: Paranoid + non-ASCII Unicode
            //  - Edgy: Everything except quotes, backslashes
            //  - NoJS: Standards compliance, e.g. sod IE. Note that
            //    with some judicious character escaping (since certain
            //    types of escaping doesn't work) this is theoretically
            //    OK as long as innerHTML/cssText is not called.
            // We believe that international is a reasonable default
            // (that we will implement now), and once we do more
            // extensive research, we may feel comfortable with dropping
            // it down to edgy.

            // Edgy: alphanumeric, spaces, dashes, underscores and Unicode.  Use of
            // str(c)spn assumes that the string was already well formed
            // Unicode (which of course it is).
            if (strspn(
$font$this->mask) !== strlen($font)) {
                continue;
            }

            // Historical:
            // In the absence of innerHTML/cssText, these ugly
            // transforms don't pose a security risk (as \ and "
            
// might--these escapes are not supported by most browsers).
            // We could try to be clever and use single-quote wrapping
            // when there is a double quote present, but I have choosen
            // not to implement that.  (NOTE: you can reduce the amount
            // of escapes by one depending on what quoting style you use)
            // $font = str_replace('\', '\5C ', $font);
            // $font = str_replace('"',  '\22 ', $font);
            // $font = str_replace("'",  '\27 ', $font);

            // font possibly with spaces, requires quoting
            
$final .= "'$font', ";
        }
        
$final rtrim($final', ');
        if (
$final === '') {
            return 
false;
        }
        return 
$final;
    }

}





/**
 * Validates based on {ident} CSS grammar production
 */
class HTMLPurifier_AttrDef_CSS_Ident extends HTMLPurifier_AttrDef
{

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$string trim($string);

        
// early abort: '' and '0' (strings that convert to false) are invalid
        
if (!$string) {
            return 
false;
        }

        
$pattern '/^(-?[A-Za-z_][A-Za-z_-0-9]*)$/';
        if (!
preg_match($pattern$string)) {
            return 
false;
        }
        return 
$string;
    }
}





/**
 * Decorator which enables !important to be used in CSS values.
 */
class HTMLPurifier_AttrDef_CSS_ImportantDecorator extends HTMLPurifier_AttrDef
{
    
/**
     * @type HTMLPurifier_AttrDef
     */
    
public $def;
    
/**
     * @type bool
     */
    
public $allow;

    
/**
     * @param HTMLPurifier_AttrDef $def Definition to wrap
     * @param bool $allow Whether or not to allow !important
     */
    
public function __construct($def$allow false)
    {
        
$this->def $def;
        
$this->allow $allow;
    }

    
/**
     * Intercepts and removes !important if necessary
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
// test for ! and important tokens
        
$string trim($string);
        
$is_important false;
        
// :TODO: optimization: test directly for !important and ! important
        
if (strlen($string) >= && substr($string, -9) === 'important') {
            
$temp rtrim(substr($string0, -9));
            
// use a temp, because we might want to restore important
            
if (strlen($temp) >= && substr($temp, -1) === '!') {
                
$string rtrim(substr($temp0, -1));
                
$is_important true;
            }
        }
        
$string $this->def->validate($string$config$context);
        if (
$this->allow && $is_important) {
            
$string .= ' !important';
        }
        return 
$string;
    }
}





/**
 * Represents a Length as defined by CSS.
 */
class HTMLPurifier_AttrDef_CSS_Length extends HTMLPurifier_AttrDef
{

    
/**
     * @type HTMLPurifier_Length|string
     */
    
protected $min;

    
/**
     * @type HTMLPurifier_Length|string
     */
    
protected $max;

    
/**
     * @param HTMLPurifier_Length|string $min Minimum length, or null for no bound. String is also acceptable.
     * @param HTMLPurifier_Length|string $max Maximum length, or null for no bound. String is also acceptable.
     */
    
public function __construct($min null$max null)
    {
        
$this->min $min !== null HTMLPurifier_Length::make($min) : null;
        
$this->max $max !== null HTMLPurifier_Length::make($max) : null;
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$string $this->parseCDATA($string);

        
// Optimizations
        
if ($string === '') {
            return 
false;
        }
        if (
$string === '0') {
            return 
'0';
        }
        if (
strlen($string) === 1) {
            return 
false;
        }

        
$length HTMLPurifier_Length::make($string);
        if (!
$length->isValid()) {
            return 
false;
        }

        if (
$this->min) {
            
$c $length->compareTo($this->min);
            if (
$c === false) {
                return 
false;
            }
            if (
$c 0) {
                return 
false;
            }
        }
        if (
$this->max) {
            
$c $length->compareTo($this->max);
            if (
$c === false) {
                return 
false;
            }
            if (
$c 0) {
                return 
false;
            }
        }
        return 
$length->toString();
    }
}





/**
 * Validates shorthand CSS property list-style.
 * @warning Does not support url tokens that have internal spaces.
 */
class HTMLPurifier_AttrDef_CSS_ListStyle extends HTMLPurifier_AttrDef
{

    
/**
     * Local copy of validators.
     * @type HTMLPurifier_AttrDef[]
     * @note See HTMLPurifier_AttrDef_CSS_Font::$info for a similar impl.
     */
    
protected $info;

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function __construct($config)
    {
        
$def $config->getCSSDefinition();
        
$this->info['list-style-type'] = $def->info['list-style-type'];
        
$this->info['list-style-position'] = $def->info['list-style-position'];
        
$this->info['list-style-image'] = $def->info['list-style-image'];
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
// regular pre-processing
        
$string $this->parseCDATA($string);
        if (
$string === '') {
            return 
false;
        }

        
// assumes URI doesn't have spaces in it
        
$bits explode(' 'strtolower($string)); // bits to process

        
$caught = array();
        
$caught['type'] = false;
        
$caught['position'] = false;
        
$caught['image'] = false;

        
$i 0// number of catches
        
$none false;

        foreach (
$bits as $bit) {
            if (
$i >= 3) {
                return;
            } 
// optimization bit
            
if ($bit === '') {
                continue;
            }
            foreach (
$caught as $key => $status) {
                if (
$status !== false) {
                    continue;
                }
                
$r $this->info['list-style-' $key]->validate($bit$config$context);
                if (
$r === false) {
                    continue;
                }
                if (
$r === 'none') {
                    if (
$none) {
                        continue;
                    } else {
                        
$none true;
                    }
                    if (
$key == 'image') {
                        continue;
                    }
                }
                
$caught[$key] = $r;
                
$i++;
                break;
            }
        }

        if (!
$i) {
            return 
false;
        }

        
$ret = array();

        
// construct type
        
if ($caught['type']) {
            
$ret[] = $caught['type'];
        }

        
// construct image
        
if ($caught['image']) {
            
$ret[] = $caught['image'];
        }

        
// construct position
        
if ($caught['position']) {
            
$ret[] = $caught['position'];
        }

        if (empty(
$ret)) {
            return 
false;
        }
        return 
implode(' '$ret);
    }
}





/**
 * Framework class for strings that involve multiple values.
 *
 * Certain CSS properties such as border-width and margin allow multiple
 * lengths to be specified.  This class can take a vanilla border-width
 * definition and multiply it, usually into a max of four.
 *
 * @note Even though the CSS specification isn't clear about it, inherit
 *       can only be used alone: it will never manifest as part of a multi
 *       shorthand declaration.  Thus, this class does not allow inherit.
 */
class HTMLPurifier_AttrDef_CSS_Multiple extends HTMLPurifier_AttrDef
{
    
/**
     * Instance of component definition to defer validation to.
     * @type HTMLPurifier_AttrDef
     * @todo Make protected
     */
    
public $single;

    
/**
     * Max number of values allowed.
     * @todo Make protected
     */
    
public $max;

    
/**
     * @param HTMLPurifier_AttrDef $single HTMLPurifier_AttrDef to multiply
     * @param int $max Max number of values allowed (usually four)
     */
    
public function __construct($single$max 4)
    {
        
$this->single $single;
        
$this->max $max;
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$string $this->mungeRgb($this->parseCDATA($string));
        if (
$string === '') {
            return 
false;
        }
        
$parts explode(' '$string); // parseCDATA replaced r, t and n
        
$length count($parts);
        
$final '';
        for (
$i 0$num 0$i $length && $num $this->max$i++) {
            if (
ctype_space($parts[$i])) {
                continue;
            }
            
$result $this->single->validate($parts[$i], $config$context);
            if (
$result !== false) {
                
$final .= $result ' ';
                
$num++;
            }
        }
        if (
$final === '') {
            return 
false;
        }
        return 
rtrim($final);
    }
}





/**
 * Validates a Percentage as defined by the CSS spec.
 */
class HTMLPurifier_AttrDef_CSS_Percentage extends HTMLPurifier_AttrDef
{

    
/**
     * Instance to defer number validation to.
     * @type HTMLPurifier_AttrDef_CSS_Number
     */
    
protected $number_def;

    
/**
     * @param bool $non_negative Whether to forbid negative values
     */
    
public function __construct($non_negative false)
    {
        
$this->number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative);
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$string $this->parseCDATA($string);

        if (
$string === '') {
            return 
false;
        }
        
$length strlen($string);
        if (
$length === 1) {
            return 
false;
        }
        if (
$string[$length 1] !== '%') {
            return 
false;
        }

        
$number substr($string0$length 1);
        
$number $this->number_def->validate($number$config$context);

        if (
$number === false) {
            return 
false;
        }
        return 
"$number%";
    }
}





/**
 * Validates the value for the CSS property text-decoration
 * @note This class could be generalized into a version that acts sort of
 *       like Enum except you can compound the allowed values.
 */
class HTMLPurifier_AttrDef_CSS_TextDecoration extends HTMLPurifier_AttrDef
{

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        static 
$allowed_values = array(
            
'line-through' => true,
            
'overline' => true,
            
'underline' => true,
        );

        
$string strtolower($this->parseCDATA($string));

        if (
$string === 'none') {
            return 
$string;
        }

        
$parts explode(' '$string);
        
$final '';
        foreach (
$parts as $part) {
            if (isset(
$allowed_values[$part])) {
                
$final .= $part ' ';
            }
        }
        
$final rtrim($final);
        if (
$final === '') {
            return 
false;
        }
        return 
$final;
    }
}





/**
 * Validates a URI in CSS syntax, which uses url('http://example.com')
 * @note While theoretically speaking a URI in a CSS document could
 *       be non-embedded, as of CSS2 there is no such usage so we're
 *       generalizing it. This may need to be changed in the future.
 * @warning Since HTMLPurifier_AttrDef_CSS blindly uses semicolons as
 *          the separator, you cannot put a literal semicolon in
 *          in the URI. Try percent encoding it, in that case.
 */
class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI
{

    public function 
__construct()
    {
        
parent::__construct(true); // always embedded
    
}

    
/**
     * @param string $uri_string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($uri_string$config$context)
    {
        
// parse the URI out of the string and then pass it onto
        // the parent object

        
$uri_string $this->parseCDATA($uri_string);
        if (
strpos($uri_string'url(') !== 0) {
            return 
false;
        }
        
$uri_string substr($uri_string4);
        if (
strlen($uri_string) == 0) {
            return 
false;
        }
        
$new_length strlen($uri_string) - 1;
        if (
$uri_string[$new_length] != ')') {
            return 
false;
        }
        
$uri trim(substr($uri_string0$new_length));

        if (!empty(
$uri) && ($uri[0] == "'" || $uri[0] == '"')) {
            
$quote $uri[0];
            
$new_length strlen($uri) - 1;
            if (
$uri[$new_length] !== $quote) {
                return 
false;
            }
            
$uri substr($uri1$new_length 1);
        }

        
$uri $this->expandCSSEscape($uri);

        
$result parent::validate($uri$config$context);

        if (
$result === false) {
            return 
false;
        }

        
// extra sanity check; should have been done by URI
        
$result str_replace(array('"'"\", "n", "x0c", "r"), "", $result);

        // suspicious characters are ()'; we're going to percent encode
        // them for safety.
        
$result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result);

        // there'
s an extra bug where ampersands lose their escaping on
        
// an innerHTML cycle, so a very unlucky query parameter could
        // then change the meaning of the URL.  Unfortunately, there's
        // not much we can do about that...
        
return "url("$result")";
    }
}





/**
 * Validates a boolean attribute
 */
class HTMLPurifier_AttrDef_HTML_Bool extends HTMLPurifier_AttrDef
{

    
/**
     * @type bool
     */
    
protected $name;

    
/**
     * @type bool
     */
    
public $minimized true;

    
/**
     * @param bool $name
     */
    
public function __construct($name false)
    {
        
$this->name $name;
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        return 
$this->name;
    }

    
/**
     * @param string $string Name of attribute
     * @return HTMLPurifier_AttrDef_HTML_Bool
     */
    
public function make($string)
    {
        return new 
HTMLPurifier_AttrDef_HTML_Bool($string);
    }
}





/**
 * Validates contents based on NMTOKENS attribute type.
 */
class HTMLPurifier_AttrDef_HTML_Nmtokens extends HTMLPurifier_AttrDef
{

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$string trim($string);

        
// early abort: '' and '0' (strings that convert to false) are invalid
        
if (!$string) {
            return 
false;
        }

        
$tokens $this->split($string$config$context);
        
$tokens $this->filter($tokens$config$context);
        if (empty(
$tokens)) {
            return 
false;
        }
        return 
implode(' '$tokens);
    }

    
/**
     * Splits a space separated list of tokens into its constituent parts.
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
protected function split($string$config$context)
    {
        
// OPTIMIZABLE!
        // do the preg_match, capture all subpatterns for reformulation

        // we don't support U+00A1 and up codepoints or
        // escaping because I don't know how to do that with regexps
        // and plus it would complicate optimization efforts (you never
        // see that anyway).
        
$pattern '/(?:(?<=s)|A)' // look behind for space or string start
            
'((?:--|-?[A-Za-z_])[A-Za-z_-0-9]*)' .
            
'(?:(?=s)|z)/'// look ahead for space or string end
        
preg_match_all($pattern$string$matches);
        return 
$matches[1];
    }

    
/**
     * Template method for removing certain tokens based on arbitrary criteria.
     * @note If we wanted to be really functional, we'd do an array_filter
     *       with a callback. But... we're not.
     * @param array $tokens
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
protected function filter($tokens$config$context)
    {
        return 
$tokens;
    }
}





/**
 * Implements special behavior for class attribute (normally NMTOKENS)
 */
class HTMLPurifier_AttrDef_HTML_Class extends HTMLPurifier_AttrDef_HTML_Nmtokens
{
    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
protected function split($string$config$context)
    {
        
// really, this twiddle should be lazy loaded
        
$name $config->getDefinition('HTML')->doctype->name;
        if (
$name == "XHTML 1.1" || $name == "XHTML 2.0") {
            return 
parent::split($string$config$context);
        } else {
            return 
preg_split('/s+/'$string);
        }
    }

    
/**
     * @param array $tokens
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
protected function filter($tokens$config$context)
    {
        
$allowed $config->get('Attr.AllowedClasses');
        
$forbidden $config->get('Attr.ForbiddenClasses');
        
$ret = array();
        foreach (
$tokens as $token) {
            if ((
$allowed === null || isset($allowed[$token])) &&
                !isset(
$forbidden[$token]) &&
                
// We need this O(n) check because of PHP's array
                // implementation that casts -0 to 0.
                
!in_array($token$rettrue)
            ) {
                
$ret[] = $token;
            }
        }
        return 
$ret;
    }
}



/**
 * Validates a color according to the HTML spec.
 */
class HTMLPurifier_AttrDef_HTML_Color extends HTMLPurifier_AttrDef
{

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        static 
$colors null;
        if (
$colors === null) {
            
$colors $config->get('Core.ColorKeywords');
        }

        
$string trim($string);

        if (empty(
$string)) {
            return 
false;
        }
        
$lower strtolower($string);
        if (isset(
$colors[$lower])) {
            return 
$colors[$lower];
        }
        if (
$string[0] === '#') {
            
$hex substr($string1);
        } else {
            
$hex $string;
        }

        
$length strlen($hex);
        if (
$length !== && $length !== 6) {
            return 
false;
        }
        if (!
ctype_xdigit($hex)) {
            return 
false;
        }
        if (
$length === 3) {
            
$hex $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
        }
        return 
"#$hex";
    }
}





/**
 * Special-case enum attribute definition that lazy loads allowed frame targets
 */
class HTMLPurifier_AttrDef_HTML_FrameTarget extends HTMLPurifier_AttrDef_Enum
{

    
/**
     * @type array
     */
    
public $valid_values false// uninitialized value

    /**
     * @type bool
     */
    
protected $case_sensitive false;

    public function 
__construct()
    {
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        if (
$this->valid_values === false) {
            
$this->valid_values $config->get('Attr.AllowedFrameTargets');
        }
        return 
parent::validate($string$config$context);
    }
}





/**
 * Validates the HTML attribute ID.
 * @warning Even though this is the id processor, it
 *          will ignore the directive Attr:IDBlacklist, since it will only
 *          go according to the ID accumulator. Since the accumulator is
 *          automatically generated, it will have already absorbed the
 *          blacklist. If you're hacking around, make sure you use load()!
 */

class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef
{

    
// selector is NOT a valid thing to use for IDREFs, because IDREFs
    // *must* target IDs that exist, whereas selector #ids do not.

    /**
     * Determines whether or not we're validating an ID in a CSS
     * selector context.
     * @type bool
     */
    
protected $selector;

    
/**
     * @param bool $selector
     */
    
public function __construct($selector false)
    {
        
$this->selector $selector;
    }

    
/**
     * @param string $id
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($id$config$context)
    {
        if (!
$this->selector && !$config->get('Attr.EnableID')) {
            return 
false;
        }

        
$id trim($id); // trim it first

        
if ($id === '') {
            return 
false;
        }

        
$prefix $config->get('Attr.IDPrefix');
        if (
$prefix !== '') {
            
$prefix .= $config->get('Attr.IDPrefixLocal');
            
// prevent re-appending the prefix
            
if (strpos($id$prefix) !== 0) {
                
$id $prefix $id;
            }
        } elseif (
$config->get('Attr.IDPrefixLocal') !== '') {
            
trigger_error(
                
'%Attr.IDPrefixLocal cannot be used unless ' .
                
'%Attr.IDPrefix is set',
                
E_USER_WARNING
            
);
        }

        if (!
$this->selector) {
            
$id_accumulator =& $context->get('IDAccumulator');
            if (isset(
$id_accumulator->ids[$id])) {
                return 
false;
            }
        }

        
// we purposely avoid using regex, hopefully this is faster

        
if ($config->get('Attr.ID.HTML5') === true) {
            if (
preg_match('/[tnx0bx0c ]/'$id)) {
                return 
false;
            }
        } else {
            if (
ctype_alpha($id)) {
                
// OK
            
} else {
                if (!
ctype_alpha(@$id[0])) {
                    return 
false;
                }
                
// primitive style of regexps, I suppose
                
$trim trim(
                    
$id,
                    
'A..Za..z0..9:-._'
                
);
                if (
$trim !== '') {
                    return 
false;
                }
            }
        }

        
$regexp $config->get('Attr.IDBlacklistRegexp');
        if (
$regexp && preg_match($regexp$id)) {
            return 
false;
        }

        if (!
$this->selector) {
            
$id_accumulator->add($id);
        }

        
// if no change was made to the ID, return the result
        // else, return the new id if stripping whitespace made it
        //     valid, or return false.
        
return $id;
    }
}





/**
 * Validates an integer representation of pixels according to the HTML spec.
 */
class HTMLPurifier_AttrDef_HTML_Pixels extends HTMLPurifier_AttrDef
{

    
/**
     * @type int
     */
    
protected $max;

    
/**
     * @param int $max
     */
    
public function __construct($max null)
    {
        
$this->max $max;
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$string trim($string);
        if (
$string === '0') {
            return 
$string;
        }
        if (
$string === '') {
            return 
false;
        }
        
$length strlen($string);
        if (
substr($string$length 2) == 'px') {
            
$string substr($string0$length 2);
        }
        if (!
is_numeric($string)) {
            return 
false;
        }
        
$int = (int)$string;

        if (
$int 0) {
            return 
'0';
        }

        
// upper-bound value, extremely high values can
        // crash operating systems, see <http://ha.ckers.org/imagecrash.html>
        // WARNING, above link WILL crash you if you're using Windows

        
if ($this->max !== null && $int $this->max) {
            return (string)
$this->max;
        }
        return (string)
$int;
    }

    
/**
     * @param string $string
     * @return HTMLPurifier_AttrDef
     */
    
public function make($string)
    {
        if (
$string === '') {
            
$max null;
        } else {
            
$max = (int)$string;
        }
        
$class get_class($this);
        return new 
$class($max);
    }
}





/**
 * Validates the HTML type length (not to be confused with CSS's length).
 *
 * This accepts integer pixels or percentages as lengths for certain
 * HTML attributes.
 */

class HTMLPurifier_AttrDef_HTML_Length extends HTMLPurifier_AttrDef_HTML_Pixels
{

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$string trim($string);
        if (
$string === '') {
            return 
false;
        }

        
$parent_result parent::validate($string$config$context);
        if (
$parent_result !== false) {
            return 
$parent_result;
        }

        
$length strlen($string);
        
$last_char $string[$length 1];

        if (
$last_char !== '%') {
            return 
false;
        }

        
$points substr($string0$length 1);

        if (!
is_numeric($points)) {
            return 
false;
        }

        
$points = (int)$points;

        if (
$points 0) {
            return 
'0%';
        }
        if (
$points 100) {
            return 
'100%';
        }
        return ((string)
$points) . '%';
    }
}





/**
 * Validates a rel/rev link attribute against a directive of allowed values
 * @note We cannot use Enum because link types allow multiple
 *       values.
 * @note Assumes link types are ASCII text
 */
class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef
{

    
/**
     * Name config attribute to pull.
     * @type string
     */
    
protected $name;

    
/**
     * @param string $name
     */
    
public function __construct($name)
    {
        
$configLookup = array(
            
'rel' => 'AllowedRel',
            
'rev' => 'AllowedRev'
        
);
        if (!isset(
$configLookup[$name])) {
            
trigger_error(
                
'Unrecognized attribute name for link ' .
                
'relationship.',
                
E_USER_ERROR
            
);
            return;
        }
        
$this->name $configLookup[$name];
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$allowed $config->get('Attr.' $this->name);
        if (empty(
$allowed)) {
            return 
false;
        }

        
$string $this->parseCDATA($string);
        
$parts explode(' '$string);

        
// lookup to prevent duplicates
        
$ret_lookup = array();
        foreach (
$parts as $part) {
            
$part strtolower(trim($part));
            if (!isset(
$allowed[$part])) {
                continue;
            }
            
$ret_lookup[$part] = true;
        }

        if (empty(
$ret_lookup)) {
            return 
false;
        }
        
$string implode(' 'array_keys($ret_lookup));
        return 
$string;
    }
}





/**
 * Validates a MultiLength as defined by the HTML spec.
 *
 * A multilength is either a integer (pixel count), a percentage, or
 * a relative number.
 */
class HTMLPurifier_AttrDef_HTML_MultiLength extends HTMLPurifier_AttrDef_HTML_Length
{

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$string trim($string);
        if (
$string === '') {
            return 
false;
        }

        
$parent_result parent::validate($string$config$context);
        if (
$parent_result !== false) {
            return 
$parent_result;
        }

        
$length strlen($string);
        
$last_char $string[$length 1];

        if (
$last_char !== '*') {
            return 
false;
        }

        
$int substr($string0$length 1);

        if (
$int == '') {
            return 
'*';
        }
        if (!
is_numeric($int)) {
            return 
false;
        }

        
$int = (int)$int;
        if (
$int 0) {
            return 
false;
        }
        if (
$int == 0) {
            return 
'0';
        }
        if (
$int == 1) {
            return 
'*';
        }
        return ((string)
$int) . '*';
    }
}





abstract class 
HTMLPurifier_AttrDef_URI_Email extends HTMLPurifier_AttrDef
{

    
/**
     * Unpacks a mailbox into its display-name and address
     * @param string $string
     * @return mixed
     */
    
public function unpack($string)
    {
        
// needs to be implemented
    
}

}

// sub-implementations





/**
 * Validates a host according to the IPv4, IPv6 and DNS (future) specifications.
 */
class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
{

    
/**
     * IPv4 sub-validator.
     * @type HTMLPurifier_AttrDef_URI_IPv4
     */
    
protected $ipv4;

    
/**
     * IPv6 sub-validator.
     * @type HTMLPurifier_AttrDef_URI_IPv6
     */
    
protected $ipv6;

    public function 
__construct()
    {
        
$this->ipv4 = new HTMLPurifier_AttrDef_URI_IPv4();
        
$this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6();
    }

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
$length strlen($string);
        
// empty hostname is OK; it's usually semantically equivalent:
        // the default host as defined by a URI scheme is used:
        //
        //      If the URI scheme defines a default for host, then that
        //      default applies when the host subcomponent is undefined
        //      or when the registered name is empty (zero length).
        
if ($string === '') {
            return 
'';
        }
        if (
$length && $string[0] === '[' && $string[$length 1] === ']') {
            
//IPv6
            
$ip substr($string1$length 2);
            
$valid $this->ipv6->validate($ip$config$context);
            if (
$valid === false) {
                return 
false;
            }
            return 
'[' $valid ']';
        }

        
// need to do checks on unusual encodings too
        
$ipv4 $this->ipv4->validate($string$config$context);
        if (
$ipv4 !== false) {
            return 
$ipv4;
        }

        
// A regular domain name.

        // This doesn't match I18N domain names, but we don't have proper IRI support,
        // so force users to insert Punycode.

        // There is not a good sense in which underscores should be
        // allowed, since it's technically not! (And if you go as
        // far to allow everything as specified by the DNS spec...
        // well, that's literally everything, modulo some space limits
        // for the components and the overall name (which, by the way,
        // we are NOT checking!).  So we (arbitrarily) decide this:
        // let's allow underscores wherever we would have allowed
        // hyphens, if they are enabled.  This is a pretty good match
        // for browser behavior, for example, a large number of browsers
        // cannot handle foo_.example.com, but foo_bar.example.com is
        // fairly well supported.
        
$underscore $config->get('Core.AllowHostnameUnderscore') ? '_' '';

        
// Based off of RFC 1738, but amended so that
        // as per RFC 3696, the top label need only not be all numeric.
        // The productions describing this are:
        
$a   '[a-z]';     // alpha
        
$an  '[a-z0-9]';  // alphanum
        
$and "[a-z0-9-$underscore]"// alphanum | "-"
        // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
        
$domainlabel "$an(?:$and*$an)?";
        
// AMENDED as per RFC 3696
        // toplabel    = alphanum | alphanum *( alphanum | "-" ) alphanum
        //      side condition: not all numeric
        
$toplabel "$an(?:$and*$an)?";
        
// hostname    = *( domainlabel "." ) toplabel [ "." ]
        
if (preg_match("/^(?:$domainlabel.)*($toplabel).?$/i"$string$matches)) {
            if (!
ctype_digit($matches[1])) {
                return 
$string;
            }
        }

        
// PHP 5.3 and later support this functionality natively
        
if (function_exists('idn_to_ascii')) {
            
$string idn_to_ascii($string);

        
// If we have Net_IDNA2 support, we can support IRIs by
        // punycoding them. (This is the most portable thing to do,
        // since otherwise we have to assume browsers support
        
} elseif ($config->get('Core.EnableIDNA')) {
            
$idna = new Net_IDNA2(array('encoding' => 'utf8''overlong' => false'strict' => true));
            
// we need to encode each period separately
            
$parts explode('.'$string);
            try {
                
$new_parts = array();
                foreach (
$parts as $part) {
                    
$encodable false;
                    for (
$i 0$c strlen($part); $i $c$i++) {
                        if (
ord($part[$i]) > 0x7a) {
                            
$encodable true;
                            break;
                        }
                    }
                    if (!
$encodable) {
                        
$new_parts[] = $part;
                    } else {
                        
$new_parts[] = $idna->encode($part);
                    }
                }
                
$string implode('.'$new_parts);
            } catch (
Exception $e) {
                
// XXX error reporting
            
}
        }
        
// Try again
        
if (preg_match("/^($domainlabel.)*$toplabel.?$/i"$string)) {
            return 
$string;
        }
        return 
false;
    }
}





/**
 * Validates an IPv4 address
 * @author Feyd @ forums.devnetwork.net (public domain)
 */
class HTMLPurifier_AttrDef_URI_IPv4 extends HTMLPurifier_AttrDef
{

    
/**
     * IPv4 regex, protected so that IPv6 can reuse it.
     * @type string
     */
    
protected $ip4;

    
/**
     * @param string $aIP
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($aIP$config$context)
    {
        if (!
$this->ip4) {
            
$this->_loadRegex();
        }

        if (
preg_match('#^' $this->ip4 '$#s'$aIP)) {
            return 
$aIP;
        }
        return 
false;
    }

    
/**
     * Lazy load function to prevent regex from being stuffed in
     * cache.
     */
    
protected function _loadRegex()
    {
        
$oct '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'// 0-255
        
$this->ip4 "(?:{$oct}\.{$oct}\.{$oct}\.{$oct})";
    }
}





/**
 * Validates an IPv6 address.
 * @author Feyd @ forums.devnetwork.net (public domain)
 * @note This function requires brackets to have been removed from address
 *       in URI.
 */
class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4
{

    
/**
     * @param string $aIP
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($aIP$config$context)
    {
        if (!
$this->ip4) {
            
$this->_loadRegex();
        }

        
$original $aIP;

        
$hex '[0-9a-fA-F]';
        
$blk '(?:' $hex '{1,4})';
        
$pre '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'// /0 - /128

        //      prefix check
        
if (strpos($aIP'/') !== false) {
            if (
preg_match('#' $pre '$#s'$aIP$find)) {
                
$aIP substr($aIP0strlen($find[0]));
                unset(
$find);
            } else {
                return 
false;
            }
        }

        
//      IPv4-compatiblity check
        
if (preg_match('#(?<=:' ')' $this->ip4 '$#s'$aIP$find)) {
            
$aIP substr($aIP0strlen($find[0]));
            
$ip explode('.'$find[0]);
            
$ip array_map('dechex'$ip);
            
$aIP .= $ip[0] . $ip[1] . ':' $ip[2] . $ip[3];
            unset(
$find$ip);
        }

        
//      compression check
        
$aIP explode('::'$aIP);
        
$c count($aIP);
        if (
$c 2) {
            return 
false;
        } elseif (
$c == 2) {
            list(
$first$second) = $aIP;
            
$first explode(':'$first);
            
$second explode(':'$second);

            if (
count($first) + count($second) > 8) {
                return 
false;
            }

            while (
count($first) < 8) {
                
array_push($first'0');
            }

            
array_splice($firstcount($second), 8$second);
            
$aIP $first;
            unset(
$first$second);
        } else {
            
$aIP explode(':'$aIP[0]);
        }
        
$c count($aIP);

        if (
$c != 8) {
            return 
false;
        }

        
//      All the pieces should be 16-bit hex strings. Are they?
        
foreach ($aIP as $piece) {
            if (!
preg_match('#^[0-9a-fA-F]{4}$#s'sprintf('%04s'$piece))) {
                return 
false;
            }
        }
        return 
$original;
    }
}





/**
 * Primitive email validation class based on the regexp found at
 * http://www.regular-expressions.info/email.html
 */
class HTMLPurifier_AttrDef_URI_Email_SimpleCheck extends HTMLPurifier_AttrDef_URI_Email
{

    
/**
     * @param string $string
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool|string
     */
    
public function validate($string$config$context)
    {
        
// no support for named mailboxes i.e. "Bob <bob@example.com>"
        // that needs more percent encoding to be done
        
if ($string == '') {
            return 
false;
        }
        
$string trim($string);
        
$result preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i'$string);
        return 
$result $string false;
    }
}





/**
 * Pre-transform that changes proprietary background attribute to CSS.
 */
class HTMLPurifier_AttrTransform_Background extends HTMLPurifier_AttrTransform
{
    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['background'])) {
            return 
$attr;
        }

        
$background $this->confiscateAttr($attr'background');
        
// some validation should happen here

        
$this->prependCSS($attr"background-image:url($background);");
        return 
$attr;
    }
}





// this MUST be placed in post, as it assumes that any value in dir is valid

/**
 * Post-trasnform that ensures that bdo tags have the dir attribute set.
 */
class HTMLPurifier_AttrTransform_BdoDir extends HTMLPurifier_AttrTransform
{

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (isset(
$attr['dir'])) {
            return 
$attr;
        }
        
$attr['dir'] = $config->get('Attr.DefaultTextDir');
        return 
$attr;
    }
}





/**
 * Pre-transform that changes deprecated bgcolor attribute to CSS.
 */
class HTMLPurifier_AttrTransform_BgColor extends HTMLPurifier_AttrTransform
{
    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['bgcolor'])) {
            return 
$attr;
        }

        
$bgcolor $this->confiscateAttr($attr'bgcolor');
        
// some validation should happen here

        
$this->prependCSS($attr"background-color:$bgcolor;");
        return 
$attr;
    }
}





/**
 * Pre-transform that changes converts a boolean attribute to fixed CSS
 */
class HTMLPurifier_AttrTransform_BoolToCSS extends HTMLPurifier_AttrTransform
{
    
/**
     * Name of boolean attribute that is trigger.
     * @type string
     */
    
protected $attr;

    
/**
     * CSS declarations to add to style, needs trailing semicolon.
     * @type string
     */
    
protected $css;

    
/**
     * @param string $attr attribute name to convert from
     * @param string $css CSS declarations to add to style (needs semicolon)
     */
    
public function __construct($attr$css)
    {
        
$this->attr $attr;
        
$this->css $css;
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr[$this->attr])) {
            return 
$attr;
        }
        unset(
$attr[$this->attr]);
        
$this->prependCSS($attr$this->css);
        return 
$attr;
    }
}





/**
 * Pre-transform that changes deprecated border attribute to CSS.
 */
class HTMLPurifier_AttrTransform_Border extends HTMLPurifier_AttrTransform
{
    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['border'])) {
            return 
$attr;
        }
        
$border_width $this->confiscateAttr($attr'border');
        
// some validation should happen here
        
$this->prependCSS($attr"border:{$border_width}px solid;");
        return 
$attr;
    }
}





/**
 * Generic pre-transform that converts an attribute with a fixed number of
 * values (enumerated) to CSS.
 */
class HTMLPurifier_AttrTransform_EnumToCSS extends HTMLPurifier_AttrTransform
{
    
/**
     * Name of attribute to transform from.
     * @type string
     */
    
protected $attr;

    
/**
     * Lookup array of attribute values to CSS.
     * @type array
     */
    
protected $enumToCSS = array();

    
/**
     * Case sensitivity of the matching.
     * @type bool
     * @warning Currently can only be guaranteed to work with ASCII
     *          values.
     */
    
protected $caseSensitive false;

    
/**
     * @param string $attr Attribute name to transform from
     * @param array $enum_to_css Lookup array of attribute values to CSS
     * @param bool $case_sensitive Case sensitivity indicator, default false
     */
    
public function __construct($attr$enum_to_css$case_sensitive false)
    {
        
$this->attr $attr;
        
$this->enumToCSS $enum_to_css;
        
$this->caseSensitive = (bool)$case_sensitive;
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr[$this->attr])) {
            return 
$attr;
        }

        
$value trim($attr[$this->attr]);
        unset(
$attr[$this->attr]);

        if (!
$this->caseSensitive) {
            
$value strtolower($value);
        }

        if (!isset(
$this->enumToCSS[$value])) {
            return 
$attr;
        }
        
$this->prependCSS($attr$this->enumToCSS[$value]);
        return 
$attr;
    }
}





// must be called POST validation

/**
 * Transform that supplies default values for the src and alt attributes
 * in img tags, as well as prevents the img tag from being removed
 * because of a missing alt tag. This needs to be registered as both
 * a pre and post attribute transform.
 */
class HTMLPurifier_AttrTransform_ImgRequired extends HTMLPurifier_AttrTransform
{

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        
$src true;
        if (!isset(
$attr['src'])) {
            if (
$config->get('Core.RemoveInvalidImg')) {
                return 
$attr;
            }
            
$attr['src'] = $config->get('Attr.DefaultInvalidImage');
            
$src false;
        }

        if (!isset(
$attr['alt'])) {
            if (
$src) {
                
$alt $config->get('Attr.DefaultImageAlt');
                if (
$alt === null) {
                    
$attr['alt'] = basename($attr['src']);
                } else {
                    
$attr['alt'] = $alt;
                }
            } else {
                
$attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt');
            }
        }
        return 
$attr;
    }
}





/**
 * Pre-transform that changes deprecated hspace and vspace attributes to CSS
 */
class HTMLPurifier_AttrTransform_ImgSpace extends HTMLPurifier_AttrTransform
{
    
/**
     * @type string
     */
    
protected $attr;

    
/**
     * @type array
     */
    
protected $css = array(
        
'hspace' => array('left''right'),
        
'vspace' => array('top''bottom')
    );

    
/**
     * @param string $attr
     */
    
public function __construct($attr)
    {
        
$this->attr $attr;
        if (!isset(
$this->css[$attr])) {
            
trigger_error(htmlspecialchars($attr) . ' is not valid space attribute');
        }
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr[$this->attr])) {
            return 
$attr;
        }

        
$width $this->confiscateAttr($attr$this->attr);
        
// some validation could happen here

        
if (!isset($this->css[$this->attr])) {
            return 
$attr;
        }

        
$style '';
        foreach (
$this->css[$this->attr] as $suffix) {
            
$property "margin-$suffix";
            
$style .= "$property:{$width}px;";
        }
        
$this->prependCSS($attr$style);
        return 
$attr;
    }
}





/**
 * Performs miscellaneous cross attribute validation and filtering for
 * input elements. This is meant to be a post-transform.
 */
class HTMLPurifier_AttrTransform_Input extends HTMLPurifier_AttrTransform
{
    
/**
     * @type HTMLPurifier_AttrDef_HTML_Pixels
     */
    
protected $pixels;

    public function 
__construct()
    {
        
$this->pixels = new HTMLPurifier_AttrDef_HTML_Pixels();
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['type'])) {
            
$t 'text';
        } else {
            
$t strtolower($attr['type']);
        }
        if (isset(
$attr['checked']) && $t !== 'radio' && $t !== 'checkbox') {
            unset(
$attr['checked']);
        }
        if (isset(
$attr['maxlength']) && $t !== 'text' && $t !== 'password') {
            unset(
$attr['maxlength']);
        }
        if (isset(
$attr['size']) && $t !== 'text' && $t !== 'password') {
            
$result $this->pixels->validate($attr['size'], $config$context);
            if (
$result === false) {
                unset(
$attr['size']);
            } else {
                
$attr['size'] = $result;
            }
        }
        if (isset(
$attr['src']) && $t !== 'image') {
            unset(
$attr['src']);
        }
        if (!isset(
$attr['value']) && ($t === 'radio' || $t === 'checkbox')) {
            
$attr['value'] = '';
        }
        return 
$attr;
    }
}





/**
 * Post-transform that copies lang's value to xml:lang (and vice-versa)
 * @note Theoretically speaking, this could be a pre-transform, but putting
 *       post is more efficient.
 */
class HTMLPurifier_AttrTransform_Lang extends HTMLPurifier_AttrTransform
{

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        
$lang = isset($attr['lang']) ? $attr['lang'] : false;
        
$xml_lang = isset($attr['xml:lang']) ? $attr['xml:lang'] : false;

        if (
$lang !== false && $xml_lang === false) {
            
$attr['xml:lang'] = $lang;
        } elseif (
$xml_lang !== false) {
            
$attr['lang'] = $xml_lang;
        }
        return 
$attr;
    }
}





/**
 * Class for handling width/height length attribute transformations to CSS
 */
class HTMLPurifier_AttrTransform_Length extends HTMLPurifier_AttrTransform
{

    
/**
     * @type string
     */
    
protected $name;

    
/**
     * @type string
     */
    
protected $cssName;

    public function 
__construct($name$css_name null)
    {
        
$this->name $name;
        
$this->cssName $css_name $css_name $name;
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr[$this->name])) {
            return 
$attr;
        }
        
$length $this->confiscateAttr($attr$this->name);
        if (
ctype_digit($length)) {
            
$length .= 'px';
        }
        
$this->prependCSS($attr$this->cssName ":$length;");
        return 
$attr;
    }
}





/**
 * Pre-transform that changes deprecated name attribute to ID if necessary
 */
class HTMLPurifier_AttrTransform_Name extends HTMLPurifier_AttrTransform
{

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        
// Abort early if we're using relaxed definition of name
        
if ($config->get('HTML.Attr.Name.UseCDATA')) {
            return 
$attr;
        }
        if (!isset(
$attr['name'])) {
            return 
$attr;
        }
        
$id $this->confiscateAttr($attr'name');
        if (isset(
$attr['id'])) {
            return 
$attr;
        }
        
$attr['id'] = $id;
        return 
$attr;
    }
}





/**
 * Post-transform that performs validation to the name attribute; if
 * it is present with an equivalent id attribute, it is passed through;
 * otherwise validation is performed.
 */
class HTMLPurifier_AttrTransform_NameSync extends HTMLPurifier_AttrTransform
{

    public function 
__construct()
    {
        
$this->idDef = new HTMLPurifier_AttrDef_HTML_ID();
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['name'])) {
            return 
$attr;
        }
        
$name $attr['name'];
        if (isset(
$attr['id']) && $attr['id'] === $name) {
            return 
$attr;
        }
        
$result $this->idDef->validate($name$config$context);
        if (
$result === false) {
            unset(
$attr['name']);
        } else {
            
$attr['name'] = $result;
        }
        return 
$attr;
    }
}





// must be called POST validation

/**
 * Adds rel="nofollow" to all outbound links.  This transform is
 * only attached if Attr.Nofollow is TRUE.
 */
class HTMLPurifier_AttrTransform_Nofollow extends HTMLPurifier_AttrTransform
{
    
/**
     * @type HTMLPurifier_URIParser
     */
    
private $parser;

    public function 
__construct()
    {
        
$this->parser = new HTMLPurifier_URIParser();
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['href'])) {
            return 
$attr;
        }

        
// XXX Kind of inefficient
        
$url $this->parser->parse($attr['href']);
        
$scheme $url->getSchemeObj($config$context);

        if (
$scheme->browsable && !$url->isLocal($config$context)) {
            if (isset(
$attr['rel'])) {
                
$rels explode(' '$attr['rel']);
                if (!
in_array('nofollow'$rels)) {
                    
$rels[] = 'nofollow';
                }
                
$attr['rel'] = implode(' '$rels);
            } else {
                
$attr['rel'] = 'nofollow';
            }
        }
        return 
$attr;
    }
}





class 
HTMLPurifier_AttrTransform_SafeEmbed extends HTMLPurifier_AttrTransform
{
    
/**
     * @type string
     */
    
public $name "SafeEmbed";

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        
$attr['allowscriptaccess'] = 'never';
        
$attr['allownetworking'] = 'internal';
        
$attr['type'] = 'application/x-shockwave-flash';
        return 
$attr;
    }
}





/**
 * Writes default type for all objects. Currently only supports flash.
 */
class HTMLPurifier_AttrTransform_SafeObject extends HTMLPurifier_AttrTransform
{
    
/**
     * @type string
     */
    
public $name "SafeObject";

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['type'])) {
            
$attr['type'] = 'application/x-shockwave-flash';
        }
        return 
$attr;
    }
}





/**
 * Validates name/value pairs in param tags to be used in safe objects. This
 * will only allow name values it recognizes, and pre-fill certain attributes
 * with required values.
 *
 * @note
 *      This class only supports Flash. In the future, Quicktime support
 *      may be added.
 *
 * @warning
 *      This class expects an injector to add the necessary parameters tags.
 */
class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform
{
    
/**
     * @type string
     */
    
public $name "SafeParam";

    
/**
     * @type HTMLPurifier_AttrDef_URI
     */
    
private $uri;

    public function 
__construct()
    {
        
$this->uri = new HTMLPurifier_AttrDef_URI(true); // embedded
        
$this->wmode = new HTMLPurifier_AttrDef_Enum(array('window''opaque''transparent'));
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        
// If we add support for other objects, we'll need to alter the
        // transforms.
        
switch ($attr['name']) {
            
// application/x-shockwave-flash
            // Keep this synchronized with Injector/SafeObject.php
            
case 'allowScriptAccess':
                
$attr['value'] = 'never';
                break;
            case 
'allowNetworking':
                
$attr['value'] = 'internal';
                break;
            case 
'allowFullScreen':
                if (
$config->get('HTML.FlashAllowFullScreen')) {
                    
$attr['value'] = ($attr['value'] == 'true') ? 'true' 'false';
                } else {
                    
$attr['value'] = 'false';
                }
                break;
            case 
'wmode':
                
$attr['value'] = $this->wmode->validate($attr['value'], $config$context);
                break;
            case 
'movie':
            case 
'src':
                
$attr['name'] = "movie";
                
$attr['value'] = $this->uri->validate($attr['value'], $config$context);
                break;
            case 
'flashvars':
                
// we're going to allow arbitrary inputs to the SWF, on
                // the reasoning that it could only hack the SWF, not us.
                
break;
            
// add other cases to support other param name/value pairs
            
default:
                
$attr['name'] = $attr['value'] = null;
        }
        return 
$attr;
    }
}





/**
 * Implements required attribute stipulation for <script>
 */
class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform
{
    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['type'])) {
            
$attr['type'] = 'text/javascript';
        }
        return 
$attr;
    }
}





// must be called POST validation

/**
 * Adds target="blank" to all outbound links.  This transform is
 * only attached if Attr.TargetBlank is TRUE.  This works regardless
 * of whether or not Attr.AllowedFrameTargets
 */
class HTMLPurifier_AttrTransform_TargetBlank extends HTMLPurifier_AttrTransform
{
    
/**
     * @type HTMLPurifier_URIParser
     */
    
private $parser;

    public function 
__construct()
    {
        
$this->parser = new HTMLPurifier_URIParser();
    }

    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (!isset(
$attr['href'])) {
            return 
$attr;
        }

        
// XXX Kind of inefficient
        
$url $this->parser->parse($attr['href']);
        
$scheme $url->getSchemeObj($config$context);

        if (
$scheme->browsable && !$url->isBenign($config$context)) {
            
$attr['target'] = '_blank';
        }
        return 
$attr;
    }
}





// must be called POST validation

/**
 * Adds rel="noopener" to any links which target a different window
 * than the current one.  This is used to prevent malicious websites
 * from silently replacing the original window, which could be used
 * to do phishing.
 * This transform is controlled by %HTML.TargetNoopener.
 */
class HTMLPurifier_AttrTransform_TargetNoopener extends HTMLPurifier_AttrTransform
{
    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (isset(
$attr['rel'])) {
            
$rels explode(' '$attr['rel']);
        } else {
            
$rels = array();
        }
        if (isset(
$attr['target']) && !in_array('noopener'$rels)) {
            
$rels[] = 'noopener';
        }
        if (!empty(
$rels) || isset($attr['rel'])) {
            
$attr['rel'] = implode(' '$rels);
        }

        return 
$attr;
    }
}




// must be called POST validation

/**
 * Adds rel="noreferrer" to any links which target a different window
 * than the current one.  This is used to prevent malicious websites
 * from silently replacing the original window, which could be used
 * to do phishing.
 * This transform is controlled by %HTML.TargetNoreferrer.
 */
class HTMLPurifier_AttrTransform_TargetNoreferrer extends HTMLPurifier_AttrTransform
{
    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        if (isset(
$attr['rel'])) {
            
$rels explode(' '$attr['rel']);
        } else {
            
$rels = array();
        }
        if (isset(
$attr['target']) && !in_array('noreferrer'$rels)) {
            
$rels[] = 'noreferrer';
        }
        if (!empty(
$rels) || isset($attr['rel'])) {
            
$attr['rel'] = implode(' '$rels);
        }

        return 
$attr;
    }
}




/**
 * Sets height/width defaults for <textarea>
 */
class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform
{
    
/**
     * @param array $attr
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function transform($attr$config$context)
    {
        
// Calculated from Firefox
        
if (!isset($attr['cols'])) {
            
$attr['cols'] = '22';
        }
        if (!isset(
$attr['rows'])) {
            
$attr['rows'] = '3';
        }
        return 
$attr;
    }
}





/**
 * Definition that uses different definitions depending on context.
 *
 * The del and ins tags are notable because they allow different types of
 * elements depending on whether or not they're in a block or inline context.
 * Chameleon allows this behavior to happen by using two different
 * definitions depending on context.  While this somewhat generalized,
 * it is specifically intended for those two tags.
 */
class HTMLPurifier_ChildDef_Chameleon extends HTMLPurifier_ChildDef
{

    
/**
     * Instance of the definition object to use when inline. Usually stricter.
     * @type HTMLPurifier_ChildDef_Optional
     */
    
public $inline;

    
/**
     * Instance of the definition object to use when block.
     * @type HTMLPurifier_ChildDef_Optional
     */
    
public $block;

    
/**
     * @type string
     */
    
public $type 'chameleon';

    
/**
     * @param array $inline List of elements to allow when inline.
     * @param array $block List of elements to allow when block.
     */
    
public function __construct($inline$block)
    {
        
$this->inline = new HTMLPurifier_ChildDef_Optional($inline);
        
$this->block = new HTMLPurifier_ChildDef_Optional($block);
        
$this->elements $this->block->elements;
    }

    
/**
     * @param HTMLPurifier_Node[] $children
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool
     */
    
public function validateChildren($children$config$context)
    {
        if (
$context->get('IsInline') === false) {
            return 
$this->block->validateChildren(
                
$children,
                
$config,
                
$context
            
);
        } else {
            return 
$this->inline->validateChildren(
                
$children,
                
$config,
                
$context
            
);
        }
    }
}





/**
 * Custom validation class, accepts DTD child definitions
 *
 * @warning Currently this class is an all or nothing proposition, that is,
 *          it will only give a bool return value.
 */
class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef
{
    
/**
     * @type string
     */
    
public $type 'custom';

    
/**
     * @type bool
     */
    
public $allow_empty false;

    
/**
     * Allowed child pattern as defined by the DTD.
     * @type string
     */
    
public $dtd_regex;

    
/**
     * PCRE regex derived from $dtd_regex.
     * @type string
     */
    
private $_pcre_regex;

    
/**
     * @param $dtd_regex Allowed child pattern from the DTD
     */
    
public function __construct($dtd_regex)
    {
        
$this->dtd_regex $dtd_regex;
        
$this->_compileRegex();
    }

    
/**
     * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex)
     */
    
protected function _compileRegex()
    {
        
$raw str_replace(' '''$this->dtd_regex);
        if (
$raw{0} != '(') {
            
$raw "($raw)";
        }
        
$el '[#a-zA-Z0-9_.-]+';
        
$reg $raw;

        
// COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M
        // DOING! Seriously: if there's problems, please report them.

        // collect all elements into the $elements array
        
preg_match_all("/$el/"$reg$matches);
        foreach (
$matches[0] as $match) {
            
$this->elements[$match] = true;
        }

        
// setup all elements as parentheticals with leading commas
        
$reg preg_replace("/$el/"'(,\0)'$reg);

        
// remove commas when they were not solicited
        
$reg preg_replace("/([^,(|](+),/"'\1'$reg);

        
// remove all non-paranthetical commas: they are handled by first regex
        
$reg preg_replace("/,(/"'('$reg);

        
$this->_pcre_regex $reg;
    }

    
/**
     * @param HTMLPurifier_Node[] $children
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return bool
     */
    
public function validateChildren($children$config$context)
    {
        
$list_of_children '';
        
$nesting 0// depth into the nest
        
foreach ($children as $node) {
            if (!empty(
$node->is_whitespace)) {
                continue;
            }
            
$list_of_children .= $node->name ',';
        }
        
// add leading comma to deal with stray comma declarations
        
$list_of_children ',' rtrim($list_of_children',');
        
$okay =
            
preg_match(
                
'/^,?' $this->_pcre_regex '$/',
                
$list_of_children
            
);
        return (bool)
$okay;
    }
}





/**
 * Definition that disallows all elements.
 * @warning validateChildren() in this class is actually never called, because
 *          empty elements are corrected in HTMLPurifier_Strategy_MakeWellFormed
 *          before child definitions are parsed in earnest by
 *          HTMLPurifier_Strategy_FixNesting.
 */
class HTMLPurifier_ChildDef_Empty extends HTMLPurifier_ChildDef
{
    
/**
     * @type bool
     */
    
public $allow_empty true;

    
/**
     * @type string
     */
    
public $type 'empty';

    public function 
__construct()
    {
    }

    
/**
     * @param HTMLPurifier_Node[] $children
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function validateChildren($children$config$context)
    {
        return array();
    }
}





/**
 * Definition for list containers ul and ol.
 *
 * What does this do?  The big thing is to handle ol/ul at the top
 * level of list nodes, which should be handled specially by /folding/
 * them into the previous list node.  We generally shouldn't ever
 * see other disallowed elements, because the autoclose behavior
 * in MakeWellFormed handles it.
 */
class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef
{
    
/**
     * @type string
     */
    
public $type 'list';
    
/**
     * @type array
     */
    // lying a little bit, so that we can handle ul and ol ourselves
    // XXX: This whole business with 'wrap' is all a bit unsatisfactory
    
public $elements = array('li' => true'ul' => true'ol' => true);

    
/**
     * @param array $children
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function validateChildren($children$config$context)
    {
        
// Flag for subclasses
        
$this->whitespace false;

        
// if there are no tokens, delete parent node
        
if (empty($children)) {
            return 
false;
        }

        
// if li is not allowed, delete parent node
        
if (!isset($config->getHTMLDefinition()->info['li'])) {
            
trigger_error("Cannot allow ul/ol without allowing li"E_USER_WARNING);
            return 
false;
        }

        
// the new set of children
        
$result = array();

        
// a little sanity check to make sure it's not ALL whitespace
        
$all_whitespace true;

        
$current_li false;

        foreach (
$children as $node) {
            if (!empty(
$node->is_whitespace)) {
                
$result[] = $node;
                continue;
            }
            
$all_whitespace false// phew, we're not talking about whitespace

            
if ($node->name === 'li') {
                
// good
                
$current_li $node;
                
$result[] = $node;
            } else {
                
// we want to tuck this into the previous li
                // Invariant: we expect the node to be ol/ul
                // ToDo: Make this more robust in the case of not ol/ul
                // by distinguishing between existing li and li created
                // to handle non-list elements; non-list elements should
                // not be appended to an existing li; only li created
                // for non-list. This distinction is not currently made.
                
if ($current_li === false) {
                    
$current_li = new HTMLPurifier_Node_Element('li');
                    
$result[] = $current_li;
                }
                
$current_li->children[] = $node;
                
$current_li->empty false// XXX fascinating! Check for this error elsewhere ToDo
            
}
        }
        if (empty(
$result)) {
            return 
false;
        }
        if (
$all_whitespace) {
            return 
false;
        }
        return 
$result;
    }
}





/**
 * Definition that allows a set of elements, but disallows empty children.
 */
class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef
{
    
/**
     * Lookup table of allowed elements.
     * @type array
     */
    
public $elements = array();

    
/**
     * Whether or not the last passed node was all whitespace.
     * @type bool
     */
    
protected $whitespace false;

    
/**
     * @param array|string $elements List of allowed element names (lowercase).
     */
    
public function __construct($elements)
    {
        if (
is_string($elements)) {
            
$elements str_replace(' '''$elements);
            
$elements explode('|'$elements);
        }
        
$keys array_keys($elements);
        if (
$keys == array_keys($keys)) {
            
$elements array_flip($elements);
            foreach (
$elements as $i => $x) {
                
$elements[$i] = true;
                if (empty(
$i)) {
                    unset(
$elements[$i]);
                } 
// remove blank
            
}
        }
        
$this->elements $elements;
    }

    
/**
     * @type bool
     */
    
public $allow_empty false;

    
/**
     * @type string
     */
    
public $type 'required';

    
/**
     * @param array $children
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function validateChildren($children$config$context)
    {
        
// Flag for subclasses
        
$this->whitespace false;

        
// if there are no tokens, delete parent node
        
if (empty($children)) {
            return 
false;
        }

        
// the new set of children
        
$result = array();

        
// whether or not parsed character data is allowed
        // this controls whether or not we silently drop a tag
        // or generate escaped HTML from it
        
$pcdata_allowed = isset($this->elements['#PCDATA']);

        
// a little sanity check to make sure it's not ALL whitespace
        
$all_whitespace true;

        
$stack array_reverse($children);
        while (!empty(
$stack)) {
            
$node array_pop($stack);
            if (!empty(
$node->is_whitespace)) {
                
$result[] = $node;
                continue;
            }
            
$all_whitespace false// phew, we're not talking about whitespace

            
if (!isset($this->elements[$node->name])) {
                
// special case text
                // XXX One of these ought to be redundant or something
                
if ($pcdata_allowed && $node instanceof HTMLPurifier_Node_Text) {
                    
$result[] = $node;
                    continue;
                }
                
// spill the child contents in
                // ToDo: Make configurable
                
if ($node instanceof HTMLPurifier_Node_Element) {
                    for (
$i count($node->children) - 1$i >= 0$i--) {
                        
$stack[] = $node->children[$i];
                    }
                    continue;
                }
                continue;
            }
            
$result[] = $node;
        }
        if (empty(
$result)) {
            return 
false;
        }
        if (
$all_whitespace) {
            
$this->whitespace true;
            return 
false;
        }
        return 
$result;
    }
}





/**
 * Definition that allows a set of elements, and allows no children.
 * @note This is a hack to reuse code from HTMLPurifier_ChildDef_Required,
 *       really, one shouldn't inherit from the other.  Only altered behavior
 *       is to overload a returned false with an array.  Thus, it will never
 *       return false.
 */
class HTMLPurifier_ChildDef_Optional extends HTMLPurifier_ChildDef_Required
{
    
/**
     * @type bool
     */
    
public $allow_empty true;

    
/**
     * @type string
     */
    
public $type 'optional';

    
/**
     * @param array $children
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function validateChildren($children$config$context)
    {
        
$result parent::validateChildren($children$config$context);
        
// we assume that $children is not modified
        
if ($result === false) {
            if (empty(
$children)) {
                return 
true;
            } elseif (
$this->whitespace) {
                return 
$children;
            } else {
                return array();
            }
        }
        return 
$result;
    }
}





/**
 * Takes the contents of blockquote when in strict and reformats for validation.
 */
class HTMLPurifier_ChildDef_StrictBlockquote extends HTMLPurifier_ChildDef_Required
{
    
/**
     * @type array
     */
    
protected $real_elements;

    
/**
     * @type array
     */
    
protected $fake_elements;

    
/**
     * @type bool
     */
    
public $allow_empty true;

    
/**
     * @type string
     */
    
public $type 'strictblockquote';

    
/**
     * @type bool
     */
    
protected $init false;

    
/**
     * @param HTMLPurifier_Config $config
     * @return array
     * @note We don't want MakeWellFormed to auto-close inline elements since
     *       they might be allowed.
     */
    
public function getAllowedElements($config)
    {
        
$this->init($config);
        return 
$this->fake_elements;
    }

    
/**
     * @param array $children
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function validateChildren($children$config$context)
    {
        
$this->init($config);

        
// trick the parent class into thinking it allows more
        
$this->elements $this->fake_elements;
        
$result parent::validateChildren($children$config$context);
        
$this->elements $this->real_elements;

        if (
$result === false) {
            return array();
        }
        if (
$result === true) {
            
$result $children;
        }

        
$def $config->getHTMLDefinition();
        
$block_wrap_name $def->info_block_wrapper;
        
$block_wrap false;
        
$ret = array();

        foreach (
$result as $node) {
            if (
$block_wrap === false) {
                if ((
$node instanceof HTMLPurifier_Node_Text && !$node->is_whitespace) ||
                    (
$node instanceof HTMLPurifier_Node_Element && !isset($this->elements[$node->name]))) {
                        
$block_wrap = new HTMLPurifier_Node_Element($def->info_block_wrapper);
                        
$ret[] = $block_wrap;
                }
            } else {
                if (
$node instanceof HTMLPurifier_Node_Element && isset($this->elements[$node->name])) {
                    
$block_wrap false;

                }
            }
            if (
$block_wrap) {
                
$block_wrap->children[] = $node;
            } else {
                
$ret[] = $node;
            }
        }
        return 
$ret;
    }

    
/**
     * @param HTMLPurifier_Config $config
     */
    
private function init($config)
    {
        if (!
$this->init) {
            
$def $config->getHTMLDefinition();
            
// allow all inline elements
            
$this->real_elements $this->elements;
            
$this->fake_elements $def->info_content_sets['Flow'];
            
$this->fake_elements['#PCDATA'] = true;
            
$this->init true;
        }
    }
}





/**
 * Definition for tables.  The general idea is to extract out all of the
 * essential bits, and then reconstruct it later.
 *
 * This is a bit confusing, because the DTDs and the W3C
 * validators seem to disagree on the appropriate definition. The
 * DTD claims:
 *
 *      (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+)
 *
 * But actually, the HTML4 spec then has this to say:
 *
 *      The TBODY start tag is always required except when the table
 *      contains only one table body and no table head or foot sections.
 *      The TBODY end tag may always be safely omitted.
 *
 * So the DTD is kind of wrong.  The validator is, unfortunately, kind
 * of on crack.
 *
 * The definition changed again in XHTML1.1; and in my opinion, this
 * formulation makes the most sense.
 *
 *      caption?, ( col* | colgroup* ), (( thead?, tfoot?, tbody+ ) | ( tr+ ))
 *
 * Essentially, we have two modes: thead/tfoot/tbody mode, and tr mode.
 * If we encounter a thead, tfoot or tbody, we are placed in the former
 * mode, and we *must* wrap any stray tr segments with a tbody. But if
 * we don't run into any of them, just have tr tags is OK.
 */
class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
{
    
/**
     * @type bool
     */
    
public $allow_empty false;

    
/**
     * @type string
     */
    
public $type 'table';

    
/**
     * @type array
     */
    
public $elements = array(
        
'tr' => true,
        
'tbody' => true,
        
'thead' => true,
        
'tfoot' => true,
        
'caption' => true,
        
'colgroup' => true,
        
'col' => true
    
);

    public function 
__construct()
    {
    }

    
/**
     * @param array $children
     * @param HTMLPurifier_Config $config
     * @param HTMLPurifier_Context $context
     * @return array
     */
    
public function validateChildren($children$config$context)
    {
        if (empty(
$children)) {
            return 
false;
        }

        
// only one of these elements is allowed in a table
        
$caption false;
        
$thead false;
        
$tfoot false;

        
// whitespace
        
$initial_ws = array();
        
$after_caption_ws = array();
        
$after_thead_ws = array();
        
$after_tfoot_ws = array();

        
// as many of these as you want
        
$cols = array();
        
$content = array();

        
$tbody_mode false// if true, then we need to wrap any stray
                             // <tr>s with a <tbody>.

        
$ws_accum =& $initial_ws;

        foreach (
$children as $node) {
            if (
$node instanceof HTMLPurifier_Node_Comment) {
                
$ws_accum[] = $node;
                continue;
            }
            switch (
$node->name) {
            case 
'tbody':
                
$tbody_mode true;
                
// fall through
            
case 'tr':
                
$content[] = $node;
                
$ws_accum =& $content;
                break;
            case 
'caption':
                
// there can only be one caption!
                
if ($caption !== false)  break;
                
$caption $node;
                
$ws_accum =& $after_caption_ws;
                break;
            case 
'thead':
                
$tbody_mode true;
                
// XXX This breaks rendering properties with
                // Firefox, which never floats a <thead> to
                // the top. Ever. (Our scheme will float the
                // first <thead> to the top.)  So maybe
                // <thead>s that are not first should be
                // turned into <tbody>? Very tricky, indeed.
                
if ($thead === false) {
                    
$thead $node;
                    
$ws_accum =& $after_thead_ws;
                } else {
                    
// Oops, there's a second one! What
                    // should we do?  Current behavior is to
                    // transmutate the first and last entries into
                    // tbody tags, and then put into content.
                    // Maybe a better idea is to *attach
                    // it* to the existing thead or tfoot?
                    // We don't do this, because Firefox
                    // doesn't float an extra tfoot to the
                    // bottom like it does for the first one.
                    
$node->name 'tbody';
                    
$content[] = $node;
                    
$ws_accum =& $content;
                }
                break;
            case 
'tfoot':
                
// see above for some aveats
                
$tbody_mode true;
                if (
$tfoot === false) {
                    
$tfoot $node;
                    
$ws_accum =& $after_tfoot_ws;
                } else {
                    
$node->name 'tbody';
                    
$content[] = $node;
                    
$ws_accum =& $content;
                }
                break;
            case 
'colgroup':
            case 
'col':
                
$cols[] = $node;
                
$ws_accum =& $cols;
                break;
            case 
'#PCDATA':
                
// How is whitespace handled? We treat is as sticky to
                // the *end* of the previous element. So all of the
                // nonsense we have worked on is to keep things
                // together.
                
if (!empty($node->is_whitespace)) {
                    
$ws_accum[] = $node;
                }
                break;
            }
        }

        if (empty(
$content)) {
            return 
false;
        }

        
$ret $initial_ws;
        if (
$caption !== false) {
            
$ret[] = $caption;
            
$ret array_merge($ret$after_caption_ws);
        }
        if (
$cols !== false) {
            
$ret array_merge($ret$cols);
        }
        if (
$thead !== false) {
            
$ret[] = $thead;
            
$ret array_merge($ret$after_thead_ws);
        }
        if (
$tfoot !== false) {
            
$ret[] = $tfoot;
            
$ret array_merge($ret$after_tfoot_ws);
        }

        if (
$tbody_mode) {
            
// we have to shuffle tr into tbody
            
$current_tr_tbody null;

            foreach(
$content as $node) {
                switch (
$node->name) {
                case 
'tbody':
                    
$current_tr_tbody null;
                    
$ret[] = $node;
                    break;
                case 
'tr':
                    if (
$current_tr_tbody === null) {
                        
$current_tr_tbody = new HTMLPurifier_Node_Element('tbody');
                        
$ret[] = $current_tr_tbody;
                    }
                    
$current_tr_tbody->children[] = $node;
                    break;
                case 
'#PCDATA':
                    
//assert($node->is_whitespace);
                    
if ($current_tr_tbody === null) {
                        
$ret[] = $node;
                    } else {
                        
$current_tr_tbody->children[] = $node;
                    }
                    break;
                }
            }
        } else {
            
$ret array_merge($ret$content);
        }

        return 
$ret;

    }
}





class 
HTMLPurifier_DefinitionCache_Decorator extends HTMLPurifier_DefinitionCache
{

    
/**
     * Cache object we are decorating
     * @type HTMLPurifier_DefinitionCache
     */
    
public $cache;

    
/**
     * The name of the decorator
     * @var string
     */
    
public $name;

    public function 
__construct()
    {
    }

    
/**
     * Lazy decorator function
     * @param HTMLPurifier_DefinitionCache $cache Reference to cache object to decorate
     * @return HTMLPurifier_DefinitionCache_Decorator
     */
    
public function decorate(&$cache)
    {
        
$decorator $this->copy();
        
// reference is necessary for mocks in PHP 4
        
$decorator->cache =& $cache;
        
$decorator->type $cache->type;
        return 
$decorator;
    }

    
/**
     * Cross-compatible clone substitute
     * @return HTMLPurifier_DefinitionCache_Decorator
     */
    
public function copy()
    {
        return new 
HTMLPurifier_DefinitionCache_Decorator();
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function add($def$config)
    {
        return 
$this->cache->add($def$config);
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function set($def$config)
    {
        return 
$this->cache->set($def$config);
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function replace($def$config)
    {
        return 
$this->cache->replace($def$config);
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function get($config)
    {
        return 
$this->cache->get($config);
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function remove($config)
    {
        return 
$this->cache->remove($config);
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function flush($config)
    {
        return 
$this->cache->flush($config);
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function cleanup($config)
    {
        return 
$this->cache->cleanup($config);
    }
}





/**
 * Null cache object to use when no caching is on.
 */
class HTMLPurifier_DefinitionCache_Null extends HTMLPurifier_DefinitionCache
{

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function add($def$config)
    {
        return 
false;
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function set($def$config)
    {
        return 
false;
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function replace($def$config)
    {
        return 
false;
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function remove($config)
    {
        return 
false;
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function get($config)
    {
        return 
false;
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function flush($config)
    {
        return 
false;
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function cleanup($config)
    {
        return 
false;
    }
}





class 
HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCache
{

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return int|bool
     */
    
public function add($def$config)
    {
        if (!
$this->checkDefType($def)) {
            return;
        }
        
$file $this->generateFilePath($config);
        if (
file_exists($file)) {
            return 
false;
        }
        if (!
$this->_prepareDir($config)) {
            return 
false;
        }
        return 
$this->_write($fileserialize($def), $config);
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return int|bool
     */
    
public function set($def$config)
    {
        if (!
$this->checkDefType($def)) {
            return;
        }
        
$file $this->generateFilePath($config);
        if (!
$this->_prepareDir($config)) {
            return 
false;
        }
        return 
$this->_write($fileserialize($def), $config);
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return int|bool
     */
    
public function replace($def$config)
    {
        if (!
$this->checkDefType($def)) {
            return;
        }
        
$file $this->generateFilePath($config);
        if (!
file_exists($file)) {
            return 
false;
        }
        if (!
$this->_prepareDir($config)) {
            return 
false;
        }
        return 
$this->_write($fileserialize($def), $config);
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return bool|HTMLPurifier_Config
     */
    
public function get($config)
    {
        
$file $this->generateFilePath($config);
        if (!
file_exists($file)) {
            return 
false;
        }
        return 
unserialize(file_get_contents($file));
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function remove($config)
    {
        
$file $this->generateFilePath($config);
        if (!
file_exists($file)) {
            return 
false;
        }
        return 
unlink($file);
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function flush($config)
    {
        if (!
$this->_prepareDir($config)) {
            return 
false;
        }
        
$dir $this->generateDirectoryPath($config);
        
$dh opendir($dir);
        
// Apparently, on some versions of PHP, readdir will return
        // an empty string if you pass an invalid argument to readdir.
        // So you need this test.  See #49.
        
if (false === $dh) {
            return 
false;
        }
        while (
false !== ($filename readdir($dh))) {
            if (empty(
$filename)) {
                continue;
            }
            if (
$filename[0] === '.') {
                continue;
            }
            
unlink($dir '/' $filename);
        }
        
closedir($dh);
        return 
true;
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return bool
     */
    
public function cleanup($config)
    {
        if (!
$this->_prepareDir($config)) {
            return 
false;
        }
        
$dir $this->generateDirectoryPath($config);
        
$dh opendir($dir);
        
// See #49 (and above).
        
if (false === $dh) {
            return 
false;
        }
        while (
false !== ($filename readdir($dh))) {
            if (empty(
$filename)) {
                continue;
            }
            if (
$filename[0] === '.') {
                continue;
            }
            
$key substr($filename0strlen($filename) - 4);
            if (
$this->isOld($key$config)) {
                
unlink($dir '/' $filename);
            }
        }
        
closedir($dh);
        return 
true;
    }

    
/**
     * Generates the file path to the serial file corresponding to
     * the configuration and definition name
     * @param HTMLPurifier_Config $config
     * @return string
     * @todo Make protected
     */
    
public function generateFilePath($config)
    {
        
$key $this->generateKey($config);
        return 
$this->generateDirectoryPath($config) . '/' $key '.ser';
    }

    
/**
     * Generates the path to the directory contain this cache's serial files
     * @param HTMLPurifier_Config $config
     * @return string
     * @note No trailing slash
     * @todo Make protected
     */
    
public function generateDirectoryPath($config)
    {
        
$base $this->generateBaseDirectoryPath($config);
        return 
$base '/' $this->type;
    }

    
/**
     * Generates path to base directory that contains all definition type
     * serials
     * @param HTMLPurifier_Config $config
     * @return mixed|string
     * @todo Make protected
     */
    
public function generateBaseDirectoryPath($config)
    {
        
$base $config->get('Cache.SerializerPath');
        
$base is_null($base) ? HTMLPURIFIER_PREFIX '/HTMLPurifier/DefinitionCache/Serializer' $base;
        return 
$base;
    }

    
/**
     * Convenience wrapper function for file_put_contents
     * @param string $file File name to write to
     * @param string $data Data to write into file
     * @param HTMLPurifier_Config $config
     * @return int|bool Number of bytes written if success, or false if failure.
     */
    
private function _write($file$data$config)
    {
        
$result file_put_contents($file$data);
        if (
$result !== false) {
            
// set permissions of the new file (no execute)
            
$chmod $config->get('Cache.SerializerPermissions');
            if (
$chmod !== null) {
                
chmod($file$chmod 0666);
            }
        }
        return 
$result;
    }

    
/**
     * Prepares the directory that this type stores the serials in
     * @param HTMLPurifier_Config $config
     * @return bool True if successful
     */
    
private function _prepareDir($config)
    {
        
$directory $this->generateDirectoryPath($config);
        
$chmod $config->get('Cache.SerializerPermissions');
        if (
$chmod === null) {
            
// TODO: This races
            
if (is_dir($directory)) return true;
            return 
mkdir($directory);
        }
        if (!
is_dir($directory)) {
            
$base $this->generateBaseDirectoryPath($config);
            if (!
is_dir($base)) {
                
trigger_error(
                    
'Base directory ' $base ' does not exist,
                    please create or change using %Cache.SerializerPath'
,
                    
E_USER_WARNING
                
);
                return 
false;
            } elseif (!
$this->_testPermissions($base$chmod)) {
                return 
false;
            }
            if (!
mkdir($directory$chmod)) {
                
trigger_error(
                    
'Could not create directory ' $directory '',
                    
E_USER_WARNING
                
);
                return 
false;
            }
            if (!
$this->_testPermissions($directory$chmod)) {
                return 
false;
            }
        } elseif (!
$this->_testPermissions($directory$chmod)) {
            return 
false;
        }
        return 
true;
    }

    
/**
     * Tests permissions on a directory and throws out friendly
     * error messages and attempts to chmod it itself if possible
     * @param string $dir Directory path
     * @param int $chmod Permissions
     * @return bool True if directory is writable
     */
    
private function _testPermissions($dir$chmod)
    {
        
// early abort, if it is writable, everything is hunky-dory
        
if (is_writable($dir)) {
            return 
true;
        }
        if (!
is_dir($dir)) {
            
// generally, you'll want to handle this beforehand
            // so a more specific error message can be given
            
trigger_error(
                
'Directory ' $dir ' does not exist',
                
E_USER_WARNING
            
);
            return 
false;
        }
        if (
function_exists('posix_getuid') && $chmod !== null) {
            
// POSIX system, we can give more specific advice
            
if (fileowner($dir) === posix_getuid()) {
                
// we can chmod it ourselves
                
$chmod $chmod 0700;
                if (
chmod($dir$chmod)) {
                    return 
true;
                }
            } elseif (
filegroup($dir) === posix_getgid()) {
                
$chmod $chmod 0070;
            } else {
                
// PHP's probably running as nobody, so we'll
                // need to give global permissions
                
$chmod $chmod 0777;
            }
            
trigger_error(
                
'Directory ' $dir ' not writable, ' .
                
'please chmod to ' decoct($chmod),
                
E_USER_WARNING
            
);
        } else {
            
// generic error message
            
trigger_error(
                
'Directory ' $dir ' not writable, ' .
                
'please alter file permissions',
                
E_USER_WARNING
            
);
        }
        return 
false;
    }
}





/**
 * Definition cache decorator class that cleans up the cache
 * whenever there is a cache miss.
 */
class HTMLPurifier_DefinitionCache_Decorator_Cleanup extends HTMLPurifier_DefinitionCache_Decorator
{
    
/**
     * @type string
     */
    
public $name 'Cleanup';

    
/**
     * @return HTMLPurifier_DefinitionCache_Decorator_Cleanup
     */
    
public function copy()
    {
        return new 
HTMLPurifier_DefinitionCache_Decorator_Cleanup();
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function add($def$config)
    {
        
$status parent::add($def$config);
        if (!
$status) {
            
parent::cleanup($config);
        }
        return 
$status;
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function set($def$config)
    {
        
$status parent::set($def$config);
        if (!
$status) {
            
parent::cleanup($config);
        }
        return 
$status;
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function replace($def$config)
    {
        
$status parent::replace($def$config);
        if (!
$status) {
            
parent::cleanup($config);
        }
        return 
$status;
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function get($config)
    {
        
$ret parent::get($config);
        if (!
$ret) {
            
parent::cleanup($config);
        }
        return 
$ret;
    }
}





/**
 * Definition cache decorator class that saves all cache retrievals
 * to PHP's memory; good for unit tests or circumstances where
 * there are lots of configuration objects floating around.
 */
class HTMLPurifier_DefinitionCache_Decorator_Memory extends HTMLPurifier_DefinitionCache_Decorator
{
    
/**
     * @type array
     */
    
protected $definitions;

    
/**
     * @type string
     */
    
public $name 'Memory';

    
/**
     * @return HTMLPurifier_DefinitionCache_Decorator_Memory
     */
    
public function copy()
    {
        return new 
HTMLPurifier_DefinitionCache_Decorator_Memory();
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function add($def$config)
    {
        
$status parent::add($def$config);
        if (
$status) {
            
$this->definitions[$this->generateKey($config)] = $def;
        }
        return 
$status;
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function set($def$config)
    {
        
$status parent::set($def$config);
        if (
$status) {
            
$this->definitions[$this->generateKey($config)] = $def;
        }
        return 
$status;
    }

    
/**
     * @param HTMLPurifier_Definition $def
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function replace($def$config)
    {
        
$status parent::replace($def$config);
        if (
$status) {
            
$this->definitions[$this->generateKey($config)] = $def;
        }
        return 
$status;
    }

    
/**
     * @param HTMLPurifier_Config $config
     * @return mixed
     */
    
public function get($config)
    {
        
$key $this->generateKey($config);
        if (isset(
$this->definitions[$key])) {
            return 
$this->definitions[$key];
        }
        
$this->definitions[$key] = parent::get($config);
        return 
$this->definitions[$key];
    }
}





/**
 * XHTML 1.1 Bi-directional Text Module, defines elements that
 * declare directionality of content. Text Extension Module.
 */
class HTMLPurifier_HTMLModule_Bdo extends HTMLPurifier_HTMLModule
{

    
/**
     * @type string
     */
    
public $name 'Bdo';

    
/**
     * @type array
     */
    
public $attr_collections = array(
        
'I18N' => array('dir' => false)
    );

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$bdo $this->addElement(
            
'bdo',
            
'Inline',
            
'Inline',
            array(
'Core''Lang'),
            array(
                
'dir' => 'Enum#ltr,rtl'// required
                // The Abstract Module specification has the attribute
                // inclusions wrong for bdo: bdo allows Lang
            
)
        );
        
$bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir();

        
$this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl';
    }
}





class 
HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'CommonAttributes';

    
/**
     * @type array
     */
    
public $attr_collections = array(
        
'Core' => array(
            
=> array('Style'),
            
// 'xml:space' => false,
            
'class' => 'Class',
            
'id' => 'ID',
            
'title' => 'CDATA',
        ),
        
'Lang' => array(),
        
'I18N' => array(
            
=> array('Lang'), // proprietary, for xml:lang/lang
        
),
        
'Common' => array(
            
=> array('Core''I18N')
        )
    );
}





/**
 * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
 * Module.
 */
class HTMLPurifier_HTMLModule_Edit extends HTMLPurifier_HTMLModule
{

    
/**
     * @type string
     */
    
public $name 'Edit';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$contents 'Chameleon: #PCDATA | Inline ! #PCDATA | Flow';
        
$attr = array(
            
'cite' => 'URI',
            
// 'datetime' => 'Datetime', // not implemented
        
);
        
$this->addElement('del''Inline'$contents'Common'$attr);
        
$this->addElement('ins''Inline'$contents'Common'$attr);
    }

    
// HTML 4.01 specifies that ins/del must not contain block
    // elements when used in an inline context, chameleon is
    // a complicated workaround to acheive this effect

    // Inline context ! Block context (exclamation mark is
    // separator, see getChildDef for parsing)

    /**
     * @type bool
     */
    
public $defines_child_def true;

    
/**
     * @param HTMLPurifier_ElementDef $def
     * @return HTMLPurifier_ChildDef_Chameleon
     */
    
public function getChildDef($def)
    {
        if (
$def->content_model_type != 'chameleon') {
            return 
false;
        }
        
$value explode('!'$def->content_model);
        return new 
HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]);
    }
}





/**
 * XHTML 1.1 Forms module, defines all form-related elements found in HTML 4.
 */
class HTMLPurifier_HTMLModule_Forms extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Forms';

    
/**
     * @type bool
     */
    
public $safe false;

    
/**
     * @type array
     */
    
public $content_sets = array(
        
'Block' => 'Form',
        
'Inline' => 'Formctrl',
    );

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$form $this->addElement(
            
'form',
            
'Form',
            
'Required: Heading | List | Block | fieldset',
            
'Common',
            array(
                
'accept' => 'ContentTypes',
                
'accept-charset' => 'Charsets',
                
'action*' => 'URI',
                
'method' => 'Enum#get,post',
                
// really ContentType, but these two are the only ones used today
                
'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data',
            )
        );
        
$form->excludes = array('form' => true);

        
$input $this->addElement(
            
'input',
            
'Formctrl',
            
'Empty',
            
'Common',
            array(
                
'accept' => 'ContentTypes',
                
'accesskey' => 'Character',
                
'alt' => 'Text',
                
'checked' => 'Bool#checked',
                
'disabled' => 'Bool#disabled',
                
'maxlength' => 'Number',
                
'name' => 'CDATA',
                
'readonly' => 'Bool#readonly',
                
'size' => 'Number',
                
'src' => 'URI#embedded',
                
'tabindex' => 'Number',
                
'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image',
                
'value' => 'CDATA',
            )
        );
        
$input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input();

        
$this->addElement(
            
'select',
            
'Formctrl',
            
'Required: optgroup | option',
            
'Common',
            array(
                
'disabled' => 'Bool#disabled',
                
'multiple' => 'Bool#multiple',
                
'name' => 'CDATA',
                
'size' => 'Number',
                
'tabindex' => 'Number',
            )
        );

        
$this->addElement(
            
'option',
            
false,
            
'Optional: #PCDATA',
            
'Common',
            array(
                
'disabled' => 'Bool#disabled',
                
'label' => 'Text',
                
'selected' => 'Bool#selected',
                
'value' => 'CDATA',
            )
        );
        
// It's illegal for there to be more than one selected, but not
        // be multiple. Also, no selected means undefined behavior. This might
        // be difficult to implement; perhaps an injector, or a context variable.

        
$textarea $this->addElement(
            
'textarea',
            
'Formctrl',
            
'Optional: #PCDATA',
            
'Common',
            array(
                
'accesskey' => 'Character',
                
'cols*' => 'Number',
                
'disabled' => 'Bool#disabled',
                
'name' => 'CDATA',
                
'readonly' => 'Bool#readonly',
                
'rows*' => 'Number',
                
'tabindex' => 'Number',
            )
        );
        
$textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea();

        
$button $this->addElement(
            
'button',
            
'Formctrl',
            
'Optional: #PCDATA | Heading | List | Block | Inline',
            
'Common',
            array(
                
'accesskey' => 'Character',
                
'disabled' => 'Bool#disabled',
                
'name' => 'CDATA',
                
'tabindex' => 'Number',
                
'type' => 'Enum#button,submit,reset',
                
'value' => 'CDATA',
            )
        );

        
// For exclusions, ideally we'd specify content sets, not literal elements
        
$button->excludes $this->makeLookup(
            
'form',
            
'fieldset'// Form
            
'input',
            
'select',
            
'textarea',
            
'label',
            
'button'// Formctrl
            
'a'// as per HTML 4.01 spec, this is omitted by modularization
            
'isindex',
            
'iframe' // legacy items
        
);

        
// Extra exclusion: img usemap="" is not permitted within this element.
        // We'll omit this for now, since we don't have any good way of
        // indicating it yet.

        // This is HIGHLY user-unfriendly; we need a custom child-def for this
        
$this->addElement('fieldset''Form''Custom: (#WS?,legend,(Flow|#PCDATA)*)''Common');

        
$label $this->addElement(
            
'label',
            
'Formctrl',
            
'Optional: #PCDATA | Inline',
            
'Common',
            array(
                
'accesskey' => 'Character',
                
// 'for' => 'IDREF', // IDREF not implemented, cannot allow
            
)
        );
        
$label->excludes = array('label' => true);

        
$this->addElement(
            
'legend',
            
false,
            
'Optional: #PCDATA | Inline',
            
'Common',
            array(
                
'accesskey' => 'Character',
            )
        );

        
$this->addElement(
            
'optgroup',
            
false,
            
'Required: option',
            
'Common',
            array(
                
'disabled' => 'Bool#disabled',
                
'label*' => 'Text',
            )
        );
        
// Don't forget an injector for <isindex>. This one's a little complex
        // because it maps to multiple elements.
    
}
}





/**
 * XHTML 1.1 Hypertext Module, defines hypertext links. Core Module.
 */
class HTMLPurifier_HTMLModule_Hypertext extends HTMLPurifier_HTMLModule
{

    
/**
     * @type string
     */
    
public $name 'Hypertext';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$a $this->addElement(
            
'a',
            
'Inline',
            
'Inline',
            
'Common',
            array(
                
// 'accesskey' => 'Character',
                // 'charset' => 'Charset',
                
'href' => 'URI',
                
// 'hreflang' => 'LanguageCode',
                
'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'),
                
'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'),
                
// 'tabindex' => 'Number',
                // 'type' => 'ContentType',
            
)
        );
        
$a->formatting true;
        
$a->excludes = array('a' => true);
    }
}





/**
 * XHTML 1.1 Iframe Module provides inline frames.
 *
 * @note This module is not considered safe unless an Iframe
 * whitelisting mechanism is specified.  Currently, the only
 * such mechanism is %URL.SafeIframeRegexp
 */
class HTMLPurifier_HTMLModule_Iframe extends HTMLPurifier_HTMLModule
{

    
/**
     * @type string
     */
    
public $name 'Iframe';

    
/**
     * @type bool
     */
    
public $safe false;

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        if (
$config->get('HTML.SafeIframe')) {
            
$this->safe true;
        }
        
$this->addElement(
            
'iframe',
            
'Inline',
            
'Flow',
            
'Common',
            array(
                
'src' => 'URI#embedded',
                
'width' => 'Length',
                
'height' => 'Length',
                
'name' => 'ID',
                
'scrolling' => 'Enum#yes,no,auto',
                
'frameborder' => 'Enum#0,1',
                
'longdesc' => 'URI',
                
'marginheight' => 'Pixels',
                
'marginwidth' => 'Pixels',
            )
        );
    }
}





/**
 * XHTML 1.1 Image Module provides basic image embedding.
 * @note There is specialized code for removing empty images in
 *       HTMLPurifier_Strategy_RemoveForeignElements
 */
class HTMLPurifier_HTMLModule_Image extends HTMLPurifier_HTMLModule
{

    
/**
     * @type string
     */
    
public $name 'Image';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$max $config->get('HTML.MaxImgLength');
        
$img $this->addElement(
            
'img',
            
'Inline',
            
'Empty',
            
'Common',
            array(
                
'alt*' => 'Text',
                
// According to the spec, it's Length, but percents can
                // be abused, so we allow only Pixels.
                
'height' => 'Pixels#' $max,
                
'width' => 'Pixels#' $max,
                
'longdesc' => 'URI',
                
'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded
            
)
        );
        if (
$max === null || $config->get('HTML.Trusted')) {
            
$img->attr['height'] =
            
$img->attr['width'] = 'Length';
        }

        
// kind of strange, but splitting things up would be inefficient
        
$img->attr_transform_pre[] =
        
$img->attr_transform_post[] =
            new 
HTMLPurifier_AttrTransform_ImgRequired();
    }
}





/**
 * XHTML 1.1 Legacy module defines elements that were previously
 * deprecated.
 *
 * @note Not all legacy elements have been implemented yet, which
 *       is a bit of a reverse problem as compared to browsers! In
 *       addition, this legacy module may implement a bit more than
 *       mandated by XHTML 1.1.
 *
 * This module can be used in combination with TransformToStrict in order
 * to transform as many deprecated elements as possible, but retain
 * questionably deprecated elements that do not have good alternatives
 * as well as transform elements that don't have an implementation.
 * See docs/ref-strictness.txt for more details.
 */

class HTMLPurifier_HTMLModule_Legacy extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Legacy';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$this->addElement(
            
'basefont',
            
'Inline',
            
'Empty',
            
null,
            array(
                
'color' => 'Color',
                
'face' => 'Text'// extremely broad, we should
                
'size' => 'Text'// tighten it
                
'id' => 'ID'
            
)
        );
        
$this->addElement('center''Block''Flow''Common');
        
$this->addElement(
            
'dir',
            
'Block',
            
'Required: li',
            
'Common',
            array(
                
'compact' => 'Bool#compact'
            
)
        );
        
$this->addElement(
            
'font',
            
'Inline',
            
'Inline',
            array(
'Core''I18N'),
            array(
                
'color' => 'Color',
                
'face' => 'Text'// extremely broad, we should
                
'size' => 'Text'// tighten it
            
)
        );
        
$this->addElement(
            
'menu',
            
'Block',
            
'Required: li',
            
'Common',
            array(
                
'compact' => 'Bool#compact'
            
)
        );

        
$s $this->addElement('s''Inline''Inline''Common');
        
$s->formatting true;

        
$strike $this->addElement('strike''Inline''Inline''Common');
        
$strike->formatting true;

        
$u $this->addElement('u''Inline''Inline''Common');
        
$u->formatting true;

        
// setup modifications to old elements

        
$align 'Enum#left,right,center,justify';

        
$address $this->addBlankElement('address');
        
$address->content_model 'Inline | #PCDATA | p';
        
$address->content_model_type 'optional';
        
$address->child false;

        
$blockquote $this->addBlankElement('blockquote');
        
$blockquote->content_model 'Flow | #PCDATA';
        
$blockquote->content_model_type 'optional';
        
$blockquote->child false;

        
$br $this->addBlankElement('br');
        
$br->attr['clear'] = 'Enum#left,all,right,none';

        
$caption $this->addBlankElement('caption');
        
$caption->attr['align'] = 'Enum#top,bottom,left,right';

        
$div $this->addBlankElement('div');
        
$div->attr['align'] = $align;

        
$dl $this->addBlankElement('dl');
        
$dl->attr['compact'] = 'Bool#compact';

        for (
$i 1$i <= 6$i++) {
            
$h $this->addBlankElement("h$i");
            
$h->attr['align'] = $align;
        }

        
$hr $this->addBlankElement('hr');
        
$hr->attr['align'] = $align;
        
$hr->attr['noshade'] = 'Bool#noshade';
        
$hr->attr['size'] = 'Pixels';
        
$hr->attr['width'] = 'Length';

        
$img $this->addBlankElement('img');
        
$img->attr['align'] = 'IAlign';
        
$img->attr['border'] = 'Pixels';
        
$img->attr['hspace'] = 'Pixels';
        
$img->attr['vspace'] = 'Pixels';

        
// figure out this integer business

        
$li $this->addBlankElement('li');
        
$li->attr['value'] = new HTMLPurifier_AttrDef_Integer();
        
$li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle';

        
$ol $this->addBlankElement('ol');
        
$ol->attr['compact'] = 'Bool#compact';
        
$ol->attr['start'] = new HTMLPurifier_AttrDef_Integer();
        
$ol->attr['type'] = 'Enum#s:1,i,I,a,A';

        
$p $this->addBlankElement('p');
        
$p->attr['align'] = $align;

        
$pre $this->addBlankElement('pre');
        
$pre->attr['width'] = 'Number';

        
// script omitted

        
$table $this->addBlankElement('table');
        
$table->attr['align'] = 'Enum#left,center,right';
        
$table->attr['bgcolor'] = 'Color';

        
$tr $this->addBlankElement('tr');
        
$tr->attr['bgcolor'] = 'Color';

        
$th $this->addBlankElement('th');
        
$th->attr['bgcolor'] = 'Color';
        
$th->attr['height'] = 'Length';
        
$th->attr['nowrap'] = 'Bool#nowrap';
        
$th->attr['width'] = 'Length';

        
$td $this->addBlankElement('td');
        
$td->attr['bgcolor'] = 'Color';
        
$td->attr['height'] = 'Length';
        
$td->attr['nowrap'] = 'Bool#nowrap';
        
$td->attr['width'] = 'Length';

        
$ul $this->addBlankElement('ul');
        
$ul->attr['compact'] = 'Bool#compact';
        
$ul->attr['type'] = 'Enum#square,disc,circle';

        
// "safe" modifications to "unsafe" elements
        // WARNING: If you want to add support for an unsafe, legacy
        // attribute, make a new TrustedLegacy module with the trusted
        // bit set appropriately

        
$form $this->addBlankElement('form');
        
$form->content_model 'Flow | #PCDATA';
        
$form->content_model_type 'optional';
        
$form->attr['target'] = 'FrameTarget';

        
$input $this->addBlankElement('input');
        
$input->attr['align'] = 'IAlign';

        
$legend $this->addBlankElement('legend');
        
$legend->attr['align'] = 'LAlign';
    }
}





/**
 * XHTML 1.1 List Module, defines list-oriented elements. Core Module.
 */
class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'List';

    
// According to the abstract schema, the List content set is a fully formed
    // one or more expr, but it invariably occurs in an optional declaration
    // so we're not going to do that subtlety. It might cause trouble
    // if a user defines "List" and expects that multiple lists are
    // allowed to be specified, but then again, that's not very intuitive.
    // Furthermore, the actual XML Schema may disagree. Regardless,
    // we don't have support for such nested expressions without using
    // the incredibly inefficient and draconic Custom ChildDef.

    /**
     * @type array
     */
    
public $content_sets = array('Flow' => 'List');

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$ol $this->addElement('ol''List', new HTMLPurifier_ChildDef_List(), 'Common');
        
$ul $this->addElement('ul''List', new HTMLPurifier_ChildDef_List(), 'Common');
        
// XXX The wrap attribute is handled by MakeWellFormed.  This is all
        // quite unsatisfactory, because we generated this
        // *specifically* for lists, and now a big chunk of the handling
        // is done properly by the List ChildDef.  So actually, we just
        // want enough information to make autoclosing work properly,
        // and then hand off the tricky stuff to the ChildDef.
        
$ol->wrap 'li';
        
$ul->wrap 'li';
        
$this->addElement('dl''List''Required: dt | dd''Common');

        
$this->addElement('li'false'Flow''Common');

        
$this->addElement('dd'false'Flow''Common');
        
$this->addElement('dt'false'Inline''Common');
    }
}





class 
HTMLPurifier_HTMLModule_Name extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Name';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$elements = array('a''applet''form''frame''iframe''img''map');
        foreach (
$elements as $name) {
            
$element $this->addBlankElement($name);
            
$element->attr['name'] = 'CDATA';
            if (!
$config->get('HTML.Attr.Name.UseCDATA')) {
                
$element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync();
            }
        }
    }
}





/**
 * Module adds the nofollow attribute transformation to a tags.  It
 * is enabled by HTML.Nofollow
 */
class HTMLPurifier_HTMLModule_Nofollow extends HTMLPurifier_HTMLModule
{

    
/**
     * @type string
     */
    
public $name 'Nofollow';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$a $this->addBlankElement('a');
        
$a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow();
    }
}





class 
HTMLPurifier_HTMLModule_NonXMLCommonAttributes extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'NonXMLCommonAttributes';

    
/**
     * @type array
     */
    
public $attr_collections = array(
        
'Lang' => array(
            
'lang' => 'LanguageCode',
        )
    );
}





/**
 * XHTML 1.1 Object Module, defines elements for generic object inclusion
 * @warning Users will commonly use <embed> to cater to legacy browsers: this
 *      module does not allow this sort of behavior
 */
class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Object';

    
/**
     * @type bool
     */
    
public $safe false;

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$this->addElement(
            
'object',
            
'Inline',
            
'Optional: #PCDATA | Flow | param',
            
'Common',
            array(
                
'archive' => 'URI',
                
'classid' => 'URI',
                
'codebase' => 'URI',
                
'codetype' => 'Text',
                
'data' => 'URI',
                
'declare' => 'Bool#declare',
                
'height' => 'Length',
                
'name' => 'CDATA',
                
'standby' => 'Text',
                
'tabindex' => 'Number',
                
'type' => 'ContentType',
                
'width' => 'Length'
            
)
        );

        
$this->addElement(
            
'param',
            
false,
            
'Empty',
            
null,
            array(
                
'id' => 'ID',
                
'name*' => 'Text',
                
'type' => 'Text',
                
'value' => 'Text',
                
'valuetype' => 'Enum#data,ref,object'
            
)
        );
    }
}





/**
 * XHTML 1.1 Presentation Module, defines simple presentation-related
 * markup. Text Extension Module.
 * @note The official XML Schema and DTD specs further divide this into
 *       two modules:
 *          - Block Presentation (hr)
 *          - Inline Presentation (b, big, i, small, sub, sup, tt)
 *       We have chosen not to heed this distinction, as content_sets
 *       provides satisfactory disambiguation.
 */
class HTMLPurifier_HTMLModule_Presentation extends HTMLPurifier_HTMLModule
{

    
/**
     * @type string
     */
    
public $name 'Presentation';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$this->addElement('hr''Block''Empty''Common');
        
$this->addElement('sub''Inline''Inline''Common');
        
$this->addElement('sup''Inline''Inline''Common');
        
$b $this->addElement('b''Inline''Inline''Common');
        
$b->formatting true;
        
$big $this->addElement('big''Inline''Inline''Common');
        
$big->formatting true;
        
$i $this->addElement('i''Inline''Inline''Common');
        
$i->formatting true;
        
$small $this->addElement('small''Inline''Inline''Common');
        
$small->formatting true;
        
$tt $this->addElement('tt''Inline''Inline''Common');
        
$tt->formatting true;
    }
}





/**
 * Module defines proprietary tags and attributes in HTML.
 * @warning If this module is enabled, standards-compliance is off!
 */
class HTMLPurifier_HTMLModule_Proprietary extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Proprietary';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$this->addElement(
            
'marquee',
            
'Inline',
            
'Flow',
            
'Common',
            array(
                
'direction' => 'Enum#left,right,up,down',
                
'behavior' => 'Enum#alternate',
                
'width' => 'Length',
                
'height' => 'Length',
                
'scrolldelay' => 'Number',
                
'scrollamount' => 'Number',
                
'loop' => 'Number',
                
'bgcolor' => 'Color',
                
'hspace' => 'Pixels',
                
'vspace' => 'Pixels',
            )
        );
    }
}





/**
 * XHTML 1.1 Ruby Annotation Module, defines elements that indicate
 * short runs of text alongside base text for annotation or pronounciation.
 */
class HTMLPurifier_HTMLModule_Ruby extends HTMLPurifier_HTMLModule
{

    
/**
     * @type string
     */
    
public $name 'Ruby';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$this->addElement(
            
'ruby',
            
'Inline',
            
'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))',
            
'Common'
        
);
        
$this->addElement('rbc'false'Required: rb''Common');
        
$this->addElement('rtc'false'Required: rt''Common');
        
$rb $this->addElement('rb'false'Inline''Common');
        
$rb->excludes = array('ruby' => true);
        
$rt $this->addElement('rt'false'Inline''Common', array('rbspan' => 'Number'));
        
$rt->excludes = array('ruby' => true);
        
$this->addElement('rp'false'Optional: #PCDATA''Common');
    }
}





/**
 * A "safe" embed module. See SafeObject. This is a proprietary element.
 */
class HTMLPurifier_HTMLModule_SafeEmbed extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'SafeEmbed';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$max $config->get('HTML.MaxImgLength');
        
$embed $this->addElement(
            
'embed',
            
'Inline',
            
'Empty',
            
'Common',
            array(
                
'src*' => 'URI#embedded',
                
'type' => 'Enum#application/x-shockwave-flash',
                
'width' => 'Pixels#' $max,
                
'height' => 'Pixels#' $max,
                
'allowscriptaccess' => 'Enum#never',
                
'allownetworking' => 'Enum#internal',
                
'flashvars' => 'Text',
                
'wmode' => 'Enum#window,transparent,opaque',
                
'name' => 'ID',
            )
        );
        
$embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed();
    }
}





/**
 * A "safe" object module. In theory, objects permitted by this module will
 * be safe, and untrusted users can be allowed to embed arbitrary flash objects
 * (maybe other types too, but only Flash is supported as of right now).
 * Highly experimental.
 */
class HTMLPurifier_HTMLModule_SafeObject extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'SafeObject';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
// These definitions are not intrinsically safe: the attribute transforms
        // are a vital part of ensuring safety.

        
$max $config->get('HTML.MaxImgLength');
        
$object $this->addElement(
            
'object',
            
'Inline',
            
'Optional: param | Flow | #PCDATA',
            
'Common',
            array(
                
// While technically not required by the spec, we're forcing
                // it to this value.
                
'type' => 'Enum#application/x-shockwave-flash',
                
'width' => 'Pixels#' $max,
                
'height' => 'Pixels#' $max,
                
'data' => 'URI#embedded',
                
'codebase' => new HTMLPurifier_AttrDef_Enum(
                    array(
                        
'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0'
                    
)
                ),
            )
        );
        
$object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject();

        
$param $this->addElement(
            
'param',
            
false,
            
'Empty',
            
false,
            array(
                
'id' => 'ID',
                
'name*' => 'Text',
                
'value' => 'Text'
            
)
        );
        
$param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam();
        
$this->info_injector[] = 'SafeObject';
    }
}





/**
 * A "safe" script module. No inline JS is allowed, and pointed to JS
 * files must match whitelist.
 */
class HTMLPurifier_HTMLModule_SafeScripting extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'SafeScripting';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
// These definitions are not intrinsically safe: the attribute transforms
        // are a vital part of ensuring safety.

        
$allowed $config->get('HTML.SafeScripting');
        
$script $this->addElement(
            
'script',
            
'Inline',
            
'Empty',
            
null,
            array(
                
// While technically not required by the spec, we're forcing
                // it to this value.
                
'type' => 'Enum#text/javascript',
                
'src*' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed))
            )
        );
        
$script->attr_transform_pre[] =
        
$script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired();
    }
}





/*

WARNING: THIS MODULE IS EXTREMELY DANGEROUS AS IT ENABLES INLINE SCRIPTING
INSIDE HTML PURIFIER DOCUMENTS. USE ONLY WITH TRUSTED USER INPUT!!!

*/

/**
 * XHTML 1.1 Scripting module, defines elements that are used to contain
 * information pertaining to executable scripts or the lack of support
 * for executable scripts.
 * @note This module does not contain inline scripting elements
 */
class HTMLPurifier_HTMLModule_Scripting extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Scripting';

    
/**
     * @type array
     */
    
public $elements = array('script''noscript');

    
/**
     * @type array
     */
    
public $content_sets = array('Block' => 'script | noscript''Inline' => 'script | noscript');

    
/**
     * @type bool
     */
    
public $safe false;

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
// TODO: create custom child-definition for noscript that
        // auto-wraps stray #PCDATA in a similar manner to
        // blockquote's custom definition (we would use it but
        // blockquote's contents are optional while noscript's contents
        // are required)

        // TODO: convert this to new syntax, main problem is getting
        // both content sets working

        // In theory, this could be safe, but I don't see any reason to
        // allow it.
        
$this->info['noscript'] = new HTMLPurifier_ElementDef();
        
$this->info['noscript']->attr = array(=> array('Common'));
        
$this->info['noscript']->content_model 'Heading | List | Block';
        
$this->info['noscript']->content_model_type 'required';

        
$this->info['script'] = new HTMLPurifier_ElementDef();
        
$this->info['script']->attr = array(
            
'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')),
            
'src' => new HTMLPurifier_AttrDef_URI(true),
            
'type' => new HTMLPurifier_AttrDef_Enum(array('text/javascript'))
        );
        
$this->info['script']->content_model '#PCDATA';
        
$this->info['script']->content_model_type 'optional';
        
$this->info['script']->attr_transform_pre[] =
        
$this->info['script']->attr_transform_post[] =
            new 
HTMLPurifier_AttrTransform_ScriptRequired();
    }
}





/**
 * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
 * Module.
 */
class HTMLPurifier_HTMLModule_StyleAttribute extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'StyleAttribute';

    
/**
     * @type array
     */
    
public $attr_collections = array(
        
// The inclusion routine differs from the Abstract Modules but
        // is in line with the DTD and XML Schemas.
        
'Style' => array('style' => false), // see constructor
        
'Core' => array(=> array('Style'))
    );

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS();
    }
}





/**
 * XHTML 1.1 Tables Module, fully defines accessible table elements.
 */
class HTMLPurifier_HTMLModule_Tables extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Tables';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$this->addElement('caption'false'Inline''Common');

        
$this->addElement(
            
'table',
            
'Block',
            new 
HTMLPurifier_ChildDef_Table(),
            
'Common',
            array(
                
'border' => 'Pixels',
                
'cellpadding' => 'Length',
                
'cellspacing' => 'Length',
                
'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border',
                
'rules' => 'Enum#none,groups,rows,cols,all',
                
'summary' => 'Text',
                
'width' => 'Length'
            
)
        );

        
// common attributes
        
$cell_align = array(
            
'align' => 'Enum#left,center,right,justify,char',
            
'charoff' => 'Length',
            
'valign' => 'Enum#top,middle,bottom,baseline',
        );

        
$cell_t array_merge(
            array(
                
'abbr' => 'Text',
                
'colspan' => 'Number',
                
'rowspan' => 'Number',
                
// Apparently, as of HTML5 this attribute only applies
                // to 'th' elements.
                
'scope' => 'Enum#row,col,rowgroup,colgroup',
            ),
            
$cell_align
        
);
        
$this->addElement('td'false'Flow''Common'$cell_t);
        
$this->addElement('th'false'Flow''Common'$cell_t);

        
$this->addElement('tr'false'Required: td | th''Common'$cell_align);

        
$cell_col array_merge(
            array(
                
'span' => 'Number',
                
'width' => 'MultiLength',
            ),
            
$cell_align
        
);
        
$this->addElement('col'false'Empty''Common'$cell_col);
        
$this->addElement('colgroup'false'Optional: col''Common'$cell_col);

        
$this->addElement('tbody'false'Required: tr''Common'$cell_align);
        
$this->addElement('thead'false'Required: tr''Common'$cell_align);
        
$this->addElement('tfoot'false'Required: tr''Common'$cell_align);
    }
}





/**
 * XHTML 1.1 Target Module, defines target attribute in link elements.
 */
class HTMLPurifier_HTMLModule_Target extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Target';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$elements = array('a');
        foreach (
$elements as $name) {
            
$e $this->addBlankElement($name);
            
$e->attr = array(
                
'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget()
            );
        }
    }
}





/**
 * Module adds the target=blank attribute transformation to a tags.  It
 * is enabled by HTML.TargetBlank
 */
class HTMLPurifier_HTMLModule_TargetBlank extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'TargetBlank';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
$a $this->addBlankElement('a');
        
$a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetBlank();
    }
}





/**
 * Module adds the target-based noopener attribute transformation to a tags.  It
 * is enabled by HTML.TargetNoopener
 */
class HTMLPurifier_HTMLModule_TargetNoopener extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'TargetNoopener';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config) {
        
$a $this->addBlankElement('a');
        
$a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoopener();
    }
}



/**
 * Module adds the target-based noreferrer attribute transformation to a tags.  It
 * is enabled by HTML.TargetNoreferrer
 */
class HTMLPurifier_HTMLModule_TargetNoreferrer extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'TargetNoreferrer';

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config) {
        
$a $this->addBlankElement('a');
        
$a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoreferrer();
    }
}



/**
 * XHTML 1.1 Text Module, defines basic text containers. Core Module.
 * @note In the normative XML Schema specification, this module
 *       is further abstracted into the following modules:
 *          - Block Phrasal (address, blockquote, pre, h1, h2, h3, h4, h5, h6)
 *          - Block Structural (div, p)
 *          - Inline Phrasal (abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var)
 *          - Inline Structural (br, span)
 *       This module, functionally, does not distinguish between these
 *       sub-modules, but the code is internally structured to reflect
 *       these distinctions.
 */
class HTMLPurifier_HTMLModule_Text extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'Text';

    
/**
     * @type array
     */
    
public $content_sets = array(
        
'Flow' => 'Heading | Block | Inline'
    
);

    
/**
     * @param HTMLPurifier_Config $config
     */
    
public function setup($config)
    {
        
// Inline Phrasal -------------------------------------------------
        
$this->addElement('abbr''Inline''Inline''Common');
        
$this->addElement('acronym''Inline''Inline''Common');
        
$this->addElement('cite''Inline''Inline''Common');
        
$this->addElement('dfn''Inline''Inline''Common');
        
$this->addElement('kbd''Inline''Inline''Common');
        
$this->addElement('q''Inline''Inline''Common', array('cite' => 'URI'));
        
$this->addElement('samp''Inline''Inline''Common');
        
$this->addElement('var''Inline''Inline''Common');

        
$em $this->addElement('em''Inline''Inline''Common');
        
$em->formatting true;

        
$strong $this->addElement('strong''Inline''Inline''Common');
        
$strong->formatting true;

        
$code $this->addElement('code''Inline''Inline''Common');
        
$code->formatting true;

        
// Inline Structural ----------------------------------------------
        
$this->addElement('span''Inline''Inline''Common');
        
$this->addElement('br''Inline''Empty''Core');

        
// Block Phrasal --------------------------------------------------
        
$this->addElement('address''Block''Inline''Common');
        
$this->addElement('blockquote''Block''Optional: Heading | Block | List''Common', array('cite' => 'URI'));
        
$pre $this->addElement('pre''Block''Inline''Common');
        
$pre->excludes $this->makeLookup(
            
'img',
            
'big',
            
'small',
            
'object',
            
'applet',
            
'font',
            
'basefont'
        
);
        
$this->addElement('h1''Heading''Inline''Common');
        
$this->addElement('h2''Heading''Inline''Common');
        
$this->addElement('h3''Heading''Inline''Common');
        
$this->addElement('h4''Heading''Inline''Common');
        
$this->addElement('h5''Heading''Inline''Common');
        
$this->addElement('h6''Heading''Inline''Common');

        
// Block Structural -----------------------------------------------
        
$p $this->addElement('p''Block''Inline''Common');
        
$p->autoclose array_flip(
            array(
"address""blockquote""center""dir""div""dl""fieldset""ol""p""ul")
        );

        
$this->addElement('div''Block''Flow''Common');
    }
}





/**
 * Abstract class for a set of proprietary modules that clean up (tidy)
 * poorly written HTML.
 * @todo Figure out how to protect some of these methods/properties
 */
class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule
{
    
/**
     * List of supported levels.
     * Index zero is a special case "no fixes" level.
     * @type array
     */
    
public $levels = array(=> 'none''light''medium''heavy');

    
/**
     * Default level to place all fixes in.
     * Disabled by default.
     * @type string
     */
    
public $defaultLevel null;

    
/**
     * Lists of fixes used by getFixesForLevel().
     * Format is:
     *      HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2');
     * @type array
     */
    
public $fixesForLevel = array(
        
'light' => array(),
        
'medium' => array(),
        
'heavy' => array()
    );

    
/**
     * Lazy load constructs the module by determining the necessary
     * fixes to create and then delegating to the populate() function.
     * @param HTMLPurifier_Config $config
     * @todo Wildcard matching and error reporting when an added or
     *       subtracted fix has no effect.
     */
    
public function setup($config)
    {
        
// create fixes, initialize fixesForLevel
        
$fixes $this->makeFixes();
        
$this->makeFixesForLevel($fixes);

        
// figure out which fixes to use
        
$level $config->get('HTML.TidyLevel');
        
$fixes_lookup $this->getFixesForLevel($level);

        
// get custom fix declarations: these need namespace processing
        
$add_fixes $config->get('HTML.TidyAdd');
        
$remove_fixes $config->get('HTML.TidyRemove');

        foreach (
$fixes as $name => $fix) {
            
// needs to be refactored a little to implement globbing
            
if (isset($remove_fixes[$name]) ||
                (!isset(
$add_fixes[$name]) && !isset($fixes_lookup[$name]))) {
                unset(
$fixes[$name]);
            }
        }

        
// populate this module with necessary fixes
        
$this->populate($fixes);
    }

    
/**
     * Retrieves all fixes per a level, returning fixes for that specific
     * level as well as all levels below it.
     * @param string $level level identifier, see $levels for valid values
     * @return array Lookup up table of fixes
     */
    
public function getFixesForLevel($level)
    {
        if (
$level == $this->levels[0]) {
            return array();
        }
        
$activated_levels = array();
        for (
$i 1$c count($this->levels); $i $c$i++) {
            
$activated_levels[] = $this->levels[$i];
            if (
$this->levels[$i] == $level) {
                break;
            }
        }
        if (
$i == $c) {
            
trigger_error(
                
'Tidy level ' htmlspecialchars($level) . ' not recognized',
                
E_USER_WARNING
            
);
            return array();
        }
        
$ret = array();
        foreach (
$activated_levels as $level) {
            foreach (
$this->fixesForLevel[$level] as $fix) {
                
$ret[$fix] = true;
            }
        }
        return 
$ret;
    }

    
/**
     * Dynamically populates the $fixesForLevel member variable using
     * the fixes array. It may be custom overloaded, used in conjunction
     * with $defaultLevel, or not used at all.
     * @param array $fixes
     */
    
public function makeFixesForLevel($fixes)
    {
        if (!isset(
$this->defaultLevel)) {
            return;
        }
        if (!isset(
$this->fixesForLevel[$this->defaultLevel])) {
            
trigger_error(
                
'Default level ' $this->defaultLevel ' does not exist',
                
E_USER_ERROR
            
);
            return;
        }
        
$this->fixesForLevel[$this->defaultLevel] = array_keys($fixes);
    }

    
/**
     * Populates the module with transforms and other special-case code
     * based on a list of fixes passed to it
     * @param array $fixes Lookup table of fixes to activate
     */
    
public function populate($fixes)
    {
        foreach (
$fixes as $name => $fix) {
            
// determine what the fix is for
            
list($type$params) = $this->getFixType($name);
            switch (
$type) {
                case 
'attr_transform_pre':
                case 
'attr_transform_post':
                    
$attr $params['attr'];
                    if (isset(
$params['element'])) {
                        
$element $params['element'];
                        if (empty(
$this->info[$element])) {
                            
$e $this->addBlankElement($element);
                        } else {
                            
$e $this->info[$element];
                        }
                    } else {
                        
$type "info_$type";
                        
$e $this;
                    }
                    
// PHP does some weird parsing when I do
                    // $e->$type[$attr], so I have to assign a ref.
                    
$f =& $e->$type;
                    
$f[$attr] = $fix;
                    break;
                case 
'tag_transform':
                    
$this->info_tag_transform[$params['element']] = $fix;
                    break;
                case 
'child':
                case 
'content_model_type':
                    
$element $params['element'];
                    if (empty(
$this->info[$element])) {
                        
$e $this->addBlankElement($element);
                    } else {
                        
$e $this->info[$element];
                    }
                    
$e->$type $fix;
                    break;
                default:
                    
trigger_error("Fix type $type not supported"E_USER_ERROR);
                    break;
            }
        }
    }

    
/**
     * Parses a fix name and determines what kind of fix it is, as well
     * as other information defined by the fix
     * @param $name String name of fix
     * @return array(string $fix_type, array $fix_parameters)
     * @note $fix_parameters is type dependant, see populate() for usage
     *       of these parameters
     */
    
public function getFixType($name)
    {
        
// parse it
        
$property $attr null;
        if (
strpos($name'#') !== false) {
            list(
$name$property) = explode('#'$name);
        }
        if (
strpos($name'@') !== false) {
            list(
$name$attr) = explode('@'$name);
        }

        
// figure out the parameters
        
$params = array();
        if (
$name !== '') {
            
$params['element'] = $name;
        }
        if (!
is_null($attr)) {
            
$params['attr'] = $attr;
        }

        
// special case: attribute transform
        
if (!is_null($attr)) {
            if (
is_null($property)) {
                
$property 'pre';
            }
            
$type 'attr_transform_' $property;
            return array(
$type$params);
        }

        
// special case: tag transform
        
if (is_null($property)) {
            return array(
'tag_transform'$params);
        }

        return array(
$property$params);

    }

    
/**
     * Defines all fixes the module will perform in a compact
     * associative array of fix name to fix implementation.
     * @return array
     */
    
public function makeFixes()
    {
    }
}





class 
HTMLPurifier_HTMLModule_XMLCommonAttributes extends HTMLPurifier_HTMLModule
{
    
/**
     * @type string
     */
    
public $name 'XMLCommonAttributes';

    
/**
     * @type array
     */
    
public $attr_collections = array(
        
'Lang' => array(
            
'xml:lang' => 'LanguageCode',
        )
    );
}





/**
 * Name is deprecated, but allowed in strict doctypes, so onl
 */
class HTMLPurifier_HTMLModule_Tidy_Name extends HTMLPurifier_HTMLModule_Tidy
{
    
/**
     * @type string
     */
    
public $name 'Tidy_Name';

    
/**
     * @type string
     */
    
public $defaultLevel 'heavy';

    
/**
     * @return array
     */
    
public function makeFixes()
    {
        
$r = array();
        
// @name for img, a -----------------------------------------------
        // Technically, it's allowed even on strict, so we allow authors to use
        // it. However, it's deprecated in future versions of XHTML.
        
$r['img@name'] =
        
$r['a@name'] = new HTMLPurifier_AttrTransform_Name();
        return 
$r;
    }
}





class 
HTMLPurifier_HTMLModule_Tidy_Proprietary extends HTMLPurifier_HTMLModule_Tidy
{

    
/**
     * @type string
     */
    
public $name 'Tidy_Proprietary';

    
/**
     * @type string
     */
    
public $defaultLevel 'light';

    
/**
     * @return array
     */
    
public function makeFixes()
    {
        
$r = array();
        
$r['table@background'] = new HTMLPurifier_AttrTransform_Background();
        
$r['td@background']    = new HTMLPurifier_AttrTransform_Background();
        
$r['th@background']    = new HTMLPurifier_AttrTransform_Background();
        
$r['tr@background']    = new HTMLPurifier_AttrTransform_Background();
        
$r['thead@background'] = new HTMLPurifier_AttrTransform_Background();
        
$r['tfoot@background'] = new HTMLPurifier_AttrTransform_Background();
        
$r['tbody@background'] = new HTMLPurifier_AttrTransform_Background();
        
$r['table@height']     = new HTMLPurifier_AttrTransform_Length('height');
        return 
$r;
    }
}





class 
HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends HTMLPurifier_HTMLModule_Tidy
{

    
/**
     * @return array
     */
    
public function makeFixes()
    {
        
$r = array();

        
// == deprecated tag transforms ===================================

        
$r['font'] = new HTMLPurifier_TagTransform_Font();
        
$r['menu'] = new HTMLPurifier_TagTransform_Simple('ul');
        
$r['dir'] = new HTMLPurifier_TagTransform_Simple('ul');
        
$r['center'] = new HTMLPurifier_TagTransform_Simple('div''text-align:center;');
        
$r['u'] = new HTMLPurifier_TagTransform_Simple('span''text-decoration:underline;');
        
$r['s'] = new HTMLPurifier_TagTransform_Simple('span''text-decoration:line-through;');
        
$r['strike'] = new HTMLPurifier_TagTransform_Simple('span''text-decoration:line-through;');

        
// == deprecated attribute transforms =============================

        
$r['caption@align'] =
            new 
HTMLPurifier_AttrTransform_EnumToCSS(
                
'align',
                array(
                    
// we're following IE's behavior, not Firefox's, due
                    // to the fact that no one supports caption-side:right,
                    // W3C included (with CSS 2.1). This is a slightly
                    // unreasonable attribute!
                    
'left' => 'text-align:left;',
                    
'right' => 'text-align:right;',
                    
'top' => 'caption-side:top;',
                    
'bottom' => 'caption-side:bottom;' // not supported by IE
                
)
            );

        
// @align for img -------------------------------------------------
        
$r['img@align'] =
            new 
HTMLPurifier_AttrTransform_EnumToCSS(
                
'align',
                array(
                    
'left' => 'float:left;',
                    
'right' => 'float:right;',
                    
'top' => 'vertical-align:top;',
                    
'middle' => 'vertical-align:middle;',
                    
'bottom' => 'vertical-align:baseline;',
                )
            );

        
// @align for table -----------------------------------------------
        
$r['table@align'] =
            new 
HTMLPurifier_AttrTransform_EnumToCSS(
                
'align',
                array(
                    
'left' => 'float:left;',
                    
'center' => 'margin-left:auto;margin-right:auto;',
                    
'right' => 'float:right;'
                
)
            );

        
// @align for hr -----------------------------------------------
        
$r['hr@align'] =
            new 
HTMLPurifier_AttrTransform_EnumToCSS(
                
'align',
                array(
                    
// we use both text-align and margin because these work
                    // for different browsers (IE and Firefox, respectively)
                    // and the melange makes for a pretty cross-compatible
                    // solution
                    
'left' => 'margin-left:0;margin-right:auto;text-align:left;',
                    
'center' => 'margin-left:auto;margin-right:auto;text-align:center;',
                    
'right' => 'margin-left:auto;margin-right:0;text-align:right;'
                
)
            );

        
// @align for h1, h2, h3, h4, h5, h6, p, div ----------------------
        // {{{
        
$align_lookup = array();
        
$align_values = array('left''right''center''justify');
        foreach (
$align_values as $v) {
            
$align_lookup[$v] = "text-align:$v;";
        }
        
// }}}
        
$r['h1@align'] =
        
$r['h2@align'] =
        
$r['h3@align'] =
        
$r['h4@align'] =
        
$r['h5@align'] =
        
$r['h6@align'] =
        
$r['p@align'] =
        
$r['div@align'] =
            new 
HTMLPurifier_AttrTransform_EnumToCSS('align'$align_lookup);

        
// @bgcolor for table, tr, td, th ---------------------------------
        
$r['table@bgcolor'] =
        
$r['td@bgcolor'] =
        
$r['th@bgcolor'] =
            new 
HTMLPurifier_AttrTransform_BgColor();

        
// @border for img ------------------------------------------------
        
$r['img@border'] = new HTMLPurifier_AttrTransform_Border();

        
// @clear for br --------------------------------------------------
        
$r['br@clear'] =
            new 
HTMLPurifier_AttrTransform_EnumToCSS(
                
'clear',
                array(
                    
'left' => 'clear:left;',
                    
'right' => 'clear:right;',
                    
'all' => 'clear:both;',
                    
'none' => 'clear:none;',
                )
            );

        
// @height for td, th ---------------------------------------------
        
$r['td@height'] =
        
$r['th@height'] =
            new 
HTMLPurifier_AttrTransform_Length('height');

        
// @hspace for img ------------------------------------------------
        
$r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace');

        
// @noshade for hr ------------------------------------------------
        // this transformation is not precise but often good enough.
        // different browsers use different styles to designate noshade
        
$r['hr@noshade'] =
            new 
HTMLPurifier_AttrTransform_BoolToCSS(
                
'noshade',
                
'color:#808080;background-color:#808080;border:0;'
            
);

        
// @nowrap for td, th ---------------------------------------------
        
$r['td@nowrap'] =
        
$r['th@nowrap'] =
            new 
HTMLPurifier_AttrTransform_BoolToCSS(
                
'nowrap',
                
'white-space:nowrap;'
            
);

        
// @size for hr  --------------------------------------------------
        
$r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size''height');

        
// @type for li, ol, ul -------------------------------------------
        // {{{
        
$ul_types = array(
            
'disc' => 'list-style-type:disc;',
            
'square' => 'list-style-type:square;',
            
'circle' => 'list-style-type:circle;'
        
);
        
$ol_types = array(
            
'1' => 'list-style-type:decimal;',
            
'i' => 'list-style-type:lower-roman;',
            
'I' => 'list-style-type:upper-roman;',
            
'a' => 'list-style-type:lower-alpha;',
            
'A' => 'list-style-type:upper-alpha;'
        
);
        
$li_types $ul_types $ol_types;
        
// }}}

        
$r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type'$ul_types);
        
$r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type'$ol_typestrue);
        
$r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type'$li_typestrue);

        
// @vspace for img ------------------------------------------------
        
$r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace');

        
// @width for hr, td, th ------------------------------------------
        
$r['td@width'] =
        
$r['th@width'] =
        
$r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width');

        return 
$r;
    }
}





class 
HTMLPurifier_HTMLModule_Tidy_Strict extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
{
    
/**
     * @type string
     */
    
public $name 'Tidy_Strict';

    
/**
     * @type string
     */
    
public $defaultLevel 'light';

    
/**
     * @return array
     */
    
public function makeFixes()
    {
        
$r parent::makeFixes();
        
$r['blockquote#content_model_type'] = 'strictblockquote';
        return 
$r;
    }

    
/**
     * @type bool
     */
    
public $defines_child_def true;

    
/**
     * @param HTMLPurifier_ElementDef $def
     * @return HTMLPurifier_ChildDef_StrictBlockquote
     */
    
public function getChildDef($def)
    {
        if (
$def->content_model_type != 'strictblockquote') {
            return 
parent::getChildDef($def);
        }
        return new 
HTMLPurifier_ChildDef_StrictBlockquote($def->content_model);
    }
}





class 
HTMLPurifier_HTMLModule_Tidy_Transitional extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
{
    
/**
     * @type string
     */
    
public $name 'Tidy_Transitional';

    
/**
     * @type string
     */
    
public $defaultLevel 'heavy';
}





class 
HTMLPurifier_HTMLModule_Tidy_XHTML extends HTMLPurifier_HTMLModule_Tidy
{
    
/**
     * @type string
     */
    
public $name 'Tidy_XHTML';

    
/**
     * @type string
     */
    
public $defaultLevel 'medium';

    
/**
     * @return array
     */
    
public function makeFixes()
    {
        
$r = array();
        
$r['@lang'] = new HTMLPurifier_AttrTransform_Lang();
        return 
$r;
    }
}





/**
 * Injector that auto paragraphs text in the root node based on
 * double-spacing.
 * @todo Ensure all states are unit tested, including variations as well.
 * @todo Make a graph of the flow control for this Injector.
 */
class HTMLPurifier_Injector_AutoParagraph extends HTMLPurifier_Injector
{
    
/**
     * @type string
     */
    
public $name 'AutoParagraph';

    
/**
     * @type array
     */
    
public $needed = array('p');

    
/**
     * @return HTMLPurifier_Token_Start
     */
    
private function _pStart()
    {
        
$par = new HTMLPurifier_Token_Start('p');
        
$par->armor['MakeWellFormed_TagClosedError'] = true;
        return 
$par;
    }

    
/**
     * @param HTMLPurifier_Token_Text $token
     */
    
public function handleText(&$token)
    {
        
$text $token->data;
        
// Does the current parent allow <p> tags?
        
if ($this->allowsElement('p')) {
            if (empty(
$this->currentNesting) || strpos($text"nn") !== false) {
                
// Note that we have differing behavior when dealing with text
                // in the anonymous root node, or a node inside the document.
                // If the text as a double-newline, the treatment is the same;
                // if it doesn't, see the next if-block if you're in the document.

                
$i $nesting null;
                if (!
$this->forwardUntilEndToken($i$current$nesting) && $token->is_whitespace) {
                    
// State 1.1: ...    ^ (whitespace, then document end)
                    //               ----
                    // This is a degenerate case
                
} else {
                    if (!
$token->is_whitespace || $this->_isInline($current)) {
                        
// State 1.2: PAR1
                        //            ----

                        // State 1.3: PAR1nnPAR2
                        //            ------------

                        // State 1.4: <div>PAR1nnPAR2 (see State 2)
                        //                 ------------
                        
$token = array($this->_pStart());
                        
$this->_splitText($text$token);
                    } else {
                        
// State 1.5: n<hr />
                        //            --
                    
}
                }
            } else {
                
// State 2:   <div>PAR1... (similar to 1.4)
                //                 ----

                // We're in an element that allows paragraph tags, but we're not
                // sure if we're going to need them.
                
if ($this->_pLookAhead()) {
                    
// State 2.1: <div>PAR1<b>PAR1nnPAR2
                    //                 ----
                    // Note: This will always be the first child, since any
                    // previous inline element would have triggered this very
                    // same routine, and found the double newline. One possible
                    // exception would be a comment.
                    
$token = array($this->_pStart(), $token);
                } else {
                    
// State 2.2.1: <div>PAR1<div>
                    //                   ----

                    // State 2.2.2: <div>PAR1<b>PAR1</b></div>
                    //                   ----
                
}
            }
            
// Is the current parent a <p> tag?
        
} elseif (!empty($this->currentNesting) &&
            
$this->currentNesting[count($this->currentNesting) - 1]->name == 'p') {
            
// State 3.1: ...<p>PAR1
            //                  ----

            // State 3.2: ...<p>PAR1nnPAR2
            //                  ------------
            
$token = array();
            
$this->_splitText($text$token);
            
// Abort!
        
} else {
            
// State 4.1: ...<b>PAR1
            //                  ----

            // State 4.2: ...<b>PAR1nnPAR2
            //                  ------------
        
}
    }

    
/**
     * @param HTMLPurifier_Token $token
     */
    
public function handleElement(&$token)
    {
        
// We don't have to check if we're already in a <p> tag for block
        // tokens, because the tag would have been autoclosed by MakeWellFormed.
        
if ($this->allowsElement('p')) {
            if (!empty(
$this->currentNesting)) {
                if (
$this->_isInline($token)) {
                    
// State 1: <div>...<b>
                    //                  ---
                    // Check if this token is adjacent to the parent token
                    // (seek backwards until token isn't whitespace)
                    
$i null;
                    
$this->backward($i$prev);

                    if (!
$prev instanceof HTMLPurifier_Token_Start) {
                        
// Token wasn't adjacent
                        
if ($prev instanceof HTMLPurifier_Token_Text &&
                            
substr($prev->data, -2) === "nn"
                        
) {
                            
// State 1.1.4: <div><p>PAR1</p>nn<b>
                            //                                  ---
                            // Quite frankly, this should be handled by splitText
                            
$token = array($this->_pStart(), $token);
                        } else {
                            
// State 1.1.1: <div><p>PAR1</p><b>
                            //                              ---
                            // State 1.1.2: <div><br /><b>
                            //                         ---
                            // State 1.1.3: <div>PAR<b>
                            //                      ---
                        
}
                    } else {
                        
// State 1.2.1: <div><b>
                        //                   ---
                        // Lookahead to see if <p> is needed.
                        
if ($this->_pLookAhead()) {
                            
// State 1.3.1: <div><b>PAR1nnPAR2
                            //                   ---
                            
$token = array($this->_pStart(), $token);
                        } else {
                            
// State 1.3.2: <div><b>PAR1</b></div>
                            //                   ---

                            // State 1.3.3: <div><b>PAR1</b><div></div>nn</div>
                            //                   ---
                        
}
                    }
                } else {
                    
// State 2.3: ...<div>
                    //               -----
                
}
            } else {
                if (
$this->_isInline($token)) {
                    
// State 3.1: <b>
                    //            ---
                    // This is where the {p} tag is inserted, not reflected in
                    // inputTokens yet, however.
                    
$token = array($this->_pStart(), $token);
                } else {
                    
// State 3.2: <div>
                    //            -----
                
}

                
$i null;
                if (
$this->backward($i$prev)) {
                    if (!
$prev instanceof HTMLPurifier_Token_Text) {
                        
// State 3.1.1: ...</p>{p}<b>
                        //                        ---
                        // State 3.2.1: ...</p><div>
                        //                     -----
                        
if (!is_array($token)) {
                            
$token = array($token);
                        }
                        
array_unshift($token, new HTMLPurifier_Token_Text("nn"));
                    } else {
                        
// State 3.1.2: ...</p>nn{p}<b>
                        //                            ---
                        // State 3.2.2: ...</p>nn<div>
                        //                         -----
                        // Note: PAR<ELEM> cannot occur because PAR would have been
                        // wrapped in <p> tags.
                    
}
                }
            }
        } else {
            
// State 2.2: <ul><li>
            //                ----
            // State 2.4: <p><b>
            //               ---
        
}
    }

    
/**
     * Splits up a text in paragraph tokens and appends them
     * to the result stream that will replace the original
     * @param string $data String text data that will be processed
     *    into paragraphs
     * @param HTMLPurifier_Token[] $result Reference to array of tokens that the
     *    tags will be appended onto
     */
    
private function _splitText($data, &$result)
    {
        
$raw_paragraphs explode("nn"$data);
        
$paragraphs = array(); // without empty paragraphs
        
$needs_start false;
        
$needs_end false;

        
$c count($raw_paragraphs);
        if (
$c == 1) {
            
// There were no double-newlines, abort quickly. In theory this
            // should never happen.
            
$result[] = new HTMLPurifier_Token_Text($data);
            return;
        }
        for (
$i 0$i $c$i++) {
            
$par $raw_paragraphs[$i];
            if (
trim($par) !== '') {
                
$paragraphs[] = $par;
            } else {
                if (
$i == 0) {
                    
// Double newline at the front
                    
if (empty($result)) {
                        
// The empty result indicates that the AutoParagraph
                        // injector did not add any start paragraph tokens.
                        // This means that we have been in a paragraph for
                        // a while, and the newline means we should start a new one.
                        
$result[] = new HTMLPurifier_Token_End('p');
                        
$result[] = new HTMLPurifier_Token_Text("nn");
                        
// However, the start token should only be added if
                        // there is more processing to be done (i.e. there are
                        // real paragraphs in here). If there are none, the
                        // next start paragraph tag will be handled by the
                        // next call to the injector
                        
$needs_start true;
                    } else {
                        
// We just started a new paragraph!
                        // Reinstate a double-newline for presentation's sake, since
                        // it was in the source code.
                        
array_unshift($result, new HTMLPurifier_Token_Text("nn"));
                    }
                } elseif (
$i == $c) {
                    
// Double newline at the end
                    // There should be a trailing </p> when we're finally done.
                    
$needs_end true;
                }
            }
        }

        
// Check if this was just a giant blob of whitespace. Move this earlier,
        // perhaps?
        
if (empty($paragraphs)) {
            return;
        }

        
// Add the start tag indicated by nn at the beginning of $data
        
if ($needs_start) {
            
$result[] = $this->_pStart();
        }

        
// Append the paragraphs onto the result
        
foreach ($paragraphs as $par) {
            
$result[] = new HTMLPurifier_Token_Text($par);
            
$result[] = new HTMLPurifier_Token_End('p');
            
$result[] = new HTMLPurifier_Token_Text("nn");
            
$result[] = $this->_pStart();
        }

        
// Remove trailing start token; Injector will handle this later if
        // it was indeed needed. This prevents from needing to do a lookahead,
        // at the cost of a lookbehind later.
        
array_pop($result);

        
// If there is no need for an end tag, remove all of it and let
        // MakeWellFormed close it later.
        
if (!$needs_end) {
            
array_pop($result); // removes nn
            
array_pop($result); // removes </p>
        
}
    }

    
/**
     * Returns true if passed token is inline (and, ergo, allowed in
     * paragraph tags)
     * @param HTMLPurifier_Token $token
     * @return bool
     */
    
private function _isInline($token)
    {
        return isset(
$this->htmlDefinition->info['p']->child->elements[$token->name]);
    }

    
/**
     * Looks ahead in the token list and determines whether or not we need
     * to insert a <p> tag.
     * @return bool
     */
    
private function _pLookAhead()
    {
        if (
$this->currentToken instanceof HTMLPurifier_Token_Start) {
            
$nesting 1;
        } else {
            
$nesting 0;
        }
        
$ok false;
        
$i null;
        while (
$this->forwardUntilEndToken($i$current$nesting)) {
            
$result $this->_checkNeedsP($current);
            if (
$result !== null) {
                
$ok $result;
                break;
            }
        }
        return 
$ok;
    }

    
/**
     * Determines if a particular token requires an earlier inline token
     * to get a paragraph. This should be used with _forwardUntilEndToken
     * @param HTMLPurifier_Token $current
     * @return bool
     */
    
private function _checkNeedsP($current)
    {
        if (
$current instanceof HTMLPurifier_Token_Start) {
            if (!
$this->_isInline($current)) {
                
// <div>PAR1<div>
                //      ----
                // Terminate early, since we hit a block element
                
return false;
            }
        } elseif (
$current instanceof HTMLPurifier_Token_Text) {
            if (
strpos($current->data"nn") !== false) {
                
// <div>PAR1<b>PAR1nnPAR2
                //      ----
                
return true;
            } else {
                
// <div>PAR1<b>PAR1...
                //      ----
            
}
        }
        return 
null;
    }
}





/**
 * Injector that displays the URL of an anchor instead of linking to it, in addition to showing the text of the link.
 */
class HTMLPurifier_Injector_DisplayLinkURI extends HTMLPurifier_Injector
{
    
/**
     * @type string
     */
    
public $name 'DisplayLinkURI';

    
/**
     * @type array
     */
    
public $needed = array('a');

    
/**
     * @param $token
     */
    
public function handleElement(&$token)
    {
    }

    
/**
     * @param HTMLPurifier_Token $token
     */
    
public function handleEnd(&$token)
    {
        if (isset(
$token->start->attr['href'])) {
            
$url $token->start->attr['href'];
            unset(
$token->start->attr['href']);
            
$token = array($token, new HTMLPurifier_Token_Text(" ($url)"));
        } else {
            
// nothing to display
        
}
    }
}





/**
 * Injector that converts http, https and ftp text URLs to actual links.
 */
class HTMLPurifier_Injector_Linkify extends HTMLPurifier_Injector
{
    
/**
     * @type string
     */
    
public $name 'Linkify';

    
/**
     * @type array
     */
    
public $needed = array('a' => array('href'));

    
/**
     * @param HTMLPurifier_Token $token
     */
    
public function handleText(&$token)
    {
        if (!
$this->allowsElement('a')) {
            return;
        }

        if (
strpos($token->data'://') === false) {
            
// our really quick heuristic failed, abort
            // this may not work so well if we want to match things like
            // "google.com", but then again, most people don't
            
return;
        }

        
// there is/are URL(s). Let's split the string.
        // We use this regex:
        // https://gist.github.com/gruber/249502
        // but with @cscott's backtracking fix and also
        // the Unicode characters un-Unicodified.
        
$bits preg_split(
            
'/\b((?:[a-z][\w\-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\((?:[^\s()<>]|(?:\([^\s()<>]+\)))*\))+(?:\((?:[^\s()<>]|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?x{00ab}x{00bb}x{201c}x{201d}x{2018}x{2019}]))/iu',
            
$token->data, -1, PREG_SPLIT_DELIM_CAPTURE);


        
$token = array();

        // 
$i = index
        // 
$c = count
        // 
$l = is link
        for (
$i = 0, $c = count($bits), $l = false; $i < $c$i++, $l = !$l) {
            if (!
$l) {
                if (
$bits[$i] === '') {
                    continue;
                }
                
$token[] = new HTMLPurifier_Token_Text($bits[$i]);
            } else {
                
$token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i]));
                
$token[] = new HTMLPurifier_Token_Text($bits[$i]);
                
$token[] = new HTMLPurifier_Token_End('a');
            }
        }
    }
}





/**
 * Injector that converts configuration directive syntax %Namespace.Directive
 * to links
 */
class HTMLPurifier_Injector_PurifierLinkify extends HTMLPurifier_Injector
{
    /**
     * @type string
     */
    public 
$name = 'PurifierLinkify';

    /**
     * @type string
     */
    public 
$docURL;

    /**
     * @type array
     */
    public 
$needed = array('a' => array('href'));

    /**
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return string
     */
    public function prepare(
$config$context)
    {
        
$this->docURL = $config->get('AutoFormat.PurifierLinkify.DocURL');
        return parent::prepare(
$config$context);
    }

    /**
     * @param HTMLPurifier_Token 
$token
     */
    public function handleText(&
$token)
    {
        if (!
$this->allowsElement('a')) {
            return;
        }
        if (strpos(
$token->data, '%') === false) {
            return;
        }

        
$bits = preg_split('#%([a-z0-9]+.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
        
$token = array();

        // 
$i = index
        // 
$c = count
        // 
$l = is link
        for (
$i = 0, $c = count($bits), $l = false; $i < $c$i++, $l = !$l) {
            if (!
$l) {
                if (
$bits[$i] === '') {
                    continue;
                }
                
$token[] = new HTMLPurifier_Token_Text($bits[$i]);
            } else {
                
$token[] = new HTMLPurifier_Token_Start(
                    'a',
                    array('href' => str_replace('%s', 
$bits[$i]$this->docURL))
                );
                
$token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]);
                
$token[] = new HTMLPurifier_Token_End('a');
            }
        }
    }
}





class HTMLPurifier_Injector_RemoveEmpty extends HTMLPurifier_Injector
{
    /**
     * @type HTMLPurifier_Context
     */
    private 
$context;

    /**
     * @type HTMLPurifier_Config
     */
    private 
$config;

    /**
     * @type HTMLPurifier_AttrValidator
     */
    private 
$attrValidator;

    /**
     * @type bool
     */
    private 
$removeNbsp;

    /**
     * @type bool
     */
    private 
$removeNbspExceptions;

    /**
     * Cached contents of %AutoFormat.RemoveEmpty.Predicate
     * @type array
     */
    private 
$exclude;

    /**
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return void
     */
    public function prepare(
$config$context)
    {
        parent::prepare(
$config$context);
        
$this->config = $config;
        
$this->context = $context;
        
$this->removeNbsp = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp');
        
$this->removeNbspExceptions = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions');
        
$this->exclude = $config->get('AutoFormat.RemoveEmpty.Predicate');
        foreach (
$this->exclude as $key => $attrs) {
            if (!is_array(
$attrs)) {
                // HACK, see HTMLPurifier/Printer/ConfigForm.php
                
$this->exclude[$key] = explode(';', $attrs);
            }
        }
        
$this->attrValidator = new HTMLPurifier_AttrValidator();
    }

    /**
     * @param HTMLPurifier_Token 
$token
     */
    public function handleElement(&
$token)
    {
        if (!
$token instanceof HTMLPurifier_Token_Start) {
            return;
        }
        
$next = false;
        
$deleted = 1; // the current tag
        for (
$i = count($this->inputZipper->back) - 1; $i >= 0; $i--, $deleted++) {
            
$next = $this->inputZipper->back[$i];
            if (
$next instanceof HTMLPurifier_Token_Text) {
                if (
$next->is_whitespace) {
                    continue;
                }
                if (
$this->removeNbsp && !isset($this->removeNbspExceptions[$token->name])) {
                    
$plain = str_replace("xC2xA0", "", $next->data);
                    
$isWsOrNbsp = $plain === '' || ctype_space($plain);
                    if (
$isWsOrNbsp) {
                        continue;
                    }
                }
            }
            break;
        }
        if (!
$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) {
            
$this->attrValidator->validateToken($token$this->config$this->context);
            
$token->armor['ValidateAttributes'] = true;
            if (isset(
$this->exclude[$token->name])) {
                
$r = true;
                foreach (
$this->exclude[$token->name] as $elem) {
                    if (!isset(
$token->attr[$elem])) $r = false;
                }
                if (
$r) return;
            }
            if (isset(
$token->attr['id']) || isset($token->attr['name'])) {
                return;
            }
            
$token = $deleted + 1;
            for (
$b = 0, $c = count($this->inputZipper->front); $b < $c$b++) {
                
$prev = $this->inputZipper->front[$b];
                if (
$prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) {
                    continue;
                }
                break;
            }
            // This is safe because we removed the token that triggered this.
            
$this->rewindOffset($b+$deleted);
            return;
        }
    }
}





/**
 * Injector that removes spans with no attributes
 */
class HTMLPurifier_Injector_RemoveSpansWithoutAttributes extends HTMLPurifier_Injector
{
    /**
     * @type string
     */
    public 
$name = 'RemoveSpansWithoutAttributes';

    /**
     * @type array
     */
    public 
$needed = array('span');

    /**
     * @type HTMLPurifier_AttrValidator
     */
    private 
$attrValidator;

    /**
     * Used by AttrValidator.
     * @type HTMLPurifier_Config
     */
    private 
$config;

    /**
     * @type HTMLPurifier_Context
     */
    private 
$context;

    public function prepare(
$config$context)
    {
        
$this->attrValidator = new HTMLPurifier_AttrValidator();
        
$this->config = $config;
        
$this->context = $context;
        return parent::prepare(
$config$context);
    }

    /**
     * @param HTMLPurifier_Token 
$token
     */
    public function handleElement(&
$token)
    {
        if (
$token->name !== 'span' || !$token instanceof HTMLPurifier_Token_Start) {
            return;
        }

        // We need to validate the attributes now since this doesn't normally
        // happen until after MakeWellFormed. If all the attributes are removed
        // the span needs to be removed too.
        
$this->attrValidator->validateToken($token$this->config$this->context);
        
$token->armor['ValidateAttributes'] = true;

        if (!empty(
$token->attr)) {
            return;
        }

        
$nesting = 0;
        while (
$this->forwardUntilEndToken($i$current$nesting)) {
        }

        if (
$current instanceof HTMLPurifier_Token_End && $current->name === 'span') {
            // Mark closing span tag for deletion
            
$current->markForDeletion = true;
            // Delete open span tag
            
$token = false;
        }
    }

    /**
     * @param HTMLPurifier_Token 
$token
     */
    public function handleEnd(&
$token)
    {
        if (
$token->markForDeletion) {
            
$token = false;
        }
    }
}





/**
 * Adds important param elements to inside of object in order to make
 * things safe.
 */
class HTMLPurifier_Injector_SafeObject extends HTMLPurifier_Injector
{
    /**
     * @type string
     */
    public 
$name = 'SafeObject';

    /**
     * @type array
     */
    public 
$needed = array('object', 'param');

    /**
     * @type array
     */
    protected 
$objectStack = array();

    /**
     * @type array
     */
    protected 
$paramStack = array();

    /**
     * Keep this synchronized with AttrTransform/SafeParam.php.
     * @type array
     */
    protected 
$addParam = array(
        'allowScriptAccess' => 'never',
        'allowNetworking' => 'internal',
    );

    /**
     * These are all lower-case keys.
     * @type array
     */
    protected 
$allowedParam = array(
        'wmode' => true,
        'movie' => true,
        'flashvars' => true,
        'src' => true,
        'allowfullscreen' => true, // if omitted, assume to be 'false'
    );

    /**
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return void
     */
    public function prepare(
$config$context)
    {
        parent::prepare(
$config$context);
    }

    /**
     * @param HTMLPurifier_Token 
$token
     */
    public function handleElement(&
$token)
    {
        if (
$token->name == 'object') {
            
$this->objectStack[] = $token;
            
$this->paramStack[] = array();
            
$new = array($token);
            foreach (
$this->addParam as $name => $value) {
                
$new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value));
            }
            
$token = $new;
        } elseif (
$token->name == 'param') {
            
$nest = count($this->currentNesting) - 1;
            if (
$nest >= 0 && $this->currentNesting[$nest]->name === 'object') {
                
$i = count($this->objectStack) - 1;
                if (!isset(
$token->attr['name'])) {
                    
$token = false;
                    return;
                }
                
$n = $token->attr['name'];
                // We need this fix because YouTube doesn't supply a data
                // attribute, which we need if a type is specified. This is
                // *very* Flash specific.
                if (!isset(
$this->objectStack[$i]->attr['data']) &&
                    (
$token->attr['name'] == 'movie' || $token->attr['name'] == 'src')
                ) {
                    
$this->objectStack[$i]->attr['data'] = $token->attr['value'];
                }
                // Check if the parameter is the correct value but has not
                // already been added
                if (!isset(
$this->paramStack[$i][$n]) &&
                    isset(
$this->addParam[$n]) &&
                    
$token->attr['name'] === $this->addParam[$n]) {
                    // keep token, and add to param stack
                    
$this->paramStack[$i][$n] = true;
                } elseif (isset(
$this->allowedParam[strtolower($n)])) {
                    // keep token, don't do anything to it
                    // (could possibly check for duplicates here)
                    // Note: In principle, parameters should be case sensitive.
                    // But it seems they are not really; so accept any case.
                } else {
                    
$token = false;
                }
            } else {
                // not directly inside an object, DENY!
                
$token = false;
            }
        }
    }

    public function handleEnd(&
$token)
    {
        // This is the WRONG way of handling the object and param stacks;
        // we should be inserting them directly on the relevant object tokens
        // so that the global stack handling handles it.
        if (
$token->name == 'object') {
            array_pop(
$this->objectStack);
            array_pop(
$this->paramStack);
        }
    }
}





/**
 * Parser that uses PHP 5's DOM extension (part of the core).
 *
 * In PHP 5, the DOM XML extension was revamped into DOM and added to the core.
 * It gives us a forgiving HTML parser, which we use to transform the HTML
 * into a DOM, and then into the tokens.  It is blazingly fast (for large
 * documents, it performs twenty times faster than
 * HTMLPurifier_Lexer_DirectLex,and is the default choice for PHP 5.
 *
 * @note Any empty elements will have empty tokens associated with them, even if
 * this is prohibited by the spec. This is cannot be fixed until the spec
 * comes into play.
 *
 * @note PHP's DOM extension does not actually parse any entities, we use
 *       our own function to do that.
 *
 * @warning DOM tends to drop whitespace, which may wreak havoc on indenting.
 *          If this is a huge problem, due to the fact that HTML is hand
 *          edited and you are unable to get a parser cache that caches the
 *          the output of HTML Purifier while keeping the original HTML lying
 *          around, you may want to run Tidy on the resulting output or use
 *          HTMLPurifier_DirectLex
 */

class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
{

    /**
     * @type HTMLPurifier_TokenFactory
     */
    private 
$factory;

    public function __construct()
    {
        // setup the factory
        parent::__construct();
        
$this->factory = new HTMLPurifier_TokenFactory();
    }

    /**
     * @param string 
$html
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return HTMLPurifier_Token[]
     */
    public function tokenizeHTML(
$html$config$context)
    {
        
$html = $this->normalize($html$config$context);

        // attempt to armor stray angled brackets that cannot possibly
        // form tags and thus are probably being used as emoticons
        if (
$config->get('Core.AggressivelyFixLt')) {
            
$char = '[^a-z!/]';
            
$comment = "/<!--(.*?)(-->|z)/is";
            
$html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html);
            do {
                
$old = $html;
                
$html = preg_replace("/<($char)/i", '&lt;\1', $html);
            } while (
$html !== $old);
            
$html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments
        }

        // preprocess html, essential for UTF-8
        
$html = $this->wrapHTML($html$config$context);

        
$doc = new DOMDocument();
        
$doc->encoding = 'UTF-8'; // theoretically, the above has this covered

        set_error_handler(array(
$this, 'muteErrorHandler'));
        
$doc->loadHTML($html);
        restore_error_handler();

        
$body = $doc->getElementsByTagName('html')->item(0)-> // <html>
                      getElementsByTagName('body')->item(0);  // <body>

        
$div = $body->getElementsByTagName('div')->item(0); // <div>
        
$tokens = array();
        
$this->tokenizeDOM($div$tokens$config);
        // If the div has a sibling, that means we tripped across
        // a premature </div> tag.  So remove the div we parsed,
        // and then tokenize the rest of body.  We can't tokenize
        // the sibling directly as we'll lose the tags in that case.
        if (
$div->nextSibling) {
            
$body->removeChild($div);
            
$this->tokenizeDOM($body$tokens$config);
        }
        return 
$tokens;
    }

    /**
     * Iterative function that tokenizes a node, putting it into an accumulator.
     * To iterate is human, to recurse divine - L. Peter Deutsch
     * @param DOMNode 
$node DOMNode to be tokenized.
     * @param HTMLPurifier_Token[] 
$tokens   Array-list of already tokenized tokens.
     * @return HTMLPurifier_Token of node appended to previously passed tokens.
     */
    protected function tokenizeDOM(
$node, &$tokens$config)
    {
        
$level = 0;
        
$nodes = array($level => new HTMLPurifier_Queue(array($node)));
        
$closingNodes = array();
        do {
            while (!
$nodes[$level]->isEmpty()) {
                
$node = $nodes[$level]->shift(); // FIFO
                
$collect = $level > 0 ? true : false;
                
$needEndingTag = $this->createStartNode($node$tokens$collect$config);
                if (
$needEndingTag) {
                    
$closingNodes[$level][] = $node;
                }
                if (
$node->childNodes && $node->childNodes->length) {
                    
$level++;
                    
$nodes[$level] = new HTMLPurifier_Queue();
                    foreach (
$node->childNodes as $childNode) {
                        
$nodes[$level]->push($childNode);
                    }
                }
            }
            
$level--;
            if (
$level && isset($closingNodes[$level])) {
                while (
$node = array_pop($closingNodes[$level])) {
                    
$this->createEndNode($node$tokens);
                }
            }
        } while (
$level > 0);
    }

    /**
     * @param DOMNode 
$node DOMNode to be tokenized.
     * @param HTMLPurifier_Token[] 
$tokens   Array-list of already tokenized tokens.
     * @param bool 
$collect  Says whether or start and close are collected, set to
     *                    false at first recursion because it's the implicit DIV
     *                    tag you're dealing with.
     * @return bool if the token needs an endtoken
     * @todo data and tagName properties don't seem to exist in DOMNode?
     */
    protected function createStartNode(
$node, &$tokens$collect$config)
    {
        // intercept non element nodes. WE MUST catch all of them,
        // but we're not getting the character reference nodes because
        // those should have been preprocessed
        if (
$node->nodeType === XML_TEXT_NODE) {
            
$tokens[] = $this->factory->createText($node->data);
            return false;
        } elseif (
$node->nodeType === XML_CDATA_SECTION_NODE) {
            // undo libxml's special treatment of <script> and <style> tags
            
$last = end($tokens);
            
$data = $node->data;
            // (note 
$node->tagname is already normalized)
            if (
$last instanceof HTMLPurifier_Token_Start && ($last->name == 'script' || $last->name == 'style')) {
                
$new_data = trim($data);
                if (substr(
$new_data, 0, 4) === '<!--') {
                    
$data = substr($new_data, 4);
                    if (substr(
$data, -3) === '-->') {
                        
$data = substr($data, 0, -3);
                    } else {
                        // Highly suspicious! Not sure what to do...
                    }
                }
            }
            
$tokens[] = $this->factory->createText($this->parseText($data$config));
            return false;
        } elseif (
$node->nodeType === XML_COMMENT_NODE) {
            // this is code is only invoked for comments in script/style in versions
            // of libxml pre-2.6.28 (regular comments, of course, are still
            // handled regularly)
            
$tokens[] = $this->factory->createComment($node->data);
            return false;
        } elseif (
$node->nodeType !== XML_ELEMENT_NODE) {
            // not-well tested: there may be other nodes we have to grab
            return false;
        }

        
$attr = $node->hasAttributes() ? $this->transformAttrToAssoc($node->attributes) : array();

        // We still have to make sure that the element actually IS empty
        if (!
$node->childNodes->length) {
            if (
$collect) {
                
$tokens[] = $this->factory->createEmpty($node->tagName$attr);
            }
            return false;
        } else {
            if (
$collect) {
                
$tokens[] = $this->factory->createStart(
                    
$tag_name = $node->tagName, // somehow, it get's dropped
                    
$attr
                );
            }
            return true;
        }
    }

    /**
     * @param DOMNode 
$node
     * @param HTMLPurifier_Token[] 
$tokens
     */
    protected function createEndNode(
$node, &$tokens)
    {
        
$tokens[] = $this->factory->createEnd($node->tagName);
    }


    /**
     * Converts a DOMNamedNodeMap of DOMAttr objects into an assoc array.
     *
     * @param DOMNamedNodeMap 
$node_map DOMNamedNodeMap of DOMAttr objects.
     * @return array Associative array of attributes.
     */
    protected function transformAttrToAssoc(
$node_map)
    {
        // NamedNodeMap is documented very well, so we're using undocumented
        // features, namely, the fact that it implements Iterator and
        // has a ->length attribute
        if (
$node_map->length === 0) {
            return array();
        }
        
$array = array();
        foreach (
$node_map as $attr) {
            
$array[$attr->name] = $attr->value;
        }
        return 
$array;
    }

    /**
     * An error handler that mutes all errors
     * @param int 
$errno
     * @param string 
$errstr
     */
    public function muteErrorHandler(
$errno$errstr)
    {
    }

    /**
     * Callback function for undoing escaping of stray angled brackets
     * in comments
     * @param array 
$matches
     * @return string
     */
    public function callbackUndoCommentSubst(
$matches)
    {
        return '<!--' . strtr(
$matches[1], array('&amp;' => '&', '&lt;' => '<')) . $matches[2];
    }

    /**
     * Callback function that entity-izes ampersands in comments so that
     * callbackUndoCommentSubst doesn't clobber them
     * @param array 
$matches
     * @return string
     */
    public function callbackArmorCommentEntities(
$matches)
    {
        return '<!--' . str_replace('&', '&amp;', 
$matches[1]) . $matches[2];
    }

    /**
     * Wraps an HTML fragment in the necessary HTML
     * @param string 
$html
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return string
     */
    protected function wrapHTML(
$html$config$context$use_div = true)
    {
        
$def = $config->getDefinition('HTML');
        
$ret = '';

        if (!empty(
$def->doctype->dtdPublic) || !empty($def->doctype->dtdSystem)) {
            
$ret .= '<!DOCTYPE html ';
            if (!empty(
$def->doctype->dtdPublic)) {
                
$ret .= 'PUBLIC "' . $def->doctype->dtdPublic . '" ';
            }
            if (!empty(
$def->doctype->dtdSystem)) {
                
$ret .= '"' . $def->doctype->dtdSystem . '" ';
            }
            
$ret .= '>';
        }

        
$ret .= '<html><head>';
        
$ret .= '<meta http-equiv="Content-Type" content="text/htmlcharset=utf-8" />';
        // No protection if 
$html contains a stray </div>!
        
$ret .= '</head><body>';
        if (
$use_div$ret .= '<div>';
        
$ret .= $html;
        if (
$use_div$ret .= '</div>';
        
$ret .= '</body></html>';
        return 
$ret;
    }
}





/**
 * Our in-house implementation of a parser.
 *
 * A pure PHP parser, DirectLex has absolutely no dependencies, making
 * it a reasonably good default for PHP4.  Written with efficiency in mind,
 * it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it
 * pales in comparison to HTMLPurifier_Lexer_DOMLex.
 *
 * @todo Reread XML spec and document differences.
 */
class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
{
    /**
     * @type bool
     */
    public 
$tracksLineNumbers = true;

    /**
     * Whitespace characters for str(c)spn.
     * @type string
     */
    protected 
$_whitespace = "x20x09x0Dx0A";

    /**
     * Callback function for script CDATA fudge
     * @param array 
$matches, in form of array(opening tag, contents, closing tag)
     * @return string
     */
    protected function scriptCallback(
$matches)
    {
        return 
$matches[1] . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8') . $matches[3];
    }

    /**
     * @param String 
$html
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return array|HTMLPurifier_Token[]
     */
    public function tokenizeHTML(
$html$config$context)
    {
        // special normalization for script tags without any armor
        // our "
armor" heurstic is a < sign any number of whitespaces after
        // the first script tag
        if (
$config->get('HTML.Trusted')) {
            
$html = preg_replace_callback(
                '#(<script[^>]*>)(s*[^<].+?)(</script>)#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 "
<b>asdf</b>" but not "asdf <b></b>"
            // 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->parseText(
                        substr(
                            
$html,
                            
$cursor,
                            
$position_next_lt - $cursor
                        ), 
$config
                    )
                );
                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->parseText(
                        substr(
                            
$html,
                            
$cursor
                        ), 
$config
                    )
                );
                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 <br>, 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->parseText(
                        substr(
$html$cursor), $config
                    )
                );
                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
     * @param string 
$haystack
     * @param string 
$needle
     * @param int 
$offset
     * @param int 
$length
     * @return int
     */
    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 
$string Inside of tag excluding name.
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return array 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->parseAttr($value$config));
        }

        // 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 .= ' ';

        
$old_cursor = -1;
        while (
$cursor < $size) {
            if (
$old_cursor >= $cursor) {
                throw new Exception("
Infinite loop detected");
            }
            
$old_cursor = $cursor;

            
$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 += 1 + 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->parseAttr($value$config);
                
$cursor++;
            } else {
                // boolattr
                if (
$key !== '') {
                    
$array[$key] = $key;
                } else {
                    // purely theoretical
                    if (
$e) {
                        
$e->send(E_ERROR, 'Lexer: Missing attribute key');
                    }
                }
            }
        }
        return 
$array;
    }
}





/**
 * Concrete comment node class.
 */
class HTMLPurifier_Node_Comment extends HTMLPurifier_Node
{
    /**
     * Character data within comment.
     * @type string
     */
    public 
$data;

    /**
     * @type bool
     */
    public 
$is_whitespace = true;

    /**
     * Transparent constructor.
     *
     * @param string 
$data String comment data.
     * @param int 
$line
     * @param int 
$col
     */
    public function __construct(
$data$line = null, $col = null)
    {
        
$this->data = $data;
        
$this->line = $line;
        
$this->col = $col;
    }

    public function toTokenPair() {
        return array(new HTMLPurifier_Token_Comment(
$this->data$this->line$this->col), null);
    }
}



/**
 * Concrete element node class.
 */
class HTMLPurifier_Node_Element extends HTMLPurifier_Node
{
    /**
     * 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.
     * @type string
     */
    public 
$name;

    /**
     * Associative array of the node's attributes.
     * @type array
     */
    public 
$attr = array();

    /**
     * List of child elements.
     * @type array
     */
    public 
$children = array();

    /**
     * Does this use the <a></a> form or the </a> form, i.e.
     * is it a pair of start/end tokens or an empty token.
     * @bool
     */
    public 
$empty = false;

    public 
$endCol = null, $endLine = null, $endArmor = array();

    public function __construct(
$name$attr = array(), $line = null, $col = null, $armor = array()) {
        
$this->name = $name;
        
$this->attr = $attr;
        
$this->line = $line;
        
$this->col = $col;
        
$this->armor = $armor;
    }

    public function toTokenPair() {
        // XXX inefficiency here, normalization is not necessary
        if (
$this->empty) {
            return array(new HTMLPurifier_Token_Empty(
$this->name$this->attr$this->line$this->col$this->armor), null);
        } else {
            
$start = new HTMLPurifier_Token_Start($this->name$this->attr$this->line$this->col$this->armor);
            
$end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine$this->endCol$this->endArmor);
            //
$end->start = $start;
            return array(
$start$end);
        }
    }
}




/**
 * Concrete text token class.
 *
 * Text tokens comprise of regular parsed character data (PCDATA) and raw
 * character data (from the CDATA sections). Internally, their
 * data is parsed with all entities expanded. Surprisingly, the text token
 * does have a "
tag name" called #PCDATA, which is how the DTD represents it
 * in permissible child nodes.
 */
class HTMLPurifier_Node_Text extends HTMLPurifier_Node
{

    /**
     * PCDATA tag name compatible with DTD, see
     * HTMLPurifier_ChildDef_Custom for details.
     * @type string
     */
    public 
$name = '#PCDATA';

    /**
     * @type string
     */
    public 
$data;
    /**< Parsed character data of text. */

    /**
     * @type bool
     */
    public 
$is_whitespace;

    /**< Bool indicating if node is whitespace. */

    /**
     * Constructor, accepts data and determines if it is whitespace.
     * @param string 
$data String parsed character data.
     * @param int 
$line
     * @param int 
$col
     */
    public function __construct(
$data$is_whitespace$line = null, $col = null)
    {
        
$this->data = $data;
        
$this->is_whitespace = $is_whitespace;
        
$this->line = $line;
        
$this->col = $col;
    }

    public function toTokenPair() {
        return array(new HTMLPurifier_Token_Text(
$this->data$this->line$this->col), null);
    }
}





/**
 * Composite strategy that runs multiple strategies on tokens.
 */
abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy
{

    /**
     * List of strategies to run tokens through.
     * @type HTMLPurifier_Strategy[]
     */
    protected 
$strategies = array();

    /**
     * @param HTMLPurifier_Token[] 
$tokens
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return HTMLPurifier_Token[]
     */
    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 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.  This is
 *       easier with our new algorithm.
 */

class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
{

    /**
     * @param HTMLPurifier_Token[] 
$tokens
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return array|HTMLPurifier_Token[]
     */
    public function execute(
$tokens$config$context)
    {

        //####################################################################//
        // Pre-processing

        // O(n) pass to convert to a tree, so that we can efficiently
        // refer to substrings
        
$top_node = HTMLPurifier_Arborize::arborize($tokens$config$context);

        // get a copy of the HTML definition
        
$definition = $config->getHTMLDefinition();

        
$excludes_enabled = !$config->get('Core.DisableExcludes');

        // 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 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($definition->info_parent_def->excludes);

        // variable that contains the start token while we are processing
        // nodes. This enables error reporting to do its job
        
$node = $top_node;
        // dummy token
        list(
$token$d) = $node->toTokenPair();
        
$context->register('CurrentNode', $node);
        
$context->register('CurrentToken', $token);

        //####################################################################//
        // Loop

        // We need to implement a post-order traversal iteratively, to
        // avoid running into stack space limits.  This is pretty tricky
        // to reason about, so we just manually stack-ify the recursive
        // variant:
        //
        //  function f(
$node) {
        //      foreach (
$node->children as $child) {
        //          f(
$child);
        //      }
        //      validate(
$node);
        //  }
        //
        // Thus, we will represent a stack frame as array(
$node,
        // 
$is_inline, stack of children)
        // e.g. array_reverse(
$node->children) - already processed
        // children.

        
$parent_def = $definition->info_parent_def;
        
$stack = array(
            array(
$top_node,
                  
$parent_def->descendants_are_inline,
                  
$parent_def->excludes, // exclusions
                  0)
            );

        while (!empty(
$stack)) {
            list(
$node$is_inline$excludes$ix) = array_pop($stack);
            // recursive call
            
$go = false;
            
$def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name];
            while (isset(
$node->children[$ix])) {
                
$child = $node->children[$ix++];
                if (
$child instanceof HTMLPurifier_Node_Element) {
                    
$go = true;
                    
$stack[] = array($node$is_inline$excludes$ix);
                    
$stack[] = array($child,
                        // ToDo: I don't think it matters if it's def or
                        // child_def, but double check this...
                        
$is_inline || $def->descendants_are_inline,
                        empty(
$def->excludes) ? $excludes
                                              : array_merge(
$excludes$def->excludes),
                        0);
                    break;
                }
            };
            if (
$go) continue;
            list(
$token$d) = $node->toTokenPair();
            // base case
            if (
$excludes_enabled && isset($excludes[$node->name])) {
                
$node->dead = true;
                if (
$e$e->send(E_ERROR, 'Strategy_FixNesting: Node excluded');
            } else {
                // XXX I suppose it would be slightly more efficient to
                // avoid the allocation here and have children
                // strategies handle it
                
$children = array();
                foreach (
$node->children as $child) {
                    if (!
$child->dead$children[] = $child;
                }
                
$result = $def->child->validateChildren($children$config$context);
                if (
$result === true) {
                    // nop
                    
$node->children = $children;
                } elseif (
$result === false) {
                    
$node->dead = true;
                    if (
$e$e->send(E_ERROR, 'Strategy_FixNesting: Node removed');
                } else {
                    
$node->children = $result;
                    if (
$e) {
                        // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators
                        if (empty(
$result) && !empty($children)) {
                            
$e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed');
                        } else if (
$result != $children) {
                            
$e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized');
                        }
                    }
                }
            }
        }

        //####################################################################//
        // Post-processing

        // remove context variables
        
$context->destroy('IsInline');
        
$context->destroy('CurrentNode');
        
$context->destroy('CurrentToken');

        //####################################################################//
        // Return

        return HTMLPurifier_Arborize::flatten(
$node$config$context);
    }
}





/**
 * 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.
     * @type HTMLPurifier_Token[]
     */
    protected 
$tokens;

    /**
     * Current token.
     * @type HTMLPurifier_Token
     */
    protected 
$token;

    /**
     * Zipper managing the true state.
     * @type HTMLPurifier_Zipper
     */
    protected 
$zipper;

    /**
     * Current nesting of elements.
     * @type array
     */
    protected 
$stack;

    /**
     * Injectors active in this stream processing.
     * @type HTMLPurifier_Injector[]
     */
    protected 
$injectors;

    /**
     * Current instance of HTMLPurifier_Config.
     * @type HTMLPurifier_Config
     */
    protected 
$config;

    /**
     * Current instance of HTMLPurifier_Context.
     * @type HTMLPurifier_Context
     */
    protected 
$context;

    /**
     * @param HTMLPurifier_Token[] 
$tokens
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return HTMLPurifier_Token[]
     * @throws HTMLPurifier_Exception
     */
    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 = $definition->info_parent_def->child->getAllowedElements($config);
        
$e = $context->get('ErrorCollector', true);
        
$i = false; // injector index
        list(
$zipper$token) = HTMLPurifier_Zipper::fromArray($tokens);
        if (
$token === NULL) {
            return array();
        }
        
$reprocess = false; // whether or not to reprocess the same token
        
$stack = array();

        // member variables
        
$this->stack =& $stack;
        
$this->tokens =& $tokens;
        
$this->token =& $token;
        
$this->zipper =& $zipper;
        
$this->config = $config;
        
$this->context = $context;

        // context variables
        
$context->register('CurrentNesting', $stack);
        
$context->register('InputZipper', $zipper);
        
$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->nameinjector 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 (;;
             // only increment if we don't need to reprocess
             
$reprocess ? $reprocess = false : $token = $zipper->next($token)) {

            // check for a rewind
            if (is_int(
$i)) {
                // 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_offset = $this->injectors[$i]->getRewindOffset();
                if (is_int(
$rewind_offset)) {
                    for (
$j = 0; $j < $rewind_offset$j++) {
                        if (empty(
$zipper->front)) break;
                        
$token = $zipper->prev($token);
                        // indicate that other injectors should not process this token,
                        // but we need to reprocess it.  See Note [Injector skips]
                        unset(
$token->skip[$i]);
                        
$token->rewind = $i;
                        if (
$token instanceof HTMLPurifier_Token_Start) {
                            array_pop(
$this->stack);
                        } elseif (
$token instanceof HTMLPurifier_Token_End) {
                            
$this->stack[] = $token->start;
                        }
                    }
                }
                
$i = false;
            }

            // handle case of document end
            if (
$token === NULL) {
                // 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
                
$token = new HTMLPurifier_Token_End($top_nesting->name);

                // punt!
                
$reprocess = true;
                continue;
            }

            //echo '<br>'; printZipper(
$zipper$token);//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])) {
                            // See Note [Injector skips]
                            continue;
                        }
                        if (
$token->rewind !== null && $token->rewind !== $i) {
                            continue;
                        }
                        // XXX fuckup
                        
$r = $token;
                        
$injector->handleText($r);
                        
$token = $this->processToken($r$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
                // NB: this assignment is required
                
$old_token = $token;
                
$token = new HTMLPurifier_Token_End($token->name);
                
$token = $this->insertBefore(
                    new HTMLPurifier_Token_Start(
$old_token->name$old_token->attr$old_token->line$old_token->col$old_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;

                    
$parent_def = null;
                    
$parent_elements = null;
                    
$autoclose = false;
                    if (isset(
$definition->info[$parent->name])) {
                        
$parent_def = $definition->info[$parent->name];
                        
$parent_elements = $parent_def->child->getAllowedElements($config);
                        
$autoclose = !isset($parent_elements[$token->name]);
                    }

                    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, <ul><ul> needs a <li> in between)
                        
$wrapname = $definition->info[$token->name]->wrap;
                        
$wrapdef = $definition->info[$wrapname];
                        
$elements = $wrapdef->child->getAllowedElements($config);
                        if (isset(
$elements[$token->name]) && isset($parent_elements[$wrapname])) {
                            
$newtoken = new HTMLPurifier_Token_Start($wrapname);
                            
$token = $this->insertBefore($newtoken);
                            
$reprocess = true;
                            continue;
                        }
                    }

                    
$carryover = false;
                    if (
$autoclose && $parent_def->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;
                            // [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);
                                }
                            }
                            if (
$carryover) {
                                
$element = clone $parent;
                                // [TagClosedAuto]
                                
$element->armor['MakeWellFormed_TagClosedError'] = true;
                                
$element->carryover = true;
                                
$token = $this->processToken(array($new_token$token$element));
                            } else {
                                
$token = $this->insertBefore($new_token);
                            }
                        } else {
                            
$token = $this->remove();
                        }
                        
$reprocess = true;
                        continue;
                    }

                }
                
$ok = true;
            }

            if (
$ok) {
                foreach (
$this->injectors as $i => $injector) {
                    if (isset(
$token->skip[$i])) {
                        // See Note [Injector skips]
                        continue;
                    }
                    if (
$token->rewind !== null && $token->rewind !== $i) {
                        continue;
                    }
                    
$r = $token;
                    
$injector->handleElement($r);
                    
$token = $this->processToken($r$i);
                    
$reprocess = true;
                    break;
                }
                if (!
$reprocess) {
                    // ah, nothing interesting happened; do normal processing
                    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');
                    }
                    
$token = new HTMLPurifier_Token_Text($generator->generateFromToken($token));
                } else {
                    if (
$e) {
                        
$e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
                    }
                    
$token = $this->remove();
                }
                
$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])) {
                        // See Note [Injector skips]
                        continue;
                    }
                    if (
$token->rewind !== null && $token->rewind !== $i) {
                        continue;
                    }
                    
$r = $token;
                    
$injector->handleEnd($r);
                    
$token = $this->processToken($r$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) {
                    if (
$e) {
                        
$e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
                    }
                    
$token = new HTMLPurifier_Token_Text($generator->generateFromToken($token));
                } else {
                    if (
$e) {
                        
$e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
                    }
                    
$token = $this->remove();
                }
                
$reprocess = true;
                continue;
            }

            // do errors, in REVERSE 
$j order: a,b,c with </a></b></c>
            
$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 </a></b></c>
            
$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;
                }
            }
            
$token = $this->processToken($replace);
            
$reprocess = true;
            continue;
        }

        
$context->destroy('CurrentToken');
        
$context->destroy('CurrentNesting');
        
$context->destroy('InputZipper');

        unset(
$this->injectors$this->stack$this->tokens);
        return 
$zipper->toArray($token);
    }

    /**
     * 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 HTMLPurifier_Token|array|int|bool 
$token Token substitution value
     * @param HTMLPurifier_Injector|int 
$injector Injector that performed the substitution; default is if
     *        this is not an injector related operation.
     * @throws HTMLPurifier_Exception
     */
    protected function processToken(
$token$injector = -1)
    {
        // Zend OpCache miscompiles 
$token = array($token), so
        // avoid this pattern.  See: https://github.com/ezyang/htmlpurifier/issues/108

        // normalize forms of token
        if (is_object(
$token)) {
            
$tmp = $token;
            
$token = array(1, $tmp);
        }
        if (is_int(
$token)) {
            
$tmp = $token;
            
$token = array($tmp);
        }
        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);
        list(
$old$r) = $this->zipper->splice($this->token$delete$token);

        if (
$injector > -1) {
            // See Note [Injector skips]
            // Determine appropriate skips.  Here's what the code does:
            //  *If* we deleted one or more tokens, copy the skips
            //  of those tokens into the skips of the new tokens (in 
$token).
            //  Also, mark the newly inserted tokens as having come from
            //  
$injector.
            
$oldskip = isset($old[0]) ? $old[0]->skip : array();
            foreach (
$token as $object) {
                
$object->skip = $oldskip;
                
$object->skip[$injector] = true;
            }
        }

        return 
$r;

    }

    /**
     * Inserts a token before the current token. Cursor now points to
     * this token.  You must reprocess after this.
     * @param HTMLPurifier_Token 
$token
     */
    private function insertBefore(
$token)
    {
        // NB not 
$this->zipper->insertBefore(), due to positioning
        // differences
        
$splice = $this->zipper->splice($this->token, 0, array($token));

        return 
$splice[1];
    }

    /**
     * Removes current token. Cursor now points to new token occupying previously
     * occupied space.  You must reprocess after this.
     */
    private function remove()
    {
        return 
$this->zipper->delete();
    }
}

// Note [Injector skips]
// ~~~~~~~~~~~~~~~~~~~~~
// When I originally designed this class, the idea behind the 'skip'
// property of HTMLPurifier_Token was to help avoid infinite loops
// in injector processing.  For example, suppose you wrote an injector
// that bolded swear words.  Naively, you might write it so that
// whenever you saw ****, you replaced it with <strong>****</strong>.
//
// When this happens, we will reprocess all of the tokens with the
// other injectors.  Now there is an opportunity for infinite loop:
// if we rerun the swear-word injector on these tokens, we might
// see **** and then reprocess again to get
// <strong><strong>****</strong></strong> ad infinitum.
//
// Thus, the idea of a skip is that once we process a token with
// an injector, we mark all of those tokens as having "
come from"
// the injector, and we never run the injector again on these
// tokens.
//
// There were two more complications, however:
//
//  - With HTMLPurifier_Injector_RemoveEmpty, we noticed that if
//    you had <b><i></i></b>, after you removed the <i></i>, you
//    really would like this injector to go back and reprocess
//    the <b> tag, discovering that it is now empty and can be
//    removed.  So we reintroduced the possibility of infinite looping
//    by adding a "
rewind" function, which let you go back to an
//    earlier point in the token stream and reprocess it with injectors.
//    Needless to say, we need to UN-skip the token so it gets
//    reprocessed.
//
//  - Suppose that you successfuly process a token, replace it with
//    one with your skip mark, but now another injector wants to
//    process the skipped token with another token.  Should you continue
//    to skip that new token, or reprocess it?  If you reprocess,
//    you can end up with an infinite loop where one injector converts
//    <a> to <b>, and then another injector converts it back.  So
//    we inherit the skips, but for some reason, I thought that we
//    should inherit the skip from the first token of the token
//    that we deleted.  Why?  Well, it seems to work OK.
//
// If I were to redesign this functionality, I would absolutely not
// go about doing it this way: the semantics are just not very well
// defined, and in any case you probably wanted to operate on trees,
// not token streams.





/**
 * 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
{

    /**
     * @param HTMLPurifier_Token[] 
$tokens
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return array|HTMLPurifier_Token[]
     */
    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
{

    /**
     * @param HTMLPurifier_Token[] 
$tokens
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return HTMLPurifier_Token[]
     */
    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);
        }
        
$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
{
    /**
     * @type string
     */
    public 
$transform_to = 'span';

    /**
     * @type array
     */
    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%'
    );

    /**
     * @param HTMLPurifier_Token_Tag 
$tag
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return HTMLPurifier_Token_End|string
     */
    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
{
    /**
     * @type string
     */
    protected 
$style;

    /**
     * @param string 
$transform_to Tag name to transform to.
     * @param string 
$style CSS style to add to the tag
     */
    public function __construct(
$transform_to$style = null)
    {
        
$this->transform_to = $transform_to;
        
$this->style = $style;
    }

    /**
     * @param HTMLPurifier_Token_Tag 
$tag
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return string
     */
    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
{
    /**
     * Character data within comment.
     * @type string
     */
    public 
$data;

    /**
     * @type bool
     */
    public 
$is_whitespace = true;

    /**
     * Transparent constructor.
     *
     * @param string 
$data String comment data.
     * @param int 
$line
     * @param int 
$col
     */
    public function __construct(
$data$line = null, $col = null)
    {
        
$this->data = $data;
        
$this->line = $line;
        
$this->col = $col;
    }

    public function toNode() {
        return new HTMLPurifier_Node_Comment(
$this->data$this->line$this->col);
    }
}





/**
 * Abstract class of a tag token (start, end or empty), and its behavior.
 */
abstract class HTMLPurifier_Token_Tag extends HTMLPurifier_Token
{
    /**
     * Static bool marker that indicates the class is a tag.
     *
     * This allows us to check objects with <tt>!empty(
$obj->is_tag)</tt>
     * without having to use a function call <tt>is_a()</tt>.
     * @type bool
     */
    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.
     * @type string
     */
    public 
$name;

    /**
     * Associative array of the tag's attributes.
     * @type array
     */
    public 
$attr = array();

    /**
     * Non-overloaded constructor, which lower-cases passed tag name.
     *
     * @param string 
$name String name.
     * @param array 
$attr Associative array of attributes.
     * @param int 
$line
     * @param int 
$col
     * @param array 
$armor
     */
    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;
    }

    public function toNode() {
        return new HTMLPurifier_Node_Element(
$this->name$this->attr$this->line$this->col$this->armor);
    }
}





/**
 * Concrete empty token class.
 */
class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag
{
    public function toNode() {
        
$n = parent::toNode();
        
$n->empty = true;
        return 
$n;
    }
}





/**
 * 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!
     * @type HTMLPurifier_Token
     */
    public 
$start;

    public function toNode() {
        throw new Exception("
HTMLPurifier_Token_End->toNode not supported!");
    }
}





/**
 * 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
{

    /**
     * @type string
     */
    public 
$name = '#PCDATA';
    /**< PCDATA tag name compatible with DTD. */

    /**
     * @type string
     */
    public 
$data;
    /**< Parsed character data of text. */

    /**
     * @type bool
     */
    public 
$is_whitespace;

    /**< Bool indicating if node is whitespace. */

    /**
     * Constructor, accepts data and determines if it is whitespace.
     * @param string 
$data String parsed character data.
     * @param int 
$line
     * @param int 
$col
     */
    public function __construct(
$data$line = null, $col = null)
    {
        
$this->data = $data;
        
$this->is_whitespace = ctype_space($data);
        
$this->line = $line;
        
$this->col = $col;
    }

    public function toNode() {
        return new HTMLPurifier_Node_Text(
$this->data$this->is_whitespace$this->line$this->col);
    }
}





class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter
{
    /**
     * @type string
     */
    public 
$name = 'DisableExternal';

    /**
     * @type array
     */
    protected 
$ourHostParts = false;

    /**
     * @param HTMLPurifier_Config 
$config
     * @return void
     */
    public function prepare(
$config)
    {
        
$our_host = $config->getDefinition('URI')->host;
        if (
$our_host !== null) {
            
$this->ourHostParts = array_reverse(explode('.', $our_host));
        }
    }

    /**
     * @param HTMLPurifier_URI 
$uri Reference
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type string
     */
    public 
$name = 'DisableExternalResources';

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type string
     */
    public 
$name = 'DisableResources';

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type string
     */
    public 
$name = 'HostBlacklist';

    /**
     * @type array
     */
    protected 
$blacklist = array();

    /**
     * @param HTMLPurifier_Config 
$config
     * @return bool
     */
    public function prepare(
$config)
    {
        
$this->blacklist = $config->get('URI.HostBlacklist');
        return true;
    }

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type string
     */
    public 
$name = 'MakeAbsolute';

    /**
     * @type
     */
    protected 
$base;

    /**
     * @type array
     */
    protected 
$basePathStack = array();

    /**
     * @param HTMLPurifier_Config 
$config
     * @return bool
     */
    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;
    }

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
     * @param array 
$stack
     * @return array
     */
    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
{
    /**
     * @type string
     */
    public 
$name = 'Munge';

    /**
     * @type bool
     */
    public 
$post = true;

    /**
     * @type string
     */
    private 
$target;

    /**
     * @type HTMLPurifier_URIParser
     */
    private 
$parser;

    /**
     * @type bool
     */
    private 
$doEmbed;

    /**
     * @type string
     */
    private 
$secretKey;

    /**
     * @type array
     */
    protected 
$replace = array();

    /**
     * @param HTMLPurifier_Config 
$config
     * @return bool
     */
    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');
        if (
$this->secretKey && !function_exists('hash_hmac')) {
            throw new Exception("
Cannot use %URI.MungeSecretKey without hash_hmac support.");
        }
        return true;
    }

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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;
    }

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     */
    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'] = hash_hmac("sha256", $string$this->secretKey);
        }
    }
}





/**
 * 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
{
    /**
     * @type string
     */
    public 
$name = 'SafeIframe';

    /**
     * @type bool
     */
    public 
$always_load = true;

    /**
     * @type string
     */
    protected 
$regexp = null;

    // XXX: The not so good bit about how this is all set up now is we
    // can't check HTML.SafeIframe in the 'prepare' step: we have to
    // defer till the actual filtering.
    /**
     * @param HTMLPurifier_Config 
$config
     * @return bool
     */
    public function prepare(
$config)
    {
        
$this->regexp = $config->get('URI.SafeIframeRegexp');
        return true;
    }

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type bool
     */
    public 
$browsable = true;

    /**
     * @type array
     */
    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
    /**
     * @type bool
     */
    public 
$may_omit_host = true;

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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;
        }
        if ( strlen(
$raw_data) < 12 ) {
            // error; exif_imagetype throws exception with small files,
            // and this likely indicates a corrupt URI/failed parse anyway
            return false;
        }
        // XXX probably want to refactor this into a general mechanism
        // for filtering arbitrary content types
        if (function_exists('sys_get_temp_dir')) {
            
$file = tempnam(sys_get_temp_dir(), "");
        } else {
            
$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;
    }

    /**
     * @param int 
$errno
     * @param string 
$errstr
     */
    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.
     * @type bool
     */
    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.
     * @type bool
     */
    public 
$may_omit_host = true;

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type int
     */
    public 
$default_port = 21;

    /**
     * @type bool
     */
    public 
$browsable = true; // usually

    /**
     * @type bool
     */
    public 
$hierarchical = true;

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type int
     */
    public 
$default_port = 80;

    /**
     * @type bool
     */
    public 
$browsable = true;

    /**
     * @type bool
     */
    public 
$hierarchical = true;

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type int
     */
    public 
$default_port = 443;
    /**
     * @type bool
     */
    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
{
    /**
     * @type bool
     */
    public 
$browsable = false;

    /**
     * @type bool
     */
    public 
$may_omit_host = true;

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type bool
     */
    public 
$browsable = false;

    /**
     * @type bool
     */
    public 
$may_omit_host = true;

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    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
{
    /**
     * @type int
     */
    public 
$default_port = 119;

    /**
     * @type bool
     */
    public 
$browsable = false;

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    public function doValidate(&
$uri$config$context)
    {
        
$uri->userinfo = null;
        
$uri->query = null;
        return true;
    }
}





/**
 * Validates tel (for phone numbers).
 *
 * The relevant specifications for this protocol are RFC 3966 and RFC 5341,
 * but this class takes a much simpler approach: we normalize phone
 * numbers so that they only include (possibly) a leading plus,
 * and then any number of digits and x'es.
 */

class HTMLPurifier_URIScheme_tel extends HTMLPurifier_URIScheme
{
    /**
     * @type bool
     */
    public 
$browsable = false;

    /**
     * @type bool
     */
    public 
$may_omit_host = true;

    /**
     * @param HTMLPurifier_URI 
$uri
     * @param HTMLPurifier_Config 
$config
     * @param HTMLPurifier_Context 
$context
     * @return bool
     */
    public function doValidate(&
$uri$config$context)
    {
        
$uri->userinfo = null;
        
$uri->host     = null;
        
$uri->port     = null;

        // Delete all non-numeric characters, non-x characters
        // from phone number, EXCEPT for a leading plus sign.
        
$uri->path = preg_replace('/(?!^+)[^dx]/', '',
                     // Normalize e(x)tension to lower-case
                     str_replace('X', 'x', 
$uri->path));

        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
{
    /**
     * @param mixed 
$var
     * @param int 
$type
     * @param bool 
$allow_null
     * @return array|bool|float|int|mixed|null|string
     * @throws HTMLPurifier_VarParserException
     */
    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('/(,|[nr]+)/', $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
{

    /**
     * @param mixed 
$var
     * @param int 
$type
     * @param bool 
$allow_null
     * @return null|string
     */
    protected function parseImplementation(
$var$type$allow_null)
    {
        return 
$this->evalExpression($var);
    }

    /**
     * @param string 
$expr
     * @return mixed
     * @throws HTMLPurifier_VarParserException
     */
    protected function evalExpression(
$expr)
    {
        
$var = null;
        
$result = eval("$var $expr;");
        if (
$result === false) {
            throw new HTMLPurifier_VarParserException("
Fatal error in evaluated code");
        }
        return 
$var;
    }
}
Онлайн: 1
Реклама