Commit 146f12270f1 for woocommerce

commit 146f12270f13fce211e72b51f32da90dab97d66c
Author: Rostislav Wolný <1082140+costasovo@users.noreply.github.com>
Date:   Tue Sep 29 15:23:33 2026 +0200

    Restore block data attributes even when another plugin errors during an email render (#69148)

    * Restore the data attributes filter before other render end callbacks

    WooCommerce suspends its data- attributes filter while an email is built
    and hooks the restore on the email editor's render end action. The
    restore ran at the default priority, so another plugin's callback on the
    same action could throw first and skip it, leaving the filter suspended
    for the rest of the request. Blocks rendered later in that request then
    lost their data- attributes, which the cart and checkout blocks read
    back.

    Running the restore first means no other callback can prevent it. A
    render that never fires the end action at all is still repaired by the
    next one.

diff --git a/plugins/woocommerce/changelog/wooairr-417-restore-data-attributes-first b/plugins/woocommerce/changelog/wooairr-417-restore-data-attributes-first
new file mode 100644
index 00000000000..2675941be59
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooairr-417-restore-data-attributes-first
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep block data- attributes working for the rest of a request when another plugin errors while an email is being built.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php
index 4f04fa08a55..9db33000532 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php
@@ -25,6 +25,13 @@ final class BlockTypesController {
 	 */
 	private const DATA_ATTRIBUTES_PRIORITY = 10;

+	/**
+	 * Priority of the callback that ends the data attributes suspension. The lowest possible priority puts it ahead
+	 * of other callbacks on the render end action, so one of those throwing cannot leave the filter suspended for
+	 * the rest of the request.
+	 */
+	private const RESTORE_DATA_ATTRIBUTES_PRIORITY = PHP_INT_MIN;
+
 	/**
 	 * Instance of the asset API.
 	 *
@@ -180,19 +187,19 @@ final class BlockTypesController {
 		}

 		remove_filter( 'render_block', $data_attributes_callback, self::DATA_ATTRIBUTES_PRIORITY );
-		add_action( 'woocommerce_email_editor_render_end', array( $this, 'restore_data_attributes_after_email_render' ) );
+		add_action( 'woocommerce_email_editor_render_end', array( $this, 'restore_data_attributes_after_email_render' ), self::RESTORE_DATA_ATTRIBUTES_PRIORITY );
 	}

 	/**
 	 * Add the data- attributes filter back once the email render has ended.
 	 *
-	 * Only a suspension hooks this, so a render whose end action never reached it is repaired by the next one.
+	 * Only a suspension hooks this, and a render that never fired the end action is repaired by the next one.
 	 *
 	 * @internal
 	 */
 	public function restore_data_attributes_after_email_render(): void {
 		add_filter( 'render_block', array( $this, 'add_data_attributes' ), self::DATA_ATTRIBUTES_PRIORITY, 2 );
-		remove_action( 'woocommerce_email_editor_render_end', array( $this, 'restore_data_attributes_after_email_render' ) );
+		remove_action( 'woocommerce_email_editor_render_end', array( $this, 'restore_data_attributes_after_email_render' ), self::RESTORE_DATA_ATTRIBUTES_PRIORITY );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
index afab487d21e..18e9d5d01c5 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
@@ -530,9 +530,9 @@ HTML;
 	}

 	/**
-	 * @testdox A render whose end action never reaches the restore is repaired by the next render.
+	 * @testdox The data attributes filter is put back even when another render end callback throws.
 	 */
-	public function test_data_attributes_filter_is_restored_by_a_later_render(): void {
+	public function test_data_attributes_filter_is_restored_when_another_end_callback_throws(): void {
 		$throwing_callback = function () {
 			throw new \RuntimeException( 'Another plugin failed to clean up' );
 		};
@@ -541,12 +541,57 @@ HTML;
 		do_action( 'woocommerce_email_editor_render_start' );
 		try {
 			do_action( 'woocommerce_email_editor_render_end' );
+			$this->fail( 'The throwing callback should have stopped the action.' );
 		} catch ( \RuntimeException $e ) {
 			$this->assertSame( 'Another plugin failed to clean up', $e->getMessage() );
 		} finally {
 			remove_action( 'woocommerce_email_editor_render_end', $throwing_callback, 5 );
 		}

+		$this->assertSame(
+			10,
+			$this->data_attributes_filter_priority(),
+			'A third-party callback that throws must not leave the filter suspended for the rest of the request.'
+		);
+	}
+
+	/**
+	 * @testdox The data attributes filter is put back even when a callback hooked at load time throws.
+	 */
+	public function test_data_attributes_filter_is_restored_when_an_earlier_hooked_callback_throws(): void {
+		$throwing_callback = function () {
+			throw new \RuntimeException( 'A plugin hooked at load time failed' );
+		};
+
+		// The common case: default priority, hooked before the render starts. Registration order alone used to
+		// decide whether the restore ran, since it is hooked during the render.
+		add_action( 'woocommerce_email_editor_render_end', $throwing_callback );
+		do_action( 'woocommerce_email_editor_render_start' );
+		try {
+			do_action( 'woocommerce_email_editor_render_end' );
+			$this->fail( 'The throwing callback should have stopped the action.' );
+		} catch ( \RuntimeException $e ) {
+			$this->assertSame( 'A plugin hooked at load time failed', $e->getMessage() );
+		} finally {
+			remove_action( 'woocommerce_email_editor_render_end', $throwing_callback );
+		}
+
+		$this->assertSame(
+			10,
+			$this->data_attributes_filter_priority(),
+			'The restore must run before a callback a plugin hooked at the default priority.'
+		);
+	}
+
+	/**
+	 * @testdox A render that never fires the end action is repaired by the next render.
+	 */
+	public function test_data_attributes_filter_is_restored_by_a_later_render(): void {
+		// An integration that fires the start action on its own, for example around its own rendering, and never
+		// fires the end action.
+		do_action( 'woocommerce_email_editor_render_start' );
+		$this->assertFalse( $this->data_attributes_filter_priority(), 'The filter should be suspended for the render.' );
+
 		do_action( 'woocommerce_email_editor_render_start' );
 		do_action( 'woocommerce_email_editor_render_end' );