1
0
mirror of https://github.com/ezyang/htmlpurifier.git synced 2025-08-04 21:28:06 +02:00

Merge in r657-674, prompted by near release of 1.4.0.

git-svn-id: http://htmlpurifier.org/svnroot/htmlpurifier/branches/strict@675 48356398-32a2-884e-a903-53898d9a118a
This commit is contained in:
Edward Z. Yang
2007-01-21 16:07:36 +00:00
parent 37ea1673dd
commit 9a84e11f34
56 changed files with 1356 additions and 509 deletions

View File

@@ -0,0 +1,87 @@
<?php
require_once 'HTMLPurifier/AttrDef.php';
require_once 'HTMLPurifier/CSSDefinition.php';
/**
* Validates shorthand CSS property background.
* @warning Does not support url tokens that have internal spaces.
*/
class HTMLPurifier_AttrDef_Background extends HTMLPurifier_AttrDef
{
/**
* Local copy of component validators.
* @note See HTMLPurifier_AttrDef_Font::$info for a similar impl.
*/
var $info;
function HTMLPurifier_AttrDef_Background($config) {
$def = $config->getCSSDefinition();
$this->info['background-color'] = $def->info['background-color'];
$this->info['background-image'] = $def->info['background-image'];
$this->info['background-repeat'] = $def->info['background-repeat'];
$this->info['background-attachment'] = $def->info['background-attachment'];
$this->info['background-position'] = $def->info['background-position'];
}
function validate($string, $config, &$context) {
// regular pre-processing
$string = $this->parseCDATA($string);
if ($string === '') return false;
// assumes URI doesn't have spaces in it
$bits = explode(' ', strtolower($string)); // bits to process
$caught = array();
$caught['color'] = false;
$caught['image'] = false;
$caught['repeat'] = false;
$caught['attachment'] = false;
$caught['position'] = false;
$i = 0; // number of catches
$none = false;
foreach ($bits as $bit) {
if ($bit === '') continue;
foreach ($caught as $key => $status) {
if ($key != 'position') {
if ($status !== false) continue;
$r = $this->info['background-' . $key]->validate($bit, $config, $context);
} else {
$r = $bit;
}
if ($r === false) continue;
if ($key == 'position') {
if ($caught[$key] === false) $caught[$key] = '';
$caught[$key] .= $r . ' ';
} else {
$caught[$key] = $r;
}
$i++;
break;
}
}
if (!$i) return false;
if ($caught['position'] !== false) {
$caught['position'] = $this->info['background-position']->
validate($caught['position'], $config, $context);
}
$ret = array();
foreach ($caught as $value) {
if ($value === false) continue;
$ret[] = $value;
}
if (empty($ret)) return false;
return implode(' ', $ret);
}
}
?>

View File

