Commit 8f46e9035ff for woocommerce
commit 8f46e9035ffc4c6706b61a369b6c09dcfb1175c4
Author: Mike Jolley <mike.jolley@me.com>
Date: Thu Nov 12 13:40:36 2026 +0000
Fix email addresses being reformatted on block theme order pages (#70665)
* Keep order emails and addresses unchanged by wptexturize in block themes
Block themes texturize the whole template after blocks render, which
changed emails such as o'neil--shop@48x17.com on order pages. On the
order received and view order pages, skip <address> content, and
render the Summary block email in an <address>.
* Move texturize exclusions into Internal\Orders\TexturizeExclusions
diff --git a/plugins/woocommerce/changelog/66192-fix-order-email-texturize b/plugins/woocommerce/changelog/66192-fix-order-email-texturize
new file mode 100644
index 00000000000..c0e3da0667b
--- /dev/null
+++ b/plugins/woocommerce/changelog/66192-fix-order-email-texturize
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show customer emails and addresses exactly as entered on order pages in block themes.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/order-confirmation/summary/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/order-confirmation/summary/style.scss
index db9e9d444df..c02c83bb07e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/order-confirmation/summary/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/order-confirmation/summary/style.scss
@@ -25,6 +25,12 @@
font-weight: inherit;
display: block;
}
+
+ // The email value is an <address>, which browsers show in italics.
+ > address.wc-block-order-confirmation-summary-list-item__value {
+ font-style: inherit;
+ margin: 0;
+ }
}
@media (max-width: #{$break-small}) {
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index fb7a1347049..73a00a5bc38 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -454,6 +454,7 @@ final class WooCommerce {
$container->get( Automattic\WooCommerce\Internal\Orders\PointOfSaleEmailHandler::class )->register();
$container->get( Automattic\WooCommerce\Internal\Orders\UnpaidOrderTimeoutMarker::class )->register();
$container->get( Automattic\WooCommerce\Internal\Orders\ProductSales::class )->register();
+ $container->get( Automattic\WooCommerce\Internal\Orders\TexturizeExclusions::class )->register();
$container->get( Automattic\WooCommerce\Internal\POS\POSController::class )->register();
$container->get( Automattic\WooCommerce\Internal\ShopperLists\ShopperListsController::class )->register();
$container->get( Automattic\WooCommerce\Internal\StockNotifications\StockNotifications::class )->register();
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Summary.php b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Summary.php
index 3560292ab02..b978d083989 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Summary.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/OrderConfirmation/Summary.php
@@ -32,13 +32,27 @@ class Summary extends AbstractOrderConfirmationBlock {
$content .= $this->render_summary_row( __( 'Order #:', 'woocommerce' ), $order->get_order_number() );
$content .= $this->render_summary_row( __( 'Date:', 'woocommerce' ), wc_format_datetime( $order->get_date_created() ) );
$content .= $this->render_summary_row( __( 'Total:', 'woocommerce' ), $order->get_formatted_order_total() );
- $content .= $this->render_summary_row( __( 'Email:', 'woocommerce' ), $order->get_billing_email() );
+ $content .= $this->render_email_row( (string) $order->get_billing_email() );
$content .= $this->render_summary_row( __( 'Payment:', 'woocommerce' ), $order->get_payment_method_title() );
$content .= '</ul>';
return $content;
}
+ /**
+ * Render the email row. The value is an `<address>`, which wptexturize() skips on order pages.
+ *
+ * @param string $email Billing email.
+ * @return string
+ */
+ private function render_email_row( string $email ): string {
+ if ( '' === $email ) {
+ return '';
+ }
+
+ return '<li class="wc-block-order-confirmation-summary-list-item"><span class="wc-block-order-confirmation-summary-list-item__key">' . esc_html__( 'Email:', 'woocommerce' ) . '</span> <address class="wc-block-order-confirmation-summary-list-item__value">' . esc_html( $email ) . '</address></li>';
+ }
+
/**
* Render row in the order summary.
*
diff --git a/plugins/woocommerce/src/Internal/Orders/TexturizeExclusions.php b/plugins/woocommerce/src/Internal/Orders/TexturizeExclusions.php
new file mode 100644
index 00000000000..050327aa025
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Orders/TexturizeExclusions.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * TexturizeExclusions class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\Orders;
+
+use Automattic\WooCommerce\Internal\RegisterHooksInterface;
+
+/**
+ * Stops wptexturize() changing customer details, such as emails like `o'neil--shop@48x17.com`.
+ *
+ * @internal Just for internal use.
+ *
+ * @since 11.4.0
+ */
+class TexturizeExclusions implements RegisterHooksInterface {
+
+ /**
+ * Register hooks and filters.
+ */
+ public function register(): void {
+ add_action( 'template_redirect', array( $this, 'skip_address_texturize_on_order_pages' ) );
+ }
+
+ /**
+ * Add `address` to the tags wptexturize() skips on the order received and view order pages.
+ *
+ * Block themes texturize the whole template HTML, including the customer details.
+ *
+ * @internal For exclusive usage within this class, backwards compatibility not guaranteed.
+ */
+ public function skip_address_texturize_on_order_pages(): void {
+ if ( wp_is_block_theme() && ( is_order_received_page() || is_view_order_page() ) ) {
+ add_filter( 'no_texturize_tags', array( $this, 'add_address_tag' ) );
+ }
+ }
+
+ /**
+ * Add `address` to the tags wptexturize() skips.
+ *
+ * @internal For exclusive usage within this class, backwards compatibility not guaranteed.
+ *
+ * @param mixed $tags HTML element names.
+ * @return mixed
+ */
+ public function add_address_tag( $tags ) {
+ if ( ! is_array( $tags ) || in_array( 'address', $tags, true ) ) {
+ return $tags;
+ }
+
+ $tags[] = 'address';
+ return $tags;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/SummaryTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/SummaryTest.php
new file mode 100644
index 00000000000..1c0ed137ba0
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/SummaryTest.php
@@ -0,0 +1,63 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\OrderConfirmation;
+
+use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\Summary as SummaryBlock;
+use WC_Order;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the Order Confirmation Summary block.
+ */
+class SummaryTest extends WC_Unit_Test_Case {
+
+ /**
+ * @testdox The billing email renders as an escaped address element.
+ */
+ public function test_billing_email_renders_in_address(): void {
+ $order = wc_create_order();
+ $order->set_billing_email( "o'neil--shop@48x17.com" );
+ $order->save();
+
+ $html = $this->render( $order );
+
+ $this->assertStringContainsString(
+ '<address class="wc-block-order-confirmation-summary-list-item__value">o'neil--shop@48x17.com</address>',
+ $html,
+ 'The email value should be an escaped address element, which wptexturize() skips on order pages.'
+ );
+ }
+
+ /**
+ * @testdox Without a billing email, the block renders no email row.
+ */
+ public function test_no_email_row_without_email(): void {
+ $order = wc_create_order();
+ $order->set_billing_email( '' );
+ $order->save();
+
+ $this->assertStringNotContainsString( 'Email:', $this->render( $order ), 'The email row should not render.' );
+ }
+
+ /**
+ * Render the block content with full view permission.
+ *
+ * @param WC_Order $order Order to render.
+ * @return string
+ */
+ private function render( WC_Order $order ): string {
+ $sut = new class() extends SummaryBlock {
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function __construct() {
+ }
+
+ // phpcs:ignore Squiz.Commenting.FunctionComment.Missing
+ public function render_content_proxy( $order, $permission ) {
+ return $this->render_content( $order, $permission );
+ }
+ };
+
+ return $sut->render_content_proxy( $order, 'full' );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Orders/TexturizeExclusionsTest.php b/plugins/woocommerce/tests/php/src/Internal/Orders/TexturizeExclusionsTest.php
new file mode 100644
index 00000000000..4a051561942
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Orders/TexturizeExclusionsTest.php
@@ -0,0 +1,139 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Orders;
+
+use Automattic\WooCommerce\Internal\Orders\TexturizeExclusions;
+use WC_Helper_Order;
+use WC_Order;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the TexturizeExclusions class.
+ *
+ * @covers \Automattic\WooCommerce\Internal\Orders\TexturizeExclusions
+ */
+class TexturizeExclusionsTest extends WC_Unit_Test_Case {
+
+ /**
+ * The System Under Test.
+ *
+ * @var TexturizeExclusions
+ */
+ private $sut;
+
+ /**
+ * Active theme before each test, restored in tearDown.
+ *
+ * @var string
+ */
+ private $original_theme;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->original_theme = get_stylesheet();
+ switch_theme( 'twentytwentyfour' );
+ $this->sut = new TexturizeExclusions();
+ }
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ switch_theme( $this->original_theme );
+ parent::tearDown();
+ }
+
+ /**
+ * @testdox On the order received and view order pages, address content is not texturized, while other text is.
+ *
+ * @testWith ["order-received"]
+ * ["view-order"]
+ *
+ * @param string $endpoint Order page endpoint.
+ */
+ public function test_skips_address_texturize_on_order_pages( string $endpoint ): void {
+ $this->go_to_order_page( $endpoint, WC_Helper_Order::create_order() );
+
+ $this->sut->skip_address_texturize_on_order_pages();
+ $text = $this->texturize_to_text( "<address>o'neil--shop@48x17.com</address><p>3x4 -- done</p>" );
+
+ $this->assertStringContainsString( "o'neil--shop@48x17.com", $text, 'Address content should not be texturized.' );
+ $this->assertStringContainsString( '3×4 — done', $text, 'Text outside the address should still be texturized.' );
+ }
+
+ /**
+ * @testdox Address content is texturized as usual on other pages.
+ */
+ public function test_texturizes_address_on_other_pages(): void {
+ $this->go_to( get_permalink( self::factory()->post->create( array( 'post_type' => 'page' ) ) ) );
+
+ $this->sut->skip_address_texturize_on_order_pages();
+
+ $this->assertStringNotContainsString(
+ "o'neil--shop@48x17.com",
+ $this->texturize_to_text( "<address>o'neil--shop@48x17.com</address>" ),
+ 'Address content should be texturized.'
+ );
+ }
+
+ /**
+ * @testdox Address content is texturized as usual on order pages in classic themes.
+ */
+ public function test_texturizes_address_on_order_pages_in_classic_themes(): void {
+ switch_theme( 'storefront' );
+ $this->assertFalse( wp_is_block_theme(), 'The test requires a classic theme.' );
+ $this->go_to_order_page( 'view-order', WC_Helper_Order::create_order() );
+
+ $this->sut->skip_address_texturize_on_order_pages();
+
+ $this->assertStringNotContainsString(
+ "o'neil--shop@48x17.com",
+ $this->texturize_to_text( "<address>o'neil--shop@48x17.com</address>" ),
+ 'Address content should be texturized.'
+ );
+ }
+
+ /**
+ * @testdox On the view order page, the customer details template shows the billing email and address as entered, after kses and wptexturize().
+ */
+ public function test_customer_details_template_survives_texturize_on_view_order_page(): void {
+ $order = WC_Helper_Order::create_order();
+ $order->set_billing_email( "o'neil--shop@48x17.com" );
+ $order->set_billing_address_1( '12--14 Main St' );
+ $order->save();
+ $this->go_to_order_page( 'view-order', $order );
+
+ $this->sut->skip_address_texturize_on_order_pages();
+ $text = $this->texturize_to_text( wp_kses_post( wc_get_template_html( 'order/order-details-customer.php', array( 'order' => $order ) ) ) );
+
+ $this->assertStringContainsString( "o'neil--shop@48x17.com", $text, 'The billing email should not be texturized.' );
+ $this->assertStringContainsString( '12--14 Main St', $text, 'The billing address should not be texturized.' );
+ }
+
+ /**
+ * Load an order endpoint page, so the WooCommerce conditional tags match.
+ *
+ * @param string $endpoint Order page endpoint.
+ * @param WC_Order $order Order to view.
+ */
+ private function go_to_order_page( string $endpoint, WC_Order $order ): void {
+ $page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
+ update_option( 'order-received' === $endpoint ? 'woocommerce_checkout_page_id' : 'woocommerce_myaccount_page_id', $page_id );
+
+ $this->go_to( add_query_arg( $endpoint, $order->get_id(), get_permalink( $page_id ) ) );
+ }
+
+ /**
+ * Texturize HTML and return its decoded text.
+ *
+ * @param string $html HTML to texturize.
+ * @return string
+ */
+ private function texturize_to_text( string $html ): string {
+ return html_entity_decode( wp_strip_all_tags( wptexturize( $html ) ), ENT_QUOTES | ENT_HTML5 );
+ }
+}