dev와 composer 버전 일치

This commit is contained in:
2020-03-22 23:22:50 +09:00
parent 869ad39ddc
commit bdcc79ce34
421 changed files with 14006 additions and 16258 deletions
@@ -0,0 +1,116 @@
<?php
/**
* Validates a floating point number
*/
class HTMLPurifier_AttrDef_Float extends HTMLPurifier_AttrDef
{
/**
* @var int|float
*/
protected $min;
/**
* @var int|float
*/
protected $max;
/**
* @var bool
*/
protected $minInclusive = true;
/**
* @var bool
*/
protected $maxInclusive = true;
/**
* Supported options:
*
* - 'min' => int|float
* - 'max' => int|float
* - 'minInclusive' => bool
* - 'maxInclusive' => bool
*
* @param array $options OPTIONAL
*/
public function __construct($options = null)
{
$options = is_array($options) ? $options : array();
$this->min = isset($options['min']) ? floatval($options['min']) : null;
$this->max = isset($options['max']) ? floatval($options['max']) : null;
if (isset($options['minInclusive'])) {
$this->minInclusive = (bool) $options['minInclusive'];
}
if (isset($options['maxInclusive'])) {
$this->maxInclusive = (bool) $options['maxInclusive'];
}
}
/**
* @param string $number
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return string
*/
public function validate($number, $config, $context)
{
$number = $this->parseCDATA($number);
if ($number === '') {
return false;
}
// Up to PHP 5.6 is_numeric() returns TRUE for hex strings
// http://php.net/manual/en/function.is-numeric.php
if (!preg_match('/^[-+.0-9Ee]+$/', $number) || !is_numeric($number)) {
return false;
}
// HTML numbers cannot start with '+' character
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-floating-point-number
if (substr($number, 0, 1) === '+') {
$number = substr($number, 1);
}
$value = floatval($number);
if (($this->min !== null) &&
(($this->minInclusive && $value < $this->min) || (!$this->minInclusive && $value <= $this->min))
) {
return false;
}
if (($this->max !== null) &&
(($this->maxInclusive && $this->max < $value) || (!$this->maxInclusive && $this->max <= $value))
) {
return false;
}
return $number;
}
/**
* Factory function
*
* @param string $string A comma-delimited list of key:value pairs. Example: "min:0,max:10".
* @return HTMLPurifier_AttrDef_Float
*/
public function make($string)
{
$options = array();
foreach (explode(',', $string) as $pair) {
$parts = explode(':', $pair, 2);
if (count($parts) === 2) {
list($key, $value) = $parts;
$options[$key] = $value;
}
}
$class = get_class($this);
return new $class($options);
}
}
@@ -0,0 +1,148 @@
<?php
/**
* Validates 'rel' attribute on <a> and <area> elements, as defined by the
* HTML5 spec and the MicroFormats link type extensions tables.
*/
class HTMLPurifier_AttrDef_HTML5_ARel extends HTMLPurifier_AttrDef
{
/**
* Lookup table for valid values
* @var array
*/
protected static $values = array(
// https://html.spec.whatwg.org/multipage/links.html#linkTypes
'alternate' => true,
'author' => true,
'bookmark' => true,
'external' => true,
'help' => true,
'license' => true,
'next' => true,
'nofollow' => true,
'noopener' => true,
'noreferrer' => true,
'opener' => true,
'prev' => true,
'search' => true,
'sidebar' => true,
'tag' => true,
// http://microformats.org/wiki/existing-rel-values#HTML5_link_type_extensions
'acquaintance' => true,
'amphtml' => true,
'appendix' => true,
'archived' => true,
'attachment' => true,
'canonical' => true,
'category' => true,
'chapter' => true,
'child' => true,
'co-resident' => true,
'co-worker' => true,
'code-license' => true,
'code-repository' => true,
'colleague' => true,
'contact' => true,
'content-license' => true,
'content-repository' => true,
'contents' => true,
'copyright' => true,
'crush' => true,
'date' => true,
'disclosure' => true,
'discussion' => true,
'enclosure' => true,
'entry-content' => true,
'first' => true,
'friend' => true,
'glossary' => true,
'home' => true,
'http://docs.oasis-open.org/ns/cmis/link/200908/acl' => true,
'hub' => true,
'in-reply-to' => true,
'index' => true,
'issues' => true,
'jslicense' => true,
'last' => true,
'kin' => true,
'lightbox' => true,
'lightvideo' => true,
'me' => true,
'met' => true,
'muse' => true,
'neighbor' => true,
'parent' => true,
'prerender' => true,
'previous' => true,
'profile' => true,
'publisher' => true,
'radioepg' => true,
'rendition' => true,
'reply-to' => true,
'root' => true,
'section' => true,
'sibling' => true,
'spouse' => true,
'start' => true,
'subsection' => true,
'sweetheart' => true,
'syndication' => true,
'toc' => true,
'transformation' => true,
'webmention' => true,
'widget' => true,
);
/**
* Return lookup table for valid 'rel' values
*
* @return array
* @codeCoverageIgnore
*/
public static function values()
{
return self::$values;
}
/**
* @var array
*/
protected $allowed;
/**
* @param string $string
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool|string
*/
public function validate($string, $config, $context)
{
if ($this->allowed === null) {
$allowedRel = (array) $config->get('Attr.AllowedRel');
if (empty($allowedRel)) {
$allowed = array();
} else {
$allowed = array_intersect_key($allowedRel, self::$values);
}
$this->allowed = $allowed;
}
$string = $this->parseCDATA($string);
$parts = explode(' ', $string);
$result = array();
foreach ($parts as $part) {
$part = strtolower(trim($part));
if (!isset($this->allowed[$part])) {
continue;
}
$result[$part] = true;
}
if (empty($result)) {
return false;
}
return implode(' ', array_keys($result));
}
}
@@ -0,0 +1,360 @@
<?php
/**
* Validates HTML5 date and time strings according to spec
* https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#dates-and-times
*
* This validator tries to parse as much data as possible and then tries to
* render it in the desired format. It fails if either no datetime data can
* be extracted from the input, or the extracted data is insufficient for
* the desired format (with the exception of DatetimeGlobal format, which
* uses server timezone offset if none is detected).
*/
class HTMLPurifier_AttrDef_HTML5_Datetime extends HTMLPurifier_AttrDef
{
const REGEX = '/^
(
(?P<year>\d{4,})
(
-
(?P<month>[01]\d)
(
-
(?P<day>[0-3]\d)
)?
)?
)?
(
(^|(\s+|T))
(?P<hour>[0-2]\d)
:
(?P<minute>[0-5]\d)
(
:
(?P<second>[0-5]\d(\.\d+)?)
)?
)?
(
(?P<tzZulu>Z)
|
(
(?P<tzHour>[+-][0-2]\d)
:?
(?P<tzMinute>[0-5]\d)
)
)?
$/xi';
/**
* Lookup table for supported formats and if they are enabled by default
* @var array
*/
protected static $formats = array(
'Datetime' => true,
'DatetimeGlobal' => false,
'DatetimeLocal' => false,
'Date' => true,
'Month' => true,
'Year' => true,
'Time' => true,
'TimezoneOffset' => true,
);
/**
* Lookup table for allowed formats
* @var array
*/
protected $allowedFormats = array();
/**
* @param array $allowedFormats OPTIONAL
* @throws HTMLPurifier_Exception If an invalid format is provided
*/
public function __construct(array $allowedFormats = array())
{
// Validate allowed formats
$allowedFormatsLookup = array();
foreach ($allowedFormats as $format) {
if (!isset(self::$formats[$format])) {
throw new HTMLPurifier_Exception("'$format' is not a valid format");
}
$allowedFormatsLookup[$format] = true;
}
// Formats must be set in the same order as in self::$formats, so that
// in default mode the result will be the longest matching format
foreach (self::$formats as $format => $_) {
if (isset($allowedFormatsLookup[$format])) {
$this->allowedFormats[$format] = true;
}
}
if (empty($this->allowedFormats)) {
$this->allowedFormats = array_filter(self::$formats, 'intval');
}
}
/**
* @param string $string
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool|string
*/
public function validate($string, $config, $context)
{
if (($data = $this->parse($string)) === false) {
return false;
}
return $this->format($data);
}
/**
* @param string $string
* @return array|bool
*/
public function parse($string)
{
$string = $this->parseCDATA($string);
if ($string === '' || !preg_match(self::REGEX, $string, $match)) {
return false;
}
// Make sure all named patterns are present in the match array
$match += array(
'year' => '',
'month' => '',
'day' => '',
'hour' => '',
'minute' => '',
'second' => '',
'tzZulu' => '',
'tzHour' => '',
'tzMinute' => '',
);
$year = $month = $day = null;
if ($match['year'] !== '') {
$year = (int) $match['year'];
// Dates before the year one can't be represented as a datetime in HTML5
if ($year <= 0) {
return false;
}
if ($match['month'] !== '') {
$month = (int) $match['month'];
if ($month < 1 || $month > 12) {
return false;
}
if ($match['day'] !== '') {
$day = (int) $match['day'];
if (!checkdate($month, $day, $year)) {
return false;
}
}
}
}
$hour = $minute = $second = null;
if ($match['hour'] !== '') {
$hour = (int) $match['hour'];
$minute = (int) $match['minute'];
$second = $match['second'] !== '' ? (float) $match['second'] : null;
if ($hour > 23) {
return false;
}
}
$tzHour = $tzMinute = null;
if ($match['tzZulu'] !== '') {
$tzHour = 'Z';
} elseif ($match['tzHour'] !== '') {
$tzHour = (int) $match['tzHour'];
$tzMinute = (int) $match['tzMinute'];
if ($tzHour < -23 || $tzHour > 23) {
return false;
}
}
return compact(
'year', 'month', 'day', 'hour', 'minute', 'second', 'tzHour', 'tzMinute'
);
}
/**
* @param array $data
* @return bool|string
*/
protected function format(array $data)
{
foreach ($this->allowedFormats as $format => $_) {
if (($result = call_user_func(array($this, 'format' . $format), $data)) !== false) {
return $result;
}
}
return false;
}
/**
* @param array $data
* @return bool|string
*/
protected function formatYear(array $data)
{
if (($year = $data['year']) === null) {
return false;
}
return sprintf('%04d', $year);
}
/**
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#months
* @param array $data
* @return bool|string
*/
protected function formatMonth(array $data)
{
if (($year = $data['year']) === null ||
($month = $data['month']) === null
) {
return false;
}
return sprintf('%04d-%02d', $year, $month);
}
/**
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#dates
* @param array $data
* @return bool|string
*/
protected function formatDate(array $data)
{
if (($year = $data['year']) === null ||
($month = $data['month']) === null ||
($day = $data['day']) === null
) {
return false;
}
return sprintf('%04d-%02d-%02d', $year, $month, $day);
}
/**
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#times
* @param array $data
* @return bool|string
*/
protected function formatTime(array $data)
{
if (($hour = $data['hour']) === null ||
($minute = $data['minute']) === null
) {
return false;
}
$time = sprintf('%02d:%02d', $hour, $minute);
if (($second = $data['second']) !== null) {
$sec = (int) $second;
$time .= sprintf(':%02d', $sec);
$msec = round(($second - $sec) * 1000);
if ($msec > 0) {
$time .= rtrim(sprintf('.%03d', $msec), '0');
}
}
return $time;
}
/**
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#local-dates-and-times
* @param array $data
* @return bool|string
*/
protected function formatDatetimeLocal(array $data)
{
if (($date = $this->formatDate($data)) === false ||
($time = $this->formatTime($data)) === false
) {
return false;
}
// Use 'T' as normalized date/time separator, see:
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-normalised-local-date-and-time-string
// Also it's the only separator recognized by input[type=datetime-local]
return $date . 'T' . $time;
}
/**
* Formats data as datetime with timezone offset. If no timezone offset
* is present, the default server timezone offset is used.
*
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#global-dates-and-times
* @param array $data
* @return bool|string
*/
protected function formatDatetimeGlobal(array $data)
{
if (($datetime = $this->formatDatetimeLocal($data)) === false) {
return false;
}
if (($timezoneOffset = $this->formatTimezoneOffset($data)) === false) {
$timezoneOffset = date('P');
}
return $datetime . $timezoneOffset;
}
/**
* Formats data as datetime with optional timezone offset.
*
* This is used in particular for 'datetime' attribute of <time> element.
*
* @param array $data
* @return bool|string
*/
protected function formatDatetime(array $data)
{
if (($datetime = $this->formatDatetimeLocal($data)) === false) {
return false;
}
if (($timezoneOffset = $this->formatTimezoneOffset($data)) !== false) {
$datetime .= $timezoneOffset;
}
return $datetime;
}
/**
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#time-zones
* @param array $data
* @return string
*/
protected function formatTimezoneOffset(array $data)
{
if (($tzHour = $data['tzHour']) === null) {
return false;
}
if ($tzHour === 'Z') {
$tzOffset = 'Z';
} else {
$tzMinute = (int) $data['tzMinute'];
$tzOffset = sprintf('%s%02d:%02d', $tzHour < 0 ? '-' : '+', abs($tzHour), $tzMinute);
}
return $tzOffset;
}
/**
* @param string $formats
* @return HTMLPurifier_AttrDef_HTML5_Datetime
* @throws HTMLPurifier_Exception If an invalid format is provided
*/
public function make($formats)
{
return new self(explode(',', $formats));
}
}
@@ -0,0 +1,173 @@
<?php
/**
* Validates HTML5 duration string according to spec
* https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
*/
class HTMLPurifier_AttrDef_HTML5_Duration extends HTMLPurifier_AttrDef
{
const REGEX_ISO8601 = '/^
P
(?P<w>\d+W)?
(?P<d>\d+D)?
(
T
(?P<h>\d+H)?
(?P<m>\d+M)?
(?P<s>\d+(\.\d+)?S)?
)?
$/xi';
const REGEX_HUMAN = '/(\d+(\s*[WDHMS]|\.\d+\s*S))/i';
/**
* @param string $string
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool|string
*/
public function validate($string, $config, $context)
{
$string = $this->parseCDATA($string);
if (($result = $this->validateISODuration($string)) !== false) {
return $result;
}
if (($result = $this->validateHumanDuration($string)) !== false) {
return $result;
}
return false;
}
/**
* Validate ISO-8601 duration string
*
* Note: duration as defined in the HTML5 spec cannot include months or years
*
* @param string $string
* @return boolean
*/
protected function validateISODuration($string)
{
if (!preg_match(self::REGEX_ISO8601, $string, $match)) {
return false;
}
$parts = array(
'w' => 0,
'd' => 0,
'h' => 0,
'm' => 0,
's' => 0,
);
foreach ($parts as $unit => $_) {
if (!isset($match[$unit])) {
continue;
}
$value = substr($match[$unit], 0, -1);
$value = $unit === 's' ? (float) $value : (int) $value;
$parts[$unit] = $value;
}
// The spec self-contradicts itself in disallowing weeks in ISO-8601
// format, but allowing them in Human format - each week being equal
// to 604800 seconds (7 days).
if ($parts['w'] > 0) {
$parts['d'] += $parts['w'] * 7;
$parts['w'] = 0;
}
$duration = 'P';
foreach ($parts as $unit => $value) {
if ($unit === 'h') {
$duration .= 'T';
}
if ($value > 0) {
$duration .= ($unit === 's' ? $this->formatSeconds($value) : $value) . strtoupper($unit);
}
}
$duration = rtrim($duration, 'T');
// At least one element must be present, thus "P" is not a valid
// representation for a duration of 0 seconds. "PT0S" or "P0D" are both
// valid and represent the same duration.
// https://en.wikipedia.org/wiki/ISO_8601#Durations
if ($duration === 'P') {
$duration = 'PT0S';
}
return $duration;
}
/**
* Validate human readable HTML5 duration string
*
* @param string $string
* @return boolean|string
*/
protected function validateHumanDuration($string)
{
if (!preg_match_all(self::REGEX_HUMAN, $string, $matches)) {
return false;
}
// One or more duration time components, each with a different duration
// time component scale, in any order.
$parts = array(
'w' => false,
'd' => false,
'h' => false,
'm' => false,
's' => false,
);
foreach ($matches[0] as $match) {
$unit = strtolower(substr($match, -1));
$value = rtrim(substr($match, 0, -1));
$value = $unit === 's' ? (float) $value : (int) $value;
if ($value > 0 && $parts[$unit] === false) {
$parts[$unit] = $value;
}
}
$duration = array();
foreach ($parts as $unit => $value) {
if ($value === false) {
continue;
}
$duration[] = ($unit === 's' ? $this->formatSeconds($value) : $value) . $unit;
}
$duration = implode(' ', $duration);
if ($duration === '') {
$duration = '0s';
}
return $duration;
}
/**
* Formats seconds without leading zero and at most 3 non-zero decimals
*
* @param float $sec
* @return string
*/
protected function formatSeconds($sec)
{
$msec = round(($sec - (int) $sec) * 1000);
if ($msec > 0) {
return rtrim(sprintf('%d.%03d', $sec, $msec), '0');
}
return sprintf('%d', $sec);
}
}
@@ -0,0 +1,47 @@
<?php
/**
* Validates HTML5 week string according to
* https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#weeks
*/
class HTMLPurifier_AttrDef_HTML5_Week extends HTMLPurifier_AttrDef
{
const REGEX = '/^
(?P<year>\d{4,})
-W
(?P<week>[0-5]\d)
$/xi';
/**
* @param string $string
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool|string
*/
public function validate($string, $config, $context)
{
$string = $this->parseCDATA($string);
if (!preg_match(self::REGEX, $string, $match)) {
return false;
}
$year = (int) $match['year'];
$week = (int) $match['week'];
if ($year <= 0) {
return false;
}
// ISO-8601 specification says that December 28th is always in the last
// week of its year.
// https://en.wikipedia.org/wiki/ISO_8601#Week_dates
$time = mktime(0, 0, 0, 12, 28, $year);
if ($week < 1 || $week > date('W', $time)) {
return false;
}
return $string;
}
}
@@ -0,0 +1,41 @@
<?php
/**
* Validates HTML5 yearless date string according to the spec
* https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#yearless-dates
*/
class HTMLPurifier_AttrDef_HTML5_YearlessDate extends HTMLPurifier_AttrDef
{
const REGEX = '/^(?P<month>[01]\d)-(?P<day>[0-3]\d)$/';
protected static $daysInMonths = array(31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
/**
* @param string $string
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool|string
*/
public function validate($string, $config, $context)
{
$string = $this->parseCDATA($string);
if (!preg_match(self::REGEX, $string, $match)) {
return false;
}
$month = (int) $match['month'];
if ($month < 1 || $month > 12) {
return false;
}
$day = (int) $match['day'];
if ($day < 1 || $day > self::$daysInMonths[$month - 1]) {
return false;
}
return $string;
}
}
@@ -1,48 +0,0 @@
<?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,19 @@
<?php
class HTMLPurifier_AttrTransform_HTML5_Dialog extends HTMLPurifier_AttrTransform
{
/**
* @param array $attr
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
*/
public function transform($attr, $config, $context)
{
// The tabindex attribute must not be specified on dialog elements.
// https://html.spec.whatwg.org/dev/interactive-elements.html#the-dialog-element
unset($attr['tabindex']);
return $attr;
}
}
@@ -0,0 +1,38 @@
<?php
/**
* Post-transform performing validations for <progress> elements ensuring
* that if value is present, it is within a valid range (0..1) or (0..max)
*
* Implementation is based on sanitization performed by browsers (compared
* against Chrome 68 and Firefox 61).
*/
class HTMLPurifier_AttrTransform_HTML5_Progress extends HTMLPurifier_AttrTransform
{
/**
* @param array $attr
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
*/
public function transform($attr, $config, $context)
{
$max = isset($attr['max']) ? (float) $attr['max'] : 1;
if ($max <= 0) {
$this->confiscateAttr($attr, 'max');
}
if (isset($attr['value'])) {
$value = (float) $attr['value'];
if ($value < 0) {
$this->confiscateAttr($attr, 'value');
} elseif ($value > $max) {
$attr['value'] = isset($attr['max']) ? $attr['max'] : 1;
}
}
return $attr;
}
}
@@ -0,0 +1,23 @@
<?php
class HTMLPurifier_AttrTransform_HTML5_Script extends HTMLPurifier_AttrTransform
{
/**
* @param array $attr
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
*/
public function transform($attr, $config, $context)
{
// If 'src' is specified, it must be a valid non-empty URL potentially
// surrounded by spaces.
// If 'src' is present, regardless it's empty or not, script text is
// ignored by browsers.
if (isset($attr['src']) && trim($attr['src']) === '') {
unset($attr['src']);
}
return $attr;
}
}
@@ -0,0 +1,70 @@
<?php
abstract class HTMLPurifier_ChildDef_HTML5_Abstract extends HTMLPurifier_ChildDef
{
/**
* @var array
*/
protected $allowedElements = array();
/**
* @var boolean
*/
protected $init = false;
/**
* @param string $type
* @throws HTMLPurifier_Exception
*/
public function __construct($type = null)
{
if ($type) {
$this->type = $type;
}
// Ensure that the type property is not empty, otherwise the element
// will be treated as having an Empty content model (closing tag will
// be omitted) if no children are present.
if (empty($this->type)) {
throw new HTMLPurifier_Exception("The 'type' property is not initialized");
}
}
/**
* @param HTMLPurifier_Config $config
* @return array
*/
public function getAllowedElements($config)
{
$this->init($config);
return $this->allowedElements;
}
/**
* @param HTMLPurifier_Config $config
* @return void
*/
protected function init(HTMLPurifier_Config $config)
{
if ($this->init) {
return;
}
$def = $config->getHTMLDefinition();
$elements = array();
foreach ($this->elements as $name => $_) {
if (is_int($name)) {
$name = $_;
}
if (isset($def->info_content_sets[$name])) {
$elements = array_merge($elements, $def->info_content_sets[$name]);
} else {
$elements[$name] = true;
}
}
$this->allowedElements = $elements;
$this->init = true;
}
}
@@ -1,6 +1,6 @@
<?php
class HTMLPurifier_ChildDef_Details extends HTMLPurifier_ChildDef
class HTMLPurifier_ChildDef_HTML5_Details extends HTMLPurifier_ChildDef
{
public $type = 'details';
@@ -33,52 +33,53 @@ class HTMLPurifier_ChildDef_Details extends HTMLPurifier_ChildDef
* @param array $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
* @return array|bool
*/
public function validateChildren($children, $config, $context)
{
if (empty($children)) {
return false;
}
// if summary is not allowed, delete parent node
if (!isset($config->getHTMLDefinition()->info['summary'])) {
trigger_error("Cannot allow details without allowing summary", E_USER_WARNING);
return false;
}
$summary = null;
$result = array();
$summary = array();
$spaces = array();
$others = array();
while ($children) {
$child = reset($children);
if ($child instanceof HTMLPurifier_Node_Text && $child->is_whitespace) {
$spaces[] = array_shift($children);
} else {
break;
}
}
// Content model:
// One summary element followed by flow content
foreach ($children as $node) {
if (!$summary && $node->name === 'summary') {
$summary = $node;
$summary[] = $node;
continue;
}
if ($node->name === 'summary') {
// duplicated summary, add only its children
$result = array_merge($result, (array) $node->children);
$others = array_merge($others, (array) $node->children);
} else {
$result[] = $node;
$others[] = $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) {
// remove empty <details> without <summary>
if (!$others) {
return false;
}
$summary = new HTMLPurifier_Node_Element('summary');
$summary[] = new HTMLPurifier_Node_Element('summary');
}
array_unshift($result, $summary);
return $result;
return array_merge($spaces, $summary, $others);
}
}
@@ -0,0 +1,77 @@
<?php
class HTMLPurifier_ChildDef_HTML5_Fieldset extends HTMLPurifier_ChildDef
{
public $type = 'fieldset';
public $elements = array(
'legend' => 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|bool
*/
public function validateChildren($children, $config, $context)
{
$children = (array) $children;
// Content model:
// An optional <legend> element, followed by flow content.
// Only one legend element should occur in the content and if present
// should only be preceded by whitespace.
// https://www.w3.org/TR/xhtml1/dtds.html
$legend = array();
$spaces = array();
$others = array();
while ($children) {
$child = reset($children);
if ($child instanceof HTMLPurifier_Node_Text && $child->is_whitespace) {
$spaces[] = array_shift($children);
} else {
break;
}
}
foreach ($children as $node) {
if (!$legend && $node->name === 'legend') {
$legend[] = $node;
continue;
}
if ($node->name === 'legend') {
// duplicated <legend>, add only its children
$others = array_merge($others, (array) $node->children);
} else {
$others[] = $node;
}
}
return array_merge($spaces, $legend, $others);
}
}
@@ -1,6 +1,6 @@
<?php
class HTMLPurifier_ChildDef_Figure extends HTMLPurifier_ChildDef
class HTMLPurifier_ChildDef_HTML5_Figure extends HTMLPurifier_ChildDef
{
public $type = 'figure';
@@ -33,11 +33,10 @@ class HTMLPurifier_ChildDef_Figure extends HTMLPurifier_ChildDef
* @param array $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
* @return array|bool
*/
public function validateChildren($children, $config, $context)
{
$allowFigcaption = isset($config->getHTMLDefinition()->info['figcaption']);
$hasFigcaption = false;
$figcaptionPos = -1;
@@ -48,20 +47,14 @@ class HTMLPurifier_ChildDef_Figure extends HTMLPurifier_ChildDef
// 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
// Scan through children, accept at most one figcaption.
foreach ($children as $node) {
if ($node->name === 'figcaption') {
if ($allowFigcaption && !$hasFigcaption) {
if (!$hasFigcaption) {
$hasFigcaption = true;
$figcaptionPos = count($result);
$result[] = $node;
continue;
}
$div = new HTMLPurifier_Node_Element('div', $node->attr);
$div->children = $node->children;
$result[] = $div;
continue;
}
@@ -74,16 +67,6 @@ class HTMLPurifier_ChildDef_Figure extends HTMLPurifier_ChildDef
$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;
}
}
@@ -1,6 +1,6 @@
<?php
class HTMLPurifier_ChildDef_Media extends HTMLPurifier_ChildDef
class HTMLPurifier_ChildDef_HTML5_Media extends HTMLPurifier_ChildDef
{
public $type = 'media';
@@ -38,7 +38,7 @@ class HTMLPurifier_ChildDef_Media extends HTMLPurifier_ChildDef
* @param array $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
* @return array|bool
*/
public function validateChildren($children, $config, $context)
{
@@ -1,6 +1,6 @@
<?php
class HTMLPurifier_ChildDef_Picture extends HTMLPurifier_ChildDef
class HTMLPurifier_ChildDef_HTML5_Picture extends HTMLPurifier_ChildDef
{
public $type = 'picture';
@@ -13,7 +13,7 @@ class HTMLPurifier_ChildDef_Picture extends HTMLPurifier_ChildDef
* @param array $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return array
* @return array|bool
*/
public function validateChildren($children, $config, $context)
{
@@ -27,16 +27,14 @@ class HTMLPurifier_ChildDef_Picture extends HTMLPurifier_ChildDef
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') {
if ($node->name === 'source' || $node->name === 'img') {
$result[] = $node;
}
if ($node->name === 'img') {
@@ -0,0 +1,81 @@
<?php
class HTMLPurifier_ChildDef_HTML5_Script extends HTMLPurifier_ChildDef
{
public $type = 'script';
/**
* Whether children (text contents) are allowed
* @var bool
*/
public $allow_children = true;
/**
* @param HTMLPurifier_Node[] $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return HTMLPurifier_Node[]|bool
*/
public function validateChildren($children, $config, $context)
{
$node = $context->exists('CurrentNode')
? $context->get('CurrentNode')
: null;
// Content model:
// If there is no src attribute, depends on the value of the type
// attribute, but must match script content restrictions.
// If there is a src attribute, the element must be either empty
// or contain only script documentation that also matches script
// content restrictions.
// https://html.spec.whatwg.org/multipage/scripting.html#the-script-element
if ($node instanceof HTMLPurifier_Node_Element) {
// must validate src attribute here, because children validation is
// executed before attribute validation
// This part I don't like, but currently it's unavoidable because
// of how HTMLPurifier works internally. Attribute transformations
// and validations are done after children validation. So there is
// no way of knowing whether src attribute is valid other than
// do the validation here as well.
$src = $this->getSrc($node, $config, $context);
if (strlen($src)) {
return array();
}
// Remove <script> if there is no 'src' attribute and no children
// or if children are explicitly forbidden
if (empty($children) || !$this->allow_children) {
return false;
}
}
return $this->allow_children ? true : array();
}
/**
* @param HTMLPurifier_Node_Element $element
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return string
*/
protected function getSrc(HTMLPurifier_Node_Element $element, HTMLPurifier_Config $config, HTMLPurifier_Context $context)
{
$src = isset($element->attr['src']) ? trim($element->attr['src']) : '';
if (strlen($src)) {
$info = $config->getHTMLDefinition()->info['script'];
if (isset($info->attr['src'])) {
/** @var HTMLPurifier_AttrDef $srcAttrDef */
$srcAttrDef = $info->attr['src'];
$result = $srcAttrDef->validate($src, $config, $context);
$src = $result === true ? $src : $result;
}
}
return $src;
}
}
@@ -0,0 +1,68 @@
<?php
/**
* Definition for <time> element contents
*
* As a side effect for child validation it ensures that 'datetime' attribute
* is present if text content of the children is not a valid datetime string.
*
* @see https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-time-element
*/
class HTMLPurifier_ChildDef_HTML5_Time extends HTMLPurifier_ChildDef_HTML5_Abstract
{
public $type = 'time';
public $allow_empty = true;
public $elements = array(
'Inline' => true,
);
/**
* @param HTMLPurifier_Node[] $children
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool|HTMLPurifier_Node[]
*/
public function validateChildren($children, $config, $context)
{
$currentNode = $context->get('CurrentNode', true);
if ($currentNode instanceof HTMLPurifier_Node_Element) {
// Unfortunately at this point invalid 'datetime' attribute is not
// yet removed, so we need to validate it here
/** @var HTMLPurifier_AttrDef $attr */
$attr = $config->getHTMLDefinition()->info['time']->attr['datetime'];
$datetime = isset($currentNode->attr['datetime'])
? $attr->validate($currentNode->attr['datetime'], $config, $context)
: false;
// If datetime attribute is invalid, we need to check whether element's
// contents are a valid datetime string. If not, add a dummy datetime
// attribute with UNIX epoch date, to satisfy the spec requirements.
// This can't be done in the AttrTransform step, because CurrentNode is
// not available there.
if ($datetime === false) {
$textContent = '';
foreach ($currentNode->children as $child) {
if ($child instanceof HTMLPurifier_Node_Element) {
$textContent = '';
break;
} elseif ($child instanceof HTMLPurifier_Node_Text) {
$textContent .= $child->data;
}
}
if (!$attr->validate($textContent, $config, $context)) {
$currentNode->attr['datetime'] = '1970-01-01';
}
}
}
return $children;
}
}
@@ -2,12 +2,12 @@
class HTMLPurifier_HTML5Config extends HTMLPurifier_Config
{
const REVISION = 2018060702;
const REVISION = 2019080701;
/**
* @param string|array|HTMLPurifier_Config $config
* @param HTMLPurifier_ConfigSchema $schema
* @return HTMLPurifier_Config
* @param HTMLPurifier_ConfigSchema $schema OPTIONAL
* @return HTMLPurifier_HTML5Config
*/
public static function create($config, $schema = null)
{
@@ -21,9 +21,6 @@ class HTMLPurifier_HTML5Config extends HTMLPurifier_Config
}
$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);
@@ -40,26 +37,49 @@ class HTMLPurifier_HTML5Config extends HTMLPurifier_Config
/**
* Creates a configuration object using the default config schema instance
*
* @return HTMLPurifier_Config
* @return HTMLPurifier_HTML5Config
*/
public static function createDefault()
{
$schema = HTMLPurifier_ConfigSchema::instance();
$config = self::create(null, $schema);
return $config;
return self::create(null);
}
/**
* Creates a new config object that inherits from a previous one
*
* @param HTMLPurifier_Config $config
* @return HTMLPurifier_Config
* @return HTMLPurifier_HTML5Config
*/
public static function inherit(HTMLPurifier_Config $config)
{
return new self($config->def, $config->plist);
}
/**
* @param HTMLPurifier_ConfigSchema $schema
* @param HTMLPurifier_PropertyList $parent OPTIONAL
*/
public function __construct(HTMLPurifier_ConfigSchema $schema, HTMLPurifier_PropertyList $parent = null)
{
// ensure 'HTML5' is among allowed 'HTML.Doctype' values
$doctypeConfig = $schema->info['HTML.Doctype'];
if (empty($doctypeConfig->allowed['HTML5'])) {
$allowed = array_merge($doctypeConfig->allowed, array('HTML5' => true));
$schema->addAllowedValues('HTML.Doctype', $allowed);
}
if (empty($schema->info['HTML.IframeAllowFullscreen'])) {
$schema->add('HTML.IframeAllowFullscreen', false, 'bool', false);
}
parent::__construct($schema, $parent);
$this->set('HTML.Doctype', 'HTML5');
$this->set('Attr.ID.HTML5', true);
$this->set('Output.CommentScriptContents', false);
}
public function getDefinition($type, $raw = false, $optimized = false)
{
// Setting HTML.* keys removes any previously instantiated HTML
@@ -67,7 +87,8 @@ class HTMLPurifier_HTML5Config extends HTMLPurifier_Config
$needSetup = $type === 'HTML' && !isset($this->definitions[$type]);
if ($needSetup) {
if ($def = parent::getDefinition($type, true, true)) {
HTMLPurifier_HTML5Definition::setup($def);
/** @var HTMLPurifier_HTMLDefinition $def */
HTMLPurifier_HTML5Definition::setupDefinition($def);
}
}
return parent::getDefinition($type, $raw, $optimized);
@@ -7,111 +7,43 @@ class HTMLPurifier_HTML5Definition
*
* @param HTMLPurifier_HTMLDefinition $def
* @return HTMLPurifier_HTMLDefinition
* @throws HTMLPurifier_Exception
*/
public static function setup(HTMLPurifier_HTMLDefinition $def)
public static function setupDefinition(HTMLPurifier_HTMLDefinition $def)
{
$def->manager->doctypes->register(
'HTML5',
false,
// Order of modules is important - the latter ones override the former.
// Place common HTML5 modules at the end of the list
array(
'CommonAttributes', 'HTML5_Text', 'HTML5_Hypertext', 'HTML5_List',
'Presentation', 'HTML5_Edit', 'HTML5_Bdo', 'Tables', 'Image',
'StyleAttribute', 'HTML5_Media', 'HTML5_Ruby', 'Name',
'NonXMLCommonAttributes',
// Unsafe:
'HTML5_Scripting', 'HTML5_Interactive', 'Object', 'HTML5_Forms',
'HTML5_Iframe',
),
array('Tidy_Transitional', 'Tidy_Proprietary'),
array()
);
// override default SafeScripting module
// Because how the built-in SafeScripting module is enabled in ModuleManager,
// to override it exactly the same name must be provided (without HTML5_ prefix)
$safeScripting = new HTMLPurifier_HTMLModule_HTML5_SafeScripting();
$safeScripting->name = 'SafeScripting';
$def->manager->registerModule($safeScripting);
// 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');
// add support for Floating point number attributes
$def->manager->attrTypes->set('Float', new HTMLPurifier_AttrDef_Float());
// 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');
$def->manager->attrTypes->set('Datetime', new HTMLPurifier_AttrDef_HTML5_Datetime());
return $def;
}
@@ -0,0 +1,44 @@
<?php
/**
* HTML5 Bi-directional text
*/
class HTMLPurifier_HTMLModule_HTML5_Bdo extends HTMLPurifier_HTMLModule
{
/**
* @type string
*/
public $name = 'HTML5_Bdo';
/**
* @type array
*/
public $attr_collections = array(
'I18N' => array(
'dir' => 'Enum#ltr,rtl,auto',
),
);
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
// Bidirectional Text Override element
// https://www.w3.org/TR/html50/text-level-semantics.html#the-bdo-element
$bdo = $this->addElement(
'bdo',
'Inline',
'Inline',
array('Core', 'Lang'),
array(
'dir' => 'Enum#ltr,rtl', // required, the 'auto' value must not be specified
)
);
$bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir();
// Bidirectional Isolate element
// https://www.w3.org/TR/html50/text-level-semantics.html#the-bdi-element
$this->addElement('bdi', 'Inline', 'Inline', 'Common');
}
}
@@ -0,0 +1,32 @@
<?php
/**
* HTML5 extension to Edit Module
* http://developers.whatwg.org/edits.html
*
* @property HTMLPurifier_ElementDef[] $info
*/
class HTMLPurifier_HTMLModule_HTML5_Edit extends HTMLPurifier_HTMLModule_Edit
{
/**
* @type string
*/
public $name = 'HTML5_Edit';
/**
* @param HTMLPurifier_Config $config
* @throws HTMLPurifier_Exception
*/
public function setup($config)
{
parent::setup($config);
$editDatetime = new HTMLPurifier_AttrDef_HTML5_Datetime(array('Date', 'DatetimeGlobal'));
// https://html.spec.whatwg.org/dev/edits.html#the-ins-element
$this->info['ins']->attr['datetime'] = $editDatetime;
// https://html.spec.whatwg.org/dev/edits.html#the-del-element
$this->info['del']->attr['datetime'] = $editDatetime;
}
}
@@ -0,0 +1,69 @@
<?php
/**
* HTML5 additions to built-in Forms module
*
* This module is marked as safe to support static elements like <progress>
* out of the box. Only elements inherited from parent module are unsafe,
* and enabled conditionally with %HTML.Trusted flag.
*/
class HTMLPurifier_HTMLModule_HTML5_Forms extends HTMLPurifier_HTMLModule_Forms
{
public $name = 'HTML5_Forms';
public $safe = true;
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
if ($config->get('HTML.Trusted')) {
parent::setup($config);
// https://html.spec.whatwg.org/multipage/forms.html#the-form-element
$form = $this->addElement(
'form',
'Form',
'Flow',
'Common',
array(
'accept-charset' => 'Charsets',
'action' => 'URI',
'method' => 'Enum#get,post',
'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data,text/plain',
'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget(),
)
);
$form->excludes = array('form' => true);
$this->addElement(
'fieldset',
'Form',
new HTMLPurifier_ChildDef_HTML5_Fieldset(),
'Common',
array(
'name' => 'CDATA',
'disabled' => 'Bool#disabled',
// 'form' => 'IDREF', // IDREF not implemented, cannot allow
)
);
}
// https://html.spec.whatwg.org/dev/form-elements.html#the-progress-element
$progress = $this->addElement(
'progress',
'Flow',
'Inline',
'Common',
array(
'value' => 'Float#min:0',
'max' => 'Float#min:0',
)
);
$progress->excludes = array('progress' => true);
$this->addElementToContentSet('progress', 'Inline');
$progress->attr_transform_post[] = new HTMLPurifier_AttrTransform_HTML5_Progress();
}
}
@@ -0,0 +1,31 @@
<?php
/**
* HTML5 compliant replacement for {@link HTMLPurifier_HTMLModule_Hypertext},
* defining block-level hypertext links.
*/
class HTMLPurifier_HTMLModule_HTML5_Hypertext extends HTMLPurifier_HTMLModule
{
/**
* @type string
*/
public $name = 'HTML5_Hypertext';
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
// https://html.spec.whatwg.org/dev/text-level-semantics.html#the-a-element
$a = $this->addElement('a', 'Flow', 'Flow', 'Common', array(
'download' => 'Text',
'href' => 'URI',
'hreflang' => 'Text', // 'LanguageCode',
'rel' => new HTMLPurifier_AttrDef_HTML5_ARel(),
'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget(),
'type' => 'Text',
));
$a->excludes = array('a' => true);
$this->addElementToContentSet('a', 'Inline');
}
}
@@ -0,0 +1,60 @@
<?php
/**
* HTML5 compliant replacement for {@link HTMLPurifier_HTMLModule_Iframe}
*
* This module is not considered safe unless an Iframe whitelisting mechanism
* is specified. Currently, the only such mechanism is %URL.SafeIframeRegexp
*/
class HTMLPurifier_HTMLModule_HTML5_Iframe extends HTMLPurifier_HTMLModule
{
public $name = 'HTML5_Iframe';
/**
* @type bool
*/
public $safe = false;
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
if ($config->get('HTML.SafeIframe')) {
$this->safe = true;
}
// HTML Living Standard does not allow content in iframes, whereas W3C
// spec does. On the other hand W3C validator follows WHATWG spec.
// See:
// - https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element
// - https://www.w3.org/TR/html52/semantics-embedded-content.html#the-iframe-element
// - https://www.w3.org/TR/html50/embedded-content-0.html#the-iframe-element
// type must not be 'empty', otherwise <iframe> will not have an end tag
$iframeContents = new HTMLPurifier_ChildDef_Empty();
$iframeContents->type = 'iframe';
$iframe = $this->addElement(
'iframe',
'Inline',
$iframeContents,
'Common',
array(
'src' => 'URI#embedded',
'width' => 'Length',
'height' => 'Length',
'name' => 'ID',
// other attributes that are present in HTML4 / XHTML spec were
// declared as non-conforming, and as such are not included here
// https://www.w3.org/TR/2016/WD-html52-20161206/obsolete.html#non-conforming-features
)
);
if (isset($config->def->info['HTML.IframeAllowFullscreen']) &&
$config->get('HTML.IframeAllowFullscreen')
) {
$iframe->attr['allowfullscreen'] = 'Bool#allowfullscreen';
}
}
}
@@ -0,0 +1,34 @@
<?php
/**
* HTML5 Interactive elements module
* https://html.spec.whatwg.org/dev/interactive-elements.html
*/
class HTMLPurifier_HTMLModule_HTML5_Interactive extends HTMLPurifier_HTMLModule
{
/**
* @type string
*/
public $name = 'HTML5_Interactive';
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
// https://html.spec.whatwg.org/dev/interactive-elements.html#the-details-element
$this->addElement('details', 'Flow', new HTMLPurifier_ChildDef_HTML5_Details(), 'Common', array(
'open' => 'Bool',
));
// https://html.spec.whatwg.org/dev/interactive-elements.html#the-summary-element
$this->addElement('summary', false, 'Flow', 'Common');
// https://html.spec.whatwg.org/dev/interactive-elements.html#the-dialog-element
$dialog = $this->addElement('dialog', 'Flow', 'Flow', 'Common', array(
'open' => 'Bool',
));
$dialog->attr_transform_pre[] = new HTMLPurifier_AttrTransform_HTML5_Dialog();
}
}
@@ -0,0 +1,30 @@
<?php
/**
* HTML5 extension to {@link HTMLPurifier_HTMLModule_List}
*
* @property HTMLPurifier_ElementDef[] $info
*/
class HTMLPurifier_HTMLModule_HTML5_List extends HTMLPurifier_HTMLModule_List
{
/**
* @type string
*/
public $name = 'HTML5_List';
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
parent::setup($config);
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-ol-element
$ol = $this->info['ol'];
$ol->attr['reversed'] = 'Bool#reversed';
// Attributes that were deprecated in HTML4, but reintroduced in HTML5
$ol->attr['start'] = new HTMLPurifier_AttrDef_Integer();
$ol->attr['type'] = 'Enum#s:1,a,A,i,I';
}
}
@@ -0,0 +1,71 @@
<?php
/**
* HTML5 Multimedia and embedded content
*
* https://html.spec.whatwg.org/dev/media.html
* https://html.spec.whatwg.org/dev/embedded-content.html
*/
class HTMLPurifier_HTMLModule_HTML5_Media extends HTMLPurifier_HTMLModule
{
/**
* @type string
*/
public $name = 'HTML5_Media';
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
$mediaContent = new HTMLPurifier_ChildDef_HTML5_Media();
// https://html.spec.whatwg.org/dev/media.html#the-video-element
$this->addElement('video', 'Flow', $mediaContent, 'Common', array(
'controls' => 'Bool',
'height' => 'Length',
'poster' => 'URI',
'preload' => 'Enum#auto,metadata,none',
'src' => 'URI',
'width' => 'Length',
));
$this->addElementToContentSet('video', 'Inline');
// https://html.spec.whatwg.org/dev/media.html#the-audio-element
$this->addElement('audio', 'Flow', $mediaContent, 'Common', array(
'controls' => 'Bool',
'preload' => 'Enum#auto,metadata,none',
'src' => 'URI',
));
$this->addElementToContentSet('audio', 'Inline');
// https://html.spec.whatwg.org/dev/embedded-content.html#the-source-element
$this->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
$this->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
$this->addElement('picture', 'Flow', new HTMLPurifier_ChildDef_HTML5_Picture(), 'Common');
$this->addElementToContentSet('picture', 'Inline');
// https://html.spec.whatwg.org/dev/embedded-content.html#the-img-element
$img = $this->addBlankElement('img');
$img->attr = array(
'srcset' => 'Text',
'sizes' => 'Text',
);
}
}
@@ -0,0 +1,36 @@
<?php
/**
* HTML 5.2 Ruby markup
* https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-ruby-element
*
* Note: {@link HTMLPurifier_HTMLModule_Ruby} implementation is based on
* XHTML 1.1 Ruby Annotation module which differs from HTML5 spec.
*/
class HTMLPurifier_HTMLModule_HTML5_Ruby extends HTMLPurifier_HTMLModule
{
/**
* @type string
*/
public $name = 'HTML5_Ruby';
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
$this->addElement(
'ruby',
'Inline',
'Custom: ((rb | Inline | #PCDATA)*, (rt | (rp, rt, rp) | rtc))+',
'Common'
);
$this->addElement('rtc', false, 'Custom: (rt | rp | Inline | #PCDATA)*', 'Common');
$this->addElement('rb', false, 'Custom: (Inline | #PCDATA)*', 'Common');
$this->addElement('rt', false, 'Custom: (Inline | #PCDATA)*', 'Common');
$this->addElement('rp', false, 'Custom: (Inline | #PCDATA)*', 'Common');
// <ruby> elements can be nested as children of <rtc>, <rb>, <rt> and <rp>
// https://www.w3.org/TR/2014/NOTE-html-ruby-extensions-20140204/#changes-compared-to-the-current-ruby-model
}
}
@@ -0,0 +1,46 @@
<?php
/**
* A "safe" script module. No inline JS is allowed, and pointed to JS
* files must match whitelist.
*/
class HTMLPurifier_HTMLModule_HTML5_SafeScripting extends HTMLPurifier_HTMLModule
{
/**
* @type string
*/
public $name = 'HTML5_SafeScripting';
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
// These definitions are not intrinsically safe: the attribute transforms
// are a vital part of ensuring safety.
$allowed = $config->get('HTML.SafeScripting');
$scriptContents = new HTMLPurifier_ChildDef_HTML5_Script();
$scriptContents->allow_children = false;
$script = $this->addElement(
'script',
'Inline',
$scriptContents,
null,
array(
'src' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed), true),
'type' => new HTMLPurifier_AttrDef_Enum(array(
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#textjavascript
'text/javascript',
)),
'async' => new HTMLPurifier_AttrDef_HTML_Bool2(),
'defer' => new HTMLPurifier_AttrDef_HTML_Bool2(),
'charset' => 'Enum#utf-8',
)
);
$script->attr_transform_pre[] = new HTMLPurifier_AttrTransform_HTML5_Script();
}
}
@@ -0,0 +1,49 @@
<?php
/*
* HTML5 Scripting
*
* WARNING: THIS MODULE IS EXTREMELY DANGEROUS AS IT ENABLES INLINE SCRIPTING
* INSIDE HTML PURIFIER DOCUMENTS. USE ONLY WITH TRUSTED USER INPUT!!!
*
* https://www.w3.org/TR/html50/scripting-1.html
*/
class HTMLPurifier_HTMLModule_HTML5_Scripting extends HTMLPurifier_HTMLModule
{
/**
* @type string
*/
public $name = 'HTML5_Scripting';
/**
* @type bool
*/
public $safe = false;
/**
* @param HTMLPurifier_Config $config
*/
public function setup($config)
{
$noscript = $this->addElement('noscript', 'Flow', 'Required: Flow | #PCDATA', 'Common');
$noscript->excludes = array('noscript' => true);
$this->addElementToContentSet('noscript', 'Inline');
$scriptContents = new HTMLPurifier_ChildDef_HTML5_Script();
$script = $this->addElement('script', 'Flow', $scriptContents, null, array(
'src' => new HTMLPurifier_AttrDef_URI(true),
'type' => new HTMLPurifier_AttrDef_Enum(array(
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#textjavascript
'text/javascript',
)),
'async' => new HTMLPurifier_AttrDef_HTML_Bool2(),
'defer' => new HTMLPurifier_AttrDef_HTML_Bool2(),
// If present, its value must be an ASCII case-insensitive match for "utf-8"
// Deprecated: https://html.spec.whatwg.org/multipage/scripting.html#the-script-element
'charset' => 'Enum#utf-8',
));
$this->addElementToContentSet('script', 'Inline');
$script->attr_transform_pre[] = new HTMLPurifier_AttrTransform_HTML5_Script();
}
}
@@ -0,0 +1,91 @@
<?php
/**
* Extension to {@link HTMLPurifier_HTMLModule_Text} defining HTML5 text-level
* and grouping elements.
*/
class HTMLPurifier_HTMLModule_HTML5_Text extends HTMLPurifier_HTMLModule_Text
{
/**
* @type string
*/
public $name = 'HTML5_Text';
public $content_sets = array(
'Flow' => 'Heading | Block | Inline | Sectioning'
);
/**
* @param HTMLPurifier_Config $config
* @throws HTMLPurifier_Exception
*/
public function setup($config)
{
parent::setup($config);
// http://developers.whatwg.org/sections.html
$this->addElement('section', 'Sectioning', 'Flow', 'Common');
$this->addElement('nav', 'Sectioning', 'Flow', 'Common');
$this->addElement('article', 'Sectioning', 'Flow', 'Common');
$this->addElement('aside', 'Sectioning', 'Flow', 'Common');
// https://html.spec.whatwg.org/dev/sections.html#the-header-element
$header = $this->addElement('header', 'Block', 'Flow', 'Common');
$header->excludes = $this->makeLookup('header', 'footer', 'main');
// https://html.spec.whatwg.org/dev/sections.html#the-footer-element
$footer = $this->addElement('footer', 'Block', 'Flow', 'Common');
$footer->excludes = $this->makeLookup('header', 'footer', 'main');
// https://html.spec.whatwg.org/dev/sections.html#the-address-element
$address = $this->addElement('address', 'Block', 'Flow', 'Common');
$address->excludes = $this->makeLookup(
// no heading content
// https://html.spec.whatwg.org/dev/dom.html#heading-content
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hgroup',
// no sectioning content
// https://html.spec.whatwg.org/dev/dom.html#sectioning-content
'article', 'aside', 'nav', 'section',
// no header, footer and address
'address', 'footer', 'header'
);
// https://html.spec.whatwg.org/dev/sections.html#the-hgroup-element
$this->addElement('hgroup', 'Heading', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common');
// https://html.spec.whatwg.org/dev/grouping-content.html#the-main-element
$this->addElement('main', 'Block', 'Flow', 'Common');
// https://html.spec.whatwg.org/dev/grouping-content.html#the-figure-element
$this->addElement('figure', 'Block', new HTMLPurifier_ChildDef_HTML5_Figure(), 'Common');
$this->addElement('figcaption', false, 'Flow', 'Common');
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-blockquote-element
$this->addElement('blockquote', 'Block', 'Flow', 'Common', array(
'cite' => 'URI',
));
// http://developers.whatwg.org/text-level-semantics.html
$this->addElement('s', 'Inline', 'Inline', 'Common');
$this->addElement('u', 'Inline', 'Inline', 'Common');
$this->addElement('var', 'Inline', 'Inline', 'Common');
$this->addElement('sub', 'Inline', 'Inline', 'Common');
$this->addElement('sup', 'Inline', 'Inline', 'Common');
$this->addElement('mark', 'Inline', 'Inline', 'Common');
$this->addElement('wbr', 'Inline', 'Empty', 'Core');
// https://html.spec.whatwg.org/dev/text-level-semantics.html#the-time-element
// https://w3c.github.io/html-reference/datatypes.html#common.data.time-datetime-def
// Composite attr def is sufficiently general to be used in non-CSS contexts
$timeDatetime = new HTMLPurifier_AttrDef_CSS_Composite(array(
new HTMLPurifier_AttrDef_HTML5_Datetime(),
new HTMLPurifier_AttrDef_HTML5_YearlessDate(),
new HTMLPurifier_AttrDef_HTML5_Week(),
new HTMLPurifier_AttrDef_HTML5_Duration(),
));
$timeContents = new HTMLPurifier_ChildDef_HTML5_Time();
$this->addElement('time', 'Inline', $timeContents, 'Common', array(
'datetime' => $timeDatetime,
));
}
}