f试ssefahah可能卡卡卡马卡卡计算机视觉事实上事实上飒飒飒gj◐◐◐◐hfkttkgdgkmcuocucocucoc顶顶顶顶顶顶顶顶顶顶顶顶uco朵朵可可放2巍峨2555555666369🌹 �����ѩ�����һ����dddf2qevf2fv69666 ? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!htmlpurifier/library/HTMLPurifier/PercentEncoder.php000064400000006755152665541550016640 0ustar00preserve[$i] = true; } for ($i = 65; $i <= 90; $i++) { // upper-case $this->preserve[$i] = true; } for ($i = 97; $i <= 122; $i++) { // lower-case $this->preserve[$i] = true; } $this->preserve[45] = true; // Dash - $this->preserve[46] = true; // Period . $this->preserve[95] = true; // Underscore _ $this->preserve[126]= true; // Tilde ~ // extra letters not to escape if ($preserve !== false) { for ($i = 0, $c = strlen($preserve); $i < $c; $i++) { $this->preserve[ord($preserve[$i])] = true; } } } /** * Our replacement for urlencode, it encodes all non-reserved characters, * as well as any extra characters that were instructed to be preserved. * @note * Assumes that the string has already been normalized, making any * and all percent escape sequences valid. Percents will not be * re-escaped, regardless of their status in $preserve * @param string $string String to be encoded * @return string Encoded string. */ public function encode($string) { $ret = ''; for ($i = 0, $c = strlen($string); $i < $c; $i++) { if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])])) { $ret .= '%' . sprintf('%02X', $int); } else { $ret .= $string[$i]; } } return $ret; } /** * Fix up percent-encoding by decoding unreserved characters and normalizing. * @warning This function is affected by $preserve, even though the * usual desired behavior is for this not to preserve those * characters. Be careful when reusing instances of PercentEncoder! * @param string $string String to normalize * @return string */ public function normalize($string) { if ($string == '') { return ''; } $parts = explode('%', $string); $ret = array_shift($parts); foreach ($parts as $part) { $length = strlen($part); if ($length < 2) { $ret .= '%25' . $part; continue; } $encoding = substr($part, 0, 2); $text = substr($part, 2); if (!ctype_xdigit($encoding)) { $ret .= '%25' . $part; continue; } $int = hexdec($encoding); if (isset($this->preserve[$int])) { $ret .= chr($int) . $text; continue; } $encoding = strtoupper($encoding); $ret .= '%' . $encoding . $text; } return $ret; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URIDefinition.php000064400000006546152665541550016406 0ustar00registerFilter(new HTMLPurifier_URIFilter_DisableExternal()); $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources()); $this->registerFilter(new HTMLPurifier_URIFilter_DisableResources()); $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist()); $this->registerFilter(new HTMLPurifier_URIFilter_SafeIframe()); $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute()); $this->registerFilter(new HTMLPurifier_URIFilter_Munge()); } public function registerFilter($filter) { $this->registeredFilters[$filter->name] = $filter; } public function addFilter($filter, $config) { $r = $filter->prepare($config); if ($r === false) return; // null is ok, for backwards compat if ($filter->post) { $this->postFilters[$filter->name] = $filter; } else { $this->filters[$filter->name] = $filter; } } protected function doSetup($config) { $this->setupMemberVariables($config); $this->setupFilters($config); } protected function setupFilters($config) { foreach ($this->registeredFilters as $name => $filter) { if ($filter->always_load) { $this->addFilter($filter, $config); } else { $conf = $config->get('URI.' . $name); if ($conf !== false && $conf !== null) { $this->addFilter($filter, $config); } } } unset($this->registeredFilters); } protected function setupMemberVariables($config) { $this->host = $config->get('URI.Host'); $base_uri = $config->get('URI.Base'); if (!is_null($base_uri)) { $parser = new HTMLPurifier_URIParser(); $this->base = $parser->parse($base_uri); $this->defaultScheme = $this->base->scheme; if (is_null($this->host)) $this->host = $this->base->host; } if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme'); } public function getDefaultScheme($config, $context) { return HTMLPurifier_URISchemeRegistry::instance()->getScheme($this->defaultScheme, $config, $context); } public function filter(&$uri, $config, $context) { foreach ($this->filters as $name => $f) { $result = $f->filter($uri, $config, $context); if (!$result) return false; } return true; } public function postFilter(&$uri, $config, $context) { foreach ($this->postFilters as $name => $f) { $result = $f->filter($uri, $config, $context); if (!$result) return false; } return true; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Exception.php000064400000000261152665541550015660 0ustar00get('URI.AllowedSchemes'); if (!$config->get('URI.OverrideAllowedSchemes') && !isset($allowed_schemes[$scheme]) ) { return; } if (isset($this->schemes[$scheme])) { return $this->schemes[$scheme]; } if (!isset($allowed_schemes[$scheme])) { return; } $class = 'HTMLPurifier_URIScheme_' . $scheme; if (!class_exists($class)) { return; } $this->schemes[$scheme] = new $class(); return $this->schemes[$scheme]; } /** * Registers a custom scheme to the cache, bypassing reflection. * @param string $scheme Scheme name * @param HTMLPurifier_URIScheme $scheme_obj */ public function register($scheme, $scheme_obj) { $this->schemes[$scheme] = $scheme_obj; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/ErrorCollector.php000064400000016711152665541550016671 0ustar00locale =& $context->get('Locale'); $this->context = $context; $this->_current =& $this->_stacks[0]; $this->errors =& $this->_stacks[0]; } /** * Sends an error message to the collector for later use * @param int $severity Error severity, PHP error style (don't use E_USER_) * @param string $msg Error message text */ public function send($severity, $msg) { $args = array(); if (func_num_args() > 2) { $args = func_get_args(); array_shift($args); unset($args[0]); } $token = $this->context->get('CurrentToken', true); $line = $token ? $token->line : $this->context->get('CurrentLine', true); $col = $token ? $token->col : $this->context->get('CurrentCol', true); $attr = $this->context->get('CurrentAttr', true); // perform special substitutions, also add custom parameters $subst = array(); if (!is_null($token)) { $args['CurrentToken'] = $token; } if (!is_null($attr)) { $subst['$CurrentAttr.Name'] = $attr; if (isset($token->attr[$attr])) { $subst['$CurrentAttr.Value'] = $token->attr[$attr]; } } if (empty($args)) { $msg = $this->locale->getMessage($msg); } else { $msg = $this->locale->formatMessage($msg, $args); } if (!empty($subst)) { $msg = strtr($msg, $subst); } // (numerically indexed) $error = array( self::LINENO => $line, self::SEVERITY => $severity, self::MESSAGE => $msg, self::CHILDREN => array() ); $this->_current[] = $error; // NEW CODE BELOW ... // Top-level errors are either: // TOKEN type, if $value is set appropriately, or // "syntax" type, if $value is null $new_struct = new HTMLPurifier_ErrorStruct(); $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; if ($token) { $new_struct->value = clone $token; } if (is_int($line) && is_int($col)) { if (isset($this->lines[$line][$col])) { $struct = $this->lines[$line][$col]; } else { $struct = $this->lines[$line][$col] = $new_struct; } // These ksorts may present a performance problem ksort($this->lines[$line], SORT_NUMERIC); } else { if (isset($this->lines[-1])) { $struct = $this->lines[-1]; } else { $struct = $this->lines[-1] = $new_struct; } } ksort($this->lines, SORT_NUMERIC); // Now, check if we need to operate on a lower structure if (!empty($attr)) { $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); if (!$struct->value) { $struct->value = array($attr, 'PUT VALUE HERE'); } } if (!empty($cssprop)) { $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); if (!$struct->value) { // if we tokenize CSS this might be a little more difficult to do $struct->value = array($cssprop, 'PUT VALUE HERE'); } } // Ok, structs are all setup, now time to register the error $struct->addError($severity, $msg); } /** * Retrieves raw error data for custom formatter to use */ public function getRaw() { return $this->errors; } /** * Default HTML formatting implementation for error messages * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature * @param array $errors Errors array to display; used for recursion. * @return string */ public function getHTMLFormatted($config, $errors = null) { $ret = array(); $this->generator = new HTMLPurifier_Generator($config, $this->context); if ($errors === null) { $errors = $this->errors; } // 'At line' message needs to be removed // generation code for new structure goes here. It needs to be recursive. foreach ($this->lines as $line => $col_array) { if ($line == -1) { continue; } foreach ($col_array as $col => $struct) { $this->_renderStruct($ret, $struct, $line, $col); } } if (isset($this->lines[-1])) { $this->_renderStruct($ret, $this->lines[-1]); } if (empty($errors)) { return '

' . $this->locale->getMessage('ErrorCollector: No errors') . '

'; } else { return ''; } } private function _renderStruct(&$ret, $struct, $line = null, $col = null) { $stack = array($struct); $context_stack = array(array()); while ($current = array_pop($stack)) { $context = array_pop($context_stack); foreach ($current->errors as $error) { list($severity, $msg) = $error; $string = ''; $string .= '
'; // W3C uses an icon to indicate the severity of the error. $error = $this->locale->getErrorName($severity); $string .= "$error "; if (!is_null($line) && !is_null($col)) { $string .= "Line $line, Column $col: "; } else { $string .= 'End of Document: '; } $string .= '' . $this->generator->escape($msg) . ' '; $string .= '
'; // Here, have a marker for the character on the column appropriate. // Be sure to clip extremely long lines. //$string .= '
';
                //$string .= '';
                //$string .= '
'; $ret[] = $string; } foreach ($current->children as $array) { $context[] = $current; $stack = array_merge($stack, array_reverse($array, true)); for ($i = count($array); $i > 0; $i--) { $context_stack[] = $context; } } } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/StringHashParser.php000064400000007071152665541550017157 0ustar00 'DefaultKeyValue', * 'KEY' => 'Value', * 'KEY2' => 'Value2', * 'MULTILINE-KEY' => "Multiline\nvalue.\n", * ) * * We use this as an easy to use file-format for configuration schema * files, but the class itself is usage agnostic. * * You can use ---- to forcibly terminate parsing of a single string-hash; * this marker is used in multi string-hashes to delimit boundaries. */ class HTMLPurifier_StringHashParser { /** * @type string */ public $default = 'ID'; /** * Parses a file that contains a single string-hash. * @param string $file * @return array */ public function parseFile($file) { if (!file_exists($file)) { return false; } $fh = fopen($file, 'r'); if (!$fh) { return false; } $ret = $this->parseHandle($fh); fclose($fh); return $ret; } /** * Parses a file that contains multiple string-hashes delimited by '----' * @param string $file * @return array */ public function parseMultiFile($file) { if (!file_exists($file)) { return false; } $ret = array(); $fh = fopen($file, 'r'); if (!$fh) { return false; } while (!feof($fh)) { $ret[] = $this->parseHandle($fh); } fclose($fh); return $ret; } /** * Internal parser that acepts a file handle. * @note While it's possible to simulate in-memory parsing by using * custom stream wrappers, if such a use-case arises we should * factor out the file handle into its own class. * @param resource $fh File handle with pointer at start of valid string-hash * block. * @return array */ protected function parseHandle($fh) { $state = false; $single = false; $ret = array(); do { $line = fgets($fh); if ($line === false) { break; } $line = rtrim($line, "\n\r"); if (!$state && $line === '') { continue; } if ($line === '----') { break; } if (strncmp('--#', $line, 3) === 0) { // Comment continue; } elseif (strncmp('--', $line, 2) === 0) { // Multiline declaration $state = trim($line, '- '); if (!isset($ret[$state])) { $ret[$state] = ''; } continue; } elseif (!$state) { $single = true; if (strpos($line, ':') !== false) { // Single-line declaration list($state, $line) = explode(':', $line, 2); $line = trim($line); } else { // Use default declaration $state = $this->default; } } if ($single) { $ret[$state] = $line; $single = false; $state = false; } else { $ret[$state] .= "$line\n"; } } while (!feof($fh)); return $ret; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/AttrDef.php000064400000012113152665541550015252 0ustar00 by removing * leading and trailing whitespace, ignoring line feeds, and replacing * carriage returns and tabs with spaces. While most useful for HTML * attributes specified as CDATA, it can also be applied to most CSS * values. * * @note This method is not entirely standards compliant, as trim() removes * more types of whitespace than specified in the spec. In practice, * this is rarely a problem, as those extra characters usually have * already been removed by HTMLPurifier_Encoder. * * @warning This processing is inconsistent with XML's whitespace handling * as specified by section 3.3.3 and referenced XHTML 1.0 section * 4.7. However, note that we are NOT necessarily * parsing XML, thus, this behavior may still be correct. We * assume that newlines have been normalized. */ public function parseCDATA($string) { $string = trim($string); $string = str_replace(array("\n", "\t", "\r"), ' ', $string); return $string; } /** * Factory method for creating this class from a string. * @param string $string String construction info * @return HTMLPurifier_AttrDef Created AttrDef object corresponding to $string */ public function make($string) { // default implementation, return a flyweight of this object. // If $string has an effect on the returned object (i.e. you // need to overload this method), it is best // to clone or instantiate new copies. (Instantiation is safer.) return $this; } /** * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work * properly. THIS IS A HACK! * @param string $string a CSS colour definition * @return string */ protected function mungeRgb($string) { $p = '\s*(\d+(\.\d+)?([%]?))\s*'; if (preg_match('/(rgba|hsla)\(/', $string)) { return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string); } return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string); } /** * Parses a possibly escaped CSS string and returns the "pure" * version of it. */ protected function expandCSSEscape($string) { // flexibly parse it $ret = ''; for ($i = 0, $c = strlen($string); $i < $c; $i++) { if ($string[$i] === '\\') { $i++; if ($i >= $c) { $ret .= '\\'; break; } if (ctype_xdigit($string[$i])) { $code = $string[$i]; for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { if (!ctype_xdigit($string[$i])) { break; } $code .= $string[$i]; } // We have to be extremely careful when adding // new characters, to make sure we're not breaking // the encoding. $char = HTMLPurifier_Encoder::unichr(hexdec($code)); if (HTMLPurifier_Encoder::cleanUTF8($char) === '') { continue; } $ret .= $char; if ($i < $c && trim($string[$i]) !== '') { $i--; } continue; } if ($string[$i] === "\n") { continue; } } $ret .= $string[$i]; } return $ret; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/IDAccumulator.php000064400000003157152665541550016425 0ustar00load($config->get('Attr.IDBlacklist')); return $id_accumulator; } /** * Add an ID to the lookup table. * @param string $id ID to be added. * @return bool status, true if success, false if there's a dupe */ public function add($id) { if (isset($this->ids[$id])) { return false; } return $this->ids[$id] = true; } /** * Load a list of IDs into the lookup table * @param $array_of_ids Array of IDs to load * @note This function doesn't care about duplicates */ public function load($array_of_ids) { foreach ($array_of_ids as $id) { $this->ids[$id] = true; } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URI.php000064400000024570152665541550014372 0ustar00scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme); $this->userinfo = $userinfo; $this->host = $host; $this->port = is_null($port) ? $port : (int)$port; $this->path = $path; $this->query = $query; $this->fragment = $fragment; } /** * Retrieves a scheme object corresponding to the URI's scheme/default * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return HTMLPurifier_URIScheme Scheme object appropriate for validating this URI */ public function getSchemeObj($config, $context) { $registry = HTMLPurifier_URISchemeRegistry::instance(); if ($this->scheme !== null) { $scheme_obj = $registry->getScheme($this->scheme, $config, $context); if (!$scheme_obj) { return false; } // invalid scheme, clean it out } else { // no scheme: retrieve the default one $def = $config->getDefinition('URI'); $scheme_obj = $def->getDefaultScheme($config, $context); if (!$scheme_obj) { if ($def->defaultScheme !== null) { // something funky happened to the default scheme object trigger_error( 'Default scheme object "' . $def->defaultScheme . '" was not readable', E_USER_WARNING ); } // suppress error if it's null return false; } } return $scheme_obj; } /** * Generic validation method applicable for all schemes. May modify * this URI in order to get it into a compliant form. * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool True if validation/filtering succeeds, false if failure */ public function validate($config, $context) { // ABNF definitions from RFC 3986 $chars_sub_delims = $config->get('URI.AllowedSymbols'); $chars_gen_delims = ':/?#[]@'; $chars_pchar = $chars_sub_delims . ':@'; // validate host if (!is_null($this->host)) { $host_def = new HTMLPurifier_AttrDef_URI_Host(); $this->host = $host_def->validate($this->host, $config, $context); if ($this->host === false) { $this->host = null; } } // validate scheme // NOTE: It's not appropriate to check whether or not this // scheme is in our registry, since a URIFilter may convert a // URI that we don't allow into one we do. So instead, we just // check if the scheme can be dropped because there is no host // and it is our default scheme. if (!is_null($this->scheme) && is_null($this->host) || $this->host === '') { // support for relative paths is pretty abysmal when the // scheme is present, so axe it when possible $def = $config->getDefinition('URI'); if ($def->defaultScheme === $this->scheme) { $this->scheme = null; } } // validate username if (!is_null($this->userinfo)) { $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':'); $this->userinfo = $encoder->encode($this->userinfo); } // validate port if (!is_null($this->port)) { if ($this->port < 1 || $this->port > 65535) { $this->port = null; } } // validate path $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/'); if (!is_null($this->host)) { // this catches $this->host === '' // path-abempty (hier and relative) // http://www.example.com/my/path // //www.example.com/my/path (looks odd, but works, and // recognized by most browsers) // (this set is valid or invalid on a scheme by scheme // basis, so we'll deal with it later) // file:///my/path // ///my/path $this->path = $segments_encoder->encode($this->path); } elseif ($this->path !== '') { if ($this->path[0] === '/') { // path-absolute (hier and relative) // http:/my/path // /my/path if (strlen($this->path) >= 2 && $this->path[1] === '/') { // This could happen if both the host gets stripped // out // http://my/path // //my/path $this->path = ''; } else { $this->path = $segments_encoder->encode($this->path); } } elseif (!is_null($this->scheme)) { // path-rootless (hier) // http:my/path // Short circuit evaluation means we don't need to check nz $this->path = $segments_encoder->encode($this->path); } else { // path-noscheme (relative) // my/path // (once again, not checking nz) $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@'); $c = strpos($this->path, '/'); if ($c !== false) { $this->path = $segment_nc_encoder->encode(substr($this->path, 0, $c)) . $segments_encoder->encode(substr($this->path, $c)); } else { $this->path = $segment_nc_encoder->encode($this->path); } } } else { // path-empty (hier and relative) $this->path = ''; // just to be safe } // qf = query and fragment $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?'); if (!is_null($this->query)) { $this->query = $qf_encoder->encode($this->query); } if (!is_null($this->fragment)) { $this->fragment = $qf_encoder->encode($this->fragment); } return true; } /** * Convert URI back to string * @return string URI appropriate for output */ public function toString() { // reconstruct authority $authority = null; // there is a rendering difference between a null authority // (http:foo-bar) and an empty string authority // (http:///foo-bar). if (!is_null($this->host)) { $authority = ''; if (!is_null($this->userinfo)) { $authority .= $this->userinfo . '@'; } $authority .= $this->host; if (!is_null($this->port)) { $authority .= ':' . $this->port; } } // Reconstruct the result // One might wonder about parsing quirks from browsers after // this reconstruction. Unfortunately, parsing behavior depends // on what *scheme* was employed (file:///foo is handled *very* // differently than http:///foo), so unfortunately we have to // defer to the schemes to do the right thing. $result = ''; if (!is_null($this->scheme)) { $result .= $this->scheme . ':'; } if (!is_null($authority)) { $result .= '//' . $authority; } $result .= $this->path; if (!is_null($this->query)) { $result .= '?' . $this->query; } if (!is_null($this->fragment)) { $result .= '#' . $this->fragment; } return $result; } /** * Returns true if this URL might be considered a 'local' URL given * the current context. This is true when the host is null, or * when it matches the host supplied to the configuration. * * Note that this does not do any scheme checking, so it is mostly * only appropriate for metadata that doesn't care about protocol * security. isBenign is probably what you actually want. * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function isLocal($config, $context) { if ($this->host === null) { return true; } $uri_def = $config->getDefinition('URI'); if ($uri_def->host === $this->host) { return true; } return false; } /** * Returns true if this URL should be considered a 'benign' URL, * that is: * * - It is a local URL (isLocal), and * - It has a equal or better level of security * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function isBenign($config, $context) { if (!$this->isLocal($config, $context)) { return false; } $scheme_obj = $this->getSchemeObj($config, $context); if (!$scheme_obj) { return false; } // conservative approach $current_scheme_obj = $config->getDefinition('URI')->getDefaultScheme($config, $context); if ($current_scheme_obj->secure) { if (!$scheme_obj->secure) { return false; } } return true; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php000064400000024373152665541550020134 0ustar00config =& $config; $this->def = $config->getHTMLDefinition(); $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); $ret .= $this->renderDoctype(); $ret .= $this->renderEnvironment(); $ret .= $this->renderContentSets(); $ret .= $this->renderInfo(); $ret .= $this->end('div'); return $ret; } /** * Renders the Doctype table * @return string */ protected function renderDoctype() { $doctype = $this->def->doctype; $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Doctype'); $ret .= $this->row('Name', $doctype->name); $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); $ret .= $this->row('Default Modules', implode(', ', $doctype->modules)); $ret .= $this->row('Default Tidy Modules', implode(', ', $doctype->tidyModules)); $ret .= $this->end('table'); return $ret; } /** * Renders environment table, which is miscellaneous info * @return string */ protected function renderEnvironment() { $def = $this->def; $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Environment'); $ret .= $this->row('Parent of fragment', $def->info_parent); $ret .= $this->renderChildren($def->info_parent_def->child); $ret .= $this->row('Block wrap name', $def->info_block_wrapper); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Global attributes'); $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Tag transforms'); $list = array(); foreach ($def->info_tag_transform as $old => $new) { $new = $this->getClass($new, 'TagTransform_'); $list[] = "<$old> with $new"; } $ret .= $this->element('td', $this->listify($list)); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Pre-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Post-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); $ret .= $this->end('tr'); $ret .= $this->end('table'); return $ret; } /** * Renders the Content Sets table * @return string */ protected function renderContentSets() { $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Content Sets'); foreach ($this->def->info_content_sets as $name => $lookup) { $ret .= $this->heavyHeader($name); $ret .= $this->start('tr'); $ret .= $this->element('td', $this->listifyTagLookup($lookup)); $ret .= $this->end('tr'); } $ret .= $this->end('table'); return $ret; } /** * Renders the Elements ($info) table * @return string */ protected function renderInfo() { $ret = ''; $ret .= $this->start('table'); $ret .= $this->element('caption', 'Elements ($info)'); ksort($this->def->info); $ret .= $this->heavyHeader('Allowed tags', 2); $ret .= $this->start('tr'); $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); $ret .= $this->end('tr'); foreach ($this->def->info as $name => $def) { $ret .= $this->start('tr'); $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Inline content'); $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); $ret .= $this->end('tr'); if (!empty($def->excludes)) { $ret .= $this->start('tr'); $ret .= $this->element('th', 'Excludes'); $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); $ret .= $this->end('tr'); } if (!empty($def->attr_transform_pre)) { $ret .= $this->start('tr'); $ret .= $this->element('th', 'Pre-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); $ret .= $this->end('tr'); } if (!empty($def->attr_transform_post)) { $ret .= $this->start('tr'); $ret .= $this->element('th', 'Post-AttrTransform'); $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); $ret .= $this->end('tr'); } if (!empty($def->auto_close)) { $ret .= $this->start('tr'); $ret .= $this->element('th', 'Auto closed by'); $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); $ret .= $this->end('tr'); } $ret .= $this->start('tr'); $ret .= $this->element('th', 'Allowed attributes'); $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); $ret .= $this->end('tr'); if (!empty($def->required_attr)) { $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); } $ret .= $this->renderChildren($def->child); } $ret .= $this->end('table'); return $ret; } /** * Renders a row describing the allowed children of an element * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element * @return string */ protected function renderChildren($def) { $context = new HTMLPurifier_Context(); $ret = ''; $ret .= $this->start('tr'); $elements = array(); $attr = array(); if (isset($def->elements)) { if ($def->type == 'strictblockquote') { $def->validateChildren(array(), $this->config, $context); } $elements = $def->elements; } if ($def->type == 'chameleon') { $attr['rowspan'] = 2; } elseif ($def->type == 'empty') { $elements = array(); } elseif ($def->type == 'table') { $elements = array_flip( array( 'col', 'caption', 'colgroup', 'thead', 'tfoot', 'tbody', 'tr' ) ); } $ret .= $this->element('th', 'Allowed children', $attr); if ($def->type == 'chameleon') { $ret .= $this->element( 'td', 'Block: ' . $this->escape($this->listifyTagLookup($def->block->elements)), null, 0 ); $ret .= $this->end('tr'); $ret .= $this->start('tr'); $ret .= $this->element( 'td', 'Inline: ' . $this->escape($this->listifyTagLookup($def->inline->elements)), null, 0 ); } elseif ($def->type == 'custom') { $ret .= $this->element( 'td', '' . ucfirst($def->type) . ': ' . $def->dtd_regex ); } else { $ret .= $this->element( 'td', '' . ucfirst($def->type) . ': ' . $this->escape($this->listifyTagLookup($elements)), null, 0 ); } $ret .= $this->end('tr'); return $ret; } /** * Listifies a tag lookup table. * @param array $array Tag lookup array in form of array('tagname' => true) * @return string */ protected function listifyTagLookup($array) { ksort($array); $list = array(); foreach ($array as $name => $discard) { if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { continue; } $list[] = $name; } return $this->listify($list); } /** * Listifies a list of objects by retrieving class names and internal state * @param array $array List of objects * @return string * @todo Also add information about internal state */ protected function listifyObjectList($array) { ksort($array); $list = array(); foreach ($array as $obj) { $list[] = $this->getClass($obj, 'AttrTransform_'); } return $this->listify($list); } /** * Listifies a hash of attributes to AttrDef classes * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) * @return string */ protected function listifyAttr($array) { ksort($array); $list = array(); foreach ($array as $name => $obj) { if ($obj === false) { continue; } $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; } return $this->listify($list); } /** * Creates a heavy header row * @param string $text * @param int $num * @return string */ protected function heavyHeader($text, $num = 1) { $ret = ''; $ret .= $this->start('tr'); $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); $ret .= $this->end('tr'); return $ret; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php000064400000035056152665541550017410 0ustar00docURL = $doc_url; $this->name = $name; $this->compress = $compress; // initialize sub-printers $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); $this->fields[HTMLPurifier_VarParser::C_BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); } /** * Sets default column and row size for textareas in sub-printers * @param $cols Integer columns of textarea, null to use default * @param $rows Integer rows of textarea, null to use default */ public function setTextareaDimensions($cols = null, $rows = null) { if ($cols) { $this->fields['default']->cols = $cols; } if ($rows) { $this->fields['default']->rows = $rows; } } /** * Retrieves styling, in case it is not accessible by webserver */ public static function getCSS() { return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); } /** * Retrieves JavaScript, in case it is not accessible by webserver */ public static function getJavaScript() { return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); } /** * Returns HTML output for a configuration form * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array * where [0] has an HTML namespace and [1] is being rendered. * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. * @param bool $render_controls * @return string */ public function render($config, $allowed = true, $render_controls = true) { if (is_array($config) && isset($config[0])) { $gen_config = $config[0]; $config = $config[1]; } else { $gen_config = $config; } $this->config = $config; $this->genConfig = $gen_config; $this->prepareGenerator($gen_config); $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); $all = array(); foreach ($allowed as $key) { list($ns, $directive) = $key; $all[$ns][$directive] = $config->get($ns . '.' . $directive); } $ret = ''; $ret .= $this->start('table', array('class' => 'hp-config')); $ret .= $this->start('thead'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); $ret .= $this->end('tr'); $ret .= $this->end('thead'); foreach ($all as $ns => $directives) { $ret .= $this->renderNamespace($ns, $directives); } if ($render_controls) { $ret .= $this->start('tbody'); $ret .= $this->start('tr'); $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); $ret .= '[Reset]'; $ret .= $this->end('td'); $ret .= $this->end('tr'); $ret .= $this->end('tbody'); } $ret .= $this->end('table'); return $ret; } /** * Renders a single namespace * @param $ns String namespace name * @param array $directives array of directives to values * @return string */ 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)); } $attr = array('for' => "{$this->name}:$ns.$directive"); // crop directive name if it's too long if (!$this->compress || (strlen($directive) < $this->compress)) { $directive_disp = $directive; } else { $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; $attr['title'] = $directive; } $ret .= $this->element( 'label', $directive_disp, // component printers must create an element with this id $attr ); if ($this->docURL) { $ret .= $this->end('a'); } $ret .= $this->end('th'); $ret .= $this->start('td'); $def = $this->config->def->info["$ns.$directive"]; if (is_int($def)) { $allow_null = $def < 0; $type = abs($def); } else { $type = $def->type; $allow_null = isset($def->allow_null); } if (!isset($this->fields[$type])) { $type = 0; } // default $type_obj = $this->fields[$type]; if ($allow_null) { $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); } $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $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 * @type HTMLPurifier_Printer */ protected $obj; /** * @param HTMLPurifier_Printer $obj Printer to decorate */ public function __construct($obj) { parent::__construct(); $this->obj = $obj; } /** * @param string $ns * @param string $directive * @param string $value * @param string $name * @param HTMLPurifier_Config|array $config * @return string */ public function render($ns, $directive, $value, $name, $config) { if (is_array($config) && isset($config[0])) { $gen_config = $config[0]; $config = $config[1]; } else { $gen_config = $config; } $this->prepareGenerator($gen_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 ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { // modify inline javascript slightly $attr['onclick'] = "toggleWriteability('$name:Yes_$ns.$directive',checked);" . "toggleWriteability('$name:No_$ns.$directive',checked)"; } 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, array($gen_config, $config)); return $ret; } } /** * Swiss-army knife configuration form field printer */ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { /** * @type int */ public $cols = 18; /** * @type int */ public $rows = 5; /** * @param string $ns * @param string $directive * @param string $value * @param string $name * @param HTMLPurifier_Config|array $config * @return string */ public function render($ns, $directive, $value, $name, $config) { if (is_array($config) && isset($config[0])) { $gen_config = $config[0]; $config = $config[1]; } else { $gen_config = $config; } $this->prepareGenerator($gen_config); // this should probably be split up a little $ret = ''; $def = $config->def->info["$ns.$directive"]; if (is_int($def)) { $type = abs($def); } else { $type = $def->type; } if (is_array($value)) { switch ($type) { case HTMLPurifier_VarParser::LOOKUP: $array = $value; $value = array(); foreach ($array as $val => $b) { $value[] = $val; } //TODO does this need a break? case HTMLPurifier_VarParser::ALIST: $value = implode(PHP_EOL, $value); break; case HTMLPurifier_VarParser::HASH: $nvalue = ''; foreach ($value as $i => $v) { if (is_array($v)) { // HACK $v = implode(";", $v); } $nvalue .= "$i:$v" . PHP_EOL; } $value = $nvalue; break; default: $value = ''; } } if ($type === HTMLPurifier_VarParser::C_MIXED) { return 'Not supported'; $value = serialize($value); } $attr = array( 'name' => "$name" . "[$ns.$directive]", 'id' => "$name:$ns.$directive" ); if ($value === null) { $attr['disabled'] = 'disabled'; } if (isset($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'); } elseif ($type === HTMLPurifier_VarParser::TEXT || $type === HTMLPurifier_VarParser::ITEXT || $type === HTMLPurifier_VarParser::ALIST || $type === HTMLPurifier_VarParser::HASH || $type === HTMLPurifier_VarParser::LOOKUP) { $attr['cols'] = $this->cols; $attr['rows'] = $this->rows; $ret .= $this->start('textarea', $attr); $ret .= $this->text($value); $ret .= $this->end('textarea'); } else { $attr['value'] = $value; $attr['type'] = 'text'; $ret .= $this->elementEmpty('input', $attr); } return $ret; } } /** * Bool form field printer */ class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer { /** * @param string $ns * @param string $directive * @param string $value * @param string $name * @param HTMLPurifier_Config|array $config * @return string */ public function render($ns, $directive, $value, $name, $config) { if (is_array($config) && isset($config[0])) { $gen_config = $config[0]; $config = $config[1]; } else { $gen_config = $config; } $this->prepareGenerator($gen_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 === true) { $attr['checked'] = 'checked'; } if ($value === null) { $attr['disabled'] = 'disabled'; } $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 === false) { $attr['checked'] = 'checked'; } if ($value === null) { $attr['disabled'] = 'disabled'; } $ret .= $this->elementEmpty('input', $attr); $ret .= $this->end('div'); return $ret; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js000064400000000216152665541550017223 0ustar00function toggleWriteability(id_of_patient, checked) { document.getElementById(id_of_patient).disabled = checked; } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php000064400000002252152665541550020010 0ustar00def = $config->getCSSDefinition(); $ret = ''; $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); $ret .= $this->start('table'); $ret .= $this->element('caption', 'Properties ($info)'); $ret .= $this->start('thead'); $ret .= $this->start('tr'); $ret .= $this->element('th', 'Property', array('class' => 'heavy')); $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); $ret .= $this->end('tr'); $ret .= $this->end('thead'); ksort($this->def->info); foreach ($this->def->info as $property => $obj) { $name = $this->getClass($obj, 'AttrDef_'); $ret .= $this->row($property, $name); } $ret .= $this->end('table'); $ret .= $this->end('div'); return $ret; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css000064400000000455152665541550017404 0ustar00 .hp-config {} .hp-config tbody th {text-align:right; padding-right:0.5em;} .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;} /* vim: et sw=4 sts=4 */ htmlpurifier/library/HTMLPurifier/ContentSets.php000064400000012744152665541550016204 0ustar00 true) indexed by name. * @type array * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets */ public $lookup = array(); /** * Synchronized list of defined content sets (keys of info). * @type array */ protected $keys = array(); /** * Synchronized list of defined content values (values of info). * @type array */ protected $values = array(); /** * Merges in module's content sets, expands identifiers in the content * sets and populates the keys, values and lookup member variables. * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule */ public function __construct($modules) { if (!is_array($modules)) { $modules = array($modules); } // populate content_sets based on module hints // sorry, no way of overloading foreach ($modules as $module) { foreach ($module->content_sets as $key => $value) { $temp = $this->convertToLookup($value); if (isset($this->lookup[$key])) { // add it into the existing content set $this->lookup[$key] = array_merge($this->lookup[$key], $temp); } else { $this->lookup[$key] = $temp; } } } $old_lookup = false; while ($old_lookup !== $this->lookup) { $old_lookup = $this->lookup; foreach ($this->lookup as $i => $set) { $add = array(); foreach ($set as $element => $x) { if (isset($this->lookup[$element])) { $add += $this->lookup[$element]; unset($this->lookup[$i][$element]); } } $this->lookup[$i] += $add; } } foreach ($this->lookup as $key => $lookup) { $this->info[$key] = implode(' | ', array_keys($lookup)); } $this->keys = array_keys($this->info); $this->values = array_values($this->info); } /** * Accepts a definition; generates and assigns a ChildDef for it * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef */ public function generateChildDef(&$def, $module) { if (!empty($def->child)) { // already done! return; } $content_model = $def->content_model; if (is_string($content_model)) { // Assume that $this->keys is alphanumeric $def->content_model = preg_replace_callback( '/\b(' . implode('|', $this->keys) . ')\b/', array($this, 'generateChildDefCallback'), $content_model ); //$def->content_model = str_replace( // $this->keys, $this->values, $content_model); } $def->child = $this->getChildDef($def, $module); } public function generateChildDefCallback($matches) { return $this->info[$matches[0]]; } /** * Instantiates a ChildDef based on content_model and content_model_type * member variables in HTMLPurifier_ElementDef * @note This will also defer to modules for custom HTMLPurifier_ChildDef * subclasses that need content set expansion * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef * @return HTMLPurifier_ChildDef corresponding to ElementDef */ public function getChildDef($def, $module) { $value = $def->content_model; if (is_object($value)) { trigger_error( 'Literal object child definitions should be stored in '. 'ElementDef->child not ElementDef->content_model', E_USER_NOTICE ); return $value; } switch ($def->content_model_type) { case 'required': return new HTMLPurifier_ChildDef_Required($value); case 'optional': return new HTMLPurifier_ChildDef_Optional($value); case 'empty': return new HTMLPurifier_ChildDef_Empty(); case 'custom': return new HTMLPurifier_ChildDef_Custom($value); } // defer to its module $return = false; if ($module->defines_child_def) { // save a func call $return = $module->getChildDef($def); } if ($return !== false) { return $return; } throw new Exception( 'Could not determine which ChildDef class to instantiate', E_USER_ERROR ); } /** * Converts a string list of elements separated by pipes into * a lookup array. * @param string $string List of elements * @return array Lookup array of elements */ protected function convertToLookup($string) { $array = explode('|', str_replace(' ', '', $string)); $ret = array(); foreach ($array as $k) { $ret[$k] = true; } return $ret; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternal.php000064400000002404152665541550020576 0ustar00getDefinition('URI')->host; if ($our_host !== null) { $this->ourHostParts = array_reverse(explode('.', $our_host)); } } /** * @param HTMLPurifier_URI $uri Reference * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function filter(&$uri, $config, $context) { if (is_null($uri->host)) { return true; } if ($this->ourHostParts === false) { return false; } $host_parts = array_reverse(explode('.', $uri->host)); foreach ($this->ourHostParts as $i => $x) { if (!isset($host_parts[$i])) { return false; } if ($host_parts[$i] != $this->ourHostParts[$i]) { return false; } } return true; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URIFilter/SafeIframe.php000064400000003467152665541550017544 0ustar00regexp = $config->get('URI.SafeIframeRegexp'); return true; } /** * @param HTMLPurifier_URI $uri * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function filter(&$uri, $config, $context) { // check if filter not applicable if (!$config->get('HTML.SafeIframe')) { return true; } // check if the filter should actually trigger if (!$context->get('EmbeddedURI', true)) { return true; } $token = $context->get('CurrentToken', true); if (!($token && $token->name == 'iframe')) { return true; } // check if we actually have some whitelists enabled if ($this->regexp !== null) { return preg_match($this->regexp, $uri->toString()); } // check if the host is in a whitelist for safe iframe hosts $safeHosts = $config->get('URI.SafeIframeHosts'); return $safeHosts !== null && isset($safeHosts[$uri->host]); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URIFilter/MakeAbsolute.php000064400000011541152665541550020106 0ustar00getDefinition('URI'); $this->base = $def->base; if (is_null($this->base)) { trigger_error( 'URI.MakeAbsolute is being ignored due to lack of ' . 'value for URI.Base configuration', E_USER_WARNING ); return false; } $this->base->fragment = null; // fragment is invalid for base URI $stack = explode('/', $this->base->path); array_pop($stack); // discard last segment $stack = $this->_collapseStack($stack); // do pre-parsing $this->basePathStack = $stack; return true; } /** * @param HTMLPurifier_URI $uri * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function filter(&$uri, $config, $context) { if (is_null($this->base)) { return true; } // abort early if ($uri->path === '' && is_null($uri->scheme) && is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)) { // reference to current document $uri = clone $this->base; return true; } if (!is_null($uri->scheme)) { // absolute URI already: don't change if (!is_null($uri->host)) { return true; } $scheme_obj = $uri->getSchemeObj($config, $context); if (!$scheme_obj) { // scheme not recognized return false; } if (!$scheme_obj->hierarchical) { // non-hierarchal URI with explicit scheme, don't change return true; } // special case: had a scheme but always is hierarchical and had no authority } if (!is_null($uri->host)) { // network path, don't bother return true; } if ($uri->path === '') { $uri->path = $this->base->path; } elseif ($uri->path[0] !== '/') { // relative path, needs more complicated processing $stack = explode('/', $uri->path); $new_stack = array_merge($this->basePathStack, $stack); if ($new_stack[0] !== '' && !is_null($this->base->host)) { array_unshift($new_stack, ''); } $new_stack = $this->_collapseStack($new_stack); $uri->path = implode('/', $new_stack); } else { // absolute path, but still we should collapse $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path))); } // re-combine $uri->scheme = $this->base->scheme; if (is_null($uri->userinfo)) { $uri->userinfo = $this->base->userinfo; } if (is_null($uri->host)) { $uri->host = $this->base->host; } if (is_null($uri->port)) { $uri->port = $this->base->port; } return true; } /** * Resolve dots and double-dots in a path stack * @param array $stack * @return array */ private function _collapseStack($stack) { $result = array(); $is_folder = false; for ($i = 0; isset($stack[$i]); $i++) { $is_folder = false; // absorb an internally duplicated slash if ($stack[$i] == '' && $i && isset($stack[$i + 1])) { continue; } if ($stack[$i] == '..') { if (!empty($result)) { $segment = array_pop($result); if ($segment === '' && empty($result)) { // error case: attempted to back out too far: // restore the leading slash $result[] = ''; } elseif ($segment === '..') { $result[] = '..'; // cannot remove .. with .. } } else { // relative path, preserve the double-dots $result[] = '..'; } $is_folder = true; continue; } if ($stack[$i] == '.') { // silently absorb $is_folder = true; continue; } $result[] = $stack[$i]; } if ($is_folder) { $result[] = ''; } return $result; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URIFilter/DisableExternalResources.php000064400000001110152665541550022462 0ustar00get('EmbeddedURI', true)) { return true; } return parent::filter($uri, $config, $context); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URIFilter/Munge.php000064400000006130152665541550016603 0ustar00target = $config->get('URI.' . $this->name); $this->parser = new HTMLPurifier_URIParser(); $this->doEmbed = $config->get('URI.MungeResources'); $this->secretKey = $config->get('URI.MungeSecretKey'); if ($this->secretKey && !function_exists('hash_hmac')) { throw new Exception("Cannot use %URI.MungeSecretKey without hash_hmac support."); } return true; } /** * @param HTMLPurifier_URI $uri * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function filter(&$uri, $config, $context) { if ($context->get('EmbeddedURI', true) && !$this->doEmbed) { return true; } $scheme_obj = $uri->getSchemeObj($config, $context); if (!$scheme_obj) { return true; } // ignore unknown schemes, maybe another postfilter did it if (!$scheme_obj->browsable) { return true; } // ignore non-browsable schemes, since we can't munge those in a reasonable way if ($uri->isBenign($config, $context)) { return true; } // don't redirect if a benign URL $this->makeReplace($uri, $config, $context); $this->replace = array_map('rawurlencode', $this->replace); $new_uri = strtr($this->target, $this->replace); $new_uri = $this->parser->parse($new_uri); // don't redirect if the target host is the same as the // starting host if ($uri->host === $new_uri->host) { return true; } $uri = $new_uri; // overwrite return true; } /** * @param HTMLPurifier_URI $uri * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context */ protected function makeReplace($uri, $config, $context) { $string = $uri->toString(); // always available $this->replace['%s'] = $string; $this->replace['%r'] = $context->get('EmbeddedURI', true) ?: ''; $token = $context->get('CurrentToken', true) ?: ''; $this->replace['%n'] = $token ? $token->name : ''; $this->replace['%m'] = $context->get('CurrentAttr', true) ?: ''; $this->replace['%p'] = $context->get('CurrentCSSProperty', true) ?: ''; // not always available if ($this->secretKey) { $this->replace['%t'] = hash_hmac("sha256", $string, $this->secretKey); } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URIFilter/HostBlacklist.php000064400000002227152665541550020301 0ustar00blacklist = $config->get('URI.HostBlacklist'); return true; } /** * @param HTMLPurifier_URI $uri * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return bool */ public function filter(&$uri, $config, $context) { foreach ($this->blacklist as $blacklisted_host_fragment) { if ($uri->host !== null && strpos($uri->host, $blacklisted_host_fragment) !== false) { return false; } } return true; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/URIFilter/DisableResources.php000064400000000716152665541550020772 0ustar00get('EmbeddedURI', true); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Lexer/DirectLex.php000064400000046467152665541550016706 0ustar00get('HTML.Trusted')) { $html = preg_replace_callback( '#(]*>)(\s*[^<].+?)()#si', array($this, 'scriptCallback'), $html ); } $html = $this->normalize($html, $config, $context); $cursor = 0; // our location in the text $inside_tag = false; // whether or not we're parsing the inside of a tag $array = array(); // result array // This is also treated to mean maintain *column* numbers too $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); if ($maintain_line_numbers === null) { // automatically determine line numbering by checking // if error collection is on $maintain_line_numbers = $config->get('Core.CollectErrors'); } if ($maintain_line_numbers) { $current_line = 1; $current_col = 0; $length = strlen($html); } else { $current_line = false; $current_col = false; $length = false; } $context->register('CurrentLine', $current_line); $context->register('CurrentCol', $current_col); $nl = "\n"; // how often to manually recalculate. This will ALWAYS be right, // but it's pretty wasteful. Set to 0 to turn off $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); $e = false; if ($config->get('Core.CollectErrors')) { $e =& $context->get('ErrorCollector'); } // for testing synchronization $loops = 0; while (++$loops) { // $cursor is either at the start of a token, or inside of // a tag (i.e. there was a < immediately before it), as indicated // by $inside_tag if ($maintain_line_numbers) { // $rcursor, however, is always at the start of a token. $rcursor = $cursor - (int)$inside_tag; // Column number is cheap, so we calculate it every round. // We're interested at the *end* of the newline string, so // we need to add strlen($nl) == 1 to $nl_pos before subtracting it // from our "rcursor" position. $nl_pos = strrpos($html, $nl, $rcursor - $length); $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); // recalculate lines if ($synchronize_interval && // synchronization is on $cursor > 0 && // cursor is further than zero $loops % $synchronize_interval === 0) { // time to synchronize! $current_line = 1 + substr_count($html, $nl, 0, $cursor); } } $position_next_lt = strpos($html, '<', $cursor); $position_next_gt = strpos($html, '>', $cursor); // triggers on "asdf" but not "asdf " // special case to set up context if ($position_next_lt === $cursor) { $inside_tag = true; $cursor++; } if (!$inside_tag && $position_next_lt !== false) { // We are not inside tag and there still is another tag to parse $token = new HTMLPurifier_Token_Text( $this->parseText( substr( $html, $cursor, $position_next_lt - $cursor ), $config ) ); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += substr_count($html, $nl, $cursor, $position_next_lt - $cursor); } $array[] = $token; $cursor = $position_next_lt + 1; $inside_tag = true; continue; } elseif (!$inside_tag) { // We are not inside tag but there are no more tags // If we're already at the end, break if ($cursor === strlen($html)) { break; } // Create Text of rest of string $token = new HTMLPurifier_Token_Text( $this->parseText( substr( $html, $cursor ), $config ) ); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); } $array[] = $token; break; } elseif ($inside_tag && $position_next_gt !== false) { // We are in tag and it is well formed // Grab the internals of the tag $strlen_segment = $position_next_gt - $cursor; if ($strlen_segment < 1) { // there's nothing to process! $token = new HTMLPurifier_Token_Text('<'); $cursor++; continue; } $segment = substr($html, $cursor, $strlen_segment); if ($segment === false) { // somehow, we attempted to access beyond the end of // the string, defense-in-depth, reported by Nate Abele break; } // Check if it's a comment if (substr($segment, 0, 3) === '!--') { // re-determine segment length, looking for --> $position_comment_end = strpos($html, '-->', $cursor); if ($position_comment_end === false) { // uh oh, we have a comment that extends to // infinity. Can't be helped: set comment // end position to end of string if ($e) { $e->send(E_WARNING, 'Lexer: Unclosed comment'); } $position_comment_end = strlen($html); $end = true; } else { $end = false; } $strlen_segment = $position_comment_end - $cursor; $segment = substr($html, $cursor, $strlen_segment); $token = new HTMLPurifier_Token_Comment( substr( $segment, 3, $strlen_segment - 3 ) ); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += substr_count($html, $nl, $cursor, $strlen_segment); } $array[] = $token; $cursor = $end ? $position_comment_end : $position_comment_end + 3; $inside_tag = false; continue; } // Check if it's an end tag $is_end_tag = (strpos($segment, '/') === 0); if ($is_end_tag) { $type = substr($segment, 1); $token = new HTMLPurifier_Token_End($type); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += substr_count($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; $inside_tag = false; $cursor = $position_next_gt + 1; continue; } // Check leading character is alnum, if not, we may // have accidently grabbed an emoticon. Translate into // text and go our merry way if (!ctype_alpha($segment[0])) { // XML: $segment[0] !== '_' && $segment[0] !== ':' if ($e) { $e->send(E_NOTICE, 'Lexer: Unescaped lt'); } $token = new HTMLPurifier_Token_Text('<'); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += substr_count($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; $inside_tag = false; continue; } // Check if it is explicitly self closing, if so, remove // trailing slash. Remember, we could have a tag like
, so // any later token processing scripts must convert improperly // classified EmptyTags from StartTags. $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); if ($is_self_closing) { $strlen_segment--; $segment = substr($segment, 0, $strlen_segment); } // Check if there are any attributes $position_first_space = strcspn($segment, $this->_whitespace); if ($position_first_space >= $strlen_segment) { if ($is_self_closing) { $token = new HTMLPurifier_Token_Empty($segment); } else { $token = new HTMLPurifier_Token_Start($segment); } if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += substr_count($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; $inside_tag = false; $cursor = $position_next_gt + 1; continue; } // Grab out all the data $type = substr($segment, 0, $position_first_space); $attribute_string = trim( substr( $segment, $position_first_space ) ); if ($attribute_string) { $attr = $this->parseAttributeString( $attribute_string, $config, $context ); } else { $attr = array(); } if ($is_self_closing) { $token = new HTMLPurifier_Token_Empty($type, $attr); } else { $token = new HTMLPurifier_Token_Start($type, $attr); } if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); $current_line += substr_count($html, $nl, $cursor, $position_next_gt - $cursor); } $array[] = $token; $cursor = $position_next_gt + 1; $inside_tag = false; continue; } else { // inside tag, but there's no ending > sign if ($e) { $e->send(E_WARNING, 'Lexer: Missing gt'); } $token = new HTMLPurifier_Token_Text( '<' . $this->parseText( substr($html, $cursor), $config ) ); if ($maintain_line_numbers) { $token->rawPosition($current_line, $current_col); } // no cursor scroll? Hmm... $array[] = $token; break; } break; } $context->destroy('CurrentLine'); $context->destroy('CurrentCol'); return $array; } /** * Takes the inside of an HTML tag and makes an assoc array of attributes. * * @param string $string Inside of tag excluding name. * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return array Assoc array of attributes. */ public function parseAttributeString($string, $config, $context) { $string = (string)$string; // quick typecast if ($string == '') { return array(); } // no attributes $e = false; if ($config->get('Core.CollectErrors')) { $e =& $context->get('ErrorCollector'); } // let's see if we can abort as quickly as possible // one equal sign, no spaces => one attribute $num_equal = substr_count($string, '='); $has_space = strpos($string, ' '); if ($num_equal === 0 && !$has_space) { // bool attribute return array($string => $string); } elseif ($num_equal === 1 && !$has_space) { // only one attribute list($key, $quoted_value) = explode('=', $string); $quoted_value = trim($quoted_value); if (!$key) { if ($e) { $e->send(E_ERROR, 'Lexer: Missing attribute key'); } return array(); } if (!$quoted_value) { return array($key => ''); } $first_char = @$quoted_value[0]; $last_char = @$quoted_value[strlen($quoted_value) - 1]; $same_quote = ($first_char == $last_char); $open_quote = ($first_char == '"' || $first_char == "'"); if ($same_quote && $open_quote) { // well behaved $value = substr($quoted_value, 1, strlen($quoted_value) - 2); } else { // not well behaved if ($open_quote) { if ($e) { $e->send(E_ERROR, 'Lexer: Missing end quote'); } $value = substr($quoted_value, 1); } else { $value = $quoted_value; } } if ($value === false) { $value = ''; } return array($key => $this->parseAttr($value, $config)); } // setup loop environment $array = array(); // return assoc array of attributes $cursor = 0; // current position in string (moves forward) $size = strlen($string); // size of the string (stays the same) // if we have unquoted attributes, the parser expects a terminating // space, so let's guarantee that there's always a terminating space. $string .= ' '; $old_cursor = -1; while ($cursor < $size) { if ($old_cursor >= $cursor) { throw new Exception("Infinite loop detected"); } $old_cursor = $cursor; $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); // grab the key $key_begin = $cursor; //we're currently at the start of the key // scroll past all characters that are the key (not whitespace or =) $cursor += strcspn($string, $this->_whitespace . '=', $cursor); $key_end = $cursor; // now at the end of the key $key = substr($string, $key_begin, $key_end - $key_begin); if (!$key) { if ($e) { $e->send(E_ERROR, 'Lexer: Missing attribute key'); } $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop continue; // empty key } // scroll past all whitespace $cursor += strspn($string, $this->_whitespace, $cursor); if ($cursor >= $size) { $array[$key] = $key; break; } // if the next character is an equal sign, we've got a regular // pair, otherwise, it's a bool attribute $first_char = @$string[$cursor]; if ($first_char == '=') { // key="value" $cursor++; $cursor += strspn($string, $this->_whitespace, $cursor); if ($cursor === false) { $array[$key] = ''; break; } // we might be in front of a quote right now $char = @$string[$cursor]; if ($char == '"' || $char == "'") { // it's quoted, end bound is $char $cursor++; $value_begin = $cursor; $cursor = strpos($string, $char, $cursor); $value_end = $cursor; } else { // it's not quoted, end bound is whitespace $value_begin = $cursor; $cursor += strcspn($string, $this->_whitespace, $cursor); $value_end = $cursor; } // we reached a premature end if ($cursor === false) { $cursor = $size; $value_end = $cursor; } $value = substr($string, $value_begin, $value_end - $value_begin); if ($value === false) { $value = ''; } $array[$key] = $this->parseAttr($value, $config); $cursor++; } else { // boolattr if ($key !== '') { $array[$key] = $key; } else { // purely theoretical if ($e) { $e->send(E_ERROR, 'Lexer: Missing attribute key'); } } } } return $array; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php000064400000544475152665541550015541 0ustar00normalize($html, $config, $context); $new_html = $this->wrapHTML($new_html, $config, $context, false /* no div */); try { $parser = new HTML5($new_html); $doc = $parser->save(); } catch (DOMException $e) { // Uh oh, it failed. Punt to DirectLex. $lexer = new HTMLPurifier_Lexer_DirectLex(); $context->register('PH5PError', $e); // save the error, so we can detect it return $lexer->tokenizeHTML($html, $config, $context); // use original HTML } $tokens = array(); $this->tokenizeDOM( $doc->getElementsByTagName('html')->item(0)-> // getElementsByTagName('body')->item(0) // , $tokens, $config ); return $tokens; } } /* Copyright 2007 Jeroen van der Meer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ class HTML5 { private $data; private $char; private $EOF; private $state; private $tree; private $token; private $content_model; private $escape = false; private $entities = array( 'AElig;', 'AElig', 'AMP;', 'AMP', 'Aacute;', 'Aacute', 'Acirc;', 'Acirc', 'Agrave;', 'Agrave', 'Alpha;', 'Aring;', 'Aring', 'Atilde;', 'Atilde', 'Auml;', 'Auml', 'Beta;', 'COPY;', 'COPY', 'Ccedil;', 'Ccedil', 'Chi;', 'Dagger;', 'Delta;', 'ETH;', 'ETH', 'Eacute;', 'Eacute', 'Ecirc;', 'Ecirc', 'Egrave;', 'Egrave', 'Epsilon;', 'Eta;', 'Euml;', 'Euml', 'GT;', 'GT', 'Gamma;', 'Iacute;', 'Iacute', 'Icirc;', 'Icirc', 'Igrave;', 'Igrave', 'Iota;', 'Iuml;', 'Iuml', 'Kappa;', 'LT;', 'LT', 'Lambda;', 'Mu;', 'Ntilde;', 'Ntilde', 'Nu;', 'OElig;', 'Oacute;', 'Oacute', 'Ocirc;', 'Ocirc', 'Ograve;', 'Ograve', 'Omega;', 'Omicron;', 'Oslash;', 'Oslash', 'Otilde;', 'Otilde', 'Ouml;', 'Ouml', 'Phi;', 'Pi;', 'Prime;', 'Psi;', 'QUOT;', 'QUOT', 'REG;', 'REG', 'Rho;', 'Scaron;', 'Sigma;', 'THORN;', 'THORN', 'TRADE;', 'Tau;', 'Theta;', 'Uacute;', 'Uacute', 'Ucirc;', 'Ucirc', 'Ugrave;', 'Ugrave', 'Upsilon;', 'Uuml;', 'Uuml', 'Xi;', 'Yacute;', 'Yacute', 'Yuml;', 'Zeta;', 'aacute;', 'aacute', 'acirc;', 'acirc', 'acute;', 'acute', 'aelig;', 'aelig', 'agrave;', 'agrave', 'alefsym;', 'alpha;', 'amp;', 'amp', 'and;', 'ang;', 'apos;', 'aring;', 'aring', 'asymp;', 'atilde;', 'atilde', 'auml;', 'auml', 'bdquo;', 'beta;', 'brvbar;', 'brvbar', 'bull;', 'cap;', 'ccedil;', 'ccedil', 'cedil;', 'cedil', 'cent;', 'cent', 'chi;', 'circ;', 'clubs;', 'cong;', 'copy;', 'copy', 'crarr;', 'cup;', 'curren;', 'curren', 'dArr;', 'dagger;', 'darr;', 'deg;', 'deg', 'delta;', 'diams;', 'divide;', 'divide', 'eacute;', 'eacute', 'ecirc;', 'ecirc', 'egrave;', 'egrave', 'empty;', 'emsp;', 'ensp;', 'epsilon;', 'equiv;', 'eta;', 'eth;', 'eth', 'euml;', 'euml', 'euro;', 'exist;', 'fnof;', 'forall;', 'frac12;', 'frac12', 'frac14;', 'frac14', 'frac34;', 'frac34', 'frasl;', 'gamma;', 'ge;', 'gt;', 'gt', 'hArr;', 'harr;', 'hearts;', 'hellip;', 'iacute;', 'iacute', 'icirc;', 'icirc', 'iexcl;', 'iexcl', 'igrave;', 'igrave', 'image;', 'infin;', 'int;', 'iota;', 'iquest;', 'iquest', 'isin;', 'iuml;', 'iuml', 'kappa;', 'lArr;', 'lambda;', 'lang;', 'laquo;', 'laquo', 'larr;', 'lceil;', 'ldquo;', 'le;', 'lfloor;', 'lowast;', 'loz;', 'lrm;', 'lsaquo;', 'lsquo;', 'lt;', 'lt', 'macr;', 'macr', 'mdash;', 'micro;', 'micro', 'middot;', 'middot', 'minus;', 'mu;', 'nabla;', 'nbsp;', 'nbsp', 'ndash;', 'ne;', 'ni;', 'not;', 'not', 'notin;', 'nsub;', 'ntilde;', 'ntilde', 'nu;', 'oacute;', 'oacute', 'ocirc;', 'ocirc', 'oelig;', 'ograve;', 'ograve', 'oline;', 'omega;', 'omicron;', 'oplus;', 'or;', 'ordf;', 'ordf', 'ordm;', 'ordm', 'oslash;', 'oslash', 'otilde;', 'otilde', 'otimes;', 'ouml;', 'ouml', 'para;', 'para', 'part;', 'permil;', 'perp;', 'phi;', 'pi;', 'piv;', 'plusmn;', 'plusmn', 'pound;', 'pound', 'prime;', 'prod;', 'prop;', 'psi;', 'quot;', 'quot', 'rArr;', 'radic;', 'rang;', 'raquo;', 'raquo', 'rarr;', 'rceil;', 'rdquo;', 'real;', 'reg;', 'reg', 'rfloor;', 'rho;', 'rlm;', 'rsaquo;', 'rsquo;', 'sbquo;', 'scaron;', 'sdot;', 'sect;', 'sect', 'shy;', 'shy', 'sigma;', 'sigmaf;', 'sim;', 'spades;', 'sub;', 'sube;', 'sum;', 'sup1;', 'sup1', 'sup2;', 'sup2', 'sup3;', 'sup3', 'sup;', 'supe;', 'szlig;', 'szlig', 'tau;', 'there4;', 'theta;', 'thetasym;', 'thinsp;', 'thorn;', 'thorn', 'tilde;', 'times;', 'times', 'trade;', 'uArr;', 'uacute;', 'uacute', 'uarr;', 'ucirc;', 'ucirc', 'ugrave;', 'ugrave', 'uml;', 'uml', 'upsih;', 'upsilon;', 'uuml;', 'uuml', 'weierp;', 'xi;', 'yacute;', 'yacute', 'yen;', 'yen', 'yuml;', 'yuml', 'zeta;', 'zwj;', 'zwnj;' ); const PCDATA = 0; const RCDATA = 1; const CDATA = 2; const PLAINTEXT = 3; const DOCTYPE = 0; const STARTTAG = 1; const ENDTAG = 2; const COMMENT = 3; const CHARACTR = 4; const EOF = 5; public function __construct($data) { $this->data = $data; $this->char = -1; $this->EOF = strlen($data); $this->tree = new HTML5TreeConstructer; $this->content_model = self::PCDATA; $this->state = 'data'; while ($this->state !== null) { $this->{$this->state . 'State'}(); } } public function save() { return $this->tree->save(); } private function char() { return ($this->char < $this->EOF) ? $this->data[$this->char] : false; } private function character($s, $l = 0) { if ($s + $l < $this->EOF) { if ($l === 0) { return $this->data[$s]; } else { return substr($this->data, $s, $l); } } } private function characters($char_class, $start) { return preg_replace('#^([' . $char_class . ']+).*#s', '\\1', substr($this->data, $start)); } private function dataState() { // Consume the next input character $this->char++; $char = $this->char(); if ($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { /* U+0026 AMPERSAND (&) When the content model flag is set to one of the PCDATA or RCDATA states: switch to the entity data state. Otherwise: treat it as per the "anything else" entry below. */ $this->state = 'entityData'; } elseif ($char === '-') { /* If the content model flag is set to either the RCDATA state or the CDATA state, and the escape flag is false, and there are at least three characters before this one in the input stream, and the last four characters in the input stream, including this one, are U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, and U+002D HYPHEN-MINUS (""), set the escape flag to false. */ if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && $this->escape === true && $this->character($this->char, 3) === '-->' ) { $this->escape = false; } /* In any case, emit the input character as a character token. Stay in the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => $char ) ); } elseif ($this->char === $this->EOF) { /* EOF Emit an end-of-file token. */ $this->EOF(); } elseif ($this->content_model === self::PLAINTEXT) { /* When the content model flag is set to the PLAINTEXT state THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of the text and emit it as a character token. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => substr($this->data, $this->char) ) ); $this->EOF(); } else { /* Anything else THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that otherwise would also be treated as a character token and emit it as a single character token. Stay in the data state. */ $len = strcspn($this->data, '<&', $this->char); $char = substr($this->data, $this->char, $len); $this->char += $len - 1; $this->emitToken( array( 'type' => self::CHARACTR, 'data' => $char ) ); $this->state = 'data'; } } private function entityDataState() { // Attempt to consume an entity. $entity = $this->entity(); // If nothing is returned, emit a U+0026 AMPERSAND character token. // Otherwise, emit the character token that was returned. $char = (!$entity) ? '&' : $entity; $this->emitToken( array( 'type' => self::CHARACTR, 'data' => $char ) ); // Finally, switch to the data state. $this->state = 'data'; } private function tagOpenState() { switch ($this->content_model) { case self::RCDATA: case self::CDATA: /* If the next input character is a U+002F SOLIDUS (/) character, consume it and switch to the close tag open state. If the next input character is not a U+002F SOLIDUS (/) character, emit a U+003C LESS-THAN SIGN character token and switch to the data state to process the next input character. */ if ($this->character($this->char + 1) === '/') { $this->char++; $this->state = 'closeTagOpen'; } else { $this->emitToken( array( 'type' => self::CHARACTR, 'data' => '<' ) ); $this->state = 'data'; } break; case self::PCDATA: // If the content model flag is set to the PCDATA state // Consume the next input character: $this->char++; $char = $this->char(); if ($char === '!') { /* U+0021 EXCLAMATION MARK (!) Switch to the markup declaration open state. */ $this->state = 'markupDeclarationOpen'; } elseif ($char === '/') { /* U+002F SOLIDUS (/) Switch to the close tag open state. */ $this->state = 'closeTagOpen'; } elseif (preg_match('/^[A-Za-z]$/', $char)) { /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z Create a new start tag token, set its tag name to the lowercase version of the input character (add 0x0020 to the character's code point), then switch to the tag name state. (Don't emit the token yet; further details will be filled in before it is emitted.) */ $this->token = array( 'name' => strtolower($char), 'type' => self::STARTTAG, 'attr' => array() ); $this->state = 'tagName'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+003E GREATER-THAN SIGN character token. Switch to the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => '<>' ) ); $this->state = 'data'; } elseif ($char === '?') { /* U+003F QUESTION MARK (?) Parse error. Switch to the bogus comment state. */ $this->state = 'bogusComment'; } else { /* Anything else Parse error. Emit a U+003C LESS-THAN SIGN character token and reconsume the current input character in the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => '<' ) ); $this->char--; $this->state = 'data'; } break; } } private function closeTagOpenState() { $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && (!$the_same || ($the_same && (!preg_match( '/[\t\n\x0b\x0c >\/]/', $this->character($this->char + 1 + strlen($next_node)) ) || $this->EOF === $this->char))) ) { /* If the content model flag is set to the RCDATA or CDATA states then examine the next few characters. If they do not match the tag name of the last start tag token emitted (case insensitively), or if they do but they are not immediately followed by one of the following characters: * U+0009 CHARACTER TABULATION * U+000A LINE FEED (LF) * U+000B LINE TABULATION * U+000C FORM FEED (FF) * U+0020 SPACE * U+003E GREATER-THAN SIGN (>) * U+002F SOLIDUS (/) * EOF ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character token, and switch to the data state to process the next input character. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => 'state = 'data'; } else { /* Otherwise, if the content model flag is set to the PCDATA state, or if the next few characters do match that tag name, consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[A-Za-z]$/', $char)) { /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z Create a new end tag token, set its tag name to the lowercase version of the input character (add 0x0020 to the character's code point), then switch to the tag name state. (Don't emit the token yet; further details will be filled in before it is emitted.) */ $this->token = array( 'name' => strtolower($char), 'type' => self::ENDTAG ); $this->state = 'tagName'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Parse error. Switch to the data state. */ $this->state = 'data'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS character token. Reconsume the EOF character in the data state. */ $this->emitToken( array( 'type' => self::CHARACTR, 'data' => 'char--; $this->state = 'data'; } else { /* Parse error. Switch to the bogus comment state. */ $this->state = 'bogusComment'; } } } private function tagNameState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } elseif ($char === '/') { /* U+002F SOLIDUS (/) Parse error unless this is a permitted slash. Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } else { /* Anything else Append the current input character to the current tag token's tag name. Stay in the tag name state. */ $this->token['name'] .= strtolower($char); $this->state = 'tagName'; } } private function beforeAttributeNameState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Stay in the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } elseif ($char === '/') { /* U+002F SOLIDUS (/) Parse error unless this is a permitted slash. Stay in the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Start a new attribute in the current tag token. Set that attribute's name to the current input character, and its value to the empty string. Switch to the attribute name state. */ $this->token['attr'][] = array( 'name' => strtolower($char), 'value' => null ); $this->state = 'attributeName'; } } private function attributeNameState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Stay in the before attribute name state. */ $this->state = 'afterAttributeName'; } elseif ($char === '=') { /* U+003D EQUALS SIGN (=) Switch to the before attribute value state. */ $this->state = 'beforeAttributeValue'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { /* U+002F SOLIDUS (/) Parse error unless this is a permitted slash. Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's name. Stay in the attribute name state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['name'] .= strtolower($char); $this->state = 'attributeName'; } } private function afterAttributeNameState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Stay in the after attribute name state. */ $this->state = 'afterAttributeName'; } elseif ($char === '=') { /* U+003D EQUALS SIGN (=) Switch to the before attribute value state. */ $this->state = 'beforeAttributeValue'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { /* U+002F SOLIDUS (/) Parse error unless this is a permitted slash. Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Start a new attribute in the current tag token. Set that attribute's name to the current input character, and its value to the empty string. Switch to the attribute name state. */ $this->token['attr'][] = array( 'name' => strtolower($char), 'value' => null ); $this->state = 'attributeName'; } } private function beforeAttributeValueState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Stay in the before attribute value state. */ $this->state = 'beforeAttributeValue'; } elseif ($char === '"') { /* U+0022 QUOTATION MARK (") Switch to the attribute value (double-quoted) state. */ $this->state = 'attributeValueDoubleQuoted'; } elseif ($char === '&') { /* U+0026 AMPERSAND (&) Switch to the attribute value (unquoted) state and reconsume this input character. */ $this->char--; $this->state = 'attributeValueUnquoted'; } elseif ($char === '\'') { /* U+0027 APOSTROPHE (') Switch to the attribute value (single-quoted) state. */ $this->state = 'attributeValueSingleQuoted'; } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's value. Switch to the attribute value (unquoted) state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; $this->state = 'attributeValueUnquoted'; } } private function attributeValueDoubleQuotedState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if ($char === '"') { /* U+0022 QUOTATION MARK (") Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '&') { /* U+0026 AMPERSAND (&) Switch to the entity in attribute value state. */ $this->entityInAttributeValueState('double'); } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's value. Stay in the attribute value (double-quoted) state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; $this->state = 'attributeValueDoubleQuoted'; } } private function attributeValueSingleQuotedState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if ($char === '\'') { /* U+0022 QUOTATION MARK (') Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '&') { /* U+0026 AMPERSAND (&) Switch to the entity in attribute value state. */ $this->entityInAttributeValueState('single'); } elseif ($this->char === $this->EOF) { /* EOF Parse error. Emit the current tag token. Reconsume the character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's value. Stay in the attribute value (single-quoted) state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; $this->state = 'attributeValueSingleQuoted'; } } private function attributeValueUnquotedState() { // Consume the next input character: $this->char++; $char = $this->character($this->char); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { /* U+0009 CHARACTER TABULATION U+000A LINE FEED (LF) U+000B LINE TABULATION U+000C FORM FEED (FF) U+0020 SPACE Switch to the before attribute name state. */ $this->state = 'beforeAttributeName'; } elseif ($char === '&') { /* U+0026 AMPERSAND (&) Switch to the entity in attribute value state. */ $this->entityInAttributeValueState(); } elseif ($char === '>') { /* U+003E GREATER-THAN SIGN (>) Emit the current tag token. Switch to the data state. */ $this->emitToken($this->token); $this->state = 'data'; } else { /* Anything else Append the current input character to the current attribute's value. Stay in the attribute value (unquoted) state. */ $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; $this->state = 'attributeValueUnquoted'; } } private function entityInAttributeValueState() { // Attempt to consume an entity. $entity = $this->entity(); // If nothing is returned, append a U+0026 AMPERSAND character to the // current attribute's value. Otherwise, emit the character token that // was returned. $char = (!$entity) ? '&' : $entity; $last = count($this->token['attr']) - 1; $this->token['attr'][$last]['value'] .= $char; } private function bogusCommentState() { /* Consume every character up to the first U+003E GREATER-THAN SIGN character (>) or the end of the file (EOF), whichever comes first. Emit a comment token whose data is the concatenation of all the characters starting from and including the character that caused the state machine to switch into the bogus comment state, up to and including the last consumed character before the U+003E character, if any, or up to the end of the file otherwise. (If the comment was started by the end of the file (EOF), the token is empty.) */ $data = $this->characters('^>', $this->char); $this->emitToken( array( 'data' => $data, 'type' => self::COMMENT ) ); $this->char += strlen($data); /* Switch to the data state. */ $this->state = 'data'; /* If the end of the file was reached, reconsume the EOF character. */ if ($this->char === $this->EOF) { $this->char = $this->EOF - 1; } } private function markupDeclarationOpenState() { /* If the next two characters are both U+002D HYPHEN-MINUS (-) characters, consume those two characters, create a comment token whose data is the empty string, and switch to the comment state. */ if ($this->character($this->char + 1, 2) === '--') { $this->char += 2; $this->state = 'comment'; $this->token = array( 'data' => null, 'type' => self::COMMENT ); /* Otherwise if the next seven characters are a case-insensitive match for the word "DOCTYPE", then consume those characters and switch to the DOCTYPE state. */ } elseif (strtolower($this->character($this->char + 1, 7)) === 'doctype') { $this->char += 7; $this->state = 'doctype'; /* Otherwise, it is a parse error. Switch to the bogus comment state. The next character that is consumed, if any, is the first character that will be in the comment. */ } else { $this->char++; $this->state = 'bogusComment'; } } private function commentState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); /* U+002D HYPHEN-MINUS (-) */ if ($char === '-') { /* Switch to the comment dash state */ $this->state = 'commentDash'; /* EOF */ } elseif ($this->char === $this->EOF) { /* Parse error. Emit the comment token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; /* Anything else */ } else { /* Append the input character to the comment token's data. Stay in the comment state. */ $this->token['data'] .= $char; } } private function commentDashState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); /* U+002D HYPHEN-MINUS (-) */ if ($char === '-') { /* Switch to the comment end state */ $this->state = 'commentEnd'; /* EOF */ } elseif ($this->char === $this->EOF) { /* Parse error. Emit the comment token. Reconsume the EOF character in the data state. */ $this->emitToken($this->token); $this->char--; $this->state = 'data'; /* Anything else */ } else { /* Append a U+002D HYPHEN-MINUS (-) character and the input character to the comment token's data. Switch to the comment state. */ $this->token['data'] .= '-' . $char; $this->state = 'comment'; } } private function commentEndState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if ($char === '>') { $this->emitToken($this->token); $this->state = 'data'; } elseif ($char === '-') { $this->token['data'] .= '-'; } elseif ($this->char === $this->EOF) { $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { $this->token['data'] .= '--' . $char; $this->state = 'comment'; } } private function doctypeState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { $this->state = 'beforeDoctypeName'; } else { $this->char--; $this->state = 'beforeDoctypeName'; } } private function beforeDoctypeNameState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { // Stay in the before DOCTYPE name state. } elseif (preg_match('/^[a-z]$/', $char)) { $this->token = array( 'name' => strtoupper($char), 'type' => self::DOCTYPE, 'error' => true ); $this->state = 'doctypeName'; } elseif ($char === '>') { $this->emitToken( array( 'name' => null, 'type' => self::DOCTYPE, 'error' => true ) ); $this->state = 'data'; } elseif ($this->char === $this->EOF) { $this->emitToken( array( 'name' => null, 'type' => self::DOCTYPE, 'error' => true ) ); $this->char--; $this->state = 'data'; } else { $this->token = array( 'name' => $char, 'type' => self::DOCTYPE, 'error' => true ); $this->state = 'doctypeName'; } } private function doctypeNameState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { $this->state = 'AfterDoctypeName'; } elseif ($char === '>') { $this->emitToken($this->token); $this->state = 'data'; } elseif (preg_match('/^[a-z]$/', $char)) { $this->token['name'] .= strtoupper($char); } elseif ($this->char === $this->EOF) { $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { $this->token['name'] .= $char; } $this->token['error'] = ($this->token['name'] === 'HTML') ? false : true; } private function afterDoctypeNameState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { // Stay in the DOCTYPE name state. } elseif ($char === '>') { $this->emitToken($this->token); $this->state = 'data'; } elseif ($this->char === $this->EOF) { $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { $this->token['error'] = true; $this->state = 'bogusDoctype'; } } private function bogusDoctypeState() { /* Consume the next input character: */ $this->char++; $char = $this->char(); if ($char === '>') { $this->emitToken($this->token); $this->state = 'data'; } elseif ($this->char === $this->EOF) { $this->emitToken($this->token); $this->char--; $this->state = 'data'; } else { // Stay in the bogus DOCTYPE state. } } private function entity() { $start = $this->char; // This section defines how to consume an entity. This definition is // used when parsing entities in text and in attributes. // The behaviour depends on the identity of the next character (the // one immediately after the U+0026 AMPERSAND character): switch ($this->character($this->char + 1)) { // U+0023 NUMBER SIGN (#) case '#': // The behaviour further depends on the character after the // U+0023 NUMBER SIGN: switch ($this->character($this->char + 1)) { // U+0078 LATIN SMALL LETTER X // U+0058 LATIN CAPITAL LETTER X case 'x': case 'X': // Follow the steps below, but using the range of // characters U+0030 DIGIT ZERO through to U+0039 DIGIT // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER // A, through to U+0046 LATIN CAPITAL LETTER F (in other // words, 0-9, A-F, a-f). $char = 1; $char_class = '0-9A-Fa-f'; break; // Anything else default: // Follow the steps below, but using the range of // characters U+0030 DIGIT ZERO through to U+0039 DIGIT // NINE (i.e. just 0-9). $char = 0; $char_class = '0-9'; break; } // Consume as many characters as match the range of characters // given above. $this->char++; $e_name = $this->characters($char_class, $this->char + $char + 1); $entity = $this->character($start, $this->char); $cond = strlen($e_name) > 0; // The rest of the parsing happens below. break; // Anything else default: // Consume the maximum number of characters possible, with the // consumed characters case-sensitively matching one of the // identifiers in the first column of the entities table. $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); $len = strlen($e_name); for ($c = 1; $c <= $len; $c++) { $id = substr($e_name, 0, $c); $this->char++; if (in_array($id, $this->entities)) { if ($e_name[$c - 1] !== ';') { if ($c < $len && $e_name[$c] == ';') { $this->char++; // consume extra semicolon } } $entity = $id; break; } } $cond = isset($entity); // The rest of the parsing happens below. break; } if (!$cond) { // If no match can be made, then this is a parse error. No // characters are consumed, and nothing is returned. $this->char = $start; return false; } // Return a character token for the character corresponding to the // entity name (as given by the second column of the entities table). return html_entity_decode('&' . rtrim($entity, ';') . ';', ENT_QUOTES, 'UTF-8'); } private function emitToken($token) { $emit = $this->tree->emitToken($token); if (is_int($emit)) { $this->content_model = $emit; } elseif ($token['type'] === self::ENDTAG) { $this->content_model = self::PCDATA; } } private function EOF() { $this->state = null; $this->tree->emitToken( array( 'type' => self::EOF ) ); } } class HTML5TreeConstructer { public $stack = array(); private $phase; private $mode; private $dom; private $foster_parent = null; private $a_formatting = array(); private $head_pointer = null; private $form_pointer = null; private $scoping = array('button', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'th'); private $formatting = array( 'a', 'b', 'big', 'em', 'font', 'i', 'nobr', 's', 'small', 'strike', 'strong', 'tt', 'u' ); private $special = array( 'address', 'area', 'base', 'basefont', 'bgsound', 'blockquote', 'body', 'br', 'center', 'col', 'colgroup', 'dd', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'iframe', 'image', 'img', 'input', 'isindex', 'li', 'link', 'listing', 'menu', 'meta', 'noembed', 'noframes', 'noscript', 'ol', 'optgroup', 'option', 'p', 'param', 'plaintext', 'pre', 'script', 'select', 'spacer', 'style', 'tbody', 'textarea', 'tfoot', 'thead', 'title', 'tr', 'ul', 'wbr' ); // The different phases. const INIT_PHASE = 0; const ROOT_PHASE = 1; const MAIN_PHASE = 2; const END_PHASE = 3; // The different insertion modes for the main phase. const BEFOR_HEAD = 0; const IN_HEAD = 1; const AFTER_HEAD = 2; const IN_BODY = 3; const IN_TABLE = 4; const IN_CAPTION = 5; const IN_CGROUP = 6; const IN_TBODY = 7; const IN_ROW = 8; const IN_CELL = 9; const IN_SELECT = 10; const AFTER_BODY = 11; const IN_FRAME = 12; const AFTR_FRAME = 13; // The different types of elements. const SPECIAL = 0; const SCOPING = 1; const FORMATTING = 2; const PHRASING = 3; const MARKER = 0; public function __construct() { $this->phase = self::INIT_PHASE; $this->mode = self::BEFOR_HEAD; $this->dom = new DOMDocument; $this->dom->encoding = 'UTF-8'; $this->dom->preserveWhiteSpace = true; $this->dom->substituteEntities = true; $this->dom->strictErrorChecking = false; } // Process tag tokens public function emitToken($token) { switch ($this->phase) { case self::INIT_PHASE: return $this->initPhase($token); break; case self::ROOT_PHASE: return $this->rootElementPhase($token); break; case self::MAIN_PHASE: return $this->mainPhase($token); break; case self::END_PHASE : return $this->trailingEndPhase($token); break; } } private function initPhase($token) { /* Initially, the tree construction stage must handle each token emitted from the tokenisation stage as follows: */ /* A DOCTYPE token that is marked as being in error A comment token A start tag token An end tag token A character token that is not one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE An end-of-file token */ if ((isset($token['error']) && $token['error']) || $token['type'] === HTML5::COMMENT || $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG || $token['type'] === HTML5::EOF || ($token['type'] === HTML5::CHARACTR && isset($token['data']) && !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ) { /* This specification does not define how to handle this case. In particular, user agents may ignore the entirety of this specification altogether for such documents, and instead invoke special parse modes with a greater emphasis on backwards compatibility. */ $this->phase = self::ROOT_PHASE; return $this->rootElementPhase($token); /* A DOCTYPE token marked as being correct */ } elseif (isset($token['error']) && !$token['error']) { /* Append a DocumentType node to the Document node, with the name attribute set to the name given in the DOCTYPE token (which will be "HTML"), and the other attributes specific to DocumentType objects set to null, empty lists, or the empty string as appropriate. */ $doctype = new DOMDocumentType(null, null, 'HTML'); /* Then, switch to the root element phase of the tree construction stage. */ $this->phase = self::ROOT_PHASE; /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ } elseif (isset($token['data']) && preg_match( '/^[\t\n\x0b\x0c ]+$/', $token['data'] ) ) { /* Append that character to the Document node. */ $text = $this->dom->createTextNode($token['data']); $this->dom->appendChild($text); } } private function rootElementPhase($token) { /* After the initial phase, as each token is emitted from the tokenisation stage, it must be processed as described in this section. */ /* A DOCTYPE token */ if ($token['type'] === HTML5::DOCTYPE) { // Parse error. Ignore the token. /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the Document object with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); $this->dom->appendChild($comment); /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ } elseif ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append that character to the Document node. */ $text = $this->dom->createTextNode($token['data']); $this->dom->appendChild($text); /* A character token that is not one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE A start tag token An end tag token An end-of-file token */ } elseif (($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG || $token['type'] === HTML5::EOF ) { /* Create an HTMLElement node with the tag name html, in the HTML namespace. Append it to the Document object. Switch to the main phase and reprocess the current token. */ $html = $this->dom->createElement('html'); $this->dom->appendChild($html); $this->stack[] = $html; $this->phase = self::MAIN_PHASE; return $this->mainPhase($token); } } private function mainPhase($token) { /* Tokens in the main phase must be handled as follows: */ /* A DOCTYPE token */ if ($token['type'] === HTML5::DOCTYPE) { // Parse error. Ignore the token. /* A start tag token with the tag name "html" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { /* If this start tag token was not the first start tag token, then it is a parse error. */ /* For each attribute on the token, check to see if the attribute is already present on the top element of the stack of open elements. If it is not, add the attribute and its corresponding value to that element. */ foreach ($token['attr'] as $attr) { if (!$this->stack[0]->hasAttribute($attr['name'])) { $this->stack[0]->setAttribute($attr['name'], $attr['value']); } } /* An end-of-file token */ } elseif ($token['type'] === HTML5::EOF) { /* Generate implied end tags. */ $this->generateImpliedEndTags(); /* Anything else. */ } else { /* Depends on the insertion mode: */ switch ($this->mode) { case self::BEFOR_HEAD: return $this->beforeHead($token); break; case self::IN_HEAD: return $this->inHead($token); break; case self::AFTER_HEAD: return $this->afterHead($token); break; case self::IN_BODY: return $this->inBody($token); break; case self::IN_TABLE: return $this->inTable($token); break; case self::IN_CAPTION: return $this->inCaption($token); break; case self::IN_CGROUP: return $this->inColumnGroup($token); break; case self::IN_TBODY: return $this->inTableBody($token); break; case self::IN_ROW: return $this->inRow($token); break; case self::IN_CELL: return $this->inCell($token); break; case self::IN_SELECT: return $this->inSelect($token); break; case self::AFTER_BODY: return $this->afterBody($token); break; case self::IN_FRAME: return $this->inFrameset($token); break; case self::AFTR_FRAME: return $this->afterFrameset($token); break; case self::END_PHASE: return $this->trailingEndPhase($token); break; } } } private function beforeHead($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* A start tag token with the tag name "head" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { /* Create an element for the token, append the new element to the current node and push it onto the stack of open elements. */ $element = $this->insertElement($token); /* Set the head element pointer to this new element node. */ $this->head_pointer = $element; /* Change the insertion mode to "in head". */ $this->mode = self::IN_HEAD; /* A start tag token whose tag name is one of: "base", "link", "meta", "script", "style", "title". Or an end tag with the tag name "html". Or a character token that is not one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE. Or any other start tag token */ } elseif ($token['type'] === HTML5::STARTTAG || ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || ($token['type'] === HTML5::CHARACTR && !preg_match( '/^[\t\n\x0b\x0c ]$/', $token['data'] )) ) { /* Act as if a start tag token with the tag name "head" and no attributes had been seen, then reprocess the current token. */ $this->beforeHead( array( 'name' => 'head', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); return $this->inHead($token); /* Any other end tag */ } elseif ($token['type'] === HTML5::ENDTAG) { /* Parse error. Ignore the token. */ } } private function inHead($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE. THIS DIFFERS FROM THE SPEC: If the current node is either a title, style or script element, append the character to the current node regardless of its content. */ if (($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( $token['type'] === HTML5::CHARACTR && in_array( end($this->stack)->nodeName, array('title', 'style', 'script') )) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); } elseif ($token['type'] === HTML5::ENDTAG && in_array($token['name'], array('title', 'style', 'script')) ) { array_pop($this->stack); return HTML5::PCDATA; /* A start tag with the tag name "title" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { /* Create an element for the token and append the new element to the node pointed to by the head element pointer, or, if that is null (innerHTML case), to the current node. */ if ($this->head_pointer !== null) { $element = $this->insertElement($token, false); $this->head_pointer->appendChild($element); } else { $element = $this->insertElement($token); } /* Switch the tokeniser's content model flag to the RCDATA state. */ return HTML5::RCDATA; /* A start tag with the tag name "style" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { /* Create an element for the token and append the new element to the node pointed to by the head element pointer, or, if that is null (innerHTML case), to the current node. */ if ($this->head_pointer !== null) { $element = $this->insertElement($token, false); $this->head_pointer->appendChild($element); } else { $this->insertElement($token); } /* Switch the tokeniser's content model flag to the CDATA state. */ return HTML5::CDATA; /* A start tag with the tag name "script" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { /* Create an element for the token. */ $element = $this->insertElement($token, false); $this->head_pointer->appendChild($element); /* Switch the tokeniser's content model flag to the CDATA state. */ return HTML5::CDATA; /* A start tag with the tag name "base", "link", or "meta" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('base', 'link', 'meta') ) ) { /* Create an element for the token and append the new element to the node pointed to by the head element pointer, or, if that is null (innerHTML case), to the current node. */ if ($this->head_pointer !== null) { $element = $this->insertElement($token, false); $this->head_pointer->appendChild($element); array_pop($this->stack); } else { $this->insertElement($token); } /* An end tag with the tag name "head" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { /* If the current node is a head element, pop the current node off the stack of open elements. */ if ($this->head_pointer->isSameNode(end($this->stack))) { array_pop($this->stack); /* Otherwise, this is a parse error. */ } else { // k } /* Change the insertion mode to "after head". */ $this->mode = self::AFTER_HEAD; /* A start tag with the tag name "head" or an end tag except "html". */ } elseif (($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html') ) { // Parse error. Ignore the token. /* Anything else */ } else { /* If the current node is a head element, act as if an end tag token with the tag name "head" had been seen. */ if ($this->head_pointer->isSameNode(end($this->stack))) { $this->inHead( array( 'name' => 'head', 'type' => HTML5::ENDTAG ) ); /* Otherwise, change the insertion mode to "after head". */ } else { $this->mode = self::AFTER_HEAD; } /* Then, reprocess the current token. */ return $this->afterHead($token); } } private function afterHead($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* A start tag token with the tag name "body" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { /* Insert a body element for the token. */ $this->insertElement($token); /* Change the insertion mode to "in body". */ $this->mode = self::IN_BODY; /* A start tag token with the tag name "frameset" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { /* Insert a frameset element for the token. */ $this->insertElement($token); /* Change the insertion mode to "in frameset". */ $this->mode = self::IN_FRAME; /* A start tag token whose tag name is one of: "base", "link", "meta", "script", "style", "title" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('base', 'link', 'meta', 'script', 'style', 'title') ) ) { /* Parse error. Switch the insertion mode back to "in head" and reprocess the token. */ $this->mode = self::IN_HEAD; return $this->inHead($token); /* Anything else */ } else { /* Act as if a start tag token with the tag name "body" and no attributes had been seen, and then reprocess the current token. */ $this->afterHead( array( 'name' => 'body', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); return $this->inBody($token); } } private function inBody($token) { /* Handle the token as follows: */ switch ($token['type']) { /* A character token */ case HTML5::CHARACTR: /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Append the token's character to the current node. */ $this->insertText($token['data']); break; /* A comment token */ case HTML5::COMMENT: /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); break; case HTML5::STARTTAG: switch ($token['name']) { /* A start tag token whose tag name is one of: "script", "style" */ case 'script': case 'style': /* Process the token as if the insertion mode had been "in head". */ return $this->inHead($token); break; /* A start tag token whose tag name is one of: "base", "link", "meta", "title" */ case 'base': case 'link': case 'meta': case 'title': /* Parse error. Process the token as if the insertion mode had been "in head". */ return $this->inHead($token); break; /* A start tag token with the tag name "body" */ case 'body': /* Parse error. If the second element on the stack of open elements is not a body element, or, if the stack of open elements has only one node on it, then ignore the token. (innerHTML case) */ if (count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { // Ignore /* Otherwise, for each attribute on the token, check to see if the attribute is already present on the body element (the second element) on the stack of open elements. If it is not, add the attribute and its corresponding value to that element. */ } else { foreach ($token['attr'] as $attr) { if (!$this->stack[1]->hasAttribute($attr['name'])) { $this->stack[1]->setAttribute($attr['name'], $attr['value']); } } } break; /* A start tag whose tag name is one of: "address", "blockquote", "center", "dir", "div", "dl", "fieldset", "listing", "menu", "ol", "p", "ul" */ case 'address': case 'blockquote': case 'center': case 'dir': case 'div': case 'dl': case 'fieldset': case 'listing': case 'menu': case 'ol': case 'p': case 'ul': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); break; /* A start tag whose tag name is "form" */ case 'form': /* If the form element pointer is not null, ignore the token with a parse error. */ if ($this->form_pointer !== null) { // Ignore. /* Otherwise: */ } else { /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token, and set the form element pointer to point to the element created. */ $element = $this->insertElement($token); $this->form_pointer = $element; } break; /* A start tag whose tag name is "li", "dd" or "dt" */ case 'li': case 'dd': case 'dt': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } $stack_length = count($this->stack) - 1; for ($n = $stack_length; 0 <= $n; $n--) { /* 1. Initialise node to be the current node (the bottommost node of the stack). */ $stop = false; $node = $this->stack[$n]; $cat = $this->getElementCategory($node->tagName); /* 2. If node is an li, dd or dt element, then pop all the nodes from the current node up to node, including node, then stop this algorithm. */ if ($token['name'] === $node->tagName || ($token['name'] !== 'li' && ($node->tagName === 'dd' || $node->tagName === 'dt')) ) { for ($x = $stack_length; $x >= $n; $x--) { array_pop($this->stack); } break; } /* 3. If node is not in the formatting category, and is not in the phrasing category, and is not an address or div element, then stop this algorithm. */ if ($cat !== self::FORMATTING && $cat !== self::PHRASING && $node->tagName !== 'address' && $node->tagName !== 'div' ) { break; } } /* Finally, insert an HTML element with the same tag name as the token's. */ $this->insertElement($token); break; /* A start tag token whose tag name is "plaintext" */ case 'plaintext': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); return HTML5::PLAINTEXT; break; /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6" */ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* If the stack of open elements has in scope an element whose tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then this is a parse error; pop elements from the stack until an element with one of those tag names has been popped from the stack. */ while ($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { array_pop($this->stack); } /* Insert an HTML element for the token. */ $this->insertElement($token); break; /* A start tag whose tag name is "a" */ case 'a': /* If the list of active formatting elements contains an element whose tag name is "a" between the end of the list and the last marker on the list (or the start of the list if there is no marker on the list), then this is a parse error; act as if an end tag with the tag name "a" had been seen, then remove that element from the list of active formatting elements and the stack of open elements if the end tag didn't already remove it (it might not have if the element is not in table scope). */ $leng = count($this->a_formatting); for ($n = $leng - 1; $n >= 0; $n--) { if ($this->a_formatting[$n] === self::MARKER) { break; } elseif ($this->a_formatting[$n]->nodeName === 'a') { $this->emitToken( array( 'name' => 'a', 'type' => HTML5::ENDTAG ) ); break; } } /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $el = $this->insertElement($token); /* Add that element to the list of active formatting elements. */ $this->a_formatting[] = $el; break; /* A start tag whose tag name is one of: "b", "big", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ case 'b': case 'big': case 'em': case 'font': case 'i': case 'nobr': case 's': case 'small': case 'strike': case 'strong': case 'tt': case 'u': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $el = $this->insertElement($token); /* Add that element to the list of active formatting elements. */ $this->a_formatting[] = $el; break; /* A start tag token whose tag name is "button" */ case 'button': /* If the stack of open elements has a button element in scope, then this is a parse error; act as if an end tag with the tag name "button" had been seen, then reprocess the token. (We don't do that. Unnecessary.) */ if ($this->elementInScope('button')) { $this->inBody( array( 'name' => 'button', 'type' => HTML5::ENDTAG ) ); } /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Insert a marker at the end of the list of active formatting elements. */ $this->a_formatting[] = self::MARKER; break; /* A start tag token whose tag name is one of: "marquee", "object" */ case 'marquee': case 'object': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Insert a marker at the end of the list of active formatting elements. */ $this->a_formatting[] = self::MARKER; break; /* A start tag token whose tag name is "xmp" */ case 'xmp': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Switch the content model flag to the CDATA state. */ return HTML5::CDATA; break; /* A start tag whose tag name is "table" */ case 'table': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); /* Change the insertion mode to "in table". */ $this->mode = self::IN_TABLE; break; /* A start tag whose tag name is one of: "area", "basefont", "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ case 'area': case 'basefont': case 'bgsound': case 'br': case 'embed': case 'img': case 'param': case 'spacer': case 'wbr': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Immediately pop the current node off the stack of open elements. */ array_pop($this->stack); break; /* A start tag whose tag name is "hr" */ case 'hr': /* If the stack of open elements has a p element in scope, then act as if an end tag with the tag name p had been seen. */ if ($this->elementInScope('p')) { $this->emitToken( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); /* Immediately pop the current node off the stack of open elements. */ array_pop($this->stack); break; /* A start tag whose tag name is "image" */ case 'image': /* Parse error. Change the token's tag name to "img" and reprocess it. (Don't ask.) */ $token['name'] = 'img'; return $this->inBody($token); break; /* A start tag whose tag name is "input" */ case 'input': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an input element for the token. */ $element = $this->insertElement($token, false); /* If the form element pointer is not null, then associate the input element with the form element pointed to by the form element pointer. */ $this->form_pointer !== null ? $this->form_pointer->appendChild($element) : end($this->stack)->appendChild($element); /* Pop that input element off the stack of open elements. */ array_pop($this->stack); break; /* A start tag whose tag name is "isindex" */ case 'isindex': /* Parse error. */ // w/e /* If the form element pointer is not null, then ignore the token. */ if ($this->form_pointer === null) { /* Act as if a start tag token with the tag name "form" had been seen. */ $this->inBody( array( 'name' => 'body', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); /* Act as if a start tag token with the tag name "hr" had been seen. */ $this->inBody( array( 'name' => 'hr', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); /* Act as if a start tag token with the tag name "p" had been seen. */ $this->inBody( array( 'name' => 'p', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); /* Act as if a start tag token with the tag name "label" had been seen. */ $this->inBody( array( 'name' => 'label', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); /* Act as if a stream of character tokens had been seen. */ $this->insertText( 'This is a searchable index. ' . 'Insert your search keywords here: ' ); /* Act as if a start tag token with the tag name "input" had been seen, with all the attributes from the "isindex" token, except with the "name" attribute set to the value "isindex" (ignoring any explicit "name" attribute). */ $attr = $token['attr']; $attr[] = array('name' => 'name', 'value' => 'isindex'); $this->inBody( array( 'name' => 'input', 'type' => HTML5::STARTTAG, 'attr' => $attr ) ); /* Act as if a stream of character tokens had been seen (see below for what they should say). */ $this->insertText( 'This is a searchable index. ' . 'Insert your search keywords here: ' ); /* Act as if an end tag token with the tag name "label" had been seen. */ $this->inBody( array( 'name' => 'label', 'type' => HTML5::ENDTAG ) ); /* Act as if an end tag token with the tag name "p" had been seen. */ $this->inBody( array( 'name' => 'p', 'type' => HTML5::ENDTAG ) ); /* Act as if a start tag token with the tag name "hr" had been seen. */ $this->inBody( array( 'name' => 'hr', 'type' => HTML5::ENDTAG ) ); /* Act as if an end tag token with the tag name "form" had been seen. */ $this->inBody( array( 'name' => 'form', 'type' => HTML5::ENDTAG ) ); } break; /* A start tag whose tag name is "textarea" */ case 'textarea': $this->insertElement($token); /* Switch the tokeniser's content model flag to the RCDATA state. */ return HTML5::RCDATA; break; /* A start tag whose tag name is one of: "iframe", "noembed", "noframes" */ case 'iframe': case 'noembed': case 'noframes': $this->insertElement($token); /* Switch the tokeniser's content model flag to the CDATA state. */ return HTML5::CDATA; break; /* A start tag whose tag name is "select" */ case 'select': /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); /* Insert an HTML element for the token. */ $this->insertElement($token); /* Change the insertion mode to "in select". */ $this->mode = self::IN_SELECT; break; /* A start or end tag whose tag name is one of: "caption", "col", "colgroup", "frame", "frameset", "head", "option", "optgroup", "tbody", "td", "tfoot", "th", "thead", "tr". */ case 'caption': case 'col': case 'colgroup': case 'frame': case 'frameset': case 'head': case 'option': case 'optgroup': case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': case 'tr': // Parse error. Ignore the token. break; /* A start or end tag whose tag name is one of: "event-source", "section", "nav", "article", "aside", "header", "footer", "datagrid", "command" */ case 'event-source': case 'section': case 'nav': case 'article': case 'aside': case 'header': case 'footer': case 'datagrid': case 'command': // Work in progress! break; /* A start tag token not covered by the previous entries */ default: /* Reconstruct the active formatting elements, if any. */ $this->reconstructActiveFormattingElements(); $this->insertElement($token, true, true); break; } break; case HTML5::ENDTAG: switch ($token['name']) { /* An end tag with the tag name "body" */ case 'body': /* If the second element in the stack of open elements is not a body element, this is a parse error. Ignore the token. (innerHTML case) */ if (count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { // Ignore. /* If the current node is not the body element, then this is a parse error. */ } elseif (end($this->stack)->nodeName !== 'body') { // Parse error. } /* Change the insertion mode to "after body". */ $this->mode = self::AFTER_BODY; break; /* An end tag with the tag name "html" */ case 'html': /* Act as if an end tag with tag name "body" had been seen, then, if that token wasn't ignored, reprocess the current token. */ $this->inBody( array( 'name' => 'body', 'type' => HTML5::ENDTAG ) ); return $this->afterBody($token); break; /* An end tag whose tag name is one of: "address", "blockquote", "center", "dir", "div", "dl", "fieldset", "listing", "menu", "ol", "pre", "ul" */ case 'address': case 'blockquote': case 'center': case 'dir': case 'div': case 'dl': case 'fieldset': case 'listing': case 'menu': case 'ol': case 'pre': case 'ul': /* If the stack of open elements has an element in scope with the same tag name as that of the token, then generate implied end tags. */ if ($this->elementInScope($token['name'])) { $this->generateImpliedEndTags(); /* Now, if the current node is not an element with the same tag name as that of the token, then this is a parse error. */ // w/e /* If the stack of open elements has an element in scope with the same tag name as that of the token, then pop elements from this stack until an element with that tag name has been popped from the stack. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === $token['name']) { $n = -1; } array_pop($this->stack); } } break; /* An end tag whose tag name is "form" */ case 'form': /* If the stack of open elements has an element in scope with the same tag name as that of the token, then generate implied end tags. */ if ($this->elementInScope($token['name'])) { $this->generateImpliedEndTags(); } if (end($this->stack)->nodeName !== $token['name']) { /* Now, if the current node is not an element with the same tag name as that of the token, then this is a parse error. */ // w/e } else { /* Otherwise, if the current node is an element with the same tag name as that of the token pop that element from the stack. */ array_pop($this->stack); } /* In any case, set the form element pointer to null. */ $this->form_pointer = null; break; /* An end tag whose tag name is "p" */ case 'p': /* If the stack of open elements has a p element in scope, then generate implied end tags, except for p elements. */ if ($this->elementInScope('p')) { $this->generateImpliedEndTags(array('p')); /* If the current node is not a p element, then this is a parse error. */ // k /* If the stack of open elements has a p element in scope, then pop elements from this stack until the stack no longer has a p element in scope. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->elementInScope('p')) { array_pop($this->stack); } else { break; } } } break; /* An end tag whose tag name is "dd", "dt", or "li" */ case 'dd': case 'dt': case 'li': /* If the stack of open elements has an element in scope whose tag name matches the tag name of the token, then generate implied end tags, except for elements with the same tag name as the token. */ if ($this->elementInScope($token['name'])) { $this->generateImpliedEndTags(array($token['name'])); /* If the current node is not an element with the same tag name as the token, then this is a parse error. */ // w/e /* If the stack of open elements has an element in scope whose tag name matches the tag name of the token, then pop elements from this stack until an element with that tag name has been popped from the stack. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === $token['name']) { $n = -1; } array_pop($this->stack); } } break; /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", "h5", "h6" */ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); /* If the stack of open elements has in scope an element whose tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then generate implied end tags. */ if ($this->elementInScope($elements)) { $this->generateImpliedEndTags(); /* Now, if the current node is not an element with the same tag name as that of the token, then this is a parse error. */ // w/e /* If the stack of open elements has in scope an element whose tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then pop elements from the stack until an element with one of those tag names has been popped from the stack. */ while ($this->elementInScope($elements)) { array_pop($this->stack); } } break; /* An end tag whose tag name is one of: "a", "b", "big", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ case 'a': case 'b': case 'big': case 'em': case 'font': case 'i': case 'nobr': case 's': case 'small': case 'strike': case 'strong': case 'tt': case 'u': /* 1. Let the formatting element be the last element in the list of active formatting elements that: * is between the end of the list and the last scope marker in the list, if any, or the start of the list otherwise, and * has the same tag name as the token. */ while (true) { for ($a = count($this->a_formatting) - 1; $a >= 0; $a--) { if ($this->a_formatting[$a] === self::MARKER) { break; } elseif ($this->a_formatting[$a]->tagName === $token['name']) { $formatting_element = $this->a_formatting[$a]; $in_stack = in_array($formatting_element, $this->stack, true); $fe_af_pos = $a; break; } } /* If there is no such node, or, if that node is also in the stack of open elements but the element is not in scope, then this is a parse error. Abort these steps. The token is ignored. */ if (!isset($formatting_element) || ($in_stack && !$this->elementInScope($token['name'])) ) { break; /* Otherwise, if there is such a node, but that node is not in the stack of open elements, then this is a parse error; remove the element from the list, and abort these steps. */ } elseif (isset($formatting_element) && !$in_stack) { unset($this->a_formatting[$fe_af_pos]); $this->a_formatting = array_merge($this->a_formatting); break; } /* 2. Let the furthest block be the topmost node in the stack of open elements that is lower in the stack than the formatting element, and is not an element in the phrasing or formatting categories. There might not be one. */ $fe_s_pos = array_search($formatting_element, $this->stack, true); $length = count($this->stack); for ($s = $fe_s_pos + 1; $s < $length; $s++) { $category = $this->getElementCategory($this->stack[$s]->nodeName); if ($category !== self::PHRASING && $category !== self::FORMATTING) { $furthest_block = $this->stack[$s]; } } /* 3. If there is no furthest block, then the UA must skip the subsequent steps and instead just pop all the nodes from the bottom of the stack of open elements, from the current node up to the formatting element, and remove the formatting element from the list of active formatting elements. */ if (!isset($furthest_block)) { for ($n = $length - 1; $n >= $fe_s_pos; $n--) { array_pop($this->stack); } unset($this->a_formatting[$fe_af_pos]); $this->a_formatting = array_merge($this->a_formatting); break; } /* 4. Let the common ancestor be the element immediately above the formatting element in the stack of open elements. */ $common_ancestor = $this->stack[$fe_s_pos - 1]; /* 5. If the furthest block has a parent node, then remove the furthest block from its parent node. */ if ($furthest_block->parentNode !== null) { $furthest_block->parentNode->removeChild($furthest_block); } /* 6. Let a bookmark note the position of the formatting element in the list of active formatting elements relative to the elements on either side of it in the list. */ $bookmark = $fe_af_pos; /* 7. Let node and last node be the furthest block. Follow these steps: */ $node = $furthest_block; $last_node = $furthest_block; while (true) { for ($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { /* 7.1 Let node be the element immediately prior to node in the stack of open elements. */ $node = $this->stack[$n]; /* 7.2 If node is not in the list of active formatting elements, then remove node from the stack of open elements and then go back to step 1. */ if (!in_array($node, $this->a_formatting, true)) { unset($this->stack[$n]); $this->stack = array_merge($this->stack); } else { break; } } /* 7.3 Otherwise, if node is the formatting element, then go to the next step in the overall algorithm. */ if ($node === $formatting_element) { break; /* 7.4 Otherwise, if last node is the furthest block, then move the aforementioned bookmark to be immediately after the node in the list of active formatting elements. */ } elseif ($last_node === $furthest_block) { $bookmark = array_search($node, $this->a_formatting, true) + 1; } /* 7.5 If node has any children, perform a shallow clone of node, replace the entry for node in the list of active formatting elements with an entry for the clone, replace the entry for node in the stack of open elements with an entry for the clone, and let node be the clone. */ if ($node->hasChildNodes()) { $clone = $node->cloneNode(); $s_pos = array_search($node, $this->stack, true); $a_pos = array_search($node, $this->a_formatting, true); $this->stack[$s_pos] = $clone; $this->a_formatting[$a_pos] = $clone; $node = $clone; } /* 7.6 Insert last node into node, first removing it from its previous parent node if any. */ if ($last_node->parentNode !== null) { $last_node->parentNode->removeChild($last_node); } $node->appendChild($last_node); /* 7.7 Let last node be node. */ $last_node = $node; } /* 8. Insert whatever last node ended up being in the previous step into the common ancestor node, first removing it from its previous parent node if any. */ if ($last_node->parentNode !== null) { $last_node->parentNode->removeChild($last_node); } $common_ancestor->appendChild($last_node); /* 9. Perform a shallow clone of the formatting element. */ $clone = $formatting_element->cloneNode(); /* 10. Take all of the child nodes of the furthest block and append them to the clone created in the last step. */ while ($furthest_block->hasChildNodes()) { $child = $furthest_block->firstChild; $furthest_block->removeChild($child); $clone->appendChild($child); } /* 11. Append that clone to the furthest block. */ $furthest_block->appendChild($clone); /* 12. Remove the formatting element from the list of active formatting elements, and insert the clone into the list of active formatting elements at the position of the aforementioned bookmark. */ $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); unset($this->a_formatting[$fe_af_pos]); $this->a_formatting = array_merge($this->a_formatting); $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); /* 13. Remove the formatting element from the stack of open elements, and insert the clone into the stack of open elements immediately after (i.e. in a more deeply nested position than) the position of the furthest block in that stack. */ $fe_s_pos = array_search($formatting_element, $this->stack, true); $fb_s_pos = array_search($furthest_block, $this->stack, true); unset($this->stack[$fe_s_pos]); $s_part1 = array_slice($this->stack, 0, $fb_s_pos); $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); $this->stack = array_merge($s_part1, array($clone), $s_part2); /* 14. Jump back to step 1 in this series of steps. */ unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); } break; /* An end tag token whose tag name is one of: "button", "marquee", "object" */ case 'button': case 'marquee': case 'object': /* If the stack of open elements has an element in scope whose tag name matches the tag name of the token, then generate implied tags. */ if ($this->elementInScope($token['name'])) { $this->generateImpliedEndTags(); /* Now, if the current node is not an element with the same tag name as the token, then this is a parse error. */ // k /* Now, if the stack of open elements has an element in scope whose tag name matches the tag name of the token, then pop elements from the stack until that element has been popped from the stack, and clear the list of active formatting elements up to the last marker. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === $token['name']) { $n = -1; } array_pop($this->stack); } $marker = end(array_keys($this->a_formatting, self::MARKER, true)); for ($n = count($this->a_formatting) - 1; $n > $marker; $n--) { array_pop($this->a_formatting); } } break; /* Or an end tag whose tag name is one of: "area", "basefont", "bgsound", "br", "embed", "hr", "iframe", "image", "img", "input", "isindex", "noembed", "noframes", "param", "select", "spacer", "table", "textarea", "wbr" */ case 'area': case 'basefont': case 'bgsound': case 'br': case 'embed': case 'hr': case 'iframe': case 'image': case 'img': case 'input': case 'isindex': case 'noembed': case 'noframes': case 'param': case 'select': case 'spacer': case 'table': case 'textarea': case 'wbr': // Parse error. Ignore the token. break; /* An end tag token not covered by the previous entries */ default: for ($n = count($this->stack) - 1; $n >= 0; $n--) { /* Initialise node to be the current node (the bottommost node of the stack). */ $node = end($this->stack); /* If node has the same tag name as the end tag token, then: */ if ($token['name'] === $node->nodeName) { /* Generate implied end tags. */ $this->generateImpliedEndTags(); /* If the tag name of the end tag token does not match the tag name of the current node, this is a parse error. */ // k /* Pop all the nodes from the current node up to node, including node, then stop this algorithm. */ for ($x = count($this->stack) - $n; $x >= $n; $x--) { array_pop($this->stack); } } else { $category = $this->getElementCategory($node); if ($category !== self::SPECIAL && $category !== self::SCOPING) { /* Otherwise, if node is in neither the formatting category nor the phrasing category, then this is a parse error. Stop this algorithm. The end tag token is ignored. */ return false; } } } break; } break; } } private function inTable($token) { $clear = array('html', 'table'); /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $text = $this->dom->createTextNode($token['data']); end($this->stack)->appendChild($text); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); end($this->stack)->appendChild($comment); /* A start tag whose tag name is "caption" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'caption' ) { /* Clear the stack back to a table context. */ $this->clearStackToTableContext($clear); /* Insert a marker at the end of the list of active formatting elements. */ $this->a_formatting[] = self::MARKER; /* Insert an HTML element for the token, then switch the insertion mode to "in caption". */ $this->insertElement($token); $this->mode = self::IN_CAPTION; /* A start tag whose tag name is "colgroup" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'colgroup' ) { /* Clear the stack back to a table context. */ $this->clearStackToTableContext($clear); /* Insert an HTML element for the token, then switch the insertion mode to "in column group". */ $this->insertElement($token); $this->mode = self::IN_CGROUP; /* A start tag whose tag name is "col" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col' ) { $this->inTable( array( 'name' => 'colgroup', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); $this->inColumnGroup($token); /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('tbody', 'tfoot', 'thead') ) ) { /* Clear the stack back to a table context. */ $this->clearStackToTableContext($clear); /* Insert an HTML element for the token, then switch the insertion mode to "in table body". */ $this->insertElement($token); $this->mode = self::IN_TBODY; /* A start tag whose tag name is one of: "td", "th", "tr" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array($token['name'], array('td', 'th', 'tr')) ) { /* Act as if a start tag token with the tag name "tbody" had been seen, then reprocess the current token. */ $this->inTable( array( 'name' => 'tbody', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); return $this->inTableBody($token); /* A start tag whose tag name is "table" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table' ) { /* Parse error. Act as if an end tag token with the tag name "table" had been seen, then, if that token wasn't ignored, reprocess the current token. */ $this->inTable( array( 'name' => 'table', 'type' => HTML5::ENDTAG ) ); return $this->mainPhase($token); /* An end tag whose tag name is "table" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'table' ) { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope($token['name'], true)) { return false; /* Otherwise: */ } else { /* Generate implied end tags. */ $this->generateImpliedEndTags(); /* Now, if the current node is not a table element, then this is a parse error. */ // w/e /* Pop elements from this stack until a table element has been popped from the stack. */ while (true) { $current = end($this->stack)->nodeName; array_pop($this->stack); if ($current === 'table') { break; } } /* Reset the insertion mode appropriately. */ $this->resetInsertionMode(); } /* An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array( 'body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr' ) ) ) { // Parse error. Ignore the token. /* Anything else */ } else { /* Parse error. Process the token as if the insertion mode was "in body", with the following exception: */ /* If the current node is a table, tbody, tfoot, thead, or tr element, then, whenever a node would be inserted into the current node, it must instead be inserted into the foster parent element. */ if (in_array( end($this->stack)->nodeName, array('table', 'tbody', 'tfoot', 'thead', 'tr') ) ) { /* The foster parent element is the parent element of the last table element in the stack of open elements, if there is a table element and it has such a parent element. If there is no table element in the stack of open elements (innerHTML case), then the foster parent element is the first element in the stack of open elements (the html element). Otherwise, if there is a table element in the stack of open elements, but the last table element in the stack of open elements has no parent, or its parent node is not an element, then the foster parent element is the element before the last table element in the stack of open elements. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === 'table') { $table = $this->stack[$n]; break; } } if (isset($table) && $table->parentNode !== null) { $this->foster_parent = $table->parentNode; } elseif (!isset($table)) { $this->foster_parent = $this->stack[0]; } elseif (isset($table) && ($table->parentNode === null || $table->parentNode->nodeType !== XML_ELEMENT_NODE) ) { $this->foster_parent = $this->stack[$n - 1]; } } $this->inBody($token); } } private function inCaption($token) { /* An end tag whose tag name is "caption" */ if ($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope($token['name'], true)) { // Ignore /* Otherwise: */ } else { /* Generate implied end tags. */ $this->generateImpliedEndTags(); /* Now, if the current node is not a caption element, then this is a parse error. */ // w/e /* Pop elements from this stack until a caption element has been popped from the stack. */ while (true) { $node = end($this->stack)->nodeName; array_pop($this->stack); if ($node === 'caption') { break; } } /* Clear the list of active formatting elements up to the last marker. */ $this->clearTheActiveFormattingElementsUpToTheLastMarker(); /* Switch the insertion mode to "in table". */ $this->mode = self::IN_TABLE; } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag name is "table" */ } elseif (($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array( 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr' ) )) || ($token['type'] === HTML5::ENDTAG && $token['name'] === 'table') ) { /* Parse error. Act as if an end tag with the tag name "caption" had been seen, then, if that token wasn't ignored, reprocess the current token. */ $this->inCaption( array( 'name' => 'caption', 'type' => HTML5::ENDTAG ) ); return $this->inTable($token); /* An end tag whose tag name is one of: "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array( 'body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th', 'thead', 'tr' ) ) ) { // Parse error. Ignore the token. /* Anything else */ } else { /* Process the token as if the insertion mode was "in body". */ $this->inBody($token); } } private function inColumnGroup($token) { /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $text = $this->dom->createTextNode($token['data']); end($this->stack)->appendChild($text); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); end($this->stack)->appendChild($comment); /* A start tag whose tag name is "col" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { /* Insert a col element for the token. Immediately pop the current node off the stack of open elements. */ $this->insertElement($token); array_pop($this->stack); /* An end tag whose tag name is "colgroup" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'colgroup' ) { /* If the current node is the root html element, then this is a parse error, ignore the token. (innerHTML case) */ if (end($this->stack)->nodeName === 'html') { // Ignore /* Otherwise, pop the current node (which will be a colgroup element) from the stack of open elements. Switch the insertion mode to "in table". */ } else { array_pop($this->stack); $this->mode = self::IN_TABLE; } /* An end tag whose tag name is "col" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { /* Parse error. Ignore the token. */ /* Anything else */ } else { /* Act as if an end tag with the tag name "colgroup" had been seen, and then, if that token wasn't ignored, reprocess the current token. */ $this->inColumnGroup( array( 'name' => 'colgroup', 'type' => HTML5::ENDTAG ) ); return $this->inTable($token); } } private function inTableBody($token) { $clear = array('tbody', 'tfoot', 'thead', 'html'); /* A start tag whose tag name is "tr" */ if ($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { /* Clear the stack back to a table body context. */ $this->clearStackToTableContext($clear); /* Insert a tr element for the token, then switch the insertion mode to "in row". */ $this->insertElement($token); $this->mode = self::IN_ROW; /* A start tag whose tag name is one of: "th", "td" */ } elseif ($token['type'] === HTML5::STARTTAG && ($token['name'] === 'th' || $token['name'] === 'td') ) { /* Parse error. Act as if a start tag with the tag name "tr" had been seen, then reprocess the current token. */ $this->inTableBody( array( 'name' => 'tr', 'type' => HTML5::STARTTAG, 'attr' => array() ) ); return $this->inRow($token); /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array($token['name'], array('tbody', 'tfoot', 'thead')) ) { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. */ if (!$this->elementInScope($token['name'], true)) { // Ignore /* Otherwise: */ } else { /* Clear the stack back to a table body context. */ $this->clearStackToTableContext($clear); /* Pop the current node from the stack of open elements. Switch the insertion mode to "in table". */ array_pop($this->stack); $this->mode = self::IN_TABLE; } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ } elseif (($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead') )) || ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table') ) { /* If the stack of open elements does not have a tbody, thead, or tfoot element in table scope, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { // Ignore. /* Otherwise: */ } else { /* Clear the stack back to a table body context. */ $this->clearStackToTableContext($clear); /* Act as if an end tag with the same tag name as the current node ("tbody", "tfoot", or "thead") had been seen, then reprocess the current token. */ $this->inTableBody( array( 'name' => end($this->stack)->nodeName, 'type' => HTML5::ENDTAG ) ); return $this->mainPhase($token); } /* An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th", "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') ) ) { /* Parse error. Ignore the token. */ /* Anything else */ } else { /* Process the token as if the insertion mode was "in table". */ $this->inTable($token); } } private function inRow($token) { $clear = array('tr', 'html'); /* A start tag whose tag name is one of: "th", "td" */ if ($token['type'] === HTML5::STARTTAG && ($token['name'] === 'th' || $token['name'] === 'td') ) { /* Clear the stack back to a table row context. */ $this->clearStackToTableContext($clear); /* Insert an HTML element for the token, then switch the insertion mode to "in cell". */ $this->insertElement($token); $this->mode = self::IN_CELL; /* Insert a marker at the end of the list of active formatting elements. */ $this->a_formatting[] = self::MARKER; /* An end tag whose tag name is "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope($token['name'], true)) { // Ignore. /* Otherwise: */ } else { /* Clear the stack back to a table row context. */ $this->clearStackToTableContext($clear); /* Pop the current node (which will be a tr element) from the stack of open elements. Switch the insertion mode to "in table body". */ array_pop($this->stack); $this->mode = self::IN_TBODY; } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr') ) ) { /* Act as if an end tag with the tag name "tr" had been seen, then, if that token wasn't ignored, reprocess the current token. */ $this->inRow( array( 'name' => 'tr', 'type' => HTML5::ENDTAG ) ); return $this->inCell($token); /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array($token['name'], array('tbody', 'tfoot', 'thead')) ) { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. */ if (!$this->elementInScope($token['name'], true)) { // Ignore. /* Otherwise: */ } else { /* Otherwise, act as if an end tag with the tag name "tr" had been seen, then reprocess the current token. */ $this->inRow( array( 'name' => 'tr', 'type' => HTML5::ENDTAG ) ); return $this->inCell($token); } /* An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html", "td", "th" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') ) ) { /* Parse error. Ignore the token. */ /* Anything else */ } else { /* Process the token as if the insertion mode was "in table". */ $this->inTable($token); } } private function inCell($token) { /* An end tag whose tag name is one of: "td", "th" */ if ($token['type'] === HTML5::ENDTAG && ($token['name'] === 'td' || $token['name'] === 'th') ) { /* If the stack of open elements does not have an element in table scope with the same tag name as that of the token, then this is a parse error and the token must be ignored. */ if (!$this->elementInScope($token['name'], true)) { // Ignore. /* Otherwise: */ } else { /* Generate implied end tags, except for elements with the same tag name as the token. */ $this->generateImpliedEndTags(array($token['name'])); /* Now, if the current node is not an element with the same tag name as the token, then this is a parse error. */ // k /* Pop elements from this stack until an element with the same tag name as the token has been popped from the stack. */ while (true) { $node = end($this->stack)->nodeName; array_pop($this->stack); if ($node === $token['name']) { break; } } /* Clear the list of active formatting elements up to the last marker. */ $this->clearTheActiveFormattingElementsUpToTheLastMarker(); /* Switch the insertion mode to "in row". (The current node will be a tr element at this point.) */ $this->mode = self::IN_ROW; } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array( 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr' ) ) ) { /* If the stack of open elements does not have a td or th element in table scope, then this is a parse error; ignore the token. (innerHTML case) */ if (!$this->elementInScope(array('td', 'th'), true)) { // Ignore. /* Otherwise, close the cell (see below) and reprocess the current token. */ } else { $this->closeCell(); return $this->inRow($token); } /* A start tag whose tag name is one of: "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr" */ } elseif ($token['type'] === HTML5::STARTTAG && in_array( $token['name'], array( 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr' ) ) ) { /* If the stack of open elements does not have a td or th element in table scope, then this is a parse error; ignore the token. (innerHTML case) */ if (!$this->elementInScope(array('td', 'th'), true)) { // Ignore. /* Otherwise, close the cell (see below) and reprocess the current token. */ } else { $this->closeCell(); return $this->inRow($token); } /* An end tag whose tag name is one of: "body", "caption", "col", "colgroup", "html" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array('body', 'caption', 'col', 'colgroup', 'html') ) ) { /* Parse error. Ignore the token. */ /* An end tag whose tag name is one of: "table", "tbody", "tfoot", "thead", "tr" */ } elseif ($token['type'] === HTML5::ENDTAG && in_array( $token['name'], array('table', 'tbody', 'tfoot', 'thead', 'tr') ) ) { /* If the stack of open elements does not have an element in table scope with the same tag name as that of the token (which can only happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), then this is a parse error and the token must be ignored. */ if (!$this->elementInScope($token['name'], true)) { // Ignore. /* Otherwise, close the cell (see below) and reprocess the current token. */ } else { $this->closeCell(); return $this->inRow($token); } /* Anything else */ } else { /* Process the token as if the insertion mode was "in body". */ $this->inBody($token); } } private function inSelect($token) { /* Handle the token as follows: */ /* A character token */ if ($token['type'] === HTML5::CHARACTR) { /* Append the token's character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* A start tag token whose tag name is "option" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'option' ) { /* If the current node is an option element, act as if an end tag with the tag name "option" had been seen. */ if (end($this->stack)->nodeName === 'option') { $this->inSelect( array( 'name' => 'option', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); /* A start tag token whose tag name is "optgroup" */ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'optgroup' ) { /* If the current node is an option element, act as if an end tag with the tag name "option" had been seen. */ if (end($this->stack)->nodeName === 'option') { $this->inSelect( array( 'name' => 'option', 'type' => HTML5::ENDTAG ) ); } /* If the current node is an optgroup element, act as if an end tag with the tag name "optgroup" had been seen. */ if (end($this->stack)->nodeName === 'optgroup') { $this->inSelect( array( 'name' => 'optgroup', 'type' => HTML5::ENDTAG ) ); } /* Insert an HTML element for the token. */ $this->insertElement($token); /* An end tag token whose tag name is "optgroup" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'optgroup' ) { /* First, if the current node is an option element, and the node immediately before it in the stack of open elements is an optgroup element, then act as if an end tag with the tag name "option" had been seen. */ $elements_in_stack = count($this->stack); if ($this->stack[$elements_in_stack - 1]->nodeName === 'option' && $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup' ) { $this->inSelect( array( 'name' => 'option', 'type' => HTML5::ENDTAG ) ); } /* If the current node is an optgroup element, then pop that node from the stack of open elements. Otherwise, this is a parse error, ignore the token. */ if ($this->stack[$elements_in_stack - 1] === 'optgroup') { array_pop($this->stack); } /* An end tag token whose tag name is "option" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'option' ) { /* If the current node is an option element, then pop that node from the stack of open elements. Otherwise, this is a parse error, ignore the token. */ if (end($this->stack)->nodeName === 'option') { array_pop($this->stack); } /* An end tag whose tag name is "select" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'select' ) { /* If the stack of open elements does not have an element in table scope with the same tag name as the token, this is a parse error. Ignore the token. (innerHTML case) */ if (!$this->elementInScope($token['name'], true)) { // w/e /* Otherwise: */ } else { /* Pop elements from the stack of open elements until a select element has been popped from the stack. */ while (true) { $current = end($this->stack)->nodeName; array_pop($this->stack); if ($current === 'select') { break; } } /* Reset the insertion mode appropriately. */ $this->resetInsertionMode(); } /* A start tag whose tag name is "select" */ } elseif ($token['name'] === 'select' && $token['type'] === HTML5::STARTTAG ) { /* Parse error. Act as if the token had been an end tag with the tag name "select" instead. */ $this->inSelect( array( 'name' => 'select', 'type' => HTML5::ENDTAG ) ); /* An end tag whose tag name is one of: "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th" */ } elseif (in_array( $token['name'], array( 'caption', 'table', 'tbody', 'tfoot', 'thead', 'tr', 'td', 'th' ) ) && $token['type'] === HTML5::ENDTAG ) { /* Parse error. */ // w/e /* If the stack of open elements has an element in table scope with the same tag name as that of the token, then act as if an end tag with the tag name "select" had been seen, and reprocess the token. Otherwise, ignore the token. */ if ($this->elementInScope($token['name'], true)) { $this->inSelect( array( 'name' => 'select', 'type' => HTML5::ENDTAG ) ); $this->mainPhase($token); } /* Anything else */ } else { /* Parse error. Ignore the token. */ } } private function afterBody($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Process the token as it would be processed if the insertion mode was "in body". */ $this->inBody($token); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the first element in the stack of open elements (the html element), with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); $this->stack[0]->appendChild($comment); /* An end tag with the tag name "html" */ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { /* If the parser was originally created in order to handle the setting of an element's innerHTML attribute, this is a parse error; ignore the token. (The element will be an html element in this case.) (innerHTML case) */ /* Otherwise, switch to the trailing end phase. */ $this->phase = self::END_PHASE; /* Anything else */ } else { /* Parse error. Set the insertion mode to "in body" and reprocess the token. */ $this->mode = self::IN_BODY; return $this->inBody($token); } } private function inFrameset($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* A start tag with the tag name "frameset" */ } elseif ($token['name'] === 'frameset' && $token['type'] === HTML5::STARTTAG ) { $this->insertElement($token); /* An end tag with the tag name "frameset" */ } elseif ($token['name'] === 'frameset' && $token['type'] === HTML5::ENDTAG ) { /* If the current node is the root html element, then this is a parse error; ignore the token. (innerHTML case) */ if (end($this->stack)->nodeName === 'html') { // Ignore } else { /* Otherwise, pop the current node from the stack of open elements. */ array_pop($this->stack); /* If the parser was not originally created in order to handle the setting of an element's innerHTML attribute (innerHTML case), and the current node is no longer a frameset element, then change the insertion mode to "after frameset". */ $this->mode = self::AFTR_FRAME; } /* A start tag with the tag name "frame" */ } elseif ($token['name'] === 'frame' && $token['type'] === HTML5::STARTTAG ) { /* Insert an HTML element for the token. */ $this->insertElement($token); /* Immediately pop the current node off the stack of open elements. */ array_pop($this->stack); /* A start tag with the tag name "noframes" */ } elseif ($token['name'] === 'noframes' && $token['type'] === HTML5::STARTTAG ) { /* Process the token as if the insertion mode had been "in body". */ $this->inBody($token); /* Anything else */ } else { /* Parse error. Ignore the token. */ } } private function afterFrameset($token) { /* Handle the token as follows: */ /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ if ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Append the character to the current node. */ $this->insertText($token['data']); /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the current node with the data attribute set to the data given in the comment token. */ $this->insertComment($token['data']); /* An end tag with the tag name "html" */ } elseif ($token['name'] === 'html' && $token['type'] === HTML5::ENDTAG ) { /* Switch to the trailing end phase. */ $this->phase = self::END_PHASE; /* A start tag with the tag name "noframes" */ } elseif ($token['name'] === 'noframes' && $token['type'] === HTML5::STARTTAG ) { /* Process the token as if the insertion mode had been "in body". */ $this->inBody($token); /* Anything else */ } else { /* Parse error. Ignore the token. */ } } private function trailingEndPhase($token) { /* After the main phase, as each token is emitted from the tokenisation stage, it must be processed as described in this section. */ /* A DOCTYPE token */ if ($token['type'] === HTML5::DOCTYPE) { // Parse error. Ignore the token. /* A comment token */ } elseif ($token['type'] === HTML5::COMMENT) { /* Append a Comment node to the Document object with the data attribute set to the data given in the comment token. */ $comment = $this->dom->createComment($token['data']); $this->dom->appendChild($comment); /* A character token that is one of one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE */ } elseif ($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) ) { /* Process the token as it would be processed in the main phase. */ $this->mainPhase($token); /* A character token that is not one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), or U+0020 SPACE. Or a start tag token. Or an end tag token. */ } elseif (($token['type'] === HTML5::CHARACTR && preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG ) { /* Parse error. Switch back to the main phase and reprocess the token. */ $this->phase = self::MAIN_PHASE; return $this->mainPhase($token); /* An end-of-file token */ } elseif ($token['type'] === HTML5::EOF) { /* OMG DONE!! */ } } private function insertElement($token, $append = true, $check = false) { // Proprietary workaround for libxml2's limitations with tag names if ($check) { // Slightly modified HTML5 tag-name modification, // removing anything that's not an ASCII letter, digit, or hyphen $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); // Remove leading hyphens and numbers $token['name'] = ltrim($token['name'], '-0..9'); // In theory, this should ever be needed, but just in case if ($token['name'] === '') { $token['name'] = 'span'; } // arbitrary generic choice } $el = $this->dom->createElement($token['name']); foreach ($token['attr'] as $attr) { if (!$el->hasAttribute($attr['name'])) { $el->setAttribute($attr['name'], (string)$attr['value']); } } $this->appendToRealParent($el); $this->stack[] = $el; return $el; } private function insertText($data) { $text = $this->dom->createTextNode($data); $this->appendToRealParent($text); } private function insertComment($data) { $comment = $this->dom->createComment($data); $this->appendToRealParent($comment); } private function appendToRealParent($node) { if ($this->foster_parent === null) { end($this->stack)->appendChild($node); } elseif ($this->foster_parent !== null) { /* If the foster parent element is the parent element of the last table element in the stack of open elements, then the new node must be inserted immediately before the last table element in the stack of open elements in the foster parent element; otherwise, the new node must be appended to the foster parent element. */ for ($n = count($this->stack) - 1; $n >= 0; $n--) { if ($this->stack[$n]->nodeName === 'table' && $this->stack[$n]->parentNode !== null ) { $table = $this->stack[$n]; break; } } if (isset($table) && $this->foster_parent->isSameNode($table->parentNode)) { $this->foster_parent->insertBefore($node, $table); } else { $this->foster_parent->appendChild($node); } $this->foster_parent = null; } } private function elementInScope($el, $table = false) { if (is_array($el)) { foreach ($el as $element) { if ($this->elementInScope($element, $table)) { return true; } } return false; } $leng = count($this->stack); for ($n = 0; $n < $leng; $n++) { /* 1. Initialise node to be the current node (the bottommost node of the stack). */ $node = $this->stack[$leng - 1 - $n]; if ($node->tagName === $el) { /* 2. If node is the target node, terminate in a match state. */ return true; } elseif ($node->tagName === 'table') { /* 3. Otherwise, if node is a table element, terminate in a failure state. */ return false; } elseif ($table === true && in_array( $node->tagName, array( 'caption', 'td', 'th', 'button', 'marquee', 'object' ) ) ) { /* 4. Otherwise, if the algorithm is the "has an element in scope" variant (rather than the "has an element in table scope" variant), and node is one of the following, terminate in a failure state. */ return false; } elseif ($node === $node->ownerDocument->documentElement) { /* 5. Otherwise, if node is an html element (root element), terminate in a failure state. (This can only happen if the node is the topmost node of the stack of open elements, and prevents the next step from being invoked if there are no more elements in the stack.) */ return false; } /* Otherwise, set node to the previous entry in the stack of open elements and return to step 2. (This will never fail, since the loop will always terminate in the previous step if the top of the stack is reached.) */ } } private function reconstructActiveFormattingElements() { /* 1. If there are no entries in the list of active formatting elements, then there is nothing to reconstruct; stop this algorithm. */ $formatting_elements = count($this->a_formatting); if ($formatting_elements === 0) { return false; } /* 3. Let entry be the last (most recently added) element in the list of active formatting elements. */ $entry = end($this->a_formatting); /* 2. If the last (most recently added) entry in the list of active formatting elements is a marker, or if it is an element that is in the stack of open elements, then there is nothing to reconstruct; stop this algorithm. */ if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { return false; } for ($a = $formatting_elements - 1; $a >= 0; true) { /* 4. If there are no entries before entry in the list of active formatting elements, then jump to step 8. */ if ($a === 0) { $step_seven = false; break; } /* 5. Let entry be the entry one earlier than entry in the list of active formatting elements. */ $a--; $entry = $this->a_formatting[$a]; /* 6. If entry is neither a marker nor an element that is also in thetack of open elements, go to step 4. */ if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { break; } } while (true) { /* 7. Let entry be the element one later than entry in the list of active formatting elements. */ if (isset($step_seven) && $step_seven === true) { $a++; $entry = $this->a_formatting[$a]; } /* 8. Perform a shallow clone of the element entry to obtain clone. */ $clone = $entry->cloneNode(); /* 9. Append clone to the current node and push it onto the stack of open elements so that it is the new current node. */ end($this->stack)->appendChild($clone); $this->stack[] = $clone; /* 10. Replace the entry for entry in the list with an entry for clone. */ $this->a_formatting[$a] = $clone; /* 11. If the entry for clone in the list of active formatting elements is not the last entry in the list, return to step 7. */ if (end($this->a_formatting) !== $clone) { $step_seven = true; } else { break; } } } private function clearTheActiveFormattingElementsUpToTheLastMarker() { /* When the steps below require the UA to clear the list of active formatting elements up to the last marker, the UA must perform the following steps: */ while (true) { /* 1. Let entry be the last (most recently added) entry in the list of active formatting elements. */ $entry = end($this->a_formatting); /* 2. Remove entry from the list of active formatting elements. */ array_pop($this->a_formatting); /* 3. If entry was a marker, then stop the algorithm at this point. The list has been cleared up to the last marker. */ if ($entry === self::MARKER) { break; } } } private function generateImpliedEndTags($exclude = array()) { /* When the steps below require the UA to generate implied end tags, then, if the current node is a dd element, a dt element, an li element, a p element, a td element, a th element, or a tr element, the UA must act as if an end tag with the respective tag name had been seen and then generate implied end tags again. */ $node = end($this->stack); $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); while (in_array(end($this->stack)->nodeName, $elements)) { array_pop($this->stack); } } private function getElementCategory($node) { $name = $node->tagName; if (in_array($name, $this->special)) { return self::SPECIAL; } elseif (in_array($name, $this->scoping)) { return self::SCOPING; } elseif (in_array($name, $this->formatting)) { return self::FORMATTING; } else { return self::PHRASING; } } private function clearStackToTableContext($elements) { /* When the steps above require the UA to clear the stack back to a table context, it means that the UA must, while the current node is not a table element or an html element, pop elements from the stack of open elements. If this causes any elements to be popped from the stack, then this is a parse error. */ while (true) { $node = end($this->stack)->nodeName; if (in_array($node, $elements)) { break; } else { array_pop($this->stack); } } } private function resetInsertionMode() { /* 1. Let last be false. */ $last = false; $leng = count($this->stack); for ($n = $leng - 1; $n >= 0; $n--) { /* 2. Let node be the last node in the stack of open elements. */ $node = $this->stack[$n]; /* 3. If node is the first node in the stack of open elements, then set last to true. If the element whose innerHTML attribute is being set is neither a td element nor a th element, then set node to the element whose innerHTML attribute is being set. (innerHTML case) */ if ($this->stack[0]->isSameNode($node)) { $last = true; } /* 4. If node is a select element, then switch the insertion mode to "in select" and abort these steps. (innerHTML case) */ if ($node->nodeName === 'select') { $this->mode = self::IN_SELECT; break; /* 5. If node is a td or th element, then switch the insertion mode to "in cell" and abort these steps. */ } elseif ($node->nodeName === 'td' || $node->nodeName === 'th') { $this->mode = self::IN_CELL; break; /* 6. If node is a tr element, then switch the insertion mode to "in row" and abort these steps. */ } elseif ($node->nodeName === 'tr') { $this->mode = self::IN_ROW; break; /* 7. If node is a tbody, thead, or tfoot element, then switch the insertion mode to "in table body" and abort these steps. */ } elseif (in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { $this->mode = self::IN_TBODY; break; /* 8. If node is a caption element, then switch the insertion mode to "in caption" and abort these steps. */ } elseif ($node->nodeName === 'caption') { $this->mode = self::IN_CAPTION; break; /* 9. If node is a colgroup element, then switch the insertion mode to "in column group" and abort these steps. (innerHTML case) */ } elseif ($node->nodeName === 'colgroup') { $this->mode = self::IN_CGROUP; break; /* 10. If node is a table element, then switch the insertion mode to "in table" and abort these steps. */ } elseif ($node->nodeName === 'table') { $this->mode = self::IN_TABLE; break; /* 11. If node is a head element, then switch the insertion mode to "in body" ("in body"! not "in head"!) and abort these steps. (innerHTML case) */ } elseif ($node->nodeName === 'head') { $this->mode = self::IN_BODY; break; /* 12. If node is a body element, then switch the insertion mode to "in body" and abort these steps. */ } elseif ($node->nodeName === 'body') { $this->mode = self::IN_BODY; break; /* 13. If node is a frameset element, then switch the insertion mode to "in frameset" and abort these steps. (innerHTML case) */ } elseif ($node->nodeName === 'frameset') { $this->mode = self::IN_FRAME; break; /* 14. If node is an html element, then: if the head element pointer is null, switch the insertion mode to "before head", otherwise, switch the insertion mode to "after head". In either case, abort these steps. (innerHTML case) */ } elseif ($node->nodeName === 'html') { $this->mode = ($this->head_pointer === null) ? self::BEFOR_HEAD : self::AFTER_HEAD; break; /* 15. If last is true, then set the insertion mode to "in body" and abort these steps. (innerHTML case) */ } elseif ($last) { $this->mode = self::IN_BODY; break; } } } private function closeCell() { /* If the stack of open elements has a td or th element in table scope, then act as if an end tag token with that tag name had been seen. */ foreach (array('td', 'th') as $cell) { if ($this->elementInScope($cell, true)) { $this->inCell( array( 'name' => $cell, 'type' => HTML5::ENDTAG ) ); break; } } } public function save() { return $this->dom; } } htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php000064400000034110152665541550016071 0ustar00factory = new HTMLPurifier_TokenFactory(); } /** * @param string $html * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return HTMLPurifier_Token[] */ public function tokenizeHTML($html, $config, $context) { $html = $this->normalize($html, $config, $context); // attempt to armor stray angled brackets that cannot possibly // form tags and thus are probably being used as emoticons if ($config->get('Core.AggressivelyFixLt')) { $html = $this->aggressivelyFixLt($html); } // preprocess html, essential for UTF-8 $html = $this->wrapHTML($html, $config, $context); $doc = new DOMDocument(); $doc->encoding = 'UTF-8'; // theoretically, the above has this covered $options = 0; if ($config->get('Core.AllowParseManyTags') && defined('LIBXML_PARSEHUGE')) { $options |= LIBXML_PARSEHUGE; } if ($config->get('Core.RemoveBlanks') && defined('LIBXML_NOBLANKS')) { $options |= LIBXML_NOBLANKS; } set_error_handler(array($this, 'muteErrorHandler')); // loadHTML() fails on PHP 5.3 when second parameter is given if ($options) { $doc->loadHTML($html, $options); } else { $doc->loadHTML($html); } restore_error_handler(); $body = $doc->getElementsByTagName('html')->item(0)-> // getElementsByTagName('body')->item(0); // $div = $body->getElementsByTagName('div')->item(0); //
$tokens = array(); $this->tokenizeDOM($div, $tokens, $config); // If the div has a sibling, that means we tripped across // a premature
tag. So remove the div we parsed, // and then tokenize the rest of body. We can't tokenize // the sibling directly as we'll lose the tags in that case. if ($div->nextSibling) { $body->removeChild($div); $this->tokenizeDOM($body, $tokens, $config); } return $tokens; } /** * Iterative function that tokenizes a node, putting it into an accumulator. * To iterate is human, to recurse divine - L. Peter Deutsch * @param DOMNode $node DOMNode to be tokenized. * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. */ protected function tokenizeDOM($node, &$tokens, $config) { $level = 0; $nodes = array($level => new HTMLPurifier_Queue(array($node))); $closingNodes = array(); do { while (!$nodes[$level]->isEmpty()) { $node = $nodes[$level]->shift(); // FIFO $collect = $level > 0 ? true : false; $needEndingTag = $this->createStartNode($node, $tokens, $collect, $config); if ($needEndingTag) { $closingNodes[$level][] = $node; } if ($node->childNodes && $node->childNodes->length) { $level++; $nodes[$level] = new HTMLPurifier_Queue(); foreach ($node->childNodes as $childNode) { $nodes[$level]->push($childNode); } } } $level--; if ($level && isset($closingNodes[$level])) { while ($node = array_pop($closingNodes[$level])) { $this->createEndNode($node, $tokens); } } } while ($level > 0); } /** * Portably retrieve the tag name of a node; deals with older versions * of libxml like 2.7.6 * @param DOMNode $node */ protected function getTagName($node) { if (isset($node->tagName)) { return $node->tagName; } else if (isset($node->nodeName)) { return $node->nodeName; } else if (isset($node->localName)) { return $node->localName; } return null; } /** * Portably retrieve the data of a node; deals with older versions * of libxml like 2.7.6 * @param DOMNode $node */ protected function getData($node) { if (isset($node->data)) { return $node->data; } else if (isset($node->nodeValue)) { return $node->nodeValue; } else if (isset($node->textContent)) { return $node->textContent; } return null; } /** * @param DOMNode $node DOMNode to be tokenized. * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. * @param bool $collect Says whether or start and close are collected, set to * false at first recursion because it's the implicit DIV * tag you're dealing with. * @return bool if the token needs an endtoken * @todo data and tagName properties don't seem to exist in DOMNode? */ protected function createStartNode($node, &$tokens, $collect, $config) { // intercept non element nodes. WE MUST catch all of them, // but we're not getting the character reference nodes because // those should have been preprocessed if ($node->nodeType === XML_TEXT_NODE) { $data = $this->getData($node); // Handle variable data property if ($data !== null) { $tokens[] = $this->factory->createText($data); } return false; } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) { // undo libxml's special treatment of #i', '', $html); } return $html; } /** * Takes a string of HTML (fragment or document) and returns the content * @todo Consider making protected */ public function extractBody($html) { $matches = array(); $result = preg_match('|(.*?)]*>(.*)|is', $html, $matches); if ($result) { // Make sure it's not in a comment $comment_start = strrpos($matches[1], ''); if ($comment_start === false || ($comment_end !== false && $comment_end > $comment_start)) { return $matches[2]; } } return $html; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Zipper.php000064400000010532152665541550015175 0ustar00front = $front; $this->back = $back; } /** * Creates a zipper from an array, with a hole in the * 0-index position. * @param Array to zipper-ify. * @return Tuple of zipper and element of first position. */ static public function fromArray($array) { $z = new self(array(), array_reverse($array)); $t = $z->delete(); // delete the "dummy hole" return array($z, $t); } /** * Convert zipper back into a normal array, optionally filling in * the hole with a value. (Usually you should supply a $t, unless you * are at the end of the array.) */ public function toArray($t = NULL) { $a = $this->front; if ($t !== NULL) $a[] = $t; for ($i = count($this->back)-1; $i >= 0; $i--) { $a[] = $this->back[$i]; } return $a; } /** * Move hole to the next element. * @param $t Element to fill hole with * @return Original contents of new hole. */ public function next($t) { if ($t !== NULL) array_push($this->front, $t); return empty($this->back) ? NULL : array_pop($this->back); } /** * Iterated hole advancement. * @param $t Element to fill hole with * @param $i How many forward to advance hole * @return Original contents of new hole, i away */ public function advance($t, $n) { for ($i = 0; $i < $n; $i++) { $t = $this->next($t); } return $t; } /** * Move hole to the previous element * @param $t Element to fill hole with * @return Original contents of new hole. */ public function prev($t) { if ($t !== NULL) array_push($this->back, $t); return empty($this->front) ? NULL : array_pop($this->front); } /** * Delete contents of current hole, shifting hole to * next element. * @return Original contents of new hole. */ public function delete() { return empty($this->back) ? NULL : array_pop($this->back); } /** * Returns true if we are at the end of the list. * @return bool */ public function done() { return empty($this->back); } /** * Insert element before hole. * @param Element to insert */ public function insertBefore($t) { if ($t !== NULL) array_push($this->front, $t); } /** * Insert element after hole. * @param Element to insert */ public function insertAfter($t) { if ($t !== NULL) array_push($this->back, $t); } /** * Splice in multiple elements at hole. Functional specification * in terms of array_splice: * * $arr1 = $arr; * $old1 = array_splice($arr1, $i, $delete, $replacement); * * list($z, $t) = HTMLPurifier_Zipper::fromArray($arr); * $t = $z->advance($t, $i); * list($old2, $t) = $z->splice($t, $delete, $replacement); * $arr2 = $z->toArray($t); * * assert($old1 === $old2); * assert($arr1 === $arr2); * * NB: the absolute index location after this operation is * *unchanged!* * * @param Current contents of hole. */ public function splice($t, $delete, $replacement) { // delete $old = array(); $r = $t; for ($i = $delete; $i > 0; $i--) { $old[] = $r; $r = $this->delete(); } // insert for ($i = count($replacement)-1; $i >= 0; $i--) { $this->insertAfter($r); $r = $replacement[$i]; } return array($old, $r); } } htmlpurifier/library/HTMLPurifier/Length.php000064400000007464152665541550015157 0ustar00 true, 'ex' => true, 'px' => true, 'in' => true, 'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true, 'ch' => true, 'rem' => true, 'vw' => true, 'vh' => true, 'vmin' => true, 'vmax' => true ); /** * @param string $n Magnitude * @param bool|string $u Unit */ public function __construct($n = '0', $u = false) { $this->n = (string) $n; $this->unit = $u !== false ? (string) $u : false; } /** * @param string $s Unit string, like '2em' or '3.4in' * @return HTMLPurifier_Length * @warning Does not perform validation. */ public static function make($s) { if ($s instanceof HTMLPurifier_Length) { return $s; } $n_length = strspn($s, '1234567890.+-'); $n = substr($s, 0, $n_length); $unit = substr($s, $n_length); if ($unit === '') { $unit = false; } return new HTMLPurifier_Length($n, $unit); } /** * Validates the number and unit. * @return bool */ protected function validate() { // Special case: if ($this->n === '+0' || $this->n === '-0') { $this->n = '0'; } if ($this->n === '0' && $this->unit === false) { return true; } if ($this->unit === false || !ctype_lower($this->unit)) { $this->unit = strtolower($this->unit); } if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) { return false; } // Hack: $def = new HTMLPurifier_AttrDef_CSS_Number(); $result = $def->validate($this->n, false, false); if ($result === false) { return false; } $this->n = $result; return true; } /** * Returns string representation of number. * @return string */ public function toString() { if (!$this->isValid()) { return false; } return $this->n . $this->unit; } /** * Retrieves string numeric magnitude. * @return string */ public function getN() { return $this->n; } /** * Retrieves string unit. * @return string */ public function getUnit() { return $this->unit; } /** * Returns true if this length unit is valid. * @return bool */ public function isValid() { if ($this->isValid === null) { $this->isValid = $this->validate(); } return $this->isValid; } /** * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal. * @param HTMLPurifier_Length $l * @return int * @warning If both values are too large or small, this calculation will * not work properly */ public function compareTo($l) { if ($l === false) { return false; } if ($l->unit !== $this->unit) { $converter = new HTMLPurifier_UnitConverter(); $l = $converter->convert($l, $this->unit); if ($l === false) { return false; } } return $this->n - $l->n; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Queue.php000064400000003017152665541550015010 0ustar00input = $input; $this->output = array(); } /** * Shifts an element off the front of the queue. */ public function shift() { if (empty($this->output)) { $this->output = array_reverse($this->input); $this->input = array(); } if (empty($this->output)) { return NULL; } return array_pop($this->output); } /** * Pushes an element onto the front of the queue. */ public function push($x) { array_push($this->input, $x); } /** * Checks if it's empty. */ public function isEmpty() { return empty($this->input) && empty($this->output); } } htmlpurifier/library/HTMLPurifier/EntityLookup/entities.ser000064400000012277152665541550020230 0ustar00a:253:{s:4:"fnof";s:2:"ƒ";s:5:"Alpha";s:2:"Α";s:4:"Beta";s:2:"Β";s:5:"Gamma";s:2:"Γ";s:5:"Delta";s:2:"Δ";s:7:"Epsilon";s:2:"Ε";s:4:"Zeta";s:2:"Ζ";s:3:"Eta";s:2:"Η";s:5:"Theta";s:2:"Θ";s:4:"Iota";s:2:"Ι";s:5:"Kappa";s:2:"Κ";s:6:"Lambda";s:2:"Λ";s:2:"Mu";s:2:"Μ";s:2:"Nu";s:2:"Ν";s:2:"Xi";s:2:"Ξ";s:7:"Omicron";s:2:"Ο";s:2:"Pi";s:2:"Π";s:3:"Rho";s:2:"Ρ";s:5:"Sigma";s:2:"Σ";s:3:"Tau";s:2:"Τ";s:7:"Upsilon";s:2:"Υ";s:3:"Phi";s:2:"Φ";s:3:"Chi";s:2:"Χ";s:3:"Psi";s:2:"Ψ";s:5:"Omega";s:2:"Ω";s:5:"alpha";s:2:"α";s:4:"beta";s:2:"β";s:5:"gamma";s:2:"γ";s:5:"delta";s:2:"δ";s:7:"epsilon";s:2:"ε";s:4:"zeta";s:2:"ζ";s:3:"eta";s:2:"η";s:5:"theta";s:2:"θ";s:4:"iota";s:2:"ι";s:5:"kappa";s:2:"κ";s:6:"lambda";s:2:"λ";s:2:"mu";s:2:"μ";s:2:"nu";s:2:"ν";s:2:"xi";s:2:"ξ";s:7:"omicron";s:2:"ο";s:2:"pi";s:2:"π";s:3:"rho";s:2:"ρ";s:6:"sigmaf";s:2:"ς";s:5:"sigma";s:2:"σ";s:3:"tau";s:2:"τ";s:7:"upsilon";s:2:"υ";s:3:"phi";s:2:"φ";s:3:"chi";s:2:"χ";s:3:"psi";s:2:"ψ";s:5:"omega";s:2:"ω";s:8:"thetasym";s:2:"ϑ";s:5:"upsih";s:2:"ϒ";s:3:"piv";s:2:"ϖ";s:4:"bull";s:3:"•";s:6:"hellip";s:3:"…";s:5:"prime";s:3:"′";s:5:"Prime";s:3:"″";s:5:"oline";s:3:"‾";s:5:"frasl";s:3:"⁄";s:6:"weierp";s:3:"℘";s:5:"image";s:3:"ℑ";s:4:"real";s:3:"ℜ";s:5:"trade";s:3:"™";s:7:"alefsym";s:3:"ℵ";s:4:"larr";s:3:"←";s:4:"uarr";s:3:"↑";s:4:"rarr";s:3:"→";s:4:"darr";s:3:"↓";s:4:"harr";s:3:"↔";s:5:"crarr";s:3:"↵";s:4:"lArr";s:3:"⇐";s:4:"uArr";s:3:"⇑";s:4:"rArr";s:3:"⇒";s:4:"dArr";s:3:"⇓";s:4:"hArr";s:3:"⇔";s:6:"forall";s:3:"∀";s:4:"part";s:3:"∂";s:5:"exist";s:3:"∃";s:5:"empty";s:3:"∅";s:5:"nabla";s:3:"∇";s:4:"isin";s:3:"∈";s:5:"notin";s:3:"∉";s:2:"ni";s:3:"∋";s:4:"prod";s:3:"∏";s:3:"sum";s:3:"∑";s:5:"minus";s:3:"−";s:6:"lowast";s:3:"∗";s:5:"radic";s:3:"√";s:4:"prop";s:3:"∝";s:5:"infin";s:3:"∞";s:3:"ang";s:3:"∠";s:3:"and";s:3:"∧";s:2:"or";s:3:"∨";s:3:"cap";s:3:"∩";s:3:"cup";s:3:"∪";s:3:"int";s:3:"∫";s:6:"there4";s:3:"∴";s:3:"sim";s:3:"∼";s:4:"cong";s:3:"≅";s:5:"asymp";s:3:"≈";s:2:"ne";s:3:"≠";s:5:"equiv";s:3:"≡";s:2:"le";s:3:"≤";s:2:"ge";s:3:"≥";s:3:"sub";s:3:"⊂";s:3:"sup";s:3:"⊃";s:4:"nsub";s:3:"⊄";s:4:"sube";s:3:"⊆";s:4:"supe";s:3:"⊇";s:5:"oplus";s:3:"⊕";s:6:"otimes";s:3:"⊗";s:4:"perp";s:3:"⊥";s:4:"sdot";s:3:"⋅";s:5:"lceil";s:3:"⌈";s:5:"rceil";s:3:"⌉";s:6:"lfloor";s:3:"⌊";s:6:"rfloor";s:3:"⌋";s:4:"lang";s:3:"〈";s:4:"rang";s:3:"〉";s:3:"loz";s:3:"◊";s:6:"spades";s:3:"♠";s:5:"clubs";s:3:"♣";s:6:"hearts";s:3:"♥";s:5:"diams";s:3:"♦";s:4:"quot";s:1:""";s:3:"amp";s:1:"&";s:2:"lt";s:1:"<";s:2:"gt";s:1:">";s:4:"apos";s:1:"'";s:5:"OElig";s:2:"Œ";s:5:"oelig";s:2:"œ";s:6:"Scaron";s:2:"Š";s:6:"scaron";s:2:"š";s:4:"Yuml";s:2:"Ÿ";s:4:"circ";s:2:"ˆ";s:5:"tilde";s:2:"˜";s:4:"ensp";s:3:" ";s:4:"emsp";s:3:" ";s:6:"thinsp";s:3:" ";s:4:"zwnj";s:3:"‌";s:3:"zwj";s:3:"‍";s:3:"lrm";s:3:"‎";s:3:"rlm";s:3:"‏";s:5:"ndash";s:3:"–";s:5:"mdash";s:3:"—";s:5:"lsquo";s:3:"‘";s:5:"rsquo";s:3:"’";s:5:"sbquo";s:3:"‚";s:5:"ldquo";s:3:"“";s:5:"rdquo";s:3:"”";s:5:"bdquo";s:3:"„";s:6:"dagger";s:3:"†";s:6:"Dagger";s:3:"‡";s:6:"permil";s:3:"‰";s:6:"lsaquo";s:3:"‹";s:6:"rsaquo";s:3:"›";s:4:"euro";s:3:"€";s:4:"nbsp";s:2:" ";s:5:"iexcl";s:2:"¡";s:4:"cent";s:2:"¢";s:5:"pound";s:2:"£";s:6:"curren";s:2:"¤";s:3:"yen";s:2:"¥";s:6:"brvbar";s:2:"¦";s:4:"sect";s:2:"§";s:3:"uml";s:2:"¨";s:4:"copy";s:2:"©";s:4:"ordf";s:2:"ª";s:5:"laquo";s:2:"«";s:3:"not";s:2:"¬";s:3:"shy";s:2:"­";s:3:"reg";s:2:"®";s:4:"macr";s:2:"¯";s:3:"deg";s:2:"°";s:6:"plusmn";s:2:"±";s:4:"sup2";s:2:"²";s:4:"sup3";s:2:"³";s:5:"acute";s:2:"´";s:5:"micro";s:2:"µ";s:4:"para";s:2:"¶";s:6:"middot";s:2:"·";s:5:"cedil";s:2:"¸";s:4:"sup1";s:2:"¹";s:4:"ordm";s:2:"º";s:5:"raquo";s:2:"»";s:6:"frac14";s:2:"¼";s:6:"frac12";s:2:"½";s:6:"frac34";s:2:"¾";s:6:"iquest";s:2:"¿";s:6:"Agrave";s:2:"À";s:6:"Aacute";s:2:"Á";s:5:"Acirc";s:2:"Â";s:6:"Atilde";s:2:"Ã";s:4:"Auml";s:2:"Ä";s:5:"Aring";s:2:"Å";s:5:"AElig";s:2:"Æ";s:6:"Ccedil";s:2:"Ç";s:6:"Egrave";s:2:"È";s:6:"Eacute";s:2:"É";s:5:"Ecirc";s:2:"Ê";s:4:"Euml";s:2:"Ë";s:6:"Igrave";s:2:"Ì";s:6:"Iacute";s:2:"Í";s:5:"Icirc";s:2:"Î";s:4:"Iuml";s:2:"Ï";s:3:"ETH";s:2:"Ð";s:6:"Ntilde";s:2:"Ñ";s:6:"Ograve";s:2:"Ò";s:6:"Oacute";s:2:"Ó";s:5:"Ocirc";s:2:"Ô";s:6:"Otilde";s:2:"Õ";s:4:"Ouml";s:2:"Ö";s:5:"times";s:2:"×";s:6:"Oslash";s:2:"Ø";s:6:"Ugrave";s:2:"Ù";s:6:"Uacute";s:2:"Ú";s:5:"Ucirc";s:2:"Û";s:4:"Uuml";s:2:"Ü";s:6:"Yacute";s:2:"Ý";s:5:"THORN";s:2:"Þ";s:5:"szlig";s:2:"ß";s:6:"agrave";s:2:"à";s:6:"aacute";s:2:"á";s:5:"acirc";s:2:"â";s:6:"atilde";s:2:"ã";s:4:"auml";s:2:"ä";s:5:"aring";s:2:"å";s:5:"aelig";s:2:"æ";s:6:"ccedil";s:2:"ç";s:6:"egrave";s:2:"è";s:6:"eacute";s:2:"é";s:5:"ecirc";s:2:"ê";s:4:"euml";s:2:"ë";s:6:"igrave";s:2:"ì";s:6:"iacute";s:2:"í";s:5:"icirc";s:2:"î";s:4:"iuml";s:2:"ï";s:3:"eth";s:2:"ð";s:6:"ntilde";s:2:"ñ";s:6:"ograve";s:2:"ò";s:6:"oacute";s:2:"ó";s:5:"ocirc";s:2:"ô";s:6:"otilde";s:2:"õ";s:4:"ouml";s:2:"ö";s:6:"divide";s:2:"÷";s:6:"oslash";s:2:"ø";s:6:"ugrave";s:2:"ù";s:6:"uacute";s:2:"ú";s:5:"ucirc";s:2:"û";s:4:"uuml";s:2:"ü";s:6:"yacute";s:2:"ý";s:5:"thorn";s:2:"þ";s:4:"yuml";s:2:"ÿ";}htmlpurifier/library/HTMLPurifier/HTMLModule.php000064400000023723152665541550015644 0ustar00info, since the object's data is only info, * with extra behavior associated with it. * @type array */ public $attr_collections = array(); /** * Associative array of deprecated tag name to HTMLPurifier_TagTransform. * @type array */ public $info_tag_transform = array(); /** * List of HTMLPurifier_AttrTransform to be performed before validation. * @type array */ public $info_attr_transform_pre = array(); /** * List of HTMLPurifier_AttrTransform to be performed after validation. * @type array */ public $info_attr_transform_post = array(); /** * List of HTMLPurifier_Injector to be performed during well-formedness fixing. * An injector will only be invoked if all of it's pre-requisites are met; * if an injector fails setup, there will be no error; it will simply be * silently disabled. * @type array */ public $info_injector = array(); /** * Boolean flag that indicates whether or not getChildDef is implemented. * For optimization reasons: may save a call to a function. Be sure * to set it if you do implement getChildDef(), otherwise it will have * no effect! * @type bool */ public $defines_child_def = false; /** * Boolean flag whether or not this module is safe. If it is not safe, all * of its members are unsafe. Modules are safe by default (this might be * slightly dangerous, but it doesn't make much sense to force HTML Purifier, * which is based off of safe HTML, to explicitly say, "This is safe," even * though there are modules which are "unsafe") * * @type bool * @note Previously, safety could be applied at an element level granularity. * We've removed this ability, so in order to add "unsafe" elements * or attributes, a dedicated module with this property set to false * must be used. */ public $safe = true; /** * Retrieves a proper HTMLPurifier_ChildDef subclass based on * content_model and content_model_type member variables of * the HTMLPurifier_ElementDef class. There is a similar function * in HTMLPurifier_HTMLDefinition. * @param HTMLPurifier_ElementDef $def * @return HTMLPurifier_ChildDef subclass */ public function getChildDef($def) { return false; } // -- Convenience ----------------------------------------------------- /** * Convenience function that sets up a new element * @param string $element Name of element to add * @param string|bool $type What content set should element be registered to? * Set as false to skip this step. * @param string|HTMLPurifier_ChildDef $contents Allowed children in form of: * "$content_model_type: $content_model" * @param array|string $attr_includes What attribute collections to register to * element? * @param array $attr What unique attributes does the element define? * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters. * @return HTMLPurifier_ElementDef Created element definition object, so you * can set advanced parameters */ public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) { $this->elements[] = $element; // parse content_model list($content_model_type, $content_model) = $this->parseContents($contents); // merge in attribute inclusions $this->mergeInAttrIncludes($attr, $attr_includes); // add element to content sets if ($type) { $this->addElementToContentSet($element, $type); } // create element $this->info[$element] = HTMLPurifier_ElementDef::create( $content_model, $content_model_type, $attr ); // literal object $contents means direct child manipulation if (!is_string($contents)) { $this->info[$element]->child = $contents; } return $this->info[$element]; } /** * Convenience function that creates a totally blank, non-standalone * element. * @param string $element Name of element to create * @return HTMLPurifier_ElementDef Created element */ public function addBlankElement($element) { if (!isset($this->info[$element])) { $this->elements[] = $element; $this->info[$element] = new HTMLPurifier_ElementDef(); $this->info[$element]->standalone = false; } else { trigger_error("Definition for $element already exists in module, cannot redefine"); } return $this->info[$element]; } /** * Convenience function that registers an element to a content set * @param string $element Element to register * @param string $type Name content set (warning: case sensitive, usually upper-case * first letter) */ public function addElementToContentSet($element, $type) { if (!isset($this->content_sets[$type])) { $this->content_sets[$type] = ''; } else { $this->content_sets[$type] .= ' | '; } $this->content_sets[$type] .= $element; } /** * Convenience function that transforms single-string contents * into separate content model and content model type * @param string $contents Allowed children in form of: * "$content_model_type: $content_model" * @return array * @note If contents is an object, an array of two nulls will be * returned, and the callee needs to take the original $contents * and use it directly. */ public function parseContents($contents) { if (!is_string($contents)) { return array(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 array $attr Reference to attr array to modify * @param array $attr_includes Array of includes / string include to merge in */ public function mergeInAttrIncludes(&$attr, $attr_includes) { if (!is_array($attr_includes)) { if (empty($attr_includes)) { $attr_includes = array(); } else { $attr_includes = array($attr_includes); } } $attr[0] = $attr_includes; } /** * Convenience function that generates a lookup table with boolean * true as value. * @param string $list List of values to turn into a lookup * @note You can also pass an arbitrary number of arguments in * place of the regular argument * @return array array equivalent of list */ public function makeLookup($list) { $args = func_get_args(); if (is_string($list)) { $list = $args; } $ret = array(); foreach ($list as $value) { if (is_null($value)) { continue; } $ret[$value] = true; } return $ret; } /** * Lazy load construction of the module after determining whether * or not it's needed, and also when a finalized configuration object * is available. * @param HTMLPurifier_Config $config */ public function setup($config) { } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/AttrValidator.php000064400000014655152665541550016516 0ustar00getHTMLDefinition(); $e =& $context->get('ErrorCollector', true); // initialize IDAccumulator if necessary $ok =& $context->get('IDAccumulator', true); if (!$ok) { $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); $context->register('IDAccumulator', $id_accumulator); } // initialize CurrentToken if necessary $current_token =& $context->get('CurrentToken', true); if (!$current_token) { $context->register('CurrentToken', $token); } if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty ) { return; } // create alias to global definition array, see also $defs // DEFINITION CALL $d_defs = $definition->info_global_attr; // don't update token until the very end, to ensure an atomic update $attr = $token->attr; // do global transformations (pre) // nothing currently utilizes this foreach ($definition->info_attr_transform_pre as $transform) { $attr = $transform->transform($o = $attr, $config, $context); if ($e) { if ($attr != $o) { $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); } } } // do local transformations only applicable to this element (pre) // ex.

to

foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { $attr = $transform->transform($o = $attr, $config, $context); if ($e) { if ($attr != $o) { $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); } } } // create alias to this element's attribute definition array, see // also $d_defs (global attribute definition array) // DEFINITION CALL $defs = $definition->info[$token->name]->attr; $attr_key = false; $context->register('CurrentAttr', $attr_key); // iterate through all the attribute keypairs // Watch out for name collisions: $key has previously been used foreach ($attr as $attr_key => $value) { // call the definition if (isset($defs[$attr_key])) { // there is a local definition defined if ($defs[$attr_key] === false) { // We've explicitly been told not to allow this element. // This is usually when there's a global definition // that must be overridden. // Theoretically speaking, we could have a // AttrDef_DenyAll, but this is faster! $result = false; } else { // validate according to the element's definition $result = $defs[$attr_key]->validate( $value, $config, $context ); } } elseif (isset($d_defs[$attr_key])) { // there is a global definition defined, validate according // to the global definition $result = $d_defs[$attr_key]->validate( $value, $config, $context ); } else { // system never heard of the attribute? DELETE! $result = false; } // put the results into effect if ($result === false || $result === null) { // this is a generic error message that should replaced // with more specific ones when possible if ($e) { $e->send(E_ERROR, 'AttrValidator: Attribute removed'); } // remove the attribute unset($attr[$attr_key]); } elseif (is_string($result)) { // generally, if a substitution is happening, there // was some sort of implicit correction going on. We'll // delegate it to the attribute classes to say exactly what. // simple substitution $attr[$attr_key] = $result; } else { // nothing happens } // we'd also want slightly more complicated substitution // involving an array as the return value, // although we're not sure how colliding attributes would // resolve (certain ones would be completely overridden, // others would prepend themselves). } $context->destroy('CurrentAttr'); // post transforms // global (error reporting untested) foreach ($definition->info_attr_transform_post as $transform) { $attr = $transform->transform($o = $attr, $config, $context); if ($e) { if ($attr != $o) { $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); } } } // local (error reporting untested) foreach ($definition->info[$token->name]->attr_transform_post as $transform) { $attr = $transform->transform($o = $attr, $config, $context); if ($e) { if ($attr != $o) { $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); } } } $token->attr = $attr; // destroy CurrentToken if we made it ourselves if (!$current_token) { $context->destroy('CurrentToken'); } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Node/Comment.php000064400000001325152665541550016213 0ustar00data = $data; $this->line = $line; $this->col = $col; } public function toTokenPair() { return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); } } htmlpurifier/library/HTMLPurifier/Node/Text.php000064400000002544152665541550015541 0ustar00data = $data; $this->is_whitespace = $is_whitespace; $this->line = $line; $this->col = $col; } public function toTokenPair() { return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Node/Element.php000064400000003275152665541550016210 0ustar00 form or the form, i.e. * is it a pair of start/end tokens or an empty token. * @bool */ public $empty = false; public $endCol = null, $endLine = null, $endArmor = array(); public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { $this->name = $name; $this->attr = $attr; $this->line = $line; $this->col = $col; $this->armor = $armor; } public function toTokenPair() { // XXX inefficiency here, normalization is not necessary if ($this->empty) { return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); } else { $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); //$end->start = $start; return array($start, $end); } } } htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php000064400000003423152665541550020615 0ustar00 array('href')); /** * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return string */ public function prepare($config, $context) { $this->docURL = $config->get('AutoFormat.PurifierLinkify.DocURL'); return parent::prepare($config, $context); } /** * @param HTMLPurifier_Token $token */ public function handleText(&$token) { if (!$this->allowsElement('a')) { return; } if (strpos($token->data, '%') === false) { return; } $bits = preg_split('#%([a-z0-9]+\.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE); $token = array(); // $i = index // $c = count // $l = is link for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) { if (!$l) { if ($bits[$i] === '') { continue; } $token[] = new HTMLPurifier_Token_Text($bits[$i]); } else { $token[] = new HTMLPurifier_Token_Start( 'a', array('href' => str_replace('%s', $bits[$i], $this->docURL)) ); $token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]); $token[] = new HTMLPurifier_Token_End('a'); } } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Injector/Linkify.php000064400000004050152665541550017104 0ustar00 array('href')); /** * @param HTMLPurifier_Token $token */ public function handleText(&$token) { if (!$this->allowsElement('a')) { return; } if (strpos($token->data, '://') === false) { // our really quick heuristic failed, abort // this may not work so well if we want to match things like // "google.com", but then again, most people don't return; } // there is/are URL(s). Let's split the string. // We use this regex: // https://gist.github.com/gruber/249502 // but with @cscott's backtracking fix and also // the Unicode characters un-Unicodified. $bits = preg_split( '/\\b((?:[a-z][\\w\\-]+:(?:\\/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}\\/)(?:[^\\s()<>]|\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\))+(?:\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:\'".,<>?\x{00ab}\x{00bb}\x{201c}\x{201d}\x{2018}\x{2019}]))/iu', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE); if ($bits === false) { return; } $token = array(); // $i = index // $c = count // $l = is link for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) { if (!$l) { if ($bits[$i] === '') { continue; } $token[] = new HTMLPurifier_Token_Text($bits[$i]); } else { $token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i])); $token[] = new HTMLPurifier_Token_Text($bits[$i]); $token[] = new HTMLPurifier_Token_End('a'); } } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php000064400000001533152665541550020305 0ustar00start->attr['href'])) { $url = $token->start->attr['href']; unset($token->start->attr['href']); $token = array($token, new HTMLPurifier_Token_Text(" ($url)")); } else { // nothing to display } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php000064400000032744152665541550020250 0ustar00armor['MakeWellFormed_TagClosedError'] = true; return $par; } /** * @param HTMLPurifier_Token_Text $token */ public function handleText(&$token) { $text = $token->data; // Does the current parent allow

tags? if ($this->allowsElement('p')) { if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) { // Note that we have differing behavior when dealing with text // in the anonymous root node, or a node inside the document. // If the text as a double-newline, the treatment is the same; // if it doesn't, see the next if-block if you're in the document. $i = $nesting = null; if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) { // State 1.1: ... ^ (whitespace, then document end) // ---- // This is a degenerate case } else { if (!$token->is_whitespace || $this->_isInline($current)) { // State 1.2: PAR1 // ---- // State 1.3: PAR1\n\nPAR2 // ------------ // State 1.4:

PAR1\n\nPAR2 (see State 2) // ------------ $token = array($this->_pStart()); $this->_splitText($text, $token); } else { // State 1.5: \n
// -- } } } else { // State 2:
PAR1... (similar to 1.4) // ---- // We're in an element that allows paragraph tags, but we're not // sure if we're going to need them. if ($this->_pLookAhead()) { // State 2.1:
PAR1PAR1\n\nPAR2 // ---- // Note: This will always be the first child, since any // previous inline element would have triggered this very // same routine, and found the double newline. One possible // exception would be a comment. $token = array($this->_pStart(), $token); } else { // State 2.2.1:
PAR1
// ---- // State 2.2.2:
PAR1PAR1
// ---- } } // Is the current parent a

tag? } elseif (!empty($this->currentNesting) && $this->currentNesting[count($this->currentNesting) - 1]->name == 'p') { // State 3.1: ...

PAR1 // ---- // State 3.2: ...

PAR1\n\nPAR2 // ------------ $token = array(); $this->_splitText($text, $token); // Abort! } else { // State 4.1: ...PAR1 // ---- // State 4.2: ...PAR1\n\nPAR2 // ------------ } } /** * @param HTMLPurifier_Token $token */ public function handleElement(&$token) { // We don't have to check if we're already in a

tag for block // tokens, because the tag would have been autoclosed by MakeWellFormed. if ($this->allowsElement('p')) { if (!empty($this->currentNesting)) { if ($this->_isInline($token)) { // State 1:

... // --- // Check if this token is adjacent to the parent token // (seek backwards until token isn't whitespace) $i = null; $this->backward($i, $prev); if (!$prev instanceof HTMLPurifier_Token_Start) { // Token wasn't adjacent if ($prev instanceof HTMLPurifier_Token_Text && substr($prev->data, -2) === "\n\n" ) { // State 1.1.4:

PAR1

\n\n // --- // Quite frankly, this should be handled by splitText $token = array($this->_pStart(), $token); } else { // State 1.1.1:

PAR1

// --- // State 1.1.2:

// --- // State 1.1.3:
PAR // --- } } else { // State 1.2.1:
// --- // Lookahead to see if

is needed. if ($this->_pLookAhead()) { // State 1.3.1:

PAR1\n\nPAR2 // --- $token = array($this->_pStart(), $token); } else { // State 1.3.2:
PAR1
// --- // State 1.3.3:
PAR1
\n\n
// --- } } } else { // State 2.3: ...
// ----- } } else { if ($this->_isInline($token)) { // State 3.1: // --- // This is where the {p} tag is inserted, not reflected in // inputTokens yet, however. $token = array($this->_pStart(), $token); } else { // State 3.2:
// ----- } $i = null; if ($this->backward($i, $prev)) { if (!$prev instanceof HTMLPurifier_Token_Text) { // State 3.1.1: ...

{p} // --- // State 3.2.1: ...

// ----- if (!is_array($token)) { $token = array($token); } array_unshift($token, new HTMLPurifier_Token_Text("\n\n")); } else { // State 3.1.2: ...

\n\n{p} // --- // State 3.2.2: ...

\n\n
// ----- // Note: PAR cannot occur because PAR would have been // wrapped in

tags. } } } } else { // State 2.2:

  • // ---- // State 2.4:

    // --- } } /** * Splits up a text in paragraph tokens and appends them * to the result stream that will replace the original * @param string $data String text data that will be processed * into paragraphs * @param HTMLPurifier_Token[] $result Reference to array of tokens that the * tags will be appended onto */ private function _splitText($data, &$result) { $raw_paragraphs = explode("\n\n", $data); $paragraphs = array(); // without empty paragraphs $needs_start = false; $needs_end = false; $c = count($raw_paragraphs); if ($c == 1) { // There were no double-newlines, abort quickly. In theory this // should never happen. $result[] = new HTMLPurifier_Token_Text($data); return; } for ($i = 0; $i < $c; $i++) { $par = $raw_paragraphs[$i]; if (trim($par) !== '') { $paragraphs[] = $par; } else { if ($i == 0) { // Double newline at the front if (empty($result)) { // The empty result indicates that the AutoParagraph // injector did not add any start paragraph tokens. // This means that we have been in a paragraph for // a while, and the newline means we should start a new one. $result[] = new HTMLPurifier_Token_End('p'); $result[] = new HTMLPurifier_Token_Text("\n\n"); // However, the start token should only be added if // there is more processing to be done (i.e. there are // real paragraphs in here). If there are none, the // next start paragraph tag will be handled by the // next call to the injector $needs_start = true; } else { // We just started a new paragraph! // Reinstate a double-newline for presentation's sake, since // it was in the source code. array_unshift($result, new HTMLPurifier_Token_Text("\n\n")); } } elseif ($i + 1 == $c) { // Double newline at the end // There should be a trailing

    when we're finally done. $needs_end = true; } } } // Check if this was just a giant blob of whitespace. Move this earlier, // perhaps? if (empty($paragraphs)) { return; } // Add the start tag indicated by \n\n at the beginning of $data if ($needs_start) { $result[] = $this->_pStart(); } // Append the paragraphs onto the result foreach ($paragraphs as $par) { $result[] = new HTMLPurifier_Token_Text($par); $result[] = new HTMLPurifier_Token_End('p'); $result[] = new HTMLPurifier_Token_Text("\n\n"); $result[] = $this->_pStart(); } // Remove trailing start token; Injector will handle this later if // it was indeed needed. This prevents from needing to do a lookahead, // at the cost of a lookbehind later. array_pop($result); // If there is no need for an end tag, remove all of it and let // MakeWellFormed close it later. if (!$needs_end) { array_pop($result); // removes \n\n array_pop($result); // removes

    } } /** * Returns true if passed token is inline (and, ergo, allowed in * paragraph tags) * @param HTMLPurifier_Token $token * @return bool */ private function _isInline($token) { return isset($this->htmlDefinition->info['p']->child->elements[$token->name]); } /** * Looks ahead in the token list and determines whether or not we need * to insert a

    tag. * @return bool */ private function _pLookAhead() { if ($this->currentToken instanceof HTMLPurifier_Token_Start) { $nesting = 1; } else { $nesting = 0; } $ok = false; $i = null; while ($this->forwardUntilEndToken($i, $current, $nesting)) { $result = $this->_checkNeedsP($current); if ($result !== null) { $ok = $result; break; } } return $ok; } /** * Determines if a particular token requires an earlier inline token * to get a paragraph. This should be used with _forwardUntilEndToken * @param HTMLPurifier_Token $current * @return bool */ private function _checkNeedsP($current) { if ($current instanceof HTMLPurifier_Token_Start) { if (!$this->_isInline($current)) { //

    PAR1
    // ---- // Terminate early, since we hit a block element return false; } } elseif ($current instanceof HTMLPurifier_Token_Text) { if (strpos($current->data, "\n\n") !== false) { //
    PAR1PAR1\n\nPAR2 // ---- return true; } else { //
    PAR1PAR1... // ---- } } return null; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php000064400000006664152665541550017770 0ustar00config = $config; $this->context = $context; $this->removeNbsp = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp'); $this->removeNbspExceptions = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions'); $this->exclude = $config->get('AutoFormat.RemoveEmpty.Predicate'); foreach ($this->exclude as $key => $attrs) { if (!is_array($attrs)) { // HACK, see HTMLPurifier/Printer/ConfigForm.php $this->exclude[$key] = explode(';', $attrs); } } $this->attrValidator = new HTMLPurifier_AttrValidator(); } /** * @param HTMLPurifier_Token $token */ public function handleElement(&$token) { if (!$token instanceof HTMLPurifier_Token_Start) { return; } $next = false; $deleted = 1; // the current tag for ($i = count($this->inputZipper->back) - 1; $i >= 0; $i--, $deleted++) { $next = $this->inputZipper->back[$i]; if ($next instanceof HTMLPurifier_Token_Text) { if ($next->is_whitespace) { continue; } if ($this->removeNbsp && !isset($this->removeNbspExceptions[$token->name])) { $plain = str_replace("\xC2\xA0", "", $next->data); $isWsOrNbsp = $plain === '' || ctype_space($plain); if ($isWsOrNbsp) { continue; } } } break; } if (!$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) { $this->attrValidator->validateToken($token, $this->config, $this->context); $token->armor['ValidateAttributes'] = true; if (isset($this->exclude[$token->name])) { $r = true; foreach ($this->exclude[$token->name] as $elem) { if (!isset($token->attr[$elem])) $r = false; } if ($r) return; } if (isset($token->attr['id']) || isset($token->attr['name'])) { return; } $token = $deleted + 1; for ($b = 0, $c = count($this->inputZipper->front); $b < $c; $b++) { $prev = $this->inputZipper->front[$b]; if ($prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) { continue; } break; } // This is safe because we removed the token that triggered this. $this->rewindOffset($b+$deleted); return; } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php000064400000004350152665541550023377 0ustar00markForDeletion = new SplObjectStorage(); } public function prepare($config, $context) { $this->attrValidator = new HTMLPurifier_AttrValidator(); $this->config = $config; $this->context = $context; return parent::prepare($config, $context); } /** * @param HTMLPurifier_Token $token */ public function handleElement(&$token) { if ($token->name !== 'span' || !$token instanceof HTMLPurifier_Token_Start) { return; } // We need to validate the attributes now since this doesn't normally // happen until after MakeWellFormed. If all the attributes are removed // the span needs to be removed too. $this->attrValidator->validateToken($token, $this->config, $this->context); $token->armor['ValidateAttributes'] = true; if (!empty($token->attr)) { return; } $nesting = 0; while ($this->forwardUntilEndToken($i, $current, $nesting)) { } if ($current instanceof HTMLPurifier_Token_End && $current->name === 'span') { // Mark closing span tag for deletion $this->markForDeletion->attach($current); // Delete open span tag $token = false; } } /** * @param HTMLPurifier_Token $token */ public function handleEnd(&$token) { if ($this->markForDeletion->contains($token)) { $this->markForDeletion->detach($token); $token = false; } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php000064400000007557152665541550017523 0ustar00 'never', 'allowNetworking' => 'internal', ); /** * These are all lower-case keys. * @type array */ protected $allowedParam = array( 'wmode' => true, 'movie' => true, 'flashvars' => true, 'src' => true, 'allowfullscreen' => true, // if omitted, assume to be 'false' ); /** * @param HTMLPurifier_Config $config * @param HTMLPurifier_Context $context * @return void */ public function prepare($config, $context) { parent::prepare($config, $context); } /** * @param HTMLPurifier_Token $token */ public function handleElement(&$token) { if ($token->name == 'object') { $this->objectStack[] = $token; $this->paramStack[] = array(); $new = array($token); foreach ($this->addParam as $name => $value) { $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value)); } $token = $new; } elseif ($token->name == 'param') { $nest = count($this->currentNesting) - 1; if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') { $i = count($this->objectStack) - 1; if (!isset($token->attr['name'])) { $token = false; return; } $n = $token->attr['name']; // We need this fix because YouTube doesn't supply a data // attribute, which we need if a type is specified. This is // *very* Flash specific. if (!isset($this->objectStack[$i]->attr['data']) && ($token->attr['name'] == 'movie' || $token->attr['name'] == 'src') ) { $this->objectStack[$i]->attr['data'] = $token->attr['value']; } // Check if the parameter is the correct value but has not // already been added if (!isset($this->paramStack[$i][$n]) && isset($this->addParam[$n]) && $token->attr['name'] === $this->addParam[$n]) { // keep token, and add to param stack $this->paramStack[$i][$n] = true; } elseif (isset($this->allowedParam[strtolower($n)])) { // keep token, don't do anything to it // (could possibly check for duplicates here) // Note: In principle, parameters should be case sensitive. // But it seems they are not really; so accept any case. } else { $token = false; } } else { // not directly inside an object, DENY! $token = false; } } } public function handleEnd(&$token) { // This is the WRONG way of handling the object and param stacks; // we should be inserting them directly on the relevant object tokens // so that the global stack handling handles it. if ($token->name == 'object') { array_pop($this->objectStack); array_pop($this->paramStack); } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php000064400000001127152665541550017064 0ustar00addBlankElement($name); $e->attr = array( 'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget() ); } } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php000064400000000542152665541550022171 0ustar00 array( 'lang' => 'LanguageCode', ) ); } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php000064400000001744152665541560017640 0ustar00addElement( 'a', 'Inline', 'Inline', 'Common', array( // 'accesskey' => 'Character', // 'charset' => 'Charset', 'href' => 'URI', // 'hreflang' => 'LanguageCode', 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'), 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'), // 'tabindex' => 'Number', // 'type' => 'ContentType', ) ); $a->formatting = true; $a->excludes = array('a' => true); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php000064400000004441152665541560017603 0ustar00 'script | noscript', 'Inline' => 'script | noscript'); /** * @type bool */ public $safe = false; /** * @param HTMLPurifier_Config $config */ public function setup($config) { // TODO: create custom child-definition for noscript that // auto-wraps stray #PCDATA in a similar manner to // blockquote's custom definition (we would use it but // blockquote's contents are optional while noscript's contents // are required) // TODO: convert this to new syntax, main problem is getting // both content sets working // In theory, this could be safe, but I don't see any reason to // allow it. $this->info['noscript'] = new HTMLPurifier_ElementDef(); $this->info['noscript']->attr = array(0 => array('Common')); $this->info['noscript']->content_model = 'Heading | List | Block'; $this->info['noscript']->content_model_type = 'required'; $this->info['script'] = new HTMLPurifier_ElementDef(); $this->info['script']->attr = array( 'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')), 'src' => new HTMLPurifier_AttrDef_URI(true), 'type' => new HTMLPurifier_AttrDef_Enum(array('text/javascript')) ); $this->info['script']->content_model = '#PCDATA'; $this->info['script']->content_model_type = 'optional'; $this->info['script']->attr_transform_pre[] = $this->info['script']->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired(); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php000064400000013337152665541560017051 0ustar00addElement( 'basefont', 'Inline', 'Empty', null, array( 'color' => 'Color', 'face' => 'Text', // extremely broad, we should 'size' => 'Text', // tighten it 'id' => 'ID' ) ); $this->addElement('center', 'Block', 'Flow', 'Common'); $this->addElement( 'dir', 'Block', 'Required: li', 'Common', array( 'compact' => 'Bool#compact' ) ); $this->addElement( 'font', 'Inline', 'Inline', array('Core', 'I18N'), array( 'color' => 'Color', 'face' => 'Text', // extremely broad, we should 'size' => 'Text', // tighten it ) ); $this->addElement( 'menu', 'Block', 'Required: li', 'Common', array( 'compact' => 'Bool#compact' ) ); $s = $this->addElement('s', 'Inline', 'Inline', 'Common'); $s->formatting = true; $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common'); $strike->formatting = true; $u = $this->addElement('u', 'Inline', 'Inline', 'Common'); $u->formatting = true; // setup modifications to old elements $align = 'Enum#left,right,center,justify'; $address = $this->addBlankElement('address'); $address->content_model = 'Inline | #PCDATA | p'; $address->content_model_type = 'optional'; $address->child = false; $blockquote = $this->addBlankElement('blockquote'); $blockquote->content_model = 'Flow | #PCDATA'; $blockquote->content_model_type = 'optional'; $blockquote->child = false; $br = $this->addBlankElement('br'); $br->attr['clear'] = 'Enum#left,all,right,none'; $caption = $this->addBlankElement('caption'); $caption->attr['align'] = 'Enum#top,bottom,left,right'; $div = $this->addBlankElement('div'); $div->attr['align'] = $align; $dl = $this->addBlankElement('dl'); $dl->attr['compact'] = 'Bool#compact'; for ($i = 1; $i <= 6; $i++) { $h = $this->addBlankElement("h$i"); $h->attr['align'] = $align; } $hr = $this->addBlankElement('hr'); $hr->attr['align'] = $align; $hr->attr['noshade'] = 'Bool#noshade'; $hr->attr['size'] = 'Pixels'; $hr->attr['width'] = 'Length'; $img = $this->addBlankElement('img'); $img->attr['align'] = 'IAlign'; $img->attr['border'] = 'Pixels'; $img->attr['hspace'] = 'Pixels'; $img->attr['vspace'] = 'Pixels'; // figure out this integer business $li = $this->addBlankElement('li'); $li->attr['value'] = new HTMLPurifier_AttrDef_Integer(); $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle'; $ol = $this->addBlankElement('ol'); $ol->attr['compact'] = 'Bool#compact'; $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer(); $ol->attr['type'] = 'Enum#s:1,i,I,a,A'; $p = $this->addBlankElement('p'); $p->attr['align'] = $align; $pre = $this->addBlankElement('pre'); $pre->attr['width'] = 'Number'; // script omitted $table = $this->addBlankElement('table'); $table->attr['align'] = 'Enum#left,center,right'; $table->attr['bgcolor'] = 'Color'; $tr = $this->addBlankElement('tr'); $tr->attr['bgcolor'] = 'Color'; $th = $this->addBlankElement('th'); $th->attr['bgcolor'] = 'Color'; $th->attr['height'] = 'Length'; $th->attr['nowrap'] = 'Bool#nowrap'; $th->attr['width'] = 'Length'; $td = $this->addBlankElement('td'); $td->attr['bgcolor'] = 'Color'; $td->attr['height'] = 'Length'; $td->attr['nowrap'] = 'Bool#nowrap'; $td->attr['width'] = 'Length'; $ul = $this->addBlankElement('ul'); $ul->attr['compact'] = 'Bool#compact'; $ul->attr['type'] = 'Enum#square,disc,circle'; // "safe" modifications to "unsafe" elements // WARNING: If you want to add support for an unsafe, legacy // attribute, make a new TrustedLegacy module with the trusted // bit set appropriately $form = $this->addBlankElement('form'); $form->content_model = 'Flow | #PCDATA'; $form->content_model_type = 'optional'; $form->attr['target'] = 'FrameTarget'; $input = $this->addBlankElement('input'); $input->attr['align'] = 'IAlign'; $legend = $this->addBlankElement('legend'); $legend->attr['align'] = 'LAlign'; } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php000064400000002402152665541560017037 0ustar00get('HTML.SafeIframe')) { $this->safe = true; } $attrs = 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', ); if ($config->get('HTML.Trusted')) { $attrs['allowfullscreen'] = 'Bool#allowfullscreen'; } $this->addElement( 'iframe', 'Inline', 'Flow', 'Common', $attrs ); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php000064400000002632152665541560016526 0ustar00 'URI', // 'datetime' => 'Datetime', // not implemented ); $this->addElement('del', 'Inline', $contents, 'Common', $attr); $this->addElement('ins', 'Inline', $contents, 'Common', $attr); } // HTML 4.01 specifies that ins/del must not contain block // elements when used in an inline context, chameleon is // a complicated workaround to achieve this effect // Inline context ! Block context (exclamation mark is // separator, see getChildDef for parsing) /** * @type bool */ public $defines_child_def = true; /** * @param HTMLPurifier_ElementDef $def * @return HTMLPurifier_ChildDef_Chameleon */ public function getChildDef($def) { if ($def->content_model_type != 'chameleon') { return false; } $value = explode('!', $def->content_model); return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php000064400000001004152665541560020565 0ustar00addBlankElement('a'); $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoopener(); } } htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php000064400000001743152665541560020163 0ustar00addElement( 'marquee', 'Inline', 'Flow', 'Common', array( 'direction' => 'Enum#left,right,up,down', 'behavior' => 'Enum#alternate', 'width' => 'Length', 'height' => 'Length', 'scrolldelay' => 'Number', 'scrollamount' => 'Number', 'loop' => 'Number', 'bgcolor' => 'Color', 'hspace' => 'Pixels', 'vspace' => 'Pixels', ) ); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php000064400000004460152665541560017054 0ustar00addElement('caption', false, 'Inline', 'Common'); $this->addElement( 'table', 'Block', new HTMLPurifier_ChildDef_Table(), 'Common', array( 'border' => 'Pixels', 'cellpadding' => 'Length', 'cellspacing' => 'Length', 'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border', 'rules' => 'Enum#none,groups,rows,cols,all', 'summary' => 'Text', 'width' => 'Length' ) ); // common attributes $cell_align = array( 'align' => 'Enum#left,center,right,justify,char', 'charoff' => 'Length', 'valign' => 'Enum#top,middle,bottom,baseline', ); $cell_t = array_merge( array( 'abbr' => 'Text', 'colspan' => 'Number', 'rowspan' => 'Number', // Apparently, as of HTML5 this attribute only applies // to 'th' elements. 'scope' => 'Enum#row,col,rowgroup,colgroup', ), $cell_align ); $this->addElement('td', false, 'Flow', 'Common', $cell_t); $this->addElement('th', false, 'Flow', 'Common', $cell_t); $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align); $cell_col = array_merge( array( 'span' => 'Number', 'width' => 'MultiLength', ), $cell_align ); $this->addElement('col', false, 'Empty', 'Common', $cell_col); $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col); $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align); $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align); $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php000064400000001012152665541560020026 0ustar00addBlankElement('a'); $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetBlank(); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php000064400000002763152665541560017054 0ustar00 to cater to legacy browsers: this * module does not allow this sort of behavior */ class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule { /** * @type string */ public $name = 'Object'; /** * @type bool */ public $safe = false; /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->addElement( 'object', 'Inline', 'Optional: #PCDATA | Flow | param', 'Common', array( 'archive' => 'URI', 'classid' => 'URI', 'codebase' => 'URI', 'codetype' => 'Text', 'data' => 'URI', 'declare' => 'Bool#declare', 'height' => 'Length', 'name' => 'CDATA', 'standby' => 'Text', 'tabindex' => 'Number', 'type' => 'ContentType', 'width' => 'Length' ) ); $this->addElement( 'param', false, 'Empty', null, array( 'id' => 'ID', 'name*' => 'Text', 'type' => 'Text', 'value' => 'Text', 'valuetype' => 'Enum#data,ref,object' ) ); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php000064400000000540152665541560021515 0ustar00 array( 'xml:lang' => 'LanguageCode', ) ); } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php000064400000001414152665541560020622 0ustar00 array('style' => false), // see constructor 'Core' => array(0 => array('Style')) ); /** * @param HTMLPurifier_Config $config */ public function setup($config) { $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS(); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php000064400000006553152665541560016573 0ustar00 'Heading | Block | Inline' ); /** * @param HTMLPurifier_Config $config */ public function setup($config) { // Inline Phrasal ------------------------------------------------- $this->addElement('abbr', 'Inline', 'Inline', 'Common'); $this->addElement('acronym', 'Inline', 'Inline', 'Common'); $this->addElement('cite', 'Inline', 'Inline', 'Common'); $this->addElement('dfn', 'Inline', 'Inline', 'Common'); $this->addElement('kbd', 'Inline', 'Inline', 'Common'); $this->addElement('q', 'Inline', 'Inline', 'Common', array('cite' => 'URI')); $this->addElement('samp', 'Inline', 'Inline', 'Common'); $this->addElement('var', 'Inline', 'Inline', 'Common'); $em = $this->addElement('em', 'Inline', 'Inline', 'Common'); $em->formatting = true; $strong = $this->addElement('strong', 'Inline', 'Inline', 'Common'); $strong->formatting = true; $code = $this->addElement('code', 'Inline', 'Inline', 'Common'); $code->formatting = true; // Inline Structural ---------------------------------------------- $this->addElement('span', 'Inline', 'Inline', 'Common'); $this->addElement('br', 'Inline', 'Empty', 'Core'); // Block Phrasal -------------------------------------------------- $this->addElement('address', 'Block', 'Inline', 'Common'); $this->addElement('blockquote', 'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI')); $pre = $this->addElement('pre', 'Block', 'Inline', 'Common'); $pre->excludes = $this->makeLookup( 'img', 'big', 'small', 'object', 'applet', 'font', 'basefont' ); $this->addElement('h1', 'Heading', 'Inline', 'Common'); $this->addElement('h2', 'Heading', 'Inline', 'Common'); $this->addElement('h3', 'Heading', 'Inline', 'Common'); $this->addElement('h4', 'Heading', 'Inline', 'Common'); $this->addElement('h5', 'Heading', 'Inline', 'Common'); $this->addElement('h6', 'Heading', 'Inline', 'Common'); // Block Structural ----------------------------------------------- $p = $this->addElement('p', 'Block', 'Inline', 'Common'); $p->autoclose = array_flip( array("address", "blockquote", "center", "dir", "div", "dl", "fieldset", "ol", "p", "ul") ); $this->addElement('div', 'Block', 'Flow', 'Common'); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php000064400000001322152665541560021133 0ustar00 array( 0 => array('Style'), // 'xml:space' => false, 'class' => 'Class', 'id' => 'ID', 'title' => 'CDATA', 'contenteditable' => 'ContentEditable', ), 'Lang' => array(), 'I18N' => array( 0 => array('Lang'), // proprietary, for xml:lang/lang ), 'Common' => array( 0 => array('Core', 'I18N') ) ); } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php000064400000001016152665541560021114 0ustar00addBlankElement('a'); $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoreferrer(); } } htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php000064400000002035152665541560016557 0ustar00addElement( 'ruby', 'Inline', 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))', 'Common' ); $this->addElement('rbc', false, 'Required: rb', 'Common'); $this->addElement('rtc', false, 'Required: rt', 'Common'); $rb = $this->addElement('rb', false, 'Inline', 'Common'); $rb->excludes = array('ruby' => true); $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number')); $rt->excludes = array('ruby' => true); $this->addElement('rp', false, 'Optional: #PCDATA', 'Common'); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/List.php000064400000003565152665541560016562 0ustar00 'List'); /** * @param HTMLPurifier_Config $config */ public function setup($config) { $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); // XXX The wrap attribute is handled by MakeWellFormed. This is all // quite unsatisfactory, because we generated this // *specifically* for lists, and now a big chunk of the handling // is done properly by the List ChildDef. So actually, we just // want enough information to make autoclosing work properly, // and then hand off the tricky stuff to the ChildDef. $ol->wrap = 'li'; $ul->wrap = 'li'; $this->addElement('dl', 'List', 'Required: dt | dd', 'Common'); $this->addElement('li', false, 'Flow', 'Common'); $this->addElement('dd', false, 'Flow', 'Common'); $this->addElement('dt', false, 'Inline', 'Common'); } } // vim: et sw=4 sts=4 htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php000064400000002357152665541560020406 0ustar00get('HTML.SafeScripting'); $script = $this->addElement( 'script', 'Inline', 'Optional:', // Not `Empty` to not allow to autoclose the