[ '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; } }