@@ -0,0 +1,130 @@
<?php
require_once 'HTMLPurifier/AttrDef.php';
require_once 'HTMLPurifier/AttrDef/CSSLength.php';
require_once 'HTMLPurifier/AttrDef/Percentage.php';
/* W3C says:
[ // adjective and number must be in correct order, even if
// you could switch them without introducing ambiguity.
// some browsers support that syntax
[
<percentage> | <length> | left | center | right
]
[
<percentage> | <length> | top | center | bottom
]?
] |
[ // this signifies that the vertical and horizontal adjectives
// can be arbitrarily ordered, however, there can only be two,
// one of each, or none at all
[
left | center | right
] ||
[
top | center | bottom
]
]
top, left = 0%
center, (none) = 50%
bottom, right = 100%
*/
/* QuirksMode says:
keyword + length/percentage must be ordered correctly, as per W3C
Internet Explorer and Opera, however, support arbitrary ordering. We
should fix it up.
Minor issue though, not strictly necessary.
*/
// control freaks may appreciate the ability to convert these to
// percentages or something, but it's not necessary
/**
* Validates the value of background-position.
*/
class HTMLPurifier_AttrDef_BackgroundPosition extends HTMLPurifier_AttrDef
{
var $length;
var $percentage;
function HTMLPurifier_AttrDef_BackgroundPosition() {
$this->length = new HTMLPurifier_AttrDef_CSSLength();
$this->percentage = new HTMLPurifier_AttrDef_Percentage();
}
function validate($string, $config, &$context) {
$string = $this->parseCDATA($string);
$bits = explode(' ', $string);
$keywords = array();
$keywords['h'] = false; // left, right
$keywords['v'] = false; // top, bottom
$keywords['c'] = false; // center
$measures = array();
$i = 0;
$lookup = array(
'top' => 'v',
'bottom' => 'v',
'left' => 'h',
'right' => 'h',
'center' => 'c'
);
foreach ($bits as $bit) {
if ($bit === '') continue;
// test for keyword
$lbit = ctype_lower($bit) ? $bit : strtolower($bit);
if (isset($lookup[$lbit])) {
$status = $lookup[$lbit];
$keywords[$status] = $lbit;
$i++;
}
// test for length
$r = $this->length->validate($bit, $config, &$context);
if ($r !== false) {
$measures[] = $r;
$i++;
}
// test for percentage
$r = $this->percentage->validate($bit, $config, &$context);
if ($r !== false) {
$measures[] = $r;
$i++;
}
}
if (!$i) return false; // no valid values were caught
$ret = array();
// first keyword
if ($keywords['h']) $ret[] = $keywords['h'];
elseif (count($measures)) $ret[] = array_shift($measures);
elseif ($keywords['c']) {
$ret[] = $keywords['c'];
$keywords['c'] = false; // prevent re-use: center = center center
}
if ($keywords['v']) $ret[] = $keywords['v'];
elseif (count($measures)) $ret[] = array_shift($measures);
elseif ($keywords['c']) $ret[] = $keywords['c'];
if (empty($ret)) return false;
return implode(' ', $ret);
}
}
?>

View File

@@ -40,6 +40,7 @@ class HTMLPurifier_AttrDef_CSSLength extends HTMLPurifier_AttrDef
// we assume all units are two characters
$unit = substr($length, $strlen - 2);
if (!ctype_lower($unit)) $unit = strtolower($unit);
$number = substr($length, 0, $strlen - 2);
if (!isset($this->units[$unit])) return false;

View File

@@ -53,6 +53,7 @@ class HTMLPurifier_AttrDef_ListStyle extends HTMLPurifier_AttrDef
}
$caught[$key] = $r;
$i++;
break;
}
}

View File

