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 & 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 & 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' );
+ }
}