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
 	 */