[ 'id', 'class', 'style', 'title' ],
'em' => [ 'id', 'class', 'style', 'title' ],
'u' => [ 'id', 'class', 'style', 'title' ],
's' => [ 'id', 'class', 'style', 'title' ],
'sup' => [ 'id', 'class', 'style', 'title' ],
'sub' => [ 'id', 'class', 'style', 'title' ],
'a' => [ 'id', 'class', 'style', 'title', 'href', 'target', 'rel' ],
'span' => [ 'id', 'class', 'style', 'title' ],
'img' => [ 'id', 'class', 'style', 'src', 'alt', 'title', 'width', 'height', 'loading', 'decoding' ],
'br' => [],
];
public static function init(): void {
// Frontend rendering.
add_action(
'elementor/frontend/widget/before_render',
[ __CLASS__, 'start_buffer' ]
);
add_action(
'elementor/frontend/widget/after_render',
[ __CLASS__, 'finish_buffer' ]
);
// Elementor editor preview.
add_action(
'elementor/editor/footer',
[ __CLASS__, 'editor_preview_script' ],
30
);
add_action(
'wp_footer',
[ __CLASS__, 'editor_preview_script' ],
30
);
}
/**
* Start frontend output buffer.
*/
public static function start_buffer( $element ): void {
if ( ! self::is_supported_widget( $element ) ) {
return;
}
$GLOBALS['wpaddons_atomic_html_buffers'][ spl_object_id( $element ) ] = true;
ob_start();
}
/**
* Process frontend output.
*/
public static function finish_buffer( $element ): void {
if ( ! is_object( $element ) ) {
return;
}
$object_id = spl_object_id( $element );
if ( empty( $GLOBALS['wpaddons_atomic_html_buffers'][ $object_id ] ) ) {
return;
}
unset( $GLOBALS['wpaddons_atomic_html_buffers'][ $object_id ] );
$html = ob_get_clean();
if ( false === $html ) {
return;
}
echo self::restore_inline_html( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Supported Elementor Atomic widgets.
*/
private static function is_supported_widget( $element ): bool {
return is_object( $element )
&& method_exists( $element, 'get_name' )
&& in_array(
$element->get_name(),
self::WIDGETS,
true
);
}
/**
* Restore escaped inline HTML.
*/
private static function restore_inline_html( string $html ): string {
$inline_tags = implode(
'|',
array_map(
static function ( string $tag ): string {
return preg_quote( $tag, '/' );
},
self::INLINE_TAGS
)
);
$html = preg_replace_callback(
'/<(\/)?\s*(' . $inline_tags . ')\b(.*?)>/is',
static function ( array $matches ): string {
$tag = strtolower( $matches[2] );
if ( ! empty( $matches[1] ) ) {
return in_array( $tag, [ 'br', 'img' ], true )
? ''
: '' . $tag . '>';
}
if ( 'br' === $tag ) {
return '
';
}
return '<' . $tag . self::sanitize_attributes( $matches[3], $tag ) . '>';
},
$html
) ?? $html;
return $html;
}
/**
* Sanitize allowed HTML attributes.
*/
private static function sanitize_attributes(
string $raw_attrs,
string $tag
): string {
$raw_attrs = html_entity_decode(
$raw_attrs,
ENT_QUOTES | ENT_HTML5,
'UTF-8'
);
if ( ! preg_match_all(
'/([A-Za-z][A-Za-z0-9_-]*)\s*=\s*(["\'])(.*?)\2/',
$raw_attrs,
$matches,
PREG_SET_ORDER
) ) {
return '';
}
$output = '';
foreach ( $matches as $match ) {
$name = strtolower( $match[1] );
$value = html_entity_decode(
$match[3],
ENT_QUOTES | ENT_HTML5,
'UTF-8'
);
if ( ! in_array(
$name,
self::INLINE_ATTRIBUTES[ $tag ] ?? [],
true
) ) {
continue;
}
switch ( $name ) {
case 'id':
$value = preg_match( '/^[A-Za-z0-9_-]+$/', $value ) ? $value : '';
break;
case 'href':
case 'src':
$value = esc_url( $value );
break;
case 'class':
$classes = preg_split(
'/\s+/',
trim( $value )
) ?: [];
$classes = array_filter(
array_map(
'sanitize_html_class',
$classes
)
);
$value = implode( ' ', $classes );
break;
case 'style':
$value = self::sanitize_inline_style( $value );
break;
case 'target':
if ( ! in_array(
$value,
[
'_blank',
'_self',
'_parent',
'_top',
],
true
) ) {
$value = '';
}
break;
default:
$value = sanitize_text_field( $value );
break;
}
if ( '' === $value ) {
continue;
}
$output .= sprintf(
' %s="%s"',
esc_attr( $name ),
esc_attr( $value )
);
}
return $output;
}
/**
* Sanitize an inline style while allowing text background clipping.
*/
private static function sanitize_inline_style( string $value ): string {
$allow_background_clip = static function ( array $properties ): array {
$properties[] = 'background-clip';
return array_unique( $properties );
};
add_filter( 'safe_style_css', $allow_background_clip );
try {
return safecss_filter_attr( $value );
} finally {
remove_filter( 'safe_style_css', $allow_background_clip );
}
}
/**
* Make inline HTML render inside Elementor's live editor canvas.
*/
public static function editor_preview_script(): void {
if (
! is_user_logged_in() ||
! defined( 'ELEMENTOR_VERSION' )
) {
return;
}
?>