[ 'srcset', 'sizes', 'decoding', 'fetchpriority' ],
'video' => [ 'controlslist', 'disablepictureinpicture' ],
'meta' => [],
'picture' => [],
'source' => [ 'src', 'srcset', 'sizes', 'type', 'media', 'width', 'height' ],
'noscript' => [],
'canvas' => [ 'width', 'height' ],
'iframe' => [ 'src', 'width', 'height', 'name', 'frameborder', 'allow', 'allowfullscreen', 'loading', 'referrerpolicy', 'sandbox' ],
'form' => [ 'action', 'method', 'name', 'target', 'novalidate', 'autocomplete', 'enctype', 'accept-charset' ],
'input' => [ 'type', 'name', 'value', 'placeholder', 'required', 'checked', 'disabled', 'readonly', 'min', 'max', 'step', 'pattern', 'autocomplete', 'size', 'maxlength', 'minlength', 'multiple', 'accept', 'inputmode', 'list' ],
'select' => [ 'name', 'required', 'disabled', 'multiple', 'size', 'autocomplete' ],
'option' => [ 'value', 'selected', 'disabled', 'label' ],
'optgroup' => [ 'label', 'disabled' ],
'textarea' => [ 'name', 'rows', 'cols', 'placeholder', 'required', 'disabled', 'readonly', 'maxlength', 'minlength', 'autocomplete' ],
'datalist' => [],
'output' => [ 'for', 'name' ],
'svg' => [ 'xmlns', 'xmlns:xlink', 'viewbox', 'width', 'height', 'x', 'y', 'version', 'preserveaspectratio', 'focusable', 'xml:space' ],
'g' => [],
'path' => [ 'd' ],
'circle' => [ 'cx', 'cy', 'r' ],
'ellipse' => [ 'cx', 'cy', 'rx', 'ry' ],
'rect' => [ 'x', 'y', 'width', 'height', 'rx', 'ry' ],
'line' => [ 'x1', 'y1', 'x2', 'y2' ],
'polyline' => [ 'points' ],
'polygon' => [ 'points' ],
'defs' => [],
'symbol' => [ 'viewbox', 'preserveaspectratio' ],
'use' => [ 'href', 'xlink:href', 'x', 'y', 'width', 'height' ],
'title' => [],
'desc' => [],
'lineargradient' => [ 'x1', 'y1', 'x2', 'y2', 'gradientunits', 'gradienttransform' ],
'radialgradient' => [ 'cx', 'cy', 'r', 'fx', 'fy', 'gradientunits', 'gradienttransform' ],
'stop' => [ 'offset', 'stop-color', 'stop-opacity' ],
'clippath' => [ 'clippathunits' ],
'text' => [ 'x', 'y', 'dx', 'dy', 'text-anchor', 'font-family', 'font-size', 'font-weight' ],
'tspan' => [ 'x', 'y', 'dx', 'dy' ],
'textpath' => [ 'href', 'xlink:href', 'startoffset' ],
];
/**
* SVG elements that take the presentation attributes.
*
* @var array
*/
const SVG_SHAPES = [ 'svg', 'g', 'path', 'circle', 'ellipse', 'rect', 'line', 'polyline', 'polygon', 'use', 'text', 'tspan', 'textpath' ];
/**
* Handle of the registered style that carries the CSS of the style elements.
*
* @var string
*/
const STYLE_HANDLE = 'headfoot-inline';
/**
* The list of permitted elements, built once per request.
*
* @var array|null
*/
private static $allowed = null;
/**
* Returns the elements and attributes that pass.
*
* @return array element => [ attribute => true ]
*/
public static function allowed_html() {
if ( null !== self::$allowed ) {
return self::$allowed;
}
$allowed = wp_kses_allowed_html( 'post' );
foreach ( self::ELEMENTS as $element => $attributes ) {
$current = isset( $allowed[ $element ] ) && is_array( $allowed[ $element ] ) ? $allowed[ $element ] : [];
$allowed[ $element ] = $current + array_fill_keys( $attributes, true );
}
foreach ( self::SVG_SHAPES as $element ) {
$allowed[ $element ] += array_fill_keys( self::SVG_ATTRIBUTES, true );
}
$global = array_fill_keys( self::GLOBAL_ATTRIBUTES, true );
foreach ( $allowed as $element => $attributes ) {
$allowed[ $element ] = ( is_array( $attributes ) ? $attributes : [] ) + $global;
}
/**
* Filters the elements and attributes that pass in template markup.
*
* @param array $allowed element => [ attribute => true ], with all names in lowercase.
*/
$allowed = apply_filters( 'headfoot_kses_allowed_html', $allowed );
self::$allowed = is_array( $allowed ) ? $allowed : [];
return self::$allowed;
}
/**
* Escapes template markup.
*
* The CSS of each style element goes to wp_add_inline_style(). The return
* value holds no style element.
*
* @param string $html Markup from Elementor.
* @return string
*/
public static function filter( $html ) {
$html = (string) $html;
if ( '' === $html ) {
return '';
}
// wp_kses() removes a tag but keeps the text inside it, so a script
// element goes away together with its source.
$without_scripts = preg_replace( '##is', '', $html );
if ( is_string( $without_scripts ) ) {
$html = $without_scripts;
}
// wp_kses() changes ">" and "&" in text, and that breaks CSS. The parts
// around each style element go through wp_kses() on their own.
$parts = preg_split( '##is', $html, -1, PREG_SPLIT_DELIM_CAPTURE );
if ( ! is_array( $parts ) ) {
return wp_kses( $html, self::allowed_html() );
}
$allowed = self::allowed_html();
$output = '';
foreach ( $parts as $index => $part ) {
if ( 0 === $index % 2 ) {
$output .= wp_kses( $part, $allowed );
continue;
}
// CSS without "<" cannot close the style element or open another one.
if ( '' !== trim( $part ) && false === strpos( $part, '<' ) ) {
self::add_inline_style( $part );
}
}
return $output;
}
/**
* Hands the CSS of a style element to WordPress.
*
* WordPress prints the CSS in the head when the call comes before wp_head(),
* and with the late styles in the footer after that. A handle that WordPress
* printed already takes no more CSS, so the next handle gets a number.
*
* @param string $css CSS without "<".
* @return void
*/
private static function add_inline_style( $css ) {
$handle = self::STYLE_HANDLE;
$count = 1;
while ( wp_style_is( $handle, 'done' ) ) {
++$count;
$handle = self::STYLE_HANDLE . '-' . $count;
}
if ( ! wp_style_is( $handle, 'registered' ) ) {
wp_register_style( $handle, false, [], HEADFOOT_VERSION );
}
wp_enqueue_style( $handle );
wp_add_inline_style( $handle, $css );
}
/**
* Clears the cached list. Tests use this.
*
* @return void
*/
public static function reset() {
self::$allowed = null;
}
}