diff --git a/Doxyfile b/Doxyfile index 4e548067..c96f6773 100644 --- a/Doxyfile +++ b/Doxyfile @@ -4,7 +4,7 @@ # Project related configuration options #--------------------------------------------------------------------------- PROJECT_NAME = HTML Purifier -PROJECT_NUMBER = 1.6.1 +PROJECT_NUMBER = 2.0.0 OUTPUT_DIRECTORY = "C:/Documents and Settings/Edward/My Documents/My Webs/htmlpurifier/docs/doxygen" CREATE_SUBDIRS = NO OUTPUT_LANGUAGE = English diff --git a/INSTALL.fr.utf8 b/INSTALL.fr.utf8 index 625078e2..f260ddbc 100644 --- a/INSTALL.fr.utf8 +++ b/INSTALL.fr.utf8 @@ -17,7 +17,7 @@ ce document pour quelques choses. 1. Compatibilité -HTML Purifier fonctionne dans PHP 4 et PHP 5. PHP 4.3.9 est le dernier +HTML Purifier fonctionne dans PHP 4 et PHP 5. PHP 4.3.2 est le dernier version que je le testais. Il ne dépend de les autre librairies. Les extensions optionnel est iconv (en général déjà installer) et diff --git a/VERSION b/VERSION index 2eda823f..359a5b95 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.1 \ No newline at end of file +2.0.0 \ No newline at end of file diff --git a/WHATSNEW b/WHATSNEW index 7ce6b516..0a971637 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -1,7 +1,7 @@ -The 1.6.1 release, code-named 'Ach! We missed something! Run!', completes -HTML Purifier's roster of attribute transformations. It also implements -a number of minor features (such as better font transformations, smarter -HTML parsing, the CSS property 'white-space' and XHTML 1.1), a few bug -fixes (most notably fixed __autoload compatibility issues) and a ton -of refactoring. 1.6 was for things that absolutely could not wait: this -release, developed in a more leisurely pace, fills in the gaps. \ No newline at end of file +HTML Purifier 2.0 is the culmination of two major architectural changes. +The first is Tidy, which enables HTML Purifier to both natively support +deprecated elements and also convert them to standards-compliant +alternatives. The second is the Advanced API, which enables users to +create new elements and attributes with ease. Keeping in line with a +commitment to high quality, there are also five esoteric bug-fixes and a +plethora of subtle improvements that enhance the library. diff --git a/WYSIWYG b/WYSIWYG index 718f8959..097bd243 100644 --- a/WYSIWYG +++ b/WYSIWYG @@ -16,7 +16,3 @@ trouble. Therein lies the solution: HTML Purifier is perfect for filtering pure-HTML input from WYSIWYG editors. Enough said. - -There is a proof-of-concept integration of HTML Purifier with the Mantis -bugtracker at http://hp.jpsband.org/mantis/ You can see notes on how -this integration was acheived at http://hp.jpsband.org/mantis_notes.txt diff --git a/configdoc/generate.php b/configdoc/generate.php index a5b06e96..80eacd76 100644 --- a/configdoc/generate.php +++ b/configdoc/generate.php @@ -2,216 +2,37 @@ /** * Generates XML and HTML documents describing configuration. + * @note PHP 5 only! */ /* TODO: -- make XML format richer (see below) +- make XML format richer (see XMLSerializer_ConfigSchema) - extend XSLT transformation (see the corresponding XSLT file) - allow generation of packaged docs that can be easily moved - multipage documentation - determine how to multilingualize -- factor out code into classes +- add blurbs to ToC */ -// --------------------------------------------------------------------------- -// Check and configure environment - if (version_compare('5', PHP_VERSION, '>')) exit('Requires PHP 5 or higher.'); -error_reporting(E_ALL); - - -// --------------------------------------------------------------------------- -// Include HTML Purifier library +error_reporting(E_ALL); // probably not possible to use E_STRICT +// load dual-libraries require_once '../library/HTMLPurifier.auto.php'; - - -// --------------------------------------------------------------------------- -// Setup convenience functions - -function appendHTMLDiv($document, $node, $html) { - global $purifier; - $html = $purifier->purify($html); - $dom_html = $document->createDocumentFragment(); - $dom_html->appendXML($html); - - $dom_div = $document->createElement('div'); - $dom_div->setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); - $dom_div->appendChild($dom_html); - - $node->appendChild($dom_div); -} - - -// --------------------------------------------------------------------------- -// Load copies of HTMLPurifier_ConfigDef and HTMLPurifier +require_once 'library/ConfigDoc.auto.php'; $schema = HTMLPurifier_ConfigSchema::instance(); -$purifier = new HTMLPurifier(); +$style = 'plain'; // use $_GET in the future +$configdoc = new ConfigDoc(); +$output = $configdoc->generate($schema, $style); - -// --------------------------------------------------------------------------- -// Generate types.xml, a document describing the constraint "type" - -$types_document = new DOMDocument('1.0', 'UTF-8'); -$types_root = $types_document->createElement('types'); -$types_document->appendChild($types_root); -$types_document->formatOutput = true; -foreach ($schema->types as $name => $expanded_name) { - $types_type = $types_document->createElement('type', $expanded_name); - $types_type->setAttribute('id', $name); - $types_root->appendChild($types_type); -} -$types_document->save('types.xml'); - - -// --------------------------------------------------------------------------- -// Generate configdoc.xml, a document documenting configuration directives - -$dom_document = new DOMDocument('1.0', 'UTF-8'); -$dom_root = $dom_document->createElement('configdoc'); -$dom_document->appendChild($dom_root); -$dom_document->formatOutput = true; - -// add the name of the application -$dom_root->appendChild($dom_document->createElement('title', 'HTML Purifier')); - -/* -TODO for XML format: -- create a definition (DTD or other) once interface stabilizes -*/ - -foreach($schema->info as $namespace_name => $namespace_info) { - - $dom_namespace = $dom_document->createElement('namespace'); - $dom_root->appendChild($dom_namespace); - - $dom_namespace->setAttribute('id', $namespace_name); - $dom_namespace->appendChild( - $dom_document->createElement('name', $namespace_name) - ); - $dom_namespace_description = $dom_document->createElement('description'); - $dom_namespace->appendChild($dom_namespace_description); - appendHTMLDiv($dom_document, $dom_namespace_description, - $schema->info_namespace[$namespace_name]->description); - - foreach ($namespace_info as $name => $info) { - - if ($info->class == 'alias') continue; - - $dom_directive = $dom_document->createElement('directive'); - $dom_namespace->appendChild($dom_directive); - - $dom_directive->setAttribute('id', $namespace_name . '.' . $name); - $dom_directive->appendChild( - $dom_document->createElement('name', $name) - ); - - $dom_constraints = $dom_document->createElement('constraints'); - $dom_directive->appendChild($dom_constraints); - - $dom_type = $dom_document->createElement('type', $info->type); - if ($info->allow_null) { - $dom_type->setAttribute('allow-null', 'yes'); - } - $dom_constraints->appendChild($dom_type); - - if ($info->allowed !== true) { - $dom_allowed = $dom_document->createElement('allowed'); - $dom_constraints->appendChild($dom_allowed); - foreach ($info->allowed as $allowed => $bool) { - $dom_allowed->appendChild( - $dom_document->createElement('value', $allowed) - ); - } - } - - $raw_default = $schema->defaults[$namespace_name][$name]; - if (is_bool($raw_default)) { - $default = $raw_default ? 'true' : 'false'; - } elseif (is_string($raw_default)) { - $default = "\"$raw_default\""; - } elseif (is_null($raw_default)) { - $default = 'null'; - } else { - $default = print_r( - $schema->defaults[$namespace_name][$name], true - ); - } - - $dom_default = $dom_document->createElement('default', $default); - - // remove this once we get a DTD - $dom_default->setAttribute('xml:space', 'preserve'); - - $dom_constraints->appendChild($dom_default); - - $dom_descriptions = $dom_document->createElement('descriptions'); - $dom_directive->appendChild($dom_descriptions); - - foreach ($info->descriptions as $file => $file_descriptions) { - foreach ($file_descriptions as $line => $description) { - $dom_description = $dom_document->createElement('description'); - $dom_description->setAttribute('file', $file); - $dom_description->setAttribute('line', $line); - appendHTMLDiv($dom_document, $dom_description, $description); - $dom_descriptions->appendChild($dom_description); - } - } - - } - -} - -// print_r($dom_document->saveXML()); - -// save a copy of the raw XML -$dom_document->save('configdoc.xml'); - - -// --------------------------------------------------------------------------- -// Generate final output using XSLT - -// load the stylesheet -$xsl_stylesheet_name = 'plain'; -$xsl_stylesheet = "styles/$xsl_stylesheet_name.xsl"; -$xsl_dom_stylesheet = new DOMDocument(); -$xsl_dom_stylesheet->load($xsl_stylesheet); - -// setup the XSLT processor -$xsl_processor = new XSLTProcessor(); - -// perform the transformation -$xsl_processor->importStylesheet($xsl_dom_stylesheet); -$html_output = $xsl_processor->transformToXML($dom_document); - -// some slight fudges to preserve backwards compatibility -$html_output = str_replace('/>', ' />', $html_output); //
not
-$html_output = str_replace(' xmlns=""', '', $html_output); // rm unnecessary xmlns - -if (class_exists('Tidy')) { - // cleanup output - $config = array( - 'indent' => true, - 'output-xhtml' => true, - 'wrap' => 80 - ); - $tidy = new Tidy; - $tidy->parseString($html_output, $config, 'utf8'); - $tidy->cleanRepair(); - $html_output = (string) $tidy; -} - -// write it to a file (todo: parse into seperate pages) -file_put_contents("$xsl_stylesheet_name.html", $html_output); - - -// --------------------------------------------------------------------------- -// Output for instant feedback +// write out +file_put_contents("$style.html", $output); if (php_sapi_name() != 'cli') { - echo $html_output; + // output = instant feedback + echo $output; } else { echo 'Files generated successfully.'; } diff --git a/configdoc/library/ConfigDoc.auto.php b/configdoc/library/ConfigDoc.auto.php new file mode 100644 index 00000000..8dbe120b --- /dev/null +++ b/configdoc/library/ConfigDoc.auto.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/configdoc/library/ConfigDoc.php b/configdoc/library/ConfigDoc.php new file mode 100644 index 00000000..2e2ae610 --- /dev/null +++ b/configdoc/library/ConfigDoc.php @@ -0,0 +1,39 @@ +serialize($schema); + $types_document->save(dirname(__FILE__) . '/../types.xml'); // only ONE + + // generate configdoc.xml, documents configuration directives + $schema_serializer = new ConfigDoc_XMLSerializer_ConfigSchema(); + $schema_document = $schema_serializer->serialize($schema); + $schema_document->save('configdoc.xml'); + + // setup transformation + $xsl_stylesheet = dirname(__FILE__) . "/../styles/$xsl_stylesheet_name.xsl"; + $xslt_processor = new ConfigDoc_HTMLXSLTProcessor(); + $xslt_processor->setParameters($parameters); + $xslt_processor->importStylesheet($xsl_stylesheet); + + return $xslt_processor->transformToHTML($schema_document); + } + + /** + * Remove any generated files + */ + function cleanup() { + unlink('configdoc.xml'); + } + +} + +?> \ No newline at end of file diff --git a/configdoc/library/ConfigDoc/HTMLXSLTProcessor.php b/configdoc/library/ConfigDoc/HTMLXSLTProcessor.php new file mode 100644 index 00000000..64966c5b --- /dev/null +++ b/configdoc/library/ConfigDoc/HTMLXSLTProcessor.php @@ -0,0 +1,62 @@ +xsltProcessor = new XSLTProcessor(); + } + + /** + * Imports stylesheet for processor to use + * @param $xsl XSLT DOM tree, or filename of the XSL transformation + */ + public function importStylesheet($xsl) { + if (is_string($xsl)) { + $xsl_file = $xsl; + $xsl = new DOMDocument(); + $xsl->load($xsl_file); + } + return $this->xsltProcessor->importStylesheet($xsl); + } + + /** + * Transforms an XML file into HTML based on the stylesheet + * @param $xml XML DOM tree + */ + public function transformToHTML($xml) { + $out = $this->xsltProcessor->transformToXML($xml); + + // fudges for HTML backwards compatibility + $out = str_replace('/>', ' />', $out); //
not
+ $out = str_replace(' xmlns=""', '', $out); // rm unnecessary xmlns + if (class_exists('Tidy')) { + // cleanup output + $config = array( + 'indent' => true, + 'output-xhtml' => true, + 'wrap' => 80 + ); + $tidy = new Tidy; + $tidy->parseString($out, $config, 'utf8'); + $tidy->cleanRepair(); + $out = (string) $tidy; + } + return $out; + } + + public function setParameters($options) { + foreach ($options as $name => $value) { + $this->xsltProcessor->setParameter('', $name, $value); + } + } + +} + +?> \ No newline at end of file diff --git a/configdoc/library/ConfigDoc/XMLSerializer.php b/configdoc/library/ConfigDoc/XMLSerializer.php new file mode 100644 index 00000000..cbab5d3b --- /dev/null +++ b/configdoc/library/ConfigDoc/XMLSerializer.php @@ -0,0 +1,26 @@ +purify($html); + $dom_html = $document->createDocumentFragment(); + $dom_html->appendXML($html); + + $dom_div = $document->createElement('div'); + $dom_div->setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $dom_div->appendChild($dom_html); + + $node->appendChild($dom_div); + } + +} + +?> \ No newline at end of file diff --git a/configdoc/library/ConfigDoc/XMLSerializer/ConfigSchema.php b/configdoc/library/ConfigDoc/XMLSerializer/ConfigSchema.php new file mode 100644 index 00000000..57675a2a --- /dev/null +++ b/configdoc/library/ConfigDoc/XMLSerializer/ConfigSchema.php @@ -0,0 +1,118 @@ +createElement('configdoc'); + $dom_document->appendChild($dom_root); + $dom_document->formatOutput = true; + + // add the name of the application + $dom_root->appendChild($dom_document->createElement('title', 'HTML Purifier')); + + /* + TODO for XML format: + - create a definition (DTD or other) once interface stabilizes + */ + + foreach($schema->info as $namespace_name => $namespace_info) { + + $dom_namespace = $dom_document->createElement('namespace'); + $dom_root->appendChild($dom_namespace); + + $dom_namespace->setAttribute('id', $namespace_name); + $dom_namespace->appendChild( + $dom_document->createElement('name', $namespace_name) + ); + $dom_namespace_description = $dom_document->createElement('description'); + $dom_namespace->appendChild($dom_namespace_description); + $this->appendHTMLDiv($dom_document, $dom_namespace_description, + $schema->info_namespace[$namespace_name]->description); + + foreach ($namespace_info as $name => $info) { + + if ($info->class == 'alias') continue; + + $dom_directive = $dom_document->createElement('directive'); + $dom_namespace->appendChild($dom_directive); + + $dom_directive->setAttribute('id', $namespace_name . '.' . $name); + $dom_directive->appendChild( + $dom_document->createElement('name', $name) + ); + + $dom_constraints = $dom_document->createElement('constraints'); + $dom_directive->appendChild($dom_constraints); + + $dom_type = $dom_document->createElement('type', $info->type); + if ($info->allow_null) { + $dom_type->setAttribute('allow-null', 'yes'); + } + $dom_constraints->appendChild($dom_type); + + if ($info->allowed !== true) { + $dom_allowed = $dom_document->createElement('allowed'); + $dom_constraints->appendChild($dom_allowed); + foreach ($info->allowed as $allowed => $bool) { + $dom_allowed->appendChild( + $dom_document->createElement('value', $allowed) + ); + } + } + + $raw_default = $schema->defaults[$namespace_name][$name]; + if (is_bool($raw_default)) { + $default = $raw_default ? 'true' : 'false'; + } elseif (is_string($raw_default)) { + $default = "\"$raw_default\""; + } elseif (is_null($raw_default)) { + $default = 'null'; + } else { + $default = print_r( + $schema->defaults[$namespace_name][$name], true + ); + } + + $dom_default = $dom_document->createElement('default', $default); + + // remove this once we get a DTD + $dom_default->setAttribute('xml:space', 'preserve'); + + $dom_constraints->appendChild($dom_default); + + $dom_descriptions = $dom_document->createElement('descriptions'); + $dom_directive->appendChild($dom_descriptions); + + foreach ($info->descriptions as $file => $file_descriptions) { + foreach ($file_descriptions as $line => $description) { + $dom_description = $dom_document->createElement('description'); + // refuse to write $file if it's a full path + if (str_replace('\\', '/', realpath($file)) != $file) { + $dom_description->setAttribute('file', $file); + $dom_description->setAttribute('line', $line); + } + $this->appendHTMLDiv($dom_document, $dom_description, $description); + $dom_descriptions->appendChild($dom_description); + } + } + + } + + } + + return $dom_document; + + } + +} + +?> \ No newline at end of file diff --git a/configdoc/library/ConfigDoc/XMLSerializer/Types.php b/configdoc/library/ConfigDoc/XMLSerializer/Types.php new file mode 100644 index 00000000..a3d4716c --- /dev/null +++ b/configdoc/library/ConfigDoc/XMLSerializer/Types.php @@ -0,0 +1,27 @@ +createElement('types'); + $types_document->appendChild($types_root); + $types_document->formatOutput = true; + foreach ($schema->types as $name => $expanded_name) { + $types_type = $types_document->createElement('type', $expanded_name); + $types_type->setAttribute('id', $name); + $types_root->appendChild($types_type); + } + return $types_document; + } + +} + +?> \ No newline at end of file diff --git a/configdoc/styles/plain.xsl b/configdoc/styles/plain.xsl index cfd5a7ca..3952bf0d 100644 --- a/configdoc/styles/plain.xsl +++ b/configdoc/styles/plain.xsl @@ -12,19 +12,21 @@ indent = "no" media-type = "text/html" /> + + - Configuration Documentation - <xsl:value-of select="/configdoc/title" /> + <xsl:value-of select="$title" /> - <xsl:value-of select="/configdoc/title" /> - +
-

Configuration Documentation

+

Table of Contents

    @@ -76,15 +78,17 @@ - - - - + + + + + +
    Used by: - - , - - -
    Used by: + + , + + +
    diff --git a/docs/dev-advanced-api.html b/docs/dev-advanced-api.html index a9d9f745..9cbe6366 100644 --- a/docs/dev-advanced-api.html +++ b/docs/dev-advanced-api.html @@ -17,9 +17,12 @@
    HTML Purifier End-User Documentation

    HTML Purifier currently natively supports only a subset of HTML's -allowed elements, attributes, and behavior. This is by design, -but as the user is always right, they'll need some method to overload -these behaviors.

    +allowed elements, attributes, and behavior; specifically, this subset +is the set of elements that are safe for untrusted users to use. +However, HTML Purifier is often utilized to ensure standards-compliance +from input that is trusted (making it a sort of Tidy substitute), +and often users need to define new elements or attributes. The +advanced API is oriented specifically for these use-cases.

    Our goals are to let the user:

    @@ -27,20 +30,15 @@ these behaviors.

    Select
    • Doctype
    • -
    • Mode: Lenient / Correctional
    • +
    • Elements / Attributes / Modules
    • -
    • Filterset
    • +
    • Tidy
    Customize
    • Attributes
    • Elements
    • -
    -
    Internals
    -
      -
    • Modules / Elements / Attributes / Attribute Types
    • -
    • Filtersets
    • -
    • Doctype
    • +
    @@ -68,136 +66,64 @@ Transitional, however, we really shouldn't be guessing what the user's doctype is. Fortunantely, people who can't be bothered to set this won't be bothered when their pages stop validating.

    -

    Selecting Mode

    - -

    Within doctypes, there are various modes of operation. -These indicate variant behaviors that, while not strictly changing the -allowed set of elements and attributes, definitely affect the output. -Currently, we have two modes, which may be used together:

    - -
    -
    Lenient
    -
    -

    Deprecated elements and attributes will be transformed into - standards-compliant alternatives when explicitly disallowed.

    -

    For example, in the XHTML 1.0 Strict doctype, a center - element would be turned into a div with the CSS property - text-align:center;, but in XHTML 1.0 Transitional - the element would be preserved.

    -

    This mode is on by default.

    -
    -
    Correctional[items to correct]
    -
    -

    Deprecated elements and attributes will be transformed into - standards-compliant alternatives whenever possible. - It may have various levels of operation.

    -

    Referring back to the previous example, the center element would - be transformed in both cases. However, elements without a - reasonable standards-compliant alternative will be preserved - in their form.

    -

    A user may want to correct certain deprecated attributes, but - not others. For example, the bgcolor attribute may be - acceptable, but the center element not; also, possibly, - an HTML Purifier transformation may be buggy, so the user wants - to forgo it. Thus, correctional accepts an array defining which - elements and attributes to cleanup, or no parameter at all, which - means everything gets corrected. This also means that each - correction needs to be given a unique ID that can be referenced - in this manner. (We may also allow globbing, like *.name or a.* - for mass-enabling correction, and subtractive mode, where things - specified stop correction.) This array gets passed into the - constructor of the mode's module.

    -

    This mode is on by default.

    -
    -
    - -

    A possible call to select modes would be:

    - -
    $config->set('HTML', 'Mode', array('correctional', 'lenient'));
    - -

    If modes have extra parameters, a hash is necessary:

    - -
    $config->set('HTML', 'Mode', array(
    -    'correctional' => 'center,a.name',
    -    'lenient' => true // this one's just boolean
    -));
    - -

    Modes may be specified along with the doctype declaration (we may want -to get a better set of separator characters):

    - -
    $config->setDoctype('XHTML Transitional 1.0', '+correctional[center,a.name] -lenient');
    - -

    -With regards to the various levels of operation conjectured in the -Correctional mode, this is prompted by the fact that a user may want to -correct certain problems but not others, for example, fix the center -element but not the u element, both of which are deprecated. -Having an integer level will not work very well for such fine -grained tweaking, but an array of specific settings might.

    -

    Selecting Elements / Attributes / Modules

    -

    +

    HTML Purifier will, by default, allow as many elements and attributes +as possible. However, a user may decide to roll their own filterset by +selecting modules, elements and attributes to allow for their own +specific use-case. This can be done using %HTML.Allowed:

    -

    If this cookie cutter approach doesn't appeal to a user, they may -decide to roll their own filterset by selecting modules, elements and -attributes to allow.

    +
    $config->set('HTML', 'Allowed', 'a[href|title],em,p,blockquote');
    -

    This would make use of the same facilities -as a filterset author would use, except that it would go under an -anonymous filterset that would be auto-selected if any of the -relevant module/elements/attribute selection configuration directives were -non-null.

    +

    The directive %HTML.Allowed is a convenience feature +that may be fully expressed with the legacy interface.

    -

    In practice, this is the most commonly demanded feature. Most users are -perfectly happy defining a filterset that looks like:

    - -
    $config->setAllowedHTML('a[href,title];em;p;blockquote');
    - -

    The directive %HTML.Allowed is a convenience function -that may be fully expressed with the legacy interface, and thus is -given its own setter.

    - -

    We currently support a separated interface, which also must be preserved:

    +

    We currently support another interface from older versions:

    $config->set('HTML', 'AllowedElements', 'a,em,p,blockquote');
     $config->set('HTML', 'AllowedAttributes', 'a.href,a.title');
    -

    A user may also choose to allow modules:

    +

    A user may also choose to allow modules using a specialized +directive:

    -
    $config->set('HTML', 'AllowedModules', 'Hypertext,Text,Lists'); // or
    -$config->setAllowedHTML('Hypertext,Text,Lists');
    +
    $config->set('HTML', 'AllowedModules', 'Hypertext,Text,Lists');

    But it is not expected that this feature will be widely used.

    -

    The granularity of these modules is too coarse for -the average user (for example, the core module loads everything from -the essential p element to the not-so-safe h1 -element). How do we make this still a viable solution? Possible answers -may be sub-modules or module parameters. This may not even be a problem, -considering that most people won't be selecting modules.

    +

    Module selection will work slightly differently +from the other AllowedElements and AllowedAttributes directives by +directly modifying the doctype you are operating in, in the spirit of +XHTML 1.1's modularization. We stop users from shooting themselves in the +foot by mandating the modules in %HTML.CoreModules be used.

    Modules are distinguished from regular elements by the case of their first letter. While XML distinguishes between and allows -lower and uppercase letters in element names, most well-known XML -languages use only lower-case +lower and uppercase letters in element names, XHTML uses only lower-case element names for sake of consistency.

    -

    Considering that, internally speaking, as mandated by -the XHTML 1.1 Modularization specification, we have organized our -elements around modules, considerable gymnastics will be needed to -get this sort of functionality working.

    +

    Selecting Tidy

    +

    The name of this segment of functionality is inspired off of Dave +Ragget's program HTML Tidy, which purported to help clean up HTML. In +HTML Purifier, Tidy functionality involves turning unsupported and +deprecated elements into standards-compliant ones, maintaining +backwards compatibility, and enforcing best practices.

    + +

    This is a complicated feature, and is explained more in depth at +the Tidy documentation page.

    + +

    Customize

    @@ -209,38 +135,34 @@ use-cases.

    Note that the functions described here are only available if a raw copy of HTMLPurifier_HTMLDefinition was retrieved. -addAttribute may work on a processed copy, but for -consistency's sake we will mandate this for everything.

    +Furthermore, caching may prevent your changes from immediately +being seen: consult enduser-customize.html on how +to work around this.

    Attributes

    An attribute is bound to an element by a name and has a specific -AttrDef that validates it. Thus, the interface should -be:

    +AttrDef that validates it. The interface is therefore:

    function addAttribute($element, $attribute, $attribute_def);
    -

    With a use-case that looks like:

    +

    Example of the functionality in action:

    -
    $def->addAttribute('a', 'rel', new HTMLPurifier_AttrDef_Enum(array('nofollow')));
    +
    $def->addAttribute('a', 'rel', 'Enum#nofollow');
    -

    The $attribute_def value can be a little flexible, -to make things simpler. We'll let it also be:

    +

    The $attribute_def value is flexible, +to make things simpler. It can be a literal object or:

      -
    • Class name: We'll instantiate it for you
    • +
    • String attribute type: We'll use HTMLPurifier_AttrTypes -
    • -
    • String starting with enum(: We'll explode it and stuff it in an - HTMLPurifier_AttrDef_Enum for you.
    • + to resolve it for you. Any data that follows a hash mark (#) will + be used to customize the attribute type: in the example above, + we specify which values for Enum to allow.
    -

    Making the previous example written as:

    - -
    $def->addAttribute('a', 'rel', 'enum(nofollow)');
    -

    Elements

    An element requires certain information as specified by @@ -255,7 +177,8 @@ the usual things required are:

    This suggests an API like this:

    -
    function addElement($element, $type, $content_model, $attributes = array());
    +
    function addElement($element, $type, $contents,
    +    $attr_collections = array(); $attributes = array());

    Each parameter explained in depth:

    @@ -264,11 +187,15 @@ the usual things required are:

    Element name, ex. 'label'
    $type
    Content set to register in, ex. 'Inline' or 'Flow'
    -
    $content_model
    +
    $contents
    Description of allowed children. This is a merged form of HTMLPurifier_ElementDef's member variables $content_model and $content_model_type, - where the form is Type: Model, ex. 'Optional: Inline'.
    + where the form is Type: Model, ex. 'Optional: Inline'. + There are also a number of predefined templates one may use. +
    $attr_collections
    +
    Array (or string if only one) of attribute collection(s) to + merge into the attributes array.
    $attributes
    Array of attribute names to attribute definitions, much like the above-described attribute customization.
    @@ -276,11 +203,10 @@ the usual things required are:

    A possible usage:

    -
    $def->addElement('font', 'Inline', 'Optional: Inline',
    -    array(0 => array('Common'), 'color' => 'Color'));
    +
    $def->addElement('font', 'Inline', 'Optional: Inline', 'Common',
    +    array('color' => 'Color'));
    -

    We may want to Common attribute collection inclusion to be added -by default.

    +

    See HTMLPurifier/HTMLModule.php for details.

    $Id$
    diff --git a/docs/enduser-customize.html b/docs/enduser-customize.html new file mode 100644 index 00000000..672e92c6 --- /dev/null +++ b/docs/enduser-customize.html @@ -0,0 +1,791 @@ + + + + + + + +Customize - HTML Purifier + + + +

    Customize!

    +
    HTML Purifier is a Swiss-Army Knife
    + +
    Filed under End-User
    +
    Return to the index.
    +
    HTML Purifier End-User Documentation
    + +
    + This document covers currently unreleased functionality and + only applies to recent SVN checkouts. +
    + +

    + You may have heard of the Advanced API. + If you're interested in reading dry prose and boring functional + specifications, feel free to click that link to get a no-nonsense overview + on the Advanced API. For the rest of us, there's this tutorial. By the time + you're finished reading this, you should have a pretty good idea on + how to implement custom tags and attributes that HTML Purifier may not have. +

    + +

    Is it necessary?

    + +

    + Before we even write any code, it is paramount to consider whether or + not the code we're writing is necessary or not. HTML Purifier, by default, + contains a large set of elements and attributes: large enough so that + any element or attribute in XHTML 1.0 (and its HTML variant) + that can be safely used by the general public is implemented. +

    + +

    + So what needs to be implemented? (Feel free to skip this section if + you know what you want). +

    + +

    XHTML 1.0

    + +

    + All of the modules listed below are based off of the + modularization of + XHTML, which, while technically for XHTML 1.1, is quite a useful + resource. +

    + +
      +
    • Structure
    • +
    • Frames
    • +
    • Applets (deprecated)
    • +
    • Forms
    • +
    • Image maps
    • +
    • Objects
    • +
    • Frames
    • +
    • Events
    • +
    • Meta-information
    • +
    • Style sheets
    • +
    • Link (not hypertext)
    • +
    • Base
    • +
    • Name
    • +
    + +

    + If you don't recognize it, you probably don't need it. But the curious + can look all of these modules up in the above-mentioned document. Note + that inline scripting comes packaged with HTML Purifier (more on this + later). +

    + +

    XHTML 1.1

    + +

    + We have not implemented the + Ruby module, + which defines a set of tags + for publishing short annotations for text, used mostly in Japanese + and Chinese school texts. +

    + +

    XHTML 2.0

    + +

    + XHTML 2.0 is still a + working draft, so any elements introduced in the + specification have not been implemented and will not be implemented + until we get a recommendation or proposal. Because XHTML 2.0 is + an entirely new markup language, implementing rules for it will be + no easy task. +

    + +

    HTML 5

    + +

    + HTML 5 + is a fork of HTML 4.01 by WHATWG, who believed that XHTML 2.0 was headed + in the wrong direction. It too is a working draft, and may change + drastically before publication, but it should be noted that the + canvas tag has been implemented by many browser vendors. +

    + +

    Proprietary

    + +

    + There are a number of proprietary tags still in the wild. Many of them + have been documented in ref-proprietary-tags.txt, + but there is currently no implementation for any of them. +

    + +

    Extensions

    + +

    + There are also a number of other XML languages out there that can + be embedded in HTML documents: two of the most popular are MathML and + SVG, and I frequently get requests to implement these. But they are + expansive, comprehensive specifications, and it would take far too long + to implement them correctly (most systems I've seen go as far + as whitelisting tags and no further; come on, what about nesting!) +

    + +

    + Word of warning: HTML Purifier is currently not namespace + aware. +

    + +

    Giving back

    + +

    + As you may imagine from the details above (don't be abashed if you didn't + read it all: a glance over would have done), there's quite a bit that + HTML Purifier doesn't implement. Recent architectural changes have + allowed HTML Purifier to implement elements and attributes that are not + safe! Don't worry, they won't be activated unless you set %HTML.Trusted + to true, but they certainly help out users who need to put, say, forms + on their page and don't want to go through the trouble of reading this + and implementing it themself. +

    + +

    + So any of the above that you implement for your own application could + help out some other poor sap on the other side of the globe. Help us + out, and send back code so that it can be hammered into a module and + released with the core. Any code would be greatly appreciated! +

    + +

    And now...

    + +

    + Enough philosophical talk, time for some code: +

    + +
    $config = HTMLPurifier_Config::createDefault();
    +$config->set('HTML', 'DefinitionID', 'enduser-customize.html tutorial');
    +$config->set('HTML', 'DefinitionRev', 1);
    +$def =& $config->getHTMLDefinition(true);
    + +

    + Assuming that HTML Purifier has already been properly loaded (hint: + include HTMLPurifier.auto.php), this code will set up + the environment that you need to start customizing the HTML definition. + What's going on? +

    + +
      +
    • + The first three lines are regular configuration code: +
        +
      • + %HTML.DefinitionID is set to a unique identifier for your + custom HTML definition. This prevents it from clobbering + other custom definitions on the same installation. +
      • +
      • + %HTML.DefinitionRev is a revision integer of your HTML + definition. Because HTML definitions are cached, you'll need + to increment this whenever you make a change in order to flush + the cache. +
      • +
      +
    • +
    • + The fourth line retrieves a raw HTMLPurifier_HTMLDefinition + object that we will be tweaking. If the parameter was removed, we + would be retrieving a fully formed definition object, which is somewhat + useless for customization purposes. +
    • +
    + +

    Broken backwards-compatibility

    + +

    + Those of you who have already been twiddling around with the raw + HTML definition object, you'll be noticing that you're getting an error + when you attempt to retrieve the raw definition object without specifying + a DefinitionID. It is vital to caching (see below) that you make a unique + name for your customized definition, so make up something right now and + things will operate again. +

    + +

    Turn off caching

    + +

    + To make development easier, we're going to temporarily turn off + definition caching: +

    + +
    $config = HTMLPurifier_Config::createDefault();
    +$config->set('HTML', 'DefinitionID', 'enduser-customize.html tutorial');
    +$config->set('HTML', 'DefinitionRev', 1);
    +$config->set('Core', 'DefinitionCache', null); // remove this later!
    +$def =& $config->getHTMLDefinition(true);
    + +

    + A few things should be mentioned about the caching mechanism before + we move on. For performance reasons, HTML Purifier caches generated + HTMLPurifier_Definition objects in serialized files + stored (by default) in library/HTMLPurifier/DefinitionCache/Serializer. + A lot of processing is done in order to create these objects, so it + makes little sense to repeat the same processing over and over again + whenever HTML Purifier is called. +

    + +

    + In order to identify a cache entry, HTML Purifier uses three variables: + the library's version number, the value of %HTML.DefinitionRev and + a serial of relevant configuration. Whenever any of these changes, + a new HTML definition is generated. Notice that there is no way + for the definition object to track changes to customizations: here, it + is up to you to supply appropriate information to DefinitionID and + DefinitionRev. +

    + +

    Add an attribute

    + +

    + For this example, we're going to implement the target attribute found + on a elements. To implement an attribute, we have to + ask a few questions: +

    + +
      +
    1. What element is it found on?
    2. +
    3. What is its name?
    4. +
    5. Is it required or optional?
    6. +
    7. What are valid values for it?
    8. +
    + +

    + The first three are easy: the element is a, the attribute + is target, and it is not a required attribute. (If it + was required, we'd need to append an asterisk to the attribute name, + you'll see an example of this in the addElement() example). +

    + +

    + The last question is a little trickier. + Lets allow the special values: _blank, _self, _target and _top. + The form of this is called an enumeration, a list of + valid values, although only one can be used at a time. To translate + this into code form, we write: +

    + +
    $config = HTMLPurifier_Config::createDefault();
    +$config->set('HTML', 'DefinitionID', 'enduser-customize.html tutorial');
    +$config->set('HTML', 'DefinitionRev', 1);
    +$config->set('Core', 'DefinitionCache', null); // remove this later!
    +$def =& $config->getHTMLDefinition(true);
    +$def->addAttribute('a', 'target', 'Enum#_blank,_self,_target,_top');
    + +

    + The Enum#_blank,_self,_target,_top does all the magic. + The string is split into two parts, separated by a hash mark (#): +

    + +
      +
    1. The first part is the name of what we call an AttrDef
    2. +
    3. The second part is the parameter of the above-mentioned AttrDef
    4. +
    + +

    + If that sounds vague and generic, it's because it is! HTML Purifier defines + an assortment of different attribute types one can use, and each of these + has their own specialized parameter format. Here are some of the more useful + ones: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TypeFormatDescription
    Enum[s:]value1,value2,... + Attribute with a number of valid values, one of which may be used. When + s: is present, the enumeration is case sensitive. +
    Boolattribute_name + Boolean attribute, with only one valid value: the name + of the attribute. +
    CDATA + Attribute of arbitrary text. Can also be referred to as Text + (the specification makes a semantic distinction between the two). +
    ID + Attribute that specifies a unique ID +
    Pixels + Attribute that specifies an integer pixel length +
    Length + Attribute that specifies a pixel or percentage length +
    NMTOKENS + Attribute that specifies a number of name tokens, example: the + class attribute +
    URI + Attribute that specifies a URI, example: the href + attribute +
    Number + Attribute that specifies an positive integer number +
    + +

    + For a complete list, consult + library/HTMLPurifier/AttrTypes.php; + more information on attributes that accept parameters can be found on their + respective includes in + library/HTMLPurifier/AttrDef. +

    + +

    + Sometimes, the restrictive list in AttrTypes just doesn't cut it. Don't + sweat: you can also use a fully instantiated object as the value. The + equivalent, verbose form of the above example is: +

    + +
    $config = HTMLPurifier_Config::createDefault();
    +$config->set('HTML', 'DefinitionID', 'enduser-customize.html tutorial');
    +$config->set('HTML', 'DefinitionRev', 1);
    +$config->set('Core', 'DefinitionCache', null); // remove this later!
    +$def =& $config->getHTMLDefinition(true);
    +$def->addAttribute('a', 'target', new HTMLPurifier_AttrDef_Enum(
    +  array('_blank','_self','_target','_top')
    +));
    + +

    + Trust me, you'll learn to love the shorthand. +

    + +

    Add an element

    + +

    + Adding attributes is really small-fry stuff, though, and it was possible + to add them (albeit a bit more wordy) prior to 2.0. The real gem of + the Advanced API is adding elements. There are five questions to + ask when adding a new element: +

    + +
      +
    1. What is the element's name?
    2. +
    3. What content set does this element belong to?
    4. +
    5. What are the allowed children of this element?
    6. +
    7. What attributes does the element allow that are general?
    8. +
    9. What attributes does the element allow that are specific to this element?
    10. +
    + +

    + It's a mouthful, and you'll be slightly lost if your not familiar with + the HTML specification, so let's explain them step by step. +

    + +

    Content set

    + +

    + The HTML specification defines two major content sets: Inline + and Block. Each of these + content sets contain a list of elements: Inline contains things like + span and b while Block contains things like + div and blockquote. +

    + +

    + These content sets amount to a macro mechanism for HTML definition. Most + elements in HTML are organized into one of these two sets, and most + elements in HTML allow elements from one of these sets. If we had + to write each element verbatim into each other element's allowed + children, we would have ridiculously large lists; instead we use + content sets to compactify the declaration. +

    + +

    + Practically speaking, there are several useful values you can use here: +

    + + + + + + + + + + + + + + + + + + + + + + +
    Content setDescription
    InlineCharacter level elements, text
    BlockBlock-like elements, like paragraphs and lists
    false + Any element that doesn't fit into the mold, for example li + or tr +
    + +

    + By specifying a valid value here, all other elements that use that + content set will also allow your element, without you having to do + anything. If you specify false, you'll have to register + your element manually. +

    + +

    Allowed children

    + +

    + Allowed children defines the elements that this element can contain. + The allowed values may range from none to a complex regexp depending on + your element. +

    + +

    + If you've ever taken a look at the HTML DTD's before, you may have + noticed declarations like this: +

    + +
    <!ELEMENT LI - O (%flow;)*             -- list item -->
    + +

    + The (%flow;)* indicates the allowed children of the + li tag: li allows any number of flow + elements as its children. In HTML Purifier, we'd write it like + Flow (here's where the content sets we were + discussing earlier come into play). There are three shorthand content models you + can specify: +

    + + + + + + + + + + + + + + + + + + + + + + +
    Content modelDescription
    EmptyNo children allowed, like br or hr
    InlineAny number of inline elements and text, like span
    FlowAny number of inline elements, block elements and text, like div
    + +

    + This covers 90% of all the cases out there, but what about elements that + break the mold like ul? This guy requires at least one + child, and the only valid children for it are li. The + content model is: Required: li. There are two parts: the + first type determines what ChildDef will be used to validate + content models. The most common values are: +

    + + + + + + + + + + + + + + + + + + + + + + +
    TypeDescription
    RequiredChildren must be one or more of the valid elements
    OptionalChildren can be any number of the valid elements
    CustomChildren must follow the DTD-style regex
    + +

    + You can also implement your own ChildDef: this was done + for a few special cases in HTML Purifier such as Chameleon + (for ins and del), StrictBlockquote + and Table. +

    + +

    + The second part specifies either valid elements or a regular expression. + Valid elements are separated with horizontal bars (|), i.e. + "a | b | c". Use #PCDATA to represent plain text. + Regular expressions are based off of DTD's style: +

    + +
      +
    • Parentheses () are used for grouping
    • +
    • Commas (,) separate elements that should come one after another
    • +
    • Horizontal bars (|) indicate one or the other elements should be used
    • +
    • Plus signs (+) are used for a one or more match
    • +
    • Asterisks (*) are used for a zero or more match
    • +
    • Question marks (?) are used for a zero or one match
    • +
    + +

    + For example, "a, b?, (c | d), e+, f*" means "In this order, + one a element, at most one b element, + one c or d element (but not both), one or more + e elements, and any number of f elements." + Regex veterans should be able to jump right in, and those not so savvy + can always copy-paste W3C's content model definitions into HTML Purifier + and hope for the best. +

    + +

    + A word of warning: while the regex format is extremely flexible on + the developer's side, it is + quite unforgiving on the user's side. If the user input does not exactly + match the specification, the entire contents of the element will + be nuked. This is why there is are specific content model types like + Optional and Required: while they could be implemented as Custom: + (valid | elements)*, the custom classes contain special recovery + measures that make sure as much of the user's original content gets + through. HTML Purifier's core, as a rule, does not use Custom. +

    + +

    + One final note: you can also use Content Sets inside your valid elements + lists or regular expressions. In fact, the three shorthand content models + mentioned above are just that: abbreviations: +

    + + + + + + + + + + + + + + + + + + +
    Content modelImplementation
    InlineOptional: Inline | #PCDATA
    FlowOptional: Flow | #PCDATA
    + +

    + When the definition is compiled, Inline will be replaced with a + horizontal-bar separated list of inline elements. Also, notice that + it does not contain text: you have to specify that yourself. +

    + +

    Common attributes

    + +

    + Congratulations: you have just gotten over the proverbial hump (Allowed + children). Common attributes is much simpler, and boils down to + one question: does your element have the id, style, + class, title and lang attributes? + If so, you'll want to specify the Common attribute collection, + which contains these five attributes that are found on almost every + HTML element in the specification. +

    + +

    + There are a few more collections, but they're really edge cases: +

    + + + + + + + + + + + + + + + + + + +
    CollectionAttributes
    I18Nlang, possibly xml:lang
    Corestyle, class, id and title
    + +

    + Common is a combination of the above-mentioned collections. +

    + +

    Attributes

    + +

    + If you didn't read the previous section on + adding attributes, read it now. The last parameter is simply + array of attribute names to attribute implementations, in the exact + same format as addAttribute(). +

    + +

    Putting it all together

    + +

    + We're going to implement form. Before we embark, lets + grab a reference implementation from over at the + transitional DTD: +

    + +
    <!ELEMENT FORM - - (%flow;)* -(FORM)   -- interactive form -->
    +<!ATTLIST FORM
    +  %attrs;                              -- %coreattrs, %i18n, %events --
    +  action      %URI;          #REQUIRED -- server-side form handler --
    +  method      (GET|POST)     GET       -- HTTP method used to submit the form--
    +  enctype     %ContentType;  "application/x-www-form-urlencoded"
    +  accept      %ContentTypes; #IMPLIED  -- list of MIME types for file upload --
    +  name        CDATA          #IMPLIED  -- name of form for scripting --
    +  onsubmit    %Script;       #IMPLIED  -- the form was submitted --
    +  onreset     %Script;       #IMPLIED  -- the form was reset --
    +  target      %FrameTarget;  #IMPLIED  -- render in this frame --
    +  accept-charset %Charsets;  #IMPLIED  -- list of supported charsets --
    +  >
    + +

    + Juicy! With just this, we can answer four of our five questions: +

    + +
      +
    1. What is the element's name? form
    2. +
    3. What content set does this element belong to? Block + (this needs a little sleuthing, I find the easiest way is to search + the DTD for FORM and determine which set it is in.)
    4. +
    5. What are the allowed children of this element? One + or more flow elements, but no nested forms
    6. +
    7. What attributes does the element allow that are general? Common
    8. +
    9. What attributes does the element allow that are specific to this element? A whole bunch, see ATTLIST; + we're going to the vital ones: action, method and name
    10. +
    + +

    + Time for some code: +

    + +
    $config = HTMLPurifier_Config::createDefault();
    +$config->set('HTML', 'DefinitionID', 'enduser-customize.html tutorial');
    +$config->set('HTML', 'DefinitionRev', 1);
    +$config->set('Core', 'DefinitionCache', null); // remove this later!
    +$def =& $config->getHTMLDefinition(true);
    +$def->addAttribute('a', 'target', new HTMLPurifier_AttrDef_Enum(
    +  array('_blank','_self','_target','_top')
    +));
    +$form =& $def->addElement(
    +  'form',   // name
    +  'Block',  // content set
    +  'Flow', // allowed children
    +  'Common', // attribute collection
    +  array( // attributes
    +    'action*' => 'URI',
    +    'method' => 'Enum#get|post',
    +    'name' => 'ID'
    +  )
    +);
    +$form->excludes = array('form' => true);
    + +

    + Each of the parameters corresponds to one of the questions we asked. + Notice that we added an asterisk to the end of the action + attribute to indicate that it is required. If someone specifies a + form without that attribute, the tag will be axed. + Also, the extra line at the end is a special extra declaration that + prevents forms from being nested within each other. +

    + +

    + And that's all there is to it! Implementing the rest of the form + module is left as an exercise to the user; to see more examples + check the library/HTMLPurifier/HTMLModule/ directory + in your local HTML Purifier installation. +

    + +

    And beyond...

    + +

    + Perceptive users may have realized that, to a certain extent, we + have simply re-implemented the facilities of XML Schema or the + Document Type Definition. What you are seeing here, however, is + not just an XML Schema or Document Type Definition: it is a fully + expressive method of specifying the definition of HTML that is + a portable superset of the capabilities of the two above-mentioned schema + languages. What makes HTMLDefinition so powerful is the fact that + if we don't have an implementation for a content model or an attribute + definition, you can supply it yourself by writing a PHP class. +

    + +

    + There are many facets of HTMLDefinition beyond the Advanced API I have + walked you through today. To find out more about these, you can + check out these source files: +

    + + + +
    $Id: enduser-tidy.html 1158 2007-06-18 19:26:29Z Edward $
    + + \ No newline at end of file diff --git a/docs/enduser-security.txt b/docs/enduser-security.txt index d33f473c..49aff331 100644 --- a/docs/enduser-security.txt +++ b/docs/enduser-security.txt @@ -8,15 +8,11 @@ to be effective. Things to remember: 1. Character Encoding: see enduser-utf8.html for more info. -2. Doctype: document pending feature completion -Not strictly necessary, actually. More in-depth discussion once we figure -out how to get strict loose mode working. +2. IDs: see enduser-id.html for more info -3. IDs: see enduser-id.html for more info - -4. Links: document pending feature completion +3. Links: document pending feature completion Rudimentary blacklisting, we should also allow only relative URIs. We need a doc to explain the stuff. -5. CSS: document pending +4. CSS: document pending Explain which CSS styles we blocked and why. diff --git a/docs/enduser-tidy.html b/docs/enduser-tidy.html new file mode 100644 index 00000000..cc04834c --- /dev/null +++ b/docs/enduser-tidy.html @@ -0,0 +1,235 @@ + + + + + + + +Tidy - HTML Purifier + + + +

    Tidy

    + +
    Filed under Development
    +
    Return to the index.
    +
    HTML Purifier End-User Documentation
    + +
    + This document covers currently unreleased functionality and + only applies to recent SVN checkouts. +
    + +

    You've probably heard of HTML Tidy, Dave Raggett's little piece +of software that cleans up poorly written HTML. Let me say it straight +out:

    + +

    This ain't HTML Tidy!

    + +

    Rather, Tidy stands for a cool set of Tidy-inspired in HTML Purifier +that allows users to submit deprecated elements and attributes and get +valid strict markup back. For example:

    + +
    <center>Centered</center>
    + +

    ...becomes:

    + +
    <div style="text-align:center;">Centered</div>
    + +

    ...when this particular fix is run on the HTML. This tutorial will give +you down the lowdown of what exactly HTML Purifier will do when Tidy +is on, and how to fine tune this behavior. Once again, you do +not need Tidy installed on your PHP to use these features!

    + +

    What does it do?

    + +

    Tidy will do several things to your HTML:

    + +
      +
    • Convert deprecated elements and attributes to standards-compliant + alternatives
    • +
    • Enforce XHTML compatibility guidelines and other best practices
    • +
    • Preserve data that would normally be removed as per W3C
    • +
    + +

    What are levels?

    + +

    Levels describe how aggressive the Tidy module should be when +cleaning up HTML. There are four levels to pick: none, light, medium +and heavy. Each of these levels has a well-defined set of behavior +associated with it, although it may change depending on your doctype.

    + +
    +
    light
    +
    This is the lenient level. If a tag or attribute + is about to be removed because it isn't supported by the + doctype, Tidy will step in and change into an alternative that + is supported.
    +
    medium
    +
    This is the correctional level. At this level, + all the functions of light are performed, as well as some extra, + non-essential best practices enforcement. Changes made on this + level are very benign and are unlikely to cause problems.
    +
    heavy
    +
    This is the aggressive level. If a tag or + attribute is deprecated, it will be converted into a non-deprecated + version, no ifs ands or buts.
    +
    + +

    By default, Tidy operates on the medium level. You can +change the level of cleaning by setting the %HTML.TidyLevel configuration +directive:

    + +
    $config->set('HTML', 'TidyLevel', 'heavy'); // burn baby burn!
    + +

    Is the light level really light?

    + +

    It depends on what doctype you're using. If your documents are HTML +4.01 Transitional, HTML Purifier will be lazy +and won't clean up your center +or font tags. But if you're using HTML 4.01 Strict, +HTML Purifier has no choice: it has to convert them, or they will +be nuked out of existence. So while light on Transitional will result +in little to no changes, light on Strict will still result in quite +a lot of fixes.

    + +

    This is different behavior from 1.6 or before, where deprecated +tags in transitional documents would +always be cleaned up regardless. This is also better behavior.

    + +

    My pages look different!

    + +

    HTML Purifier is tasked with converting deprecated tags and +attributes to standards-compliant alternatives, which usually +need copious amounts of CSS. It's also not foolproof: sometimes +things do get lost in the translation. This is why when HTML Purifier +can get away with not doing cleaning, it won't; this is why +the default value is medium and not heavy.

    + +

    Fortunately, only a few attributes have problems with the switch +over. They are described below:

    + + + + + + + + + + + + + + + + + + + + + + + + +
    Element@AttrChanges
    caption@alignFirefox supports stuffing the caption on the + left and right side of the table, a feature that + Internet Explorer, understandably, does not have. + When align equals right or left, the text will simply + be aligned on the left or right side.
    img@alignThe implementation for align bottom is good, but not + perfect. There are a few pixel differences.
    br@clearClear both gets a little wonky in Internet Explorer. Haven't + really been able to figure out why.
    hr@noshadeAll browsers implement this slightly differently: we've + chosen to make noshade horizontal rules gray.
    + +

    There are a few more minor, although irritating, bugs. +Some older browsers support deprecated attributes, +but not CSS. Transformed elements and attributes will look unstyled +to said browsers. Also, CSS precedence is slightly different for +inline styles versus presentational markup. In increasing precedence:

    + +
      +
    1. Presentational attributes
    2. +
    3. External style sheets
    4. +
    5. Inline styling
    6. +
    + +

    This means that styling that may have been masked by external CSS +declarations will start showing up (a good thing, perhaps). Finally, +if you've turned off the style attribute, almost all of +these transformations will not work. Sorry mates.

    + +

    You can review the rendering before and after of these transformations +by consulting the attrTransform.php +smoketest.

    + +

    I like the general idea, but the specifics bug me!

    + +

    So you want HTML Purifier to clean up your HTML, but you're not +so happy about the br@clear implementation. That's perfectly fine! +HTML Purifier will make accomodations:

    + +
    $config->set('HTML', 'Doctype', 'XHTML 1.0 Transitional');
    +$config->set('HTML', 'TidyLevel', 'heavy'); // all changes, minus...
    +$config->set('HTML', 'TidyRemove', 'br@clear');
    + +

    That third line does the magic, removing the br@clear fix +from the module, ensuring that <br clear="both" /> +will pass through unharmed. The reverse is possible too:

    + +
    $config->set('HTML', 'Doctype', 'XHTML 1.0 Transitional');
    +$config->set('HTML', 'TidyLevel', 'none'); // no changes, plus...
    +$config->set('HTML', 'TidyAdd', 'p@align');
    + +

    In this case, all transformations are shut off, except for the p@align +one, which you found handy.

    + +

    To find out what the names of fixes you want to turn on or off are, +you'll have to consult the source code, specifically the files in +HTMLPurifier/HTMLModule/Tidy/. There is, however, a +general syntax:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameExampleInterpretation
    elementfontTag transform for element
    element@attrbr@clearAttribute transform for attr on element
    @attr@langGlobal attribute transform for attr
    e#content_model_typeblockquote#content_model_typeChange of child processing implementation for e
    + +

    So... what's the lowdown?

    + +

    The lowdown is, quite frankly, HTML Purifier's default settings are +probably good enough. The next step is to bump the level up to heavy, +and if that still doesn't satisfy your appetite, do some fine tuning. +Other than that, don't worry about it: this all works silently and +effectively in the background.

    + +
    $Id$
    + + \ No newline at end of file diff --git a/docs/examples/basic.php b/docs/examples/basic.php index 029ca7c8..8910254d 100644 --- a/docs/examples/basic.php +++ b/docs/examples/basic.php @@ -8,8 +8,8 @@ require_once '../../library/HTMLPurifier.auto.php'; $config = HTMLPurifier_Config::createDefault(); // configuration goes here: -$config->set('Core', 'Encoding', 'ISO-8859-1'); //replace with your encoding -$config->set('Core', 'XHTML', true); // set to false if HTML 4.01 +$config->set('Core', 'Encoding', 'UTF-8'); // replace with your encoding +$config->set('HTML', 'Doctype', 'XHTML 1.0 Transitional'); // replace with your doctype $purifier = new HTMLPurifier($config); diff --git a/docs/index.html b/docs/index.html index 7a7ec0a3..69ee6207 100644 --- a/docs/index.html +++ b/docs/index.html @@ -34,6 +34,12 @@ information for casual developers using HTML Purifier.

    UTF-8: The Secret of Character Encoding
    Describes the rationale for using UTF-8, the ramifications otherwise, and how to make the switch.
    +
    Tidy
    +
    Tutorial for tweaking HTML Purifier's Tidy-like behavior.
    + +
    Customize
    +
    Tutorial for customizing HTML Purifier's tag and attribute sets.
    +

    Development

    @@ -128,8 +134,8 @@ the code. They may be upgraded to HTML files or stay as TXT scratchpads.

    Reference - Loose vs.Strict - Differences between HTML Strict and Transitional versions. + Handling Content Model Changes + Discusses how to tidy up content model changes using custom ChildDef classes. @@ -140,14 +146,8 @@ the code. They may be upgraded to HTML files or stay as TXT scratchpads.

    Reference - Strictness - Short essay on how loose definition isn't really loose. - - - - Reference - XHTML 1.1 - What we'd have to do to support XHTML 1.1. + Modularization of HTMLDefinition + Provides a high-level overview of the concepts behind HTMLModules. diff --git a/docs/proposal-config.txt b/docs/proposal-config.txt index 93314122..a9ee73a4 100644 --- a/docs/proposal-config.txt +++ b/docs/proposal-config.txt @@ -12,29 +12,10 @@ the documentation in ConfigDef for more information on these namespaces. Since configuration is dependant on context, internal classes require a configuration object to be passed as a parameter. (They also require a -Context object). +Context object). A majority of classes do not need the config object, +but for those who do, it is a lifesaver. -In relation to HTMLDefinition and CSSDefinition, there could be a special class -of directives that influence the *construction* of the Definition object. -A theoretical call pattern would look like: - -1. Client calls Config->getHTMLDefinition() -2. Config calls HTMLDefinition->createNew(this) -3. HTMLDefinition constructs itself with base configuration -4. HTMLDefinition calls Config->get('HTML') -5. Config returns array of directives -6. HTMLDefinition performs operations and changes specified by directives -7. HTMLPurifier returns constructed definition -8. Config caches definition so it doesn't have to be generated again -9. Config returns definition - -You could also override Config's copy of the definition with your own -custom copy, which OVERRIDES all directives. Only the base, vanilla copy -is the Singleton, the object actually interfaced with is a operated-upon -clone of that object. Also, if an update to the directives would update -the definition, you'd have to force reconstruction. - -In practice, the pulling directives from the config object are -solely need-based, and the flex points are littered throughout the -setup() function. Some sort of refactoring is likely in order. See -ref-xhtml-1.1.txt for more info. +Definition objects are complex datatypes influenced by their respective +directive namespaces (HTMLDefinition with HTML and CSSDefinition with CSS). +If any of these directives is updated, HTML Purifier forces the definition +to be regenerated. diff --git a/docs/proposal-filter-levels.txt b/docs/proposal-filter-levels.txt index a8306152..9e9cfbb0 100644 --- a/docs/proposal-filter-levels.txt +++ b/docs/proposal-filter-levels.txt @@ -2,23 +2,16 @@ Filter Levels When one size *does not* fit all -The more I think about it, the less sense it makes for maintaining one huge -monolithic HTMLDefinition class. There's simply so much variation that -could go into this definition: the set of HTML good for blog entries is -definitely too large for HTML that would be allowed in blog comments. Going -from Transitional to Strict requires changes to the definition. +It makes little sense to constrain users to one set of HTML elements and +attributes and tell them that they are not allowed to mold this in +any fashion. Many users demand to be able to custom-select which elements +and attributes they want. This is fine: because HTML Purifier keeps close +track of what elements are safe to use, there is no way for them to +accidently allow an XSS-able tag. -Allowing users to specify their own whitelists is one step (implemented, btw), -but I have doubts on only doing this. Simply put, the typical programmer is too -lazy to actually go through the trouble of investigating which tags, attributes -and properties to allow. HTMLDefinition makes a big part of what HTMLPurifier -is. - -The idea, then, is to setup fundamentally different set of definitions, which -can further be customized using simpler configuration options. Alternatively, -they could be implemented as configuration profiles, which simply load -a set of recommended directives to acheive a desired affect (no simpler -config options though). +However, combing through the HTML spec to make your own whitelist can +be a daunting task. HTML Purifier ought to offer pre-canned filter levels +that amateur users can select based on what they think is their use-case. Here are some fuzzy levels you could set: @@ -46,6 +39,10 @@ make forbidden element to text transformations desirable (for example, images). == Element Risk Analysis == +Although none of the currently supported elements presents a security +threat per-say, some can cause problems for page layouts or be +extremely complicated. + Legend: [danger level] - regular tags / uncommon tags ~ deprecated tags [danger level]* - rare tags @@ -114,6 +111,10 @@ Partially presentational - table.cellpadding, table.cellspacing, == CSS Risk Analysis == +Currently, there is no support for fine-grained "allowed CSS" specification, +mainly because I'm lazy, partially because no one has asked for it. However, +this will be added eventually. + There are certain CSS elements that are extremely useful inline, but then as you get to more presentation oriented styling it may not always be appropriate to inline them. @@ -126,6 +127,7 @@ any CSS properties that are not currently implemented (such as position). Dangerous, can go outside container - float Easy to abuse - font-size, font-family (font), width Colored - background-color (background), border-color (border), color + (see proposal-colors.html) Dramatic - border, list-style-position (list-style), margin, padding, text-align, text-indent, text-transform, vertical-align, line-height diff --git a/docs/ref-content-models.txt b/docs/ref-content-models.txt new file mode 100644 index 00000000..11d4aca7 --- /dev/null +++ b/docs/ref-content-models.txt @@ -0,0 +1,48 @@ + +Handling Content Model Changes + + +1. Context + +The distinction between Transitional and Strict document types is somewhat +of an anomaly in the lineage of XHTML document types (following 1.0, no +doctypes do not have flavors: instead, modularization is used to let +document authors vary their elements). This transition is usually quite +straight-forward, as W3C usually deprecates attributes or elements, which +are quite easily handled using tag and attribute transforms. + +However, for two elements,
    , and
    , W3C elected +to also change the content model.
    and originally +accepted both inline and block elements, but in the strict doctype they +only allow block elements. With
    , the situation is inverted: +

    tags were now forbidden from appearing within this tag. + + +2. Current situation + +Currently, HTML Purifier treats

    specially during Tidy mode +using a custom ChildDef class StrictBlockquote. StrictBlockquote +operates similarly to Required, except that when it encounters an inline +element, it will wrap it in a block tag (as specified by +%HTML.BlockWrapper, the default is

    ). The naming suggests it can +only be used for

    s, although it may be possible to +genericize it to work on other cases of this nature (this would be of +little practical application, as no other element in XHTML 1.1 or earlier +has a block-only content model). + +Tidy currently contains no custom, lenient implementation for
    . +If one were to be written, it would likely operate on the principle that, +when a

    tag were to be encountered, it would be replaced with a +leading and trailing
    tag (the contents of

    , being inline, are +not an issue). There is no prior work with this sort of operation. + + +3. Outside applicability + +There are a number of other elements that contain restrictive content +models, such as

      or (the latter is restrictive in that it +does not allow block elements). In the former case, an errant node +is eliminated completely, in the latter case, the text of the node +would is preserved (as the parent node does allow PCDATA). Custom +content model implementations probably are not the best way of handling +these cases, instead, node bubbling should be implemented instead. diff --git a/docs/ref-xhtml-1.1.txt b/docs/ref-html-modularization.txt similarity index 82% rename from docs/ref-xhtml-1.1.txt rename to docs/ref-html-modularization.txt index b32db5a8..8b54b8f5 100644 --- a/docs/ref-xhtml-1.1.txt +++ b/docs/ref-html-modularization.txt @@ -1,10 +1,8 @@ -XHTML 1.1 and HTML Purifier +The Modularization of HTMLDefinition in HTML Purifier Todo for XHTML 1.1 support -1. Scratch lang entirely in favor of xml:lang -2. Scratch name entirely in favor of id (partially-done) -3. Support Ruby +1. Support Ruby HTML Purifier uses the modularization of XHTML to organize the internals @@ -12,25 +10,10 @@ of HTMLDefinition into a more manageable and extensible fashion. Rather than have one super-object, HTMLDefinition is split into HTMLModules, each of which are responsible for defining elements, their attributes, and other properties (for a more indepth coverage, see -/library/HTMLPurifier/HTMLModule.php's docblock comments). +/library/HTMLPurifier/HTMLModule.php's docblock comments). These modules +are managed by HTMLModuleManager. -The modules that W3C defines and we support are: - - * 5.1. Attribute Collections (technically not a module - * 5.2. Core Modules - o 5.2.2. Text Module - o 5.2.3. Hypertext Module - o 5.2.4. List Module - * 5.4. Text Extension Modules - o 5.4.1. Presentation Module - o 5.4.2. Edit Module - o 5.4.3. Bi-directional Text Module - * 5.6. Table Modules - o 5.6.2. Tables Module - * 5.7. Image Module - * 5.18. Style Attribute Module - -Modules that we don't support but coul support are: +Modules that we don't support but could support are: * 5.6. Table Modules o 5.6.1. Basic Tables Module [?] @@ -38,10 +21,8 @@ Modules that we don't support but coul support are: * 5.9. Server-side Image Map Module [?] * 5.12. Target Module [?] * 5.21. Name Identification Module [deprecated] - * 5.22. Legacy Module [deprecated] -These modules will not be implemented due to their dangerousness or -inapplicability as an XHTML fragment: +These modules would be implemented as "unsafe": * 5.2. Core Modules o 5.2.1. Structure Module @@ -64,11 +45,7 @@ of robust tools for handling them (the main problem is that all the current parsers are usually PHP 5 only and solely-validating, not correcting). -The abstraction of the HTMLDefinition creation process will also -contribute to a need for a caching system. Cache invalidation would be -difficult, but could be done by comparing the HTML and Attr config -namespaces with a copy that was packaged along with the serialized -HTMLDefinition object. +This system may be generalized and ported over for CSS. == General Use-Case == @@ -91,7 +68,7 @@ like this: getHTMLDefinition(true); // reference to raw - unset($def->modules['Hypertext']); // rm ''a'' link + $def->addElement('marquee', 'Block', 'Flow', 'Common'); $purifier = new HTMLPurifier($config); $purifier->purify($html); // now the definition is finalized ?> @@ -184,4 +161,4 @@ Content sets can be altered using HTMLModule->content_sets, an associative array of content set names to content set contents. If the content set already exists, your values are appended on to it (great for, say, registering the font tag as an inline element), otherwise it is -created. They are substituted into content_model. \ No newline at end of file +created. They are substituted into content_model. diff --git a/docs/ref-loose-vs-strict.txt b/docs/ref-loose-vs-strict.txt deleted file mode 100644 index 7828aa63..00000000 --- a/docs/ref-loose-vs-strict.txt +++ /dev/null @@ -1,37 +0,0 @@ - -Loose versus Strict - Changes from one doctype to another - -There are changes. Wow, how insightful. Not everything changed is relevant -to HTML Purifier, though, so let's take a look: - -== Major incompatibilities == - -[done] BLOCKQUOTE changes from 'flow' to 'block' - current behavior: inline inner contents should not be nuked, block-ify as necessary -[partially-done] U, S, STRIKE cut - current behavior: removed completely - projected behavior: replace with appropriate inline span + CSS -[done] ADDRESS from potpourri to Inline (removes p tags) - current behavior: block tags silently dropped - ideal behavior: replace tags with something like
      . (not high priority) - -== Things we can loosen up == - -Tags DIR, MENU, CENTER, ISINDEX, FONT, BASEFONT? allowed in loose - current behavior: transform to strict-valid forms -Attributes allowed in loose (see attribute transforms in 'dev-progress.html') - current behavior: projected to transform into strict-valid forms - -== Periphery issues == - -A tag's attribute 'target' (for selecting frames) cut - current behavior: not allowed at all - projected behavior: use loose doctype if needed, needs valid values -[done] OL/LI tag's attribute 'start'/'value' (for renumbering lists) cut - current behavior: no substitute, just delete when in strict, allow in loose -Attribute 'name' deprecated in favor of 'id' - current behavior: dropped silently - projected behavior: create proper AttrTransform -[done] PRE tag allows SUB/SUP? (strict dtd comment vs syntax, loose disallows) - current behavior: disallow as usual diff --git a/docs/ref-proprietary-tags.txt b/docs/ref-proprietary-tags.txt index cf0b60fc..4daba763 100644 --- a/docs/ref-proprietary-tags.txt +++ b/docs/ref-proprietary-tags.txt @@ -18,5 +18,7 @@ HTML Purifier context. , monospace pre-variant (extremely rare) , escapes all tags to the end of document -<ruby> and friends, (more research needed, appears to be XHTML 1.1 markup) <xmp>, monospace, replace with pre + +These should be put into their own Tidy module, not loaded by default(?). These +all qualify as "lenient" transforms. \ No newline at end of file diff --git a/docs/ref-strictness.txt b/docs/ref-strictness.txt deleted file mode 100644 index 81907c1e..00000000 --- a/docs/ref-strictness.txt +++ /dev/null @@ -1,37 +0,0 @@ - -Is HTML Purifier Strict or Transitional? - A little bit of helpful guidance - -Despite the fact that HTML Purifier professes to support both transitional and -strict HTML, it rejects a lot of attributes and elements that are actually, indeed, -valid. You can investigate progress.html to find out precisely what we -are doing to these *deprecated* attributes. - -However, users have found that Strict HTML imposes some quite unreasonable -restrictions on certain things. The start and value attributes in ol and -li (respectively) perhaps are the most contested. There's is currently no -widely supported browser method short of JavaScript that can replace these -two deprecated elements. It behooves us to allow these deprecated -attributes when the output is transitional. - -Fortunantely, that's the only real bugger case. The others have near-perfect -CSS equivalents, and were presentational anyway. However, the other question -pops up: should we always convert these to the CSS forms when 1. the spec -allows them anyway and 2. older browsers support them better? After all, the -whole point about CSS is to seperate styling from content, so inline styling -doesn't solve that problem. - -It's an icky question, and we'll have to deal with it as more and more -transforms get implemented. As of right now, however, we currently support -these loose-only constructs in loose mode: - -- <ul start="1">, <li value="1"> attributes -- <u>, <strike>, <s> tags -- flow children in <blockquote> -- mixed children in <address> - -The changed child definitions as well as the ul.start li.value are the most -compelling reasons why loose should be used. We may want offer disabling <u>, -<strike> and <s> by themselves. We may also want to offer no pre-emptive -deprecated conversions. This all must be unified. - diff --git a/docs/ref-whatwg.txt b/docs/ref-whatwg.txt index 78cef953..070d8e88 100644 --- a/docs/ref-whatwg.txt +++ b/docs/ref-whatwg.txt @@ -2,8 +2,23 @@ Web Hypertext Application Technology Working Group WHATWG -I don't think we need to worry about them. Untrusted users shouldn't be -submitting applications, eh? But if some interesting attribute pops up in -their spec, and might be worth supporting, stick it here. +== HTML 5 == -(none so far, as you can see) +URL: http://www.whatwg.org/specs/web-apps/current-work/ + +HTML 5 defines a kaboodle of new elements and attributes, as well as +some well-defined, "quirks mode" HTML parsing. Although WHATWG professes +to be targeted towards web applications, many of their semantic additions +would be quite useful in regular documents. Eventually, HTML +Purifier will need to audit their lists and figure out what changes need +to be made. This process is complicated by the fact that the WHATWG +doesn't buy into W3C's modularization of XHTML 1.1: we may need +to remodularize HTML 5 (probably done by section name). No sense in +committing ourselves till the spec stabilizes, though. + +More immediately speaking though, however, is the well-defined parsing +behavior that HTML 5 adds. While I have little interest in writing +another DirectLex parser, other parsers like ph5p +<http://jero.net/lab/ph5p/> can be adapted to DOMLex to support much more +flexible HTML parsing (a cool feature I've seen is how they resolve +<b>bold<i>both</b>italic</i>). diff --git a/docs/style.css b/docs/style.css index 75a3e2f7..db2dd7d7 100644 --- a/docs/style.css +++ b/docs/style.css @@ -25,6 +25,7 @@ h4 {font-family:sans-serif; font-size:0.9em; font-weight:bold; } .aside {margin-left:2em; font-family:sans-serif; font-size:0.9em; } blockquote .label {font-weight:bold; font-size:1em; margin:0 0 .1em; border-bottom:1px solid #CCC;} +.emphasis {font-weight:bold; text-align:center; font-size:1.3em;} /* A regular table */ .table {border-collapse:collapse; border-bottom:2px solid #888; margin-left:2em; } @@ -66,3 +67,5 @@ q:after { /* Marks off sections that are lacking. */ .fixme {margin-left:2em; } .fixme:before {content:"Fix me: "; font-weight:bold; color:#C00; } + +#applicability {margin: 1em 5%; font-style:italic;} diff --git a/library/HTMLPurifier.php b/library/HTMLPurifier.php index 3d538bca..731e1dfd 100644 --- a/library/HTMLPurifier.php +++ b/library/HTMLPurifier.php @@ -22,7 +22,7 @@ */ /* - HTML Purifier 1.6.1 - Standards Compliant HTML Filtering + HTML Purifier 2.0.0 - Standards Compliant HTML Filtering Copyright (C) 2006 Edward Z. Yang This library is free software; you can redistribute it and/or @@ -42,7 +42,7 @@ // almost every class has an undocumented dependency to these, so make sure // they get included -require_once 'HTMLPurifier/ConfigSchema.php'; +require_once 'HTMLPurifier/ConfigSchema.php'; // important require_once 'HTMLPurifier/Config.php'; require_once 'HTMLPurifier/Context.php'; @@ -51,6 +51,23 @@ require_once 'HTMLPurifier/Generator.php'; require_once 'HTMLPurifier/Strategy/Core.php'; require_once 'HTMLPurifier/Encoder.php'; +require_once 'HTMLPurifier/LanguageFactory.php'; + +HTMLPurifier_ConfigSchema::define( + 'Core', 'Language', 'en', 'string', ' +ISO 639 language code for localizable things in HTML Purifier to use, +which is mainly error reporting. There is currently only an English (en) +translation, so this directive is currently useless. +This directive has been available since 2.0.0. +'); + +HTMLPurifier_ConfigSchema::define( + 'Core', 'CollectErrors', false, 'bool', ' +Whether or not to collect errors found while filtering the document. This +is a useful way to give feedback to your users. CURRENTLY NOT IMPLEMENTED. +This directive has been available since 2.0.0. +'); + /** * Main library execution class. * @@ -64,12 +81,12 @@ require_once 'HTMLPurifier/Encoder.php'; class HTMLPurifier { - var $version = '1.6.1'; + var $version = '2.0.0'; var $config; var $filters; - var $lexer, $strategy, $generator; + var $strategy, $generator; /** * Final HTMLPurifier_Context of last run purification. Might be an array. @@ -89,7 +106,6 @@ class HTMLPurifier $this->config = HTMLPurifier_Config::create($config); - $this->lexer = HTMLPurifier_Lexer::create(); $this->strategy = new HTMLPurifier_Strategy_Core(); $this->generator = new HTMLPurifier_Generator(); @@ -117,7 +133,23 @@ class HTMLPurifier $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(); + + // 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->get('Core', 'Language')); + $context->register('Locale', $language); + + $error_collector = new HTMLPurifier_ErrorCollector(); + $context->register('ErrorCollector', $language); + } + $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); for ($i = 0, $size = count($this->filters); $i < $size; $i++) { @@ -130,7 +162,7 @@ class HTMLPurifier // list of tokens $this->strategy->execute( // list of un-purified tokens - $this->lexer->tokenizeHTML( + $lexer->tokenizeHTML( // un-purified HTML $html, $config, $context ), @@ -164,6 +196,23 @@ class HTMLPurifier return $array_of_html; } + /** + * Singleton for enforcing just one HTML Purifier in your system + */ + function &getInstance($prototype = null) { + static $htmlpurifier; + if (!$htmlpurifier || $prototype) { + if (is_a($prototype, 'HTMLPurifier')) { + $htmlpurifier = $prototype; + } elseif ($prototype) { + $htmlpurifier = new HTMLPurifier(HTMLPurifier_Config::create($prototype)); + } else { + $htmlpurifier = new HTMLPurifier(); + } + } + return $htmlpurifier; + } + } diff --git a/library/HTMLPurifier/AttrCollections.php b/library/HTMLPurifier/AttrCollections.php index 8318abb1..61bce40e 100644 --- a/library/HTMLPurifier/AttrCollections.php +++ b/library/HTMLPurifier/AttrCollections.php @@ -1,7 +1,6 @@ <?php require_once 'HTMLPurifier/AttrTypes.php'; -require_once 'HTMLPurifier/AttrDef/Lang.php'; /** * Defines common attribute collections that modules reference @@ -12,8 +11,6 @@ class HTMLPurifier_AttrCollections /** * Associative array of attribute collections, indexed by name - * @note Technically, the composition of these is more complicated, - * but we bypass it using our own excludes property */ var $info = array(); @@ -25,27 +22,29 @@ class HTMLPurifier_AttrCollections * @param $modules Hash array of HTMLPurifier_HTMLModule members */ function HTMLPurifier_AttrCollections($attr_types, $modules) { - $info =& $this->info; // 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 === 0 && isset($info[$coll_i][$attr_i])) { + if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) { // merge in includes - $info[$coll_i][$attr_i] = array_merge( - $info[$coll_i][$attr_i], $attr); + $this->info[$coll_i][$attr_i] = array_merge( + $this->info[$coll_i][$attr_i], $attr); continue; } - $info[$coll_i][$attr_i] = $attr; + $this->info[$coll_i][$attr_i] = $attr; } } } // perform internal expansions and inclusions - foreach ($info as $name => $attr) { + foreach ($this->info as $name => $attr) { // merge attribute collections that include others - $this->performInclusions($info[$name]); + $this->performInclusions($this->info[$name]); // replace string identifiers with actual attribute objects - $this->expandIdentifiers($info[$name], $attr_types); + $this->expandIdentifiers($this->info[$name], $attr_types); } } @@ -57,16 +56,20 @@ class HTMLPurifier_AttrCollections 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($info[$merge[$i]][0])) { + if (isset($this->info[$merge[$i]][0])) { // recursion - $merge = array_merge($merge, isset($info[$merge[$i]][0])); + $merge = array_merge($merge, $this->info[$merge[$i]][0]); } } unset($attr[0]); @@ -79,20 +82,47 @@ class HTMLPurifier_AttrCollections * @param $attr_types HTMLPurifier_AttrTypes instance */ 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 (!is_string($def)) 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 (isset($attr_types->info[$def])) { - $attr[$def_i] = $attr_types->info[$def]; + + if ($t = $attr_types->get($def)) { + $attr[$def_i] = $t; + $attr[$def_i]->required = $required; } else { - trigger_error('Attempted to reference undefined attribute type', E_USER_ERROR); unset($attr[$def_i]); } } + } } diff --git a/library/HTMLPurifier/AttrDef.php b/library/HTMLPurifier/AttrDef.php index 334a7ace..d9d2d944 100644 --- a/library/HTMLPurifier/AttrDef.php +++ b/library/HTMLPurifier/AttrDef.php @@ -14,11 +14,17 @@ class HTMLPurifier_AttrDef { /** - * Tells us whether or not an HTML attribute is minimized. Only the - * boolean attribute vapourware would use this. + * Tells us whether or not an HTML attribute is minimized. Has no + * meaning in other contexts. */ var $minimized = false; + /** + * Tells us whether or not an HTML attribute is required. Has no + * meaning in other contexts + */ + var $required = false; + /** * Validates and cleans passed string according to a definition. * @@ -62,6 +68,20 @@ class HTMLPurifier_AttrDef $string = str_replace(array("\r", "\t"), ' ', $string); return $string; } + + /** + * Factory method for creating this class from a string. + * @param $string String construction info + * @return Created AttrDef object corresponding to $string + * @public + */ + function make($string) { + // default implementation, return flyweight of this object + // if overloaded, it is *necessary* for you to clone the + // object (usually by instantiating a new copy) and return that + return $this; + } + } ?> \ No newline at end of file diff --git a/library/HTMLPurifier/AttrDef/CSS/Color.php b/library/HTMLPurifier/AttrDef/CSS/Color.php index 4e6a78ac..4b0fa231 100644 --- a/library/HTMLPurifier/AttrDef/CSS/Color.php +++ b/library/HTMLPurifier/AttrDef/CSS/Color.php @@ -2,43 +2,47 @@ require_once 'HTMLPurifier/AttrDef.php'; +HTMLPurifier_ConfigSchema::define( + 'Core', 'ColorKeywords', array( + 'maroon' => '#800000', + 'red' => '#FF0000', + 'orange' => '#FFA500', + 'yellow' => '#FFFF00', + 'olive' => '#808000', + 'purple' => '#800080', + 'fuchsia' => '#FF00FF', + 'white' => '#FFFFFF', + 'lime' => '#00FF00', + 'green' => '#008000', + 'navy' => '#000080', + 'blue' => '#0000FF', + 'aqua' => '#00FFFF', + 'teal' => '#008080', + 'black' => '#000000', + 'silver' => '#C0C0C0', + 'gray' => '#808080' + ), 'hash', ' +Lookup array of color names to six digit hexadecimal number corresponding +to color, with preceding hash mark. Used when parsing colors. +This directive has been available since 2.0.0. +'); + /** * Validates Color as defined by CSS. */ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef { - /** - * Color keyword lookup table. - * @todo Extend it to include all usually allowed colors. - */ - var $colors = array( - 'maroon' => '#800000', - 'red' => '#F00', - 'orange' => '#FFA500', - 'yellow' => '#FF0', - 'olive' => '#808000', - 'purple' => '#800080', - 'fuchsia' => '#F0F', - 'white' => '#FFF', - 'lime' => '#0F0', - 'green' => '#008000', - 'navy' => '#000080', - 'blue' => '#00F', - 'aqua' => '#0FF', - 'teal' => '#008080', - 'black' => '#000', - 'silver' => '#C0C0C0', - 'gray' => '#808080' - ); - 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($this->colors[$lower])) return $this->colors[$lower]; + if (isset($colors[$lower])) return $colors[$lower]; if ($color[0] === '#') { // hexadecimal handling diff --git a/library/HTMLPurifier/AttrDef/CSS/Font.php b/library/HTMLPurifier/AttrDef/CSS/Font.php index 1b3b0905..34dfa19a 100644 --- a/library/HTMLPurifier/AttrDef/CSS/Font.php +++ b/library/HTMLPurifier/AttrDef/CSS/Font.php @@ -18,18 +18,6 @@ class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef */ var $info = array(); - /** - * System font keywords. - */ - var $system_fonts = array( - 'caption' => true, - 'icon' => true, - 'menu' => true, - 'message-box' => true, - 'small-caption' => true, - 'status-bar' => true - ); - function HTMLPurifier_AttrDef_CSS_Font($config) { $def = $config->getCSSDefinition(); $this->info['font-style'] = $def->info['font-style']; @@ -42,13 +30,22 @@ class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef 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($this->system_fonts[$lowercase_string])) { + if (isset($system_fonts[$lowercase_string])) { return $lowercase_string; } diff --git a/library/HTMLPurifier/AttrDef/CSS/FontFamily.php b/library/HTMLPurifier/AttrDef/CSS/FontFamily.php index 15cbbf39..ab6d9d8b 100644 --- a/library/HTMLPurifier/AttrDef/CSS/FontFamily.php +++ b/library/HTMLPurifier/AttrDef/CSS/FontFamily.php @@ -10,19 +10,15 @@ require_once 'HTMLPurifier/AttrDef.php'; class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef { - /** - * Generic font family keywords. - * @protected - */ - var $generic_names = array( - 'serif' => true, - 'sans-serif' => true, - 'monospace' => true, - 'fantasy' => true, - 'cursive' => true - ); - function validate($string, $config, &$context) { + static $generic_names = array( + 'serif' => true, + 'sans-serif' => true, + 'monospace' => true, + 'fantasy' => true, + 'cursive' => true + ); + $string = $this->parseCDATA($string); // assume that no font names contain commas in them $fonts = explode(',', $string); @@ -31,7 +27,7 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef $font = trim($font); if ($font === '') continue; // match a generic name - if (isset($this->generic_names[$font])) { + if (isset($generic_names[$font])) { $final .= $font . ', '; continue; } diff --git a/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php index 294dd830..a5d82d10 100644 --- a/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php +++ b/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php @@ -10,23 +10,19 @@ require_once 'HTMLPurifier/AttrDef.php'; class HTMLPurifier_AttrDef_CSS_TextDecoration extends HTMLPurifier_AttrDef { - /** - * Lookup table of allowed values. - * @protected - */ - var $allowed_values = array( - 'line-through' => true, - 'overline' => true, - 'underline' => true - ); - function validate($string, $config, &$context) { + static $allowed_values = array( + 'line-through' => true, + 'overline' => true, + 'underline' => true + ); + $string = strtolower($this->parseCDATA($string)); $parts = explode(' ', $string); $final = ''; foreach ($parts as $part) { - if (isset($this->allowed_values[$part])) { + if (isset($allowed_values[$part])) { $final .= $part . ' '; } } diff --git a/library/HTMLPurifier/AttrDef/CSS/URI.php b/library/HTMLPurifier/AttrDef/CSS/URI.php index b310907c..ba1a628c 100644 --- a/library/HTMLPurifier/AttrDef/CSS/URI.php +++ b/library/HTMLPurifier/AttrDef/CSS/URI.php @@ -29,7 +29,7 @@ class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI if ($uri_string[$new_length] != ')') return false; $uri = trim(substr($uri_string, 0, $new_length)); - if (isset($uri[0]) && ($uri[0] == "'" || $uri[0] == '"')) { + if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) { $quote = $uri[0]; $new_length = strlen($uri) - 1; if ($uri[$new_length] !== $quote) return false; diff --git a/library/HTMLPurifier/AttrDef/Enum.php b/library/HTMLPurifier/AttrDef/Enum.php index 91a075f8..fce16b0a 100644 --- a/library/HTMLPurifier/AttrDef/Enum.php +++ b/library/HTMLPurifier/AttrDef/Enum.php @@ -45,6 +45,22 @@ class HTMLPurifier_AttrDef_Enum extends HTMLPurifier_AttrDef return $result ? $string : false; } + /** + * @param $string In form of comma-delimited list of case-insensitive + * valid values. Example: "foo,bar,baz". Prepend "s:" to make + * case sensitive + */ + function make($string) { + 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); + } + } ?> \ No newline at end of file diff --git a/library/HTMLPurifier/AttrDef/HTML/Bool.php b/library/HTMLPurifier/AttrDef/HTML/Bool.php new file mode 100644 index 00000000..7ba3f130 --- /dev/null +++ b/library/HTMLPurifier/AttrDef/HTML/Bool.php @@ -0,0 +1,30 @@ +<?php + +require_once 'HTMLPurifier/AttrDef.php'; + +/** + * Validates a boolean attribute + */ +class HTMLPurifier_AttrDef_HTML_Bool extends HTMLPurifier_AttrDef +{ + + var $name; + var $minimized = true; + + function HTMLPurifier_AttrDef_HTML_Bool($name = false) {$this->name = $name;} + + function validate($string, $config, &$context) { + if (empty($string)) return false; + return $this->name; + } + + /** + * @param $string Name of attribute + */ + function make($string) { + return new HTMLPurifier_AttrDef_HTML_Bool($string); + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/AttrDef/HTML/Color.php b/library/HTMLPurifier/AttrDef/HTML/Color.php new file mode 100644 index 00000000..8cfcfda5 --- /dev/null +++ b/library/HTMLPurifier/AttrDef/HTML/Color.php @@ -0,0 +1,35 @@ +<?php + +require_once 'HTMLPurifier/AttrDef.php'; +require_once 'HTMLPurifier/AttrDef/CSS/Color.php'; // for %Core.ColorKeywords + +/** + * Validates a color according to the HTML spec. + */ +class HTMLPurifier_AttrDef_HTML_Color extends HTMLPurifier_AttrDef +{ + + function validate($string, $config, &$context) { + + static $colors = null; + if ($colors === null) $colors = $config->get('Core', 'ColorKeywords'); + + $string = trim($string); + + if (empty($string)) return false; + if (isset($colors[$string])) return $colors[$string]; + if ($string[0] === '#') $hex = substr($string, 1); + else $hex = $string; + + $length = strlen($hex); + if ($length !== 3 && $length !== 6) return false; + if (!ctype_xdigit($hex)) return false; + if ($length === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; + + return "#$hex"; + + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php index 94a47ba9..854f88e5 100644 --- a/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php +++ b/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php @@ -26,22 +26,20 @@ HTMLPurifier_ConfigSchema::define( class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef { - /** Lookup array of attribute names to configuration name */ - var $configLookup = array( - 'rel' => 'AllowedRel', - 'rev' => 'AllowedRev' - ); - /** Name config attribute to pull. */ var $name; function HTMLPurifier_AttrDef_HTML_LinkTypes($name) { - if (!isset($this->configLookup[$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 = $this->configLookup[$name]; + $this->name = $configLookup[$name]; } function validate($string, $config, &$context) { diff --git a/library/HTMLPurifier/AttrDef/URI.php b/library/HTMLPurifier/AttrDef/URI.php index 71027181..ca214980 100644 --- a/library/HTMLPurifier/AttrDef/URI.php +++ b/library/HTMLPurifier/AttrDef/URI.php @@ -93,7 +93,6 @@ class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef { var $host; - var $PercentEncoder; var $embeds_resource; /** @@ -101,12 +100,14 @@ class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef */ function HTMLPurifier_AttrDef_URI($embeds_resource = false) { $this->host = new HTMLPurifier_AttrDef_URI_Host(); - $this->PercentEncoder = new HTMLPurifier_PercentEncoder(); $this->embeds_resource = (bool) $embeds_resource; } function validate($uri, $config, &$context) { + static $PercentEncoder = null; + if ($PercentEncoder === null) $PercentEncoder = new HTMLPurifier_PercentEncoder(); + // We'll write stack-based parsers later, for now, use regexps to // get things working as fast as possible (irony) @@ -116,7 +117,7 @@ class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef $uri = $this->parseCDATA($uri); // fix up percent-encoding - $uri = $this->PercentEncoder->normalize($uri); + $uri = $PercentEncoder->normalize($uri); // while it would be nice to use parse_url(), that's specifically // for HTTP and thus won't work for our generic URI parsing @@ -157,6 +158,14 @@ class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef ); } + // something funky weird happened in the registry, abort! + if (!$scheme_obj) { + trigger_error( + 'Default scheme object "' . $config->get('URI', 'DefaultScheme') . '" was not readable', + E_USER_WARNING + ); + return false; + } // the URI we're processing embeds_resource a resource in the page, but the URI // it references cannot be located diff --git a/library/HTMLPurifier/AttrDef/URI/IPv4.php b/library/HTMLPurifier/AttrDef/URI/IPv4.php index 0730bbc8..b9aa98ae 100644 --- a/library/HTMLPurifier/AttrDef/URI/IPv4.php +++ b/library/HTMLPurifier/AttrDef/URI/IPv4.php @@ -15,13 +15,10 @@ class HTMLPurifier_AttrDef_URI_IPv4 extends HTMLPurifier_AttrDef */ var $ip4; - function HTMLPurifier_AttrDef_URI_IPv4() { - $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})"; - } - function validate($aIP, $config, &$context) { + if (!$this->ip4) $this->_loadRegex(); + if (preg_match('#^' . $this->ip4 . '$#s', $aIP)) { return $aIP; @@ -31,6 +28,15 @@ class HTMLPurifier_AttrDef_URI_IPv4 extends HTMLPurifier_AttrDef } + /** + * Lazy load function to prevent regex from being stuffed in + * cache. + */ + 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})"; + } + } ?> \ No newline at end of file diff --git a/library/HTMLPurifier/AttrDef/URI/IPv6.php b/library/HTMLPurifier/AttrDef/URI/IPv6.php index 73f085e5..8de8f68f 100644 --- a/library/HTMLPurifier/AttrDef/URI/IPv6.php +++ b/library/HTMLPurifier/AttrDef/URI/IPv6.php @@ -13,6 +13,8 @@ class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4 function validate($aIP, $config, &$context) { + if (!$this->ip4) $this->_loadRegex(); + $original = $aIP; $hex = '[0-9a-fA-F]'; diff --git a/library/HTMLPurifier/AttrTransform/ImgRequired.php b/library/HTMLPurifier/AttrTransform/ImgRequired.php index 4ff356d8..159afd2f 100644 --- a/library/HTMLPurifier/AttrTransform/ImgRequired.php +++ b/library/HTMLPurifier/AttrTransform/ImgRequired.php @@ -20,7 +20,10 @@ HTMLPurifier_ConfigSchema::define( ); /** - * Post-transform that ensures the required attrs of img (alt and src) are set + * 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 { @@ -29,6 +32,7 @@ class HTMLPurifier_AttrTransform_ImgRequired extends HTMLPurifier_AttrTransform $src = true; if (!isset($attr['src'])) { + if ($config->get('Core', 'RemoveInvalidImg')) return $attr; $attr['src'] = $config->get('Attr', 'DefaultInvalidImage'); $src = false; } diff --git a/library/HTMLPurifier/AttrTypes.php b/library/HTMLPurifier/AttrTypes.php index e13d0d30..4064de78 100644 --- a/library/HTMLPurifier/AttrTypes.php +++ b/library/HTMLPurifier/AttrTypes.php @@ -1,10 +1,14 @@ <?php +require_once 'HTMLPurifier/AttrDef/Lang.php'; +require_once 'HTMLPurifier/AttrDef/Enum.php'; +require_once 'HTMLPurifier/AttrDef/HTML/Bool.php'; require_once 'HTMLPurifier/AttrDef/HTML/ID.php'; require_once 'HTMLPurifier/AttrDef/HTML/Length.php'; require_once 'HTMLPurifier/AttrDef/HTML/MultiLength.php'; require_once 'HTMLPurifier/AttrDef/HTML/Nmtokens.php'; require_once 'HTMLPurifier/AttrDef/HTML/Pixels.php'; +require_once 'HTMLPurifier/AttrDef/HTML/Color.php'; require_once 'HTMLPurifier/AttrDef/Integer.php'; require_once 'HTMLPurifier/AttrDef/Text.php'; require_once 'HTMLPurifier/AttrDef/URI.php'; @@ -16,14 +20,19 @@ class HTMLPurifier_AttrTypes { /** * Lookup array of attribute string identifiers to concrete implementations - * @public + * @protected */ var $info = array(); /** - * Constructs the info array + * Constructs the info array, supplying default implementations for attribute + * types. */ function HTMLPurifier_AttrTypes() { + // 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(); @@ -32,10 +41,42 @@ class HTMLPurifier_AttrTypes $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(); // number is really a positive integer (one or more digits) + // FIXME: ^^ not always, see start and value of list items $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); } + + /** + * Retrieves a type + * @param $type String type name + * @return Object AttrDef for type + */ + function get($type) { + + // determine if there is any extra info tacked on + if (strpos($type, '#') !== false) list($type, $string) = explode('#', $type, 2); + else $string = ''; + + if (!isset($this->info[$type])) { + trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); + return; + } + + return $this->info[$type]->make($string); + + } + + /** + * Sets a new implementation for a type + * @param $type String type name + * @param $impl Object AttrDef for type + */ + function set($type, $impl) { + $this->info[$type] = $impl; + } } ?> diff --git a/library/HTMLPurifier/AttrValidator.php b/library/HTMLPurifier/AttrValidator.php new file mode 100644 index 00000000..d6a1a563 --- /dev/null +++ b/library/HTMLPurifier/AttrValidator.php @@ -0,0 +1,105 @@ +<?php + +class HTMLPurifier_AttrValidator +{ + + + function validateToken($token, &$config, &$context) { + + $definition = $config->getHTMLDefinition(); + + // create alias to global definition array, see also $defs + // DEFINITION CALL + $d_defs = $definition->info_global_attr; + + // copy out attributes for easy manipulation + $attr = $token->attr; + + // do global transformations (pre) + // nothing currently utilizes this + foreach ($definition->info_attr_transform_pre as $transform) { + $attr = $transform->transform($attr, $config, $context); + } + + // 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($attr, $config, $context); + } + + // 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; + + // 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) { + // remove the attribute + unset($attr[$attr_key]); + } elseif (is_string($result)) { + // simple substitution + $attr[$attr_key] = $result; + } + + // 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). + } + + // post transforms + + // ex. <x lang="fr"> to <x lang="fr" xml:lang="fr"> + foreach ($definition->info_attr_transform_post as $transform) { + $attr = $transform->transform($attr, $config, $context); + } + + // ex. <bdo> to <bdo dir="ltr"> + foreach ($definition->info[$token->name]->attr_transform_post as $transform) { + $attr = $transform->transform($attr, $config, $context); + } + + // commit changes + $token->attr = $attr; + return $token; + + } + + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/CSSDefinition.php b/library/HTMLPurifier/CSSDefinition.php index 23a66ab7..78612f23 100644 --- a/library/HTMLPurifier/CSSDefinition.php +++ b/library/HTMLPurifier/CSSDefinition.php @@ -1,5 +1,7 @@ <?php +require_once 'HTMLPurifier/Definition.php'; + require_once 'HTMLPurifier/AttrDef/CSS/Background.php'; require_once 'HTMLPurifier/AttrDef/CSS/BackgroundPosition.php'; require_once 'HTMLPurifier/AttrDef/CSS/Border.php'; @@ -15,13 +17,24 @@ require_once 'HTMLPurifier/AttrDef/CSS/TextDecoration.php'; require_once 'HTMLPurifier/AttrDef/CSS/URI.php'; require_once 'HTMLPurifier/AttrDef/Enum.php'; +HTMLPurifier_ConfigSchema::define( + 'CSS', 'DefinitionRev', 1, 'int', ' +<p> + Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. This directive has been available + since 2.0.0. +</p> +'); + /** * Defines allowed CSS attributes and what their values are. * @see HTMLPurifier_HTMLDefinition */ -class HTMLPurifier_CSSDefinition +class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition { + var $type = 'CSS'; + /** * Assoc array of attribute name to definition object. */ @@ -30,7 +43,7 @@ class HTMLPurifier_CSSDefinition /** * Constructs the info array. The meat of this class. */ - function setup($config) { + function doSetup($config) { $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum( array('left', 'right', 'center', 'justify'), false); diff --git a/library/HTMLPurifier/ChildDef/Custom.php b/library/HTMLPurifier/ChildDef/Custom.php index de18cd70..9f90b04b 100644 --- a/library/HTMLPurifier/ChildDef/Custom.php +++ b/library/HTMLPurifier/ChildDef/Custom.php @@ -38,8 +38,21 @@ class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef if ($raw{0} != '(') { $raw = "($raw)"; } - $reg = str_replace(',', ',?', $raw); - $reg = preg_replace('/([#a-zA-Z0-9_.-]+)/', '(,?\\0)', $reg); + $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. + + // setup all elements as parentheticals with leading commas + $reg = preg_replace("/$el/", '(,\\0)', $reg); + + // remove commas when they were not solicited + $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg); + + // remove all non-paranthetical commas: they are handled by first regex + $reg = preg_replace("/,\(/", '(', $reg); + $this->_pcre_regex = $reg; } function validateChildren($tokens_of_children, $config, &$context) { @@ -60,11 +73,11 @@ class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef $list_of_children .= $token->name . ','; } } - $list_of_children = rtrim($list_of_children, ','); - + // add leading comma to deal with stray comma declarations + $list_of_children = ',' . rtrim($list_of_children, ','); $okay = preg_match( - '/^'.$this->_pcre_regex.'$/', + '/^,?'.$this->_pcre_regex.'$/', $list_of_children ); diff --git a/library/HTMLPurifier/ChildDef/Required.php b/library/HTMLPurifier/ChildDef/Required.php index c6f706e2..4d253398 100644 --- a/library/HTMLPurifier/ChildDef/Required.php +++ b/library/HTMLPurifier/ChildDef/Required.php @@ -29,7 +29,6 @@ class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef } } $this->elements = $elements; - $this->gen = new HTMLPurifier_Generator(); } var $allow_empty = false; var $type = 'required'; @@ -57,6 +56,12 @@ class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef // some configuration $escape_invalid_children = $config->get('Core', 'EscapeInvalidChildren'); + // generator + static $gen = null; + if ($gen === null) { + $gen = new HTMLPurifier_Generator(); + } + foreach ($tokens_of_children as $token) { if (!empty($token->is_whitespace)) { $result[] = $token; @@ -80,7 +85,7 @@ class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef $result[] = $token; } elseif ($pcdata_allowed && $escape_invalid_children) { $result[] = new HTMLPurifier_Token_Text( - $this->gen->generateFromToken($token, $config) + $gen->generateFromToken($token, $config) ); } continue; @@ -91,7 +96,7 @@ class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef } elseif ($pcdata_allowed && $escape_invalid_children) { $result[] = new HTMLPurifier_Token_Text( - $this->gen->generateFromToken( $token, $config ) + $gen->generateFromToken( $token, $config ) ); } else { // drop silently diff --git a/library/HTMLPurifier/ChildDef/StrictBlockquote.php b/library/HTMLPurifier/ChildDef/StrictBlockquote.php index 9280a9f5..c4c78810 100644 --- a/library/HTMLPurifier/ChildDef/StrictBlockquote.php +++ b/library/HTMLPurifier/ChildDef/StrictBlockquote.php @@ -45,8 +45,8 @@ extends HTMLPurifier_ChildDef_Required if (!$is_inline) { if (!$depth) { if ( - $token->type == 'text' || - !isset($this->elements[$token->name]) + ($token->type == 'text' && !$token->is_whitespace) || + ($token->type != 'text' && !isset($this->elements[$token->name])) ) { $is_inline = true; $ret[] = $block_wrap_start; diff --git a/library/HTMLPurifier/Config.php b/library/HTMLPurifier/Config.php index 620a3534..5ba0dfff 100644 --- a/library/HTMLPurifier/Config.php +++ b/library/HTMLPurifier/Config.php @@ -1,5 +1,28 @@ <?php +require_once 'HTMLPurifier/ConfigSchema.php'; + +// member variables +require_once 'HTMLPurifier/HTMLDefinition.php'; +require_once 'HTMLPurifier/CSSDefinition.php'; +require_once 'HTMLPurifier/Doctype.php'; +require_once 'HTMLPurifier/DefinitionCacheFactory.php'; + +// accomodations for versions earlier than 4.3.10 and 5.0.2 +// borrowed from PHP_Compat, LGPL licensed, by Aidan Lister <aidan@php.net> +if (!defined('PHP_EOL')) { + switch (strtoupper(substr(PHP_OS, 0, 3))) { + case 'WIN': + define('PHP_EOL', "\r\n"); + break; + case 'DAR': + define('PHP_EOL', "\r"); + break; + default: + define('PHP_EOL', "\n"); + } +} + /** * Configuration object that triggers customizable behavior. * @@ -15,6 +38,11 @@ class HTMLPurifier_Config { + /** + * HTML Purifier's version + */ + var $version = '2.0.0'; + /** * Two-level associative array of configuration directives */ @@ -26,14 +54,26 @@ class HTMLPurifier_Config var $def; /** - * Cached instance of HTMLPurifier_HTMLDefinition + * Indexed array of definitions */ - var $html_definition; + var $definitions; /** - * Cached instance of HTMLPurifier_CSSDefinition + * Bool indicator whether or not config is finalized */ - var $css_definition; + var $finalized = false; + + /** + * Bool indicator whether or not to automatically finalize + * the object if a read operation is done + */ + var $autoFinalize = true; + + /** + * Namespace indexed array of serials for specific namespaces (see + * getSerial for more info). + */ + var $serials = array(); /** * @param $definition HTMLPurifier_ConfigSchema that defines what directives @@ -58,6 +98,7 @@ class HTMLPurifier_Config $ret = HTMLPurifier_Config::createDefault(); if (is_string($config)) $ret->loadIni($config); elseif (is_array($config)) $ret->loadArray($config); + if (isset($revision)) $ret->revision = $revision; return $ret; } @@ -78,13 +119,16 @@ class HTMLPurifier_Config * @param $key String key */ function get($namespace, $key, $from_alias = false) { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); if (!isset($this->def->info[$namespace][$key])) { - trigger_error('Cannot retrieve value of undefined directive', + // can't add % due to SimpleTest bug + trigger_error('Cannot retrieve value of undefined directive ' . htmlspecialchars("$namespace.$key"), E_USER_WARNING); return; } if ($this->def->info[$namespace][$key]->class == 'alias') { - trigger_error('Cannot get value from aliased directive, use real name', + $d = $this->def->info[$namespace][$key]; + trigger_error('Cannot get value from aliased directive, use real name ' . $d->namespace . '.' . $d->name, E_USER_ERROR); return; } @@ -96,14 +140,35 @@ class HTMLPurifier_Config * @param $namespace String namespace */ function getBatch($namespace) { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); if (!isset($this->def->info[$namespace])) { - trigger_error('Cannot retrieve undefined namespace', + trigger_error('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace), E_USER_WARNING); return; } return $this->conf[$namespace]; } + /** + * Returns a md5 signature of a segment of the configuration object + * that uniquely identifies that particular configuration + * @param $namespace Namespace to get serial for + */ + function getBatchSerial($namespace) { + if (empty($this->serials[$namespace])) { + $this->serials[$namespace] = md5(serialize($this->getBatch($namespace))); + } + return $this->serials[$namespace]; + } + + /** + * Retrieves all directives, organized by namespace + */ + function getAll() { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); + return $this->conf; + } + /** * Sets a value to configuration. * @param $namespace String namespace @@ -111,15 +176,16 @@ class HTMLPurifier_Config * @param $value Mixed value */ function set($namespace, $key, $value, $from_alias = false) { + if ($this->isFinalized('Cannot set directive after finalization')) return; if (!isset($this->def->info[$namespace][$key])) { - trigger_error('Cannot set undefined directive to value', + trigger_error('Cannot set undefined directive ' . htmlspecialchars("$namespace.$key") . ' to value', E_USER_WARNING); return; } if ($this->def->info[$namespace][$key]->class == 'alias') { if ($from_alias) { trigger_error('Double-aliases not allowed, please fix '. - 'ConfigSchema bug'); + 'ConfigSchema bug with' . "$namespace.$key"); } $this->set($this->def->info[$namespace][$key]->namespace, $this->def->info[$namespace][$key]->name, @@ -128,7 +194,7 @@ class HTMLPurifier_Config } $value = $this->def->validate( $value, - $this->def->info[$namespace][$key]->type, + $type = $this->def->info[$namespace][$key]->type, $this->def->info[$namespace][$key]->allow_null ); if (is_string($value)) { @@ -139,23 +205,36 @@ class HTMLPurifier_Config if ($this->def->info[$namespace][$key]->allowed !== true) { // check to see if the value is allowed if (!isset($this->def->info[$namespace][$key]->allowed[$value])) { - trigger_error('Value not supported', E_USER_WARNING); + trigger_error('Value not supported, valid values are: ' . + $this->_listify($this->def->info[$namespace][$key]->allowed), E_USER_WARNING); return; } } } if ($this->def->isError($value)) { - trigger_error('Value is of invalid type', E_USER_WARNING); + trigger_error('Value for ' . "$namespace.$key" . ' is of invalid type, should be ' . $type, E_USER_WARNING); return; } $this->conf[$namespace][$key] = $value; - if ($namespace == 'HTML' || $namespace == 'Attr') { - // reset HTML definition if relevant attributes changed - $this->html_definition = null; - } - if ($namespace == 'CSS') { - $this->css_definition = null; + + // 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') { + $this->definitions[$namespace] = null; } + + $this->serials[$namespace] = false; + } + + /** + * Convenience function for error reporting + * @private + */ + function _listify($lookup) { + $list = array(); + foreach ($lookup as $name => $b) $list[] = $name; + return implode(', ', $list); } /** @@ -164,26 +243,71 @@ class HTMLPurifier_Config * called before it's been setup, otherwise won't work. */ function &getHTMLDefinition($raw = false) { - if ( - empty($this->html_definition) || // hasn't ever been setup - ($raw && $this->html_definition->setup) // requesting new one - ) { - $this->html_definition = new HTMLPurifier_HTMLDefinition($this); - if ($raw) return $this->html_definition; // no setup! - } - if (!$this->html_definition->setup) $this->html_definition->setup(); - return $this->html_definition; + return $this->getDefinition('HTML', $raw); } /** * Retrieves reference to the CSS definition */ - function &getCSSDefinition() { - if ($this->css_definition === null) { - $this->css_definition = new HTMLPurifier_CSSDefinition(); - $this->css_definition->setup($this); + function &getCSSDefinition($raw = false) { + return $this->getDefinition('CSS', $raw); + } + + /** + * Retrieves a definition + * @param $type Type of definition: HTML, CSS, etc + * @param $raw Whether or not definition should be returned raw + */ + function &getDefinition($type, $raw = false) { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create($type, $this); + if (!$raw) { + // see if we can quickly supply a definition + if (!empty($this->definitions[$type])) { + if (!$this->definitions[$type]->setup) { + $this->definitions[$type]->setup($this); + } + return $this->definitions[$type]; + } + // memory check missed, try cache + $this->definitions[$type] = $cache->get($this); + if ($this->definitions[$type]) { + // definition in cache, return it + return $this->definitions[$type]; + } + } elseif ( + !empty($this->definitions[$type]) && + !$this->definitions[$type]->setup + ) { + // raw requested, raw in memory, quick return + return $this->definitions[$type]; } - return $this->css_definition; + // quick checks failed, let's create the object + if ($type == 'HTML') { + $this->definitions[$type] = new HTMLPurifier_HTMLDefinition(); + } elseif ($type == 'CSS') { + $this->definitions[$type] = new HTMLPurifier_CSSDefinition(); + } else { + trigger_error("Definition of $type type not supported"); + $false = false; + return $false; + } + // quick abort if raw + if ($raw) { + if (is_null($this->get($type, 'DefinitionID'))) { + // fatally error out if definition ID not set + trigger_error("Cannot retrieve raw version without specifying %$type.DefinitionID", E_USER_ERROR); + $false = false; + return $false; + } + return $this->definitions[$type]; + } + // set it up + $this->definitions[$type]->setup($this); + // save in cache + $cache->set($this->definitions[$type], $this); + return $this->definitions[$type]; } /** @@ -192,6 +316,7 @@ class HTMLPurifier_Config * @param $config_array Configuration associative array */ 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) { @@ -208,15 +333,63 @@ class HTMLPurifier_Config } } + /** + * Loads configuration values from $_GET/$_POST that were posted + * via ConfigForm + * @param $array $_GET or $_POST array to import + * @param $index Index/name that the config variables are in + * @param $mq_fix Boolean whether or not to enable magic quotes fix + * @static + */ + function loadArrayFromForm($array, $index, $mq_fix = true) { + $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); + $mq = get_magic_quotes_gpc() && $mq_fix; + foreach ($array as $key => $value) { + if (!strncmp($key, 'Null_', 5) && !empty($value)) { + unset($array[substr($key, 5)]); + unset($array[$key]); + } + if ($mq) $array[$key] = stripslashes($value); + } + return @HTMLPurifier_Config::create($array); + } + /** * Loads configuration values from an ini file * @param $filename Name of ini file */ function loadIni($filename) { + if ($this->isFinalized('Cannot load directives after finalization')) return; $array = parse_ini_file($filename, true); $this->loadArray($array); } + /** + * Checks whether or not the configuration object is finalized. + * @param $error String error message, or false for no error + */ + function isFinalized($error = false) { + if ($this->finalized && $error) { + trigger_error($error, E_USER_ERROR); + } + return $this->finalized; + } + + /** + * Finalizes configuration only if auto finalize is on and not + * already finalized + */ + function autoFinalize() { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); + } + + /** + * Finalizes a configuration object, prohibiting further change + */ + function finalize() { + $this->finalized = true; + } + } ?> diff --git a/library/HTMLPurifier/ConfigSchema.php b/library/HTMLPurifier/ConfigSchema.php index 3d502285..5a3fc07e 100644 --- a/library/HTMLPurifier/ConfigSchema.php +++ b/library/HTMLPurifier/ConfigSchema.php @@ -8,6 +8,7 @@ require_once 'HTMLPurifier/ConfigDef/DirectiveAlias.php'; /** * Configuration definition, defines directives and their defaults. + * @note If you update this, please update Printer_ConfigForm * @todo The ability to define things multiple times is confusing and should * be factored out to its own function named registerDependency() or * addNote(), where only the namespace.name and an extra descriptions @@ -66,6 +67,8 @@ class HTMLPurifier_ConfigSchema { $this->defineNamespace('URI', 'Features regarding Uniform Resource Identifiers.'); $this->defineNamespace('HTML', 'Configuration regarding allowed HTML.'); $this->defineNamespace('CSS', 'Configuration regarding allowed CSS.'); + $this->defineNamespace('Output', 'Configuration relating to the generation of (X)HTML.'); + $this->defineNamespace('Cache', 'Configuration for DefinitionCache and related subclasses.'); $this->defineNamespace('Test', 'Developer testing configuration for our unit tests.'); } @@ -303,6 +306,7 @@ class HTMLPurifier_ConfigSchema { if ($allow_null && $var === null) return null; switch ($type) { case 'mixed': + //if (is_string($var)) $var = unserialize($var); return $var; case 'istring': case 'string': @@ -343,6 +347,16 @@ class HTMLPurifier_ConfigSchema { $var = explode(',',$var); // remove spaces foreach ($var as $i => $j) $var[$i] = trim($j); + if ($type === 'hash') { + // key:value,key2:value2 + $nvar = array(); + foreach ($var as $keypair) { + $c = explode(':', $keypair, 2); + if (!isset($c[1])) continue; + $nvar[$c[0]] = $c[1]; + } + $var = $nvar; + } } if (!is_array($var)) break; $keys = array_keys($var); diff --git a/library/HTMLPurifier/ContentSets.php b/library/HTMLPurifier/ContentSets.php index de5c532e..42d87b4d 100644 --- a/library/HTMLPurifier/ContentSets.php +++ b/library/HTMLPurifier/ContentSets.php @@ -6,6 +6,8 @@ require_once 'HTMLPurifier/ChildDef/Empty.php'; require_once 'HTMLPurifier/ChildDef/Required.php'; require_once 'HTMLPurifier/ChildDef/Optional.php'; +// NOT UNIT TESTED!!! + class HTMLPurifier_ContentSets { diff --git a/library/HTMLPurifier/Definition.php b/library/HTMLPurifier/Definition.php new file mode 100644 index 00000000..f45951bb --- /dev/null +++ b/library/HTMLPurifier/Definition.php @@ -0,0 +1,41 @@ +<?php + +/** + * Super-class for definition datatype objects, implements serialization + * functions for the class. + */ +class HTMLPurifier_Definition +{ + + /** + * Has setup() been called yet? + */ + var $setup = false; + + /** + * What type of definition is it? + */ + var $type; + + /** + * Sets up the definition object into the final form, something + * not done by the constructor + * @param $config HTMLPurifier_Config instance + */ + function doSetup($config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Setup function that aborts if already setup + * @param $config HTMLPurifier_Config instance + */ + function setup($config) { + if ($this->setup) return; + $this->setup = true; + $this->doSetup($config); + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache.php b/library/HTMLPurifier/DefinitionCache.php new file mode 100644 index 00000000..81cd7b33 --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache.php @@ -0,0 +1,121 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache/Serializer.php'; +require_once 'HTMLPurifier/DefinitionCache/Null.php'; + +require_once 'HTMLPurifier/DefinitionCache/Decorator.php'; + +/** + * Abstract class representing Definition cache managers that implements + * useful common methods and is a factory. + * @todo Get some sort of versioning variable so the library can easily + * invalidate the cache with a new version + * @todo Make the test runner cache aware and allow the user to easily + * flush the cache + * @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 + */ +class HTMLPurifier_DefinitionCache +{ + + var $type; + + /** + * @param $name Type of definition objects this instance of the + * cache will handle. + */ + function HTMLPurifier_DefinitionCache($type) { + $this->type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param Instance of HTMLPurifier_Config + */ + function generateKey($config) { + return $config->version . '-' . // possibly replace with function calls + $config->get($this->type, 'DefinitionRev') . '-' . + $config->getBatchSerial($this->type); + } + + /** + * Tests whether or not a key is old with respect to the configuration's + * version and revision number. + * @param $key Key to test + * @param $config Instance of HTMLPurifier_Config to test against + */ + function isOld($key, $config) { + list($version, $revision, $hash) = explode('-', $key, 3); + $compare = version_compare($version, $config->version); + if ($compare > 0) return false; + if ($compare == 0 && $revision >= $config->get($this->type, 'DefinitionRev')) return false; + return true; + } + + /** + * Checks if a definition's type jives with the cache's type + * @note Throws an error on failure + * @param $def Definition object to check + * @return Boolean true if good, false if not + */ + function checkDefType($def) { + if ($def->type !== $this->type) { + trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); + return false; + } + return true; + } + + /** + * Adds a definition object to the cache + */ + function add($def, $config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Unconditionally saves a definition object to the cache + */ + function set($def, $config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Replace an object in the cache + */ + function replace($def, $config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Retrieves a definition object from the cache + */ + function get($config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Removes a definition object to the cache + */ + function remove($config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Clears all objects from cache + */ + function flush($config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Clears all expired (older version or revision) objects from cache + */ + function cleanup($config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Decorator.php b/library/HTMLPurifier/DefinitionCache/Decorator.php new file mode 100644 index 00000000..46546717 --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Decorator.php @@ -0,0 +1,63 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache.php'; + +require_once 'HTMLPurifier/DefinitionCache/Decorator/Memory.php'; +require_once 'HTMLPurifier/DefinitionCache/Decorator/Cleanup.php'; + +class HTMLPurifier_DefinitionCache_Decorator extends HTMLPurifier_DefinitionCache +{ + + /** + * Cache object we are decorating + */ + var $cache; + + function HTMLPurifier_DefinitionCache_Decorator() {} + + /** + * Lazy decorator function + * @param $cache Reference to cache object to decorate + */ + 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 + */ + function copy() { + return new HTMLPurifier_DefinitionCache_Decorator(); + } + + function add($def, $config) { + return $this->cache->add($def, $config); + } + + function set($def, $config) { + return $this->cache->set($def, $config); + } + + function replace($def, $config) { + return $this->cache->replace($def, $config); + } + + function get($config) { + return $this->cache->get($config); + } + + function flush($config) { + return $this->cache->flush($config); + } + + function cleanup($config) { + return $this->cache->cleanup($config); + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php new file mode 100644 index 00000000..f67a0c6a --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php @@ -0,0 +1,45 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache/Decorator.php'; + +/** + * Definition cache decorator class that cleans up the cache + * whenever there is a cache miss. + */ +class HTMLPurifier_DefinitionCache_Decorator_Cleanup extends + HTMLPurifier_DefinitionCache_Decorator +{ + + var $name = 'Cleanup'; + + function copy() { + return new HTMLPurifier_DefinitionCache_Decorator_Cleanup(); + } + + function add($def, $config) { + $status = parent::add($def, $config); + if (!$status) parent::cleanup($config); + return $status; + } + + function set($def, $config) { + $status = parent::set($def, $config); + if (!$status) parent::cleanup($config); + return $status; + } + + function replace($def, $config) { + $status = parent::replace($def, $config); + if (!$status) parent::cleanup($config); + return $status; + } + + function get($config) { + $ret = parent::get($config); + if (!$ret) parent::cleanup($config); + return $ret; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php b/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php new file mode 100644 index 00000000..91817f9a --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php @@ -0,0 +1,48 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache/Decorator.php'; + +/** + * 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 +{ + + var $definitions; + var $name = 'Memory'; + + function copy() { + return new HTMLPurifier_DefinitionCache_Decorator_Memory(); + } + + function add($def, $config) { + $status = parent::add($def, $config); + if ($status) $this->definitions[$this->generateKey($config)] = $def; + return $status; + } + + function set($def, $config) { + $status = parent::set($def, $config); + if ($status) $this->definitions[$this->generateKey($config)] = $def; + return $status; + } + + function replace($def, $config) { + $status = parent::replace($def, $config); + if ($status) $this->definitions[$this->generateKey($config)] = $def; + return $status; + } + + 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]; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in new file mode 100644 index 00000000..71acf0c9 --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in @@ -0,0 +1,47 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache/Decorator.php'; + +/** + * Definition cache decorator template. + */ +class HTMLPurifier_DefinitionCache_Decorator_Template extends + HTMLPurifier_DefinitionCache_Decorator +{ + + var $name = 'Template'; // replace this + + function copy() { + // replace class name with yours + return new HTMLPurifier_DefinitionCache_Decorator_Template(); + } + + // remove methods you don't need + + function add($def, $config) { + return parent::add($def, $config); + } + + function set($def, $config) { + return parent::set($def, $config); + } + + function replace($def, $config) { + return parent::replace($def, $config); + } + + function get($config) { + return parent::get($config); + } + + function flush() { + return parent::flush(); + } + + function cleanup($config) { + return parent::cleanup($config); + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Null.php b/library/HTMLPurifier/DefinitionCache/Null.php new file mode 100644 index 00000000..bc7e25ac --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Null.php @@ -0,0 +1,37 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache.php'; + +/** + * Null cache object to use when no caching is on. + */ +class HTMLPurifier_DefinitionCache_Null extends HTMLPurifier_DefinitionCache +{ + + function add($def, $config) { + return false; + } + + function set($def, $config) { + return false; + } + + function replace($def, $config) { + return false; + } + + function get($config) { + return false; + } + + function flush($config) { + return false; + } + + function cleanup($config) { + return false; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Serializer.php b/library/HTMLPurifier/DefinitionCache/Serializer.php new file mode 100644 index 00000000..64b79eba --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Serializer.php @@ -0,0 +1,129 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache.php'; + +HTMLPurifier_ConfigSchema::define( + 'Cache', 'SerializerPath', null, 'string/null', ' +<p> + Absolute path with no trailing slash to store serialized definitions in. + Default is within the + HTML Purifier library inside DefinitionCache/Serializer. This + path must be writable by the webserver. This directive has been + available since 2.0.0. +</p> +'); + +class HTMLPurifier_DefinitionCache_Serializer extends + HTMLPurifier_DefinitionCache +{ + + function add($def, $config) { + if (!$this->checkDefType($def)) return; + $file = $this->generateFilePath($config); + if (file_exists($file)) return false; + $this->_prepareDir($config); + return $this->_write($file, serialize($def)); + } + + function set($def, $config) { + if (!$this->checkDefType($def)) return; + $file = $this->generateFilePath($config); + $this->_prepareDir($config); + return $this->_write($file, serialize($def)); + } + + function replace($def, $config) { + if (!$this->checkDefType($def)) return; + $file = $this->generateFilePath($config); + if (!file_exists($file)) return false; + $this->_prepareDir($config); + return $this->_write($file, serialize($def)); + } + + function get($config) { + $file = $this->generateFilePath($config); + if (!file_exists($file)) return false; + return unserialize(file_get_contents($file)); + } + + function remove($config) { + $file = $this->generateFilePath($config); + if (!file_exists($file)) return false; + return unlink($file); + } + + function flush($config) { + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) continue; + if ($filename[0] === '.') continue; + unlink($dir . '/' . $filename); + } + } + + function cleanup($config) { + $this->_prepareDir($config); + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) continue; + if ($filename[0] === '.') continue; + $key = substr($filename, 0, strlen($filename) - 4); + if ($this->isOld($key, $config)) unlink($dir . '/' . $filename); + } + } + + /** + * Generates the file path to the serial file corresponding to + * the configuration and definition name + */ + function generateFilePath($config) { + $key = $this->generateKey($config); + return $this->generateDirectoryPath($config) . '/' . $key . '.ser'; + } + + /** + * Generates the path to the directory contain this cache's serial files + * @note No trailing slash + */ + function generateDirectoryPath($config) { + $base = $config->get('Cache', 'SerializerPath'); + $base = is_null($base) ? dirname(__FILE__) . '/Serializer' : $base; + return $base . '/' . $this->type; + } + + /** + * Convenience wrapper function for file_put_contents + * @param $file File name to write to + * @param $data Data to write into file + * @return Number of bytes written if success, or false if failure. + */ + function _write($file, $data) { + static $file_put_contents; + if ($file_put_contents === null) { + $file_put_contents = function_exists('file_put_contents'); + } + if ($file_put_contents) { + return file_put_contents($file, $data); + } + $fh = fopen($file, 'w'); + if (!$fh) return false; + $status = fwrite($fh, $data); + fclose($fh); + return $status; + } + + /** + * Prepares the directory that this type stores the serials in + */ + function _prepareDir($config) { + $directory = $this->generateDirectoryPath($config); + if (!is_dir($directory)) { + mkdir($directory); + } + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCacheFactory.php b/library/HTMLPurifier/DefinitionCacheFactory.php new file mode 100644 index 00000000..cf4253e6 --- /dev/null +++ b/library/HTMLPurifier/DefinitionCacheFactory.php @@ -0,0 +1,90 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache.php'; + +HTMLPurifier_ConfigSchema::define( + 'Core', 'DefinitionCache', 'Serializer', 'string/null', ' +This directive defines which method to use when caching definitions, +the complex data-type that makes HTML Purifier tick. Set to null +to disable caching (not recommended, as you will see a definite +performance degradation). This directive has been available since 2.0.0. +'); + +HTMLPurifier_ConfigSchema::defineAllowedValues( + 'Core', 'DefinitionCache', array('Serializer') +); + + +/** + * Responsible for creating definition caches. + */ +class HTMLPurifier_DefinitionCacheFactory +{ + + var $caches = array('Serializer' => array()); + var $decorators = array(); + + /** + * Initialize default decorators + */ + function setup() { + $this->addDecorator('Cleanup'); + } + + /** + * Retrieves an instance of global definition cache factory. + * @static + */ + 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; + } + + /** + * Factory method that creates a cache object based on configuration + * @param $name Name of definitions handled by cache + * @param $config Instance of HTMLPurifier_Config + */ + function &create($type, $config) { + // only one implementation as for right now, $config will + // be used to determine implementation + $method = $config->get('Core', 'DefinitionCache'); + if ($method === null) { + $null = new HTMLPurifier_DefinitionCache_Null($type); + return $null; + } + if (!empty($this->caches[$method][$type])) { + return $this->caches[$method][$type]; + } + $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 + */ + function addDecorator($decorator) { + if (is_string($decorator)) { + $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; + $decorator = new $class; + } + $this->decorators[$decorator->name] = $decorator; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/Doctype.php b/library/HTMLPurifier/Doctype.php new file mode 100644 index 00000000..d54d5fe1 --- /dev/null +++ b/library/HTMLPurifier/Doctype.php @@ -0,0 +1,55 @@ +<?php + +/** + * Represents a document type, contains information on which modules + * need to be loaded. + */ +class HTMLPurifier_Doctype +{ + /** + * Full name of doctype + */ + var $name; + + /** + * List of standard modules (string identifiers or literal objects) + * that this doctype uses + */ + var $modules = array(); + + /** + * List of modules to use for tidying up code + */ + var $tidyModules = array(); + + /** + * Is the language derived from XML (i.e. XHTML)? + */ + var $xml = true; + + /** + * List of aliases for this doctype + */ + var $aliases = array(); + + function HTMLPurifier_Doctype($name = null, $xml = true, $modules = array(), + $tidyModules = array(), $aliases = array() + ) { + $this->name = $name; + $this->xml = $xml; + $this->modules = $modules; + $this->tidyModules = $tidyModules; + $this->aliases = $aliases; + } + + /** + * Clones the doctype, use before resolving modes and the like + */ + function copy() { + return new HTMLPurifier_Doctype( + $this->name, $this->xml, $this->modules, $this->tidyModules, $this->aliases + ); + } +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DoctypeRegistry.php b/library/HTMLPurifier/DoctypeRegistry.php new file mode 100644 index 00000000..57ccd506 --- /dev/null +++ b/library/HTMLPurifier/DoctypeRegistry.php @@ -0,0 +1,125 @@ +<?php + +require_once 'HTMLPurifier/Doctype.php'; + +// Legacy directives for doctype specification +HTMLPurifier_ConfigSchema::define( + 'HTML', 'Strict', false, 'bool', + 'Determines whether or not to use Transitional (loose) or Strict rulesets. '. + 'This directive is deprecated in favor of %HTML.Doctype. '. + 'This directive has been available since 1.3.0.' +); + +HTMLPurifier_ConfigSchema::define( + 'HTML', 'XHTML', true, 'bool', + 'Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. '. + 'This directive is deprecated in favor of %HTML.Doctype. '. + 'This directive was available since 1.1.' +); +HTMLPurifier_ConfigSchema::defineAlias('Core', 'XHTML', 'HTML', 'XHTML'); + +class HTMLPurifier_DoctypeRegistry +{ + + /** + * Hash of doctype names to doctype objects + * @protected + */ + var $doctypes; + + /** + * Lookup table of aliases to real doctype names + * @protected + */ + var $aliases; + + /** + * Registers a doctype to the registry + * @note Accepts a fully-formed doctype object, or the + * parameters for constructing a doctype object + * @param $doctype Name of doctype or literal doctype object + * @param $modules Modules doctype will load + * @param $modules_for_modes Modules doctype will load for certain modes + * @param $aliases Alias names for doctype + * @return Reference to registered doctype (usable for further editing) + */ + function &register($doctype, $xml = true, $modules = array(), + $tidy_modules = array(), $aliases = array() + ) { + 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 + ); + } + $this->doctypes[$doctype->name] =& $doctype; + $name = $doctype->name; + // hookup aliases + foreach ($doctype->aliases as $alias) { + if (isset($this->doctypes[$alias])) continue; + $this->aliases[$alias] = $name; + } + // remove old aliases + if (isset($this->aliases[$name])) unset($this->aliases[$name]); + return $doctype; + } + + /** + * Retrieves reference to a doctype of a certain name + * @note This function resolves aliases + * @note When possible, use the more fully-featured make() + * @param $doctype Name of doctype + * @return Reference to doctype object + */ + 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'); + $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). + */ + function make($config) { + $original_doctype = $this->get($this->getDoctypeFromConfig($config)); + $doctype = $original_doctype->copy(); + return $doctype; + } + + /** + * Retrieves the doctype from the configuration object + */ + function getDoctypeFromConfig($config) { + // recommended test + $doctype = $config->get('HTML', 'Doctype'); + if ($doctype !== null) { + 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; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/ElementDef.php b/library/HTMLPurifier/ElementDef.php index 73c94abe..9c744ed2 100644 --- a/library/HTMLPurifier/ElementDef.php +++ b/library/HTMLPurifier/ElementDef.php @@ -51,6 +51,8 @@ class HTMLPurifier_ElementDef * Abstract string representation of internal ChildDef rules. See * HTMLPurifier_ContentSets for how this is parsed and then transformed * into an HTMLPurifier_ChildDef. + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition * @public */ var $content_model; @@ -58,6 +60,9 @@ class HTMLPurifier_ElementDef /** * Value of $child->type, used to determine which ChildDef to use, * used in combination with $content_model. + * @warning This must be lowercase + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition * @public */ var $content_model_type; @@ -78,14 +83,47 @@ class HTMLPurifier_ElementDef * have to worry about this one. * @public */ - var $descendants_are_inline; + var $descendants_are_inline = false; + + /** + * List of the names of required attributes this element has. Dynamically + * populated. + * @public + */ + var $required_attr = array(); /** * Lookup table of tags excluded from all descendants of this tag. + * @note SGML permits exclusions for all descendants, but this is + * not possible with DTDs or XML Schemas. W3C has elected to + * use complicated compositions of content_models to simulate + * exclusion for children, but we go the simpler, SGML-style + * route of flat-out exclusions, which correctly apply to + * all descendants and not just children. Note that the XHTML + * Modularization Abstract Modules are blithely unaware of such + * distinctions. * @public */ var $excludes = array(); + /** + * Is this element safe for untrusted users to use? + */ + var $safe; + + /** + * Low-level factory constructor for creating new standalone element defs + * @static + */ + static function create($safe, $content_model, $content_model_type, $attr) { + $def = new HTMLPurifier_ElementDef(); + $def->safe = (bool) $safe; + $def->content_model = $content_model; + $def->content_model_type = $content_model_type; + $def->attr = $attr; + return $def; + } + /** * Merges the values of another element definition into this one. * Values from the new element def take precedence if a value is @@ -99,24 +137,57 @@ class HTMLPurifier_ElementDef // merge in the includes // sorry, no way to override an include foreach ($v as $v2) { - $def->attr[0][] = $v2; + $this->attr[0][] = $v2; } continue; } + if ($v === false) { + if (isset($this->attr[$k])) unset($this->attr[$k]); + continue; + } $this->attr[$k] = $v; } - foreach($def->attr_transform_pre as $k => $v) $this->attr_transform_pre[$k] = $v; - foreach($def->attr_transform_post as $k => $v) $this->attr_transform_post[$k] = $v; - foreach($def->auto_close as $k => $v) $this->auto_close[$k] = $v; - foreach($def->excludes as $k => $v) $this->excludes[$k] = $v; + $this->_mergeAssocArray($this->attr_transform_pre, $def->attr_transform_pre); + $this->_mergeAssocArray($this->attr_transform_post, $def->attr_transform_post); + $this->_mergeAssocArray($this->auto_close, $def->auto_close); + $this->_mergeAssocArray($this->excludes, $def->excludes); + if(!empty($def->content_model)) { + $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(!empty($def->content_model)) $this->content_model .= ' | ' . $def->content_model; - if(!empty($def->content_model_type)) $this->content_model_type = $def->content_model_type; - if(!is_null($def->descendants_are_inline)) $this->descendants_are_inline = $def->descendants_are_inline; + if($def->descendants_are_inline) $this->descendants_are_inline = $def->descendants_are_inline; + if(!is_null($def->safe)) $this->safe = $def->safe; } + /** + * Merges one array into another, removes values which equal false + * @param $a1 Array by reference that is merged into + * @param $a2 Array that merges into $a1 + */ + function _mergeAssocArray(&$a1, $a2) { + foreach ($a2 as $k => $v) { + if ($v === false) { + if (isset($a1[$k])) unset($a1[$k]); + continue; + } + $a1[$k] = $v; + } + } + + /** + * Retrieves a copy of the element definition + */ + function copy() { + return unserialize(serialize($this)); + } + } ?> diff --git a/library/HTMLPurifier/Encoder.php b/library/HTMLPurifier/Encoder.php index 84785d74..9844850a 100644 --- a/library/HTMLPurifier/Encoder.php +++ b/library/HTMLPurifier/Encoder.php @@ -1,7 +1,5 @@ <?php -require_once 'HTMLPurifier/EntityLookup.php'; - HTMLPurifier_ConfigSchema::define( 'Core', 'Encoding', 'utf-8', 'istring', 'If for some reason you are unable to convert all webpages to UTF-8, '. diff --git a/library/HTMLPurifier/EntityParser.php b/library/HTMLPurifier/EntityParser.php index 069c5ce1..d5422995 100644 --- a/library/HTMLPurifier/EntityParser.php +++ b/library/HTMLPurifier/EntityParser.php @@ -24,8 +24,8 @@ class HTMLPurifier_EntityParser * @protected */ var $_substituteEntitiesRegex = -'/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z]+));?/'; -// 1. hex 2. dec 3. string +'/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/'; +// 1. hex 2. dec 3. string (XML style) /** @@ -97,7 +97,6 @@ class HTMLPurifier_EntityParser } else { if (isset($this->_special_ent2dec[$matches[3]])) return $entity; if (!$this->_entity_lookup) { - require_once 'HTMLPurifier/EntityLookup.php'; $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); } if (isset($this->_entity_lookup->table[$matches[3]])) { diff --git a/library/HTMLPurifier/ErrorCollector.php b/library/HTMLPurifier/ErrorCollector.php new file mode 100644 index 00000000..db0c091e --- /dev/null +++ b/library/HTMLPurifier/ErrorCollector.php @@ -0,0 +1,73 @@ +<?php + +require_once 'HTMLPurifier/Generator.php'; + +/** + * Error collection class that enables HTML Purifier to report HTML + * problems back to the user + */ +class HTMLPurifier_ErrorCollector +{ + + var $errors = array(); + + /** + * Sends an error message to the collector for later use + * @param string Error message text + * @param HTMLPurifier_Token Token that caused error + * @param array Tokens surrounding the offending token above, use true as placeholder + */ + function send($msg, $token, $context_tokens = array(true)) { + $this->errors[] = array($msg, $token, $context_tokens); + } + + /** + * Retrieves raw error data for custom formatter to use + * @param List of arrays in format of array(Error message text, + * token that caused error, tokens surrounding token) + */ + function getRaw() { + return $this->errors; + } + + /** + * Default HTML formatting implementation for error messages + * @param $config Configuration array, vital for HTML output nature + */ + function getHTMLFormatted($config) { + $generator = new HTMLPurifier_Generator(); + $context = new HTMLPurifier_Context(); + $generator->generateFromTokens(array(), $config, $context); // initialize + $ret = array(); + + $errors = $this->errors; + + // sort error array by line + if ($config->get('Core', 'MaintainLineNumbers')) { + $lines = array(); + foreach ($errors as $error) $lines[] = $error[1]->line; + array_multisort($lines, SORT_ASC, $errors); + } + + foreach ($errors as $error) { + $string = $generator->escape($error[0]); // message + if (!empty($error[1]->line)) { + $string .= ' at line ' . $error[1]->line; + } + $string .= ' (<code>'; + foreach ($error[2] as $token) { + if ($token !== true) { + $string .= $generator->escape($generator->generateFromToken($token)); + } else { + $string .= '<strong>' . $generator->escape($generator->generateFromToken($error[1])) . '</strong>'; + } + } + $string .= '</code>)'; + $ret[] = $string; + } + return $ret; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/Generator.php b/library/HTMLPurifier/Generator.php index b6a9aa24..63750e9c 100644 --- a/library/HTMLPurifier/Generator.php +++ b/library/HTMLPurifier/Generator.php @@ -1,59 +1,65 @@ <?php -require_once 'HTMLPurifier/Lexer.php'; - HTMLPurifier_ConfigSchema::define( - 'Core', 'CleanUTF8DuringGeneration', false, 'bool', - 'When true, HTMLPurifier_Generator will also check all strings it '. - 'escapes for UTF-8 well-formedness as a defense in depth measure. '. - 'This could cause a considerable performance impact, and is not '. - 'strictly necessary due to the fact that the Lexers should have '. - 'ensured that all the UTF-8 strings were well-formed. Note that '. - 'the configuration value is only read at the beginning of '. - 'generateFromTokens.' -); - -HTMLPurifier_ConfigSchema::define( - 'Core', 'XHTML', true, 'bool', - 'Determines whether or not output is XHTML or not. When disabled, HTML '. - 'Purifier goes into HTML 4.01 removes XHTML-specific markup constructs, '. - 'such as boolean attribute expansion and trailing slashes in empty tags. '. - 'This directive was available since 1.1.' + 'Output', 'CommentScriptContents', true, 'bool', + 'Determines whether or not HTML Purifier should attempt to fix up '. + 'the contents of script tags for legacy browsers with comments. This '. + 'directive was available since 1.7.' ); +HTMLPurifier_ConfigSchema::defineAlias('Core', 'CommentScriptContents', 'Output', 'CommentScriptContents'); // extension constraints could be factored into ConfigSchema HTMLPurifier_ConfigSchema::define( - 'Core', 'TidyFormat', false, 'bool', - '<p>Determines whether or not to run Tidy on the final output for pretty '. - 'formatting reasons, such as indentation and wrap.</p><p>This can greatly '. - 'improve readability for editors who are hand-editing the HTML, but is '. - 'by no means necessary as HTML Purifier has already fixed all major '. - 'errors the HTML may have had. Tidy is a non-default extension, and this directive '. - 'will silently fail if Tidy is not available.</p><p>If you are looking to make '. - 'the overall look of your page\'s source better, I recommend running Tidy '. - 'on the entire page rather than just user-content (after all, the '. - 'indentation relative to the containing blocks will be incorrect).</p><p>This '. - 'directive was available since 1.1.1.</p>' + 'Output', 'TidyFormat', false, 'bool', <<<HTML +<p> + Determines whether or not to run Tidy on the final output for pretty + formatting reasons, such as indentation and wrap. +</p> +<p> + This can greatly improve readability for editors who are hand-editing + the HTML, but is by no means necessary as HTML Purifier has already + fixed all major errors the HTML may have had. Tidy is a non-default + extension, and this directive will silently fail if Tidy is not + available. +</p> +<p> + If you are looking to make the overall look of your page's source + better, I recommend running Tidy on the entire page rather than just + user-content (after all, the indentation relative to the containing + blocks will be incorrect). +</p> +<p> + This directive was available since 1.1.1. +</p> +HTML ); +HTMLPurifier_ConfigSchema::defineAlias('Core', 'TidyFormat', 'Output', 'TidyFormat'); /** * Generates HTML from tokens. + * @todo Create a configuration-wide instance that all objects retrieve */ class HTMLPurifier_Generator { /** - * Bool cache of %Core.CleanUTF8DuringGeneration - * @private - */ - var $_clean_utf8 = false; - - /** - * Bool cache of %Core.XHTML + * Bool cache of %HTML.XHTML * @private */ var $_xhtml = true; + /** + * Bool cache of %Output.CommentScriptContents + * @private + */ + var $_scriptFix = false; + + /** + * Cache of HTMLDefinition + * @private + */ + var $_def; + /** * Generates HTML from an array of tokens. * @param $tokens Array of HTMLPurifier_Token @@ -63,13 +69,24 @@ class HTMLPurifier_Generator function generateFromTokens($tokens, $config, &$context) { $html = ''; if (!$config) $config = HTMLPurifier_Config::createDefault(); - $this->_clean_utf8 = $config->get('Core', 'CleanUTF8DuringGeneration'); - $this->_xhtml = $config->get('Core', 'XHTML'); + $this->_scriptFix = $config->get('Output', 'CommentScriptContents'); + + $this->_def = $config->getHTMLDefinition(); + $this->_xhtml = $this->_def->doctype->xml; + if (!$tokens) return ''; - foreach ($tokens as $token) { - $html .= $this->generateFromToken($token); + for ($i = 0, $size = count($tokens); $i < $size; $i++) { + if ($this->_scriptFix && $tokens[$i]->name === 'script') { + // script special case + $html .= $this->generateFromToken($tokens[$i++]); + $html .= $this->generateScriptFromToken($tokens[$i++]); + while ($tokens[$i]->name != 'script') { + $html .= $this->generateScriptFromToken($tokens[$i++]); + } + } + $html .= $this->generateFromToken($tokens[$i]); } - if ($config->get('Core', 'TidyFormat') && extension_loaded('tidy')) { + if ($config->get('Output', 'TidyFormat') && extension_loaded('tidy')) { $tidy_options = array( 'indent'=> true, @@ -104,14 +121,14 @@ class HTMLPurifier_Generator function generateFromToken($token) { if (!isset($token->type)) return ''; if ($token->type == 'start') { - $attr = $this->generateAttributes($token->attr); + $attr = $this->generateAttributes($token->attr, $token->name); return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>'; } elseif ($token->type == 'end') { return '</' . $token->name . '>'; } elseif ($token->type == 'empty') { - $attr = $this->generateAttributes($token->attr); + $attr = $this->generateAttributes($token->attr, $token->name); return '<' . $token->name . ($attr ? ' ' : '') . $attr . ( $this->_xhtml ? ' /': '' ) . '>'; @@ -125,18 +142,33 @@ class HTMLPurifier_Generator } } + /** + * Special case processor for the contents of script tags + * @warning This runs into problems if there's already a literal + * --> somewhere inside the script contents. + */ + function generateScriptFromToken($token) { + if (!$token->type == 'text') return $this->generateFromToken($token); + return '<!--' . PHP_EOL . $token->data . PHP_EOL . '// -->'; + // more advanced version: + // return '<!--//--><![CDATA[//><!--' . PHP_EOL . $token->data . PHP_EOL . '//--><!]]>'; + } + /** * Generates attribute declarations from attribute array. * @param $assoc_array_of_attributes Attribute array * @return Generate HTML fragment for insertion. */ - function generateAttributes($assoc_array_of_attributes) { + function generateAttributes($assoc_array_of_attributes, $element) { $html = ''; foreach ($assoc_array_of_attributes as $key => $value) { if (!$this->_xhtml) { // remove namespaced attributes if (strpos($key, ':') !== false) continue; - // also needed: check for attribute minimization + if (!empty($this->_def->info[$element]->attr[$key]->minimized)) { + $html .= $key . ' '; + continue; + } } $html .= $key.'="'.$this->escape($value).'" '; } @@ -149,7 +181,6 @@ class HTMLPurifier_Generator * @return String escaped data. */ function escape($string) { - if ($this->_clean_utf8) $string = HTMLPurifier_Lexer::cleanUTF8($string); return htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); } diff --git a/library/HTMLPurifier/HTMLDefinition.php b/library/HTMLPurifier/HTMLDefinition.php index c1dd6535..dc7a07b9 100644 --- a/library/HTMLPurifier/HTMLDefinition.php +++ b/library/HTMLPurifier/HTMLDefinition.php @@ -1,61 +1,132 @@ <?php -// components +require_once 'HTMLPurifier/Definition.php'; require_once 'HTMLPurifier/HTMLModuleManager.php'; // this definition and its modules MUST NOT define configuration directives // outside of the HTML or Attr namespaces -// will be superceded by more accurate doctype declaration schemes HTMLPurifier_ConfigSchema::define( - 'HTML', 'Strict', false, 'bool', - 'Determines whether or not to use Transitional (loose) or Strict rulesets. '. - 'This directive has been available since 1.3.0.' -); + 'HTML', 'DefinitionID', null, 'string/null', ' +<p> + Unique identifier for a custom-built HTML definition. If you edit + the raw version of the HTMLDefinition, introducing changes that the + configuration object does not reflect, you must specify this variable. + If you change your custom edits, you should change this directive, or + clear your cache. Example: +</p> +<pre> +$config = HTMLPurifier_Config::createDefault(); +$config->set(\'HTML\', \'DefinitionID\', \'1\'); +$def = $config->getHTMLDefinition(); +$def->addAttribute(\'a\', \'tabindex\', \'Number\'); +</pre> +<p> + In the above example, the configuration is still at the defaults, but + using the advanced API, an extra attribute has been added. The + configuration object normally has no way of knowing that this change + has taken place, so it needs an extra directive: %HTML.DefinitionID. + If someone else attempts to use the default configuration, these two + pieces of code will not clobber each other in the cache, since one has + an extra directive attached to it. +</p> +<p> + This directive has been available since 2.0.0, and in that version or + later you <em>must</em> specify a value to this directive to use the + advanced API features. +</p> +'); HTMLPurifier_ConfigSchema::define( - 'HTML', 'BlockWrapper', 'p', 'string', - 'String name of element to wrap inline elements that are inside a block '. - 'context. This only occurs in the children of blockquote in strict mode. '. - 'Example: by default value, <code>&lt;blockquote&gt;Foo&lt;/blockquote&gt;</code> '. - 'would become <code>&lt;blockquote&gt;&lt;p&gt;Foo&lt;/p&gt;&lt;/blockquote&gt;</code>. The '. - '<code>&lt;p&gt;</code> tags can be replaced '. - 'with whatever you desire, as long as it is a block level element. '. - 'This directive has been available since 1.3.0.' -); + 'HTML', 'DefinitionRev', 1, 'int', ' +<p> + Revision identifier for your custom definition specified in + %HTML.DefinitionID. This serves the same purpose: uniquely identifying + your custom definition, but this one does so in a chronological + context: revision 3 is more up-to-date then revision 2. Thus, when + this gets incremented, the cache handling is smart enough to clean + up any older revisions of your definition as well as flush the + cache. This directive has been available since 2.0.0. +</p> +'); HTMLPurifier_ConfigSchema::define( - 'HTML', 'Parent', 'div', 'string', - 'String name of element that HTML fragment passed to library will be '. - 'inserted in. An interesting variation would be using span as the '. - 'parent element, meaning that only inline tags would be allowed. '. - 'This directive has been available since 1.3.0.' -); + 'HTML', 'BlockWrapper', 'p', 'string', ' +<p> + String name of element to wrap inline elements that are inside a block + context. This only occurs in the children of blockquote in strict mode. +</p> +<p> + Example: by default value, + <code>&lt;blockquote&gt;Foo&lt;/blockquote&gt;</code> would become + <code>&lt;blockquote&gt;&lt;p&gt;Foo&lt;/p&gt;&lt;/blockquote&gt;</code>. + The <code>&lt;p&gt;</code> tags can be replaced with whatever you desire, + as long as it is a block level element. This directive has been available + since 1.3.0. +</p> +'); HTMLPurifier_ConfigSchema::define( - 'HTML', 'AllowedElements', null, 'lookup/null', - 'If HTML Purifier\'s tag set is unsatisfactory for your needs, you '. - 'can overload it with your own list of tags to allow. Note that this '. - 'method is subtractive: it does its job by taking away from HTML Purifier '. - 'usual feature set, so you cannot add a tag that HTML Purifier never '. - 'supported in the first place (like embed, form or head). If you change this, you '. - 'probably also want to change %HTML.AllowedAttributes. '. - '<strong>Warning:</strong> If another directive conflicts with the '. - 'elements here, <em>that</em> directive will win and override. '. - 'This directive has been available since 1.3.0.' -); + 'HTML', 'Parent', 'div', 'string', ' +<p> + String name of element that HTML fragment passed to library will be + inserted in. An interesting variation would be using span as the + parent element, meaning that only inline tags would be allowed. + This directive has been available since 1.3.0. +</p> +'); HTMLPurifier_ConfigSchema::define( - 'HTML', 'AllowedAttributes', null, 'lookup/null', - 'IF HTML Purifier\'s attribute set is unsatisfactory, overload it! '. - 'The syntax is \'tag.attr\' or \'*.attr\' for the global attributes '. - '(style, id, class, dir, lang, xml:lang).'. - '<strong>Warning:</strong> If another directive conflicts with the '. - 'elements here, <em>that</em> directive will win and override. For '. - 'example, %HTML.EnableAttrID will take precedence over *.id in this '. - 'directive. You must set that directive to true before you can use '. - 'IDs at all. This directive has been available since 1.3.0.' -); + 'HTML', 'AllowedElements', null, 'lookup/null', ' +<p> + If HTML Purifier\'s tag set is unsatisfactory for your needs, you + can overload it with your own list of tags to allow. Note that this + method is subtractive: it does its job by taking away from HTML Purifier + usual feature set, so you cannot add a tag that HTML Purifier never + supported in the first place (like embed, form or head). If you + change this, you probably also want to change %HTML.AllowedAttributes. +</p> +<p> + <strong>Warning:</strong> If another directive conflicts with the + elements here, <em>that</em> directive will win and override. + This directive has been available since 1.3.0. +</p> +'); + +HTMLPurifier_ConfigSchema::define( + 'HTML', 'AllowedAttributes', null, 'lookup/null', ' +<p> + If HTML Purifier\'s attribute set is unsatisfactory, overload it! + The syntax is "tag.attr" or "*.attr" for the global attributes + (style, id, class, dir, lang, xml:lang). +</p> +<p> + <strong>Warning:</strong> If another directive conflicts with the + elements here, <em>that</em> directive will win and override. For + example, %HTML.EnableAttrID will take precedence over *.id in this + directive. You must set that directive to true before you can use + IDs at all. This directive has been available since 1.3.0. +</p> +'); + +HTMLPurifier_ConfigSchema::define( + 'HTML', 'Allowed', null, 'string/null', ' +<p> + This is a convenience directive that rolls the functionality of + %HTML.AllowedElements and %HTML.AllowedAttributes into one directive. + Specify elements and attributes that are allowed using: + <code>element1[attr1|attr2],element2...</code>. +</p> +<p> + <strong>Warning</strong>: + All of the constraints on the component directives are still enforced. + The syntax is a <em>subset</em> of TinyMCE\'s <code>valid_elements</code> + whitelist: directly copy-pasting it here will probably result in + broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes + are set, this directive has no effect. + This directive has been available since 2.0.0. +</p> +'); /** * Definition of the purified HTML that describes allowed children, @@ -77,10 +148,10 @@ HTMLPurifier_ConfigSchema::define( * HTMLPurifier_Printer_HTMLDefinition is a notable exception to this * rule: in the interest of comprehensiveness, it will sniff everything. */ -class HTMLPurifier_HTMLDefinition +class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition { - /** FULLY-PUBLIC VARIABLES */ + // FULLY-PUBLIC VARIABLES --------------------------------------------- /** * Associative array of element names to HTMLPurifier_ElementDef @@ -139,50 +210,97 @@ class HTMLPurifier_HTMLDefinition */ var $info_content_sets = array(); + /** + * Doctype object + */ + var $doctype; - /** PUBLIC BUT INTERNAL VARIABLES */ - var $setup = false; /**< Has setup() been called yet? */ - var $config; /**< Temporary instance of HTMLPurifier_Config */ + // RAW CUSTOMIZATION STUFF -------------------------------------------- + /** + * Adds a custom attribute to a pre-existing element + * @param $element_name String element name to add attribute to + * @param $attr_name String name of attribute + * @param $def Attribute definition, can be string or object, see + * HTMLPurifier_AttrTypes for details + */ + function addAttribute($element_name, $attr_name, $def) { + $module =& $this->getAnonymousModule(); + $element =& $module->addBlankElement($element_name); + $element->attr[$attr_name] = $def; + } + + /** + * Adds a custom element to your HTML definition + * @note See HTMLPurifier_HTMLModule::addElement for detailed + * parameter descriptions. + */ + function addElement($element_name, $type, $contents, $attr_collections, $attributes) { + $module =& $this->getAnonymousModule(); + // assume that if the user is calling this, the element + // is safe. This may not be a good idea + $module->addElement($element_name, true, $type, $contents, $attr_collections, $attributes); + } + + /** + * Retrieves a reference to the anonymous module, so you can + * bust out advanced features without having to make your own + * module. + */ + function &getAnonymousModule() { + if (!$this->_anonModule) { + $this->_anonModule = new HTMLPurifier_HTMLModule(); + $this->_anonModule->name = 'Anonymous'; + } + return $this->_anonModule; + } + + var $_anonModule; + + + // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- + + var $type = 'HTML'; var $manager; /**< Instance of HTMLPurifier_HTMLModuleManager */ /** * Performs low-cost, preliminary initialization. - * @param $config Instance of HTMLPurifier_Config */ - function HTMLPurifier_HTMLDefinition(&$config) { - $this->config =& $config; + function HTMLPurifier_HTMLDefinition() { $this->manager = new HTMLPurifier_HTMLModuleManager(); } - /** - * Processes internals into form usable by HTMLPurifier internals. - * Modifying the definition after calling this function should not - * be done. - */ - function setup() { - - // multiple call guard - if ($this->setup) {return;} else {$this->setup = true;} - - $this->processModules(); - $this->setupConfigStuff(); - - unset($this->config); + 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 */ - function processModules() { + function processModules($config) { - $this->manager->setup($this->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); + } - foreach ($this->manager->activeModules as $module) { + $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; @@ -197,7 +315,7 @@ class HTMLPurifier_HTMLDefinition } } - $this->info = $this->manager->getElements($this->config); + $this->info = $this->manager->getElements(); $this->info_content_sets = $this->manager->contentSets->lookup; } @@ -205,9 +323,9 @@ class HTMLPurifier_HTMLDefinition /** * Sets up stuff based on config. We need a better way of doing this. */ - function setupConfigStuff() { + function setupConfigStuff($config) { - $block_wrapper = $this->config->get('HTML', 'BlockWrapper'); + $block_wrapper = $config->get('HTML', 'BlockWrapper'); if (isset($this->info_content_sets['Block'][$block_wrapper])) { $this->info_block_wrapper = $block_wrapper; } else { @@ -215,24 +333,33 @@ class HTMLPurifier_HTMLDefinition E_USER_ERROR); } - $parent = $this->config->get('HTML', 'Parent'); - $def = $this->manager->getElement($parent, $this->config); + $parent = $config->get('HTML', 'Parent'); + $def = $this->manager->getElement($parent, true); 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_parent, $this->config); + $this->info_parent_def = $this->manager->getElement($this->info_parent, true); } // support template text $support = "(for information on implementing this, see the ". "support forums) "; - // setup allowed elements, SubtractiveWhitelist module - $allowed_elements = $this->config->get('HTML', 'AllowedElements'); + // setup allowed elements + + $allowed_elements = $config->get('HTML', 'AllowedElements'); + $allowed_attributes = $config->get('HTML', 'AllowedAttributes'); + + 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]); @@ -240,11 +367,11 @@ class HTMLPurifier_HTMLDefinition } // emit errors foreach ($allowed_elements as $element => $d) { + $element = htmlspecialchars($element); trigger_error("Element '$element' is not supported $support", E_USER_WARNING); } } - $allowed_attributes = $this->config->get('HTML', 'AllowedAttributes'); $allowed_attributes_mutable = $allowed_attributes; // by copy! if (is_array($allowed_attributes)) { foreach ($this->info_global_attr as $attr_key => $info) { @@ -271,6 +398,8 @@ class HTMLPurifier_HTMLDefinition // emit errors foreach ($allowed_attributes_mutable as $elattr => $d) { list($element, $attribute) = explode('.', $elattr); + $element = htmlspecialchars($element); + $attribute = htmlspecialchars($attribute); if ($element == '*') { trigger_error("Global attribute '$attribute' is not ". "supported in any elements $support", @@ -284,6 +413,41 @@ class HTMLPurifier_HTMLDefinition } + /** + * 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 $list String list to parse + * @param array($allowed_elements, $allowed_attributes) + */ + function parseTinyMCEAllowedList($list) { + + $elements = array(); + $attributes = array(); + + $chunks = explode(',', $list); + foreach ($chunks as $chunk) { + // 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($attr, 0, strlen($attr) - 1); // remove trailing ] + $attr = explode('|', $attr); + foreach ($attr as $key) { + $attributes["$element.$key"] = true; + } + } + + return array($elements, $attributes); + + } + } diff --git a/library/HTMLPurifier/HTMLModule.php b/library/HTMLPurifier/HTMLModule.php index 930b605d..4560a184 100644 --- a/library/HTMLPurifier/HTMLModule.php +++ b/library/HTMLPurifier/HTMLModule.php @@ -16,16 +16,14 @@ class HTMLPurifier_HTMLModule { + + // -- Overloadable ---------------------------------------------------- + /** * Short unique string identifier of the module */ var $name; - /** - * Dynamically set integer that specifies when the module was loaded in. - */ - var $order; - /** * Informally, a list of elements this module changes. Not used in * any significant way. @@ -99,27 +97,128 @@ class HTMLPurifier_HTMLModule */ function getChildDef($def) {return false;} - /** - * Hook method that lets module perform arbitrary operations on - * HTMLPurifier_HTMLDefinition before the module gets processed. - * @param $definition Reference to HTMLDefinition being setup - */ - function preProcess(&$definition) {} + // -- Convenience ----------------------------------------------------- /** - * Hook method that lets module perform arbitrary operations - * on HTMLPurifier_HTMLDefinition after the module gets processed. - * @param $definition Reference to HTMLDefinition being setup + * Convenience function that sets up a new element + * @param $element Name of element to add + * @param $safe Is element safe for untrusted users to use? + * @param $type What content set should element be registered to? + * Set as false to skip this step. + * @param $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @param $attr_includes What attribute collections to register to + * element? + * @param $attr What unique attributes does the element define? + * @note See ElementDef for in-depth descriptions of these parameters. + * @return Reference to created element definition object, so you + * can set advanced parameters + * @protected */ - function postProcess(&$definition) {} + function &addElement($element, $safe, $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( + $safe, $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]; + } /** - * Hook method that is called when a module gets registered to - * the definition. - * @param $definition Reference to HTMLDefinition being setup + * Convenience function that creates a totally blank, non-standalone + * element. + * @param $element Name of element to create + * @return Reference to created element */ - function setup(&$definition) {} + 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 Element to register + * @param Name content set (warning: case sensitive, usually upper-case + * first letter) + * @protected + */ + 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 $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @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. + */ + function parseContents($contents) { + if (!is_string($contents)) return array(null, null); // 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 $attr Reference to attr array to modify + * @param $attr_includes Array of includes / string include to merge in + */ + 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 $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 Lookup array equivalent of list + */ + 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; + } } ?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/Bdo.php b/library/HTMLPurifier/HTMLModule/Bdo.php index 6feae005..84b9a0b0 100644 --- a/library/HTMLPurifier/HTMLModule/Bdo.php +++ b/library/HTMLPurifier/HTMLModule/Bdo.php @@ -11,30 +11,22 @@ class HTMLPurifier_HTMLModule_Bdo extends HTMLPurifier_HTMLModule { var $name = 'Bdo'; - var $elements = array('bdo'); - var $content_sets = array('Inline' => 'bdo'); var $attr_collections = array( 'I18N' => array('dir' => false) ); function HTMLPurifier_HTMLModule_Bdo() { - $dir = new HTMLPurifier_AttrDef_Enum(array('ltr','rtl'), false); - $this->attr_collections['I18N']['dir'] = $dir; - $this->info['bdo'] = new HTMLPurifier_ElementDef(); - $this->info['bdo']->attr = array( - 0 => array('Core', 'Lang'), - 'dir' => $dir, // required - // The Abstract Module specification has the attribute - // inclusions wrong for bdo: bdo allows - // xml:lang too (and we'll toss in lang for good measure, - // though it is not allowed for XHTML 1.1, this will - // be managed with a global attribute transform) + $bdo =& $this->addElement( + 'bdo', true, '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 + ) ); - $this->info['bdo']->content_model = '#PCDATA | Inline'; - $this->info['bdo']->content_model_type = 'optional'; - // provides fallback behavior if dir's missing (dir is required) - $this->info['bdo']->attr_transform_post['required-dir'] = - new HTMLPurifier_AttrTransform_BdoDir(); + $bdo->attr_transform_post['required-dir'] = new HTMLPurifier_AttrTransform_BdoDir(); + + $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl'; } } diff --git a/library/HTMLPurifier/HTMLModule/CommonAttributes.php b/library/HTMLPurifier/HTMLModule/CommonAttributes.php index 8f17c2f0..34b991f6 100644 --- a/library/HTMLPurifier/HTMLModule/CommonAttributes.php +++ b/library/HTMLPurifier/HTMLModule/CommonAttributes.php @@ -1,5 +1,7 @@ <?php +require_once 'HTMLPurifier/HTMLModule.php'; + class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule { var $name = 'CommonAttributes'; @@ -12,9 +14,7 @@ class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule 'id' => 'ID', 'title' => 'CDATA', ), - 'Lang' => array( - 'xml:lang' => false, // see constructor - ), + 'Lang' => array(), 'I18N' => array( 0 => array('Lang'), // proprietary, for xml:lang/lang ), @@ -22,10 +22,6 @@ class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule 0 => array('Core', 'I18N') ) ); - - function HTMLPurifier_HTMLModule_CommonAttributes() { - $this->attr_collections['Lang']['xml:lang'] = new HTMLPurifier_AttrDef_Lang(); - } } ?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/Edit.php b/library/HTMLPurifier/HTMLModule/Edit.php index c3dc0197..1d14ce8d 100644 --- a/library/HTMLPurifier/HTMLModule/Edit.php +++ b/library/HTMLPurifier/HTMLModule/Edit.php @@ -11,28 +11,24 @@ class HTMLPurifier_HTMLModule_Edit extends HTMLPurifier_HTMLModule { var $name = 'Edit'; - var $elements = array('del', 'ins'); - var $content_sets = array('Inline' => 'del | ins'); function HTMLPurifier_HTMLModule_Edit() { - foreach ($this->elements as $element) { - $this->info[$element] = new HTMLPurifier_ElementDef(); - $this->info[$element]->attr = array( - 0 => array('Common'), - 'cite' => 'URI', - // 'datetime' => 'Datetime' // Datetime not implemented - ); - // Inline context ! Block context (exclamation mark is - // separator, see getChildDef for parsing) - $this->info[$element]->content_model = - '#PCDATA | Inline ! #PCDATA | Flow'; - // 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 - $this->info[$element]->content_model_type = 'chameleon'; - } + $contents = 'Chameleon: #PCDATA | Inline ! #PCDATA | Flow'; + $attr = array( + 'cite' => 'URI', + // 'datetime' => 'Datetime', // not implemented + ); + $this->addElement('del', true, 'Inline', $contents, 'Common', $attr); + $this->addElement('ins', true, '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) + var $defines_child_def = true; function getChildDef($def) { if ($def->content_model_type != 'chameleon') return false; diff --git a/library/HTMLPurifier/HTMLModule/Hypertext.php b/library/HTMLPurifier/HTMLModule/Hypertext.php index baa20fd1..a9a9d352 100644 --- a/library/HTMLPurifier/HTMLModule/Hypertext.php +++ b/library/HTMLPurifier/HTMLModule/Hypertext.php @@ -10,25 +10,22 @@ class HTMLPurifier_HTMLModule_Hypertext extends HTMLPurifier_HTMLModule { var $name = 'Hypertext'; - var $elements = array('a'); - var $content_sets = array('Inline' => 'a'); function HTMLPurifier_HTMLModule_Hypertext() { - $this->info['a'] = new HTMLPurifier_ElementDef(); - $this->info['a']->attr = array( - 0 => array('Common'), - // '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 =& $this->addElement( + 'a', true, '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', + ) ); - $this->info['a']->content_model = '#PCDATA | Inline'; - $this->info['a']->content_model_type = 'optional'; - $this->info['a']->excludes = array('a' => true); + $a->excludes = array('a' => true); } } diff --git a/library/HTMLPurifier/HTMLModule/Image.php b/library/HTMLPurifier/HTMLModule/Image.php index bf234b13..6b1574c3 100644 --- a/library/HTMLPurifier/HTMLModule/Image.php +++ b/library/HTMLPurifier/HTMLModule/Image.php @@ -14,21 +14,21 @@ class HTMLPurifier_HTMLModule_Image extends HTMLPurifier_HTMLModule { var $name = 'Image'; - var $elements = array('img'); - var $content_sets = array('Inline' => 'img'); function HTMLPurifier_HTMLModule_Image() { - $this->info['img'] = new HTMLPurifier_ElementDef(); - $this->info['img']->attr = array( - 0 => array('Common'), - 'alt' => 'Text', - 'height' => 'Length', - 'longdesc' => 'URI', - 'src' => new HTMLPurifier_AttrDef_URI(true), // embedded - 'width' => 'Length' + $img =& $this->addElement( + 'img', true, 'Inline', 'Empty', 'Common', + array( + 'alt*' => 'Text', + 'height' => 'Length', + 'longdesc' => 'URI', + 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded + 'width' => 'Length' + ) ); - $this->info['img']->content_model_type = 'empty'; - $this->info['img']->attr_transform_post[] = + // kind of strange, but splitting things up would be inefficient + $img->attr_transform_pre[] = + $img->attr_transform_post[] = new HTMLPurifier_AttrTransform_ImgRequired(); } diff --git a/library/HTMLPurifier/HTMLModule/Legacy.php b/library/HTMLPurifier/HTMLModule/Legacy.php index a0613a2f..5ec1f66b 100644 --- a/library/HTMLPurifier/HTMLModule/Legacy.php +++ b/library/HTMLPurifier/HTMLModule/Legacy.php @@ -1,5 +1,7 @@ <?php +require_once 'HTMLPurifier/AttrDef/HTML/Bool.php'; + /** * XHTML 1.1 Legacy module defines elements that were previously * deprecated. @@ -22,36 +24,115 @@ class HTMLPurifier_HTMLModule_Legacy extends HTMLPurifier_HTMLModule // incomplete var $name = 'Legacy'; - var $elements = array('u', 's', 'strike'); - var $non_standalone_elements = array('li', 'ol', 'address', 'blockquote'); function HTMLPurifier_HTMLModule_Legacy() { - // setup new elements - foreach ($this->elements as $name) { - $this->info[$name] = new HTMLPurifier_ElementDef(); - // for u, s, strike, as more elements get added, add - // conditionals as necessary - $this->info[$name]->content_model = 'Inline | #PCDATA'; - $this->info[$name]->content_model_type = 'optional'; - $this->info[$name]->attr[0] = array('Common'); - } + + $this->addElement('basefont', true, 'Inline', 'Empty', false, array( + 'color' => 'Color', + 'face' => 'Text', // extremely broad, we should + 'size' => 'Text', // tighten it + 'id' => 'ID' + )); + $this->addElement('center', true, 'Block', 'Flow', 'Common'); + $this->addElement('dir', true, 'Block', 'Required: li', 'Common', array( + 'compact' => 'Bool#compact' + )); + $this->addElement('font', true, 'Inline', 'Inline', array('Core', 'I18N'), array( + 'color' => 'Color', + 'face' => 'Text', // extremely broad, we should + 'size' => 'Text', // tighten it + )); + $this->addElement('menu', true, 'Block', 'Required: li', 'Common', array( + 'compact' => 'Bool#compact' + )); + $this->addElement('s', true, 'Inline', 'Inline', 'Common'); + $this->addElement('strike', true, 'Inline', 'Inline', 'Common'); + $this->addElement('u', true, 'Inline', 'Inline', 'Common'); // setup modifications to old elements - foreach ($this->non_standalone_elements as $name) { - $this->info[$name] = new HTMLPurifier_ElementDef(); - $this->info[$name]->standalone = false; + + $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; } - $this->info['li']->attr['value'] = new HTMLPurifier_AttrDef_Integer(); - $this->info['ol']->attr['start'] = new HTMLPurifier_AttrDef_Integer(); + $hr =& $this->addBlankElement('hr'); + $hr->attr['align'] = $align; + $hr->attr['noshade'] = 'Bool#noshade'; + $hr->attr['size'] = 'Pixels'; + $hr->attr['width'] = 'Length'; - $this->info['address']->content_model = 'Inline | #PCDATA | p'; - $this->info['address']->content_model_type = 'optional'; - $this->info['address']->child = false; + $img =& $this->addBlankElement('img'); + $img->attr['align'] = 'Enum#top,middle,bottom,left,right'; + $img->attr['border'] = 'Pixels'; + $img->attr['hspace'] = 'Pixels'; + $img->attr['vspace'] = 'Pixels'; - $this->info['blockquote']->content_model = 'Flow | #PCDATA'; - $this->info['blockquote']->content_model_type = 'optional'; - $this->info['blockquote']->child = false; + // 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'; } diff --git a/library/HTMLPurifier/HTMLModule/List.php b/library/HTMLPurifier/HTMLModule/List.php index f9f2c4e2..894ac5aa 100644 --- a/library/HTMLPurifier/HTMLModule/List.php +++ b/library/HTMLPurifier/HTMLModule/List.php @@ -9,7 +9,6 @@ class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule { var $name = 'List'; - var $elements = array('dl', 'dt', 'dd', 'ol', 'ul', 'li'); // 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 @@ -19,26 +18,19 @@ class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule // 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. - var $content_sets = array('List' => 'dl | ol | ul', 'Flow' => 'List'); + + var $content_sets = array('Flow' => 'List'); function HTMLPurifier_HTMLModule_List() { - foreach ($this->elements as $element) { - $this->info[$element] = new HTMLPurifier_ElementDef(); - $this->info[$element]->attr = array(0 => array('Common')); - if ($element == 'li' || $element == 'dd') { - $this->info[$element]->content_model = '#PCDATA | Flow'; - $this->info[$element]->content_model_type = 'optional'; - } elseif ($element == 'ol' || $element == 'ul') { - $this->info[$element]->content_model = 'li'; - $this->info[$element]->content_model_type = 'required'; - } - } - $this->info['dt']->content_model = '#PCDATA | Inline'; - $this->info['dt']->content_model_type = 'optional'; - $this->info['dl']->content_model = 'dt | dd'; - $this->info['dl']->content_model_type = 'required'; - // this could be a LOT more robust - $this->info['li']->auto_close = array('li' => true); + $this->addElement('ol', true, 'List', 'Required: li', 'Common'); + $this->addElement('ul', true, 'List', 'Required: li', 'Common'); + $this->addElement('dl', true, 'List', 'Required: dt | dd', 'Common'); + + $li =& $this->addElement('li', true, false, 'Flow', 'Common'); + $li->auto_close = array('li' => true); + + $this->addElement('dd', true, false, 'Flow', 'Common'); + $this->addElement('dt', true, false, 'Inline', 'Common'); } } diff --git a/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php new file mode 100644 index 00000000..a0fed7e6 --- /dev/null +++ b/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php @@ -0,0 +1,16 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule.php'; + +class HTMLPurifier_HTMLModule_NonXMLCommonAttributes extends HTMLPurifier_HTMLModule +{ + var $name = 'NonXMLCommonAttributes'; + + var $attr_collections = array( + 'Lang' => array( + 'lang' => 'LanguageCode', + ) + ); +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/Presentation.php b/library/HTMLPurifier/HTMLModule/Presentation.php index 5c80db40..dbcaba27 100644 --- a/library/HTMLPurifier/HTMLModule/Presentation.php +++ b/library/HTMLPurifier/HTMLModule/Presentation.php @@ -16,23 +16,16 @@ class HTMLPurifier_HTMLModule_Presentation extends HTMLPurifier_HTMLModule { var $name = 'Presentation'; - var $elements = array('b', 'big', 'hr', 'i', 'small', 'sub', 'sup', 'tt'); - var $content_sets = array( - 'Block' => 'hr', - 'Inline' => 'b | big | i | small | sub | sup | tt' - ); function HTMLPurifier_HTMLModule_Presentation() { - foreach ($this->elements as $element) { - $this->info[$element] = new HTMLPurifier_ElementDef(); - $this->info[$element]->attr = array(0 => array('Common')); - if ($element == 'hr') { - $this->info[$element]->content_model_type = 'empty'; - } else { - $this->info[$element]->content_model = '#PCDATA | Inline'; - $this->info[$element]->content_model_type = 'optional'; - } - } + $this->addElement('b', true, 'Inline', 'Inline', 'Common'); + $this->addElement('big', true, 'Inline', 'Inline', 'Common'); + $this->addElement('hr', true, 'Block', 'Empty', 'Common'); + $this->addElement('i', true, 'Inline', 'Inline', 'Common'); + $this->addElement('small', true, 'Inline', 'Inline', 'Common'); + $this->addElement('sub', true, 'Inline', 'Inline', 'Common'); + $this->addElement('sup', true, 'Inline', 'Inline', 'Common'); + $this->addElement('tt', true, 'Inline', 'Inline', 'Common'); } } diff --git a/library/HTMLPurifier/HTMLModule/Scripting.php b/library/HTMLPurifier/HTMLModule/Scripting.php index e3ef802b..56eff967 100644 --- a/library/HTMLPurifier/HTMLModule/Scripting.php +++ b/library/HTMLPurifier/HTMLModule/Scripting.php @@ -46,8 +46,12 @@ class HTMLPurifier_HTMLModule_Scripting extends HTMLPurifier_HTMLModule // 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 foreach ($this->elements as $element) { $this->info[$element] = new HTMLPurifier_ElementDef(); + $this->info[$element]->safe = false; } $this->info['noscript']->attr = array( 0 => array('Common') ); $this->info['noscript']->content_model = 'Heading | List | Block'; diff --git a/library/HTMLPurifier/HTMLModule/Tables.php b/library/HTMLPurifier/HTMLModule/Tables.php index 003ff624..043c8101 100644 --- a/library/HTMLPurifier/HTMLModule/Tables.php +++ b/library/HTMLPurifier/HTMLModule/Tables.php @@ -10,75 +10,60 @@ class HTMLPurifier_HTMLModule_Tables extends HTMLPurifier_HTMLModule { var $name = 'Tables'; - var $elements = array('caption', 'table', 'td', 'th', 'tr', 'col', - 'colgroup', 'tbody', 'thead', 'tfoot'); - var $content_sets = array('Block' => 'table'); function HTMLPurifier_HTMLModule_Tables() { - foreach ($this->elements as $e) { - $this->info[$e] = new HTMLPurifier_ElementDef(); - $this->info[$e]->attr = array(0 => array('Common')); - $attr =& $this->info[$e]->attr; - if ($e == 'caption') continue; - if ($e == 'table'){ - $attr['border'] = 'Pixels'; - $attr['cellpadding'] = 'Length'; - $attr['cellspacing'] = 'Length'; - $attr['frame'] = new HTMLPurifier_AttrDef_Enum(array( - 'void', 'above', 'below', 'hsides', 'lhs', 'rhs', - 'vsides', 'box', 'border' - ), false); - $attr['rules'] = new HTMLPurifier_AttrDef_Enum(array( - 'none', 'groups', 'rows', 'cols', 'all' - ), false); - $attr['summary'] = 'Text'; - $attr['width'] = 'Length'; - continue; - } - if ($e == 'col' || $e == 'colgroup') { - $attr['span'] = 'Number'; - $attr['width'] = 'MultiLength'; - } - if ($e == 'td' || $e == 'th') { - $attr['abbr'] = 'Text'; - $attr['colspan'] = 'Number'; - $attr['rowspan'] = 'Number'; - } - $attr['align'] = new HTMLPurifier_AttrDef_Enum(array( - 'left', 'center', 'right', 'justify', 'char' - ), false); - $attr['valign'] = new HTMLPurifier_AttrDef_Enum(array( - 'top', 'middle', 'bottom', 'baseline' - ), false); - $attr['charoff'] = 'Length'; - } - $this->info['caption']->content_model = '#PCDATA | Inline'; - $this->info['caption']->content_model_type = 'optional'; - // Is done directly because it doesn't leverage substitution - // mechanisms. True model is: - // 'caption?, ( col* | colgroup* ), (( thead?, tfoot?, tbody+ ) | ( tr+ ))' - $this->info['table']->child = new HTMLPurifier_ChildDef_Table(); + $this->addElement('caption', true, false, 'Inline', 'Common'); - $this->info['td']->content_model = - $this->info['th']->content_model = '#PCDATA | Flow'; - $this->info['td']->content_model_type = - $this->info['th']->content_model_type = 'optional'; + $this->addElement('table', true, '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' + ) + ); - $this->info['tr']->content_model = 'td | th'; - $this->info['tr']->content_model_type = 'required'; + // common attributes + $cell_align = array( + 'align' => 'Enum#left,center,right,justify,char', + 'charoff' => 'Length', + 'valign' => 'Enum#top,middle,bottom,baseline', + ); - $this->info['col']->content_model_type = 'empty'; + $cell_t = array_merge( + array( + 'abbr' => 'Text', + 'colspan' => 'Number', + 'rowspan' => 'Number', + ), + $cell_align + ); + $this->addElement('td', true, false, 'Flow', 'Common', $cell_t); + $this->addElement('th', true, false, 'Flow', 'Common', $cell_t); - $this->info['colgroup']->content_model = 'col'; - $this->info['colgroup']->content_model_type = 'optional'; + $this->addElement('tr', true, false, 'Required: td | th', 'Common', $cell_align); - $this->info['tbody']->content_model = - $this->info['thead']->content_model = - $this->info['tfoot']->content_model = 'tr'; - $this->info['tbody']->content_model_type = - $this->info['thead']->content_model_type = - $this->info['tfoot']->content_model_type = 'required'; + $cell_col = array_merge( + array( + 'span' => 'Number', + 'width' => 'MultiLength', + ), + $cell_align + ); + $this->addElement('col', true, false, 'Empty', 'Common', $cell_col); + $colgroup =& $this->addElement('colgroup', true, false, 'Optional: col', 'Common', $cell_col); + $colgroup->auto_close = $this->makeLookup( + 'thead', 'tbody', 'tfoot', 'tr' + ); + + $this->addElement('tbody', true, false, 'Required: tr', 'Common', $cell_align); + $this->addElement('thead', true, false, 'Required: tr', 'Common', $cell_align); + $this->addElement('tfoot', true, false, 'Required: tr', 'Common', $cell_align); } diff --git a/library/HTMLPurifier/HTMLModule/Target.php b/library/HTMLPurifier/HTMLModule/Target.php index 1c2104ba..6cb8e510 100644 --- a/library/HTMLPurifier/HTMLModule/Target.php +++ b/library/HTMLPurifier/HTMLModule/Target.php @@ -9,13 +9,12 @@ class HTMLPurifier_HTMLModule_Target extends HTMLPurifier_HTMLModule { var $name = 'Target'; - var $elements = array('a'); function HTMLPurifier_HTMLModule_Target() { - foreach ($this->elements as $e) { - $this->info[$e] = new HTMLPurifier_ElementDef(); - $this->info[$e]->standalone = false; - $this->info[$e]->attr = array( + $elements = array('a'); + foreach ($elements as $name) { + $e =& $this->addBlankElement($name); + $e->attr = array( 'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget() ); } diff --git a/library/HTMLPurifier/HTMLModule/Text.php b/library/HTMLPurifier/HTMLModule/Text.php index 64b6e110..c8abe1d7 100644 --- a/library/HTMLPurifier/HTMLModule/Text.php +++ b/library/HTMLPurifier/HTMLModule/Text.php @@ -10,65 +10,60 @@ require_once 'HTMLPurifier/HTMLModule.php'; * - Block Structural (div, p) * - Inline Phrasal (abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var) * - Inline Structural (br, span) - * We have elected not to follow suite, but this may change. + * 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 { var $name = 'Text'; - - var $elements = array('abbr', 'acronym', 'address', 'blockquote', - 'br', 'cite', 'code', 'dfn', 'div', 'em', 'h1', 'h2', 'h3', - 'h4', 'h5', 'h6', 'kbd', 'p', 'pre', 'q', 'samp', 'span', 'strong', - 'var'); - var $content_sets = array( - 'Heading' => 'h1 | h2 | h3 | h4 | h5 | h6', - 'Block' => 'address | blockquote | div | p | pre', - 'Inline' => 'abbr | acronym | br | cite | code | dfn | em | kbd | q | samp | span | strong | var', 'Flow' => 'Heading | Block | Inline' ); function HTMLPurifier_HTMLModule_Text() { - foreach ($this->elements as $element) { - $this->info[$element] = new HTMLPurifier_ElementDef(); - // attributes - if ($element == 'br') { - $this->info[$element]->attr = array(0 => array('Core')); - } elseif ($element == 'blockquote' || $element == 'q') { - $this->info[$element]->attr = array(0 => array('Common'), 'cite' => 'URI'); - } else { - $this->info[$element]->attr = array(0 => array('Common')); - } - // content models - if ($element == 'br') { - $this->info[$element]->content_model_type = 'empty'; - } elseif ($element == 'blockquote') { - $this->info[$element]->content_model = 'Heading | Block | List'; - $this->info[$element]->content_model_type = 'optional'; - } elseif ($element == 'div') { - $this->info[$element]->content_model = '#PCDATA | Flow'; - $this->info[$element]->content_model_type = 'optional'; - } else { - $this->info[$element]->content_model = '#PCDATA | Inline'; - $this->info[$element]->content_model_type = 'optional'; - } - } - // 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. Note that the Abstract Module - // is blithely unaware of such distinctions. - $this->info['pre']->excludes = array_flip(array( - 'img', 'big', 'small', - 'object', 'applet', 'font', 'basefont' // generally not allowed - )); - $this->info['p']->auto_close = array_flip(array( + + // Inline Phrasal ------------------------------------------------- + $this->addElement('abbr', true, 'Inline', 'Inline', 'Common'); + $this->addElement('acronym', true, 'Inline', 'Inline', 'Common'); + $this->addElement('cite', true, 'Inline', 'Inline', 'Common'); + $this->addElement('code', true, 'Inline', 'Inline', 'Common'); + $this->addElement('dfn', true, 'Inline', 'Inline', 'Common'); + $this->addElement('em', true, 'Inline', 'Inline', 'Common'); + $this->addElement('kbd', true, 'Inline', 'Inline', 'Common'); + $this->addElement('q', true, 'Inline', 'Inline', 'Common', array('cite' => 'URI')); + $this->addElement('samp', true, 'Inline', 'Inline', 'Common'); + $this->addElement('strong', true, 'Inline', 'Inline', 'Common'); + $this->addElement('var', true, 'Inline', 'Inline', 'Common'); + + // Inline Structural ---------------------------------------------- + $this->addElement('span', true, 'Inline', 'Inline', 'Common'); + $this->addElement('br', true, 'Inline', 'Empty', 'Core'); + + // Block Phrasal -------------------------------------------------- + $this->addElement('address', true, 'Block', 'Inline', 'Common'); + $this->addElement('blockquote', true, 'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI') ); + $pre =& $this->addElement('pre', true, 'Block', 'Inline', 'Common'); + $pre->excludes = $this->makeLookup( + 'img', 'big', 'small', 'object', 'applet', 'font', 'basefont' ); + $this->addElement('h1', true, 'Heading', 'Inline', 'Common'); + $this->addElement('h2', true, 'Heading', 'Inline', 'Common'); + $this->addElement('h3', true, 'Heading', 'Inline', 'Common'); + $this->addElement('h4', true, 'Heading', 'Inline', 'Common'); + $this->addElement('h5', true, 'Heading', 'Inline', 'Common'); + $this->addElement('h6', true, 'Heading', 'Inline', 'Common'); + + // Block Structural ----------------------------------------------- + $p =& $this->addElement('p', true, 'Block', 'Inline', 'Common'); + // this seems really ad hoc: implementing some general + // heuristics would probably be better + $p->auto_close = $this->makeLookup( 'address', 'blockquote', 'dd', 'dir', 'div', 'dl', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'ol', 'p', 'pre', - 'table', 'ul' - )); + 'table', 'ul' ); + $this->addElement('div', true, 'Block', 'Flow', 'Common'); + } } diff --git a/library/HTMLPurifier/HTMLModule/Tidy.php b/library/HTMLPurifier/HTMLModule/Tidy.php new file mode 100644 index 00000000..b81bf3ad --- /dev/null +++ b/library/HTMLPurifier/HTMLModule/Tidy.php @@ -0,0 +1,241 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule.php'; + +HTMLPurifier_ConfigSchema::define( + 'HTML', 'TidyLevel', 'medium', 'string', ' +<p>General level of cleanliness the Tidy module should enforce. +There are four allowed values:</p> +<dl> + <dt>none</dt> + <dd>No extra tidying should be done</dd> + <dt>light</dt> + <dd>Only fix elements that would be discarded otherwise due to + lack of support in doctype</dd> + <dt>medium</dt> + <dd>Enforce best practices</dd> + <dt>heavy</dt> + <dd>Transform all deprecated elements and attributes to standards + compliant equivalents</dd> +</dl> +<p>This directive has been available since 2.0.0</p> +' ); +HTMLPurifier_ConfigSchema::defineAllowedValues( + 'HTML', 'TidyLevel', array('none', 'light', 'medium', 'heavy') +); + +HTMLPurifier_ConfigSchema::define( + 'HTML', 'TidyAdd', array(), 'lookup', ' +Fixes to add to the default set of Tidy fixes as per your level. This +directive has been available since 2.0.0. +' ); + +HTMLPurifier_ConfigSchema::define( + 'HTML', 'TidyRemove', array(), 'lookup', ' +Fixes to remove from the default set of Tidy fixes as per your level. This +directive has been available since 2.0.0. +' ); + +/** + * Abstract class for a set of proprietary modules that clean up (tidy) + * poorly written HTML. + */ +class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule +{ + + /** + * List of supported levels. Index zero is a special case "no fixes" + * level. + */ + var $levels = array(0 => 'none', 'light', 'medium', 'heavy'); + + /** + * Default level to place all fixes in. Disabled by default + */ + var $defaultLevel = null; + + /** + * Lists of fixes used by getFixesForLevel(). Format is: + * HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2'); + */ + var $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. + * @todo Wildcard matching and error reporting when an added or + * subtracted fix has no effect. + */ + function construct($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 $level String level identifier, see $levels for valid values + * @return Lookup up table of fixes + */ + 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. + */ + 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 $lookup Lookup table of fixes to activate + */ + 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; + } + $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 + */ + 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. + * @abstract + */ + function makeFixes() {} + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/Tidy/Proprietary.php b/library/HTMLPurifier/HTMLModule/Tidy/Proprietary.php new file mode 100644 index 00000000..624b066d --- /dev/null +++ b/library/HTMLPurifier/HTMLModule/Tidy/Proprietary.php @@ -0,0 +1,18 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule/Tidy.php'; + +class HTMLPurifier_HTMLModule_Tidy_Proprietary extends + HTMLPurifier_HTMLModule_Tidy +{ + + var $name = 'Tidy_Proprietary'; + var $defaultLevel = 'light'; + + function makeFixes() { + return array(); + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/Tidy/XHTML.php b/library/HTMLPurifier/HTMLModule/Tidy/XHTML.php new file mode 100644 index 00000000..78ddef47 --- /dev/null +++ b/library/HTMLPurifier/HTMLModule/Tidy/XHTML.php @@ -0,0 +1,21 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule/Tidy.php'; +require_once 'HTMLPurifier/AttrTransform/Lang.php'; + +class HTMLPurifier_HTMLModule_Tidy_XHTML extends + HTMLPurifier_HTMLModule_Tidy +{ + + var $name = 'Tidy_XHTML'; + var $defaultLevel = 'medium'; + + function makeFixes() { + $r = array(); + $r['@lang'] = new HTMLPurifier_AttrTransform_Lang(); + return $r; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php b/library/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php new file mode 100644 index 00000000..4d70cafc --- /dev/null +++ b/library/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php @@ -0,0 +1,193 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule/Tidy.php'; + +require_once 'HTMLPurifier/TagTransform/Simple.php'; +require_once 'HTMLPurifier/TagTransform/Font.php'; + +require_once 'HTMLPurifier/AttrTransform/BgColor.php'; +require_once 'HTMLPurifier/AttrTransform/BoolToCSS.php'; +require_once 'HTMLPurifier/AttrTransform/Border.php'; +require_once 'HTMLPurifier/AttrTransform/Name.php'; +require_once 'HTMLPurifier/AttrTransform/Length.php'; +require_once 'HTMLPurifier/AttrTransform/ImgSpace.php'; +require_once 'HTMLPurifier/AttrTransform/EnumToCSS.php'; + +class HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends + HTMLPurifier_HTMLModule_Tidy +{ + + 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'); + + // @name for img, a ----------------------------------------------- + $r['img@name'] = + $r['a@name'] = new HTMLPurifier_AttrTransform_Name(); + + // @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_types, true); + $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true); + + // @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_Transitional extends + HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 +{ + var $name = 'Tidy_Transitional'; + var $defaultLevel = 'heavy'; +} + +class HTMLPurifier_HTMLModule_Tidy_Strict extends + HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 +{ + var $name = 'Tidy_Strict'; + var $defaultLevel = 'light'; +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/Tidy/XHTMLStrict.php b/library/HTMLPurifier/HTMLModule/Tidy/XHTMLStrict.php new file mode 100644 index 00000000..b985870a --- /dev/null +++ b/library/HTMLPurifier/HTMLModule/Tidy/XHTMLStrict.php @@ -0,0 +1,27 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule/Tidy.php'; +require_once 'HTMLPurifier/ChildDef/StrictBlockquote.php'; + +class HTMLPurifier_HTMLModule_Tidy_XHTMLStrict extends + HTMLPurifier_HTMLModule_Tidy +{ + + var $name = 'Tidy_XHTMLStrict'; + var $defaultLevel = 'light'; + + function makeFixes() { + $r = array(); + $r['blockquote#content_model_type'] = 'strictblockquote'; + return $r; + } + + var $defines_child_def = true; + function getChildDef($def) { + if ($def->content_model_type != 'strictblockquote') return false; + return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model); + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/TransformToStrict.php b/library/HTMLPurifier/HTMLModule/TransformToStrict.php deleted file mode 100644 index 0b6c8370..00000000 --- a/library/HTMLPurifier/HTMLModule/TransformToStrict.php +++ /dev/null @@ -1,200 +0,0 @@ -<?php - -require_once 'HTMLPurifier/ChildDef/StrictBlockquote.php'; - -require_once 'HTMLPurifier/TagTransform/Simple.php'; -require_once 'HTMLPurifier/TagTransform/Center.php'; -require_once 'HTMLPurifier/TagTransform/Font.php'; - -require_once 'HTMLPurifier/AttrTransform/Lang.php'; -require_once 'HTMLPurifier/AttrTransform/BgColor.php'; -require_once 'HTMLPurifier/AttrTransform/BoolToCSS.php'; -require_once 'HTMLPurifier/AttrTransform/Border.php'; -require_once 'HTMLPurifier/AttrTransform/Name.php'; -require_once 'HTMLPurifier/AttrTransform/Length.php'; -require_once 'HTMLPurifier/AttrTransform/ImgSpace.php'; -require_once 'HTMLPurifier/AttrTransform/EnumToCSS.php'; - -/** - * Proprietary module that transforms deprecated elements into Strict - * HTML (see HTML 4.01 and XHTML 1.0) when possible. - */ - -class HTMLPurifier_HTMLModule_TransformToStrict extends HTMLPurifier_HTMLModule -{ - - var $name = 'TransformToStrict'; - - // we're actually modifying these elements, not defining them - var $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', - 'blockquote', 'table', 'td', 'th', 'tr', 'img', 'a', 'hr', 'br', - 'caption', 'ul', 'ol', 'li'); - - var $info_tag_transform = array( - // placeholders, see constructor for definitions - 'font' => false, - 'menu' => false, - 'dir' => false, - 'center'=> false - ); - - var $attr_collections = array( - 'Lang' => array( - 'lang' => false // placeholder - ) - ); - - var $info_attr_transform_post = array( - 'lang' => false // placeholder - ); - - function HTMLPurifier_HTMLModule_TransformToStrict() { - - // behavior with transformations when there's another CSS property - // working on it is interesting: the CSS will *always* override - // the deprecated attribute, whereas an inline CSS declaration will - // override the corresponding declaration in, say, an external - // stylesheet. This behavior won't affect most people, but it - // does represent an operational difference we CANNOT fix. - - // deprecated tag transforms - $this->info_tag_transform['font'] = new HTMLPurifier_TagTransform_Font(); - $this->info_tag_transform['menu'] = new HTMLPurifier_TagTransform_Simple('ul'); - $this->info_tag_transform['dir'] = new HTMLPurifier_TagTransform_Simple('ul'); - $this->info_tag_transform['center'] = new HTMLPurifier_TagTransform_Center(); - - foreach ($this->elements as $name) { - $this->info[$name] = new HTMLPurifier_ElementDef(); - $this->info[$name]->standalone = false; - } - - // deprecated attribute transforms - - // align battery - $align_lookup = array(); - $align_values = array('left', 'right', 'center', 'justify'); - foreach ($align_values as $v) $align_lookup[$v] = "text-align:$v;"; - $this->info['h1']->attr_transform_pre['align'] = - $this->info['h2']->attr_transform_pre['align'] = - $this->info['h3']->attr_transform_pre['align'] = - $this->info['h4']->attr_transform_pre['align'] = - $this->info['h5']->attr_transform_pre['align'] = - $this->info['h6']->attr_transform_pre['align'] = - $this->info['p'] ->attr_transform_pre['align'] = - new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup); - - // xml:lang <=> lang mirroring, implement in TransformToStrict, - // this is overridden in TransformToXHTML11 - $this->info_attr_transform_post['lang'] = new HTMLPurifier_AttrTransform_Lang(); - $this->attr_collections['Lang']['lang'] = new HTMLPurifier_AttrDef_Lang(); - - // this should not be applied to XHTML 1.0 Transitional, ONLY - // XHTML 1.0 Strict. We may need three classes - $this->info['blockquote']->content_model_type = 'strictblockquote'; - $this->info['blockquote']->child = false; // recalculate please! - - $this->info['table']->attr_transform_pre['bgcolor'] = - $this->info['tr']->attr_transform_pre['bgcolor'] = - $this->info['td']->attr_transform_pre['bgcolor'] = - $this->info['th']->attr_transform_pre['bgcolor'] = new HTMLPurifier_AttrTransform_BgColor(); - - $this->info['img']->attr_transform_pre['border'] = new HTMLPurifier_AttrTransform_Border(); - - $this->info['img']->attr_transform_pre['name'] = - $this->info['a']->attr_transform_pre['name'] = new HTMLPurifier_AttrTransform_Name(); - - $this->info['td']->attr_transform_pre['width'] = - $this->info['th']->attr_transform_pre['width'] = - $this->info['hr']->attr_transform_pre['width'] = new HTMLPurifier_AttrTransform_Length('width'); - - $this->info['td']->attr_transform_pre['nowrap'] = - $this->info['th']->attr_transform_pre['nowrap'] = new HTMLPurifier_AttrTransform_BoolToCSS('nowrap', 'white-space:nowrap;'); - - $this->info['td']->attr_transform_pre['height'] = - $this->info['th']->attr_transform_pre['height'] = new HTMLPurifier_AttrTransform_Length('height'); - - $this->info['img']->attr_transform_pre['hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace'); - $this->info['img']->attr_transform_pre['vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace'); - - $this->info['hr']->attr_transform_pre['size'] = new HTMLPurifier_AttrTransform_Length('size', 'height'); - - // this transformation is not precise but often good enough. - // different browsers use different styles to designate noshade - $this->info['hr']->attr_transform_pre['noshade'] = new HTMLPurifier_AttrTransform_BoolToCSS('noshade', 'color:#808080;background-color:#808080;border: 0;'); - - $this->info['br']->attr_transform_pre['clear'] = - new HTMLPurifier_AttrTransform_EnumToCSS('clear', array( - 'left' => 'clear:left;', - 'right' => 'clear:right;', - 'all' => 'clear:both;', - 'none' => 'clear:none;', - )); - - // this is a slightly unreasonable attribute - $this->info['caption']->attr_transform_pre['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) - 'left' => 'text-align:left;', - 'right' => 'text-align:right;', - 'top' => 'caption-side:top;', - 'bottom' => 'caption-side:bottom;' // not supported by IE - )); - - $this->info['table']->attr_transform_pre['align'] = - new HTMLPurifier_AttrTransform_EnumToCSS('align', array( - 'left' => 'float:left;', - 'center' => 'margin-left:auto;margin-right:auto;', - 'right' => 'float:right;' - )); - - $this->info['img']->attr_transform_pre['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;', - )); - - $this->info['hr']->attr_transform_pre['align'] = - new HTMLPurifier_AttrTransform_EnumToCSS('align', array( - '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;' - )); - - $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; - - $this->info['ul']->attr_transform_pre['type'] = - new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types); - $this->info['ol']->attr_transform_pre['type'] = - new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true); - $this->info['li']->attr_transform_pre['type'] = - new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true); - - - } - - var $defines_child_def = true; - function getChildDef($def) { - if ($def->content_model_type != 'strictblockquote') return false; - return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model); - } - -} - -?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/TransformToXHTML11.php b/library/HTMLPurifier/HTMLModule/TransformToXHTML11.php deleted file mode 100644 index 68aac613..00000000 --- a/library/HTMLPurifier/HTMLModule/TransformToXHTML11.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -require_once 'HTMLPurifier/AttrTransform/Lang.php'; - -/** - * Proprietary module that transforms XHTML 1.0 deprecated aspects into - * XHTML 1.1 compliant ones, when possible. For maximum effectiveness, - * HTMLPurifier_HTMLModule_TransformToStrict must also be loaded - * (otherwise, elements that were deprecated from Transitional to Strict - * will not be transformed). - * - * XHTML 1.1 compliant document are automatically XHTML 1.0 compliant too, - * although they may not be as friendly to legacy browsers. - */ - -class HTMLPurifier_HTMLModule_TransformToXHTML11 extends HTMLPurifier_HTMLModule -{ - - var $name = 'TransformToXHTML11'; - var $attr_collections = array( - 'Lang' => array( - 'lang' => false // remove it - ) - ); - - var $info_attr_transform_post = array( - 'lang' => false // remove it - ); - - function HTMLPurifier_HTMLModule_TransformToXHTML11() { - $this->info_attr_transform_pre['lang'] = new HTMLPurifier_AttrTransform_Lang(); - } - -} - -?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php b/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php new file mode 100644 index 00000000..341a8761 --- /dev/null +++ b/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php @@ -0,0 +1,16 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule.php'; + +class HTMLPurifier_HTMLModule_XMLCommonAttributes extends HTMLPurifier_HTMLModule +{ + var $name = 'XMLCommonAttributes'; + + var $attr_collections = array( + 'Lang' => array( + 'xml:lang' => 'LanguageCode', + ) + ); +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLModuleManager.php b/library/HTMLPurifier/HTMLModuleManager.php index 81ef13a5..f6b1b089 100644 --- a/library/HTMLPurifier/HTMLModuleManager.php +++ b/library/HTMLPurifier/HTMLModuleManager.php @@ -2,6 +2,8 @@ require_once 'HTMLPurifier/HTMLModule.php'; require_once 'HTMLPurifier/ElementDef.php'; +require_once 'HTMLPurifier/Doctype.php'; +require_once 'HTMLPurifier/DoctypeRegistry.php'; require_once 'HTMLPurifier/ContentSets.php'; require_once 'HTMLPurifier/AttrTypes.php'; @@ -23,14 +25,20 @@ require_once 'HTMLPurifier/HTMLModule/Image.php'; require_once 'HTMLPurifier/HTMLModule/StyleAttribute.php'; require_once 'HTMLPurifier/HTMLModule/Legacy.php'; require_once 'HTMLPurifier/HTMLModule/Target.php'; +require_once 'HTMLPurifier/HTMLModule/Scripting.php'; +require_once 'HTMLPurifier/HTMLModule/XMLCommonAttributes.php'; +require_once 'HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php'; -// proprietary modules -require_once 'HTMLPurifier/HTMLModule/TransformToStrict.php'; -require_once 'HTMLPurifier/HTMLModule/TransformToXHTML11.php'; +// tidy modules +require_once 'HTMLPurifier/HTMLModule/Tidy.php'; +require_once 'HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php'; +require_once 'HTMLPurifier/HTMLModule/Tidy/XHTML.php'; +require_once 'HTMLPurifier/HTMLModule/Tidy/XHTMLStrict.php'; +require_once 'HTMLPurifier/HTMLModule/Tidy/Proprietary.php'; HTMLPurifier_ConfigSchema::define( 'HTML', 'Doctype', null, 'string/null', - 'Doctype to use, valid values are HTML 4.01 Transitional, HTML 4.01 '. + 'Doctype to use, pre-defined values are HTML 4.01 Transitional, HTML 4.01 '. 'Strict, XHTML 1.0 Transitional, XHTML 1.0 Strict, XHTML 1.1. '. 'Technically speaking this is not actually a doctype (as it does '. 'not identify a corresponding DTD), but we are using this name '. @@ -38,173 +46,159 @@ HTMLPurifier_ConfigSchema::define( 'like %Core.XHTML or %HTML.Strict.' ); +HTMLPurifier_ConfigSchema::define( + 'HTML', 'Trusted', false, 'bool', + 'Indicates whether or not the user input is trusted or not. If the '. + 'input is trusted, a more expansive set of allowed tags and attributes '. + 'will be used. This directive has been available since 2.0.0.' +); + +HTMLPurifier_ConfigSchema::define( + 'HTML', 'AllowedModules', null, 'lookup/null', ' +<p> + A doctype comes with a set of usual modules to use. Without having + to mucking about with the doctypes, you can quickly activate or + disable these modules by specifying which modules you wish to allow + with this directive. This is most useful for unit testing specific + modules, although end users may find it useful for their own ends. +</p> +<p> + If you specify a module that does not exist, the manager will silently + fail to use it, so be careful! User-defined modules are not affected + by this directive. Modules defined in %HTML.CoreModules are not + affected by this directive. This directive has been available since 2.0.0. +</p> +'); + +HTMLPurifier_ConfigSchema::define( + 'HTML', 'CoreModules', array( + 'Structure' => true, + 'Text' => true, + 'Hypertext' => true, + 'List' => true, + 'NonXMLCommonAttributes' => true, + 'XMLCommonAttributes' => true, + 'CommonAttributes' => true + ), 'lookup', ' +<p> + Certain modularized doctypes (XHTML, namely), have certain modules + that must be included for the doctype to be an conforming document + type: put those modules here. By default, XHTML\'s core modules + are used. You can set this to a blank array to disable core module + protection, but this is not recommended. This directive has been + available since 2.0.0. +</p> +'); + class HTMLPurifier_HTMLModuleManager { /** - * Array of HTMLPurifier_Module instances, indexed by module's class name. - * All known modules, regardless of use, are in this array. + * Instance of HTMLPurifier_DoctypeRegistry + * @public + */ + var $doctypes; + + /** + * Instance of current doctype + * @public + */ + var $doctype; + + /** + * Instance of HTMLPurifier_AttrTypes + * @public + */ + var $attrTypes; + + /** + * Active instances of modules for the specified doctype are + * indexed, by name, in this array. */ var $modules = array(); /** - * String doctype we will validate against. See $validModules for use. - * - * @note - * There is a special doctype '*' that acts both as the "default" - * doctype if a customized system only defines one doctype and - * also a catch-all doctype that gets merged into all the other - * module collections. When possible, use a private collection to - * share modules between doctypes: this special doctype is to - * make life more convenient for users. + * Array of recognized HTMLPurifier_Module 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. */ - var $doctype; - var $doctypeAliases = array(); /**< Lookup array of strings to real doctypes */ + var $registeredModules = array(); /** - * Associative array: $collections[$type][$doctype] = list of modules. - * This is used to logically separate types of functionality so that - * based on the doctype and other configuration settings they may - * be easily switched and on and off. Custom setups may not need - * to use this abstraction, opting to have only one big collection - * with one valid doctype. + * List of extra modules that were added by the user using addModule(). + * These get unconditionally merged into the current doctype, whatever + * it may be. */ - var $collections = array(); + var $userModules = array(); /** - * Modules that may be used in a valid doctype of this kind. - * Correctional and leniency modules should not be placed in this - * array unless the user said so: don't stuff every possible lenient - * module for this doctype in here. + * Associative array of element name to list of modules that have + * definitions for the element; this array is dynamically filled. */ - var $validModules = array(); - var $validCollections = array(); /**< Collections to merge into $validModules */ - - /** - * Modules that we will allow in input, subset of $validModules. Single - * element definitions may result in us consulting validModules. - */ - var $activeModules = array(); - var $activeCollections = array(); /**< Collections to merge into $activeModules */ - - var $counter = 0; /**< Designates next available integer order for modules. */ - var $initialized = false; /**< Says whether initialize() was called */ - - /** - * Specifies what doctype to siphon new modules from addModule() to, - * or false to disable the functionality. Must be used in conjunction - * with $autoCollection. - */ - var $autoDoctype = false; - /** - * Specifies what collection to siphon new modules from addModule() to, - * or false to disable the functionality. Must be used in conjunction - * with $autoCollection. - */ - var $autoCollection = false; - - /** Associative array of element name to defining modules (always array) */ var $elementLookup = array(); - /** List of prefixes we should use for resolving small names */ + /** List of prefixes we should use for registering small names */ var $prefixes = array('HTMLPurifier_HTMLModule_'); - var $contentSets; /**< Instance of HTMLPurifier_ContentSets */ - var $attrTypes; /**< Instance of HTMLPurifier_AttrTypes */ + var $contentSets; /**< Instance of HTMLPurifier_ContentSets */ var $attrCollections; /**< Instance of HTMLPurifier_AttrCollections */ - /** - * @param $blank If true, don't do any initializing - */ - function HTMLPurifier_HTMLModuleManager($blank = false) { + /** If set to true, unsafe elements and attributes will be allowed */ + var $trusted = false; + + function HTMLPurifier_HTMLModuleManager() { - // the only editable internal object. The rest need to - // be manipulated through modules + // editable internal objects $this->attrTypes = new HTMLPurifier_AttrTypes(); + $this->doctypes = new HTMLPurifier_DoctypeRegistry(); - if (!$blank) $this->initialize(); + // setup default HTML doctypes - } - - function initialize() { - $this->initialized = true; - - // load default modules to the recognized modules list (not active) - $modules = array( - // define - 'CommonAttributes', - 'Text', 'Hypertext', 'List', 'Presentation', - 'Edit', 'Bdo', 'Tables', 'Image', 'StyleAttribute', - 'Target', - // define-redefine - 'Legacy', - // redefine - 'TransformToStrict', 'TransformToXHTML11' + // module reuse + $common = array( + 'CommonAttributes', 'Text', 'Hypertext', 'List', + 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image', + 'StyleAttribute', 'Scripting' ); - foreach ($modules as $module) { - $this->addModule($module); - } + $transitional = array('Legacy', 'Target'); + $xml = array('XMLCommonAttributes'); + $non_xml = array('NonXMLCommonAttributes'); - // Safe modules for supported doctypes. These are included - // in the valid and active module lists by default - $this->collections['Safe'] = array( - '_Common' => array( // leading _ indicates private - 'CommonAttributes', 'Text', 'Hypertext', 'List', - 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image', - 'StyleAttribute' - ), - // HTML definitions, defer to XHTML definitions - 'HTML 4.01 Transitional' => array(array('XHTML 1.0 Transitional')), - 'HTML 4.01 Strict' => array(array('XHTML 1.0 Strict')), - // XHTML definitions - 'XHTML 1.0 Transitional' => array( array('XHTML 1.0 Strict'), 'Legacy', 'Target' ), - 'XHTML 1.0 Strict' => array(array('_Common')), - 'XHTML 1.1' => array(array('_Common')), + $this->doctypes->register( + 'HTML 4.01 Transitional', false, + array_merge($common, $transitional, $non_xml), + array('Tidy_Transitional', 'Tidy_Proprietary') ); - // Modules that specify elements that are unsafe from untrusted - // third-parties. These should be registered in $validModules but - // almost never $activeModules unless you really know what you're - // doing. - $this->collections['Unsafe'] = array(); - - // Modules to import if lenient mode (attempt to convert everything - // to a valid representation) is on. These must not be in $validModules - // unless specified so. - $this->collections['Lenient'] = array( - 'HTML 4.01 Strict' => array(array('XHTML 1.0 Strict')), - 'XHTML 1.0 Strict' => array('TransformToStrict'), - 'XHTML 1.1' => array(array('XHTML 1.0 Strict'), 'TransformToXHTML11') + $this->doctypes->register( + 'HTML 4.01 Strict', false, + array_merge($common, $non_xml), + array('Tidy_Strict', 'Tidy_Proprietary') ); - // Modules to import if correctional mode (correct everything that - // is feasible to strict mode) is on. These must not be in $validModules - // unless specified so. - $this->collections['Correctional'] = array( - 'HTML 4.01 Transitional' => array(array('XHTML 1.0 Transitional')), - 'XHTML 1.0 Transitional' => array('TransformToStrict'), // probably want a different one + $this->doctypes->register( + 'XHTML 1.0 Transitional', true, + array_merge($common, $transitional, $xml, $non_xml), + array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary') ); - // User-space modules, custom code or whatever - $this->collections['Extension'] = array(); + $this->doctypes->register( + 'XHTML 1.0 Strict', true, + array_merge($common, $xml, $non_xml), + array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_XHTMLStrict', 'Tidy_Proprietary') + ); - // setup active versus valid modules. ORDER IS IMPORTANT! - // definition modules - $this->makeCollectionActive('Safe'); - $this->makeCollectionValid('Unsafe'); - // redefinition modules - $this->makeCollectionActive('Lenient'); - $this->makeCollectionActive('Correctional'); - - $this->autoDoctype = '*'; - $this->autoCollection = 'Extension'; + $this->doctypes->register( + 'XHTML 1.1', true, + array_merge($common, $xml), + array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary') // Tidy_XHTML1_1 + ); } /** - * Adds a module to the recognized module list. This does not - * do anything else: the module must be added to a corresponding - * collection to be "activated". + * 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. @@ -217,10 +211,15 @@ class HTMLPurifier_HTMLModuleManager * - Check for literal object name * - Throw fatal error * If your object name collides with an internal class, specify - * your module manually. + * your module manually. All modules must have been included + * externally: registerModule will not perform inclusions for you! + * @warning If your module has the same name as an already loaded + * module, your module will overload the old one WITHOUT + * warning. */ - function addModule($module) { + function registerModule($module) { if (is_string($module)) { + // attempt to load the module $original_module = $module; $ok = false; foreach ($this->prefixes as $prefix) { @@ -240,16 +239,19 @@ class HTMLPurifier_HTMLModuleManager } $module = new $module(); } - $module->order = $this->counter++; // assign then increment - $this->modules[$module->name] = $module; - if ($this->autoDoctype !== false && $this->autoCollection !== false) { - $this->collections[$this->autoCollection][$this->autoDoctype][] = $module->name; + if (empty($module->name)) { + trigger_error('Module instance of ' . get_class($module) . ' must have name'); + return; } + $this->registeredModules[$module->name] = $module; } /** * Safely tests for class existence without invoking __autoload in PHP5 + * or greater. * @param $name String class name to test + * @note If any other class needs it, we'll need to stash in a + * conjectured "compatibility" class * @private */ function _classExists($name) { @@ -265,55 +267,63 @@ class HTMLPurifier_HTMLModuleManager } /** - * Makes a collection active, while also making it valid if not - * already done so. See $activeModules for the semantics of "active". - * @param $collection_name Name of collection to activate + * Adds a module to the current doctype by first registering it, + * and then tacking it on to the active doctype */ - function makeCollectionActive($collection_name) { - if (!in_array($collection_name, $this->validCollections)) { - $this->makeCollectionValid($collection_name); - } - $this->activeCollections[] = $collection_name; + function addModule($module) { + $this->registerModule($module); + if (is_object($module)) $module = $module->name; + $this->userModules[] = $module; } /** - * Makes a collection valid. See $validModules for the semantics of "valid" - */ - function makeCollectionValid($collection_name) { - $this->validCollections[] = $collection_name; - } - - /** - * Adds a class prefix that addModule() will use to resolve a + * Adds a class prefix that registerModule() will use to resolve a * string name to a concrete class */ function addPrefix($prefix) { - $this->prefixes[] = (string) $prefix; + $this->prefixes[] = $prefix; } + /** + * Performs processing on modules, after being called you may + * use getElement() and getElements() + * @param $config Instance of HTMLPurifier_Config + */ function setup($config) { - // load up the autocollection - if ($this->autoCollection !== false) { - $this->makeCollectionActive($this->autoCollection); + $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]); + } } - // retrieve the doctype - $this->doctype = $this->getDoctype($config); - if (isset($this->doctypeAliases[$this->doctype])) { - $this->doctype = $this->doctypeAliases[$this->doctype]; + // merge in custom modules + $modules = array_merge($modules, $this->userModules); + + foreach ($modules as $module) { + $this->processModule($module); } - // process module collections to module name => module instance form - foreach ($this->collections as $col_i => $x) { - $this->processCollections($this->collections[$col_i]); + foreach ($this->doctype->tidyModules as $module) { + $this->processModule($module); + if (method_exists($this->modules[$module], 'construct')) { + $this->modules[$module]->construct($config); + } } - $this->validModules = $this->assembleModules($this->validCollections); - $this->activeModules = $this->assembleModules($this->activeCollections); - // setup lookup table based on all valid modules - foreach ($this->validModules as $module) { + foreach ($this->modules as $module) { foreach ($module->info as $name => $def) { if (!isset($this->elementLookup[$name])) { $this->elementLookup[$name] = array(); @@ -324,214 +334,51 @@ class HTMLPurifier_HTMLModuleManager // note the different choice $this->contentSets = new HTMLPurifier_ContentSets( - // content models that contain non-allowed elements are - // harmless because RemoveForeignElements will ensure - // they never get in anyway, and there is usually no - // reason why you should want to restrict a content - // model beyond what is mandated by the doctype. - // Note, however, that this means redefinitions of - // content models can't be tossed in validModels willy-nilly: - // that stuff still is regulated by configuration. - $this->validModules + // content set assembly deals with all possible modules, + // not just ones deemed to be "safe" + $this->modules ); $this->attrCollections = new HTMLPurifier_AttrCollections( $this->attrTypes, - // only explicitly allowed modules are allowed to affect - // the global attribute collections. This mean's there's - // a distinction between loading the Bdo module, and the - // bdo element: Bdo will enable the dir attribute on all - // elements, while bdo will only define the bdo element, - // which will not have an editable directionality. This might - // catch people who are loading only elements by surprise, so - // we should consider loading an entire module if all the - // elements it defines are requested by the user, especially - // if it affects the global attribute collections. - $this->activeModules + // 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 list of collections and merges together all the defined - * modules for the current doctype from those collections. - * @param $collections List of collection suffixes we should grab - * modules from (like 'Safe' or 'Lenient') + * Takes a module and adds it to the active module collection, + * registering it if necessary. */ - function assembleModules($collections) { - $modules = array(); - $numOfCollectionsUsed = 0; - foreach ($collections as $name) { - $disable_global = false; - if (!isset($this->collections[$name])) { - trigger_error("$name collection is undefined", E_USER_ERROR); - continue; - } - $cols = $this->collections[$name]; - if (isset($cols[$this->doctype])) { - if (isset($cols[$this->doctype]['*'])) { - unset($cols[$this->doctype]['*']); - $disable_global = true; - } - $modules += $cols[$this->doctype]; - $numOfCollectionsUsed++; - } - // accept catch-all doctype - if ( - $this->doctype !== '*' && - isset($cols['*']) && - !$disable_global - ) { - $modules += $cols['*']; - } + function processModule($module) { + if (!isset($this->registeredModules[$module]) || is_object($module)) { + $this->registerModule($module); } - - if ($numOfCollectionsUsed < 1) { - // possible XSS injection if user-specified doctypes - // are allowed - trigger_error("Doctype {$this->doctype} does not exist, ". - "check for typos (if you desire a doctype that allows ". - "no elements, use an empty array collection)", E_USER_ERROR); - } - return $modules; + $this->modules[$module] = $this->registeredModules[$module]; } /** - * Takes a collection and performs inclusions and substitutions for it. - * @param $cols Reference to collections class member variable + * Retrieves merged element definitions. + * @return Array of HTMLPurifier_ElementDef */ - function processCollections(&$cols) { - - // $cols is the set of collections - // $col_i is the name (index) of a collection - // $col is a collection/list of modules - - // perform inclusions - foreach ($cols as $col_i => $col) { - $seen = array(); - if (!empty($col[0]) && is_array($col[0])) { - $seen[$col_i] = true; // recursion reporting - $includes = $col[0]; - unset($cols[$col_i][0]); // remove inclusions value, recursion guard - } else { - $includes = array(); - } - if (empty($includes)) continue; - for ($i = 0; isset($includes[$i]); $i++) { - $inc = $includes[$i]; - if (isset($seen[$inc])) { - trigger_error( - "Circular inclusion detected in $col_i collection", - E_USER_ERROR - ); - continue; - } else { - $seen[$inc] = true; - } - if (!isset($cols[$inc])) { - trigger_error( - "Collection $col_i tried to include undefined ". - "collection $inc", E_USER_ERROR); - continue; - } - foreach ($cols[$inc] as $module) { - if (is_array($module)) { // another inclusion! - foreach ($module as $inc2) $includes[] = $inc2; - continue; - } - $cols[$col_i][] = $module; // merge in the other modules - } - } - } - - // replace with real modules, invert module from list to - // assoc array of module name to module instance - foreach ($cols as $col_i => $col) { - $ignore_global = false; - $order = array(); - foreach ($col as $module_i => $module) { - unset($cols[$col_i][$module_i]); - if (is_array($module)) { - trigger_error("Illegal inclusion array at index". - " $module_i found collection $col_i, inclusion". - " arrays must be at start of collection (index 0)", - E_USER_ERROR); - continue; - } - if ($module_i === '*' && $module === false) { - $ignore_global = true; - continue; - } - if (!isset($this->modules[$module])) { - trigger_error( - "Collection $col_i references undefined ". - "module $module", - E_USER_ERROR - ); - continue; - } - $module = $this->modules[$module]; - $cols[$col_i][$module->name] = $module; - $order[$module->name] = $module->order; - } - array_multisort( - $order, SORT_ASC, SORT_NUMERIC, $cols[$col_i] - ); - if ($ignore_global) $cols[$col_i]['*'] = false; - } - - // delete pseudo-collections - foreach ($cols as $col_i => $col) { - if ($col_i[0] == '_') unset($cols[$col_i]); - } - - } - - /** - * Retrieves the doctype from the configuration object - */ - function getDoctype($config) { - $doctype = $config->get('HTML', 'Doctype'); - if ($doctype !== null) { - return $doctype; - } - if (!$this->initialized) { - // don't do HTML-oriented backwards compatibility stuff - // use either the auto-doctype, or the catch-all doctype - return $this->autoDoctype ? $this->autoDoctype : '*'; - } - // this is backwards-compatibility stuff - if ($config->get('Core', 'XHTML')) { - $doctype = 'XHTML 1.0'; - } else { - $doctype = 'HTML 4.01'; - } - if ($config->get('HTML', 'Strict')) { - $doctype .= ' Strict'; - } else { - $doctype .= ' Transitional'; - } - return $doctype; - } - - /** - * Retrieves merged element definitions for all active elements. - * @note We may want to generate an elements array during setup - * and pass that on, because a specific combination of - * elements may trigger the loading of a module. - * @param $config Instance of HTMLPurifier_Config, for determining - * stray elements. - */ - function getElements($config) { + function getElements() { $elements = array(); - foreach ($this->activeModules as $module) { + foreach ($this->modules as $module) { foreach ($module->info as $name => $v) { if (isset($elements[$name])) continue; - $elements[$name] = $this->getElement($name, $config); + // if element is not safe, don't use it + if (!$this->trusted && ($v->safe === false)) continue; + $elements[$name] = $this->getElement($name); } } - // standalone elements now loaded + // 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; @@ -540,13 +387,16 @@ class HTMLPurifier_HTMLModuleManager /** * Retrieves a single merged element definition * @param $name Name of element - * @param $config Instance of HTMLPurifier_Config, may not be necessary. + * @param $trusted Boolean trusted overriding parameter: set to true + * if you want the full version of an element + * @return Merged HTMLPurifier_ElementDef */ - function getElement($name, $config) { + function getElement($name, $trusted = null) { $def = false; + if ($trusted === null) $trusted = $this->trusted; - $modules = $this->validModules; + $modules = $this->modules; if (!isset($this->elementLookup[$name])) { return false; @@ -555,9 +405,23 @@ class HTMLPurifier_HTMLModuleManager foreach($this->elementLookup[$name] as $module_name) { $module = $modules[$module_name]; - $new_def = $module->info[$name]; + + // copy 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 = $module->info[$name]->copy(); + + // refuse to create/merge in a definition that is deemed unsafe + if (!$trusted && ($new_def->safe === false)) { + $def = false; + continue; + } if (!$def && $new_def->standalone) { + // element with unknown safety is not to be trusted. + // however, a merge-in definition with undefined safety + // is fine + if (!$trusted && !$new_def->safe) continue; $def = $new_def; } elseif ($def) { $def->mergeIn($new_def); @@ -583,6 +447,13 @@ class HTMLPurifier_HTMLModuleManager $this->contentSets->generateChildDef($def, $module); } + + // add information on required attributes + foreach ($def->attr as $attr_name => $attr_def) { + if ($attr_def->required) { + $def->required_attr[] = $attr_name; + } + } return $def; diff --git a/library/HTMLPurifier/Language.php b/library/HTMLPurifier/Language.php index ca6fe031..6b0845fb 100644 --- a/library/HTMLPurifier/Language.php +++ b/library/HTMLPurifier/Language.php @@ -41,16 +41,34 @@ class HTMLPurifier_Language } /** - * Retrieves a localised message. Does not perform any operations. + * Retrieves a localised message. * @param $key string identifier of message * @return string localised message */ function getMessage($key) { if (!$this->_loaded) $this->load(); - if (!isset($this->messages[$key])) return ''; + if (!isset($this->messages[$key])) return "[$key]"; return $this->messages[$key]; } + /** + * Formats a localised message with passed parameters + * @param $key string identifier of message + * @param $param Parameter to substitute in (arbitrary number) + * @return string localised message + */ + function formatMessage($key) { + if (!$this->_loaded) $this->load(); + if (!isset($this->messages[$key])) return "[$key]"; + $raw = $this->messages[$key]; + $args = func_get_args(); + $substitutions = array(); + for ($i = 1; $i < count($args); $i++) { + $substitutions['$' . $i] = $args[$i]; + } + return strtr($raw, $substitutions); + } + } ?> \ No newline at end of file diff --git a/library/HTMLPurifier/Language/messages/en.php b/library/HTMLPurifier/Language/messages/en.php index 7650b818..3327460c 100644 --- a/library/HTMLPurifier/Language/messages/en.php +++ b/library/HTMLPurifier/Language/messages/en.php @@ -7,6 +7,8 @@ $messages = array( 'htmlpurifier' => 'HTML Purifier', 'pizza' => 'Pizza', // for unit testing purposes + + ); ?> \ No newline at end of file diff --git a/library/HTMLPurifier/LanguageFactory.php b/library/HTMLPurifier/LanguageFactory.php index 5cdf1281..c7b4b434 100644 --- a/library/HTMLPurifier/LanguageFactory.php +++ b/library/HTMLPurifier/LanguageFactory.php @@ -100,15 +100,15 @@ class HTMLPurifier_LanguageFactory // you can bypass the conditional include by loading the // file yourself if (file_exists($file) && !class_exists($class)) { - include_once $file; - } + include_once $file; + } } if (!class_exists($class)) { // go fallback - $fallback = HTMLPurifier_Language::getFallbackFor($code); + $fallback = HTMLPurifier_LanguageFactory::getFallbackFor($code); $depth++; - $lang = Language::factory( $fallback ); + $lang = HTMLPurifier_LanguageFactory::factory( $fallback ); $depth--; } else { $lang = new $class; @@ -172,15 +172,15 @@ class HTMLPurifier_LanguageFactory // merge fallback with current language foreach ( $this->keys as $key ) { - if (isset($cache[$key]) && isset($fallback_cache[$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]; - } + } else { + $cache[$key] = $fallback_cache[$key]; + } } } diff --git a/library/HTMLPurifier/Lexer.php b/library/HTMLPurifier/Lexer.php index 975fb65f..6b2e87ab 100644 --- a/library/HTMLPurifier/Lexer.php +++ b/library/HTMLPurifier/Lexer.php @@ -4,6 +4,14 @@ require_once 'HTMLPurifier/Token.php'; require_once 'HTMLPurifier/Encoder.php'; require_once 'HTMLPurifier/EntityParser.php'; +// implementations +require_once 'HTMLPurifier/Lexer/DirectLex.php'; +if (version_compare(PHP_VERSION, "5", ">=")) { + // You can remove the if statement if you are running PHP 5 only. + // We ought to get the strict version to follow those rules. + require_once 'HTMLPurifier/Lexer/DOMLex.php'; +} + HTMLPurifier_ConfigSchema::define( 'Core', 'AcceptFullDocuments', true, 'bool', 'This parameter determines whether or not the filter should accept full '. @@ -11,6 +19,52 @@ HTMLPurifier_ConfigSchema::define( 'drop all sections except the content between body.' ); +HTMLPurifier_ConfigSchema::define( + 'Core', 'LexerImpl', null, 'mixed/null', ' +<p> + This parameter determines what lexer implementation can be used. The + valid values are: +</p> +<dl> + <dt><em>null</em></dt> + <dd> + Recommended, the lexer implementation will be auto-detected based on + your PHP-version and configuration. + </dd> + <dt><em>string</em> lexer identifier</dt> + <dd> + This is a slim way of manually overridding the implementation. + Currently recognized values are: DOMLex (the default PHP5 implementation) + and DirectLex (the default PHP4 implementation). Only use this if + you know what you are doing: usually, the auto-detection will + manage things for cases you aren\'t even aware of. + </dd> + <dt><em>object</em> lexer instance</dt> + <dd> + Super-advanced: you can specify your own, custom, implementation that + implements the interface defined by <code>HTMLPurifier_Lexer</code>. + I may remove this option simply because I don\'t expect anyone + to use it. + </dd> +</dl> +<p> + This directive has been available since 2.0.0. +</p> +' +); + +HTMLPurifier_ConfigSchema::define( + 'Core', 'MaintainLineNumbers', false, 'bool', ' +<p> + If true, HTML Purifier will add line number information to all tokens. + This is useful when error reporting is turned on, but can result in + significant performance degradation and should not be used when + unnecessary. This directive must be used with the DirectLex lexer, + as the DOMLex lexer does not (yet) support this functionality. This directive + has been available since 2.0.0. +</p> +'); + /** * Forgivingly lexes HTML (SGML-style) markup into tokens. * @@ -55,11 +109,83 @@ HTMLPurifier_ConfigSchema::define( class HTMLPurifier_Lexer { + // -- STATIC ---------------------------------------------------------- + + /** + * Retrieves or sets the default Lexer as a Prototype Factory. + * + * Depending on what PHP version you are running, the abstract base + * Lexer class will determine which concrete Lexer is best for you: + * HTMLPurifier_Lexer_DirectLex for PHP 4, and HTMLPurifier_Lexer_DOMLex + * for PHP 5 and beyond. This general rule has a few exceptions to it + * involving special features that only DirectLex implements. + * + * @static + * + * @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. + * + * @note In PHP4, it is possible to call this factory method from + * subclasses, such usage is not recommended and not + * forwards-compatible. + * + * @param $prototype Optional prototype lexer or configuration object + * @return Concrete lexer. + */ + 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'); + } + + if (is_object($lexer)) { + return $lexer; + } + + if (is_null($lexer)) { do { + // auto-detection algorithm + + // once PHP DOM implements native line numbers, or we + // hack out something using XSLT, remove this stipulation + if ($config->get('Core', 'MaintainLineNumbers')) { + $lexer = 'DirectLex'; + break; + } + + if (version_compare(PHP_VERSION, "5", ">=") && // check for PHP5 + class_exists('DOMDocument')) { // check for DOM support + $lexer = 'DOMLex'; + } else { + $lexer = 'DirectLex'; + } + + } while(0); } // do..while so we can break + + // instantiate recognized string names + switch ($lexer) { + case 'DOMLex': + return new HTMLPurifier_Lexer_DOMLex(); + case 'DirectLex': + return new HTMLPurifier_Lexer_DirectLex(); + default: + trigger_error("Cannot instantiate unrecognized Lexer type " . htmlspecialchars($lexer), E_USER_ERROR); + } + + } + + // -- CONVENIENCE MEMBERS --------------------------------------------- + function HTMLPurifier_Lexer() { $this->_entity_parser = new HTMLPurifier_EntityParser(); } - /** * Most common entity to raw value conversion table for special entities. * @protected @@ -123,46 +249,6 @@ class HTMLPurifier_Lexer trigger_error('Call to abstract class', E_USER_ERROR); } - /** - * Retrieves or sets the default Lexer as a Prototype Factory. - * - * Depending on what PHP version you are running, the abstract base - * Lexer class will determine which concrete Lexer is best for you: - * HTMLPurifier_Lexer_DirectLex for PHP 4, and HTMLPurifier_Lexer_DOMLex - * for PHP 5 and beyond. - * - * Passing the optional prototype lexer parameter will override the - * default with your own implementation. A copy/reference of the prototype - * lexer will now be returned when you request a new lexer. - * - * @static - * - * @note - * Though it is possible to call this factory method from subclasses, - * such usage is not recommended. - * - * @param $prototype Optional prototype lexer. - * @return Concrete lexer. - */ - static function create($prototype = null) { - // we don't really care if it's a reference or a copy - static $lexer = null; - if ($prototype) { - $lexer = $prototype; - } - if (empty($lexer)) { - if (version_compare(PHP_VERSION, "5", ">=") && // check for PHP5 - class_exists('DOMDocument')) { // check for DOM support - require_once 'HTMLPurifier/Lexer/DOMLex.php'; - $lexer = new HTMLPurifier_Lexer_DOMLex(); - } else { - require_once 'HTMLPurifier/Lexer/DirectLex.php'; - $lexer = new HTMLPurifier_Lexer_DirectLex(); - } - } - return $lexer; - } - /** * Translates CDATA sections into regular sections (through escaping). * diff --git a/library/HTMLPurifier/Lexer/DOMLex.php b/library/HTMLPurifier/Lexer/DOMLex.php index 9286b023..de9d6871 100644 --- a/library/HTMLPurifier/Lexer/DOMLex.php +++ b/library/HTMLPurifier/Lexer/DOMLex.php @@ -53,20 +53,17 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer '</head><body><div>'.$string.'</div></body></html>'; $doc = new DOMDocument(); - $doc->encoding = 'UTF-8'; // technically does nothing, but whatever + $doc->encoding = 'UTF-8'; // theoretically, the above has this covered - // DOM will toss errors if the HTML its parsing has really big - // problems, so we're going to mute them. This can cause problems - // if a custom error handler that doesn't implement error_reporting - // is set, as noted by a Drupal plugin of HTML Purifier. Consider - // making our own error reporter to temporarily load in - @$doc->loadHTML($string); + set_error_handler(array($this, 'muteErrorHandler')); + $doc->loadHTML($string); + restore_error_handler(); $tokens = array(); $this->tokenizeDOM( - $doc->getElementsByTagName('html')->item(0)-> // html - getElementsByTagName('body')->item(0)-> // body - getElementsByTagName('div')->item(0) // div + $doc->getElementsByTagName('html')->item(0)-> // <html> + getElementsByTagName('body')->item(0)-> // <body> + getElementsByTagName('div')->item(0) // <div> , $tokens); return $tokens; } @@ -82,7 +79,6 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer * @returns Tokens of node appended to previously passed tokens. */ protected function tokenizeDOM($node, &$tokens, $collect = false) { - // recursive goodness! // intercept non element nodes. WE MUST catch all of them, // but we're not getting the character reference nodes because @@ -147,6 +143,11 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer return $array; } + /** + * An error handler that mutes all errors + */ + public function muteErrorHandler($errno, $errstr) {} + } ?> \ No newline at end of file diff --git a/library/HTMLPurifier/Lexer/DirectLex.php b/library/HTMLPurifier/Lexer/DirectLex.php index 57d116a4..a34e0517 100644 --- a/library/HTMLPurifier/Lexer/DirectLex.php +++ b/library/HTMLPurifier/Lexer/DirectLex.php @@ -2,6 +2,20 @@ require_once 'HTMLPurifier/Lexer.php'; +HTMLPurifier_ConfigSchema::define( + 'Core', 'DirectLexLineNumberSyncInterval', 0, 'int', ' +<p> + Specifies the number of tokens the DirectLex line number tracking + implementations should process before attempting to resyncronize the + current line count by manually counting all previous new-lines. When + at 0, this functionality is disabled. Lower values will decrease + performance, and this is only strictly necessary if the counting + algorithm is buggy (in which case you should report it as a bug). + This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is + not being used. This directive has been available since 2.0.0. +</p> +'); + /** * Our in-house implementation of a parser. * @@ -32,9 +46,17 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer $inside_tag = false; // whether or not we're parsing the inside of a tag $array = array(); // result array + $maintain_line_numbers = $config->get('Core', 'MaintainLineNumbers'); + $current_line = 1; + $nl = PHP_EOL; + // 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'); + // infinite loop protection // has to be pretty big, since html docs can be big // we're allow two hundred thousand tags... more than enough? + // NOTE: this is also used for synchronization, so watch out $loops = 0; while(true) { @@ -42,10 +64,21 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer // infinite loop protection if (++$loops > 200000) return array(); + // recalculate lines + if ( + $maintain_line_numbers && // line number tracking is on + $synchronize_interval && // synchronization is on + $cursor > 0 && // cursor is further than zero + $loops % $synchronize_interval === 0 // time to synchronize! + ) { + $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); + } + $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++; @@ -53,7 +86,7 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer if (!$inside_tag && $position_next_lt !== false) { // We are not inside tag and there still is another tag to parse - $array[] = new + $token = new HTMLPurifier_Token_Text( $this->parseData( substr( @@ -61,6 +94,11 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer ) ) ); + if ($maintain_line_numbers) { + $token->line = $current_line; + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); + } + $array[] = $token; $cursor = $position_next_lt + 1; $inside_tag = true; continue; @@ -69,7 +107,7 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer // If we're already at the end, break if ($cursor === strlen($html)) break; // Create Text of rest of string - $array[] = new + $token = new HTMLPurifier_Token_Text( $this->parseData( substr( @@ -77,6 +115,8 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer ) ) ); + if ($maintain_line_numbers) $token->line = $current_line; + $array[] = $token; break; } elseif ($inside_tag && $position_next_gt !== false) { // We are in tag and it is well formed @@ -89,12 +129,17 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer substr($segment, 0, 3) == '!--' && substr($segment, $strlen_segment-2, 2) == '--' ) { - $array[] = new + $token = new HTMLPurifier_Token_Comment( substr( $segment, 3, $strlen_segment - 5 ) ); + if ($maintain_line_numbers) { + $token->line = $current_line; + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; $inside_tag = false; $cursor = $position_next_gt + 1; continue; @@ -104,7 +149,12 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer $is_end_tag = (strpos($segment,'/') === 0); if ($is_end_tag) { $type = substr($segment, 1); - $array[] = new HTMLPurifier_Token_End($type); + $token = new HTMLPurifier_Token_End($type); + if ($maintain_line_numbers) { + $token->line = $current_line; + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; $inside_tag = false; $cursor = $position_next_gt + 1; continue; @@ -114,7 +164,7 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer // have accidently grabbed an emoticon. Translate into // text and go our merry way if (!ctype_alnum($segment[0])) { - $array[] = new + $token = new HTMLPurifier_Token_Text( '<' . $this->parseData( @@ -122,6 +172,11 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer ) . '>' ); + if ($maintain_line_numbers) { + $token->line = $current_line; + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; $cursor = $position_next_gt + 1; $inside_tag = false; continue; @@ -142,10 +197,15 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer if ($position_first_space >= $strlen_segment) { if ($is_self_closing) { - $array[] = new HTMLPurifier_Token_Empty($segment); + $token = new HTMLPurifier_Token_Empty($segment); } else { - $array[] = new HTMLPurifier_Token_Start($segment); + $token = new HTMLPurifier_Token_Start($segment); } + if ($maintain_line_numbers) { + $token->line = $current_line; + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; $inside_tag = false; $cursor = $position_next_gt + 1; continue; @@ -169,21 +229,29 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer } if ($is_self_closing) { - $array[] = new HTMLPurifier_Token_Empty($type, $attr); + $token = new HTMLPurifier_Token_Empty($type, $attr); } else { - $array[] = new HTMLPurifier_Token_Start($type, $attr); + $token = new HTMLPurifier_Token_Start($type, $attr); } + if ($maintain_line_numbers) { + $token->line = $current_line; + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; $cursor = $position_next_gt + 1; $inside_tag = false; continue; } else { - $array[] = new + $token = new HTMLPurifier_Token_Text( '<' . $this->parseData( substr($html, $cursor) ) ); + if ($maintain_line_numbers) $token->line = $current_line; + // no cursor scroll? Hmm... + $array[] = $token; break; } break; @@ -191,6 +259,22 @@ class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer return $array; } + /** + * PHP 4 compatible substr_count that implements offset and length + */ + 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. * diff --git a/library/HTMLPurifier/Printer.php b/library/HTMLPurifier/Printer.php index 14135fd8..95be17a2 100644 --- a/library/HTMLPurifier/Printer.php +++ b/library/HTMLPurifier/Printer.php @@ -4,6 +4,8 @@ require_once 'HTMLPurifier/Generator.php'; require_once 'HTMLPurifier/Token.php'; require_once 'HTMLPurifier/Encoder.php'; +// OUT OF DATE, NEEDS UPDATING! + class HTMLPurifier_Printer { @@ -26,9 +28,9 @@ class HTMLPurifier_Printer /** * Main function that renders object or aspect of that object - * @param $config Configuration object + * @note Parameters vary depending on printer */ - function render($config) {} + // function render() {} /** * Returns a start tag @@ -64,6 +66,18 @@ class HTMLPurifier_Printer $this->end($tag); } + function elementEmpty($tag, $attr = array()) { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Empty($tag, $attr) + ); + } + + function text($text) { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Text($text) + ); + } + /** * Prints a simple key/value row in a table. * @param $name Key diff --git a/library/HTMLPurifier/Printer/ConfigForm.css b/library/HTMLPurifier/Printer/ConfigForm.css new file mode 100644 index 00000000..23c7f999 --- /dev/null +++ b/library/HTMLPurifier/Printer/ConfigForm.css @@ -0,0 +1,8 @@ + +.hp-config {} + +.hp-config tbody th {text-align:right;} +.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;} +.hp-config .namespace th {text-align:center;} +.hp-config .verbose {display:none;} +.hp-config .controls {text-align:center;} diff --git a/library/HTMLPurifier/Printer/ConfigForm.js b/library/HTMLPurifier/Printer/ConfigForm.js new file mode 100644 index 00000000..119ca4a0 --- /dev/null +++ b/library/HTMLPurifier/Printer/ConfigForm.js @@ -0,0 +1,3 @@ +function toggleWriteability(id_of_patient, checked) { + document.getElementById(id_of_patient).disabled = checked; +} \ No newline at end of file diff --git a/library/HTMLPurifier/Printer/ConfigForm.php b/library/HTMLPurifier/Printer/ConfigForm.php new file mode 100644 index 00000000..c157f2ab --- /dev/null +++ b/library/HTMLPurifier/Printer/ConfigForm.php @@ -0,0 +1,260 @@ +<?php + +require_once 'HTMLPurifier/Printer.php'; + +class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer +{ + + /** + * Printers for specific fields + * @protected + */ + var $fields = array(); + + /** + * Documentation URL, can have fragment tagged on end + * @protected + */ + var $docURL; + + /** + * Name of form element to stuff config in + * @protected + */ + var $name; + + /** + * @param $name Form element name for directives to be stuffed into + * @param $doc_url String documentation URL, will have fragment tagged on + */ + function HTMLPurifier_Printer_ConfigForm($name, $doc_url = null) { + parent::HTMLPurifier_Printer(); + $this->docURL = $doc_url; + $this->name = $name; + $this->fields['default'] = new HTMLPurifier_Printer_ConfigForm_default(); + $this->fields['bool'] = new HTMLPurifier_Printer_ConfigForm_bool(); + } + + /** + * Returns HTML output for a configuration form + * @param $config Configuration object of current form state + * @param $ns Optional namespace(s) to restrict form to + */ + function render($config, $ns = true) { + $this->config = $config; + if ($ns === true) { + $all = $config->getAll(); + } else { + if (is_string($ns)) $ns = array($ns); + foreach ($ns as $n) { + $all = array($n => $config->getBatch($n)); + } + } + $ret = ''; + $ret .= $this->start('table', array('class' => 'hp-config')); + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Directive'); + $ret .= $this->element('th', 'Value'); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + foreach ($all as $ns => $directives) { + $ret .= $this->renderNamespace($ns, $directives); + } + $ret .= $this->start('tfoot'); + $ret .= $this->start('tr'); + $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); + $ret .= '<input type="submit" value="Submit" /> [<a href="?">Reset</a>]'; + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + $ret .= $this->end('tfoot'); + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a single namespace + * @param $ns String namespace name + * @param $directive Associative array of directives to values + * @protected + */ + function renderNamespace($ns, $directives) { + $ret = ''; + $ret .= $this->start('tbody', array('class' => 'namespace')); + $ret .= $this->start('tr'); + $ret .= $this->element('th', $ns, array('colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + $ret .= $this->start('tbody'); + foreach ($directives as $directive => $value) { + $ret .= $this->start('tr'); + $ret .= $this->start('th'); + if ($this->docURL) { + $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); + $ret .= $this->start('a', array('href' => $url)); + } + $ret .= $this->element( + 'label', + "%$ns.$directive", + // component printers must create an element with this id + array('for' => "{$this->name}:$ns.$directive") + ); + if ($this->docURL) $ret .= $this->end('a'); + $ret .= $this->end('th'); + + $ret .= $this->start('td'); + $def = $this->config->def->info[$ns][$directive]; + $type = $def->type; + if (!isset($this->fields[$type])) $type = 'default'; + $type_obj = $this->fields[$type]; + if ($def->allow_null) { + $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); + } + $ret .= $type_obj->render($ns, $directive, $value, $this->name, $this->config); + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + } + $ret .= $this->end('tbody'); + return $ret; + } + +} + +/** + * Printer decorator for directives that accept null + */ +class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer { + /** + * Printer being decorated + */ + var $obj; + /** + * @param $obj Printer to decorate + */ + function HTMLPurifier_Printer_ConfigForm_NullDecorator($obj) { + parent::HTMLPurifier_Printer(); + $this->obj = $obj; + } + function render($ns, $directive, $value, $name, $config) { + $ret = ''; + $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Null/Disabled'); + $ret .= $this->end('label'); + $attr = array( + 'type' => 'checkbox', + 'value' => '1', + 'class' => 'null-toggle', + 'name' => "$name:Null_$ns.$directive", + 'id' => "$name:Null_$ns.$directive", + 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! + ); + if ($value === null) $attr['checked'] = 'checked'; + $ret .= $this->elementEmpty('input', $attr); + $ret .= $this->text(' or '); + $ret .= $this->elementEmpty('br'); + $ret .= $this->obj->render($ns, $directive, $value, $name, $config); + return $ret; + } +} + +/** + * Swiss-army knife configuration form field printer + */ +class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { + function render($ns, $directive, $value, $name, $config) { + // this should probably be split up a little + $ret = ''; + $def = $config->def->info[$ns][$directive]; + if (is_array($value)) { + switch ($def->type) { + case 'lookup': + $array = $value; + $value = array(); + foreach ($array as $val => $b) { + $value[] = $val; + } + case 'list': + $value = implode(',', $value); + break; + case 'hash': + $nvalue = ''; + foreach ($value as $i => $v) { + $nvalue .= "$i:$v,"; + } + $value = $nvalue; + break; + default: + $value = ''; + } + } + if ($def->type === 'mixed') { + return 'Not supported'; + $value = serialize($value); + } + $attr = array( + 'type' => 'text', + 'name' => "$name"."[$ns.$directive]", + 'id' => "$name:$ns.$directive" + ); + if ($value === null) $attr['disabled'] = 'disabled'; + if (is_array($def->allowed)) { + $ret .= $this->start('select', $attr); + foreach ($def->allowed as $val => $b) { + $attr = array(); + if ($value == $val) $attr['selected'] = 'selected'; + $ret .= $this->element('option', $val, $attr); + } + $ret .= $this->end('select'); + } else { + $attr['value'] = $value; + $ret .= $this->elementEmpty('input', $attr); + } + return $ret; + } +} + +/** + * Bool form field printer + */ +class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer { + function render($ns, $directive, $value, $name, $config) { + $ret = ''; + + $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); + + $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Yes'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name"."[$ns.$directive]", + 'id' => "$name:Yes_$ns.$directive", + 'value' => '1' + ); + if ($value) $attr['checked'] = 'checked'; + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' No'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name"."[$ns.$directive]", + 'id' => "$name:No_$ns.$directive", + 'value' => '0' + ); + if (!$value) $attr['checked'] = 'checked'; + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->end('div'); + + return $ret; + } +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/Strategy/FixNesting.php b/library/HTMLPurifier/Strategy/FixNesting.php index 08f90756..e6a779e5 100644 --- a/library/HTMLPurifier/Strategy/FixNesting.php +++ b/library/HTMLPurifier/Strategy/FixNesting.php @@ -42,14 +42,16 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy $definition = $config->getHTMLDefinition(); // insert implicit "parent" node, will be removed at end. - // ! we might want to move this to configuration // DEFINITION CALL $parent_name = $definition->info_parent; array_unshift($tokens, new HTMLPurifier_Token_Start($parent_name)); $tokens[] = new HTMLPurifier_Token_End($parent_name); - // setup the context variables - $is_inline = false; // reference var that we alter + // 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); //####################################################################// @@ -60,8 +62,9 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy $stack = array(); // stack that contains all elements that are excluded - // same structure as $stack, but it is only populated when an element - // with exclusions is processed, i.e. there won't be empty exclusions. + // 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(); //####################################################################// @@ -110,7 +113,10 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy $parent_def = $definition->info[$parent_name]; } } else { - // unknown info, it won't be used anyway + // processing as if the parent were the "root" node + // unknown info, it won't be used anyway, in the future, + // we may want to enforce one element only (this is + // necessary for HTML Purifier to clean entire documents $parent_index = $parent_name = $parent_def = null; } @@ -207,6 +213,12 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy // current node is now the next possible start node // unless it turns out that we need to do a double-check + // this is a rought heuristic that covers 100% of HTML's + // cases and 99% of all other cases. A child definition + // that would be tricked by this would be something like: + // ( | a b c) where it's all or nothing. Fortunantely, + // our current implementation claims that that case would + // not allow empty, even if it did if (!$parent_def->child->allow_empty) { // we need to do a double-check $i = $parent_index; diff --git a/library/HTMLPurifier/Strategy/MakeWellFormed.php b/library/HTMLPurifier/Strategy/MakeWellFormed.php index 84580d3d..9d440452 100644 --- a/library/HTMLPurifier/Strategy/MakeWellFormed.php +++ b/library/HTMLPurifier/Strategy/MakeWellFormed.php @@ -62,6 +62,8 @@ class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy $parent_name = $parent->name; $parent_info = $definition->info[$parent_name]; + // we need to replace this with a more general + // algorithm if (isset($parent_info->auto_close[$token->name])) { $result[] = new HTMLPurifier_Token_End($parent_name); $result[] = $token; diff --git a/library/HTMLPurifier/Strategy/RemoveForeignElements.php b/library/HTMLPurifier/Strategy/RemoveForeignElements.php index cb5c4dd1..9a1e80c0 100644 --- a/library/HTMLPurifier/Strategy/RemoveForeignElements.php +++ b/library/HTMLPurifier/Strategy/RemoveForeignElements.php @@ -5,6 +5,8 @@ require_once 'HTMLPurifier/HTMLDefinition.php'; require_once 'HTMLPurifier/Generator.php'; require_once 'HTMLPurifier/TagTransform.php'; +require_once 'HTMLPurifier/AttrValidator.php'; + HTMLPurifier_ConfigSchema::define( 'Core', 'RemoveInvalidImg', true, 'bool', 'This directive enables pre-emptive URI checking in <code>img</code> '. @@ -13,6 +15,14 @@ HTMLPurifier_ConfigSchema::define( 'since 1.3.0, revert to pre-1.3.0 behavior by setting to false.' ); +HTMLPurifier_ConfigSchema::define( + 'Core', 'RemoveScriptContents', true, 'bool', ' +This directive enables HTML Purifier to remove not only script tags +but all of their contents. This directive has been available since 2.0.0, +revert to pre-2.0.0 behavior by setting to false. +' +); + /** * Removes all unrecognized tags from the list of tokens. * @@ -28,33 +38,27 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy $definition = $config->getHTMLDefinition(); $generator = new HTMLPurifier_Generator(); $result = array(); + $escape_invalid_tags = $config->get('Core', 'EscapeInvalidTags'); $remove_invalid_img = $config->get('Core', 'RemoveInvalidImg'); + $remove_script_contents = $config->get('Core', 'RemoveScriptContents'); + + $attr_validator = new HTMLPurifier_AttrValidator(); + + // removes tokens until it reaches a closing tag with its value + $remove_until = false; + foreach($tokens as $token) { + if ($remove_until) { + if (empty($token->is_tag) || $token->name !== $remove_until) { + continue; + } + } if (!empty( $token->is_tag )) { // DEFINITION CALL - if (isset($definition->info[$token->name])) { - // leave untouched, except for a few special cases: - - // hard-coded image special case, pre-emptively drop - // if not available. Probably not abstract-able - if ( $token->name == 'img' && $remove_invalid_img ) { - if (!isset($token->attr['src'])) { - continue; - } - if (!isset($definition->info['img']->attr['src'])) { - continue; - } - $token->attr['src'] = - $definition-> - info['img']-> - attr['src']-> - validate($token->attr['src'], - $config, $context); - if ($token->attr['src'] === false) continue; - } - - } elseif ( + + // before any processing, try to transform the element + if ( isset($definition->info_tag_transform[$token->name]) ) { // there is a transformation for this tag @@ -62,12 +66,45 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy $token = $definition-> info_tag_transform[$token->name]-> transform($token, $config, $context); + } + + if (isset($definition->info[$token->name])) { + + // mostly everything's good, but + // we need to make sure required attributes are in order + if ( + $definition->info[$token->name]->required_attr && + ($token->name != 'img' || $remove_invalid_img) // ensure config option still works + ) { + $token = $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) continue; + $token->armor['ValidateAttributes'] = true; + } + } elseif ($escape_invalid_tags) { // invalid tag, generate HTML and insert in $token = new HTMLPurifier_Token_Text( $generator->generateFromToken($token, $config, $context) ); } else { + // check if we need to destroy all of the tag's children + // CAN BE GENERICIZED + if ($token->name == 'script' && $remove_script_contents) { + if ($token->type == 'start') { + $remove_until = $token->name; + } elseif ($token->type == 'empty') { + // do nothing: we're still looking + } else { + $remove_until = false; + } + } continue; } } elseif ($token->type == 'comment') { diff --git a/library/HTMLPurifier/Strategy/ValidateAttributes.php b/library/HTMLPurifier/Strategy/ValidateAttributes.php index 07744f80..1c9e09b3 100644 --- a/library/HTMLPurifier/Strategy/ValidateAttributes.php +++ b/library/HTMLPurifier/Strategy/ValidateAttributes.php @@ -4,6 +4,8 @@ require_once 'HTMLPurifier/Strategy.php'; require_once 'HTMLPurifier/HTMLDefinition.php'; require_once 'HTMLPurifier/IDAccumulator.php'; +require_once 'HTMLPurifier/AttrValidator.php'; + HTMLPurifier_ConfigSchema::define( 'Attr', 'IDBlacklist', array(), 'list', 'Array of IDs not allowed in the document.'); @@ -17,16 +19,13 @@ class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy function execute($tokens, $config, &$context) { - $definition = $config->getHTMLDefinition(); - // setup id_accumulator context $id_accumulator = new HTMLPurifier_IDAccumulator(); $id_accumulator->load($config->get('Attr', 'IDBlacklist')); $context->register('IDAccumulator', $id_accumulator); - // create alias to global definition array, see also $defs - // DEFINITION CALL - $d_defs = $definition->info_global_attr; + // setup validator + $validator = new HTMLPurifier_AttrValidator(); foreach ($tokens as $key => $token) { @@ -34,91 +33,12 @@ class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy // namely start and empty tags if ($token->type !== 'start' && $token->type !== 'empty') continue; - // copy out attributes for easy manipulation - $attr = $token->attr; + // skip tokens that are armored + if (!empty($token->armor['ValidateAttributes'])) continue; - // do global transformations (pre) - // nothing currently utilizes this - foreach ($definition->info_attr_transform_pre as $transform) { - $attr = $transform->transform($attr, $config, $context); - } - - // 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($attr, $config, $context); - } - - // 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; - - // 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) { - // remove the attribute - unset($attr[$attr_key]); - } elseif (is_string($result)) { - // simple substitution - $attr[$attr_key] = $result; - } - - // 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). - } - - // post transforms - - // ex. <x lang="fr"> to <x lang="fr" xml:lang="fr"> - foreach ($definition->info_attr_transform_post as $transform) { - $attr = $transform->transform($attr, $config, $context); - } - - // ex. <bdo> to <bdo dir="ltr"> - foreach ($definition->info[$token->name]->attr_transform_post as $transform) { - $attr = $transform->transform($attr, $config, $context); - } - - // commit changes - // could interfere with flyweight implementation - $tokens[$key]->attr = $attr; + $tokens[$key] = $validator->validateToken($token, $config, $context); } + $context->destroy('IDAccumulator'); return $tokens; diff --git a/library/HTMLPurifier/TagTransform.php b/library/HTMLPurifier/TagTransform.php index f5dc5c97..367ec8be 100644 --- a/library/HTMLPurifier/TagTransform.php +++ b/library/HTMLPurifier/TagTransform.php @@ -24,6 +24,18 @@ class HTMLPurifier_TagTransform trigger_error('Call to abstract function', E_USER_ERROR); } + /** + * 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 $attr Attribute array to process (passed by reference) + * @param $css CSS to prepend + */ + function prependCSS(&$attr, $css) { + $attr['style'] = isset($attr['style']) ? $attr['style'] : ''; + $attr['style'] = $css . $attr['style']; + } + } ?> \ No newline at end of file diff --git a/library/HTMLPurifier/TagTransform/Center.php b/library/HTMLPurifier/TagTransform/Center.php deleted file mode 100644 index 571bb9df..00000000 --- a/library/HTMLPurifier/TagTransform/Center.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -require_once 'HTMLPurifier/TagTransform.php'; - -/** - * Transforms CENTER tags into proper version (DIV with text-align CSS) - * - * Takes a CENTER tag, parses the align attribute, and then if it's valid - * assigns it to the CSS property text-align. - */ -class HTMLPurifier_TagTransform_Center extends HTMLPurifier_TagTransform -{ - var $transform_to = 'div'; - - function transform($tag, $config, &$context) { - if ($tag->type == 'end') { - $new_tag = new HTMLPurifier_Token_End($this->transform_to); - return $new_tag; - } - $attr = $tag->attr; - $prepend_css = 'text-align:center;'; - if (isset($attr['style'])) { - $attr['style'] = $prepend_css . $attr['style']; - } else { - $attr['style'] = $prepend_css; - } - $new_tag = $tag->copy(); - $new_tag->name = $this->transform_to; - $new_tag->attr = $attr; - return $new_tag; - } -} - -?> \ No newline at end of file diff --git a/library/HTMLPurifier/TagTransform/Simple.php b/library/HTMLPurifier/TagTransform/Simple.php index 6ffd0eab..f8299e11 100644 --- a/library/HTMLPurifier/TagTransform/Simple.php +++ b/library/HTMLPurifier/TagTransform/Simple.php @@ -3,21 +3,32 @@ require_once 'HTMLPurifier/TagTransform.php'; /** - * Simple transformation, just change tag name to something else. + * 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 { + var $style; + /** * @param $transform_to Tag name to transform to. + * @param $style CSS style to add to the tag */ - function HTMLPurifier_TagTransform_Simple($transform_to) { + function HTMLPurifier_TagTransform_Simple($transform_to, $style = null) { $this->transform_to = $transform_to; + $this->style = $style; } function transform($tag, $config, &$context) { $new_tag = $tag->copy(); $new_tag->name = $this->transform_to; + if (!is_null($this->style) && + ($new_tag->type == 'start' || $new_tag->type == 'empty') + ) { + $this->prependCSS($new_tag->attr, $this->style); + } return $new_tag; } diff --git a/library/HTMLPurifier/Token.php b/library/HTMLPurifier/Token.php index 555e76f1..dfcc5cbc 100644 --- a/library/HTMLPurifier/Token.php +++ b/library/HTMLPurifier/Token.php @@ -11,6 +11,13 @@ */ class HTMLPurifier_Token { var $type; /**< Type of node to bypass <tt>is_a()</tt>. @public */ + var $line; /**< Line number node was on in source document. Null if unknown. @public */ + + /** + * Lookup array of processing that this token is exempt from. + * Currently, the only valid value is "ValidateAttributes". + */ + var $armor = array(); /** * Copies the tag into a new one (clone substitute). diff --git a/maintenance/flush-htmldefinition-cache.php b/maintenance/flush-htmldefinition-cache.php new file mode 100644 index 00000000..780be373 --- /dev/null +++ b/maintenance/flush-htmldefinition-cache.php @@ -0,0 +1,24 @@ +#!/usr/bin/php +<?php + +/** + * Flushes the default HTMLDefinition serial cache + */ + +if (php_sapi_name() != 'cli') { + echo 'Script cannot be called from web-browser.'; + exit; +} + +echo 'Flushing cache... '; + +require_once(dirname(__FILE__) . '/../library/HTMLPurifier.auto.php'); + +$config = HTMLPurifier_Config::createDefault(); + +$cache = new HTMLPurifier_DefinitionCache_Serializer('HTML'); +$cache->flush($config); + +echo 'Cache flushed successfully.'; + +?> \ No newline at end of file diff --git a/maintenance/generate-entity-file.php b/maintenance/generate-entity-file.php index 283650cd..8bf34a31 100644 --- a/maintenance/generate-entity-file.php +++ b/maintenance/generate-entity-file.php @@ -6,6 +6,11 @@ * writes the whole kaboodle to a file. The resulting file should be versioned. */ +if (php_sapi_name() != 'cli') { + echo 'Script cannot be called from web-browser.'; + exit; +} + chdir( dirname(__FILE__) ); // here's where the entity files are located, assuming working directory diff --git a/release.php b/release.php deleted file mode 100644 index c3ab0d84..00000000 --- a/release.php +++ /dev/null @@ -1,82 +0,0 @@ -<?php - -// release script -// PHP 5.0 only - -if (php_sapi_name() != 'cli') { - echo 'Release script cannot be called from web-browser.'; - exit; -} - -if (!isset($argv[1])) { - echo -'php release.php [version] - HTML Purifier release script -'; - exit; -} - -$version = trim($argv[1]); - -// Bump version numbers: - -// ...in VERSION -file_put_contents('VERSION', $version); - -// ...in NEWS -$date = date('Y-m-d'); -$news_c = str_replace( - $l = "$version, unknown release date", - "$version, released $date", - file_get_contents('NEWS'), - $c -); -if (!$c) { - echo 'Could not update NEWS, missing ' . $l . PHP_EOL; - exit; -} elseif ($c > 1) { - echo 'More than one release declaration in NEWS replaced' . PHP_EOL; - exit; -} -file_put_contents('NEWS', $news_c); - -// ...in Doxyfile -$doxyfile_c = preg_replace( - '/(?<=PROJECT_NUMBER {9}= )[^\s]+/m', // brittle - $version, - file_get_contents('Doxyfile'), - 1, $c -); -if (!$c) { - echo 'Could not update Doxyfile, missing PROJECT_NUMBER.' . PHP_EOL; - exit; -} -file_put_contents('Doxyfile', $doxyfile_c); - -// ...in HTMLPurifier.php -$htmlpurifier_c = file_get_contents('library/HTMLPurifier.php'); -$htmlpurifier_c = preg_replace( - '/HTML Purifier .+? - /', - "HTML Purifier $version - ", - $htmlpurifier_c, - 1, $c -); -if (!$c) { - echo 'Could not update HTMLPurifier.php, missing HTML Purifier [version] header.' . PHP_EOL; - exit; -} -$htmlpurifier_c = preg_replace( - '/var \$version = \'.+?\';/', - "var \$version = '$version';", - $htmlpurifier_c, - 1, $c -); -if (!$c) { - echo 'Could not update HTMLPurifier.php, missing var $version.' . PHP_EOL; - exit; -} -file_put_contents('library/HTMLPurifier.php', $htmlpurifier_c); - -echo "Review changes, write something in WHATSNEW, and then SVN commit with log 'Release $version.'" . PHP_EOL; - -?> \ No newline at end of file diff --git a/release2-strict.php b/release2-strict.php new file mode 100644 index 00000000..4a8b8fd9 --- /dev/null +++ b/release2-strict.php @@ -0,0 +1,31 @@ +<?php + +// Merges in changes from trunk to strict branch +// WORKING COPY MUST BE POINTED TO STRICT BRANCH + +if (php_sapi_name() != 'cli') { + echo 'Release script cannot be called from web-browser.'; + exit; +} + +require 'svn.php'; + +$svn_info = svn_info('.'); + +$last_rev = (int) $svn_info['Last Changed Rev']; +$trunk_url = $svn_info['Repository Root'] . '/htmlpurifier/trunk'; +echo "Last revision was $last_rev, merging from $last_rev to head.\n"; + +$merge_cmd = "svn merge -r $last_rev:HEAD $trunk_url ."; +$out = explode("\n", shell_exec($merge_cmd)); + +echo "Conflicted files:\n"; +foreach ($out as $line) { + if (empty($line)) continue; + if ($line{0} === 'C' || $line{1} === 'C') echo $line . "\n"; +} + +$version = trim(file_get_contents('VERSION')); +echo "Resolve conflicts and then commit as 'Release $version, merged in $last_rev to HEAD.'"; + +?> \ No newline at end of file diff --git a/release3-tag.php b/release3-tag.php new file mode 100644 index 00000000..c6f72758 --- /dev/null +++ b/release3-tag.php @@ -0,0 +1,26 @@ +<?php + +// Tags releases + +if (php_sapi_name() != 'cli') { + echo 'Release script cannot be called from web-browser.'; + exit; +} + +require 'svn.php'; + +$svn_info = svn_info('.'); + +$version = trim(file_get_contents('VERSION')); + +$trunk_url = $svn_info['Repository Root'] . '/htmlpurifier/trunk'; +$strict_url = $svn_info['Repository Root'] . '/htmlpurifier/branches/strict'; +$trunk_tag_url = $svn_info['Repository Root'] . '/htmlpurifier/tags/' . $version; +$strict_tag_url = $svn_info['Repository Root'] . '/htmlpurifier/tags/' . $version . '-strict'; + +echo "Tagging trunk to tags/$version..."; +passthru("svn copy --message \"Tag $version release.\" $trunk_url $trunk_tag_url"); +echo "Tagging strict to tags/$version-strict..."; +passthru("svn copy --message \"Tag $version-strict release.\" $strict_url $strict_tag_url"); + +?> \ No newline at end of file diff --git a/smoketests/all.php b/smoketests/all.php index 743440be..de09412c 100644 --- a/smoketests/all.php +++ b/smoketests/all.php @@ -29,6 +29,7 @@ while (false !== ($filename = readdir($dh))) { if (strpos($filename, '.php') === false) continue; if ($filename == 'common.php') continue; if ($filename == 'all.php') continue; + if ($filename == 'testSchema.php') continue; ?> <iframe src="<?php echo escapeHTML($filename); ?>"></iframe> <?php diff --git a/smoketests/attrTransform.php b/smoketests/attrTransform.php index 7b6a4fd1..05f61813 100644 --- a/smoketests/attrTransform.php +++ b/smoketests/attrTransform.php @@ -42,6 +42,7 @@ $xml = simplexml_load_file('attrTransform.xml'); // attr transform enabled HTML Purifier $config = HTMLPurifier_Config::createDefault(); +$config->set('HTML', 'Doctype', 'XHTML 1.0 Strict'); $purifier = new HTMLPurifier($config); $title = isset($_GET['title']) ? $_GET['title'] : true; diff --git a/smoketests/configForm.php b/smoketests/configForm.php new file mode 100644 index 00000000..63385dc7 --- /dev/null +++ b/smoketests/configForm.php @@ -0,0 +1,77 @@ +<?php + +require_once 'common.php'; + +if (isset($_GET['doc'])) { + + if ( + file_exists('testSchema.html') && + filemtime('testSchema.php') < filemtime('testSchema.html') && + !isset($_GET['purge']) + ) { + echo file_get_contents('testSchema.html'); + exit; + } + + if (version_compare('5', PHP_VERSION, '>')) exit('Requires PHP 5 or higher.'); + + // setup schema for parsing + require_once 'testSchema.php'; + $new_schema = $custom_schema; // dereference the reference + HTMLPurifier_ConfigSchema::instance($old); // restore old version + + // setup ConfigDoc environment + require_once '../configdoc/library/ConfigDoc.auto.php'; + + // perform the ConfigDoc generation + $configdoc = new ConfigDoc(); + $html = $configdoc->generate($new_schema, 'plain', array( + 'css' => '../configdoc/styles/plain.css', + 'title' => 'Sample Configuration Documentation' + )); + $configdoc->cleanup(); + + file_put_contents('testSchema.html', $html); + echo $html; + + exit; +} + +?><!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> +<head> + <title>HTML Purifier Config Form Smoketest</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <link rel="stylesheet" href="../library/HTMLPurifier/Printer/ConfigForm.css" type="text/css" /> + <script defer="defer" type="text/javascript" src="../library/HTMLPurifier/Printer/ConfigForm.js"></script> +</head> +<body> +<h1>HTML Purifier Config Form Smoketest</h1> +<p>This file outputs the configuration form for every single type +of directive possible.</p> +<form id="htmlpurifier-config" name="htmlpurifier-config" method="get" action="" +style="float:right;"> +<?php + +require_once 'HTMLPurifier/Printer/ConfigForm.php'; + +// fictional set, attempts to cover every possible data-type +// see source at ConfigTest.php +require_once 'testSchema.php'; + +// cleanup ( this should be rolled into Config ) +$config = HTMLPurifier_Config::loadArrayFromForm($_GET, 'config'); +$printer = new HTMLPurifier_Printer_ConfigForm('config', '?doc#%s'); +echo $printer->render($config); + +?> +</form> +<pre> +<?php +print_r($config->getAll()); +?> +</pre> +</body> +</html> \ No newline at end of file diff --git a/smoketests/printDefinition.php b/smoketests/printDefinition.php index 2c66fa4e..02552adb 100644 --- a/smoketests/printDefinition.php +++ b/smoketests/printDefinition.php @@ -4,24 +4,15 @@ require_once 'common.php'; // load library require_once 'HTMLPurifier/Printer/HTMLDefinition.php'; require_once 'HTMLPurifier/Printer/CSSDefinition.php'; +require_once 'HTMLPurifier/Printer/ConfigForm.php'; -$config = HTMLPurifier_Config::createDefault(); +$config = HTMLPurifier_Config::loadArrayFromForm($_GET, 'config'); // you can do custom configuration! if (file_exists('printDefinition.settings.php')) { include 'printDefinition.settings.php'; } -$get = $_GET; -foreach ($_GET as $key => $value) { - if (!strncmp($key, 'Null_', 5) && !empty($value)) { - unset($get[substr($key, 5)]); - unset($get[$key]); - } -} - -@$config->loadArray($get); - /* // sample local definition, obviously needs to be less clunky $html_definition =& $config->getHTMLDefinition(true); $module = new HTMLPurifier_HTMLModule(); @@ -36,6 +27,11 @@ $html_definition->manager->addModule($module); $printer_html_definition = new HTMLPurifier_Printer_HTMLDefinition(); $printer_css_definition = new HTMLPurifier_Printer_CSSDefinition(); +$printer_config_form = new HTMLPurifier_Printer_ConfigForm( + 'config', + 'http://htmlpurifier.org/live/configdoc/plain.html#%s' +); + echo '<?xml version="1.0" encoding="UTF-8" ?>'; ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" @@ -45,9 +41,7 @@ echo '<?xml version="1.0" encoding="UTF-8" ?>'; <title>HTML Purifier Printer Smoketest</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <style type="text/css"> - form table {margin:1em auto;} - form th {text-align:right;padding-right:1em;} - form .c {display:none;} + .hp-config {margin-left:auto; margin-right:auto;} .HTMLPurifier_Printer table {border-collapse:collapse; border:1px solid #000; width:600px; margin:1em auto;font-family:sans-serif;font-size:75%;} @@ -59,11 +53,8 @@ echo '<?xml version="1.0" encoding="UTF-8" ?>'; .HTMLPurifier_Printer .heavy {background:#99C;text-align:center;} dt {font-weight:bold;} </style> - <script type="text/javascript"> - function toggleWriteability(id_of_patient, checked) { - document.getElementById(id_of_patient).disabled = checked; - } - </script> + <link rel="stylesheet" href="../library/HTMLPurifier/Printer/ConfigForm.css" type="text/css" /> + <script defer="defer" type="text/javascript" src="../library/HTMLPurifier/Printer/ConfigForm.js"></script> </head> <body> @@ -81,73 +72,10 @@ influences the internal workings of the definition objects.</p> list of items, HTML Purifier will take care of the rest (including transformation into a real array list or a lookup table).</p> -<form id="edit-config" name="edit-config" method="get" action="printDefinition.php"> -<table> +<form method="get" action="" name="hp-configform"> <?php - $directives = $config->getBatch('HTML'); - // can't handle hashes - foreach ($directives as $key => $value) { - $directive = "HTML.$key"; - if (is_array($value)) { - $keys = array_keys($value); - if ($keys === array_keys($keys)) { - $value = implode(',', $keys); - } else { - $new_value = ''; - foreach ($value as $name => $bool) { - if ($bool !== true) continue; - $new_value .= "$name,"; - } - $value = rtrim($new_value, ','); - } - } - $allow_null = $config->def->info['HTML'][$key]->allow_null; + echo $printer_config_form->render($config, 'HTML'); ?> -<tr> -<th> - <a href="http://htmlpurifier.org/live/configdoc/plain.html#<?php echo $directive ?>"> - <label for="<?php echo $directive; ?>">%<?php echo $directive; ?></label> - </a> -</th> -<?php if (is_bool($value)) { ?> -<td id="<?php echo $directive; ?>"> - <label for="Yes_<?php echo $directive; ?>"><span class="c">%<?php echo $directive; ?>:</span> Yes</label> - <input type="radio" name="<?php echo $directive; ?>" id="Yes_<?php echo $directive; ?>" value="1"<?php if ($value) { ?> checked="checked"<?php } ?> /> &nbsp; - <label for="No_<?php echo $directive; ?>"><span class="c">%<?php echo $directive; ?>:</span> No</label> - <input type="radio" name="<?php echo $directive; ?>" id="No_<?php echo $directive; ?>" value="0"<?php if (!$value) { ?> checked="checked"<?php } ?> /> -<?php } else { ?> -<td> - <?php if($allow_null) { ?> - <label for="Null_<?php echo $directive; ?>"><span class="c">%<?php echo $directive; ?>:</span> Null/Disabled*</label> - <input - type="checkbox" - value="1" - onclick="toggleWriteability('<?php echo $directive ?>',checked)" - name="Null_<?php echo $directive; ?>" - id="Null_<?php echo $directive; ?>" - <?php if ($value === null) { ?> checked="checked"<?php } ?> - /> or <br /> - <?php } ?> - <input - type="text" - name="<?php echo $directive; ?>" - id="<?php echo $directive; ?>" - value="<?php echo escapeHTML($value); ?>" - <?php if($value === null) {echo 'disabled="disabled"';} ?> - /> -<?php } ?> -</td> -</tr> -<?php - } -?> -<tr> - <td colspan="2" style="text-align:right;"> - [<a href="printDefinition.php">Reset</a>] - <input type="submit" value="Submit" /> - </td> -</tr> -</table> <p>* Some configuration directives make a distinction between an empty variable and a null variable. A whitelist, for example, will take an empty array as meaning <em>no</em> allowed elements, while checking diff --git a/smoketests/testSchema.php b/smoketests/testSchema.php new file mode 100644 index 00000000..1295566e --- /dev/null +++ b/smoketests/testSchema.php @@ -0,0 +1,44 @@ +<?php + +// overload default configuration schema temporarily +$custom_schema = new HTMLPurifier_ConfigSchema(); +$old = HTMLPurifier_ConfigSchema::instance(); +$custom_schema =& HTMLPurifier_ConfigSchema::instance($custom_schema); + +if (!class_exists('CS')) { + class CS extends HTMLPurifier_ConfigSchema {} +} + +CS::defineNamespace('Element', 'Chemical substances that cannot be further decomposed'); + +CS::define('Element', 'Abbr', 'H', 'string', 'Abbreviation of element name.'); +CS::define('Element', 'Name', 'hydrogen', 'istring', 'Full name of atoms.'); +CS::define('Element', 'Number', 1, 'int', 'Atomic number, is identity.'); +CS::define('Element', 'Mass', 1.00794, 'float', 'Atomic mass.'); +CS::define('Element', 'Radioactive', false, 'bool', 'Does it have rapid decay?'); +CS::define('Element', 'Isotopes', array('1' => true, '2' => true, '3' => true), 'lookup', + 'What numbers of neutrons for this element have been observed?'); +CS::define('Element', 'Traits', array('nonmetallic', 'odorless', 'flammable'), 'list', + 'What are general properties of the element?'); +CS::define('Element', 'IsotopeNames', array('1' => 'protium', '2' => 'deuterium', '3' => 'tritium'), 'hash', + 'Lookup hash of neutron counts to formal names.'); + +CS::defineNamespace('Instrument', 'Of the musical type.'); + +CS::define('Instrument', 'Manufacturer', 'Yamaha', 'string', 'Who made it?'); +CS::defineAllowedValues('Instrument', 'Manufacturer', array( + 'Yamaha', 'Conn-Selmer', 'Vandoren', 'Laubin', 'Buffet', 'other')); +CS::defineValueAliases('Instrument', 'Manufacturer', array( + 'Selmer' => 'Conn-Selmer')); + +CS::define('Instrument', 'Family', 'woodwind', 'istring', 'What family is it?'); +CS::defineAllowedValues('Instrument', 'Family', array( + 'brass', 'woodwind', 'percussion', 'string', 'keyboard', 'electronic')); +CS::defineValueAliases('Instrument', 'Family', array( + 'synth' => 'electronic')); + +CS::defineNamespace('ReportCard', 'It is for grades.'); +CS::define('ReportCard', 'English', null, 'string/null', 'Grade from English class.'); +CS::define('ReportCard', 'Absences', 0, 'int', 'How many times missing from school?'); + +?> \ No newline at end of file diff --git a/svn.php b/svn.php new file mode 100644 index 00000000..db588754 --- /dev/null +++ b/svn.php @@ -0,0 +1,14 @@ +<?php + +function svn_info($dir) { + $raw = explode("\n", shell_exec("svn info $dir")); + $svn_info = array(); + foreach ($raw as $r) { + if (empty($r)) continue; + list($k, $v) = explode(': ', $r, 2); + $svn_info[$k] = $v; + } + return $svn_info; +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/AttrCollectionsTest.php b/tests/HTMLPurifier/AttrCollectionsTest.php new file mode 100644 index 00000000..632f75ed --- /dev/null +++ b/tests/HTMLPurifier/AttrCollectionsTest.php @@ -0,0 +1,139 @@ +<?php + +require_once 'HTMLPurifier/AttrCollections.php'; + +class HTMLPurifier_AttrCollectionsTest_NoConstructor extends HTMLPurifier_AttrCollections +{ + function HTMLPurifier_AttrCollectionsTest_NoConstructor() {} + function expandIdentifiers(&$a, $b) {} + function performInclusions(&$a) {} +} + +class HTMLPurifier_AttrCollectionsTest extends UnitTestCase +{ + + function testConstruction() { + + generate_mock_once('HTMLPurifier_AttrTypes'); + + $collections = new HTMLPurifier_AttrCollectionsTest_NoConstructor(); + + $types = new HTMLPurifier_AttrTypesMock($this); + + $modules = array(); + + $modules['Module1'] = new HTMLPurifier_HTMLModule(); + $modules['Module1']->attr_collections = array( + 'Core' => array( + 0 => array('Soup', 'Undefined'), + 'attribute' => 'Type', + 'attribute-2' => 'Type2', + ), + 'Soup' => array( + 'attribute-3' => 'Type3-old' // overwritten + ) + ); + + $modules['Module2'] = new HTMLPurifier_HTMLModule(); + $modules['Module2']->attr_collections = array( + 'Core' => array( + 0 => array('Brocolli') + ), + 'Soup' => array( + 'attribute-3' => 'Type3' + ), + 'Brocolli' => array() + ); + + $collections->HTMLPurifier_AttrCollections($types, $modules); + // this is without identifier expansion or inclusions + $this->assertIdentical( + $collections->info, + array( + 'Core' => array( + 0 => array('Soup', 'Undefined', 'Brocolli'), + 'attribute' => 'Type', + 'attribute-2' => 'Type2' + ), + 'Soup' => array( + 'attribute-3' => 'Type3' + ), + 'Brocolli' => array() + ) + ); + + } + + function test_performInclusions() { + + generate_mock_once('HTMLPurifier_AttrTypes'); + + $types = new HTMLPurifier_AttrTypesMock($this); + $collections = new HTMLPurifier_AttrCollections($types, array()); + $collections->info = array( + 'Core' => array(0 => array('Inclusion', 'Undefined'), 'attr-original' => 'Type'), + 'Inclusion' => array(0 => array('SubInclusion'), 'attr' => 'Type'), + 'SubInclusion' => array('attr2' => 'Type') + ); + + $collections->performInclusions($collections->info['Core']); + $this->assertIdentical( + $collections->info['Core'], + array( + 'attr-original' => 'Type', + 'attr' => 'Type', + 'attr2' => 'Type' + ) + ); + + // test recursive + $collections->info = array( + 'One' => array(0 => array('Two'), 'one' => 'Type'), + 'Two' => array(0 => array('One'), 'two' => 'Type') + ); + $collections->performInclusions($collections->info['One']); + $this->assertIdentical( + $collections->info['One'], + array( + 'one' => 'Type', + 'two' => 'Type' + ) + ); + + } + + function test_expandIdentifiers() { + + generate_mock_once('HTMLPurifier_AttrTypes'); + + $types = new HTMLPurifier_AttrTypesMock($this); + $collections = new HTMLPurifier_AttrCollections($types, array()); + + $attr = array( + 'attr1' => 'Color', + 'attr2*' => 'URI' + ); + $c_object = new HTMLPurifier_AttrDef(); + $c_object->_name = 'Color'; // for testing purposes only + $u_object = new HTMLPurifier_AttrDef(); + $u_object->_name = 'URL'; // for testing purposes only + + $types->setReturnValue('get', $c_object, array('Color')); + $types->setReturnValue('get', $u_object, array('URI')); + + $collections->expandIdentifiers($attr, $types); + + $u_object->required = true; + $this->assertIdentical( + $attr, + array( + 'attr1' => $c_object, + 'attr2' => $u_object + ) + ); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/AttrDef/CSS/BorderTest.php b/tests/HTMLPurifier/AttrDef/CSS/BorderTest.php index 521588db..6e130e46 100644 --- a/tests/HTMLPurifier/AttrDef/CSS/BorderTest.php +++ b/tests/HTMLPurifier/AttrDef/CSS/BorderTest.php @@ -10,9 +10,9 @@ class HTMLPurifier_AttrDef_CSS_BorderTest extends HTMLPurifier_AttrDefHarness $config = HTMLPurifier_Config::createDefault(); $this->def = new HTMLPurifier_AttrDef_CSS_Border($config); - $this->assertDef('thick solid red', 'thick solid #F00'); + $this->assertDef('thick solid red', 'thick solid #FF0000'); $this->assertDef('thick solid'); - $this->assertDef('solid red', 'solid #F00'); + $this->assertDef('solid red', 'solid #FF0000'); $this->assertDef('1px solid #000'); } diff --git a/tests/HTMLPurifier/AttrDef/CSS/ColorTest.php b/tests/HTMLPurifier/AttrDef/CSS/ColorTest.php index 1c29ae68..89afd121 100644 --- a/tests/HTMLPurifier/AttrDef/CSS/ColorTest.php +++ b/tests/HTMLPurifier/AttrDef/CSS/ColorTest.php @@ -25,7 +25,7 @@ class HTMLPurifier_AttrDef_CSS_ColorTest extends HTMLPurifier_AttrDefHarness $this->assertDef('rgb(256,-23,34)', 'rgb(255,0,34)'); // color keywords, of course - $this->assertDef('red', '#F00'); + $this->assertDef('red', '#FF0000'); // maybe hex transformations would be another nice feature // at the very least transform rgb percent to rgb integer diff --git a/tests/HTMLPurifier/AttrDef/EnumTest.php b/tests/HTMLPurifier/AttrDef/EnumTest.php index 2842b214..fe405d85 100644 --- a/tests/HTMLPurifier/AttrDef/EnumTest.php +++ b/tests/HTMLPurifier/AttrDef/EnumTest.php @@ -7,29 +7,32 @@ class HTMLPurifier_AttrDef_EnumTest extends HTMLPurifier_AttrDefHarness { function testCaseInsensitive() { - $this->def = new HTMLPurifier_AttrDef_Enum(array('one', 'two')); - $this->assertDef('one'); $this->assertDef('ONE', 'one'); - } function testCaseSensitive() { - $this->def = new HTMLPurifier_AttrDef_Enum(array('one', 'two'), true); - $this->assertDef('one'); $this->assertDef('ONE', false); - } function testFixing() { - $this->def = new HTMLPurifier_AttrDef_Enum(array('one')); - $this->assertDef(' one ', 'one'); + } + + function test_make() { + $factory = new HTMLPurifier_AttrDef_Enum(); + $def = $factory->make('foo,bar'); + $def2 = new HTMLPurifier_AttrDef_Enum(array('foo', 'bar')); + $this->assertIdentical($def, $def2); + + $def = $factory->make('s:foo,BAR'); + $def2 = new HTMLPurifier_AttrDef_Enum(array('foo', 'BAR'), true); + $this->assertIdentical($def, $def2); } } diff --git a/tests/HTMLPurifier/AttrDef/HTML/BoolTest.php b/tests/HTMLPurifier/AttrDef/HTML/BoolTest.php new file mode 100644 index 00000000..47a31fc3 --- /dev/null +++ b/tests/HTMLPurifier/AttrDef/HTML/BoolTest.php @@ -0,0 +1,25 @@ +<?php + +require_once 'HTMLPurifier/AttrDefHarness.php'; +require_once 'HTMLPurifier/AttrDef/HTML/Bool.php'; + +class HTMLPurifier_AttrDef_HTML_BoolTest extends HTMLPurifier_AttrDefHarness +{ + + function test() { + $this->def = new HTMLPurifier_AttrDef_HTML_Bool('foo'); + $this->assertDef('foo'); + $this->assertDef('', false); + $this->assertDef('bar', false); + } + + function test_make() { + $factory = new HTMLPurifier_AttrDef_HTML_Bool(); + $def = $factory->make('foo'); + $def2 = new HTMLPurifier_AttrDef_HTML_Bool('foo'); + $this->assertIdentical($def, $def2); + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/AttrDef/HTML/ColorTest.php b/tests/HTMLPurifier/AttrDef/HTML/ColorTest.php new file mode 100644 index 00000000..39bd80db --- /dev/null +++ b/tests/HTMLPurifier/AttrDef/HTML/ColorTest.php @@ -0,0 +1,23 @@ +<?php + +require_once 'HTMLPurifier/AttrDefHarness.php'; +require_once 'HTMLPurifier/AttrDef/HTML/Color.php'; + +class HTMLPurifier_AttrDef_HTML_ColorTest extends HTMLPurifier_AttrDefHarness +{ + + function test() { + $this->def = new HTMLPurifier_AttrDef_HTML_Color(); + $this->assertDef('', false); + $this->assertDef('foo', false); + $this->assertDef('43', false); + $this->assertDef('red', '#FF0000'); + $this->assertDef('#FF0000'); + $this->assertDef('#453443'); + $this->assertDef('453443', '#453443'); + $this->assertDef('#345', '#334455'); + $this->assertDef('120', '#112200'); + } +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/AttrDef/HTML/IDTest.php b/tests/HTMLPurifier/AttrDef/HTML/IDTest.php index 98fffbba..add764fd 100644 --- a/tests/HTMLPurifier/AttrDef/HTML/IDTest.php +++ b/tests/HTMLPurifier/AttrDef/HTML/IDTest.php @@ -66,9 +66,12 @@ class HTMLPurifier_AttrDef_HTML_IDTest extends HTMLPurifier_AttrDefHarness $this->assertDef('user_story95_alas'); $this->assertDef('user_alas', 'user_story95_user_alas'); // ! - + } + + function testLocalPrefixWithoutMainPrefix() { // no effect when IDPrefix isn't set $this->config->set('Attr', 'IDPrefix', ''); + $this->config->set('Attr', 'IDPrefixLocal', 'story95_'); $this->expectError('%Attr.IDPrefixLocal cannot be used unless '. '%Attr.IDPrefix is set'); $this->assertDef('amherst'); diff --git a/tests/HTMLPurifier/AttrDef/URITest.php b/tests/HTMLPurifier/AttrDef/URITest.php index f9a9ab41..e6aab057 100644 --- a/tests/HTMLPurifier/AttrDef/URITest.php +++ b/tests/HTMLPurifier/AttrDef/URITest.php @@ -209,17 +209,17 @@ class HTMLPurifier_AttrDef_URITest extends HTMLPurifier_AttrDefHarness $this->scheme = new HTMLPurifier_URISchemeMock($this); // here are the schemes we will support with overloaded mocks - $registry->setReturnReference('getScheme', $this->scheme, array('http', $this->config, $this->context)); - $registry->setReturnReference('getScheme', $this->scheme, array('mailto', $this->config, $this->context)); + $registry->setReturnReference('getScheme', $this->scheme, array('http', '*', '*')); + $registry->setReturnReference('getScheme', $this->scheme, array('mailto', '*', '*')); // default return value is false (meaning no scheme defined: reject) - $registry->setReturnValue('getScheme', false, array('*', $this->config, $this->context)); + $registry->setReturnValue('getScheme', false, array('*', '*', '*')); if ($this->components === false) { $this->scheme->expectNever('validateComponents'); } else { - $this->components[] = $this->config; // append the configuration - $this->components[] =& $this->context; // append context + $this->components[] = '*'; // append the configuration + $this->components[] = '*'; // append context $this->scheme->setReturnValue( 'validateComponents', $this->return_components, $this->components); $this->scheme->expectOnce('validateComponents', $this->components); @@ -247,13 +247,10 @@ class HTMLPurifier_AttrDef_URITest extends HTMLPurifier_AttrDefHarness $this->def = new HTMLPurifier_AttrDef_URI(); $this->config->set('URI', 'DisableExternal', true); + $this->config->set('URI', 'Host', 'sub.example.com'); $this->assertDef('/foobar.txt'); $this->assertDef('http://google.com/', false); - $this->assertDef('http://sub.example.com/alas?foo=asd', false); - - $this->config->set('URI', 'Host', 'sub.example.com'); - $this->assertDef('http://sub.example.com/alas?foo=asd'); $this->assertDef('http://example.com/teehee', false); $this->assertDef('http://www.example.com/#man', false); @@ -328,4 +325,4 @@ class HTMLPurifier_AttrDef_URITest extends HTMLPurifier_AttrDefHarness } -?> \ No newline at end of file +?> diff --git a/tests/HTMLPurifier/AttrDefTest.php b/tests/HTMLPurifier/AttrDefTest.php index 1edb3747..a8963bc4 100644 --- a/tests/HTMLPurifier/AttrDefTest.php +++ b/tests/HTMLPurifier/AttrDefTest.php @@ -17,6 +17,14 @@ class HTMLPurifier_AttrDefTest extends UnitTestCase } + function test_make() { + + $def = new HTMLPurifier_AttrDef(); + $def2 = $def->make(''); + $this->assertIdentical($def, $def2); + + } + } ?> \ No newline at end of file diff --git a/tests/HTMLPurifier/AttrTransform/ImgRequiredTest.php b/tests/HTMLPurifier/AttrTransform/ImgRequiredTest.php index dff045ee..a9ad9a8c 100644 --- a/tests/HTMLPurifier/AttrTransform/ImgRequiredTest.php +++ b/tests/HTMLPurifier/AttrTransform/ImgRequiredTest.php @@ -15,7 +15,10 @@ class HTMLPurifier_AttrTransform_ImgRequiredTest extends HTMLPurifier_AttrTransf $this->assertResult( array(), - array('src' => '', 'alt' => 'Invalid image') + array('src' => '', 'alt' => 'Invalid image'), + array( + 'Core.RemoveInvalidImg' => false + ) ); $this->assertResult( @@ -23,7 +26,8 @@ class HTMLPurifier_AttrTransform_ImgRequiredTest extends HTMLPurifier_AttrTransf array('src' => 'blank.png', 'alt' => 'Pawned!'), array( 'Attr.DefaultInvalidImage' => 'blank.png', - 'Attr.DefaultInvalidImageAlt' => 'Pawned!' + 'Attr.DefaultInvalidImageAlt' => 'Pawned!', + 'Core.RemoveInvalidImg' => false ) ); @@ -34,7 +38,10 @@ class HTMLPurifier_AttrTransform_ImgRequiredTest extends HTMLPurifier_AttrTransf $this->assertResult( array('alt' => 'intrigue'), - array('alt' => 'intrigue', 'src' => '') + array('alt' => 'intrigue', 'src' => ''), + array( + 'Core.RemoveInvalidImg' => false + ) ); } diff --git a/tests/HTMLPurifier/AttrTypesTest.php b/tests/HTMLPurifier/AttrTypesTest.php new file mode 100644 index 00000000..9d919a83 --- /dev/null +++ b/tests/HTMLPurifier/AttrTypesTest.php @@ -0,0 +1,28 @@ +<?php + +require_once 'HTMLPurifier/AttrTypes.php'; + +class HTMLPurifier_AttrTypesTest extends UnitTestCase +{ + + function test_get() { + $types = new HTMLPurifier_AttrTypes(); + + $this->assertIdentical( + $types->get('CDATA'), + $types->info['CDATA'] + ); + + $this->expectError('Cannot retrieve undefined attribute type foobar'); + $types->get('foobar'); + + $this->assertIdentical( + $types->get('Enum#foo,bar'), + new HTMLPurifier_AttrDef_Enum(array('foo', 'bar')) + ); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/ChildDef/CustomTest.php b/tests/HTMLPurifier/ChildDef/CustomTest.php index 905c9e02..e4f00a62 100644 --- a/tests/HTMLPurifier/ChildDef/CustomTest.php +++ b/tests/HTMLPurifier/ChildDef/CustomTest.php @@ -19,6 +19,49 @@ class HTMLPurifier_ChildDef_CustomTest extends HTMLPurifier_ChildDefHarness } + function testNesting() { + $this->obj = new HTMLPurifier_ChildDef_Custom('(a,b,(c|d))+'); + $this->assertResult('', false); + $this->assertResult('<a /><b /><c /><a /><b /><d />'); + $this->assertResult('<a /><b /><c /><d />', false); + } + + function testNestedEitherOr() { + $this->obj = new HTMLPurifier_ChildDef_Custom('b,(a|(c|d))+'); + $this->assertResult('', false); + $this->assertResult('<b /><a /><c /><d />'); + $this->assertResult('<b /><d /><a /><a />'); + $this->assertResult('<b /><a />'); + $this->assertResult('<acd />', false); + } + + function testNestedQuantifier() { + $this->obj = new HTMLPurifier_ChildDef_Custom('(b,c+)*'); + $this->assertResult(''); + $this->assertResult('<b /><c />'); + $this->assertResult('<b /><c /><c /><c />'); + $this->assertResult('<b /><c /><b /><c />'); + $this->assertResult('<b /><c /><b />', false); + } + + function testEitherOr() { + + $this->obj = new HTMLPurifier_ChildDef_Custom('a|b'); + $this->assertResult('', false); + $this->assertResult('<a />'); + $this->assertResult('<b />'); + $this->assertResult('<a /><b />', false); + + } + + function testCommafication() { + + $this->obj = new HTMLPurifier_ChildDef_Custom('a,b'); + $this->assertResult('<a /><b />'); + $this->assertResult('<ab />', false); + + } + } ?> \ No newline at end of file diff --git a/tests/HTMLPurifier/ChildDef/StrictBlockquoteTest.php b/tests/HTMLPurifier/ChildDef/StrictBlockquoteTest.php index 56405e91..76113bcd 100644 --- a/tests/HTMLPurifier/ChildDef/StrictBlockquoteTest.php +++ b/tests/HTMLPurifier/ChildDef/StrictBlockquoteTest.php @@ -11,10 +11,13 @@ extends HTMLPurifier_ChildDefHarness $this->obj = new HTMLPurifier_ChildDef_StrictBlockquote('div | p'); + // assuming default wrap is p + $this->assertResult(''); $this->assertResult('<p>Valid</p>'); $this->assertResult('<div>Still valid</div>'); $this->assertResult('Needs wrap', '<p>Needs wrap</p>'); + $this->assertResult('<p>Do not wrap</p> <p>Whitespace</p>'); $this->assertResult( 'Wrap'. '<p>Do not wrap</p>', '<p>Wrap</p><p>Do not wrap</p>' @@ -35,6 +38,7 @@ extends HTMLPurifier_ChildDefHarness '<foo>Bar</foo><bas /><b>People</b>Conniving.'. '<p>Fools!</p>', '<p>Bar'. '<b>People</b>Conniving.</p><p>Fools!</p>' ); + $this->assertResult('Needs wrap', '<div>Needs wrap</div>', array('HTML.BlockWrapper' => 'div')); diff --git a/tests/HTMLPurifier/ConfigSchemaTest.php b/tests/HTMLPurifier/ConfigSchemaTest.php index 8a20988f..c743606f 100644 --- a/tests/HTMLPurifier/ConfigSchemaTest.php +++ b/tests/HTMLPurifier/ConfigSchemaTest.php @@ -288,6 +288,10 @@ class HTMLPurifier_ConfigSchemaTest extends UnitTestCase $this->assertValid(array(1 => 'moo'), 'hash'); $this->assertInvalid(array(0 => 'moo'), 'hash'); $this->assertValid('', 'hash', array()); + $this->assertValid('foo:bar,too:two', 'hash', array('foo' => 'bar', 'too' => 'two')); + $this->assertValid('foo:bar,too', 'hash', array('foo' => 'bar')); + $this->assertValid('foo:bar,', 'hash', array('foo' => 'bar')); + $this->assertValid('foo:bar:baz', 'hash', array('foo' => 'bar:baz')); $this->assertValid(23, 'mixed'); diff --git a/tests/HTMLPurifier/ConfigTest-finalize.ini b/tests/HTMLPurifier/ConfigTest-finalize.ini new file mode 100644 index 00000000..81720463 --- /dev/null +++ b/tests/HTMLPurifier/ConfigTest-finalize.ini @@ -0,0 +1,2 @@ +[Poem] +Meter = alexandrine \ No newline at end of file diff --git a/tests/HTMLPurifier/ConfigTest.php b/tests/HTMLPurifier/ConfigTest.php index 69bbf81b..5387a3c4 100644 --- a/tests/HTMLPurifier/ConfigTest.php +++ b/tests/HTMLPurifier/ConfigTest.php @@ -42,6 +42,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase CS::define('Element', 'Object', new stdClass(), 'mixed', 'Model representation.'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; // test default value retrieval $this->assertIdentical($config->get('Element', 'Abbr'), 'H'); @@ -65,6 +66,12 @@ class HTMLPurifier_ConfigTest extends UnitTestCase $config->set('Element', 'IsotopeNames', array(238 => 'Plutonium-238', 239 => 'Plutonium-239')); $config->set('Element', 'Object', false); // unmodeled + $this->expectError('Cannot set undefined directive Element.Metal to value'); + $config->set('Element', 'Metal', true); + + $this->expectError('Value for Element.Radioactive is of invalid type, should be bool'); + $config->set('Element', 'Radioactive', 'very'); + // test value retrieval $this->assertIdentical($config->get('Element', 'Abbr'), 'Pu'); $this->assertIdentical($config->get('Element', 'Name'), 'plutonium'); @@ -76,17 +83,9 @@ class HTMLPurifier_ConfigTest extends UnitTestCase $this->assertIdentical($config->get('Element', 'IsotopeNames'), array(238 => 'Plutonium-238', 239 => 'Plutonium-239')); $this->assertIdentical($config->get('Element', 'Object'), false); - // errors - - $this->expectError('Cannot retrieve value of undefined directive'); + $this->expectError('Cannot retrieve value of undefined directive Element.Metal'); $config->get('Element', 'Metal'); - $this->expectError('Cannot set undefined directive to value'); - $config->set('Element', 'Metal', true); - - $this->expectError('Value is of invalid type'); - $config->set('Element', 'Radioactive', 'very'); - } function testEnumerated() { @@ -108,6 +107,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase 'synth' => 'electronic')); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; // case sensitive @@ -117,7 +117,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase $config->set('Instrument', 'Manufacturer', 'Selmer'); $this->assertIdentical($config->get('Instrument', 'Manufacturer'), 'Conn-Selmer'); - $this->expectError('Value not supported'); + $this->expectError('Value not supported, valid values are: Yamaha, Conn-Selmer, Vandoren, Laubin, Buffet, other'); $config->set('Instrument', 'Manufacturer', 'buffet'); // case insensitive @@ -143,6 +143,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase CS::define('ReportCard', 'Absences', 0, 'int', 'How many times missing from school?'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; $config->set('ReportCard', 'English', 'B-'); $this->assertIdentical($config->get('ReportCard', 'English'), 'B-'); @@ -151,22 +152,23 @@ class HTMLPurifier_ConfigTest extends UnitTestCase $this->assertIdentical($config->get('ReportCard', 'English'), null); // error - $this->expectError('Value is of invalid type'); + $this->expectError('Value for ReportCard.Absences is of invalid type, should be int'); $config->set('ReportCard', 'Absences', null); } function testAliases() { - HTMLPurifier_ConfigSchema::defineNamespace('Home', 'Sweet home.'); - HTMLPurifier_ConfigSchema::define('Home', 'Rug', 3, 'int', 'ID.'); - HTMLPurifier_ConfigSchema::defineAlias('Home', 'Carpet', 'Home', 'Rug'); + CS::defineNamespace('Home', 'Sweet home.'); + CS::define('Home', 'Rug', 3, 'int', 'ID.'); + CS::defineAlias('Home', 'Carpet', 'Home', 'Rug'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; $this->assertIdentical($config->get('Home', 'Rug'), 3); - $this->expectError('Cannot get value from aliased directive, use real name'); + $this->expectError('Cannot get value from aliased directive, use real name Home.Rug'); $config->get('Home', 'Carpet'); $config->set('Home', 'Carpet', 999); @@ -183,6 +185,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase CS::define('Variables', 'AngularAcceleration', 'alpha', 'string', 'In rad/s^2'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; // grab a namespace $this->assertIdentical( @@ -194,7 +197,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase ); // grab a non-existant namespace - $this->expectError('Cannot retrieve undefined namespace'); + $this->expectError('Cannot retrieve undefined namespace Constants'); $config->getBatch('Constants'); } @@ -207,6 +210,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase CS::define('Shortcut', 'Cut', 'x', 'istring', 'Cut text'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; $config->loadIni(dirname(__FILE__) . '/ConfigTest-loadIni.ini'); @@ -224,24 +228,30 @@ class HTMLPurifier_ConfigTest extends UnitTestCase $this->old_copy = HTMLPurifier_ConfigSchema::instance($this->old_copy); $config = HTMLPurifier_Config::createDefault(); + $config->set('HTML', 'Doctype', 'XHTML 1.0 Strict'); + $config->autoFinalize = false; $def = $config->getCSSDefinition(); $this->assertIsA($def, 'HTMLPurifier_CSSDefinition'); - $def = $config->getHTMLDefinition(); - $def2 = $config->getHTMLDefinition(); + $def =& $config->getHTMLDefinition(); + $def2 =& $config->getHTMLDefinition(); $this->assertIsA($def, 'HTMLPurifier_HTMLDefinition'); - $this->assertIdentical($def, $def2); + $this->assertReference($def, $def2); $this->assertTrue($def->setup); // test re-calculation if HTML changes - $config->set('HTML', 'Strict', true); + unset($def, $def2); + $def2 = $config->getHTMLDefinition(); // forcibly de-reference + + $config->set('HTML', 'Doctype', 'HTML 4.01 Transitional'); $def = $config->getHTMLDefinition(); $this->assertIsA($def, 'HTMLPurifier_HTMLDefinition'); $this->assertNotEqual($def, $def2); $this->assertTrue($def->setup); // test retrieval of raw definition + $config->set('HTML', 'DefinitionID', 'HTMLPurifier_ConfigTest->test_getHTMLDefinition()'); $def =& $config->getHTMLDefinition(true); $this->assertNotEqual($def, $def2); $this->assertFalse($def->setup); @@ -252,22 +262,36 @@ class HTMLPurifier_ConfigTest extends UnitTestCase } + function test_getHTMLDefinition_rawError() { + $this->old_copy = HTMLPurifier_ConfigSchema::instance($this->old_copy); + $config = HTMLPurifier_Config::createDefault(); + $this->expectError('Cannot retrieve raw version without specifying %HTML.DefinitionID'); + $def =& $config->getHTMLDefinition(true); + } + function test_getCSSDefinition() { $this->old_copy = HTMLPurifier_ConfigSchema::instance($this->old_copy); - $config = HTMLPurifier_Config::createDefault(); - $def = $config->getCSSDefinition(); $this->assertIsA($def, 'HTMLPurifier_CSSDefinition'); } + function test_getDefinition() { + CS::defineNamespace('Core', 'Core stuff'); + CS::define('Core', 'DefinitionCache', null, 'string/null', 'Cache?'); + CS::defineNamespace('Crust', 'Krusty Krabs'); + $config = HTMLPurifier_Config::createDefault(); + $this->expectError("Definition of Crust type not supported"); + $config->getDefinition('Crust'); + } + function test_loadArray() { // setup a few dummy namespaces/directives for our testing - HTMLPurifier_ConfigSchema::defineNamespace('Zoo', 'Animals we have.'); - HTMLPurifier_ConfigSchema::define('Zoo', 'Aadvark', 0, 'int', 'Have?'); - HTMLPurifier_ConfigSchema::define('Zoo', 'Boar', 0, 'int', 'Have?'); - HTMLPurifier_ConfigSchema::define('Zoo', 'Camel', 0, 'int', 'Have?'); - HTMLPurifier_ConfigSchema::define( + CS::defineNamespace('Zoo', 'Animals we have.'); + CS::define('Zoo', 'Aadvark', 0, 'int', 'Have?'); + CS::define('Zoo', 'Boar', 0, 'int', 'Have?'); + CS::define('Zoo', 'Camel', 0, 'int', 'Have?'); + CS::define( 'Zoo', 'Others', array(), 'list', 'Other animals we have one of.' ); @@ -305,9 +329,9 @@ class HTMLPurifier_ConfigTest extends UnitTestCase function test_create() { - HTMLPurifier_ConfigSchema::defineNamespace('Cake', 'Properties of it.'); - HTMLPurifier_ConfigSchema::define('Cake', 'Sprinkles', 666, 'int', 'Number of.'); - HTMLPurifier_ConfigSchema::define('Cake', 'Flavor', 'vanilla', 'string', 'Flavor of the batter.'); + CS::defineNamespace('Cake', 'Properties of it.'); + CS::define('Cake', 'Sprinkles', 666, 'int', 'Number of.'); + CS::define('Cake', 'Flavor', 'vanilla', 'string', 'Flavor of the batter.'); $config = HTMLPurifier_Config::createDefault(); $config->set('Cake', 'Sprinkles', 42); @@ -326,6 +350,31 @@ class HTMLPurifier_ConfigTest extends UnitTestCase } + function test_finalize() { + + // test finalization + + CS::defineNamespace('Poem', 'Violets are red, roses are blue...'); + CS::define('Poem', 'Meter', 'iambic', 'string', 'Rhythm of poem.'); + + $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; + + $config->set('Poem', 'Meter', 'irregular'); + + $config->finalize(); + + $this->expectError('Cannot set directive after finalization'); + $config->set('Poem', 'Meter', 'vedic'); + + $this->expectError('Cannot load directives after finalization'); + $config->loadArray(array('Poem.Meter' => 'octosyllable')); + + $this->expectError('Cannot load directives after finalization'); + $config->loadIni(dirname(__FILE__) . '/ConfigTest-finalize.ini'); + + } + } ?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCache/Decorator/CleanupTest.php b/tests/HTMLPurifier/DefinitionCache/Decorator/CleanupTest.php new file mode 100644 index 00000000..758a4cb8 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCache/Decorator/CleanupTest.php @@ -0,0 +1,59 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache/DecoratorHarness.php'; +require_once 'HTMLPurifier/DefinitionCache/Decorator/Cleanup.php'; + +generate_mock_once('HTMLPurifier_DefinitionCache'); + +class HTMLPurifier_DefinitionCache_Decorator_CleanupTest extends HTMLPurifier_DefinitionCache_DecoratorHarness +{ + + function setup() { + $this->cache = new HTMLPurifier_DefinitionCache_Decorator_Cleanup(); + parent::setup(); + } + + function setupMockForSuccess($op) { + $this->mock->expectOnce($op, array($this->def, $this->config)); + $this->mock->setReturnValue($op, true, array($this->def, $this->config)); + $this->mock->expectNever('cleanup'); + } + + function setupMockForFailure($op) { + $this->mock->expectOnce($op, array($this->def, $this->config)); + $this->mock->setReturnValue($op, false, array($this->def, $this->config)); + $this->mock->expectOnce('cleanup', array($this->config)); + } + + function test_get() { + $this->mock->expectOnce('get', array($this->config)); + $this->mock->setReturnValue('get', true, array($this->config)); + $this->mock->expectNever('cleanup'); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function test_get_failure() { + $this->mock->expectOnce('get', array($this->config)); + $this->mock->setReturnValue('get', false, array($this->config)); + $this->mock->expectOnce('cleanup', array($this->config)); + $this->assertEqual($this->cache->get($this->config), false); + } + + function test_set() { + $this->setupMockForSuccess('set'); + $this->assertEqual($this->cache->set($this->def, $this->config), true); + } + + function test_replace() { + $this->setupMockForSuccess('replace'); + $this->assertEqual($this->cache->replace($this->def, $this->config), true); + } + + function test_add() { + $this->setupMockForSuccess('add'); + $this->assertEqual($this->cache->add($this->def, $this->config), true); + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCache/Decorator/MemoryTest.php b/tests/HTMLPurifier/DefinitionCache/Decorator/MemoryTest.php new file mode 100644 index 00000000..b7f89c56 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCache/Decorator/MemoryTest.php @@ -0,0 +1,73 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache/DecoratorHarness.php'; +require_once 'HTMLPurifier/DefinitionCache/Decorator/Memory.php'; + +generate_mock_once('HTMLPurifier_DefinitionCache'); + +class HTMLPurifier_DefinitionCache_Decorator_MemoryTest extends HTMLPurifier_DefinitionCache_DecoratorHarness +{ + + function setup() { + $this->cache = new HTMLPurifier_DefinitionCache_Decorator_Memory(); + parent::setup(); + } + + function setupMockForSuccess($op) { + $this->mock->expectOnce($op, array($this->def, $this->config)); + $this->mock->setReturnValue($op, true, array($this->def, $this->config)); + $this->mock->expectNever('get'); + } + + function setupMockForFailure($op) { + $this->mock->expectOnce($op, array($this->def, $this->config)); + $this->mock->setReturnValue($op, false, array($this->def, $this->config)); + $this->mock->expectOnce('get', array($this->config)); + } + + function test_get() { + $this->mock->expectOnce('get', array($this->config)); // only ONE call! + $this->mock->setReturnValue('get', $this->def, array($this->config)); + $this->assertEqual($this->cache->get($this->config), $this->def); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function test_set() { + $this->setupMockForSuccess('set', 'get'); + $this->assertEqual($this->cache->set($this->def, $this->config), true); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function test_set_failure() { + $this->setupMockForFailure('set', 'get'); + $this->assertEqual($this->cache->set($this->def, $this->config), false); + $this->cache->get($this->config); + } + + function test_replace() { + $this->setupMockForSuccess('replace', 'get'); + $this->assertEqual($this->cache->replace($this->def, $this->config), true); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function test_replace_failure() { + $this->setupMockForFailure('replace', 'get'); + $this->assertEqual($this->cache->replace($this->def, $this->config), false); + $this->cache->get($this->config); + } + + function test_add() { + $this->setupMockForSuccess('add', 'get'); + $this->assertEqual($this->cache->add($this->def, $this->config), true); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function test_add_failure() { + $this->setupMockForFailure('add', 'get'); + $this->assertEqual($this->cache->add($this->def, $this->config), false); + $this->cache->get($this->config); + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCache/DecoratorHarness.php b/tests/HTMLPurifier/DefinitionCache/DecoratorHarness.php new file mode 100644 index 00000000..c5c75635 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCache/DecoratorHarness.php @@ -0,0 +1,26 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCacheHarness.php'; +require_once 'HTMLPurifier/DefinitionCache/Decorator/Memory.php'; + +generate_mock_once('HTMLPurifier_DefinitionCache'); + +class HTMLPurifier_DefinitionCache_DecoratorHarness extends HTMLPurifier_DefinitionCacheHarness +{ + + function setup() { + $this->mock = new HTMLPurifier_DefinitionCacheMock($this); + $this->mock->type = 'Test'; + $this->cache = $this->cache->decorate($this->mock); + $this->def = $this->generateDefinition(); + $this->config = $this->generateConfigMock(); + } + + function teardown() { + unset($this->mock); + unset($this->cache); + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCache/DecoratorTest.php b/tests/HTMLPurifier/DefinitionCache/DecoratorTest.php new file mode 100644 index 00000000..49b1c68d --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCache/DecoratorTest.php @@ -0,0 +1,45 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCacheHarness.php'; +require_once 'HTMLPurifier/DefinitionCache/Decorator.php'; + +class HTMLPurifier_DefinitionCache_DecoratorTest extends HTMLPurifier_DefinitionCacheHarness +{ + + function test() { + + generate_mock_once('HTMLPurifier_DefinitionCache'); + $mock = new HTMLPurifier_DefinitionCacheMock($this); + $mock->type = 'Test'; + + $cache = new HTMLPurifier_DefinitionCache_Decorator(); + $cache = $cache->decorate($mock); + + $this->assertIdentical($cache->type, $mock->type); + + $def = $this->generateDefinition(); + $config = $this->generateConfigMock(); + + $mock->expectOnce('add', array($def, $config)); + $cache->add($def, $config); + + $mock->expectOnce('set', array($def, $config)); + $cache->set($def, $config); + + $mock->expectOnce('replace', array($def, $config)); + $cache->replace($def, $config); + + $mock->expectOnce('get', array($config)); + $cache->get($config); + + $mock->expectOnce('flush', array($config)); + $cache->flush($config); + + $mock->expectOnce('cleanup', array($config)); + $cache->cleanup($config); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCache/SerializerTest.php b/tests/HTMLPurifier/DefinitionCache/SerializerTest.php new file mode 100644 index 00000000..99867b03 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCache/SerializerTest.php @@ -0,0 +1,176 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCacheHarness.php'; +require_once 'HTMLPurifier/DefinitionCache/Serializer.php'; + +class HTMLPurifier_DefinitionCache_SerializerTest extends HTMLPurifier_DefinitionCacheHarness +{ + + function test() { + + $cache = new HTMLPurifier_DefinitionCache_Serializer('Test'); + + $config = $this->generateConfigMock('serial'); + $config->setReturnValue('get', 2, array('Test', 'DefinitionRev')); + $config->version = '1.0.0'; + + $config_md5 = '1.0.0-2-serial'; + + $file = realpath( + $rel_file = dirname(__FILE__) . + '/../../../library/HTMLPurifier/DefinitionCache/Serializer/Test/' . + $config_md5 . '.ser' + ); + if($file && file_exists($file)) unlink($file); // prevent previous failures from causing problems + + $this->assertIdentical($config_md5, $cache->generateKey($config)); + + $def_original = $this->generateDefinition(); + + $cache->add($def_original, $config); + $this->assertFileExist($rel_file); + + $file_generated = $cache->generateFilePath($config); + $this->assertIdentical(realpath($rel_file), realpath($file_generated)); + + $def_1 = $cache->get($config); + $this->assertIdentical($def_original, $def_1); + + $def_original->info_random = 'changed'; + + $cache->set($def_original, $config); + $def_2 = $cache->get($config); + + $this->assertIdentical($def_original, $def_2); + $this->assertNotEqual ($def_original, $def_1); + + $def_original->info_random = 'did it change?'; + + $this->assertFalse($cache->add($def_original, $config)); + $def_3 = $cache->get($config); + + $this->assertNotEqual ($def_original, $def_3); // did not change! + $this->assertIdentical($def_3, $def_2); + + $cache->replace($def_original, $config); + $def_4 = $cache->get($config); + $this->assertIdentical($def_original, $def_4); + + $cache->remove($config); + $this->assertFileNotExist($file); + + $this->assertFalse($cache->replace($def_original, $config)); + $def_5 = $cache->get($config); + $this->assertFalse($def_5); + + } + + function test_errors() { + $cache = new HTMLPurifier_DefinitionCache_Serializer('Test'); + $def = new HTMLPurifier_Definition(); + $def->setup = true; + $def->type = 'NotTest'; + $config = $this->generateConfigMock('testfoo'); + + $this->expectError('Cannot use definition of type NotTest in cache for Test'); + $cache->add($def, $config); + + $this->expectError('Cannot use definition of type NotTest in cache for Test'); + $cache->set($def, $config); + + $this->expectError('Cannot use definition of type NotTest in cache for Test'); + $cache->replace($def, $config); + } + + function test_flush() { + + $cache = new HTMLPurifier_DefinitionCache_Serializer('Test'); + + $config1 = $this->generateConfigMock('test1'); + $config2 = $this->generateConfigMock('test2'); + $config3 = $this->generateConfigMock('test3'); + + $def1 = $this->generateDefinition(array('info_candles' => 1)); + $def2 = $this->generateDefinition(array('info_candles' => 2)); + $def3 = $this->generateDefinition(array('info_candles' => 3)); + + $cache->add($def1, $config1); + $cache->add($def2, $config2); + $cache->add($def3, $config3); + + $this->assertEqual($def1, $cache->get($config1)); + $this->assertEqual($def2, $cache->get($config2)); + $this->assertEqual($def3, $cache->get($config3)); + + $cache->flush($config1); // only essential directive is %Cache.SerializerPath + + $this->assertFalse($cache->get($config1)); + $this->assertFalse($cache->get($config2)); + $this->assertFalse($cache->get($config3)); + + } + + function testCleanup() { + + $cache = new HTMLPurifier_DefinitionCache_Serializer('Test'); + + // in order of age, oldest first + // note that configurations are all identical, but version/revision + // are different + + $config1 = $this->generateConfigMock(); + $config1->version = '0.9.0'; + $config1->setReturnValue('get', 574, array('Test', 'DefinitionRev')); + $def1 = $this->generateDefinition(array('info' => 1)); + + $config2 = $this->generateConfigMock(); + $config2->version = '1.0.0beta'; + $config2->setReturnValue('get', 1, array('Test', 'DefinitionRev')); + $def2 = $this->generateDefinition(array('info' => 3)); + + $cache->set($def1, $config1); + $cache->cleanup($config1); + $this->assertEqual($def1, $cache->get($config1)); // no change + + $cache->cleanup($config2); + $this->assertFalse($cache->get($config1)); + $this->assertFalse($cache->get($config2)); + + } + + /** + * Asserts that a file exists, ignoring the stat cache + */ + function assertFileExist($file) { + clearstatcache(); + $this->assertTrue(file_exists($file), 'Expected ' . $file . ' exists'); + } + + /** + * Asserts that a file does not exist, ignoring the stat cache + */ + function assertFileNotExist($file) { + $this->assertFalse(file_exists($file), 'Expected ' . $file . ' does not exist'); + } + + function testAlternatePath() { + + $cache = new HTMLPurifier_DefinitionCache_Serializer('Test'); + $config = $this->generateConfigMock('serial'); + $config->version = '1.0.0'; + $config->setReturnValue('get', 1, array('Test', 'DefinitionRev')); + $dir = dirname(__FILE__) . '/SerializerTest'; + $config->setReturnValue('get', $dir, array('Cache', 'SerializerPath')); + + $def_original = $this->generateDefinition(); + $cache->add($def_original, $config); + $this->assertFileExist($dir . '/Test/1.0.0-1-serial.ser'); + + unlink($dir . '/Test/1.0.0-1-serial.ser'); + rmdir( $dir . '/Test'); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCacheFactoryTest.php b/tests/HTMLPurifier/DefinitionCacheFactoryTest.php new file mode 100644 index 00000000..892f8805 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCacheFactoryTest.php @@ -0,0 +1,65 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCacheFactory.php'; + +class HTMLPurifier_DefinitionCacheFactoryTest extends UnitTestCase +{ + + var $newFactory; + var $oldFactory; + + function setup() { + $new = new HTMLPurifier_DefinitionCacheFactory(); + $this->oldFactory = HTMLPurifier_DefinitionCacheFactory::instance(); + HTMLPurifier_DefinitionCacheFactory::instance($new); + } + + function teardown() { + HTMLPurifier_DefinitionCacheFactory::instance($this->oldFactory); + } + + function test_create() { + $config = HTMLPurifier_Config::createDefault(); + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create('Test', $config); + $this->assertEqual($cache, new HTMLPurifier_DefinitionCache_Serializer('Test')); + } + + function test_create_withDecorator() { + $config = HTMLPurifier_Config::createDefault(); + $factory =& HTMLPurifier_DefinitionCacheFactory::instance(); + $factory->addDecorator('Memory'); + $cache =& $factory->create('Test', $config); + $cache_real = new HTMLPurifier_DefinitionCache_Decorator_Memory(); + $cache_real = $cache_real->decorate(new HTMLPurifier_DefinitionCache_Serializer('Test')); + $this->assertEqual($cache, $cache_real); + } + + function test_create_withDecoratorObject() { + $config = HTMLPurifier_Config::createDefault(); + $factory =& HTMLPurifier_DefinitionCacheFactory::instance(); + $factory->addDecorator(new HTMLPurifier_DefinitionCache_Decorator_Memory()); + $cache =& $factory->create('Test', $config); + $cache_real = new HTMLPurifier_DefinitionCache_Decorator_Memory(); + $cache_real = $cache_real->decorate(new HTMLPurifier_DefinitionCache_Serializer('Test')); + $this->assertEqual($cache, $cache_real); + } + + function test_create_recycling() { + $config = HTMLPurifier_Config::createDefault(); + $factory =& HTMLPurifier_DefinitionCacheFactory::instance(); + $cache =& $factory->create('Test', $config); + $cache2 =& $factory->create('Test', $config); + $this->assertReference($cache, $cache2); + } + + function test_null() { + $config = HTMLPurifier_Config::create(array('Core.DefinitionCache' => null)); + $factory =& HTMLPurifier_DefinitionCacheFactory::instance(); + $cache =& $factory->create('Test', $config); + $this->assertEqual($cache, new HTMLPurifier_DefinitionCache_Null('Test')); + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCacheHarness.php b/tests/HTMLPurifier/DefinitionCacheHarness.php new file mode 100644 index 00000000..efcb49a4 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCacheHarness.php @@ -0,0 +1,34 @@ +<?php + +class HTMLPurifier_DefinitionCacheHarness extends UnitTestCase +{ + + /** + * Generate a configuration mock object that returns $values + * to a getBatch() call + * @param $values Values to return when getBatch is invoked + */ + function generateConfigMock($serial = 'defaultserial') { + generate_mock_once('HTMLPurifier_Config'); + $config = new HTMLPurifier_ConfigMock($this); + $config->setReturnValue('getBatchSerial', $serial, array('Test')); + $config->version = '1.0.0'; + return $config; + } + + /** + * Returns an anonymous def that has been setup and named Test + */ + function generateDefinition($member_vars = array()) { + $def = new HTMLPurifier_Definition(); + $def->setup = true; + $def->type = 'Test'; + foreach ($member_vars as $key => $val) { + $def->$key = $val; + } + return $def; + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCacheTest.php b/tests/HTMLPurifier/DefinitionCacheTest.php new file mode 100644 index 00000000..702712ff --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCacheTest.php @@ -0,0 +1,35 @@ +<?php + +require_once 'HTMLPurifier/DefinitionCache.php'; + +class HTMLPurifier_DefinitionCacheTest extends UnitTestCase +{ + + function test_isOld() { + $cache = new HTMLPurifier_DefinitionCache('Test'); // non-functional + + $old_copy = HTMLPurifier_ConfigSchema::instance(); + $o = new HTMLPurifier_ConfigSchema(); + HTMLPurifier_ConfigSchema::instance($o); + + HTMLPurifier_ConfigSchema::defineNamespace('Test', 'Test namespace'); + HTMLPurifier_ConfigSchema::define('Test', 'DefinitionRev', 1, 'int', 'Definition revision.'); + + $config = HTMLPurifier_Config::createDefault(); + $config->version = '1.0.0'; + $config->set('Test', 'DefinitionRev', 10); + + $this->assertIdentical($cache->isOld('1.0.0-10-hashstuffhere', $config), false); + $this->assertIdentical($cache->isOld('1.5.0-1-hashstuffhere', $config), false); + + $this->assertIdentical($cache->isOld('0.9.0-1-hashstuffhere', $config), true); + $this->assertIdentical($cache->isOld('1.0.0-1-hashstuffhere', $config), true); + $this->assertIdentical($cache->isOld('1.0.0beta-11-hashstuffhere', $config), true); + + HTMLPurifier_ConfigSchema::instance($old_copy); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionTest.php b/tests/HTMLPurifier/DefinitionTest.php new file mode 100644 index 00000000..2ce85d07 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionTest.php @@ -0,0 +1,33 @@ +<?php + +require_once 'HTMLPurifier/Definition.php'; + +Mock::generatePartial( + 'HTMLPurifier_Definition', + 'HTMLPurifier_Definition_Testable', + array('doSetup')); + +class HTMLPurifier_DefinitionTest extends UnitTestCase +{ + function test_setup() { + $def = new HTMLPurifier_Definition_Testable(); + $config = HTMLPurifier_Config::createDefault(); + $def->expectOnce('doSetup', array($config)); + $def->setup($config); + } + function test_setup_redundant() { + $def = new HTMLPurifier_Definition_Testable(); + $config = HTMLPurifier_Config::createDefault(); + $def->expectNever('doSetup'); + $def->setup = true; + $def->setup($config); + } + function test_doSetup_abstract() { + $def = new HTMLPurifier_Definition(); + $this->expectError('Cannot call abstract method'); + $config = HTMLPurifier_Config::createDefault(); + $def->doSetup($config); + } +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DoctypeRegistryTest.php b/tests/HTMLPurifier/DoctypeRegistryTest.php new file mode 100644 index 00000000..385859c9 --- /dev/null +++ b/tests/HTMLPurifier/DoctypeRegistryTest.php @@ -0,0 +1,79 @@ +<?php + +require_once 'HTMLPurifier/DoctypeRegistry.php'; + +class HTMLPurifier_DoctypeRegistryTest extends UnitTestCase +{ + + function test_register() { + + $registry = new HTMLPurifier_DoctypeRegistry(); + + $d =& $registry->register( + $name = 'XHTML 1.0 Transitional', + $xml = true, + $modules = array('module-one', 'module-two'), + $tidyModules = array('lenient-module'), + $aliases = array('X10T') + ); + + $d2 = new HTMLPurifier_Doctype($name, $xml, $modules, $tidyModules, $aliases); + + $this->assertIdentical($d, $d2); + $this->assertReference($d, $registry->get('XHTML 1.0 Transitional')); + + // test shorthand + $d =& $registry->register( + $name = 'XHTML 1.0 Strict', true, 'module', 'Tidy', 'X10S' + ); + $d2 = new HTMLPurifier_Doctype($name, true, array('module'), array('Tidy'), array('X10S')); + + $this->assertIdentical($d, $d2); + + } + + function test_get() { + + // see also alias and register tests + + $registry = new HTMLPurifier_DoctypeRegistry(); + + $this->expectError('Doctype XHTML 2.0 does not exist'); + $registry->get('XHTML 2.0'); + + // prevent XSS + $this->expectError('Doctype &lt;foo&gt; does not exist'); + $registry->get('<foo>'); + + } + + function testAliases() { + + $registry = new HTMLPurifier_DoctypeRegistry(); + + $d1 =& $registry->register('Doc1', true, array(), array(), array('1')); + + $this->assertReference($d1, $registry->get('Doc1')); + $this->assertReference($d1, $registry->get('1')); + + $d2 =& $registry->register('Doc2', true, array(), array(), array('2')); + + $this->assertReference($d2, $registry->get('Doc2')); + $this->assertReference($d2, $registry->get('2')); + + $d3 =& $registry->register('1', true, array(), array(), array()); + + // literal name overrides alias + $this->assertReference($d3, $registry->get('1')); + + $d4 =& $registry->register('One', true, array(), array(), array('1')); + + $this->assertReference($d4, $registry->get('One')); + // still it overrides + $this->assertReference($d3, $registry->get('1')); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/ElementDefTest.php b/tests/HTMLPurifier/ElementDefTest.php new file mode 100644 index 00000000..23ef6e17 --- /dev/null +++ b/tests/HTMLPurifier/ElementDefTest.php @@ -0,0 +1,108 @@ +<?php + +require_once 'HTMLPurifier/ElementDef.php'; + +class HTMLPurifier_ElementDefTest extends UnitTestCase +{ + + function test_mergeIn() { + + $def1 = new HTMLPurifier_ElementDef(); + $def2 = new HTMLPurifier_ElementDef(); + $def3 = new HTMLPurifier_ElementDef(); + + $old = 1; + $new = 2; + $overloaded_old = 3; + $overloaded_new = 4; + $removed = 5; + + $def1->standalone = true; + $def1->attr = array( + 0 => array('old-include'), + 'old-attr' => $old, + 'overloaded-attr' => $overloaded_old, + 'removed-attr' => $removed, + ); + $def1->attr_transform_pre = + $def1->attr_transform_post = array( + 'old-transform' => $old, + 'overloaded-transform' => $overloaded_old, + 'removed-transform' => $removed, + ); + $def1->child = $overloaded_old; + $def1->content_model = 'old'; + $def1->content_model_type = $overloaded_old; + $def1->auto_close = array( + 'old' => true, + 'removed-old' => true + ); + $def1->descendants_are_inline = false; + $def1->excludes = array( + 'old' => true, + 'removed-old' => true + ); + $def1->safe = false; + + $def2->standalone = false; + $def2->attr = array( + 0 => array('new-include'), + 'new-attr' => $new, + 'overloaded-attr' => $overloaded_new, + 'removed-attr' => false, + ); + $def2->attr_transform_pre = + $def2->attr_transform_post = array( + 'new-transform' => $new, + 'overloaded-transform' => $overloaded_new, + 'removed-transform' => false, + ); + $def2->child = $new; + $def2->content_model = 'new'; + $def2->content_model_type = $overloaded_new; + $def2->auto_close = array( + 'new' => true, + 'removed-old' => false + ); + $def2->descendants_are_inline = true; + $def2->excludes = array( + 'new' => true, + 'removed-old' => false + ); + $def2->safe = true; + + $def1->mergeIn($def2); + $def1->mergeIn($def3); // empty, has no effect + + $this->assertIdentical($def1->standalone, true); + $this->assertIdentical($def1->attr, array( + 0 => array('old-include', 'new-include'), + 'old-attr' => $old, + 'overloaded-attr' => $overloaded_new, + 'new-attr' => $new, + )); + $this->assertIdentical($def1->attr_transform_pre, $def1->attr_transform_post); + $this->assertIdentical($def1->attr_transform_pre, array( + 'old-transform' => $old, + 'overloaded-transform' => $overloaded_new, + 'new-transform' => $new, + )); + $this->assertIdentical($def1->child, $new); + $this->assertIdentical($def1->content_model, 'old | new'); + $this->assertIdentical($def1->content_model_type, $overloaded_new); + $this->assertIdentical($def1->auto_close, array( + 'old' => true, + 'new' => true + )); + $this->assertIdentical($def1->descendants_are_inline, true); + $this->assertIdentical($def1->excludes, array( + 'old' => true, + 'new' => true + )); + $this->assertIdentical($def1->safe, true); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/EncoderTest.php b/tests/HTMLPurifier/EncoderTest.php index ef14b139..0bab873e 100644 --- a/tests/HTMLPurifier/EncoderTest.php +++ b/tests/HTMLPurifier/EncoderTest.php @@ -39,7 +39,9 @@ class HTMLPurifier_EncoderTest extends UnitTestCase ); $this->assertNoErrors(); - $config->set('Core', 'Encoding', 'ISO-8859-1'); + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1' + )); // Now it gets converted $this->assertIdentical( @@ -47,8 +49,10 @@ class HTMLPurifier_EncoderTest extends UnitTestCase "\xC3\xB6" ); - $config->set('Test', 'ForceNoIconv', true); - + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1', + 'Test.ForceNoIconv' => true + )); $this->assertIdentical( HTMLPurifier_Encoder::convertToUTF8("\xF6", $config, $context), "\xC3\xB6" @@ -69,7 +73,9 @@ class HTMLPurifier_EncoderTest extends UnitTestCase "\xC3\xB6" ); - $config->set('Core', 'Encoding', 'ISO-8859-1'); + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1' + )); // Now it gets converted $this->assertIdentical( @@ -86,7 +92,10 @@ class HTMLPurifier_EncoderTest extends UnitTestCase } // Plain PHP implementation has slightly different behavior - $config->set('Test', 'ForceNoIconv', true); + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1', + 'Test.ForceNoIconv' => true + )); $this->assertIdentical( HTMLPurifier_Encoder::convertFromUTF8("\xC3\xB6", $config, $context), "\xF6" @@ -98,8 +107,10 @@ class HTMLPurifier_EncoderTest extends UnitTestCase ); // Preserve the characters! - - $config->set('Core', 'EscapeNonASCIICharacters', true); + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1', + 'Core.EscapeNonASCIICharacters' => true + )); $this->assertIdentical( HTMLPurifier_Encoder::convertFromUTF8($chinese, $config, $context), "&#20013;&#25991; (Chinese)" diff --git a/tests/HTMLPurifier/EntityParserTest.php b/tests/HTMLPurifier/EntityParserTest.php index 11a5ce2a..54328d34 100644 --- a/tests/HTMLPurifier/EntityParserTest.php +++ b/tests/HTMLPurifier/EntityParserTest.php @@ -57,7 +57,8 @@ class HTMLPurifier_EntityParserTest extends UnitTestCase // this is only for PHP 5, the below is PHP 5 and PHP 4 //$chars = str_split($arg[1], 2); $chars = array(); - for ($i = 0; isset($arg[1][$i]); $i += 2) { + // strlen must be called in loop because strings size changes + for ($i = 0; strlen($arg[1]) > $i; $i += 2) { $chars[] = $arg[1][$i] . $arg[1][$i+1]; } foreach ($chars as $char) { diff --git a/tests/HTMLPurifier/ErrorCollectorTest.php b/tests/HTMLPurifier/ErrorCollectorTest.php new file mode 100644 index 00000000..31ac3052 --- /dev/null +++ b/tests/HTMLPurifier/ErrorCollectorTest.php @@ -0,0 +1,42 @@ +<?php + +require_once 'HTMLPurifier/ErrorCollector.php'; + +class HTMLPurifier_ErrorCollectorTest extends UnitTestCase +{ + + function test() { + + $tok1 = new HTMLPurifier_Token_Text('Token that caused error'); + $tok1->line = 23; + $tok2 = new HTMLPurifier_Token_Start('a'); // also caused error + $tok2->line = 3; + $tok3 = new HTMLPurifier_Token_Text('Context before'); // before $tok2 + $tok3->line = 3; + $tok4 = new HTMLPurifier_Token_Text('Context after'); // after $tok2 + $tok4->line = 3; + + $collector = new HTMLPurifier_ErrorCollector(); + $collector->send('Big fat error', $tok1); + $collector->send('Another <error>', $tok2, array($tok3, true, $tok4)); + + $result = array( + 0 => array('Big fat error', $tok1, array(true)), + 1 => array('Another <error>', $tok2, array($tok3, true, $tok4)) + ); + + $this->assertIdentical($collector->getRaw(), $result); + + $formatted_result = array( + 0 => 'Another &lt;error&gt; at line 3 (<code>Context before<strong>&lt;a&gt;</strong>Context after</code>)', + 1 => 'Big fat error at line 23 (<code><strong>Token that caused error</strong></code>)' + ); + + $config = HTMLPurifier_Config::create(array('Core.MaintainLineNumbers' => true)); + $this->assertIdentical($collector->getHTMLFormatted($config), $formatted_result); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/GeneratorTest.php b/tests/HTMLPurifier/GeneratorTest.php index f16b95ed..2656e82f 100644 --- a/tests/HTMLPurifier/GeneratorTest.php +++ b/tests/HTMLPurifier/GeneratorTest.php @@ -89,13 +89,21 @@ class HTMLPurifier_GeneratorTest extends HTMLPurifier_Harness $expect[4] = 'title="Theta is ' . $theta_char . '"'; foreach ($inputs as $i => $input) { - $result = $this->obj->generateAttributes($input); + $result = $this->obj->generateAttributes($input, 'irrelevant'); $this->assertIdentical($result, $expect[$i]); paintIf($result, $result != $expect[$i]); } } + function test_generateAttributes_minimized() { + $gen = new HTMLPurifier_Generator(); + $context = new HTMLPurifier_Context(); + $gen->generateFromTokens(array(), HTMLPurifier_Config::create(array('HTML.Doctype' => 'HTML 4.01 Transitional')), $context); + $result = $gen->generateAttributes(array('compact' => 'compact'), 'menu'); + $this->assertIdentical($result, 'compact'); + } + function test_generateFromTokens() { $this->func = 'generateFromTokens'; @@ -124,6 +132,31 @@ class HTMLPurifier_GeneratorTest extends HTMLPurifier_Harness $this->assertIdentical($expect, $result); } + function test_generateFromTokens_Scripting() { + $this->config = HTMLPurifier_Config::createDefault(); + + $this->assertGeneration( + array( + new HTMLPurifier_Token_Start('script'), + new HTMLPurifier_Token_Text('alert(3 < 5);'), + new HTMLPurifier_Token_End('script') + ), + "<script><!--\nalert(3 < 5);\n// --></script>" + ); + + $this->config = HTMLPurifier_Config::createDefault(); + $this->config->set('Core', 'CommentScriptContents', false); + + $this->assertGeneration( + array( + new HTMLPurifier_Token_Start('script'), + new HTMLPurifier_Token_Text('alert(3 < 5);'), + new HTMLPurifier_Token_End('script') + ), + "<script>alert(3 &lt; 5);</script>" + ); + } + function test_generateFromTokens_XHTMLoff() { $this->config = HTMLPurifier_Config::createDefault(); $this->config->set('Core', 'XHTML', false); diff --git a/tests/HTMLPurifier/HTMLDefinitionTest.php b/tests/HTMLPurifier/HTMLDefinitionTest.php new file mode 100644 index 00000000..13804472 --- /dev/null +++ b/tests/HTMLPurifier/HTMLDefinitionTest.php @@ -0,0 +1,87 @@ +<?php + +require_once 'HTMLPurifier/HTMLDefinition.php'; + +class HTMLPurifier_HTMLDefinitionTest extends UnitTestCase +{ + + function test_parseTinyMCEAllowedList() { + + $def = new HTMLPurifier_HTMLDefinition(); + + $this->assertEqual( + $def->parseTinyMCEAllowedList('a,b,c'), + array(array('a' => true, 'b' => true, 'c' => true), array()) + ); + + $this->assertEqual( + $def->parseTinyMCEAllowedList('a[x|y|z]'), + array(array('a' => true), array('a.x' => true, 'a.y' => true, 'a.z' => true)) + ); + + $this->assertEqual( + $def->parseTinyMCEAllowedList('*[id]'), + array(array(), array('*.id' => true)) + ); + + $this->assertEqual( + $def->parseTinyMCEAllowedList('a[*]'), + array(array('a' => true), array('a.*' => true)) + ); + + $this->assertEqual( + $def->parseTinyMCEAllowedList('span[style],strong,a[href|title]'), + array(array('span' => true, 'strong' => true, 'a' => true), + array('span.style' => true, 'a.href' => true, 'a.title' => true)) + ); + + } + + function test_Allowed() { + + $config1 = HTMLPurifier_Config::create(array( + 'HTML.AllowedElements' => array('b', 'i', 'p', 'a'), + 'HTML.AllowedAttributes' => array('a.href', '*.id') + )); + + $config2 = HTMLPurifier_Config::create(array( + 'HTML.Allowed' => 'b,i,p,a[href],*[id]' + )); + + $this->assertEqual($config1->getHTMLDefinition(), $config2->getHTMLDefinition()); + + } + + function test_addAttribute() { + + $config = HTMLPurifier_Config::create(array( + 'HTML.DefinitionID' => 'HTMLPurifier_HTMLDefinitionTest->test_addAttribute' + )); + $def =& $config->getHTMLDefinition(true); + $def->addAttribute('span', 'custom', 'Enum#attribute'); + + $purifier = new HTMLPurifier($config); + $input = '<span custom="attribute">Custom!</span>'; + $output = $purifier->purify($input); + $this->assertIdentical($input, $output); + + } + + function test_addElement() { + + $config = HTMLPurifier_Config::create(array( + 'HTML.DefinitionID' => 'HTMLPurifier_HTMLDefinitionTest->test_addElement' + )); + $def =& $config->getHTMLDefinition(true); + $def->addElement('marquee', 'Inline', 'Inline', 'Common', array('width' => 'Length')); + + $purifier = new HTMLPurifier($config); + $input = '<span><marquee width="50">Foobar</marquee></span>'; + $output = $purifier->purify($input); + $this->assertIdentical($input, $output); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/HTMLModule/ScriptingTest.php b/tests/HTMLPurifier/HTMLModule/ScriptingTest.php new file mode 100644 index 00000000..70008d1e --- /dev/null +++ b/tests/HTMLPurifier/HTMLModule/ScriptingTest.php @@ -0,0 +1,52 @@ +<?php + +require_once 'HTMLPurifier/HTMLModuleHarness.php'; + +class HTMLPurifier_HTMLModule_ScriptingTest extends HTMLPurifier_HTMLModuleHarness +{ + + function test() { + + // default (remove everything) + $this->assertResult( + '<script type="text/javascript">foo();</script>', '' + ); + + // enabled + $this->assertResult( + '<script type="text/javascript">foo();</script>', true, + array('HTML.Trusted' => true) + ); + + // max + $this->assertResult( + '<script + defer="defer" + src="test.js" + type="text/javascript" + >PCDATA</script>', true, + array('HTML.Trusted' => true, 'Core.CommentScriptContents' => false) + ); + + // unsupported + $this->assertResult( + '<script + type="text/javascript" + charset="utf-8" + >PCDATA</script>', + '<script type="text/javascript">PCDATA</script>', + array('HTML.Trusted' => true, 'Core.CommentScriptContents' => false) + ); + + // invalid children + $this->assertResult( + '<script type="text/javascript">PCDATA<span</script>', + '<script type="text/javascript">PCDATA</script>', + array('HTML.Trusted' => true, 'Core.CommentScriptContents' => false) + ); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/HTMLModule/TidyTest.php b/tests/HTMLPurifier/HTMLModule/TidyTest.php new file mode 100644 index 00000000..f3a1f977 --- /dev/null +++ b/tests/HTMLPurifier/HTMLModule/TidyTest.php @@ -0,0 +1,230 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule/Tidy.php'; + +Mock::generatePartial( + 'HTMLPurifier_HTMLModule_Tidy', + 'HTMLPurifier_HTMLModule_Tidy_TestForConstruct', + array('makeFixes', 'makeFixesForLevel', 'populate') +); + +class HTMLPurifier_HTMLModule_TidyTest extends UnitTestCase +{ + + function test_getFixesForLevel() { + + $module = new HTMLPurifier_HTMLModule_Tidy(); + $module->fixesForLevel['light'][] = 'light-fix'; + $module->fixesForLevel['medium'][] = 'medium-fix'; + $module->fixesForLevel['heavy'][] = 'heavy-fix'; + + $this->assertIdentical( + array(), + $module->getFixesForLevel('none') + ); + $this->assertIdentical( + array('light-fix' => true), + $module->getFixesForLevel('light') + ); + $this->assertIdentical( + array('light-fix' => true, 'medium-fix' => true), + $module->getFixesForLevel('medium') + ); + $this->assertIdentical( + array('light-fix' => true, 'medium-fix' => true, 'heavy-fix' => true), + $module->getFixesForLevel('heavy') + ); + + $this->expectError('Tidy level turbo not recognized'); + $module->getFixesForLevel('turbo'); + + } + + function test_construct() { + + $i = 0; // counter, helps us isolate expectations + + // initialize partial mock + $module = new HTMLPurifier_HTMLModule_Tidy_TestForConstruct($this); + $module->fixesForLevel['light'] = array('light-fix-1', 'light-fix-2'); + $module->fixesForLevel['medium'] = array('medium-fix-1', 'medium-fix-2'); + $module->fixesForLevel['heavy'] = array('heavy-fix-1', 'heavy-fix-2'); + + $j = 0; + $fixes = array( + 'light-fix-1' => $lf1 = $j++, + 'light-fix-2' => $lf2 = $j++, + 'medium-fix-1' => $mf1 = $j++, + 'medium-fix-2' => $mf2 = $j++, + 'heavy-fix-1' => $hf1 = $j++, + 'heavy-fix-2' => $hf2 = $j++ + ); + $module->setReturnValue('makeFixes', $fixes); + + $config = HTMLPurifier_Config::create(array( + 'HTML.TidyLevel' => 'none' + )); + $module->expectAt($i++, 'populate', array(array())); + $module->construct($config); + + // basic levels + + $config = HTMLPurifier_Config::create(array( + 'HTML.TidyLevel' => 'light' + )); + $module->expectAt($i++, 'populate', array(array( + 'light-fix-1' => $lf1, + 'light-fix-2' => $lf2 + ))); + $module->construct($config); + + $config = HTMLPurifier_Config::create(array( + 'HTML.TidyLevel' => 'heavy' + )); + $module->expectAt($i++, 'populate', array(array( + 'light-fix-1' => $lf1, + 'light-fix-2' => $lf2, + 'medium-fix-1' => $mf1, + 'medium-fix-2' => $mf2, + 'heavy-fix-1' => $hf1, + 'heavy-fix-2' => $hf2 + ))); + $module->construct($config); + + // fine grained tuning + + $config = HTMLPurifier_Config::create(array( + 'HTML.TidyLevel' => 'none', + 'HTML.TidyAdd' => array('light-fix-1', 'medium-fix-1') + )); + $module->expectAt($i++, 'populate', array(array( + 'light-fix-1' => $lf1, + 'medium-fix-1' => $mf1 + ))); + $module->construct($config); + + $config = HTMLPurifier_Config::create(array( + 'HTML.TidyLevel' => 'medium', + 'HTML.TidyRemove' => array('light-fix-1', 'medium-fix-1') + )); + $module->expectAt($i++, 'populate', array(array( + 'light-fix-2' => $lf2, + 'medium-fix-2' => $mf2 + ))); + $module->construct($config); + + // done + + $module->tally(); + + } + + function test_makeFixesForLevel() { + + $module = new HTMLPurifier_HTMLModule_Tidy(); + $module->defaultLevel = 'heavy'; + + $module->makeFixesForLevel(array( + 'fix-1' => 0, + 'fix-2' => 1, + 'fix-3' => 2 + )); + + $this->assertIdentical($module->fixesForLevel['heavy'], array('fix-1', 'fix-2', 'fix-3')); + $this->assertIdentical($module->fixesForLevel['medium'], array()); + $this->assertIdentical($module->fixesForLevel['light'], array()); + + } + function test_makeFixesForLevel_undefinedLevel() { + + $module = new HTMLPurifier_HTMLModule_Tidy(); + $module->defaultLevel = 'bananas'; + + $this->expectError('Default level bananas does not exist'); + + $module->makeFixesForLevel(array( + 'fix-1' => 0 + )); + + } + + function test_getFixType() { + + // syntax needs documenting + + $module = new HTMLPurifier_HTMLModule_Tidy(); + + $this->assertIdentical( + $module->getFixType('a'), + array('tag_transform', array('element' => 'a')) + ); + + $this->assertIdentical( + $module->getFixType('a@href'), + $reuse = array('attr_transform_pre', array('element' => 'a', 'attr' => 'href')) + ); + + $this->assertIdentical( + $module->getFixType('a@href#pre'), + $reuse + ); + + $this->assertIdentical( + $module->getFixType('a@href#post'), + array('attr_transform_post', array('element' => 'a', 'attr' => 'href')) + ); + + $this->assertIdentical( + $module->getFixType('xml:foo@xml:bar'), + array('attr_transform_pre', array('element' => 'xml:foo', 'attr' => 'xml:bar')) + ); + + $this->assertIdentical( + $module->getFixType('blockquote#child'), + array('child', array('element' => 'blockquote')) + ); + + $this->assertIdentical( + $module->getFixType('@lang'), + array('attr_transform_pre', array('attr' => 'lang')) + ); + + $this->assertIdentical( + $module->getFixType('@lang#post'), + array('attr_transform_post', array('attr' => 'lang')) + ); + + } + + function test_populate() { + + $i = 0; + + $module = new HTMLPurifier_HTMLModule_Tidy(); + $module->populate(array( + 'element' => $element = $i++, + 'element@attr' => $attr = $i++, + 'element@attr#post' => $attr_post = $i++, + 'element#child' => $child = $i++, + 'element#content_model_type' => $content_model_type = $i++, + '@attr' => $global_attr = $i++, + '@attr#post' => $global_attr_post = $i++ + )); + + $module2 = new HTMLPurifier_HTMLModule_Tidy(); + $e =& $module2->addBlankElement('element'); + $e->attr_transform_pre['attr'] = $attr; + $e->attr_transform_post['attr'] = $attr_post; + $e->child = $child; + $e->content_model_type = $content_model_type; + $module2->info_tag_transform['element'] = $element; + $module2->info_attr_transform_pre['attr'] = $global_attr; + $module2->info_attr_transform_post['attr'] = $global_attr_post; + + $this->assertEqual($module, $module2); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/HTMLModuleHarness.php b/tests/HTMLPurifier/HTMLModuleHarness.php new file mode 100644 index 00000000..1f60f435 --- /dev/null +++ b/tests/HTMLPurifier/HTMLModuleHarness.php @@ -0,0 +1,14 @@ +<?php + +require_once 'HTMLPurifier/StrategyHarness.php'; +require_once 'HTMLPurifier/Strategy/Core.php'; + +class HTMLPurifier_HTMLModuleHarness extends HTMLPurifier_StrategyHarness +{ + function setup() { + parent::setup(); + $this->obj = new HTMLPurifier_Strategy_Core(); + } +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/HTMLModuleManagerTest.php b/tests/HTMLPurifier/HTMLModuleManagerTest.php index d025c3cd..c273856a 100644 --- a/tests/HTMLPurifier/HTMLModuleManagerTest.php +++ b/tests/HTMLPurifier/HTMLModuleManagerTest.php @@ -2,273 +2,108 @@ require_once 'HTMLPurifier/HTMLModuleManager.php'; -// stub classes for unit testing -class HTMLPurifier_HTMLModule_ManagerTestModule extends HTMLPurifier_HTMLModule { - var $name = 'ManagerTestModule'; -} -class HTMLPurifier_HTMLModuleManagerTest_TestModule extends HTMLPurifier_HTMLModule { - var $name = 'TestModule'; -} - class HTMLPurifier_HTMLModuleManagerTest extends UnitTestCase { - /** - * System under test, instance of HTMLPurifier_HTMLModuleManager. - */ - var $manager; - - function setup() { - $this->manager = new HTMLPurifier_HTMLModuleManager(true); - } - - function teardown() { - tally_errors($this); - } - - function createModule($name) { - $module = new HTMLPurifier_HTMLModule(); - $module->name = $name; - return $module; - } - - function test_addModule_withAutoload() { - $this->manager->autoDoctype = 'Generic Document 0.1'; - $this->manager->autoCollection = 'Default'; + function test_addModule() { + $manager = new HTMLPurifier_HTMLModuleManager(); + $manager->doctypes->register('Blank'); // doctype normally is blank... - $module = new HTMLPurifier_HTMLModule(); - $module->name = 'Module'; + $attrdef_nmtokens = new HTMLPurifier_AttrDef(); + $attrdef_nmtokens->_name = 'nmtokens'; // for testing only - $module2 = new HTMLPurifier_HTMLModule(); - $module2->name = 'Module2'; + generate_mock_once('HTMLPurifier_AttrDef'); + $attrdef = new HTMLPurifier_AttrDefMock($this); + $attrdef->setReturnValue('make', $attrdef_nmtokens); + $manager->attrTypes->info['NMTOKENS'] =& $attrdef; - // we need to grab the dynamically generated orders from - // the object since modules are not passed by reference + // ...but we add user modules - $this->manager->addModule($module); - $module_order = $this->manager->modules['Module']->order; - $module->order = $module_order; - $this->assertIdentical($module, $this->manager->modules['Module']); + $common_module = new HTMLPurifier_HTMLModule(); + $common_module->name = 'Common'; + $common_module->attr_collections['Common'] = array('class' => 'NMTOKENS'); + $common_module->content_sets['Flow'] = 'Block | Inline'; + $manager->addModule($common_module); - $this->manager->addModule($module2); - $module2_order = $this->manager->modules['Module2']->order; - $module2->order = $module2_order; - $this->assertIdentical($module2, $this->manager->modules['Module2']); - $this->assertIdentical($module_order + 1, $module2_order); + $structural_module = new HTMLPurifier_HTMLModule(); + $structural_module->name = 'Structural'; + $structural_module->addElement('p', true, 'Block', 'Inline', 'Common'); + $structural_module->addElement('div', false, 'Block', 'Flow'); + $manager->addModule($structural_module); - $this->assertIdentical( - $this->manager->collections['Default']['Generic Document 0.1'], - array('Module', 'Module2') + $formatting_module = new HTMLPurifier_HTMLModule(); + $formatting_module->name = 'Formatting'; + $formatting_module->addElement('em', true, 'Inline', 'Inline', 'Common'); + $manager->addModule($formatting_module); + + $config = HTMLPurifier_Config::createDefault(); + $config->set('HTML', 'Trusted', false); + $config->set('HTML', 'Doctype', 'Blank'); + + $manager->setup($config); + + $p = new HTMLPurifier_ElementDef(); + $p->attr['class'] = $attrdef_nmtokens; + $p->child = new HTMLPurifier_ChildDef_Optional(array('em', '#PCDATA')); + $p->content_model = 'em | #PCDATA'; + $p->content_model_type = 'optional'; + $p->descendants_are_inline = true; + $p->safe = true; + + $em = new HTMLPurifier_ElementDef(); + $em->attr['class'] = $attrdef_nmtokens; + $em->child = new HTMLPurifier_ChildDef_Optional(array('em', '#PCDATA')); + $em->content_model = 'em | #PCDATA'; + $em->content_model_type = 'optional'; + $em->descendants_are_inline = true; + $em->safe = true; + + $this->assertEqual( + array('p' => $p, 'em' => $em), + $manager->getElements() ); - $this->manager->setup(HTMLPurifier_Config::createDefault()); + // test trusted parameter override - $modules = array( - 'Module' => $this->manager->modules['Module'], - 'Module2' => $this->manager->modules['Module2'] - ); + $div = new HTMLPurifier_ElementDef(); + $div->child = new HTMLPurifier_ChildDef_Optional(array('p', 'div', 'em', '#PCDATA')); + $div->content_model = 'p | div | em | #PCDATA'; + $div->content_model_type = 'optional'; + $div->descendants_are_inline = false; + $div->safe = false; - $this->assertIdentical( - $this->manager->collections['Default']['Generic Document 0.1'], - $modules - ); - $this->assertIdentical($this->manager->activeModules, $modules); - $this->assertIdentical($this->manager->activeCollections, array('Default')); + $this->assertEqual($div, $manager->getElement('div', true)); } - function test_addModule_undefinedClass() { - $this->expectError('TotallyCannotBeDefined module does not exist'); - $this->manager->addModule('TotallyCannotBeDefined'); - } - - function test_addModule_stringExpansion() { - $this->manager->addModule('ManagerTestModule'); - $this->assertIsA($this->manager->modules['ManagerTestModule'], - 'HTMLPurifier_HTMLModule_ManagerTestModule'); - } - - function test_addPrefix() { - $this->manager->addPrefix('HTMLPurifier_HTMLModuleManagerTest_'); - $this->manager->addModule('TestModule'); - $this->assertIsA($this->manager->modules['TestModule'], - 'HTMLPurifier_HTMLModuleManagerTest_TestModule'); - } - - function assertProcessCollections($input, $expect = false) { - if ($expect === false) $expect = $input; - $this->manager->processCollections($input); - // substitute in modules for $expect - foreach ($expect as $col_i => $col) { - $disable = false; - foreach ($col as $mod_i => $mod) { - unset($expect[$col_i][$mod_i]); - if ($mod_i === '*') { - $disable = true; - continue; - } - $expect[$col_i][$mod] = $this->manager->modules[$mod]; - } - if ($disable) $expect[$col_i]['*'] = false; - } - $this->assertIdentical($input, $expect); - } - - function testImpl_processCollections() { - $this->manager->initialize(); - $this->assertProcessCollections( - array() - ); - $this->assertProcessCollections( - array('HTML' => array('Text')) - ); - $this->assertProcessCollections( - array('HTML' => array('Text', 'Legacy')) - ); - $this->assertProcessCollections( // order is important! - array('HTML' => array('Legacy', 'Text')), - array('HTML' => array('Text', 'Legacy')) - ); - $this->assertProcessCollections( // privates removed after process - array('_Private' => array('Legacy', 'Text')), - array() - ); - $this->assertProcessCollections( // inclusions come first - array( - 'HTML' => array(array('XHTML'), 'Legacy'), - 'XHTML' => array('Text', 'Hypertext') - ), - array( - 'HTML' => array('Text', 'Hypertext', 'Legacy'), - 'XHTML' => array('Text', 'Hypertext') - ) - ); - $this->assertProcessCollections( - array( - 'HTML' => array(array('_Common'), 'Legacy'), - '_Common' => array('Text', 'Hypertext') - ), - array( - 'HTML' => array('Text', 'Hypertext', 'Legacy') - ) - ); - $this->assertProcessCollections( // nested inclusions - array( - 'Full' => array(array('Minimal'), 'Hypertext'), - 'Minimal' => array(array('Bare'), 'List'), - 'Bare' => array('Text') - ), - array( - 'Full' => array('Text', 'Hypertext', 'List'), - 'Minimal' => array('Text', 'List'), - 'Bare' => array('Text') - ) - ); - // strange but valid stuff that will be handled in assembleModules - $this->assertProcessCollections( - array( - 'Linky' => array('Hypertext'), - 'Listy' => array('List'), - '*' => array('Text') - ) - ); - $this->assertProcessCollections( - array( - 'Linky' => array('Hypertext'), - 'ListyOnly' => array('List', '*' => false), - '*' => array('Text') - ) - ); - } - - function testImpl_processCollections_error() { - $this->manager->initialize(); + function testAllowedModules() { - $this->expectError( // active variables, watch out! - 'Illegal inclusion array at index 1 found collection HTML, '. - 'inclusion arrays must be at start of collection (index 0)'); - $c = array( - 'HTML' => array('Legacy', array('XHTML')), - 'XHTML' => array('Text', 'Hypertext') + $manager = new HTMLPurifier_HTMLModuleManager(); + $manager->doctypes->register( + 'Fantasy Inventory 1.0', true, + array('Weapons', 'Magic') ); - $this->manager->processCollections($c); - unset($c); - $this->expectError('Collection HTML references undefined '. - 'module Foobar'); - $c = array( - 'HTML' => array('Foobar') - ); - $this->manager->processCollections($c); - unset($c); + // register these modules so it doesn't blow up + $weapons_module = new HTMLPurifier_HTMLModule(); + $weapons_module->name = 'Weapons'; + $manager->registerModule($weapons_module); - $this->expectError('Collection HTML tried to include undefined '. - 'collection _Common'); - $c = array( - 'HTML' => array(array('_Common'), 'Legacy') - ); - $this->manager->processCollections($c); - unset($c); + $magic_module = new HTMLPurifier_HTMLModule(); + $magic_module->name = 'Magic'; + $manager->registerModule($magic_module); - // reports the first circular inclusion it runs across - $this->expectError('Circular inclusion detected in HTML collection'); - $c = array( - 'HTML' => array(array('XHTML')), - 'XHTML' => array(array('HTML')) - ); - $this->manager->processCollections($c); - unset($c); - - } - - function test_makeCollection() { $config = HTMLPurifier_Config::create(array( - 'HTML.Doctype' => 'Custom Doctype' + 'HTML.Doctype' => 'Fantasy Inventory 1.0', + 'HTML.AllowedModules' => 'Weapons' )); - $this->manager->addModule($this->createModule('ActiveModule')); - $this->manager->addModule($this->createModule('DudModule')); - $this->manager->addModule($this->createModule('ValidModule')); - $ActiveModule = $this->manager->modules['ActiveModule']; - $DudModule = $this->manager->modules['DudModule']; - $ValidModule = $this->manager->modules['ValidModule']; - $this->manager->collections['ToBeValid']['Custom Doctype'] = array('ValidModule'); - $this->manager->collections['ToBeActive']['Custom Doctype'] = array('ActiveModule'); - $this->manager->makeCollectionValid('ToBeValid'); - $this->manager->makeCollectionActive('ToBeActive'); - $this->manager->setup($config); - $this->assertIdentical($this->manager->validModules, array( - 'ValidModule' => $ValidModule, - 'ActiveModule' => $ActiveModule - )); - $this->assertIdentical($this->manager->activeModules, array( - 'ActiveModule' => $ActiveModule - )); - } - - function test_makeCollection_undefinedCollection() { - $config = HTMLPurifier_Config::create(array( - 'HTML.Doctype' => 'Sweets Document 1.0' - )); - $this->manager->addModule($this->createModule('DonutsModule')); - $this->manager->addModule($this->createModule('ChocolateModule')); - $this->manager->collections['CocoaBased']['Sweets Document 1.0'] = array('ChocolateModule'); - // notice how BreadBased collection is missing - $this->manager->makeCollectionActive('CocoaBased'); // to prevent other errors - $this->manager->makeCollectionValid('BreadBased'); - $this->expectError('BreadBased collection is undefined'); - $this->manager->setup($config); - } - - function untest_soupStuff() { - $config = HTMLPurifier_Config::create(array( - 'HTML.Doctype' => 'The Soup Specification 8.0' - )); - $this->manager->addModule($this->createModule('VegetablesModule')); - $this->manager->addModule($this->createModule('MeatModule')); + $manager->setup($config); + + $this->assertTrue( isset($manager->modules['Weapons'])); + $this->assertFalse(isset($manager->modules['Magic'])); } - } ?> \ No newline at end of file diff --git a/tests/HTMLPurifier/HTMLModuleTest.php b/tests/HTMLPurifier/HTMLModuleTest.php new file mode 100644 index 00000000..9e0b7100 --- /dev/null +++ b/tests/HTMLPurifier/HTMLModuleTest.php @@ -0,0 +1,150 @@ +<?php + +require_once 'HTMLPurifier/HTMLModule.php'; +require_once 'HTMLPurifier/AttrDef.php'; + +class HTMLPurifier_HTMLModuleTest extends UnitTestCase +{ + + function test_addElementToContentSet() { + + $module = new HTMLPurifier_HTMLModule(); + + $module->addElementToContentSet('b', 'Inline'); + $this->assertIdentical($module->content_sets, array('Inline' => 'b')); + + $module->addElementToContentSet('i', 'Inline'); + $this->assertIdentical($module->content_sets, array('Inline' => 'b | i')); + + } + + function test_addElement() { + + $module = new HTMLPurifier_HTMLModule(); + $def =& $module->addElement( + 'a', true, 'Inline', 'Optional: #PCDATA', array('Common'), + array( + 'href' => 'URI' + ) + ); + + $module2 = new HTMLPurifier_HTMLModule(); + $def2 = new HTMLPurifier_ElementDef(); + $def2->safe = true; + $def2->content_model = '#PCDATA'; + $def2->content_model_type = 'optional'; + $def2->attr = array( + 'href' => 'URI', + 0 => array('Common') + ); + $module2->info['a'] = $def2; + $module2->elements = array('a'); + $module2->content_sets['Inline'] = 'a'; + + $this->assertIdentical($module, $module2); + $this->assertIdentical($def, $def2); + $this->assertReference($def, $module->info['a']); + + } + + function test_parseContents() { + + $module = new HTMLPurifier_HTMLModule(); + + // pre-defined templates + $this->assertIdentical( + $module->parseContents('Inline'), + array('optional', 'Inline | #PCDATA') + ); + $this->assertIdentical( + $module->parseContents('Flow'), + array('optional', 'Flow | #PCDATA') + ); + $this->assertIdentical( + $module->parseContents('Empty'), + array('empty', '') + ); + + // normalization procedures + $this->assertIdentical( + $module->parseContents('optional: a'), + array('optional', 'a') + ); + $this->assertIdentical( + $module->parseContents('OPTIONAL :a'), + array('optional', 'a') + ); + $this->assertIdentical( + $module->parseContents('Optional: a'), + array('optional', 'a') + ); + + // others + $this->assertIdentical( + $module->parseContents('Optional: a | b | c'), + array('optional', 'a | b | c') + ); + + // object pass-through + generate_mock_once('HTMLPurifier_AttrDef'); + $this->assertIdentical( + $module->parseContents(new HTMLPurifier_AttrDefMock()), + array(null, null) + ); + + } + + function test_mergeInAttrIncludes() { + + $module = new HTMLPurifier_HTMLModule(); + + $attr = array(); + $module->mergeInAttrIncludes($attr, 'Common'); + $this->assertIdentical($attr, array(0 => array('Common'))); + + $attr = array('a' => 'b'); + $module->mergeInAttrIncludes($attr, array('Common', 'Good')); + $this->assertIdentical($attr, array('a' => 'b', 0 => array('Common', 'Good'))); + + } + + function test_addBlankElement() { + + $module = new HTMLPurifier_HTMLModule(); + $def =& $module->addBlankElement('a'); + + $def2 = new HTMLPurifier_ElementDef(); + $def2->standalone = false; + + $this->assertReference($module->info['a'], $def); + $this->assertIdentical($def, $def2); + + } + + function test_makeLookup() { + + $module = new HTMLPurifier_HTMLModule(); + + $this->assertIdentical( + $module->makeLookup('foo'), + array('foo' => true) + ); + $this->assertIdentical( + $module->makeLookup(array('foo')), + array('foo' => true) + ); + + $this->assertIdentical( + $module->makeLookup('foo', 'two'), + array('foo' => true, 'two' => true) + ); + $this->assertIdentical( + $module->makeLookup(array('foo', 'two')), + array('foo' => true, 'two' => true) + ); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/Harness.php b/tests/HTMLPurifier/Harness.php index 6e94e5ab..c591db32 100644 --- a/tests/HTMLPurifier/Harness.php +++ b/tests/HTMLPurifier/Harness.php @@ -42,6 +42,11 @@ class HTMLPurifier_Harness extends UnitTestCase */ var $generator; + /** + * Default config to fall back on if no config is available + */ + var $config; + function HTMLPurifier_Harness() { $this->lexer = new HTMLPurifier_Lexer_DirectLex(); $this->generator = new HTMLPurifier_Generator(); @@ -52,9 +57,8 @@ class HTMLPurifier_Harness extends UnitTestCase * Asserts a specific result from a one parameter + config/context function * @param $input Input parameter * @param $expect Expectation - * @param $config_array Configuration array in form of - * Namespace.Directive => Value or an actual config - * object. + * @param $config Configuration array in form of Ns.Directive => Value. + * Has no effect if $this->config is set. * @param $context_array Context array in form of Key => Value or an actual * context object. */ @@ -62,23 +66,32 @@ class HTMLPurifier_Harness extends UnitTestCase $config_array = array(), $context_array = array() ) { - // setup config object - $config = HTMLPurifier_Config::createDefault(); - $config->loadArray($config_array); + // setup config + if ($this->config) { + $config = HTMLPurifier_Config::create($this->config); + $config->loadArray($config_array); + } else { + $config = HTMLPurifier_Config::create($config_array); + } // setup context object. Note that we are operating on a copy of it! - // We will extend the test harness to allow you to do post-tests + // When necessary, extend the test harness to allow post-tests // on the context object $context = new HTMLPurifier_Context(); $context->loadArray($context_array); if ($this->to_tokens && is_string($input)) { - $input = $this->lexer->tokenizeHTML($input, $config, $context); + // $func may cause $input to change, so "clone" another copy + // to sacrifice + $input = $this->lexer->tokenizeHTML($s = $input, $config, $context); + $input_c = $this->lexer->tokenizeHTML($s, $config, $context); + } else { + $input_c = $input; } // call the function $func = $this->func; - $result = $this->obj->$func($input, $config, $context); + $result = $this->obj->$func($input_c, $config, $context); // test a bool result if (is_bool($result)) { diff --git a/tests/HTMLPurifier/LanguageTest.php b/tests/HTMLPurifier/LanguageTest.php index dd88c90f..21e55206 100644 --- a/tests/HTMLPurifier/LanguageTest.php +++ b/tests/HTMLPurifier/LanguageTest.php @@ -7,14 +7,19 @@ class HTMLPurifier_LanguageTest extends UnitTestCase var $lang; - function setup() { - $factory = HTMLPurifier_LanguageFactory::instance(); - $this->lang = $factory->create('en'); + function test_getMessage() { + $lang = new HTMLPurifier_Language(); + $lang->_loaded = true; + $lang->messages['htmlpurifier'] = 'HTML Purifier'; + $this->assertIdentical($lang->getMessage('htmlpurifier'), 'HTML Purifier'); + $this->assertIdentical($lang->getMessage('totally-non-existent-key'), '[totally-non-existent-key]'); } - function test_getMessage() { - $this->assertIdentical($this->lang->getMessage('htmlpurifier'), 'HTML Purifier'); - $this->assertIdentical($this->lang->getMessage('totally-non-existent-key'), ''); + function test_formatMessage() { + $lang = new HTMLPurifier_Language(); + $lang->_loaded = true; + $lang->messages['error'] = 'Error is $1 on line $2'; + $this->assertIdentical($lang->formatMessage('error', 'fatal', 32), 'Error is fatal on line 32'); } } diff --git a/tests/HTMLPurifier/Lexer/DirectLexTest.php b/tests/HTMLPurifier/Lexer/DirectLexTest.php index 19ec0ad0..37c516f3 100644 --- a/tests/HTMLPurifier/Lexer/DirectLexTest.php +++ b/tests/HTMLPurifier/Lexer/DirectLexTest.php @@ -64,6 +64,65 @@ class HTMLPurifier_Lexer_DirectLexTest extends UnitTestCase } + function testLineNumbers() { + + $html = '<b>Line 1</b> + <i>Line 2</i> + Still Line 2<br + />Now Line 4 + + <br />'; + + $expect = array( + // line 1 + 0 => new HTMLPurifier_Token_Start('b') + ,1 => new HTMLPurifier_Token_Text('Line 1') + ,2 => new HTMLPurifier_Token_End('b') + ,3 => new HTMLPurifier_Token_Text(' + ') + // line 2 + ,4 => new HTMLPurifier_Token_Start('i') + ,5 => new HTMLPurifier_Token_Text('Line 2') + ,6 => new HTMLPurifier_Token_End('i') + ,7 => new HTMLPurifier_Token_Text(' + Still Line 2') + // line 3 + ,8 => new HTMLPurifier_Token_Empty('br') + // line 4 + ,9 => new HTMLPurifier_Token_Text('Now Line 4 + + ') + // line SIX + ,10 => new HTMLPurifier_Token_Empty('br') + ); + + $context = new HTMLPurifier_Context(); + $config = HTMLPurifier_Config::createDefault(); + $output = $this->DirectLex->tokenizeHTML($html, $config, $context); + + $this->assertIdentical($output, $expect); + + $context = new HTMLPurifier_Context(); + $config = HTMLPurifier_Config::create(array( + 'Core.MaintainLineNumbers' => true + )); + $expect[0]->line = 1; + $expect[1]->line = 1; + $expect[2]->line = 1; + $expect[3]->line = 1; + $expect[4]->line = 2; + $expect[5]->line = 2; + $expect[6]->line = 2; + $expect[7]->line = 2; + $expect[8]->line = 3; + $expect[9]->line = 4; + $expect[10]->line = 6; + + $output = $this->DirectLex->tokenizeHTML($html, $config, $context); + $this->assertIdentical($output, $expect); + + } + } diff --git a/tests/HTMLPurifier/LexerTest.php b/tests/HTMLPurifier/LexerTest.php index b5c672b9..0228b834 100644 --- a/tests/HTMLPurifier/LexerTest.php +++ b/tests/HTMLPurifier/LexerTest.php @@ -34,6 +34,12 @@ class HTMLPurifier_LexerTest extends UnitTestCase } + function test_create() { + $config = HTMLPurifier_Config::create(array('Core.MaintainLineNumbers' => true)); + $lexer = HTMLPurifier_Lexer::create($config); + $this->assertIsA($lexer, 'HTMLPurifier_Lexer_DirectLex'); + } + function assertExtractBody($text, $extract = true) { $result = $this->Lexer->extractBody($text); if ($extract === true) $extract = $text; diff --git a/tests/HTMLPurifier/Strategy/FixNestingTest.php b/tests/HTMLPurifier/Strategy/FixNestingTest.php index 20636614..e68583a0 100644 --- a/tests/HTMLPurifier/Strategy/FixNestingTest.php +++ b/tests/HTMLPurifier/Strategy/FixNestingTest.php @@ -11,13 +11,12 @@ class HTMLPurifier_Strategy_FixNestingTest extends HTMLPurifier_StrategyHarness $this->obj = new HTMLPurifier_Strategy_FixNesting(); } - function test() { + function testBlockAndInlineIntegration() { // legal inline $this->assertResult('<b>Bold text</b>'); - // legal inline and block - // as the parent element is considered FLOW + // legal inline and block (default parent element is FLOW) $this->assertResult('<a href="about:blank">Blank</a><div>Block</div>'); // illegal block in inline @@ -33,6 +32,10 @@ class HTMLPurifier_Strategy_FixNestingTest extends HTMLPurifier_StrategyHarness array('Core.EscapeInvalidChildren' => true) ); + } + + function testNodeRemovalIntegration() { + // test of empty set that's required, resulting in removal of node $this->assertResult('<ul></ul>', ''); @@ -42,27 +45,17 @@ class HTMLPurifier_Strategy_FixNestingTest extends HTMLPurifier_StrategyHarness '<ul><li>Legal item</li></ul>' ); + } + + function testTableIntegration() { // test custom table definition $this->assertResult( - '<table><tr><td>Cell 1</td></tr></table>', '<table><tr><td>Cell 1</td></tr></table>' ); $this->assertResult('<table></table>', ''); - - // breaks without the redundant checking code - $this->assertResult('<table><tr></tr></table>', ''); - - // special case, prevents scrolling one back to find parent - $this->assertResult('<table><tr></tr><tr></tr></table>', ''); - - // cascading rollbacks - $this->assertResult( - '<table><tbody><tr></tr><tr></tr></tbody><tr></tr><tr></tr></table>', - '' - ); - - // rollbacks twice - $this->assertResult('<table></table><table></table>', ''); + } + + function testChameleonIntegration() { // block in inline ins not allowed $this->assertResult( @@ -82,12 +75,6 @@ class HTMLPurifier_Strategy_FixNestingTest extends HTMLPurifier_StrategyHarness '<h1><ins>Not allowed!</ins></h1>' ); - // test exclusions - $this->assertResult( - '<a><span><a>Not allowed</a></span></a>', - '<a><span></span></a>' - ); - // stacked ins/del $this->assertResult( '<h1><ins><del><div>Not allowed!</div></del></ins></h1>', @@ -97,6 +84,17 @@ class HTMLPurifier_Strategy_FixNestingTest extends HTMLPurifier_StrategyHarness '<div><ins><del><div>Allowed!</div></del></ins></div>' ); + } + + function testExclusionsIntegration() { + // test exclusions + $this->assertResult( + '<a><span><a>Not allowed</a></span></a>', + '<a><span></span></a>' + ); + } + + function testCustomParentIntegration() { // test inline parent $this->assertResult( '<b>Bold</b>', true, array('HTML.Parent' => 'span') @@ -105,13 +103,31 @@ class HTMLPurifier_Strategy_FixNestingTest extends HTMLPurifier_StrategyHarness '<div>Reject</div>', 'Reject', array('HTML.Parent' => 'span') ); + // test fallback to div $this->expectError('Cannot use unrecognized element as parent.'); $this->assertResult( - '<div>Accept</div>', true, array('HTML.Parent' => 'script') + '<div>Accept</div>', true, array('HTML.Parent' => 'obviously-impossible') ); } + function testDoubleCheckIntegration() { + // breaks without the redundant checking code + $this->assertResult('<table><tr></tr></table>', ''); + + // special case, prevents scrolling one back to find parent + $this->assertResult('<table><tr></tr><tr></tr></table>', ''); + + // cascading rollbacks + $this->assertResult( + '<table><tbody><tr></tr><tr></tr></tbody><tr></tr><tr></tr></table>', + '' + ); + + // rollbacks twice + $this->assertResult('<table></table><table></table>', ''); + } + } ?> \ No newline at end of file diff --git a/tests/HTMLPurifier/Strategy/MakeWellFormedTest.php b/tests/HTMLPurifier/Strategy/MakeWellFormedTest.php index 07e9202d..3aa7e302 100644 --- a/tests/HTMLPurifier/Strategy/MakeWellFormedTest.php +++ b/tests/HTMLPurifier/Strategy/MakeWellFormedTest.php @@ -11,11 +11,12 @@ class HTMLPurifier_Strategy_MakeWellFormedTest extends HTMLPurifier_StrategyHarn $this->obj = new HTMLPurifier_Strategy_MakeWellFormed(); } - function test() { - + function testNormalIntegration() { $this->assertResult(''); $this->assertResult('This is <b>bold text</b>.'); - + } + + function testUnclosedTagIntegration() { $this->assertResult( '<b>Unclosed tag, gasp!', '<b>Unclosed tag, gasp!</b>' @@ -30,7 +31,9 @@ class HTMLPurifier_Strategy_MakeWellFormedTest extends HTMLPurifier_StrategyHarn 'Unused end tags... recycle!</b>', 'Unused end tags... recycle!' ); - + } + + function testEmptyTagDetectionIntegration() { $this->assertResult( '<br style="clear:both;">', '<br style="clear:both;" />' @@ -40,8 +43,10 @@ class HTMLPurifier_Strategy_MakeWellFormedTest extends HTMLPurifier_StrategyHarn '<div style="clear:both;" />', '<div style="clear:both;"></div>' ); - - // test automatic paragraph closing + } + + function testAutoClose() { + // paragraph $this->assertResult( '<p>Paragraph 1<p>Paragraph 2', @@ -53,12 +58,20 @@ class HTMLPurifier_Strategy_MakeWellFormedTest extends HTMLPurifier_StrategyHarn '<div><p>Paragraphs</p><p>In</p><p>A</p><p>Div</p></div>' ); - // automatic list closing + // list $this->assertResult( '<ol><li>Item 1<li>Item 2</ol>', '<ol><li>Item 1</li><li>Item 2</li></ol>' ); + + // colgroup + + $this->assertResult( + '<table><colgroup><col /><tr></tr></table>', + '<table><colgroup><col /></colgroup><tr></tr></table>' + ); + } } diff --git a/tests/HTMLPurifier/Strategy/RemoveForeignElementsTest.php b/tests/HTMLPurifier/Strategy/RemoveForeignElementsTest.php index 9ec193dc..a357abbb 100644 --- a/tests/HTMLPurifier/Strategy/RemoveForeignElementsTest.php +++ b/tests/HTMLPurifier/Strategy/RemoveForeignElementsTest.php @@ -14,6 +14,7 @@ class HTMLPurifier_Strategy_RemoveForeignElementsTest function test() { + $this->config = array('HTML.Doctype' => 'XHTML 1.0 Strict'); $this->assertResult(''); @@ -24,6 +25,17 @@ class HTMLPurifier_Strategy_RemoveForeignElementsTest 'BlingBong' ); + $this->assertResult( + '<script>alert();</script>', + '' + ); + + $this->assertResult( + '<script>alert();</script>', + 'alert();', + array('Core.RemoveScriptContents' => false) + ); + $this->assertResult( '<menu><li>Item 1</li></menu>', '<ul><li>Item 1</li></ul>' @@ -49,7 +61,7 @@ class HTMLPurifier_Strategy_RemoveForeignElementsTest ); // test preservation of valid img tag - $this->assertResult('<img src="foobar.gif" />'); + $this->assertResult('<img src="foobar.gif" alt="foobar.gif" />'); // test preservation of invalid img tag when removal is disabled $this->assertResult( @@ -60,6 +72,13 @@ class HTMLPurifier_Strategy_RemoveForeignElementsTest ) ); + // test transform to unallowed element + $this->assertResult( + '<font color="red" face="Arial" size="6">Big Warning!</font>', + 'Big Warning!', + array('HTML.Allowed' => 'div') + ); + } } diff --git a/tests/HTMLPurifier/Strategy/ValidateAttributesTest.php b/tests/HTMLPurifier/Strategy/ValidateAttributesTest.php index bba4e99d..06b8060a 100644 --- a/tests/HTMLPurifier/Strategy/ValidateAttributesTest.php +++ b/tests/HTMLPurifier/Strategy/ValidateAttributesTest.php @@ -11,6 +11,7 @@ class HTMLPurifier_Strategy_ValidateAttributesTest extends function setUp() { parent::setUp(); $this->obj = new HTMLPurifier_Strategy_ValidateAttributes(); + $this->config = array('HTML.Doctype' => 'XHTML 1.0 Strict'); } function testEmpty() { @@ -155,8 +156,8 @@ class HTMLPurifier_Strategy_ValidateAttributesTest extends ); // lengths $this->assertResult( - '<td height="10" width="5%" /><th height="5%" width="10" /><hr width="10" height="10" />', - '<td style="height:10px;width:5%;" /><th style="height:5%;width:10px;" /><hr style="width:10px;" />' + '<td width="5%" height="10" /><th width="10" height="5%" /><hr width="10" height="10" />', + '<td style="width:5%;height:10px;" /><th style="width:10px;height:5%;" /><hr style="width:10px;" />' ); // td boolean transformation $this->assertResult( @@ -216,11 +217,10 @@ class HTMLPurifier_Strategy_ValidateAttributesTest extends } function testImg() { - // (this should never happen, as RemoveForeignElements - // should have removed the offending image tag) $this->assertResult( '<img />', - '<img src="" alt="Invalid image" />' + '<img src="" alt="Invalid image" />', + array('Core.RemoveInvalidImg' => false) ); $this->assertResult( @@ -230,12 +230,14 @@ class HTMLPurifier_Strategy_ValidateAttributesTest extends $this->assertResult( '<img alt="pretty picture" />', - '<img alt="pretty picture" src="" />' + '<img alt="pretty picture" src="" />', + array('Core.RemoveInvalidImg' => false) ); // mailto in image is not allowed $this->assertResult( '<img src="mailto:foo@example.com" />', - '<img src="" alt="Invalid image" />' + '<img alt="mailto:foo@example.com" src="" />', + array('Core.RemoveInvalidImg' => false) ); // align transformation $this->assertResult( @@ -297,7 +299,8 @@ class HTMLPurifier_Strategy_ValidateAttributesTest extends $this->assertResult( '<a href="foo" target="_top" />', true, - array('Attr.AllowedFrameTargets' => '_top') + array('Attr.AllowedFrameTargets' => '_top', + 'HTML.Doctype' => 'XHTML 1.0 Transitional') ); $this->assertResult( '<a href="foo" target="_top" />', diff --git a/tests/HTMLPurifier/TagTransformTest.php b/tests/HTMLPurifier/TagTransformTest.php index 51317c04..ed9828ad 100644 --- a/tests/HTMLPurifier/TagTransformTest.php +++ b/tests/HTMLPurifier/TagTransformTest.php @@ -3,7 +3,6 @@ require_once 'HTMLPurifier/TagTransform.php'; // needs to be seperated into files -require_once 'HTMLPurifier/TagTransform/Center.php'; require_once 'HTMLPurifier/TagTransform/Font.php'; require_once 'HTMLPurifier/TagTransform/Simple.php'; @@ -104,9 +103,9 @@ class HTMLPurifier_TagTransformTest extends UnitTestCase } - function testCenter() { + function testSimpleWithCSS() { - $transformer = new HTMLPurifier_TagTransform_Center(); + $transformer = new HTMLPurifier_TagTransform_Simple('div', 'text-align:center;'); $this->assertTransformation( $transformer, diff --git a/tests/HTMLPurifier/URISchemeRegistryTest.php b/tests/HTMLPurifier/URISchemeRegistryTest.php index 21a24348..8edc3bdc 100644 --- a/tests/HTMLPurifier/URISchemeRegistryTest.php +++ b/tests/HTMLPurifier/URISchemeRegistryTest.php @@ -9,9 +9,10 @@ class HTMLPurifier_URISchemeRegistryTest extends UnitTestCase generate_mock_once('HTMLPurifier_URIScheme'); - $config = HTMLPurifier_Config::createDefault(); - $config->set('URI', 'AllowedSchemes', array('http' => true, 'telnet' => true)); - $config->set('URI', 'OverrideAllowedSchemes', true); + $config = HTMLPurifier_Config::create(array( + 'URI.AllowedSchemes' => 'http, telnet', + 'URI.OverrideAllowedSchemes' => true + )); $context = new HTMLPurifier_Context(); $registry = new HTMLPurifier_URISchemeRegistry(); @@ -34,7 +35,10 @@ class HTMLPurifier_URISchemeRegistryTest extends UnitTestCase $this->assertIdentical($registry->getScheme('foobar', $config, $context), $scheme_foobar); // now, test when overriding is not allowed - $config->set('URI', 'OverrideAllowedSchemes', false); + $config = HTMLPurifier_Config::create(array( + 'URI.AllowedSchemes' => 'http, telnet', + 'URI.OverrideAllowedSchemes' => false + )); $this->assertNull($registry->getScheme('foobar', $config, $context)); // scheme not allowed and never registered diff --git a/tests/HTMLPurifier/Test.php b/tests/HTMLPurifierTest.php similarity index 93% rename from tests/HTMLPurifier/Test.php rename to tests/HTMLPurifierTest.php index 0fc83b84..d39be235 100644 --- a/tests/HTMLPurifier/Test.php +++ b/tests/HTMLPurifierTest.php @@ -4,7 +4,7 @@ require_once 'HTMLPurifier.php'; // integration test -class HTMLPurifier_Test extends UnitTestCase +class HTMLPurifierTest extends UnitTestCase { var $purifier; @@ -29,7 +29,7 @@ class HTMLPurifier_Test extends UnitTestCase $this->assertPurification( '<u>Illegal underline</u>', - 'Illegal underline' + '<span style="text-decoration:underline;">Illegal underline</span>' ); $this->assertPurification( @@ -76,7 +76,7 @@ class HTMLPurifier_Test extends UnitTestCase $this->purifier->purifyArray( array('Good', '<b>Sketchy', 'foo' => '<script>bad</script>') ), - array('Good', '<b>Sketchy</b>', 'foo' => 'bad') + array('Good', '<b>Sketchy</b>', 'foo' => '') ); $this->assertIsA($this->purifier->context, 'array'); diff --git a/tests/index.php b/tests/index.php index bc2e2414..e823a347 100644 --- a/tests/index.php +++ b/tests/index.php @@ -38,8 +38,11 @@ if ( is_string($GLOBALS['HTMLPurifierTest']['PEAR']) ) { } // initialize and load HTML Purifier -set_include_path('../library' . PATH_SEPARATOR . get_include_path()); -require_once 'HTMLPurifier.php'; +require_once '../library/HTMLPurifier.auto.php'; + +// setup special DefinitionCacheFactory decorator +$factory =& HTMLPurifier_DefinitionCacheFactory::instance(); +$factory->addDecorator('Memory'); // since we deal with a lot of config objects // load tests $test_files = array(); @@ -61,19 +64,17 @@ if (isset($_GET['f']) && isset($test_file_lookup[$_GET['f']])) { // we can't use addTestFile because SimpleTest chokes on E_STRICT warnings if ($test_file = $GLOBALS['HTMLPurifierTest']['File']) { - $test = new GroupTest($test_file . ' - HTML Purifier'); - $path = 'HTMLPurifier/' . $test_file; - require_once $path; - $test->addTestClass(path2class($path)); + $test = new GroupTest($test_file); + require_once $test_file; + $test->addTestClass(path2class($test_file)); } else { - $test = new GroupTest('All Tests - HTML Purifier'); + $test = new GroupTest('All Tests'); foreach ($test_files as $test_file) { - $path = 'HTMLPurifier/' . $test_file; - require_once $path; - $test->addTestClass(path2class($path)); + require_once $test_file; + $test->addTestClass(path2class($test_file)); } } diff --git a/tests/test_files.php b/tests/test_files.php index 191e3b88..c2c8bada 100644 --- a/tests/test_files.php +++ b/tests/test_files.php @@ -3,83 +3,107 @@ if (!defined('HTMLPurifierTest')) exit; // define callable test files (sorted alphabetically) -$test_files[] = 'AttrDef/CSS/BackgroundPositionTest.php'; -$test_files[] = 'AttrDef/CSS/BackgroundTest.php'; -$test_files[] = 'AttrDef/CSS/BorderTest.php'; -$test_files[] = 'AttrDef/CSS/ColorTest.php'; -$test_files[] = 'AttrDef/CSS/CompositeTest.php'; -$test_files[] = 'AttrDef/CSS/FontFamilyTest.php'; -$test_files[] = 'AttrDef/CSS/FontTest.php'; -$test_files[] = 'AttrDef/CSS/LengthTest.php'; -$test_files[] = 'AttrDef/CSS/ListStyleTest.php'; -$test_files[] = 'AttrDef/CSS/MultipleTest.php'; -$test_files[] = 'AttrDef/CSS/NumberTest.php'; -$test_files[] = 'AttrDef/CSS/PercentageTest.php'; -$test_files[] = 'AttrDef/CSS/TextDecorationTest.php'; -$test_files[] = 'AttrDef/CSS/URITest.php'; -$test_files[] = 'AttrDef/CSSTest.php'; -$test_files[] = 'AttrDef/EnumTest.php'; -$test_files[] = 'AttrDef/HTML/IDTest.php'; -$test_files[] = 'AttrDef/HTML/LengthTest.php'; -$test_files[] = 'AttrDef/HTML/FrameTargetTest.php'; -$test_files[] = 'AttrDef/HTML/MultiLengthTest.php'; -$test_files[] = 'AttrDef/HTML/NmtokensTest.php'; -$test_files[] = 'AttrDef/HTML/PixelsTest.php'; -$test_files[] = 'AttrDef/HTML/LinkTypesTest.php'; -$test_files[] = 'AttrDef/IntegerTest.php'; -$test_files[] = 'AttrDef/LangTest.php'; -$test_files[] = 'AttrDef/TextTest.php'; -$test_files[] = 'AttrDef/URI/Email/SimpleCheckTest.php'; -$test_files[] = 'AttrDef/URI/HostTest.php'; -$test_files[] = 'AttrDef/URI/IPv4Test.php'; -$test_files[] = 'AttrDef/URI/IPv6Test.php'; -$test_files[] = 'AttrDef/URITest.php'; -$test_files[] = 'AttrDefTest.php'; -$test_files[] = 'AttrTransformTest.php'; -$test_files[] = 'AttrTransform/BdoDirTest.php'; -$test_files[] = 'AttrTransform/BgColorTest.php'; -$test_files[] = 'AttrTransform/BoolToCSSTest.php'; -$test_files[] = 'AttrTransform/BorderTest.php'; -$test_files[] = 'AttrTransform/EnumToCSSTest.php'; -$test_files[] = 'AttrTransform/ImgRequiredTest.php'; -$test_files[] = 'AttrTransform/ImgSpaceTest.php'; -$test_files[] = 'AttrTransform/LangTest.php'; -$test_files[] = 'AttrTransform/LengthTest.php'; -$test_files[] = 'AttrTransform/NameTest.php'; -$test_files[] = 'ChildDef/ChameleonTest.php'; -$test_files[] = 'ChildDef/CustomTest.php'; -$test_files[] = 'ChildDef/OptionalTest.php'; -$test_files[] = 'ChildDef/RequiredTest.php'; -$test_files[] = 'ChildDef/StrictBlockquoteTest.php'; -$test_files[] = 'ChildDef/TableTest.php'; -$test_files[] = 'ConfigSchemaTest.php'; -$test_files[] = 'ConfigTest.php'; -$test_files[] = 'ContextTest.php'; -$test_files[] = 'EncoderTest.php'; -$test_files[] = 'EntityLookupTest.php'; -$test_files[] = 'EntityParserTest.php'; -$test_files[] = 'GeneratorTest.php'; -$test_files[] = 'HTMLModuleManagerTest.php'; -$test_files[] = 'IDAccumulatorTest.php'; -$test_files[] = 'LanguageFactoryTest.php'; -$test_files[] = 'LanguageTest.php'; -$test_files[] = 'Lexer/DirectLexTest.php'; -$test_files[] = 'LexerTest.php'; -$test_files[] = 'PercentEncoderTest.php'; -$test_files[] = 'Strategy/CompositeTest.php'; -$test_files[] = 'Strategy/CoreTest.php'; -$test_files[] = 'Strategy/FixNestingTest.php'; -$test_files[] = 'Strategy/MakeWellFormedTest.php'; -$test_files[] = 'Strategy/RemoveForeignElementsTest.php'; -$test_files[] = 'Strategy/ValidateAttributesTest.php'; -$test_files[] = 'TagTransformTest.php'; -$test_files[] = 'Test.php'; -$test_files[] = 'TokenTest.php'; -$test_files[] = 'URISchemeRegistryTest.php'; -$test_files[] = 'URISchemeTest.php'; + +// HTML Purifier main library + +$test_files[] = 'HTMLPurifier/AttrCollectionsTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/BackgroundPositionTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/BackgroundTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/BorderTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/ColorTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/CompositeTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/FontFamilyTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/FontTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/LengthTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/ListStyleTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/MultipleTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/NumberTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/PercentageTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/TextDecorationTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSS/URITest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/CSSTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/EnumTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/HTML/ColorTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/HTML/IDTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/HTML/LengthTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/HTML/FrameTargetTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/HTML/MultiLengthTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/HTML/NmtokensTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/HTML/PixelsTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/HTML/LinkTypesTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/IntegerTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/LangTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/TextTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/URI/Email/SimpleCheckTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/URI/HostTest.php'; +$test_files[] = 'HTMLPurifier/AttrDef/URI/IPv4Test.php'; +$test_files[] = 'HTMLPurifier/AttrDef/URI/IPv6Test.php'; +$test_files[] = 'HTMLPurifier/AttrDef/URITest.php'; +$test_files[] = 'HTMLPurifier/AttrDefTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransformTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/BdoDirTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/BgColorTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/BoolToCSSTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/BorderTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/EnumToCSSTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/ImgRequiredTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/ImgSpaceTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/LangTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/LengthTest.php'; +$test_files[] = 'HTMLPurifier/AttrTransform/NameTest.php'; +$test_files[] = 'HTMLPurifier/AttrTypesTest.php'; +$test_files[] = 'HTMLPurifier/ChildDef/ChameleonTest.php'; +$test_files[] = 'HTMLPurifier/ChildDef/CustomTest.php'; +$test_files[] = 'HTMLPurifier/ChildDef/OptionalTest.php'; +$test_files[] = 'HTMLPurifier/ChildDef/RequiredTest.php'; +$test_files[] = 'HTMLPurifier/ChildDef/StrictBlockquoteTest.php'; +$test_files[] = 'HTMLPurifier/ChildDef/TableTest.php'; +$test_files[] = 'HTMLPurifier/ConfigSchemaTest.php'; +$test_files[] = 'HTMLPurifier/ConfigTest.php'; +$test_files[] = 'HTMLPurifier/ContextTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCacheFactoryTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCacheTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCache/Decorator/CleanupTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCache/Decorator/MemoryTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCache/DecoratorTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCache/SerializerTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionTest.php'; +$test_files[] = 'HTMLPurifier/DoctypeRegistryTest.php'; +$test_files[] = 'HTMLPurifier/ElementDefTest.php'; +$test_files[] = 'HTMLPurifier/ErrorCollectorTest.php'; +$test_files[] = 'HTMLPurifier/EncoderTest.php'; +$test_files[] = 'HTMLPurifier/EntityLookupTest.php'; +$test_files[] = 'HTMLPurifier/EntityParserTest.php'; +$test_files[] = 'HTMLPurifier/GeneratorTest.php'; +$test_files[] = 'HTMLPurifier/HTMLDefinitionTest.php'; +$test_files[] = 'HTMLPurifier/HTMLModuleManagerTest.php'; +$test_files[] = 'HTMLPurifier/HTMLModuleTest.php'; +$test_files[] = 'HTMLPurifier/HTMLModule/ScriptingTest.php'; +$test_files[] = 'HTMLPurifier/HTMLModule/TidyTest.php'; +$test_files[] = 'HTMLPurifier/IDAccumulatorTest.php'; +$test_files[] = 'HTMLPurifier/LanguageFactoryTest.php'; +$test_files[] = 'HTMLPurifier/LanguageTest.php'; +$test_files[] = 'HTMLPurifier/Lexer/DirectLexTest.php'; +$test_files[] = 'HTMLPurifier/LexerTest.php'; +$test_files[] = 'HTMLPurifier/PercentEncoderTest.php'; +$test_files[] = 'HTMLPurifier/Strategy/CompositeTest.php'; +$test_files[] = 'HTMLPurifier/Strategy/CoreTest.php'; +$test_files[] = 'HTMLPurifier/Strategy/FixNestingTest.php'; +$test_files[] = 'HTMLPurifier/Strategy/MakeWellFormedTest.php'; +$test_files[] = 'HTMLPurifier/Strategy/RemoveForeignElementsTest.php'; +$test_files[] = 'HTMLPurifier/Strategy/ValidateAttributesTest.php'; +$test_files[] = 'HTMLPurifier/TagTransformTest.php'; +$test_files[] = 'HTMLPurifier/TokenTest.php'; +$test_files[] = 'HTMLPurifier/URISchemeRegistryTest.php'; +$test_files[] = 'HTMLPurifier/URISchemeTest.php'; +$test_files[] = 'HTMLPurifierTest.php'; if (version_compare(PHP_VERSION, '5', '>=')) { - $test_files[] = 'TokenFactoryTest.php'; + $test_files[] = 'HTMLPurifier/TokenFactoryTest.php'; } +// ConfigDoc auxiliary library + +// ... none yet + ?> \ No newline at end of file