Commit 5a0205dac9 for woocommerce
commit 5a0205dac90e1c2840e593fd963c56de4616aa5d
Author: Tony Arcangelini <33258733+arcangelini@users.noreply.github.com>
Date: Wed Feb 11 21:07:13 2026 +0100
Email Editor: Fix YouTube URL truncation in subscription emails (#63252)
URLs with underscores (e.g. YouTube video IDs) were truncated because the
URL-matching regex excluded valid characters like _, ., ~, +, and #.
- Extract shared URL regex into Html_Processing_Helper::extract_url_from_text()
- Pass video URL via attrs in Embed→Video flow to avoid redundant regex extraction
- Add integration tests for URLs with underscores
diff --git a/packages/php/email-editor/changelog/fix-youtube-url-truncation-in-emails b/packages/php/email-editor/changelog/fix-youtube-url-truncation-in-emails
new file mode 100644
index 0000000000..6109dd4b8d
--- /dev/null
+++ b/packages/php/email-editor/changelog/fix-youtube-url-truncation-in-emails
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix YouTube URL truncation in subscription emails when video IDs contain underscores.
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 c9a65c8b27..7715cce102 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
@@ -359,9 +359,7 @@ class Embed extends Abstract_Block_Renderer {
$text_content = $body_element->textContent; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// Look for HTTP/HTTPS URLs in the text content.
- if ( preg_match( '/(?<![a-zA-Z0-9.-])https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[a-zA-Z0-9\/?=&%-]*(?![a-zA-Z0-9.-])/', $text_content, $matches ) ) {
- $url = $matches[0];
- }
+ $url = Html_Processing_Helper::extract_url_from_text( $text_content );
}
}
}
@@ -501,7 +499,8 @@ class Embed extends Abstract_Block_Renderer {
$mock_video_block = array(
'blockName' => 'core/video',
'attrs' => array(
- 'poster' => $poster_url,
+ 'poster' => $poster_url,
+ 'videoUrl' => $url,
),
'innerHTML' => '<figure class="wp-block-video wp-block-embed is-type-video is-provider-' . esc_attr( $provider ) . '"><div class="wp-block-embed__wrapper">' . esc_url( $url ) . '</div></figure>',
);
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 4a9ffaeacf..183817c34b 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
@@ -10,6 +10,7 @@ namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks;
use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Dom_Document_Helper;
+use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Html_Processing_Helper;
/**
* Video block renderer.
@@ -87,8 +88,8 @@ class Video extends Cover {
$inner_html = $dom_helper->get_element_inner_html( $wrapper_element );
// Look for HTTP/HTTPS URLs in the inner HTML content.
- if ( preg_match( '/(?<![a-zA-Z0-9.-])https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[a-zA-Z0-9\/?=&%-]*(?![a-zA-Z0-9.-])/', $inner_html, $matches ) ) {
- $url = $matches[0];
+ $url = Html_Processing_Helper::extract_url_from_text( $inner_html );
+ if ( ! empty( $url ) ) {
// Decode HTML entities and validate URL.
$url = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
@@ -113,10 +114,12 @@ class Video extends Cover {
$block_attrs = $video_block['attrs'] ?? array();
$block_content = $video_block['innerHTML'] ?? '';
- // Extract video URL from block content, fall back to post URL.
- // Priority: 1) Video URL (if found), 2) Post permalink (fallback).
- $video_url = $this->extract_video_url( $block_content );
- $link_url = ! empty( $video_url ) ? $video_url : $this->get_current_post_url();
+ // Extract video URL: 1) From attrs (e.g., passed by Embed renderer), 2) From content, 3) Post permalink.
+ $video_url = $block_attrs['videoUrl'] ?? '';
+ if ( empty( $video_url ) ) {
+ $video_url = $this->extract_video_url( $block_content );
+ }
+ $link_url = ! empty( $video_url ) ? $video_url : $this->get_current_post_url();
return array(
'blockName' => 'core/cover',
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 e650b3dc7a..bb75fb5732 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
@@ -574,6 +574,20 @@ class Html_Processing_Helper {
return '<img ' . implode( ' ', $sanitized_attributes ) . '>';
}
+ /**
+ * Extract the first HTTP/HTTPS URL from a text string.
+ *
+ * @param string $text Text to search for URLs.
+ * @return string Extracted URL or empty string if not found.
+ */
+ public static function extract_url_from_text( string $text ): string {
+ if ( preg_match( '/(?<![a-zA-Z0-9.-])https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[a-zA-Z0-9\/?=&%_.~+#-]*(?![a-zA-Z0-9._~+#-])/', $text, $matches ) ) {
+ return $matches[0];
+ }
+
+ return '';
+ }
+
/**
* Sanitize inline styles for image elements - only allow safe properties for email rendering.
*
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 e5e8b1f348..a1fd96bb7c 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
@@ -590,6 +590,28 @@ class Embed_Test extends \Email_Editor_Integration_Test_Case {
$this->assertStringContainsString( 'h=281', $rendered, 'Query parameters should be present' );
}
+ /**
+ * Test that YouTube embed correctly handles URLs with underscores in the video ID
+ */
+ public function test_youtube_embed_handles_urls_with_underscores(): void {
+ $parsed_youtube_underscore = array(
+ 'blockName' => 'core/embed',
+ 'attrs' => array(
+ 'url' => 'https://www.youtube.com/watch?v=dQw4w9_WgXcQ',
+ 'type' => 'video',
+ 'providerNameSlug' => 'youtube',
+ 'responsive' => true,
+ ),
+ 'innerHTML' => '<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube"><div class="wp-block-embed__wrapper">https://www.youtube.com/watch?v=dQw4w9_WgXcQ</div></figure>',
+ );
+
+ $rendered = $this->embed_renderer->render( $parsed_youtube_underscore['innerHTML'], $parsed_youtube_underscore, $this->rendering_context );
+
+ // Should extract full video ID including underscore.
+ $this->assertStringContainsString( 'https://img.youtube.com/vi/dQw4w9_WgXcQ/0.jpg', $rendered );
+ $this->assertStringContainsString( 'play2x.png', $rendered );
+ }
+
/**
* Test that VideoPress embed detects VideoPress by providerNameSlug
*/
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 b12df3aa72..cd9c0e7c48 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
@@ -234,6 +234,24 @@ class Video_Test extends \Email_Editor_Integration_Test_Case {
$this->assertStringContainsString( 'min-height:390px', $rendered );
}
+ /**
+ * Test that video URLs with underscores are not truncated
+ */
+ public function test_video_urls_with_underscores_are_not_truncated(): void {
+ $parsed_video_underscore = array(
+ 'blockName' => 'core/video',
+ 'attrs' => array(
+ 'poster' => 'https://example.com/poster.jpg',
+ ),
+ 'innerHTML' => '<figure class="wp-block-video wp-block-embed is-type-video is-provider-youtube"><div class="wp-block-embed__wrapper">https://www.youtube.com/watch?v=dQw4w9_WgXcQ</div></figure>',
+ );
+
+ $rendered = $this->video_renderer->render( '', $parsed_video_underscore, $this->rendering_context );
+
+ // The play button link should contain the full YouTube URL with the underscore intact.
+ $this->assertStringContainsString( 'dQw4w9_WgXcQ', $rendered, 'URL should not be truncated at underscore' );
+ }
+
/**
* Test that poster URLs with query parameters work correctly
*/