html purifier 보강

This commit is contained in:
2018-06-24 02:56:38 +09:00
parent 6031ec2578
commit 2add3944d3
17 changed files with 812 additions and 4 deletions
@@ -0,0 +1,38 @@
<?php
/**
* Validates a boolean attribute
*
* HTMLPurifier 4.6.0 has broken support for boolean attributes, as reported
* in: http://htmlpurifier.org/phorum/read.php?3,7631,7631
* This issue has almost been fixed in 4.7.0, boolean attributes are properly
* parsed, but their values are not validated.
*/
class HTMLPurifier_AttrDef_HTML_Bool2 extends HTMLPurifier_AttrDef_HTML_Bool
{
/**
* @param string $string
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool
*/
public function validate($string, $config, $context)
{
$name = strlen($this->name) ? $this->name : $context->get('CurrentAttr');
// boolean attribute validates if its value is either empty
// or case-insensitively equal to attribute name
return $string === '' || strcasecmp($name, $string) === 0;
}
/**
* @param string $string Name of attribute
* @return HTMLPurifier_AttrDef_HTML_Bool2
*/
public function make($string)
{
return new self($string);
}
}
// vim: et sw=4 sts=4
@@ -0,0 +1,48 @@
<?php
class HTMLPurifier_AttrDef_Regexp extends HTMLPurifier_AttrDef
{
/**
* @var string
*/
protected $pattern;
/**
* @param string $pattern
*/
public function __construct($pattern = null)
{
if ($pattern !== null) {
$pattern = (string) $pattern;
if (false === @preg_match($pattern, 'Test')) {
throw new HTMLPurifier_Exception('Invalid regular expression pattern provided');
}
$this->pattern = $pattern;
}
}
/**
* @param string $string
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool
*/
public function validate($string, $config, $context)
{
if ($this->pattern) {
return (bool) preg_match($this->pattern, $string);
}
return false;
}
/**
* @param string $string
* @return HTMLPurifier_AttrDef_Regexp
*/
public function make($string)
{
return new self($string);
}
}
// vim: et sw=4 sts=4
@@ -0,0 +1,84 @@
<?php
class HTMLPurifier_ChildDef_Details extends HTMLPurifier_ChildDef
{
public $type = 'details';
public $elements = array(
'summary' => true,
);
protected $allowedElements;
/**
* @param HTMLPurifier_Config $config
* @return array
*/
public function getAllowedElements($config)
{
if (null === $this->allowedElements) {
// Add Flow content to allowed elements to prevent MakeWellFormed
// strategy moving them outside details element
$def = $config->getHTMLDefinition();
$this->allowedElements = array_merge(
$def->info_content_sets['Flow'],
$this->elements
);
}
return $this->allowedElements;
}
/**
* @param array $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
*/
public function validateChildren($children, $config, $context)
{
if (empty($children)) {
return false;
}
if (!isset($config->getHTMLDefinition()->info['summary'])) {
trigger_error("Cannot allow details without allowing summary", E_USER_WARNING);
return false;
}
$summary = null;
$result = array();
// Content model:
// One summary element followed by flow content
foreach ($children as $node) {
if (!$summary && $node->name === 'summary') {
$summary = $node;
continue;
}
if ($node->name === 'summary') {
// duplicated summary, add only its children
$result = array_merge($result, (array) $node->children);
} else {
$result[] = $node;
}
}
$whitespaceOnly = true;
foreach ($result as $node) {
$whitespaceOnly = $whitespaceOnly && !empty($node->is_whitespace);
}
if (!$summary) {
// remove parent node if there are no children or all children are whitespace-only
if ($whitespaceOnly) {
return false;
}
$summary = new HTMLPurifier_Node_Element('summary');
}
array_unshift($result, $summary);
return $result;
}
}
@@ -0,0 +1,89 @@
<?php
class HTMLPurifier_ChildDef_Figure extends HTMLPurifier_ChildDef
{
public $type = 'figure';
public $elements = array(
'figcaption' => true,
);
protected $allowedElements;
/**
* @param HTMLPurifier_Config $config
* @return array
*/
public function getAllowedElements($config)
{
if (null === $this->allowedElements) {
// Add Flow content to allowed elements to prevent MakeWellFormed
// strategy moving them outside 'figure' element
$def = $config->getHTMLDefinition();
$this->allowedElements = array_merge(
$def->info_content_sets['Flow'],
$this->elements
);
}
return $this->allowedElements;
}
/**
* @param array $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
*/
public function validateChildren($children, $config, $context)
{
$allowFigcaption = isset($config->getHTMLDefinition()->info['figcaption']);
$hasFigcaption = false;
$figcaptionPos = -1;
$result = array();
// Content model:
// Either: one figcaption element followed by flow content.
// Or: flow content followed by one figcaption element.
// Or: flow content.
// Scan through children, accept at most one figcaption. If additional
// figcaption appears replace it with div
foreach ($children as $node) {
if ($node->name === 'figcaption') {
if ($allowFigcaption && !$hasFigcaption) {
$hasFigcaption = true;
$figcaptionPos = count($result);
$result[] = $node;
continue;
}
$div = new HTMLPurifier_Node_Element('div', $node->attr);
$div->children = $node->children;
$result[] = $div;
continue;
}
// Figcaption must be a first or last child of a figure element.
// If it's not first, then we ignore all siblings that come after.
if ($hasFigcaption && $figcaptionPos > 0) {
break;
}
$result[] = $node;
}
$whitespaceOnly = true;
foreach ($result as $node) {
$whitespaceOnly = $whitespaceOnly && !empty($node->is_whitespace);
}
// remove parent node if there are no children or all children are whitespace-only
if (empty($result) || $whitespaceOnly) {
return false;
}
return $result;
}
}
@@ -0,0 +1,94 @@
<?php
class HTMLPurifier_ChildDef_Media extends HTMLPurifier_ChildDef
{
public $type = 'media';
public $elements = array(
'source' => true,
'track' => true,
);
protected $allowedElements;
/**
* @param HTMLPurifier_Config $config
* @return array
*/
public function getAllowedElements($config)
{
if (null === $this->allowedElements) {
// Add Flow content to allowed elements to prevent MakeWellFormed
// strategy moving them outside details element
$def = $config->getHTMLDefinition();
$this->allowedElements = array_merge(
$def->info_content_sets['Flow'],
$this->elements
);
unset(
$this->allowedElements['audio'],
$this->allowedElements['video']
);
}
return $this->allowedElements;
}
/**
* @param array $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
*/
public function validateChildren($children, $config, $context)
{
// Content model:
// If the element has a src attribute: zero or more track elements,
// then transparent, but with no media element descendants.
// If the element does not have a src attribute: zero or more source
// elements, then zero or more track elements, then transparent, but
// with no media element descendants.
$allowSource = isset($config->getHTMLDefinition()->info['source']);
$allowTrack = isset($config->getHTMLDefinition()->info['track']);
$sources = array();
$tracks = array();
$content = array();
foreach ($children as $node) {
switch ($node->name) {
case 'source':
if ($allowSource) {
$sources[] = $node;
}
break;
case 'track':
if ($allowTrack) {
$tracks[] = $node;
}
break;
default:
$content[] = $node;
break;
}
}
$currentNode = $context->get('CurrentNode');
$hasSrcAttr = $currentNode instanceof HTMLPurifier_Node_Element && isset($currentNode->attr['src']);
if ($hasSrcAttr) {
$result = array_merge($tracks, $content);
} else {
$result = array_merge($sources, $tracks, $content);
}
if (empty($result) && !$hasSrcAttr) {
return false;
}
return $result;
}
}
@@ -0,0 +1,54 @@
<?php
class HTMLPurifier_ChildDef_Picture extends HTMLPurifier_ChildDef
{
public $type = 'picture';
public $elements = array(
'img' => true,
'source' => true,
);
/**
* @param array $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
*/
public function validateChildren($children, $config, $context)
{
// if there are no tokens, delete parent node
if (empty($children)) {
return false;
}
if (!isset($config->getHTMLDefinition()->info['img'])) {
trigger_error("Cannot allow picture without allowing img", E_USER_WARNING);
return false;
}
$allowSource = isset($config->getHTMLDefinition()->info['source']);
$hasImg = false;
$result = array();
// Content model:
// Zero or more source elements, followed by one img element, optionally intermixed with script-supporting elements.
// https://html.spec.whatwg.org/multipage/embedded-content.html#the-picture-element
foreach ($children as $node) {
if (($allowSource && $node->name === 'source') || $node->name === 'img') {
$result[] = $node;
}
if ($node->name === 'img') {
$hasImg = true;
break;
}
}
if (!$hasImg || empty($result)) {
return false;
}
return $result;
}
}
@@ -0,0 +1,75 @@
<?php
class HTMLPurifier_HTML5Config extends HTMLPurifier_Config
{
const REVISION = 2018060702;
/**
* @param string|array|HTMLPurifier_Config $config
* @param HTMLPurifier_ConfigSchema $schema
* @return HTMLPurifier_Config
*/
public static function create($config, $schema = null)
{
if ($config instanceof HTMLPurifier_Config) {
$schema = $config->def;
$config = null;
}
if (!$schema instanceof HTMLPurifier_ConfigSchema) {
$schema = HTMLPurifier_ConfigSchema::instance();
}
$configObj = new self($schema);
$configObj->set('Core.Encoding', 'UTF-8');
$configObj->set('HTML.Doctype', 'HTML 4.01 Transitional');
$configObj->set('HTML.DefinitionID', __CLASS__);
$configObj->set('HTML.DefinitionRev', self::REVISION);
if (is_string($config)) {
$configObj->loadIni($config);
} elseif (is_array($config)) {
$configObj->loadArray($config);
}
return $configObj;
}
/**
* Creates a configuration object using the default config schema instance
*
* @return HTMLPurifier_Config
*/
public static function createDefault()
{
$schema = HTMLPurifier_ConfigSchema::instance();
$config = self::create(null, $schema);
return $config;
}
/**
* Creates a new config object that inherits from a previous one
*
* @param HTMLPurifier_Config $config
* @return HTMLPurifier_Config
*/
public static function inherit(HTMLPurifier_Config $config)
{
return new self($config->def, $config->plist);
}
public function getDefinition($type, $raw = false, $optimized = false)
{
// Setting HTML.* keys removes any previously instantiated HTML
// definition object, so set up HTML5 definition as late as possible
$needSetup = $type === 'HTML' && !isset($this->definitions[$type]);
if ($needSetup) {
if ($def = parent::getDefinition($type, true, true)) {
HTMLPurifier_HTML5Definition::setup($def);
}
}
return parent::getDefinition($type, $raw, $optimized);
}
}
@@ -0,0 +1,118 @@
<?php
class HTMLPurifier_HTML5Definition
{
/**
* Adds HTML5 element and attributes to a provided definition object.
*
* @param HTMLPurifier_HTMLDefinition $def
* @return HTMLPurifier_HTMLDefinition
*/
public static function setup(HTMLPurifier_HTMLDefinition $def)
{
// use fixed implementation of Boolean attributes, instead of a buggy
// one provided with 4.6.0
$def->manager->attrTypes->set('Bool', new HTMLPurifier_AttrDef_HTML_Bool2());
// http://developers.whatwg.org/sections.html
$def->addElement('section', 'Block', 'Flow', 'Common');
$def->addElement('nav', 'Block', 'Flow', 'Common');
$def->addElement('article', 'Block', 'Flow', 'Common');
$def->addElement('aside', 'Block', 'Flow', 'Common');
$def->addElement('header', 'Block', 'Flow', 'Common');
$def->addElement('footer', 'Block', 'Flow', 'Common');
$def->addElement('main', 'Block', 'Flow', 'Common');
// Content model actually excludes several tags, not modelled here
$def->addElement('address', 'Block', 'Flow', 'Common');
$def->addElement('hgroup', 'Block', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common');
// https://html.spec.whatwg.org/dev/grouping-content.html#the-figure-element
$def->addElement('figure', 'Block', new HTMLPurifier_ChildDef_Figure(), 'Common');
$def->addElement('figcaption', false, 'Flow', 'Common');
$mediaContent = new HTMLPurifier_ChildDef_Media();
// https://html.spec.whatwg.org/dev/media.html#the-video-element
$def->addElement('video', 'Flow', $mediaContent, 'Common', array(
'controls' => 'Bool',
'height' => 'Length',
'poster' => 'URI',
'preload' => 'Enum#auto,metadata,none',
'src' => 'URI',
'width' => 'Length',
));
$def->getAnonymousModule()->addElementToContentSet('video', 'Inline');
// https://html.spec.whatwg.org/dev/media.html#the-audio-element
$def->addElement('audio', 'Flow', $mediaContent, 'Common', array(
'controls' => 'Bool',
'preload' => 'Enum#auto,metadata,none',
'src' => 'URI',
));
$def->getAnonymousModule()->addElementToContentSet('audio', 'Inline');
// https://html.spec.whatwg.org/dev/embedded-content.html#the-source-element
$def->addElement('source', false, 'Empty', 'Common', array(
'media' => 'Text',
'sizes' => 'Text',
'src' => 'URI',
'srcset' => 'Text',
'type' => 'Text',
));
// https://html.spec.whatwg.org/dev/media.html#the-track-element
$def->addElement('track', false, 'Empty', 'Common', array(
'kind' => 'Enum#captions,chapters,descriptions,metadata,subtitles',
'src' => 'URI',
'srclang' => 'Text',
'label' => 'Text',
'default' => 'Bool',
));
// https://html.spec.whatwg.org/dev/embedded-content.html#the-picture-element
$def->addElement('picture', 'Flow', new HTMLPurifier_ChildDef_Picture(), 'Common');
$def->getAnonymousModule()->addElementToContentSet('picture', 'Inline');
// http://developers.whatwg.org/text-level-semantics.html
$def->addElement('s', 'Inline', 'Inline', 'Common');
$def->addElement('var', 'Inline', 'Inline', 'Common');
$def->addElement('sub', 'Inline', 'Inline', 'Common');
$def->addElement('sup', 'Inline', 'Inline', 'Common');
$def->addElement('mark', 'Inline', 'Inline', 'Common');
$def->addElement('wbr', 'Inline', 'Empty', 'Core');
// http://developers.whatwg.org/edits.html
$def->addElement('ins', 'Block', 'Flow', 'Common', array('cite' => 'URI', 'datetime' => 'Text'));
$def->addElement('del', 'Block', 'Flow', 'Common', array('cite' => 'URI', 'datetime' => 'Text'));
// TIME
$time = $def->addElement('time', 'Inline', 'Inline', 'Common', array('datetime' => 'Text', 'pubdate' => 'Bool'));
$time->excludes = array('time' => true);
// https://html.spec.whatwg.org/dev/text-level-semantics.html#the-a-element
$def->addElement('a', 'Flow', 'Flow', 'Common', array(
'download' => 'Text',
'hreflang' => 'Text',
'rel' => 'Text',
'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget(),
'type' => 'Text',
));
// IMG
$def->addAttribute('img', 'srcset', 'Text');
$def->addAttribute('img', 'sizes', 'Text');
// IFRAME
$def->addAttribute('iframe', 'allowfullscreen', 'Bool');
// Interactive elements
// https://html.spec.whatwg.org/dev/interactive-elements.html#the-details-element
$def->addElement('details', 'Block', new HTMLPurifier_ChildDef_Details(), 'Common', array(
'open' => 'Bool',
));
$def->addElement('summary', false, 'Flow', 'Common');
return $def;
}
}