@@ -4,14 +4,13 @@ require_once 'HTMLPurifier/AttrDef.php';
require_once 'HTMLPurifier/AttrDef/Number.php';
/**
* Validates a Percentage as defined by the HTML spec.
* @note This also allows integer pixel values.
* Validates a Percentage as defined by the CSS spec.
*/
class HTMLPurifier_AttrDef_Percentage extends HTMLPurifier_AttrDef
{
/**
* Instance of HTMLPurifier_AttrDef_Number to defer pixel validation
* Instance of HTMLPurifier_AttrDef_Number to defer number validation
*/
var $number_def;

View File

@@ -12,6 +12,8 @@ require_once 'HTMLPurifier/AttrDef/Font.php';
require_once 'HTMLPurifier/AttrDef/Border.php';
require_once 'HTMLPurifier/AttrDef/ListStyle.php';
require_once 'HTMLPurifier/AttrDef/CSSURI.php';
require_once 'HTMLPurifier/AttrDef/BackgroundPosition.php';
require_once 'HTMLPurifier/AttrDef/Background.php';
/**
* Defines allowed CSS attributes and what their values are.
@@ -79,9 +81,7 @@ class HTMLPurifier_CSSDefinition
$this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum(
array('scroll', 'fixed')
);
// pending its own validator as a shorthand
$this->info['background'] =
$this->info['background-position'] = new HTMLPurifier_AttrDef_BackgroundPosition();
$border_color =
$this->info['border-top-color'] =
@@ -93,6 +93,8 @@ class HTMLPurifier_CSSDefinition
new HTMLPurifier_AttrDef_Color()
));
$this->info['background'] = new HTMLPurifier_AttrDef_Background($config);
$this->info['border-color'] = new HTMLPurifier_AttrDef_Multiple($border_color);
$border_width =

View File

@@ -48,14 +48,16 @@ class HTMLPurifier_Config
* Convenience constructor that creates a config object based on a mixed var
* @static
* @param mixed $config Variable that defines the state of the config
* object. Can be: a HTMLPurifier_Config() object or
* an array of directives based on loadArray().
* object. Can be: a HTMLPurifier_Config() object,
* an array of directives based on loadArray(),
* or a string filename of an ini file.
* @return Configured HTMLPurifier_Config object
*/
static function create($config) {
if ($config instanceof HTMLPurifier_Config) return $config;
$ret = HTMLPurifier_Config::createDefault();
if (is_array($config)) $ret->loadArray($config);
if (is_string($config)) $ret->loadIni($config);
elseif (is_array($config)) $ret->loadArray($config);
return $ret;
}
@@ -75,12 +77,17 @@ class HTMLPurifier_Config
* @param $namespace String namespace
* @param $key String key
*/
function get($namespace, $key) {
function get($namespace, $key, $from_alias = false) {
if (!isset($this->def->info[$namespace][$key])) {
trigger_error('Cannot retrieve value of undefined directive',
E_USER_WARNING);
return;
}
if ($this->def->info[$namespace][$key]->class == 'alias') {
trigger_error('Cannot get value from aliased directive, use real name',
E_USER_ERROR);
return;
}
return $this->conf[$namespace][$key];
}
@@ -103,12 +110,22 @@ class HTMLPurifier_Config
* @param $key String key
* @param $value Mixed value
*/
function set($namespace, $key, $value) {
function set($namespace, $key, $value, $from_alias = false) {
if (!isset($this->def->info[$namespace][$key])) {
trigger_error('Cannot set undefined directive 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');
}
$this->set($this->def->info[$namespace][$key]->namespace,
$this->def->info[$namespace][$key]->name,
$value, true);
return;
}
$value = $this->def->validate(
$value,
$this->def->info[$namespace][$key]->type,
@@ -178,6 +195,15 @@ class HTMLPurifier_Config
}
}
/**
* Loads configuration values from an ini file
* @param $filename Name of ini file
*/
function loadIni($filename) {
$array = parse_ini_file($filename, true);
$this->loadArray($array);
}
}
?>

View File

@@ -106,6 +106,11 @@ class HTMLPurifier_ConfigSchema {
E_USER_ERROR);
return;
}
if (empty($description)) {
trigger_error('Description must be non-empty',
E_USER_ERROR);
return;
}
if (isset($def->info[$namespace][$name])) {
if (
$def->info[$namespace][$name]->type !== $type ||
@@ -161,6 +166,11 @@ class HTMLPurifier_ConfigSchema {
E_USER_ERROR);
return;
}
if (empty($description)) {
trigger_error('Description must be non-empty',
E_USER_ERROR);
return;
}
$def->info[$namespace] = array();
$def->info_namespace[$namespace] = new HTMLPurifier_ConfigEntity_Namespace();
$def->info_namespace[$namespace]->description = $description;
@@ -216,12 +226,66 @@ class HTMLPurifier_ConfigSchema {
E_USER_ERROR);
return;
}
if ($def->info[$namespace][$name]->allowed === true) {
$def->info[$namespace][$name]->allowed = array();
$directive =& $def->info[$namespace][$name];
$type = $directive->type;
if ($type != 'string' && $type != 'istring') {
trigger_error('Cannot define allowed values for directive whose type is not string',
E_USER_ERROR);
return;
}
if ($directive->allowed === true) {
$directive->allowed = array();
}
foreach ($allowed_values as $value) {
$def->info[$namespace][$name]->allowed[$value] = true;
$directive->allowed[$value] = true;
}
if ($def->defaults[$namespace][$name] !== null &&
!isset($directive->allowed[$def->defaults[$namespace][$name]])) {
trigger_error('Default value must be in allowed range of variables',
E_USER_ERROR);
$directive->allowed = true; // undo undo!
return;
}
}
/**
* Defines a directive alias for backwards compatibility
* @static
* @param $namespace
* @param $name Directive that will be aliased
* @param $new_namespace
* @param $new_name Directive that the alias will be to
*/
static function defineAlias($namespace, $name, $new_namespace, $new_name) {
$def =& HTMLPurifier_ConfigSchema::instance();
if (!isset($def->info[$namespace])) {
trigger_error('Cannot define directive alias in undefined namespace',
E_USER_ERROR);
return;
}
if (!ctype_alnum($name)) {
trigger_error('Directive name must be alphanumeric',
E_USER_ERROR);
return;
}
if (isset($def->info[$namespace][$name])) {
trigger_error('Cannot define alias over directive',
E_USER_ERROR);
return;
}
if (!isset($def->info[$new_namespace][$new_name])) {
trigger_error('Cannot define alias to undefined directive',
E_USER_ERROR);
return;
}
if ($def->info[$new_namespace][$new_name]->class == 'alias') {
trigger_error('Cannot define alias to alias',
E_USER_ERROR);
return;
}
$def->info[$namespace][$name] =
new HTMLPurifier_ConfigEntity_DirectiveAlias(
$new_namespace, $new_name);
}
/**
@@ -318,13 +382,21 @@ class HTMLPurifier_ConfigSchema {
/**
* Base class for configuration entity
*/
class HTMLPurifier_ConfigEntity {}
class HTMLPurifier_ConfigEntity {
var $class = false;
}
/**
* Structure object describing of a namespace
*/
class HTMLPurifier_ConfigEntity_Namespace extends HTMLPurifier_ConfigEntity {
function HTMLPurifier_ConfigEntity_Namespace($description = null) {
$this->description = $description;
}
var $class = 'namespace';
/**
* String description of what kinds of directives go in this namespace.
*/
@@ -339,15 +411,21 @@ class HTMLPurifier_ConfigEntity_Namespace extends HTMLPurifier_ConfigEntity {
class HTMLPurifier_ConfigEntity_Directive extends HTMLPurifier_ConfigEntity
{
/**
* Hash of value aliases, i.e. values that are equivalent.
*/
var $aliases = array();
var $class = 'directive';
/**
* Lookup table of allowed values of the element, bool true if all allowed.
*/
var $allowed = true;
function HTMLPurifier_ConfigEntity_Directive(
$type = null,
$descriptions = null,
$allow_null = null,
$allowed = null,
$aliases = null
) {
if ( $type !== null) $this->type = $type;
if ($descriptions !== null) $this->descriptions = $descriptions;
if ( $allow_null !== null) $this->allow_null = $allow_null;
if ( $allowed !== null) $this->allowed = $allowed;
if ( $aliases !== null) $this->aliases = $aliases;
}
/**
* Allowed type of the directive. Values are:
@@ -364,16 +442,26 @@ class HTMLPurifier_ConfigEntity_Directive extends HTMLPurifier_ConfigEntity
var $type = 'mixed';
/**
* Is null allowed? Has no affect for mixed type.
* Plaintext descriptions of the configuration entity is. Organized by
* file and line number, so multiple descriptions are allowed.
*/
var $descriptions = array();
/**
* Is null allowed? Has no effect for mixed type.
* @bool
*/
var $allow_null = false;
/**
* Plaintext descriptions of the configuration entity is. Organized by
* file and line number, so multiple descriptions are allowed.
* Lookup table of allowed values of the element, bool true if all allowed.
*/
var $descriptions = array();
var $allowed = true;
/**
* Hash of value aliases, i.e. values that are equivalent.
*/
var $aliases = array();
/**
* Adds a description to the array
@@ -385,4 +473,26 @@ class HTMLPurifier_ConfigEntity_Directive extends HTMLPurifier_ConfigEntity
}
/**
* Structure object describing a directive alias
*/
class HTMLPurifier_ConfigEntity_DirectiveAlias extends HTMLPurifier_ConfigEntity
{
var $class = 'alias';
/**
* Namespace being aliased to
*/
var $namespace;
/**
* Directive being aliased to
*/
var $name;
function HTMLPurifier_ConfigEntity_DirectiveAlias($namespace, $name) {
$this->namespace = $namespace;
$this->name = $name;
}
}
?>

View File

@@ -6,15 +6,29 @@ HTMLPurifier_ConfigSchema::define(
'Core', 'Encoding', 'utf-8', 'istring',
'If for some reason you are unable to convert all webpages to UTF-8, '.
'you can use this directive as a stop-gap compatibility change to '.
'let HTMLPurifier deal with non UTF-8 input. This technique has '.
'let HTML Purifier deal with non UTF-8 input. This technique has '.
'notable deficiencies: absolutely no characters outside of the selected '.
'character encoding will be preserved, not even the ones that have '.
'been ampersand escaped (this is due to a UTF-8 specific <em>feature</em> '.
'that automatically resolves all entities), making it pretty useless '.
'for anything except the most I18N-blind applications. This directive '.
'for anything except the most I18N-blind applications, although '.
'%Core.EscapeNonASCIICharacters offers fixes this trouble with '.
'another tradeoff. This directive '.
'only accepts ISO-8859-1 if iconv is not enabled.'
);
HTMLPurifier_ConfigSchema::define(
'Core', 'EscapeNonASCIICharacters', false, 'bool',
'This directive overcomes a deficiency in %Core.Encoding by blindly '.
'converting all non-ASCII characters into decimal numeric entities before '.
'converting it to its native encoding. This means that even '.
'characters that can be expressed in the non-UTF-8 encoding will '.
'be entity-ized, which can be a real downer for encodings like Big5. '.
'It also assumes that the ASCII repetoire is available, although '.
'this is the case for almost all encodings. Anyway, use UTF-8! This '.
'directive has been available since 1.4.0.'
);
if ( !function_exists('iconv') ) {
// only encodings with native PHP support
HTMLPurifier_ConfigSchema::defineAllowedValues(
@@ -310,6 +324,7 @@ class HTMLPurifier_Encoder
} elseif ($encoding === 'iso-8859-1') {
return @utf8_encode($str);
}
trigger_error('Encoding not supported', E_USER_ERROR);
}
/**
@@ -323,11 +338,63 @@ class HTMLPurifier_Encoder
if ($iconv === null) $iconv = function_exists('iconv');
$encoding = $config->get('Core', 'Encoding');
if ($encoding === 'utf-8') return $str;
if ($config->get('Core', 'EscapeNonASCIICharacters')) {
$str = HTMLPurifier_Encoder::convertToASCIIDumbLossless($str);
}
if ($iconv && !$config->get('Test', 'ForceNoIconv')) {
return @iconv('utf-8', $encoding . '//IGNORE', $str);
} elseif ($encoding === 'iso-8859-1') {
return @utf8_decode($str);
}
trigger_error('Encoding not supported', E_USER_ERROR);
}
/**
* Lossless (character-wise) conversion of HTML to ASCII
* @static
* @param $str UTF-8 string to be converted to ASCII
* @returns ASCII encoded string with non-ASCII character entity-ized
* @warning Adapted from MediaWiki, claiming fair use: this is a common
* algorithm. If you disagree with this license fudgery,
* implement it yourself.
* @note Uses decimal numeric entities since they are best supported.
* @note This is a DUMB function: it has no concept of keeping
* character entities that the projected character encoding
* can allow. We could possibly implement a smart version
* but that would require it to also know which Unicode
* codepoints the charset supported (not an easy task).
* @note Sort of with cleanUTF8() but it assumes that $str is
* well-formed UTF-8
*/
static function convertToASCIIDumbLossless($str) {
$bytesleft = 0;
$result = '';
$working = 0;
$len = strlen($str);
for( $i = 0; $i < $len; $i++ ) {
$bytevalue = ord( $str[$i] );
if( $bytevalue <= 0x7F ) { //0xxx xxxx
$result .= chr( $bytevalue );
$bytesleft = 0;
} elseif( $bytevalue <= 0xBF ) { //10xx xxxx
$working = $working << 6;
$working += ($bytevalue & 0x3F);
$bytesleft--;
if( $bytesleft <= 0 ) {
$result .= "&#" . $working . ";";
}
} elseif( $bytevalue <= 0xDF ) { //110x xxxx
$working = $bytevalue & 0x1F;
$bytesleft = 1;
} elseif( $bytevalue <= 0xEF ) { //1110 xxxx
$working = $bytevalue & 0x0F;
$bytesleft = 2;
} else { //1111 0xxx
$working = $bytevalue & 0x07;
$bytesleft = 3;
}
}
return $result;
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Represents a pre or post processing filter on HTML Purifier's output
*
* Sometimes, a little ad-hoc fixing of HTML has to be done before
* it gets sent through HTML Purifier: you can use filters to acheive
* this effect. For instance, YouTube videos can be preserved using
* this manner. You could have used a decorator for this task, but
* PHP's support for them is not terribly robust, so we're going
* to just loop through the filters.
*
* Filters should be exited first in, last out. If there are three filters,
* named 1, 2 and 3, the order of execution should go 1->preFilter,
* 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter,
* 1->postFilter.
*/
class HTMLPurifier_Filter
{
/**
* Name of the filter for identification purposes
*/
var $name;
/**
* Pre-processor function, handles HTML before HTML Purifier
*/
function preFilter($html, $config, &$context) {}
/**
* Post-processor function, handles HTML after HTML Purifier
*/
function postFilter($html, $config, &$context) {}
}
?>

View File

@@ -0,0 +1,34 @@
<?php
require_once 'HTMLPurifier/Filter.php';
class HTMLPurifier_Filter_YouTube extends HTMLPurifier_Filter
{
var $name = 'YouTube preservation';
function preFilter($html, $config, &$context) {
$pre_regex = '#<object[^>]+>.+?'.
'http://www.youtube.com/v/([A-Za-z0-9]+).+?</object>#';
$pre_replace = '<span class="youtube-embed">\1</span>';
return preg_replace($pre_regex, $pre_replace, $html);
}
function postFilter($html, $config, &$context) {
$post_regex = '#<span class="youtube-embed">([A-Za-z0-9]+)</span>#';
$post_replace = '<object width="425" height="350" '.
'data="http://www.youtube.com/v/\1">'.
'<param name="movie" value="http://www.youtube.com/v/\1"></param>'.
'<param name="wmode" value="transparent"></param>'.
'<!--[if IE]>'.
'<embed src="http://www.youtube.com/v/\1"'.
'type="application/x-shockwave-flash"'.
'wmode="transparent" width="425" height="350" />'.
'<![endif]-->'.
'</object>';
return preg_replace($post_regex, $post_replace, $html);
}
}
?>

View File

@@ -151,7 +151,7 @@ class HTMLPurifier_Lexer
$lexer = $prototype;
}
if (empty($lexer)) {
if (version_compare(PHP_VERSION, '5', '>=')) {
if (class_exists('DOMDocument')) { // check for DOM support
require_once 'HTMLPurifier/Lexer/DOMLex.php';
$lexer = new HTMLPurifier_Lexer_DOMLex();
} else {