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