Commit bd1319223eb for woocommerce

commit bd1319223eba532b7f633d1ab50edc88b11f13ed
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Wed Jul 29 18:14:48 2026 +0300

    Prevent Review Order page creation from restoring the Shop page (#66959)

    * fix: scope review order page creation

    Context: Customer Review Request creates a managed Review Order page when the feature is enabled and the host page is missing.

    Problem: that internal repair path called the full WooCommerce page creation routine, so an intentionally empty shop page option could be repopulated as a side effect.

    Solution: limit the internal repair call to the review_order page while keeping the permanent page injection for explicit full repair callers. Add a regression test for the empty shop page setting.

    Refs #35361

    * fix(order-reviews): Enforce scoped page repair

    The initial scope filter ran at priority 100 and re-injected the Review Order definition. Later callbacks could add Shop back, while earlier extension customizations to Review Order were overwritten.

    Intersect the final incoming page definitions at PHP_INT_MAX, remove the filter at the same priority, and cover late callbacks, customization preservation, filter cleanup, and subsequent full repair behavior.

    Refs #35361

    * Restore Shop page option in empty-shop-page test cleanup

    The test's finally block deleted woocommerce_shop_page_id via
    reset_shop_pages() without restoring the pre-test value. Option values
    survive the per-test DB-transaction rollback, so the option stayed
    deleted for the rest of the PHPUnit process — a shared-state risk for
    later tests assuming an ambient Shop page.

    Snapshot the option before staging and restore it in finally, matching
    the snapshot/restore convention in class-wc-query-test.php and
    CoreBreadcrumbsCompatibilityTest.php.

    Refs #35361

    * Update inject_review_order_page() docblock for the scoped repair

    * Fix test reset helpers to also delete trashed pages

    * Fix changelog entry to render its text in the body

diff --git a/plugins/woocommerce/changelog/fix-35361-scope-review-order-page-creation b/plugins/woocommerce/changelog/fix-35361-scope-review-order-page-creation
new file mode 100644
index 00000000000..6994f951bc2
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-35361-scope-review-order-page-creation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Keep the Review Order page from restoring an intentionally empty Shop page setting. If your Shop page setting was cleared by this bug, clear it once more after updating.
diff --git a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
index 9cabf5ebfdc..f1779b5ff6a 100644
--- a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
+++ b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
@@ -151,9 +151,22 @@ class Endpoint {
 			return;
 		}

-		// No managed page anywhere. The permanent `woocommerce_create_pages`
-		// filter (registered in `init()`) makes the call inject our entry.
-		\WC_Install::create_pages();
+		// No managed page anywhere. Scope this internal repair to our own page
+		// so merchants' intentional choices for other WC pages are not repaired.
+		$create_review_order_page_only = static function ( $pages ) {
+			if ( ! is_array( $pages ) ) {
+				return $pages;
+			}
+
+			return array_intersect_key( $pages, array( self::PAGE_KEY => true ) );
+		};
+
+		add_filter( 'woocommerce_create_pages', $create_review_order_page_only, PHP_INT_MAX );
+		try {
+			\WC_Install::create_pages();
+		} finally {
+			remove_filter( 'woocommerce_create_pages', $create_review_order_page_only, PHP_INT_MAX );
+		}

 		// Defer the rewrite flush to wp_loaded; rewrite_rule fires later on init.
 		update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
@@ -163,8 +176,9 @@ class Endpoint {
 	 * Append the Review Order page to any caller of
 	 * `WC_Install::create_pages()` — keeps Status → Tools' "Create default
 	 * pages" repair path and any third-party callers seeded with our page
-	 * whenever the feature is on, without having to call create_pages()
-	 * with a one-off filter in `maybe_create_host_page()`.
+	 * whenever the feature is on. `maybe_create_host_page()` relies on this
+	 * injection too, layering its own scoping filter on top so its internal
+	 * repair creates only this page.
 	 *
 	 * @since 10.8.0
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
index 36c89b59d08..d54b15922d4 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
@@ -713,6 +713,78 @@ class EndpointTest extends WC_Unit_Test_Case {
 		$this->assertSame( 'yes', get_option( 'woocommerce_review_order_flush_rewrite_pending' ) );
 	}

+	/**
+	 * @testdox maybe_create_host_page() preserves an intentionally empty Shop page setting.
+	 */
+	public function test_maybe_create_host_page_preserves_empty_shop_page_option(): void {
+		// Snapshot the Shop page option before staging: option values survive the
+		// per-test DB-transaction rollback (see setUp()), so the `finally` block
+		// must restore the pre-test value rather than leave it deleted for later
+		// tests in the same PHPUnit process.
+		$original_shop_page_id = get_option( 'woocommerce_shop_page_id' );
+
+		$this->reset_review_order_pages();
+		$this->reset_shop_pages();
+
+		$shop_page_id = (int) wp_insert_post(
+			array(
+				'post_type'   => 'page',
+				'post_status' => 'publish',
+				'post_title'  => 'Shop',
+				'post_name'   => 'shop',
+			)
+		);
+		update_option( 'woocommerce_shop_page_id', '' );
+		delete_option( 'woocommerce_review_order_flush_rewrite_pending' );
+
+		$customize_review_order_page = static function ( $pages ) {
+			if ( isset( $pages[ Endpoint::PAGE_KEY ] ) ) {
+					$pages[ Endpoint::PAGE_KEY ]['title'] = 'Customized review order page';
+
+					$pages[ Endpoint::PAGE_KEY ]['content'] .= '<!-- extension-customization -->';
+			}
+			return $pages;
+		};
+		$restore_shop_page           = static function ( $pages ) {
+			$pages['shop'] = array(
+				'name'    => 'shop',
+				'title'   => 'Shop',
+				'content' => '',
+			);
+			return $pages;
+		};
+
+		add_filter( 'woocommerce_create_pages', array( $this->endpoint, 'inject_review_order_page' ) );
+		add_filter( 'woocommerce_create_pages', $customize_review_order_page, 50 );
+		add_filter( 'woocommerce_create_pages', $restore_shop_page, 200 );
+		try {
+			$this->endpoint->maybe_create_host_page();
+
+			$this->assertSame( '', get_option( 'woocommerce_shop_page_id' ), 'intentional empty shop page option should stay empty' );
+
+			$review_order_page = get_post( (int) wc_get_page_id( Endpoint::PAGE_KEY ) );
+			$this->assertInstanceOf( \WP_Post::class, $review_order_page, 'review order page should still be created' );
+			$this->assertSame( 'Customized review order page', $review_order_page->post_title, 'earlier page-definition customizations should be preserved' );
+			$this->assertStringContainsString( '<!-- extension-customization -->', $review_order_page->post_content, 'earlier content customizations should be preserved' );
+
+			\WC_Install::create_pages();
+			$this->assertSame( $shop_page_id, (int) wc_get_page_id( 'shop' ), 'a later full repair should adopt the staged Shop page' );
+		} finally {
+			remove_filter( 'woocommerce_create_pages', array( $this->endpoint, 'inject_review_order_page' ) );
+			remove_filter( 'woocommerce_create_pages', $customize_review_order_page, 50 );
+			remove_filter( 'woocommerce_create_pages', $restore_shop_page, 200 );
+			$this->reset_review_order_pages();
+			$this->reset_shop_pages();
+
+			// Restore the pre-test Shop page option. `reset_shop_pages()` deletes
+			// it for a clean slate; put back whatever was there before so the
+			// suite's shared option state is unchanged by this test.
+			if ( false !== $original_shop_page_id ) {
+				update_option( 'woocommerce_shop_page_id', $original_shop_page_id );
+			}
+		}
+	}
+
 	/**
 	 * @testdox The `woocommerce_create_pages` filter injects the Review Order entry so any caller of `WC_Install::create_pages()` (e.g. Status → Tools repair) seeds the page.
 	 */
@@ -731,14 +803,15 @@ class EndpointTest extends WC_Unit_Test_Case {
 	/**
 	 * Remove every page that could match the Review Order lookup, plus the
 	 * stored option, so a test can stage a clean slate before exercising
-	 * `maybe_create_host_page()`.
+	 * `maybe_create_host_page()`. Trashed pages must go too: `'any'` skips
+	 * `trash`, but `wc_create_page()` untrashes matching pages.
 	 */
 	private function reset_review_order_pages(): void {
 		$candidates = get_posts(
 			array(
 				'name'             => 'review-order',
 				'post_type'        => 'page',
-				'post_status'      => 'any',
+				'post_status'      => array_keys( get_post_stati() ),
 				'numberposts'      => -1,
 				'suppress_filters' => false,
 			)
@@ -748,4 +821,26 @@ class EndpointTest extends WC_Unit_Test_Case {
 		}
 		delete_option( 'woocommerce_review_order_page_id' );
 	}
+
+	/**
+	 * Remove every page that could match the Shop page lookup, plus the
+	 * stored option, so a test can stage an intentionally empty setting.
+	 * Trashed pages must go too: `'any'` skips `trash`, but
+	 * `wc_create_page()` untrashes matching pages.
+	 */
+	private function reset_shop_pages(): void {
+		$candidates = get_posts(
+			array(
+				'name'             => 'shop',
+				'post_type'        => 'page',
+				'post_status'      => array_keys( get_post_stati() ),
+				'numberposts'      => -1,
+				'suppress_filters' => false,
+			)
+		);
+		foreach ( $candidates as $page ) {
+			wp_delete_post( (int) $page->ID, true );
+		}
+		delete_option( 'woocommerce_shop_page_id' );
+	}
 }