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' );
+ }
}