Commit 71b0aeaadb for woocommerce

commit 71b0aeaadb01357f060c7851508f10c4747c98eb
Author: Allison Levine <1689238+allilevine@users.noreply.github.com>
Date:   Fri Jan 23 11:40:35 2026 -0500

    Email Editor: Add support for VideoPress embeds (#62870)

    * Email Editor: Add support for VideoPress embeds.

    * Clean up get_videopress_thumbnail.

    * Change esc_url to esc_url_raw for poster URLs, to prevent encoding HTML too early.

    * Update tests.

    * Add changefile(s) from automation for the following project(s): packages/php/email-editor

    * Stub oEmbed response to avoid external HTTP dependency.

    * Implement caching for oEmbed responses in email rendering context.

    * Add docblock comment to explain oEmbed cache time-to-live (TTL) filter.

    * Add @since annotations to docblocks.

    * Clean up the embed block rendering tests.

    * Fix potential security issue with URL discovery, fix test filter removal.

    * Remove since comments that do not match package version.

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/packages/php/email-editor/changelog/62870-add-email-rendering-for-videopress-embeds b/packages/php/email-editor/changelog/62870-add-email-rendering-for-videopress-embeds
new file mode 100644
index 0000000000..680dc85732
--- /dev/null
+++ b/packages/php/email-editor/changelog/62870-add-email-rendering-for-videopress-embeds
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add support for VideoPress embeds in the Email Editor package.
\ No newline at end of file
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
index 90fb9f0052..ca086471b4 100644
--- 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
@@ -83,10 +83,12 @@ class Cover extends Abstract_Block_Renderer {

 		// Add background image to table styles if present.
 		if ( ! empty( $background_image ) ) {
+			// Use esc_url_raw() for CSS context - esc_url() encodes & as &#038; which
+			// causes WP_Style_Engine::compile_css() to strip the background-image property.
 			$block_styles = Styles_Helper::extend_block_styles(
 				$block_styles,
 				array(
-					'background-image'    => 'url("' . esc_url( $background_image ) . '")',
+					'background-image'    => 'url("' . esc_url_raw( $background_image ) . '")',
 					'background-size'     => 'cover',
 					'background-position' => 'center',
 					'background-repeat'   => 'no-repeat',
@@ -127,6 +129,7 @@ class Cover extends Abstract_Block_Renderer {

 	/**
 	 * Extract background image from block attributes or HTML content.
+	 * Returns raw URL - escaping happens at final CSS output context.
 	 *
 	 * @param array  $block_attrs Block attributes.
 	 * @param string $block_content Original block content.
@@ -134,8 +137,9 @@ class Cover extends Abstract_Block_Renderer {
 	 */
 	private function extract_background_image( array $block_attrs, string $block_content ): string {
 		// First check block attributes for URL.
+		// Use esc_url_raw() to sanitize without HTML entity encoding.
 		if ( ! empty( $block_attrs['url'] ) ) {
-			return esc_url( $block_attrs['url'] );
+			return esc_url_raw( $block_attrs['url'] );
 		}

 		// Fallback: use HTML API to find background image src.
@@ -147,7 +151,7 @@ class Cover extends Abstract_Block_Renderer {
 			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 esc_url_raw( $src );
 				}
 			}
 		}
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php
index f28e8b4309..c9a65c8b27 100644
--- a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php
+++ b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-embed.php
@@ -56,10 +56,14 @@ class Embed extends Abstract_Block_Renderer {
 	 * @var array
 	 */
 	private const VIDEO_PROVIDERS = array(
-		'youtube' => array(
+		'youtube'    => array(
 			'domains'  => array( 'youtube.com', 'youtu.be' ),
 			'base_url' => 'https://www.youtube.com/',
 		),
+		'videopress' => array(
+			'domains'  => array( 'videopress.com', 'video.wordpress.com' ),
+			'base_url' => 'https://videopress.com/',
+		),
 	);

 	/**
@@ -322,6 +326,8 @@ class Embed extends Abstract_Block_Renderer {
 				return __( 'Listen on ReverbNation', 'woocommerce' );
 			case 'youtube':
 				return __( 'Watch on YouTube', 'woocommerce' );
+			case 'videopress':
+				return __( 'Watch on VideoPress', 'woocommerce' );
 			default:
 				return __( 'Listen to the audio', 'woocommerce' );
 		}
@@ -429,6 +435,40 @@ class Embed extends Abstract_Block_Renderer {
 		return array_key_exists( $provider, self::VIDEO_PROVIDERS );
 	}

+	/**
+	 * Validate that a URL's host matches the expected provider's domains.
+	 * This prevents SSRF when provider is set via user-controlled attributes.
+	 *
+	 * @param string $url      URL to validate.
+	 * @param string $provider Provider name.
+	 * @return bool True if URL host matches provider domains.
+	 */
+	private function url_matches_provider( string $url, string $provider ): bool {
+		if ( ! $this->is_valid_url( $url ) ) {
+			return false;
+		}
+
+		$parsed_url = wp_parse_url( $url );
+		if ( ! isset( $parsed_url['host'] ) ) {
+			return false;
+		}
+
+		$url_host = strtolower( $parsed_url['host'] );
+
+		// Get allowed domains for this provider.
+		$all_providers   = $this->get_all_provider_configs();
+		$allowed_domains = $all_providers[ $provider ]['domains'] ?? array();
+
+		foreach ( $allowed_domains as $allowed_domain ) {
+			$allowed_domain = strtolower( $allowed_domain );
+			if ( $url_host === $allowed_domain || str_ends_with( $url_host, '.' . $allowed_domain ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
 	/**
 	 * Render a video embed using the Video renderer.
 	 *
@@ -440,6 +480,14 @@ class Embed extends Abstract_Block_Renderer {
 	 * @return string Rendered video embed or fallback.
 	 */
 	private function render_video_embed( string $url, string $provider, array $parsed_block, Rendering_Context $rendering_context, string $block_content ): string {
+		// Validate URL matches the detected provider to prevent SSRF.
+		// Provider can come from user-controlled providerNameSlug attribute,
+		// so we must verify the URL actually belongs to that provider's domains.
+		if ( ! $this->url_matches_provider( $url, $provider ) ) {
+			$fallback_attr = $this->create_fallback_attributes( $url, $url );
+			return $this->render_link_fallback( $fallback_attr, $block_content, $parsed_block, $rendering_context );
+		}
+
 		// Try to get video thumbnail URL.
 		$poster_url = $this->get_video_thumbnail_url( $url, $provider );

@@ -484,11 +532,14 @@ class Embed extends Abstract_Block_Renderer {
 	 * @return string Thumbnail URL or empty string.
 	 */
 	private function get_video_thumbnail_url( string $url, string $provider ): string {
-		// Currently only YouTube supports thumbnail extraction.
 		if ( 'youtube' === $provider ) {
 			return $this->get_youtube_thumbnail( $url );
 		}

+		if ( 'videopress' === $provider ) {
+			return $this->get_videopress_thumbnail( $url );
+		}
+
 		// For other providers, we don't have thumbnail extraction implemented.
 		// Return empty to trigger link fallback.
 		return '';
@@ -516,4 +567,77 @@ class Embed extends Abstract_Block_Renderer {
 		// Using 0.jpg format as shown in the example.
 		return 'https://img.youtube.com/vi/' . $video_id . '/0.jpg';
 	}
+
+	/**
+	 * Extract VideoPress video thumbnail URL.
+	 * Uses WordPress oEmbed API to get thumbnail_url from the provider response.
+	 * Results are cached using transients to avoid repeated HTTP requests.
+	 *
+	 * Note: URL validation against VideoPress domains is done in render_video_embed()
+	 * via url_matches_provider() before this method is called.
+	 *
+	 * @param string $url VideoPress video URL (pre-validated by caller).
+	 * @return string Thumbnail URL or empty string.
+	 */
+	private function get_videopress_thumbnail( string $url ): string {
+		// Generate a cache key based on the URL.
+		$cache_key = 'wc_email_vp_thumb_' . md5( $url );
+
+		// Check for cached thumbnail URL.
+		$cached_thumbnail = get_transient( $cache_key );
+		if ( false !== $cached_thumbnail ) {
+			// Return cached value (empty string means previous lookup failed).
+			return is_string( $cached_thumbnail ) ? $cached_thumbnail : '';
+		}
+
+		// Use WP_oEmbed::get_data() to fetch thumbnail from oEmbed endpoint.
+		// URL is pre-validated by render_video_embed() via url_matches_provider(),
+		// ensuring only VideoPress domains reach this point (SSRF mitigation).
+		$oembed      = new \WP_oEmbed();
+		$oembed_data = $oembed->get_data( $url );
+
+		/**
+		 * Filter the oEmbed cache time-to-live (TTL).
+		 *
+		 * This filter matches WordPress core's oembed_ttl filter signature:
+		 * - $ttl: Time to live in seconds (default: DAY_IN_SECONDS)
+		 * - $url: The URL being embedded
+		 * - $attr: Attributes array (empty in this context)
+		 * - $post_id: Post ID where embed is used (empty string here since email rendering is not post-specific)
+		 *
+		 * @param int    $ttl     Cache TTL in seconds.
+		 * @param string $url     The embedded URL.
+		 * @param array  $attr    Attributes array.
+		 * @param string $post_id Post ID (empty string in email context).
+		 */
+		// Default TTL matches WordPress oEmbed cache (1 day).
+		$cache_ttl = (int) apply_filters( 'oembed_ttl', DAY_IN_SECONDS, $url, array(), '' );
+
+		// get_data() returns object|false, so check for false or non-object.
+		if ( false === $oembed_data || ! is_object( $oembed_data ) ) {
+			// Cache empty result to avoid repeated failed lookups.
+			set_transient( $cache_key, '', $cache_ttl );
+			return '';
+		}
+
+		// Extract thumbnail_url from oEmbed response.
+		if ( ! isset( $oembed_data->thumbnail_url ) ) {
+			// Cache empty result.
+			set_transient( $cache_key, '', $cache_ttl );
+			return '';
+		}
+
+		$thumbnail_url = $oembed_data->thumbnail_url;
+
+		// Validate the thumbnail URL.
+		if ( ! empty( $thumbnail_url ) && $this->is_valid_url( $thumbnail_url ) ) {
+			// Cache the valid thumbnail URL.
+			set_transient( $cache_key, $thumbnail_url, $cache_ttl );
+			return $thumbnail_url;
+		}
+
+		// Cache empty result for invalid URLs.
+		set_transient( $cache_key, '', $cache_ttl );
+		return '';
+	}
 }
diff --git a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-video.php b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-video.php
index 06312ff744..4a9ffaeacf 100644
--- a/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-video.php
+++ b/packages/php/email-editor/src/Integrations/Core/Renderer/Blocks/class-video.php
@@ -44,6 +44,7 @@ class Video extends Cover {

 	/**
 	 * Extract poster URL from block attributes.
+	 * Returns raw URL - escaping should happen at the final output context.
 	 *
 	 * @param array  $block_attrs Block attributes.
 	 * @param string $block_content Original block content (unused, kept for consistency).
@@ -51,8 +52,10 @@ class Video extends Cover {
 	 */
 	private function extract_poster_url( array $block_attrs, string $block_content ): string {
 		// Check for poster attribute.
+		// Use esc_url_raw() to sanitize without HTML entity encoding.
+		// Final escaping happens in Cover block based on output context.
 		if ( ! empty( $block_attrs['poster'] ) ) {
-			return esc_url( $block_attrs['poster'] );
+			return esc_url_raw( $block_attrs['poster'] );
 		}

 		return '';
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
index 15ea44c5ed..a20d0ecb69 100644
--- 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
@@ -213,4 +213,21 @@ class Cover_Test extends \Email_Editor_Integration_Test_Case {
 		$this->assertStringNotContainsString( 'wp-block-cover__background', $rendered );
 		$this->assertStringNotContainsString( 'background-color:#4b74b2', $rendered );
 	}
+
+	/**
+	 * Test that background image URLs with query parameters work correctly
+	 */
+	public function test_background_image_urls_with_query_parameters_work_correctly(): void {
+		$parsed_cover                 = $this->parsed_cover;
+		$parsed_cover['attrs']['url'] = 'https://example.com/background.jpg?w=500&h=281';
+
+		$rendered = $this->cover_renderer->render( '', $parsed_cover, $this->rendering_context );
+
+		// Should contain background-image with query parameters.
+		// The key test is that background-image is present (not stripped by WP_Style_Engine).
+		$this->assertStringContainsString( 'background-image', $rendered, 'Background image should be present in CSS' );
+		// Verify query parameters are present.
+		$this->assertStringContainsString( 'w=500', $rendered, 'Query parameters should be present' );
+		$this->assertStringContainsString( 'h=281', $rendered, 'Query parameters should be present' );
+	}
 }
diff --git a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Embed_Test.php b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Embed_Test.php
index 1425d8f62e..e5e8b1f348 100644
--- a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Embed_Test.php
+++ b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Embed_Test.php
@@ -137,15 +137,6 @@ class Embed_Test extends \Email_Editor_Integration_Test_Case {
 		$this->assertStringNotContainsString( 'Listen on Spotify', $rendered );
 	}

-	/**
-	 * Test that embed block uses default label when no custom label provided
-	 */
-	public function test_uses_default_label_when_no_custom_label(): void {
-		$rendered = $this->embed_renderer->render( $this->parsed_spotify_embed['innerHTML'], $this->parsed_spotify_embed, $this->rendering_context );
-
-		$this->assertStringContainsString( 'Listen on Spotify', $rendered );
-	}
-
 	/**
 	 * Test that embed block handles email attributes for spacing
 	 */
@@ -456,43 +447,6 @@ class Embed_Test extends \Email_Editor_Integration_Test_Case {
 		$this->assertStringContainsString( 'alt="Play"', $rendered );
 	}

-	/**
-	 * Test that YouTube embed handles email attributes for spacing
-	 */
-	public function test_youtube_embed_handles_email_attributes_for_spacing(): void {
-		$parsed_youtube_with_spacing                = $this->parsed_youtube_embed;
-		$parsed_youtube_with_spacing['email_attrs'] = array(
-			'margin' => '20px 0',
-		);
-
-		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $parsed_youtube_with_spacing, $this->rendering_context );
-
-		// Email attributes are handled by the cover block renderer.
-		$this->assertStringContainsString( 'https://img.youtube.com/vi/dQw4w9WgXcQ/0.jpg', $rendered );
-		$this->assertStringContainsString( 'play2x.png', $rendered );
-	}
-
-	/**
-	 * Test that YouTube embed includes proper security attributes
-	 */
-	public function test_youtube_embed_includes_proper_security_attributes(): void {
-		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $this->parsed_youtube_embed, $this->rendering_context );
-
-		// The play button may or may not be wrapped in a link depending on post context.
-		// In test environment, there may not be a valid post URL, so the play button is just an image.
-		$this->assertStringContainsString( 'alt="Play"', $rendered );
-		$this->assertStringContainsString( 'play2x.png', $rendered );
-	}
-
-	/**
-	 * Test that YouTube embed includes proper accessibility attributes
-	 */
-	public function test_youtube_embed_includes_proper_accessibility_attributes(): void {
-		$rendered = $this->embed_renderer->render( $this->parsed_youtube_embed['innerHTML'], $this->parsed_youtube_embed, $this->rendering_context );
-
-		$this->assertStringContainsString( 'alt="Play"', $rendered );
-	}
-
 	/**
 	 * Test that YouTube embed detects YouTube by providerNameSlug
 	 */
@@ -579,4 +533,173 @@ class Embed_Test extends \Email_Editor_Integration_Test_Case {
 		$this->assertStringContainsString( 'https://img.youtube.com/vi/invalid/0.jpg', $rendered );
 		$this->assertStringContainsString( 'play2x.png', $rendered );
 	}
+
+	/**
+	 * Test that VideoPress embed is detected and renders as video player, including handling URLs with query parameters.
+	 */
+	public function test_renders_videopress_embed(): void {
+		// Mock the oEmbed HTTP response to avoid external calls in CI.
+		// Return a thumbnail URL with query parameters to test the URL encoding fix.
+		$mock_thumbnail_url   = 'https://videos.files.wordpress.com/BZHMfMfN/thumbnail.jpg?w=500&h=281';
+		$mock_oembed_response = wp_json_encode(
+			array(
+				'type'          => 'video',
+				'thumbnail_url' => $mock_thumbnail_url,
+				'title'         => 'Test Video',
+			)
+		);
+
+		// Use pre_http_request filter to intercept oEmbed HTTP calls.
+		$filter_callback = function ( $preempt, $args, $url ) use ( $mock_oembed_response ) {
+			// Intercept VideoPress oEmbed requests.
+			if ( strpos( $url, 'public-api.wordpress.com/oembed' ) !== false ) {
+				return array(
+					'response' => array( 'code' => 200 ),
+					'body'     => $mock_oembed_response,
+				);
+			}
+			return $preempt;
+		};
+
+		add_filter( 'pre_http_request', $filter_callback, 10, 3 );
+
+		$parsed_videopress_embed = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'url'              => 'https://videopress.com/v/BZHMfMfN?w=500&h=281',
+				'type'             => 'video',
+				'providerNameSlug' => 'videopress',
+				'responsive'       => true,
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-video is-provider-videopress wp-block-embed-videopress"><div class="wp-block-embed__wrapper">https://videopress.com/v/BZHMfMfN?w=500&h=281</div></figure>',
+		);
+
+		try {
+			$rendered = $this->embed_renderer->render( $parsed_videopress_embed['innerHTML'], $parsed_videopress_embed, $this->rendering_context );
+		} finally {
+			remove_filter( 'pre_http_request', $filter_callback, 10 );
+		}
+
+		// Should detect VideoPress and render as video with thumbnail.
+		$this->assertNotEmpty( $rendered );
+		$this->assertStringContainsString( 'play2x.png', $rendered, 'VideoPress embed should render with play button' );
+		// Verify background-image is present (our fix ensures it's not stripped).
+		$this->assertStringContainsString( 'background-image', $rendered, 'Background image should be present in CSS' );
+		// Verify query parameters are present (as &amp; in HTML, which is correct).
+		$this->assertStringContainsString( 'w=500', $rendered, 'Query parameters should be present' );
+		$this->assertStringContainsString( 'h=281', $rendered, 'Query parameters should be present' );
+	}
+
+	/**
+	 * Test that VideoPress embed detects VideoPress by providerNameSlug
+	 */
+	public function test_videopress_embed_detects_videopress_by_provider_name_slug(): void {
+		$parsed_videopress_by_slug = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'providerNameSlug' => 'videopress',
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-video is-provider-videopress"><div class="wp-block-embed__wrapper">Some content</div></figure>',
+		);
+
+		$rendered = $this->embed_renderer->render( $parsed_videopress_by_slug['innerHTML'], $parsed_videopress_by_slug, $this->rendering_context );
+
+		// Should return graceful fallback link since provider is detected but no URL is available for thumbnail extraction.
+		$this->assertStringContainsString( '<a href="https://videopress.com/"', $rendered );
+		$this->assertStringContainsString( 'Watch on VideoPress', $rendered );
+		$this->assertStringContainsString( 'target="_blank"', $rendered );
+		$this->assertStringContainsString( 'rel="noopener nofollow"', $rendered );
+	}
+
+	/**
+	 * Test that VideoPress embed detects VideoPress by URL in attributes
+	 */
+	public function test_videopress_embed_detects_videopress_by_url_in_attributes(): void {
+		// Mock the oEmbed HTTP response to avoid external calls in CI.
+		$mock_thumbnail_url   = 'https://videos.files.wordpress.com/BZHMfMfN/thumbnail.jpg';
+		$mock_oembed_response = wp_json_encode(
+			array(
+				'type'          => 'video',
+				'thumbnail_url' => $mock_thumbnail_url,
+				'title'         => 'Test Video',
+			)
+		);
+
+		// Use pre_http_request filter to intercept oEmbed HTTP calls.
+		$filter_callback = function ( $preempt, $args, $url ) use ( $mock_oembed_response ) {
+			if ( strpos( $url, 'public-api.wordpress.com/oembed' ) !== false ) {
+				return array(
+					'response' => array( 'code' => 200 ),
+					'body'     => $mock_oembed_response,
+				);
+			}
+			return $preempt;
+		};
+
+		add_filter( 'pre_http_request', $filter_callback, 10, 3 );
+
+		$parsed_videopress_by_url = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'url' => 'https://videopress.com/v/BZHMfMfN',
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-video is-provider-videopress"><div class="wp-block-embed__wrapper">https://videopress.com/v/BZHMfMfN</div></figure>',
+		);
+
+		try {
+			$rendered = $this->embed_renderer->render( $parsed_videopress_by_url['innerHTML'], $parsed_videopress_by_url, $this->rendering_context );
+		} finally {
+			remove_filter( 'pre_http_request', $filter_callback, 10 );
+		}
+
+		// Should detect VideoPress by URL domain and render with thumbnail.
+		$this->assertNotEmpty( $rendered );
+		$this->assertStringContainsString( 'background-image', $rendered, 'VideoPress embed should have background image' );
+	}
+
+	/**
+	 * Test that VideoPress embed detects video.wordpress.com domain
+	 */
+	public function test_videopress_embed_detects_video_wordpress_com_domain(): void {
+		// Mock the oEmbed HTTP response to avoid external calls in CI.
+		$mock_thumbnail_url   = 'https://videos.files.wordpress.com/BZHMfMfN/thumbnail.jpg';
+		$mock_oembed_response = wp_json_encode(
+			array(
+				'type'          => 'video',
+				'thumbnail_url' => $mock_thumbnail_url,
+				'title'         => 'Test Video',
+			)
+		);
+
+		// Use pre_http_request filter to intercept oEmbed HTTP calls.
+		$filter_callback = function ( $preempt, $args, $url ) use ( $mock_oembed_response ) {
+			if ( strpos( $url, 'public-api.wordpress.com/oembed' ) !== false ) {
+				return array(
+					'response' => array( 'code' => 200 ),
+					'body'     => $mock_oembed_response,
+				);
+			}
+			return $preempt;
+		};
+
+		add_filter( 'pre_http_request', $filter_callback, 10, 3 );
+
+		$parsed_videopress_wordpress_com = array(
+			'blockName' => 'core/embed',
+			'attrs'     => array(
+				'url' => 'https://video.wordpress.com/v/BZHMfMfN',
+			),
+			'innerHTML' => '<figure class="wp-block-embed is-type-video"><div class="wp-block-embed__wrapper">https://video.wordpress.com/v/BZHMfMfN</div></figure>',
+		);
+
+		try {
+			$rendered = $this->embed_renderer->render( $parsed_videopress_wordpress_com['innerHTML'], $parsed_videopress_wordpress_com, $this->rendering_context );
+		} finally {
+			remove_filter( 'pre_http_request', $filter_callback, 10 );
+		}
+
+		// Should detect VideoPress by video.wordpress.com domain and render with thumbnail.
+		$this->assertNotEmpty( $rendered );
+		$this->assertStringContainsString( 'background-image', $rendered, 'VideoPress embed should have background image' );
+	}
 }
diff --git a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Video_Test.php b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Video_Test.php
index 1094f97dba..b12df3aa72 100644
--- a/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Video_Test.php
+++ b/packages/php/email-editor/tests/integration/Integrations/Core/Renderer/Blocks/Video_Test.php
@@ -233,4 +233,27 @@ class Video_Test extends \Email_Editor_Integration_Test_Case {
 		// Should apply minimum height for consistent video block appearance.
 		$this->assertStringContainsString( 'min-height:390px', $rendered );
 	}
+
+	/**
+	 * Test that poster URLs with query parameters work correctly
+	 */
+	public function test_poster_urls_with_query_parameters_work_correctly(): void {
+		$parsed_video_with_query = array(
+			'blockName' => 'core/video',
+			'attrs'     => array(
+				'poster' => 'https://example.com/poster.jpg?w=500&h=281',
+				'title'  => 'Sample Video',
+			),
+			'innerHTML' => '<figure class="wp-block-video"><video controls poster="https://example.com/poster.jpg?w=500&h=281"><source src="https://example.com/video.mp4" type="video/mp4" /></video></figure>',
+		);
+
+		$rendered = $this->video_renderer->render( '', $parsed_video_with_query, $this->rendering_context );
+
+		// Should contain background-image with query parameters.
+		// The key test is that background-image is present (not stripped by WP_Style_Engine).
+		$this->assertStringContainsString( 'background-image', $rendered, 'Background image should be present in CSS' );
+		// Verify query parameters are present.
+		$this->assertStringContainsString( 'w=500', $rendered, 'Query parameters should be present' );
+		$this->assertStringContainsString( 'h=281', $rendered, 'Query parameters should be present' );
+	}
 }