/** * @param array $attr * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function transform($attr, $config, $context) { $src = true; if (!isset($attr['src'])) { if ($config->get('Core.RemoveInvalidImg')) { return $attr; } $attr['src'] = $config->get('Attr.DefaultInvalidImage'); $src = false; } if (!isset($attr['alt'])) { if ($src) { $alt = $config->get('Attr.DefaultImageAlt'); if ($alt === null) { // truncate if the alt is too long $attr['alt'] = substr(basename($attr['src']), 0, 40); } else { $attr['alt'] = $alt; } } else { $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt'); } } return $attr; }
/** * @param HTMLPurifier_Config $config */ public function setup($config) { $max = $config->get('HTML.MaxImgLength'); $img = $this->addElement('img', 'Inline', 'Empty', 'Common', array('alt*' => 'Text', 'height' => 'Pixels#' . $max, 'width' => 'Pixels#' . $max, 'longdesc' => 'URI', 'src*' => new HTMLPurifier_AttrDef_URI(true))); if ($max === null || $config->get('HTML.Trusted')) { $img->attr['height'] = $img->attr['width'] = 'Length'; } // kind of strange, but splitting things up would be inefficient $img->attr_transform_pre[] = $img->attr_transform_post[] = new HTMLPurifier_AttrTransform_ImgRequired(); }
/** * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context */ public function __construct($config, $context) { $this->config = $config; $this->_scriptFix = $config->get('Output.CommentScriptContents'); $this->_innerHTMLFix = $config->get('Output.FixInnerHTML'); $this->_sortAttr = $config->get('Output.SortAttr'); $this->_flashCompat = $config->get('Output.FlashCompat'); $this->_def = $config->getHTMLDefinition(); $this->_xhtml = $this->_def->doctype->xml; }
/** * @param HTMLPurifier_Config $config * @return bool */ public function prepare($config) { $this->target = $config->get('URI.' . $this->name); $this->parser = new HTMLPurifier_URIParser(); $this->doEmbed = $config->get('URI.MungeResources'); $this->secretKey = $config->get('URI.MungeSecretKey'); if ($this->secretKey && !function_exists('hash_hmac')) { throw new Exception("Cannot use %URI.MungeSecretKey without hash_hmac support."); } return true; }
/** * @param array $tokens * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ protected function filter($tokens, $config, $context) { $allowed = $config->get('Attr.AllowedClasses'); $forbidden = $config->get('Attr.ForbiddenClasses'); $ret = array(); foreach ($tokens as $token) { if (($allowed === null || isset($allowed[$token])) && !isset($forbidden[$token]) && !in_array($token, $ret, true)) { $ret[] = $token; } } return $ret; }
/** * @param string $id * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool|string */ public function validate($id, $config, $context) { if (!$this->selector && !$config->get('Attr.EnableID')) { return false; } $id = trim($id); // trim it first if ($id === '') { return false; } $prefix = $config->get('Attr.IDPrefix'); if ($prefix !== '') { $prefix .= $config->get('Attr.IDPrefixLocal'); // prevent re-appending the prefix if (strpos($id, $prefix) !== 0) { $id = $prefix . $id; } } elseif ($config->get('Attr.IDPrefixLocal') !== '') { trigger_error('%Attr.IDPrefixLocal cannot be used unless ' . '%Attr.IDPrefix is set', E_USER_WARNING); } if (!$this->selector) { $id_accumulator =& $context->get('IDAccumulator'); if (isset($id_accumulator->ids[$id])) { return false; } } // we purposely avoid using regex, hopefully this is faster if (ctype_alpha($id)) { $result = true; } else { if (!ctype_alpha(@$id[0])) { return false; } // primitive style of regexps, I suppose $trim = trim($id, 'A..Za..z0..9:-._'); $result = $trim === ''; } $regexp = $config->get('Attr.IDBlacklistRegexp'); if ($regexp && preg_match($regexp, $id)) { return false; } if (!$this->selector && $result) { $id_accumulator->add($id); } // if no change was made to the ID, return the result // else, return the new id if stripping whitespace made it // valid, or return false. return $result ? $id : false; }
/** * Public interface for validating components of a URI. Performs a * bunch of default actions. Don't overload this method. * @param HTMLPurifier_URI $uri Reference to a HTMLPurifier_URI object * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool success or failure */ public function validate(&$uri, $config, $context) { if ($this->default_port == $uri->port) { $uri->port = null; } // kludge: browsers do funny things when the scheme but not the // authority is set if (!$this->may_omit_host && (!is_null($uri->scheme) && ($uri->host === '' || is_null($uri->host))) || is_null($uri->scheme) && $uri->host === '') { do { if (is_null($uri->scheme)) { if (substr($uri->path, 0, 2) != '//') { $uri->host = null; break; } // URI is '////path', so we cannot nullify the // host to preserve semantics. Try expanding the // hostname instead (fall through) } // first see if we can manually insert a hostname $host = $config->get('URI.Host'); if (!is_null($host)) { $uri->host = $host; } else { // we can't do anything sensible, reject the URL. return false; } } while (false); } return $this->doValidate($uri, $config, $context); }
/** * @param string $string * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool|string */ public function validate($string, $config, $context) { static $colors = null; if ($colors === null) { $colors = $config->get('Core.ColorKeywords'); } $string = trim($string); if (empty($string)) { return false; } $lower = strtolower($string); if (isset($colors[$lower])) { return $colors[$lower]; } if ($string[0] === '#') { $hex = substr($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}"; }
/** * @param HTMLPurifier_Config $config */ public function setup($config) { if ($config->get('HTML.SafeIframe')) { $this->safe = true; } $this->addElement('iframe', 'Inline', 'Flow', 'Common', array('src' => 'URI#embedded', 'width' => 'Length', 'height' => 'Length', 'name' => 'ID', 'scrolling' => 'Enum#yes,no,auto', 'frameborder' => 'Enum#0,1', 'longdesc' => 'URI', 'marginheight' => 'Pixels', 'marginwidth' => 'Pixels')); }
/** * Factory method that creates a cache object based on configuration * @param string $type Name of definitions handled by cache * @param HTMLPurifier_Config $config Config instance * @return mixed */ public function create($type, $config) { $method = $config->get('Cache.DefinitionImpl'); if ($method === null) { return new HTMLPurifier_DefinitionCache_Null($type); } if (!empty($this->caches[$method][$type])) { return $this->caches[$method][$type]; } if (isset($this->implementations[$method]) && class_exists($class = $this->implementations[$method], false)) { $cache = new $class($type); } else { if ($method != 'Serializer') { trigger_error("Unrecognized DefinitionCache {$method}, using Serializer instead", E_USER_WARNING); } $cache = new HTMLPurifier_DefinitionCache_Serializer($type); } foreach ($this->decorators as $decorator) { $new_cache = $decorator->decorate($cache); // prevent infinite recursion in PHP 4 unset($cache); $cache = $new_cache; } $this->caches[$method][$type] = $cache; return $this->caches[$method][$type]; }
/** * @param string $string * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool|string */ public function validate($string, $config, $context) { if ($this->valid_values === false) { $this->valid_values = $config->get('Attr.AllowedFrameTargets'); } return parent::validate($string, $config, $context); }
/** * @param HTMLPurifier_URI $uri * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function filter(&$uri, $config, $context) { // check if filter not applicable if (!$config->get('HTML.SafeIframe')) { return true; } // check if the filter should actually trigger if (!$context->get('EmbeddedURI', true)) { return true; } $token = $context->get('CurrentToken', true); if (!($token && $token->name == 'iframe')) { return true; } // check if we actually have some whitelists enabled if ($this->regexp === null) { return false; } // actually check the whitelists if (!preg_match($this->regexp, $uri->toString())) { return false; } // Make sure that if we're an HTTPS site, the iframe is also HTTPS if (is_https() && $uri->scheme == 'http') { // Convert it to a protocol-relative URL $uri->scheme = null; } return $uri; }
/** * @param array $attr * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function transform($attr, $config, $context) { if (isset($attr['dir'])) { return $attr; } $attr['dir'] = $config->get('Attr.DefaultTextDir'); return $attr; }
/** * @param HTMLPurifier_Config $config */ public function setup($config) { // These definitions are not intrinsically safe: the attribute transforms // are a vital part of ensuring safety. $allowed = $config->get('HTML.SafeScripting'); $script = $this->addElement('script', 'Inline', 'Empty', null, array('type' => 'Enum#application/javascript', 'src*' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed)))); $script->attr_transform_pre[] = $script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired(); }
/** * @param HTMLPurifier_Config $config */ public function setup($config) { $elements = array('a', 'applet', 'form', 'frame', 'iframe', 'img', 'map'); foreach ($elements as $name) { $element = $this->addBlankElement($name); $element->attr['name'] = 'CDATA'; if (!$config->get('HTML.Attr.Name.UseCDATA')) { $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync(); } } }
/** * Lazy load constructs the module by determining the necessary * fixes to create and then delegating to the populate() function. * @param HTMLPurifier_Config $config * @todo Wildcard matching and error reporting when an added or * subtracted fix has no effect. */ public function setup($config) { // create fixes, initialize fixesForLevel $fixes = $this->makeFixes(); $this->makeFixesForLevel($fixes); // figure out which fixes to use $level = $config->get('HTML.TidyLevel'); $fixes_lookup = $this->getFixesForLevel($level); // get custom fix declarations: these need namespace processing $add_fixes = $config->get('HTML.TidyAdd'); $remove_fixes = $config->get('HTML.TidyRemove'); foreach ($fixes as $name => $fix) { // needs to be refactored a little to implement globbing if (isset($remove_fixes[$name]) || !isset($add_fixes[$name]) && !isset($fixes_lookup[$name])) { unset($fixes[$name]); } } // populate this module with necessary fixes $this->populate($fixes); }
/** * @param HTMLPurifier_Config $config */ public function setup($config) { // These definitions are not intrinsically safe: the attribute transforms // are a vital part of ensuring safety. $max = $config->get('HTML.MaxImgLength'); $object = $this->addElement('object', 'Inline', 'Optional: param | Flow | #PCDATA', 'Common', array('type' => 'Enum#application/x-shockwave-flash', 'width' => 'Pixels#' . $max, 'height' => 'Pixels#' . $max, 'data' => 'URI#embedded', 'codebase' => new HTMLPurifier_AttrDef_Enum(array('http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0')))); $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject(); $param = $this->addElement('param', false, 'Empty', false, array('id' => 'ID', 'name*' => 'Text', 'value' => 'Text')); $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam(); $this->info_injector[] = 'SafeObject'; }
/** * @param string $uri * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool|string */ public function validate($uri, $config, $context) { if ($config->get('URI.Disable')) { return false; } $uri = $this->parseCDATA($uri); // parse the URI $uri = $this->parser->parse($uri); if ($uri === false) { return false; } // add embedded flag to context for validators $context->register('EmbeddedURI', $this->embedsResource); $ok = false; do { // generic validation $result = $uri->validate($config, $context); if (!$result) { break; } // chained filtering $uri_def = $config->getDefinition('URI'); $result = $uri_def->filter($uri, $config, $context); if (!$result) { break; } // scheme-specific validation $scheme_obj = $uri->getSchemeObj($config, $context); if (!$scheme_obj) { break; } if ($this->embedsResource && !$scheme_obj->browsable) { break; } $result = $scheme_obj->validate($uri, $config, $context); if (!$result) { break; } // Post chained filtering $result = $uri_def->postFilter($uri, $config, $context); if (!$result) { break; } // survived gauntlet $ok = true; } while (false); $context->destroy('EmbeddedURI'); if (!$ok) { return false; } // back to string return $uri->toString(); }
/** * @param array $attr * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function transform($attr, $config, $context) { $attr['allowscriptaccess'] = 'never'; $attr['allownetworking'] = 'internal'; $attr['type'] = 'application/x-shockwave-flash'; // Added by Ivan Tcholakov, 24-DEC-2013. if (!$config->get('HTML.FlashAllowFullScreen') || !$attr['allowfullscreen'] == 'true') { unset($attr['allowfullscreen']); // if omitted, assume to be 'false' } // return $attr; }
/** * @param array $attr * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function transform($attr, $config, $context) { // If we add support for other objects, we'll need to alter the // transforms. switch ($attr['name']) { // application/x-shockwave-flash // Keep this synchronized with Injector/SafeObject.php case 'allowScriptAccess': // Added by Ivan Tcholakov, 24-DEC-2013. // Added by Ivan Tcholakov, 24-DEC-2013. case 'allowscriptaccess': // $attr['value'] = 'never'; break; case 'allowNetworking': // Added by Ivan Tcholakov, 24-DEC-2013. // Added by Ivan Tcholakov, 24-DEC-2013. case 'allownetworking': // $attr['value'] = 'internal'; break; case 'allowFullScreen': // Added by Ivan Tcholakov, 24-DEC-2013. // Added by Ivan Tcholakov, 24-DEC-2013. case 'allowfullscreen': // if ($config->get('HTML.FlashAllowFullScreen')) { $attr['value'] = $attr['value'] == 'true' ? 'true' : 'false'; } else { $attr['value'] = 'false'; } break; case 'wmode': $attr['value'] = $this->wmode->validate($attr['value'], $config, $context); break; case 'movie': case 'src': $attr['name'] = "movie"; $attr['value'] = $this->uri->validate($attr['value'], $config, $context); break; case 'flashvars': // we're going to allow arbitrary inputs to the SWF, on // the reasoning that it could only hack the SWF, not us. break; // add other cases to support other param name/value pairs // add other cases to support other param name/value pairs default: $attr['name'] = $attr['value'] = null; } return $attr; }
/** * @param array $attr * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array */ public function transform($attr, $config, $context) { // Abort early if we're using relaxed definition of name if ($config->get('HTML.Attr.Name.UseCDATA')) { return $attr; } if (!isset($attr['name'])) { return $attr; } $id = $this->confiscateAttr($attr, 'name'); if (isset($attr['id'])) { return $attr; } $attr['id'] = $id; return $attr; }
/** * Tests whether or not a key is old with respect to the configuration's * version and revision number. * @param string $key Key to test * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against * @return bool */ public function isOld($key, $config) { if (substr_count($key, ',') < 2) { return true; } list($version, $hash, $revision) = explode(',', $key, 3); $compare = version_compare($version, $config->version); // version mismatch, is always old if ($compare != 0) { return true; } // versions match, ids match, check revision number if ($hash == $config->getBatchSerial($this->type) && $revision < $config->get($this->type . '.DefinitionRev')) { return true; } return false; }
/** * @param HTMLPurifier_URI $uri * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function filter(&$uri, $config, $context) { // check if filter not applicable if (!$config->get('HTML.SafeIframe')) { return true; } // check if the filter should actually trigger if (!$context->get('EmbeddedURI', true)) { return true; } $token = $context->get('CurrentToken', true); if (!($token && $token->name == 'iframe')) { return true; } // check if we actually have some whitelists enabled if ($this->regexp === null) { return false; } // actually check the whitelists return preg_match($this->regexp, $uri->toString()); }
/** * @param string $string * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool|string */ public function validate($string, $config, $context) { $allowed = $config->get('Attr.' . $this->name); if (empty($allowed)) { return false; } $string = $this->parseCDATA($string); $parts = explode(' ', $string); // lookup to prevent duplicates $ret_lookup = array(); foreach ($parts as $part) { $part = strtolower(trim($part)); if (!isset($allowed[$part])) { continue; } $ret_lookup[$part] = true; } if (empty($ret_lookup)) { return false; } $string = implode(' ', array_keys($ret_lookup)); return $string; }
/** * @param HTMLPurifier_Config $config * @return bool */ public function prepare($config) { $this->blacklist = $config->get('URI.HostBlacklist'); return true; }
public function testDeprecatedAPI() { $this->schema->add('Foo.Bar', 2, 'int', false); $config = new HTMLPurifier_Config($this->schema); $config->chatty = false; $this->expectError('Using deprecated API: use $config->set(\'Foo.Bar\', ...) instead'); $config->set('Foo', 'Bar', 4); $this->expectError('Using deprecated API: use $config->get(\'Foo.Bar\') instead'); $this->assertIdentical($config->get('Foo', 'Bar'), 4); }
/** * Sets up stuff based on config. We need a better way of doing this. * @param HTMLPurifier_Config $config */ protected function setupConfigStuff($config) { $block_wrapper = $config->get('HTML.BlockWrapper'); if (isset($this->info_content_sets['Block'][$block_wrapper])) { $this->info_block_wrapper = $block_wrapper; } else { trigger_error('Cannot use non-block element as block wrapper', E_USER_ERROR); } $parent = $config->get('HTML.Parent'); $def = $this->manager->getElement($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, true); } // support template text $support = "(for information on implementing this, see the support forums) "; // setup allowed elements ----------------------------------------- $allowed_elements = $config->get('HTML.AllowedElements'); $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early if (!is_array($allowed_elements) && !is_array($allowed_attributes)) { $allowed = $config->get('HTML.Allowed'); if (is_string($allowed)) { list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed); } } if (is_array($allowed_elements)) { foreach ($this->info as $name => $d) { if (!isset($allowed_elements[$name])) { unset($this->info[$name]); } unset($allowed_elements[$name]); } // emit errors foreach ($allowed_elements as $element => $d) { $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful! trigger_error("Element '{$element}' is not supported {$support}", E_USER_WARNING); } } // setup allowed attributes --------------------------------------- $allowed_attributes_mutable = $allowed_attributes; // by copy! if (is_array($allowed_attributes)) { // This actually doesn't do anything, since we went away from // global attributes. It's possible that userland code uses // it, but HTMLModuleManager doesn't! foreach ($this->info_global_attr as $attr => $x) { $keys = array($attr, "*@{$attr}", "*.{$attr}"); $delete = true; foreach ($keys as $key) { if ($delete && isset($allowed_attributes[$key])) { $delete = false; } if (isset($allowed_attributes_mutable[$key])) { unset($allowed_attributes_mutable[$key]); } } if ($delete) { unset($this->info_global_attr[$attr]); } } foreach ($this->info as $tag => $info) { foreach ($info->attr as $attr => $x) { $keys = array("{$tag}@{$attr}", $attr, "*@{$attr}", "{$tag}.{$attr}", "*.{$attr}"); $delete = true; foreach ($keys as $key) { if ($delete && isset($allowed_attributes[$key])) { $delete = false; } if (isset($allowed_attributes_mutable[$key])) { unset($allowed_attributes_mutable[$key]); } } if ($delete) { if ($this->info[$tag]->attr[$attr]->required) { trigger_error("Required attribute '{$attr}' in element '{$tag}' " . "was not allowed, which means '{$tag}' will not be allowed either", E_USER_WARNING); } unset($this->info[$tag]->attr[$attr]); } } } // emit errors foreach ($allowed_attributes_mutable as $elattr => $d) { $bits = preg_split('/[.@]/', $elattr, 2); $c = count($bits); switch ($c) { case 2: if ($bits[0] !== '*') { $element = htmlspecialchars($bits[0]); $attribute = htmlspecialchars($bits[1]); if (!isset($this->info[$element])) { trigger_error("Cannot allow attribute '{$attribute}' if element " . "'{$element}' is not allowed/supported {$support}"); } else { trigger_error("Attribute '{$attribute}' in element '{$element}' not supported {$support}", E_USER_WARNING); } break; } // otherwise fall through // otherwise fall through case 1: $attribute = htmlspecialchars($bits[0]); trigger_error("Global attribute '{$attribute}' is not " . "supported in any elements {$support}", E_USER_WARNING); break; } } } // setup forbidden elements --------------------------------------- $forbidden_elements = $config->get('HTML.ForbiddenElements'); $forbidden_attributes = $config->get('HTML.ForbiddenAttributes'); foreach ($this->info as $tag => $info) { if (isset($forbidden_elements[$tag])) { unset($this->info[$tag]); continue; } foreach ($info->attr as $attr => $x) { if (isset($forbidden_attributes["{$tag}@{$attr}"]) || isset($forbidden_attributes["*@{$attr}"]) || isset($forbidden_attributes[$attr])) { unset($this->info[$tag]->attr[$attr]); continue; } elseif (isset($forbidden_attributes["{$tag}.{$attr}"])) { // this segment might get removed eventually // $tag.$attr are not user supplied, so no worries! trigger_error("Error with {$tag}.{$attr}: tag.attr syntax not supported for " . "HTML.ForbiddenAttributes; use tag@attr instead", E_USER_WARNING); } } } foreach ($forbidden_attributes as $key => $v) { if (strlen($key) < 2) { continue; } if ($key[0] != '*') { continue; } if ($key[1] == '.') { trigger_error("Error with {$key}: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", E_USER_WARNING); } } // setup injectors ----------------------------------------------------- foreach ($this->info_injector as $i => $injector) { if ($injector->checkNeeded($config) !== false) { // remove injector that does not have it's required // elements/attributes present, and is thus not needed. unset($this->info_injector[$i]); } } }
/** * Builds an IDAccumulator, also initializing the default blacklist * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config * @param HTMLPurifier_Context $context Instance of HTMLPurifier_Context * @return HTMLPurifier_IDAccumulator Fully initialized HTMLPurifier_IDAccumulator */ public static function build($config, $context) { $id_accumulator = new HTMLPurifier_IDAccumulator(); $id_accumulator->load($config->get('Attr.IDBlacklist')); return $id_accumulator; }
/** * Takes CSS (the stuff found in <style>) and cleans it. * @warning Requires CSSTidy <http://csstidy.sourceforge.net/> * @param string $css CSS styling to clean * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @throws HTMLPurifier_Exception * @return string Cleaned CSS */ public function cleanCSS($css, $config, $context) { // prepare scope $scope = $config->get('Filter.ExtractStyleBlocks.Scope'); if ($scope !== null) { $scopes = array_map('trim', explode(',', $scope)); } else { $scopes = array(); } // remove comments from CSS $css = trim($css); if (strncmp('<!--', $css, 4) === 0) { $css = substr($css, 4); } if (strlen($css) > 3 && substr($css, -3) == '-->') { $css = substr($css, 0, -3); } $css = trim($css); set_error_handler('htmlpurifier_filter_extractstyleblocks_muteerrorhandler'); $this->_tidy->parse($css); restore_error_handler(); $css_definition = $config->getDefinition('CSS'); $html_definition = $config->getDefinition('HTML'); $new_css = array(); foreach ($this->_tidy->css as $k => $decls) { // $decls are all CSS declarations inside an @ selector $new_decls = array(); foreach ($decls as $selector => $style) { $selector = trim($selector); if ($selector === '') { continue; } // should not happen // Parse the selector // Here is the relevant part of the CSS grammar: // // ruleset // : selector [ ',' S* selector ]* '{' ... // selector // : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]? // combinator // : '+' S* // : '>' S* // simple_selector // : element_name [ HASH | class | attrib | pseudo ]* // | [ HASH | class | attrib | pseudo ]+ // element_name // : IDENT | '*' // ; // class // : '.' IDENT // ; // attrib // : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S* // [ IDENT | STRING ] S* ]? ']' // ; // pseudo // : ':' [ IDENT | FUNCTION S* [IDENT S*]? ')' ] // ; // // For reference, here are the relevant tokens: // // HASH #{name} // IDENT {ident} // INCLUDES == // DASHMATCH |= // STRING {string} // FUNCTION {ident}\( // // And the lexical scanner tokens // // name {nmchar}+ // nmchar [_a-z0-9-]|{nonascii}|{escape} // nonascii [\240-\377] // escape {unicode}|\\[^\r\n\f0-9a-f] // unicode \\{h}}{1,6}(\r\n|[ \t\r\n\f])? // ident -?{nmstart}{nmchar*} // nmstart [_a-z]|{nonascii}|{escape} // string {string1}|{string2} // string1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\" // string2 \'([^\n\r\f\\"]|\\{nl}|{escape})*\' // // We'll implement a subset (in order to reduce attack // surface); in particular: // // - No Unicode support // - No escapes support // - No string support (by proxy no attrib support) // - element_name is matched against allowed // elements (some people might find this // annoying...) // - Pseudo-elements one of :first-child, :link, // :visited, :active, :hover, :focus // handle ruleset $selectors = array_map('trim', explode(',', $selector)); $new_selectors = array(); foreach ($selectors as $sel) { // split on +, > and spaces $basic_selectors = preg_split('/\\s*([+> ])\\s*/', $sel, -1, PREG_SPLIT_DELIM_CAPTURE); // even indices are chunks, odd indices are // delimiters $nsel = null; $delim = null; // guaranteed to be non-null after // two loop iterations for ($i = 0, $c = count($basic_selectors); $i < $c; $i++) { $x = $basic_selectors[$i]; if ($i % 2) { // delimiter if ($x === ' ') { $delim = ' '; } else { $delim = ' ' . $x . ' '; } } else { // simple selector $components = preg_split('/([#.:])/', $x, -1, PREG_SPLIT_DELIM_CAPTURE); $sdelim = null; $nx = null; for ($j = 0, $cc = count($components); $j < $cc; $j++) { $y = $components[$j]; if ($j === 0) { if ($y === '*' || isset($html_definition->info[$y = strtolower($y)])) { $nx = $y; } else { // $nx stays null; this matters // if we don't manage to find // any valid selector content, // in which case we ignore the // outer $delim } } elseif ($j % 2) { // set delimiter $sdelim = $y; } else { $attrdef = null; if ($sdelim === '#') { $attrdef = $this->_id_attrdef; } elseif ($sdelim === '.') { $attrdef = $this->_class_attrdef; } elseif ($sdelim === ':') { $attrdef = $this->_enum_attrdef; } else { throw new HTMLPurifier_Exception('broken invariant sdelim and preg_split'); } $r = $attrdef->validate($y, $config, $context); if ($r !== false) { if ($r !== true) { $y = $r; } if ($nx === null) { $nx = ''; } $nx .= $sdelim . $y; } } } if ($nx !== null) { if ($nsel === null) { $nsel = $nx; } else { $nsel .= $delim . $nx; } } else { // delimiters to the left of invalid // basic selector ignored } } } if ($nsel !== null) { if (!empty($scopes)) { foreach ($scopes as $s) { $new_selectors[] = "{$s} {$nsel}"; } } else { $new_selectors[] = $nsel; } } } if (empty($new_selectors)) { continue; } $selector = implode(', ', $new_selectors); foreach ($style as $name => $value) { if (!isset($css_definition->info[$name])) { unset($style[$name]); continue; } $def = $css_definition->info[$name]; $ret = $def->validate($value, $config, $context); if ($ret === false) { unset($style[$name]); } else { $style[$name] = $ret; } } $new_decls[$selector] = $style; } $new_css[$k] = $new_decls; } // remove stuff that shouldn't be used, could be reenabled // after security risks are analyzed $this->_tidy->css = $new_css; $this->_tidy->import = array(); $this->_tidy->charset = null; $this->_tidy->namespace = null; $css = $this->_tidy->print->plain(); // we are going to escape any special characters <>& to ensure // that no funny business occurs (i.e. </style> in a font-family prop). if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { $css = str_replace(array('<', '>', '&'), array('\\3C ', '\\3E ', '\\26 '), $css); } return $css; }
/** * @param string $string * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool|string */ public function validate($string, $config, $context) { static $generic_names = array('serif' => true, 'sans-serif' => true, 'monospace' => true, 'fantasy' => true, 'cursive' => true); $allowed_fonts = $config->get('CSS.AllowedFonts'); // assume that no font names contain commas in them $fonts = explode(',', $string); $final = ''; foreach ($fonts as $font) { $font = trim($font); if ($font === '') { continue; } // match a generic name if (isset($generic_names[$font])) { if ($allowed_fonts === null || isset($allowed_fonts[$font])) { $final .= $font . ', '; } continue; } // match a quoted name if ($font[0] === '"' || $font[0] === "'") { $length = strlen($font); if ($length <= 2) { continue; } $quote = $font[0]; if ($font[$length - 1] !== $quote) { continue; } $font = substr($font, 1, $length - 2); } $font = $this->expandCSSEscape($font); // $font is a pure representation of the font name if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { continue; } if (ctype_alnum($font) && $font !== '') { // very simple font, allow it in unharmed $final .= $font . ', '; continue; } // bugger out on whitespace. form feed (0C) really // shouldn't show up regardless $font = str_replace(array("\n", "\t", "\r", "\f"), ' ', $font); // Here, there are various classes of characters which need // to be treated differently: // - Alphanumeric characters are essentially safe. We // handled these above. // - Spaces require quoting, though most parsers will do // the right thing if there aren't any characters that // can be misinterpreted // - Dashes rarely occur, but they fairly unproblematic // for parsing/rendering purposes. // The above characters cover the majority of Western font // names. // - Arbitrary Unicode characters not in ASCII. Because // most parsers give little thought to Unicode, treatment // of these codepoints is basically uniform, even for // punctuation-like codepoints. These characters can // show up in non-Western pages and are supported by most // major browsers, for example: "MS 明朝" is a // legitimate font-name // <http://ja.wikipedia.org/wiki/MS_明朝>. See // the CSS3 spec for more examples: // <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png> // You can see live samples of these on the Internet: // <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック> // However, most of these fonts have ASCII equivalents: // for example, 'MS Mincho', and it's considered // professional to use ASCII font names instead of // Unicode font names. Thanks Takeshi Terada for // providing this information. // The following characters, to my knowledge, have not been // used to name font names. // - Single quote. While theoretically you might find a // font name that has a single quote in its name (serving // as an apostrophe, e.g. Dave's Scribble), I haven't // been able to find any actual examples of this. // Internet Explorer's cssText translation (which I // believe is invoked by innerHTML) normalizes any // quoting to single quotes, and fails to escape single // quotes. (Note that this is not IE's behavior for all // CSS properties, just some sort of special casing for // font-family). So a single quote *cannot* be used // safely in the font-family context if there will be an // innerHTML/cssText translation. Note that Firefox 3.x // does this too. // - Double quote. In IE, these get normalized to // single-quotes, no matter what the encoding. (Fun // fact, in IE8, the 'content' CSS property gained // support, where they special cased to preserve encoded // double quotes, but still translate unadorned double // quotes into single quotes.) So, because their // fixpoint behavior is identical to single quotes, they // cannot be allowed either. Firefox 3.x displays // single-quote style behavior. // - Backslashes are reduced by one (so \\ -> \) every // iteration, so they cannot be used safely. This shows // up in IE7, IE8 and FF3 // - Semicolons, commas and backticks are handled properly. // - The rest of the ASCII punctuation is handled properly. // We haven't checked what browsers do to unadorned // versions, but this is not important as long as the // browser doesn't /remove/ surrounding quotes (as IE does // for HTML). // // With these results in hand, we conclude that there are // various levels of safety: // - Paranoid: alphanumeric, spaces and dashes(?) // - International: Paranoid + non-ASCII Unicode // - Edgy: Everything except quotes, backslashes // - NoJS: Standards compliance, e.g. sod IE. Note that // with some judicious character escaping (since certain // types of escaping doesn't work) this is theoretically // OK as long as innerHTML/cssText is not called. // We believe that international is a reasonable default // (that we will implement now), and once we do more // extensive research, we may feel comfortable with dropping // it down to edgy. // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of // str(c)spn assumes that the string was already well formed // Unicode (which of course it is). if (strspn($font, $this->mask) !== strlen($font)) { continue; } // Historical: // In the absence of innerHTML/cssText, these ugly // transforms don't pose a security risk (as \\ and \" // might--these escapes are not supported by most browsers). // We could try to be clever and use single-quote wrapping // when there is a double quote present, but I have choosen // not to implement that. (NOTE: you can reduce the amount // of escapes by one depending on what quoting style you use) // $font = str_replace('\\', '\\5C ', $font); // $font = str_replace('"', '\\22 ', $font); // $font = str_replace("'", '\\27 ', $font); // font possibly with spaces, requires quoting $final .= "'{$font}', "; } $final = rtrim($final, ', '); if ($final === '') { return false; } return $final; }