*/
private const GLOBAL_ATTRIBUTES = array(
'aria-controls' => true,
'aria-current' => true,
'aria-describedby' => true,
'aria-details' => true,
'aria-expanded' => true,
'aria-hidden' => true,
'aria-label' => true,
'aria-labelledby' => true,
'aria-live' => true,
'aria-required' => true,
'aria-selected' => true,
'class' => true,
'data-*' => true,
'dir' => true,
'hidden' => true,
'id' => true,
'lang' => true,
'role' => true,
'style' => true,
'tabindex' => true,
'title' => true,
'xml:lang' => true,
);
/**
* Elements and attributes components print that the post allowlist lacks.
*
* @var array>
*/
private const COMPONENT_TAGS = array(
'a' => array(
'download' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'fill' => true,
'r' => true,
),
'form' => array(
'action' => true,
'autocomplete' => true,
'method' => true,
'name' => true,
),
'img' => array(
'decoding' => true,
'fetchpriority' => true,
'loading' => true,
'sizes' => true,
'srcset' => true,
),
'input' => array(
'autocapitalize' => true,
'autocomplete' => true,
'checked' => true,
'max' => true,
'maxlength' => true,
'min' => true,
'minlength' => true,
'name' => true,
'placeholder' => true,
'readonly' => true,
'required' => true,
'size' => true,
'spellcheck' => true,
'type' => true,
'value' => true,
),
'noscript' => array(),
'option' => array(
'disabled' => true,
'selected' => true,
'value' => true,
),
'path' => array(
'd' => true,
'fill' => true,
),
'rect' => array(
'fill' => true,
'height' => true,
'width' => true,
),
'select' => array(
'autocomplete' => true,
'name' => true,
'required' => true,
),
'svg' => array(
'height' => true,
'preserveaspectratio' => true,
'viewbox' => true,
'width' => true,
'xmlns' => true,
),
);
/**
* Elements kept in the image subset: an image, or the editor sample tile's SVG.
*
* @var array
*/
private const IMAGE_ELEMENTS = array( 'img', 'svg', 'rect', 'circle', 'path' );
/**
* Elements kept in the form subset: what wp_login_form() prints, plus plain markup and images.
*
* @var array
*/
private const FORM_ELEMENTS = array( 'a', 'br', 'button', 'div', 'form', 'img', 'input', 'label', 'p', 'span' );
/**
* The post allowlist the merge was built from.
*
* @var array|null
*/
private static ?array $postTags = null;
/**
* The post allowlist merged with the component tags, before the filter.
*
* @var array>
*/
private static array $merged = array();
/**
* Get the HTML that component output may hold.
*
* @return array> Elements and their attributes, as wp_kses() takes them.
*/
public static function allowed(): array {
return self::filtered( self::merged(), 'component' );
}
/**
* Get the subset for image markup: WordPress attachment and avatar images, or the sample tile.
*
* @return array>
*/
public static function image(): array {
return self::filtered( array_intersect_key( self::merged(), array_flip( self::IMAGE_ELEMENTS ) ), 'image' );
}
/**
* Get the subset for wp_login_form() markup.
*
* @return array>
*/
public static function form(): array {
return self::filtered( array_intersect_key( self::merged(), array_flip( self::FORM_ELEMENTS ) ), 'form' );
}
/**
* Remove script and style elements with their content.
*
* The kses passes remove these tags but keep their code as text.
*
* @param string $html Markup that WordPress or another plugin printed.
* @return string The markup without them, or unchanged if the pattern fails (kses still removes the tags).
*/
public static function stripElements( string $html ): string {
$stripped = preg_replace( '#<(script|style)(?=[\s/>])[^>]*>.*?\1(?=[\s/>])[^>]*>#is', '', $html );
return null !== $stripped ? $stripped : $html;
}
/**
* Get the post allowlist merged with the component tags.
*
* @return array>
*/
private static function merged(): array {
$post = wp_kses_allowed_html( 'post' );
$post = is_array( $post ) ? $post : array();
// The merge is costly, so redo it only when the post allowlist changes.
if ( $post !== self::$postTags ) {
self::$postTags = $post;
self::$merged = self::merge( $post );
}
return self::$merged;
}
/**
* Run one list through the extension filter.
*
* @param array> $tags The list before the filter.
* @param string $context Which list: component, image or form.
* @return array>
*/
private static function filtered( array $tags, string $context ): array {
/**
* Filters the HTML that component output may hold.
*
* The component list applies to whole component output. Inside templates, WordPress image
* and avatar markup also passes the image list, and wp_login_form() markup the form list.
* Markup that passes those lists must be on the component list too. A callback that
* ignores the context adds to all three.
*
* @since 1.0.0
*
* @param array> $tags Elements and their attributes, as wp_kses() takes them.
* @param string $context Which list: 'component', 'image' or 'form'.
*/
$filtered = apply_filters( 'releasecart_kses_allowed_html', $tags, $context );
return is_array( $filtered ) ? $filtered : $tags;
}
/**
* Add the component tags and the global attributes to the post allowlist.
*
* @param array $post The post allowlist.
* @return array>
*/
private static function merge( array $post ): array {
$tags = $post;
foreach ( self::COMPONENT_TAGS as $element => $attrs ) {
$own = isset( $tags[ $element ] ) && is_array( $tags[ $element ] ) ? $tags[ $element ] : array();
$tags[ $element ] = array_merge( $own, $attrs );
}
foreach ( $tags as $element => $attrs ) {
$tags[ $element ] = array_merge( is_array( $attrs ) ? $attrs : array(), self::GLOBAL_ATTRIBUTES );
}
return $tags;
}
}