Commit 8a63d65fbd for woocommerce

commit 8a63d65fbd00cbf0c2b206d52da8ebf6929be4df
Author: Allison Levine <1689238+allilevine@users.noreply.github.com>
Date:   Tue Sep 16 16:08:02 2025 -0400

    Email Editor: Add email rendering instructions for cover block (#60837)

    * Email Editor: Add email rendering instructions for cover block.

    * Update and use color validation from helper.

    * Address coderabbit feedback.

    * Restore background image functionality.

    * Improve background image extraction fallback.

    * Clean up now unused code.

    * Remove extra padding.

    * Fix linting issue.

    * Use HTML API for background image fallback.

    * Get min height and default to 430px.

    * Fix HSLA validation and test.

diff --git a/packages/php/email-editor/changelog/add-cover-block-email-rendering b/packages/php/email-editor/changelog/add-cover-block-email-rendering
new file mode 100644
index 0000000000..bd000cc92d
--- /dev/null
+++ b/packages/php/email-editor/changelog/add-cover-block-email-rendering
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add email rendering instructions for the core/cover block.
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-cover.php b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-cover.php
new file mode 100644
index 0000000000..90fb9f0052
--- /dev/null
+++ b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-cover.php
@@ -0,0 +1,252 @@
+<?php
+/**
+ * This file is part of the WooCommerce Email Editor package
+ *
+ * @package Automattic\WooCommerce\EmailEditor
+ */
+
+declare( strict_types = 1 );
+namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks;
+
+use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
+use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper;
+use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper;
+use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper;
+use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Html_Processing_Helper;
+
+/**
+ * Cover block renderer.
+ * This renderer handles core/cover blocks with proper email-friendly HTML layout.
+ */
+class Cover extends Abstract_Block_Renderer {
+	/**
+	 * Renders the cover block content using a table-based layout for email compatibility.
+	 *
+	 * @param string            $block_content Block content.
+	 * @param array             $parsed_block Parsed block.
+	 * @param Rendering_Context $rendering_context Rendering context.
+	 * @return string
+	 */
+	protected function render_content( string $block_content, array $parsed_block, Rendering_Context $rendering_context ): string {
+		$block_attrs  = $parsed_block['attrs'] ?? array();
+		$inner_blocks = $parsed_block['innerBlocks'] ?? array();
+
+		// Render all inner blocks content.
+		$inner_content = '';
+		foreach ( $inner_blocks as $block ) {
+			$inner_content .= render_block( $block );
+		}
+
+		// If we don't have inner content, return empty.
+		if ( empty( $inner_content ) ) {
+			return '';
+		}
+
+		// Build the email-friendly layout.
+		$background_image = $this->extract_background_image( $block_attrs, $parsed_block['innerHTML'] ?? $block_content );
+		return $this->build_email_layout( $inner_content, $block_attrs, $block_content, $background_image, $rendering_context );
+	}
+
+	/**
+	 * Build the email-friendly layout for cover blocks.
+	 *
+	 * @param string            $inner_content Inner content.
+	 * @param array             $block_attrs Block attributes.
+	 * @param string            $block_content Original block content.
+	 * @param string            $background_image Background image URL.
+	 * @param Rendering_Context $rendering_context Rendering context.
+	 * @return string Rendered HTML.
+	 */
+	private function build_email_layout( string $inner_content, array $block_attrs, string $block_content, string $background_image, Rendering_Context $rendering_context ): string {
+		// Get original wrapper classes from block content.
+		$original_wrapper_classname = ( new Dom_Document_Helper( $block_content ) )->get_attribute_value_by_tag_name( 'div', 'class' ) ?? '';
+
+		// Get background color information.
+		$background_color = $this->get_background_color( $block_attrs, $rendering_context );
+
+		// Get block styles using the Styles_Helper.
+		$block_styles   = Styles_Helper::get_block_styles( $block_attrs, $rendering_context, array( 'padding', 'border', 'background-color' ) );
+		$default_styles = array(
+			'width'           => '100%',
+			'border-collapse' => 'collapse',
+			'text-align'      => 'center',
+		);
+
+		// Add minimum height (use specified value or default).
+		$min_height                   = $this->get_minimum_height( $block_attrs );
+		$default_styles['min-height'] = ! empty( $min_height ) ? $min_height : '430px';
+
+		$block_styles = Styles_Helper::extend_block_styles(
+			$block_styles,
+			$default_styles
+		);
+
+		// Add background image to table styles if present.
+		if ( ! empty( $background_image ) ) {
+			$block_styles = Styles_Helper::extend_block_styles(
+				$block_styles,
+				array(
+					'background-image'    => 'url("' . esc_url( $background_image ) . '")',
+					'background-size'     => 'cover',
+					'background-position' => 'center',
+					'background-repeat'   => 'no-repeat',
+				)
+			);
+		} elseif ( ! empty( $background_color ) ) {
+			// If no background image but there's a background color, use it.
+			$block_styles = Styles_Helper::extend_block_styles(
+				$block_styles,
+				array(
+					'background-color' => $background_color,
+				)
+			);
+		}
+
+		// Apply class and style attributes to the wrapper table.
+		$table_attrs = array(
+			'class' => 'email-block-cover ' . esc_attr( $original_wrapper_classname ),
+			'style' => $block_styles['css'],
+			'align' => 'center',
+			'width' => '100%',
+		);
+
+		// Build the cover content without background (background is now on the table).
+		$cover_content = $this->build_cover_content( $inner_content );
+
+		// Build individual table cell.
+		$cell_attrs = array(
+			'valign' => 'middle',
+			'align'  => 'center',
+		);
+
+		$cell = Table_Wrapper_Helper::render_table_cell( $cover_content, $cell_attrs );
+
+		// Use render_cell = false to avoid wrapping in an extra <td>.
+		return Table_Wrapper_Helper::render_table_wrapper( $cell, $table_attrs, array(), array(), false );
+	}
+
+	/**
+	 * Extract background image from block attributes or HTML content.
+	 *
+	 * @param array  $block_attrs Block attributes.
+	 * @param string $block_content Original block content.
+	 * @return string Background image URL or empty string.
+	 */
+	private function extract_background_image( array $block_attrs, string $block_content ): string {
+		// First check block attributes for URL.
+		if ( ! empty( $block_attrs['url'] ) ) {
+			return esc_url( $block_attrs['url'] );
+		}
+
+		// Fallback: use HTML API to find background image src.
+		$html = new \WP_HTML_Tag_Processor( $block_content );
+
+		while ( $html->next_tag( array( 'tag_name' => 'img' ) ) ) {
+			$class_attr = $html->get_attribute( 'class' );
+			// Check if this img tag has the wp-block-cover__image-background class.
+			if ( is_string( $class_attr ) && false !== strpos( $class_attr, 'wp-block-cover__image-background' ) ) {
+				$src = $html->get_attribute( 'src' );
+				if ( is_string( $src ) ) {
+					return esc_url( $src );
+				}
+			}
+		}
+
+		return '';
+	}
+
+	/**
+	 * Get minimum height from block attributes.
+	 *
+	 * @param array $block_attrs Block attributes.
+	 * @return string Minimum height value or empty string.
+	 */
+	private function get_minimum_height( array $block_attrs ): string {
+		// Check for minHeight attribute (legacy format).
+		if ( ! empty( $block_attrs['minHeight'] ) ) {
+			return Html_Processing_Helper::sanitize_dimension_value( $block_attrs['minHeight'] );
+		}
+
+		// Check for style.dimensions.minHeight (WordPress 6.2+ format).
+		if ( ! empty( $block_attrs['style']['dimensions']['minHeight'] ) ) {
+			return Html_Processing_Helper::sanitize_dimension_value( $block_attrs['style']['dimensions']['minHeight'] );
+		}
+
+		return '';
+	}
+
+	/**
+	 * Get background color from block attributes.
+	 *
+	 * @param array             $block_attrs Block attributes.
+	 * @param Rendering_Context $rendering_context Rendering context.
+	 * @return string Background color or empty string.
+	 */
+	private function get_background_color( array $block_attrs, Rendering_Context $rendering_context ): string {
+		// Check for custom overlay color first (used as background color when no image).
+		if ( ! empty( $block_attrs['customOverlayColor'] ) ) {
+			$color           = $block_attrs['customOverlayColor'];
+			$sanitized_color = $this->validate_and_sanitize_color( $color );
+			if ( ! empty( $sanitized_color ) ) {
+				return $sanitized_color;
+			}
+		}
+
+		// Check for overlay color slug (used as background color when no image).
+		if ( ! empty( $block_attrs['overlayColor'] ) ) {
+			$translated_color = $rendering_context->translate_slug_to_color( $block_attrs['overlayColor'] );
+			$sanitized_color  = $this->validate_and_sanitize_color( $translated_color );
+			if ( ! empty( $sanitized_color ) ) {
+				return $sanitized_color;
+			}
+		}
+
+		return '';
+	}
+
+	/**
+	 * Validate and sanitize a color value, returning empty string for invalid colors.
+	 *
+	 * @param string $color The color value to validate and sanitize.
+	 * @return string Sanitized color or empty string if invalid.
+	 */
+	private function validate_and_sanitize_color( string $color ): string {
+		$sanitized_color = Html_Processing_Helper::sanitize_color( $color );
+
+		// If sanitize_color returned the default fallback, check if the original was actually valid.
+		if ( '#000000' === $sanitized_color && '#000000' !== $color ) {
+			// The original color was invalid, so return empty string.
+			return '';
+		}
+
+		// The color is valid (either it was sanitized to something other than the default,
+		// or it was specifically #000000 which is a valid color).
+		return $sanitized_color;
+	}
+
+	/**
+	 * Build the cover content with background image or color.
+	 *
+	 * @param string $inner_content Inner content.
+	 * @return string Cover content HTML.
+	 */
+	private function build_cover_content( string $inner_content ): string {
+		$cover_style = 'position: relative; display: inline-block; width: 100%; max-width: 100%;';
+
+		// Wrap inner content with padding.
+		// Note: $inner_content is already rendered HTML from other blocks via render_block(),
+		// so it should be properly escaped by the individual block renderers.
+		$inner_wrapper_style = 'padding: 20px;';
+		$inner_wrapper_html  = sprintf(
+			'<div class="wp-block-cover__inner-container" style="%s">%s</div>',
+			$inner_wrapper_style,
+			$inner_content
+		);
+
+		return sprintf(
+			'<div class="wp-block-cover" style="%s">%s</div>',
+			$cover_style,
+			$inner_wrapper_html
+		);
+	}
+}
diff --git a/packages/php/email-editor/src/Integrations/Core/class-initializer.php b/packages/php/email-editor/src/Integrations/Core/class-initializer.php
index fc6a6ab495..42117ebd44 100644
--- a/packages/php/email-editor/src/Integrations/Core/class-initializer.php
+++ b/packages/php/email-editor/src/Integrations/Core/class-initializer.php
@@ -24,6 +24,7 @@ use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Image;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\List_Block;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\List_Item;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Media_Text;
+use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Cover;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Quote;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Social_Link;
 use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Social_Links;
@@ -70,6 +71,7 @@ class Initializer {
 		'core/media-text',
 		'core/audio',
 		'core/embed',
+		'core/cover',
 	);

 	/**
@@ -228,6 +230,9 @@ class Initializer {
 			case 'core/embed':
 				$renderer = new Embed();
 				break;
+			case 'core/cover':
+				$renderer = new Cover();
+				break;
 			default:
 				$renderer = new Fallback();
 				break;
diff --git a/packages/php/email-editor/src/Integrations/Utils/class-html-processing-helper.php b/packages/php/email-editor/src/Integrations/Utils/class-html-processing-helper.php
index 15381d21bc..e650b3dc7a 100644
--- a/packages/php/email-editor/src/Integrations/Utils/class-html-processing-helper.php
+++ b/packages/php/email-editor/src/Integrations/Utils/class-html-processing-helper.php
@@ -96,20 +96,70 @@ class Html_Processing_Helper {
 	}

 	/**
-	 * Sanitize color value to ensure it's a valid hex color or CSS variable.
+	 * Sanitize dimension value to ensure it's a valid CSS dimension.
+	 *
+	 * Supports numeric values (converted to px) and standard CSS units.
+	 *
+	 * @param mixed $value The dimension value to sanitize.
+	 * @return string Sanitized dimension value or empty string if invalid.
+	 */
+	public static function sanitize_dimension_value( $value ): string {
+		if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
+			return '';
+		}
+
+		$value = (string) $value;
+
+		// If it's just a number, assume pixels.
+		if ( is_numeric( $value ) ) {
+			$value = $value . 'px';
+		}
+
+		// Use existing CSS value sanitization for security.
+		$sanitized_value = self::sanitize_css_value( $value );
+
+		// Additional validation for dimension-specific units.
+		if ( ! empty( $sanitized_value ) && preg_match( '/^(\d+(?:\.\d+)?)(px|em|rem|%|vh|vw|ex|ch|in|cm|mm|pt|pc)$/', $sanitized_value ) ) {
+			return $sanitized_value;
+		}
+
+		return '';
+	}
+
+	/**
+	 * Sanitize color value to ensure it's a valid color format.
+	 *
+	 * Supports hex colors, rgb/rgba, hsl/hsla, named colors, and CSS variables.
 	 *
 	 * @param string $color The color value to sanitize.
-	 * @return string Sanitized color value.
+	 * @return string Sanitized color value or safe default if invalid.
 	 */
 	public static function sanitize_color( string $color ): string {
-		// Remove any whitespace and convert to lowercase.
-		$color = strtolower( trim( $color ) );
+		// Remove any whitespace.
+		$color = trim( $color );
+
+		// Check if it's a valid hex color (#fff, #ffffff, #ffffffff).
+		if ( preg_match( '/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color ) ) {
+			return strtolower( $color );
+		}

-		// Check if it's a valid hex color (3 or 6 characters).
-		if ( preg_match( '/^#([a-f0-9]{3}){1,2}$/i', $color ) ) {
+		// Check for rgb/rgba colors.
+		if ( preg_match( '/^rgba?\(\s*(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|\d{1,2})\s*(?:,\s*(?:1(?:\.0+)?|0(?:\.\d+)?|\.\d+)\s*)?\)$/', $color ) ) {
 			return $color;
 		}

+		// Check for hsl/hsla colors.
+		if ( preg_match( '/^hsla?\(\s*(360|3[0-5]\d|[12]\d{2}|\d{1,2})\s*,\s*(100|[1-9]?\d)%\s*,\s*(100|[1-9]?\d)%\s*(?:,\s*(?:1(?:\.0+)?|0(?:\.\d+)?|\.\d+)\s*)?\)$/', $color ) ) {
+			return $color;
+		}
+
+		// Check for named colors and other valid CSS color values.
+		// We use a permissive approach: accept any string that doesn't contain dangerous characters
+		// and let the CSS engine handle the actual validation.
+		if ( preg_match( '/^[a-zA-Z][a-zA-Z0-9-]*$/', $color ) && ! preg_match( '/^(expression|javascript|vbscript|data|import|behavior|binding|filter|progid)/i', $color ) ) {
+			return strtolower( $color );
+		}
+
 		// Check if it's a CSS variable (var(--variable-name)).
 		if ( preg_match( '/^var\(--[a-zA-Z0-9\-_]+\)$/', $color ) ) {
 			return $color;
diff --git a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Cover_Test.php b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Cover_Test.php
new file mode 100644
index 0000000000..15ea44c5ed
--- /dev/null
+++ b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Cover_Test.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * This file is part of the WooCommerce Email Editor package
+ *
+ * @package Automattic\WooCommerce\EmailEditor
+ */
+
+declare( strict_types = 1 );
+namespace Automattic\WooCommerce\EmailEditor\Tests\Integration\Integrations\Core\Renderer\Blocks;
+
+use Automattic\WooCommerce\EmailEditor\Engine\Email_Editor;
+use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
+use Automattic\WooCommerce\EmailEditor\Engine\Theme_Controller;
+use Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks\Cover;
+
+/**
+ * Integration test for Cover class
+ */
+class Cover_Test extends \Email_Editor_Integration_Test_Case {
+	/**
+	 * Cover renderer instance
+	 *
+	 * @var Cover
+	 */
+	private $cover_renderer;
+
+	/**
+	 * Cover block configuration
+	 *
+	 * @var array
+	 */
+	private $parsed_cover = array(
+		'blockName'   => 'core/cover',
+		'attrs'       => array(
+			'overlayColor' => 'pale-pink',
+		),
+		'innerHTML'   => '<div class="wp-block-cover is-light"><span aria-hidden="true" class="wp-block-cover__background has-pale-pink-background-color has-background-dim-100 has-background-dim"></span><div class="wp-block-cover__inner-container"><p class="has-text-align-center has-large-font-size">Cover block</p></div></div>',
+		'innerBlocks' => array(
+			0 => array(
+				'blockName'    => 'core/paragraph',
+				'attrs'        => array(
+					'align'    => 'center',
+					'fontSize' => 'large',
+				),
+				'innerBlocks'  => array(),
+				'innerHTML'    => '<p class="has-text-align-center has-large-font-size">Cover block</p>',
+				'innerContent' => array(
+					0 => '<p class="has-text-align-center has-large-font-size">Cover block</p>',
+				),
+			),
+		),
+	);
+
+	/**
+	 * Rendering context instance.
+	 *
+	 * @var Rendering_Context
+	 */
+	private $rendering_context;
+
+	/**
+	 * Set up before each test
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->di_container->get( Email_Editor::class )->initialize();
+		$this->cover_renderer    = new Cover();
+		$theme_controller        = $this->di_container->get( Theme_Controller::class );
+		$this->rendering_context = new Rendering_Context( $theme_controller->get_theme() );
+	}
+
+	/**
+	 * Test it renders cover content
+	 */
+	public function testItRendersCoverContent(): void {
+		$rendered = $this->cover_renderer->render( '', $this->parsed_cover, $this->rendering_context );
+		$this->assertStringContainsString( 'Cover block', $rendered );
+		$this->assertStringContainsString( 'wp-block-cover', $rendered );
+		$this->assertStringContainsString( 'wp-block-cover__inner-container', $rendered );
+	}
+
+	/**
+	 * Test it handles background image
+	 */
+	public function testItHandlesBackgroundImage(): void {
+		$parsed_cover                 = $this->parsed_cover;
+		$parsed_cover['attrs']['url'] = 'https://example.com/background.jpg';
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+		$this->assertStringContainsString( 'background-image:url(&quot;https://example.com/background.jpg&quot;)', $rendered );
+		$this->assertStringContainsString( 'background-size:cover', $rendered );
+		$this->assertStringContainsString( 'background-position:center', $rendered );
+	}
+
+	/**
+	 * Test it handles custom background color
+	 */
+	public function testItHandlesCustomBackgroundColor(): void {
+		$parsed_cover                                = $this->parsed_cover;
+		$parsed_cover['attrs']['customOverlayColor'] = '#4b74b2';
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+		// Should apply background color directly to the cover div when no background image.
+		$this->assertStringContainsString( 'background-color:#4b74b2', $rendered );
+	}
+
+	/**
+	 * Test it handles background color slug
+	 */
+	public function testItHandlesBackgroundColorSlug(): void {
+		$parsed_cover                          = $this->parsed_cover;
+		$parsed_cover['attrs']['overlayColor'] = 'pale-pink';
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+		// Should apply background color directly to the cover div when no background image.
+		$this->assertStringContainsString( 'background-color:', $rendered );
+	}
+
+	/**
+	 * Test it handles custom styling
+	 */
+	public function testItHandlesCustomStyling(): void {
+		$parsed_cover                   = $this->parsed_cover;
+		$parsed_cover['attrs']['style'] = array(
+			'border'  => array(
+				'color'  => '#123456',
+				'radius' => '10px',
+				'width'  => '2px',
+				'style'  => 'solid',
+			),
+			'color'   => array(
+				'background' => '#abcdef',
+			),
+			'spacing' => array(
+				'padding' => array(
+					'bottom' => '5px',
+					'left'   => '15px',
+					'right'  => '20px',
+					'top'    => '10px',
+				),
+			),
+		);
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+		$this->assertStringContainsString( 'background-color:#f78da7', $rendered );
+		$this->assertStringContainsString( 'border-color:#123456;', $rendered );
+		$this->assertStringContainsString( 'border-radius:10px;', $rendered );
+		$this->assertStringContainsString( 'border-width:2px;', $rendered );
+		$this->assertStringContainsString( 'border-style:solid;', $rendered );
+		$this->assertStringContainsString( 'padding-bottom:5px;', $rendered );
+		$this->assertStringContainsString( 'padding-left:15px;', $rendered );
+		$this->assertStringContainsString( 'padding-right:20px;', $rendered );
+		$this->assertStringContainsString( 'padding-top:10px;', $rendered );
+	}
+
+	/**
+	 * Test it preserves classes set by editor
+	 */
+	public function testItPreservesClassesSetByEditor(): void {
+		$parsed_cover = $this->parsed_cover;
+		$content      = '<div class="wp-block-cover is-light editor-class-1 another-class"></div>';
+
+		$rendered = $this->cover_renderer->render( $content, $parsed_cover, $this->rendering_context );
+		$this->assertStringContainsString( 'wp-block-cover is-light editor-class-1 another-class', $rendered );
+	}
+
+
+	/**
+	 * Test it returns empty when no inner content
+	 */
+	public function testItReturnsEmptyWhenNoInnerContent(): void {
+		$parsed_cover                = $this->parsed_cover;
+		$parsed_cover['innerBlocks'] = array();
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+		// The abstract block renderer adds spacers even for empty content, so we check for empty inner content.
+		$this->assertStringNotContainsString( 'wp-block-cover__inner-container', $rendered );
+	}
+
+	/**
+	 * Test it extracts background image from HTML content
+	 */
+	public function testItExtractsBackgroundImageFromHtml(): void {
+		$parsed_cover              = $this->parsed_cover;
+		$parsed_cover['innerHTML'] = '<div class="wp-block-cover"><img class="wp-block-cover__image-background wp-image-537 size-full" alt="" src="https://example.com/background.jpg" data-object-fit="cover"/><span aria-hidden="true" class="wp-block-cover__background has-background-dim" style="background-color:#4b74b2"></span><div class="wp-block-cover__inner-container"><p>Cover block</p></div></div>';
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+		$this->assertStringContainsString( 'background-image:url(&quot;https://example.com/background.jpg&quot;)', $rendered );
+	}
+
+	/**
+	 * Test it extracts background image from URL attribute
+	 */
+	public function testItExtractsBackgroundImageFromUrlAttribute(): void {
+		$parsed_cover                 = $this->parsed_cover;
+		$parsed_cover['attrs']['url'] = 'https://example.com/background.jpg';
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+		$this->assertStringContainsString( 'background-image:url(&quot;https://example.com/background.jpg&quot;)', $rendered );
+	}
+
+	/**
+	 * Test it handles background image without overlay (email compatibility)
+	 */
+	public function testItHandlesBackgroundImageWithoutOverlay(): void {
+		$parsed_cover                                = $this->parsed_cover;
+		$parsed_cover['attrs']['url']                = 'https://example.com/background.jpg';
+		$parsed_cover['attrs']['customOverlayColor'] = '#4b74b2';
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+		$this->assertStringContainsString( 'background-image:url(&quot;https://example.com/background.jpg&quot;)', $rendered );
+		// Should not have overlay elements for email compatibility.
+		$this->assertStringNotContainsString( 'wp-block-cover__background', $rendered );
+		$this->assertStringNotContainsString( 'background-color:#4b74b2', $rendered );
+	}
+}
diff --git a/packages/php/email-editor/tests/unit/Integrations/Utils/Html_Processing_Helper_Test.php b/packages/php/email-editor/tests/unit/Integrations/Utils/Html_Processing_Helper_Test.php
index f158f1621e..0e22d2095b 100644
--- a/packages/php/email-editor/tests/unit/Integrations/Utils/Html_Processing_Helper_Test.php
+++ b/packages/php/email-editor/tests/unit/Integrations/Utils/Html_Processing_Helper_Test.php
@@ -143,6 +143,7 @@ class Html_Processing_Helper_Test extends \Email_Editor_Unit_Test {
 		$valid_hex_colors = array(
 			'#fff',
 			'#ffffff',
+			'#ffffffff',
 			'#000',
 			'#000000',
 			'#abc',
@@ -173,16 +174,121 @@ class Html_Processing_Helper_Test extends \Email_Editor_Unit_Test {
 		}
 	}

+	/**
+	 * Test sanitize_color with rgb/rgba colors.
+	 */
+	public function testSanitizeColorWithRgbRgbaColors(): void {
+		$rgb_colors = array(
+			'rgb(255, 255, 255)',
+			'rgb(0, 0, 0)',
+			'rgb(128, 128, 128)',
+			'rgba(255, 255, 255, 0.5)',
+			'rgba(0, 0, 0, 1)',
+			'rgba(128, 128, 128, 0.8)',
+		);
+
+		foreach ( $rgb_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( $color, $result, "Failed to preserve rgb/rgba color: $color" );
+		}
+	}
+
+	/**
+	 * Test sanitize_color with hsl/hsla colors.
+	 */
+	public function testSanitizeColorWithHslHslaColors(): void {
+		$hsl_colors = array(
+			'hsl(0, 0%, 100%)',
+			'hsl(0, 0%, 0%)',
+			'hsl(120, 50%, 50%)',
+			'hsla(0, 0%, 100%, 0.5)',
+			'hsla(0, 0%, 0%, 1)',
+			'hsla(120, 50%, 50%, 0.8)',
+		);
+
+		foreach ( $hsl_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( $color, $result, "Failed to preserve hsl/hsla color: $color" );
+		}
+	}
+
+	/**
+	 * Test sanitize_color with named colors and CSS keywords.
+	 *
+	 * Uses a permissive approach that accepts any string that looks like a valid CSS color name.
+	 * This test demonstrates that the method accepts various named colors without maintaining
+	 * a hardcoded list, making it future-proof for new CSS named colors.
+	 */
+	public function testSanitizeColorWithNamedColorsAndKeywords(): void {
+		$valid_colors = array(
+			// Standard named colors.
+			'black',
+			'white',
+			'red',
+			'green',
+			'blue',
+			'yellow',
+			'orange',
+			'purple',
+			'pink',
+			'brown',
+			'gray',
+			'grey',
+			'navy',
+			'lime',
+			'teal',
+			'crimson',
+			'aqua',
+			'fuchsia',
+			'silver',
+			'maroon',
+			'olive',
+			'gold',
+			// CSS keywords.
+			'transparent',
+			'inherit',
+			'initial',
+			'unset',
+			// Future CSS named colors (will work automatically).
+			'rebeccapurple',
+			'lightcoral',
+			'mediumseagreen',
+			// Additional named colors that would work with the permissive approach.
+			'lightblue',
+			'darkgreen',
+			'hotpink',
+			'forestgreen',
+			'royalblue',
+			'orangered',
+			'deepskyblue',
+			'mediumvioletred',
+			'lightsteelblue',
+			'palegoldenrod',
+			// Made-up but valid-looking color names (demonstrates permissiveness).
+			'customcolor',
+			'brandprimary',
+			'accentcolor',
+		);
+
+		foreach ( $valid_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( strtolower( $color ), $result, "Failed to preserve named color: $color" );
+		}
+	}
+
 	/**
 	 * Test sanitize_color with invalid colors returns default.
 	 */
 	public function testSanitizeColorWithInvalidColorsReturnsDefault(): void {
 		$invalid_colors = array(
-			'invalid',
 			'#ggg',
 			'#gggggg',
 			'rgb(256, 256, 256)',
+			'rgba(255, 255, 255, 2.0)', // Invalid alpha > 1.
+			'hsl(400, 50%, 50%)', // Invalid hue > 360.
+			'hsla(120, 150%, 50%, 0.5)', // Invalid saturation > 100%.
 			'var(invalid)',
+			'var(--)', // Invalid CSS variable.
 			'',
 		);

@@ -192,6 +298,73 @@ class Html_Processing_Helper_Test extends \Email_Editor_Unit_Test {
 		}
 	}

+	/**
+	 * Test sanitize_color with dangerous color values returns default.
+	 */
+	public function testSanitizeColorWithDangerousValuesReturnsDefault(): void {
+		$dangerous_colors = array(
+			'expression(alert(1))',
+			'javascript:alert(1)',
+			'vbscript:alert(1)',
+			'data:text/html,<script>alert(1)</script>',
+			'import(url)',
+			'behavior:url(script.htc)',
+			'binding:url(script.xml)',
+			'filter:progid:DXImageTransform.Microsoft.Alpha(opacity=50)',
+		);
+
+		foreach ( $dangerous_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( '#000000', $result, "Failed to return default for dangerous color: $color" );
+		}
+	}
+
+	/**
+	 * Test sanitize_color permissive approach with various valid-looking color names.
+	 *
+	 * This test specifically demonstrates the permissive nature of the color validation,
+	 * showing that it accepts any string that looks like a valid CSS color name,
+	 * regardless of whether it's a real CSS color or not.
+	 */
+	public function testSanitizeColorPermissiveApproach(): void {
+		$permissive_colors = array(
+			// Real CSS named colors.
+			'red',
+			'blue',
+			'green',
+			// Made-up but valid-looking color names.
+			'brandcolor',
+			'primaryaccent',
+			'secondaryhighlight',
+			'customtheme',
+			'brandprimary',
+			'brandsecondary',
+			'accentcolor',
+			'highlightcolor',
+			'textcolor',
+			'backgroundcolor',
+			'linkcolor',
+			'buttoncolor',
+			'headercolor',
+			'footercolor',
+			'sidebarcolor',
+			'contentcolor',
+			'widgetcolor',
+			'formcolor',
+			'inputcolor',
+			'labelcolor',
+			'errorcolor',
+			'successcolor',
+			'warningcolor',
+			'infocolor',
+		);
+
+		foreach ( $permissive_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( strtolower( $color ), $result, "Failed to preserve permissive color: $color" );
+		}
+	}
+
 	/**
 	 * Test sanitize_color trims whitespace.
 	 */
@@ -598,4 +771,134 @@ class Html_Processing_Helper_Test extends \Email_Editor_Unit_Test {
 		$result = Html_Processing_Helper::sanitize_image_html( $html );
 		$this->assertEquals( '', $result );
 	}
+
+	/**
+	 * Test sanitize_color with valid RGBA alpha values (0-1).
+	 */
+	public function testSanitizeColorWithValidRgbaAlphaValues(): void {
+		$valid_rgba_colors = array(
+			'rgba(255, 255, 255, 0)',      // Alpha 0.
+			'rgba(255, 255, 255, 0.5)',    // Alpha 0.5.
+			'rgba(255, 255, 255, .5)',     // Alpha .5 (no leading zero).
+			'rgba(255, 255, 255, 1)',      // Alpha 1.
+			'rgba(255, 255, 255, 1.0)',    // Alpha 1.0.
+			'rgba(255, 255, 255, 1.00)',   // Alpha 1.00.
+			'rgba(255, 255, 255, 0.25)',   // Alpha 0.25.
+			'rgba(255, 255, 255, 0.75)',   // Alpha 0.75.
+			'rgba(120, 130, 140, .25)',    // Alpha .25 with different RGB values.
+		);
+
+		foreach ( $valid_rgba_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( $color, $result, "Failed to preserve valid RGBA color with alpha: $color" );
+		}
+	}
+
+	/**
+	 * Test sanitize_color with invalid RGBA alpha values (>1).
+	 */
+	public function testSanitizeColorWithInvalidRgbaAlphaValues(): void {
+		$invalid_rgba_colors = array(
+			'rgba(255, 255, 255, 1.25)',   // Alpha > 1.
+			'rgba(255, 255, 255, 2)',      // Alpha = 2.
+			'rgba(255, 255, 255, 1.5)',    // Alpha = 1.5.
+			'rgba(255, 255, 255, 10)',     // Alpha = 10.
+			'rgba(255, 255, 255, 2.0)',    // Alpha = 2.0.
+			'rgba(255, 255, 255, 1.01)',   // Alpha slightly > 1.
+		);
+
+		foreach ( $invalid_rgba_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( '#000000', $result, "Failed to reject invalid RGBA color with alpha > 1: $color" );
+		}
+	}
+
+	/**
+	 * Test sanitize_color with valid HSLA alpha values (0-1).
+	 */
+	public function testSanitizeColorWithValidHslaAlphaValues(): void {
+		$valid_hsla_colors = array(
+			'hsla(120, 50%, 50%, 0)',      // Alpha 0.
+			'hsla(120, 50%, 50%, 0.5)',    // Alpha 0.5.
+			'hsla(120, 50%, 50%, .5)',     // Alpha .5 (no leading zero).
+			'hsla(120, 50%, 50%, 1)',      // Alpha 1.
+			'hsla(120, 50%, 50%, 1.0)',    // Alpha 1.0.
+			'hsla(120, 50%, 50%, 1.00)',   // Alpha 1.00.
+			'hsla(240, 75%, 25%, 0.25)',   // Alpha 0.25.
+			'hsla(360, 100%, 75%, 0.9)',   // Alpha 0.9.
+			'hsla(0, 0%, 100%, .75)',      // Alpha .75 with white color.
+		);
+
+		foreach ( $valid_hsla_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( $color, $result, "Failed to preserve valid HSLA color with alpha: $color" );
+		}
+	}
+
+	/**
+	 * Test sanitize_color with invalid HSLA alpha values (>1).
+	 */
+	public function testSanitizeColorWithInvalidHslaAlphaValues(): void {
+		$invalid_hsla_colors = array(
+			'hsla(120, 50%, 50%, 1.25)',   // Alpha > 1.
+			'hsla(120, 50%, 50%, 2)',      // Alpha = 2.
+			'hsla(120, 50%, 50%, 1.5)',    // Alpha = 1.5.
+			'hsla(120, 50%, 50%, 10)',     // Alpha = 10.
+			'hsla(120, 50%, 50%, 2.0)',    // Alpha = 2.0.
+			'hsla(120, 50%, 50%, 1.01)',   // Alpha slightly > 1.
+		);
+
+		foreach ( $invalid_hsla_colors as $color ) {
+			$result = Html_Processing_Helper::sanitize_color( $color );
+			$this->assertEquals( '#000000', $result, "Failed to reject invalid HSLA color with alpha > 1: $color" );
+		}
+	}
+
+	/**
+	 * Test sanitize_dimension_value with valid dimension values.
+	 */
+	public function testSanitizeDimensionValueWithValidValues(): void {
+		$valid_dimensions = array(
+			'430'    => '430px',    // Numeric value gets px added.
+			'430px'  => '430px',    // Already has px.
+			'50%'    => '50%',      // Percentage.
+			'2em'    => '2em',      // Em units.
+			'1.5rem' => '1.5rem',   // Rem units.
+			'100vh'  => '100vh',    // Viewport height.
+			'50vw'   => '50vw',     // Viewport width.
+			'12pt'   => '12pt',     // Points.
+			'1in'    => '1in',      // Inches.
+			'2.5cm'  => '2.5cm',    // Centimeters.
+		);
+
+		foreach ( $valid_dimensions as $input => $expected ) {
+			$result = Html_Processing_Helper::sanitize_dimension_value( $input );
+			$this->assertEquals( $expected, $result, "Failed to sanitize dimension: $input" );
+		}
+	}
+
+	/**
+	 * Test sanitize_dimension_value with invalid dimension values.
+	 */
+	public function testSanitizeDimensionValueWithInvalidValues(): void {
+		$invalid_dimensions = array(
+			'invalid',
+			'<script>alert("xss")</script>',
+			'expression(alert(1))',
+			'javascript:alert(1)',
+			'',
+			'  ',
+			'430invalid',
+			'px430',
+			null,
+			array(),
+			false,
+		);
+
+		foreach ( $invalid_dimensions as $input ) {
+			$result            = Html_Processing_Helper::sanitize_dimension_value( $input );
+			$input_description = is_string( $input ) ? $input : gettype( $input );
+			$this->assertEquals( '', $result, 'Failed to reject invalid dimension: ' . $input_description );
+		}
+	}
 }