Commit b071f179ba3 for woocommerce

commit b071f179ba3a27bb2b77867a98942740d1d32926
Author: Ján Mikláš <neosinner@gmail.com>
Date:   Mon Aug 31 17:59:45 2026 +0200

    Defer endpoint rewrite flushes during installing requests (#67942)

    * Defer endpoint rewrite flushes during installing requests

    Route the Checkout Link and Review Order rewrite flushes through the
    WooCommerce install guard so requests made while WordPress is installing
    queue the flush instead of persisting an incomplete rewrite graph. Each
    caller keeps its hard or soft flush semantics, and the pending-option caches
    are evicted at the write site so a persistent object cache cannot hide the
    queued flag from the next normal request.

    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01GU3W7KnYnsMo2dTVk65LPB

    * Fix install-mode rewrite rules assertion

    ---------

    Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-7579-installing-rewrite-call-sites b/plugins/woocommerce/changelog/fix-7579-installing-rewrite-call-sites
new file mode 100644
index 00000000000..c95b3acca02
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-7579-installing-rewrite-call-sites
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent endpoint rewrite flushes from persisting incomplete rules during WordPress installation requests.
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutLink.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutLink.php
index 97fea7b33f5..f4f16e511e6 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutLink.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutLink.php
@@ -38,7 +38,7 @@ class CheckoutLink {

 		// maybe flush rewrite rules if it was not previously in the option.
 		if ( ! isset( $rules[ $regex ] ) ) {
-			flush_rewrite_rules();
+			\WC_Post_Types::flush_rewrite_rules();
 		}
 	}

diff --git a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
index beb17c2d3ff..4fa8a52c8d2 100644
--- a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
+++ b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
@@ -130,7 +130,7 @@ class Endpoint {
 				$needs_save = true;
 			}
 			if ( $needs_save ) {
-				update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
+				$this->queue_pending_rewrite_flush();
 			}
 			return;
 		}
@@ -146,7 +146,7 @@ class Endpoint {
 						'post_status' => 'publish',
 					)
 				);
-				update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
+				$this->queue_pending_rewrite_flush();
 			}
 			return;
 		}
@@ -169,7 +169,7 @@ class Endpoint {
 		}

 		// Defer the rewrite flush to wp_loaded; rewrite_rule fires later on init.
-		update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
+		$this->queue_pending_rewrite_flush();
 	}

 	/**
@@ -405,16 +405,36 @@ class Endpoint {
 	 * flush by setting `woocommerce_review_order_flush_rewrite_pending`;
 	 * `add_rewrite_rule()` doesn't fire until `init` priority 10, so the
 	 * flush has to happen later. `wp_loaded` runs after every `init`
-	 * callback, which is the earliest safe moment.
+	 * callback, which is the earliest safe moment. Installing requests leave
+	 * the endpoint-specific queue intact for the next normal request, when the
+	 * complete rewrite graph is available.
 	 */
 	public function maybe_flush_pending_rewrite(): void {
-		if ( 'yes' !== get_option( 'woocommerce_review_order_flush_rewrite_pending' ) ) {
+		if ( wp_installing() || 'yes' !== get_option( 'woocommerce_review_order_flush_rewrite_pending' ) ) {
 			return;
 		}
+
 		flush_rewrite_rules( false );
 		delete_option( 'woocommerce_review_order_flush_rewrite_pending' );
 	}

+	/**
+	 * Queue the endpoint-specific soft flush for `maybe_flush_pending_rewrite()`.
+	 *
+	 * While WordPress is installing, `update_option()` writes the row but skips every
+	 * cache update, so a persistent object cache keeps serving the previous value and
+	 * the next request never sees the queued flush. Evict both places the option can
+	 * be cached, mirroring `WC_Post_Types::flush_rewrite_rules()`.
+	 */
+	private function queue_pending_rewrite_flush(): void {
+		update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
+
+		if ( wp_installing() ) {
+			wp_cache_delete( 'woocommerce_review_order_flush_rewrite_pending', 'options' );
+			wp_cache_delete( 'alloptions', 'options' );
+		}
+	}
+
 	/**
 	 * Register the rewrite rule for the review-order endpoint.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
index df77bcb6ca7..8773329b10a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
@@ -10,6 +10,45 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper;
  * Unit tests for CheckoutLink.
  */
 class CheckoutLinkTest extends \WC_Unit_Test_Case {
+	/**
+	 * @testdox Installing-mode requests queue the endpoint rewrite without replacing persisted rules.
+	 */
+	public function test_endpoint_rewrite_is_deferred_during_installing_mode(): void {
+		global $wp_rewrite;
+
+		$original_installing     = wp_installing();
+		$original_rules          = get_option( 'rewrite_rules', null );
+		$original_queue          = get_option( 'woocommerce_queue_flush_rewrite_rules', null );
+		$original_top_rules      = $wp_rewrite->extra_rules_top;
+		$persisted_rewrite_rules = array( '^third-party/?$' => 'index.php?third-party=1' );
+
+		update_option( 'rewrite_rules', $persisted_rewrite_rules );
+		update_option( 'woocommerce_queue_flush_rewrite_rules', 'no' );
+		wp_installing( true );
+
+		try {
+			( new CheckoutLink() )->add_checkout_link_endpoint();
+			$this->assertSame( $persisted_rewrite_rules, get_option( 'rewrite_rules' ), 'Installing mode must preserve the complete rules from the prior normal request.' );
+
+			wp_installing( false );
+
+			$this->assertSame( 'yes', get_option( 'woocommerce_queue_flush_rewrite_rules' ), 'Installing mode should queue the missing checkout-link rule.' );
+			$this->assertArrayHasKey( '^checkout-link$', $wp_rewrite->extra_rules_top, 'The endpoint should still register its rule for the current request.' );
+		} finally {
+			wp_installing( false );
+			delete_option( 'rewrite_rules' );
+			delete_option( 'woocommerce_queue_flush_rewrite_rules' );
+			if ( null !== $original_rules ) {
+				add_option( 'rewrite_rules', $original_rules );
+			}
+			if ( null !== $original_queue ) {
+				add_option( 'woocommerce_queue_flush_rewrite_rules', $original_queue );
+			}
+			$wp_rewrite->extra_rules_top = $original_top_rules;
+			wp_installing( $original_installing );
+		}
+	}
+
 	/**
 	 * Test that products and coupon are added and token in url.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
index c7ba925f1a2..1c03a58ac89 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
@@ -726,6 +726,111 @@ class EndpointTest extends WC_Unit_Test_Case {
 		$this->assertSame( 'yes', get_option( 'woocommerce_review_order_flush_rewrite_pending' ), 'rewrite flush should be queued when the option moves' );
 	}

+	/**
+	 * @testdox Installing-mode requests defer the rewrite flush until the next normal request registers the endpoint.
+	 * @dataProvider provide_pending_rewrite_option_autoload_cases
+	 *
+	 * @param bool $autoload Whether the pending option is autoloaded.
+	 */
+	public function test_pending_rewrite_flush_is_deferred_during_installing_mode( bool $autoload ): void {
+		global $wp_actions, $wp_rewrite;
+
+		$this->reset_review_order_pages();
+		$page_id = (int) wp_insert_post(
+			array(
+				'post_type'    => 'page',
+				'post_status'  => 'draft',
+				'post_title'   => 'Review your order',
+				'post_name'    => 'review-order',
+				'post_content' => '<!-- wp:shortcode -->[woocommerce_review_order]<!-- /wp:shortcode -->',
+			)
+		);
+		update_option( 'woocommerce_review_order_page_id', $page_id );
+
+		$original_installing = wp_installing();
+		$original_pending    = get_option( 'woocommerce_review_order_flush_rewrite_pending', null );
+		$original_queue      = get_option( 'woocommerce_queue_flush_rewrite_rules', null );
+		$original_rules      = get_option( 'rewrite_rules', null );
+		$original_extra      = $wp_rewrite->extra_rules_top;
+		$original_generated  = $wp_rewrite->rules;
+		$original_permalink  = $wp_rewrite->permalink_structure;
+		$original_wp_loaded  = $wp_actions['wp_loaded'] ?? null;
+
+		$original_alloptions = wp_load_alloptions();
+
+		delete_option( 'woocommerce_review_order_flush_rewrite_pending' );
+		add_option( 'woocommerce_review_order_flush_rewrite_pending', 'no', '', $autoload );
+		$this->assertSame( 'no', get_option( 'woocommerce_review_order_flush_rewrite_pending' ), 'Prime the option cache before the install-mode write.' );
+		update_option( 'woocommerce_queue_flush_rewrite_rules', 'no' );
+		update_option( 'rewrite_rules', array() );
+		$wp_rewrite->set_permalink_structure( '/%postname%/' );
+		$wp_rewrite->extra_rules_top = array();
+		add_filter( 'flush_rewrite_rules_hard', '__return_false' );
+		$wp_actions['wp_loaded'] = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Simulate wp_loaded so WP_Rewrite::flush_rules() would write rules if the install guard did not return early.
+		wp_installing( true );
+
+		try {
+			// Republishing the draft host page queues the flush through the install-mode write path.
+			$this->endpoint->maybe_create_host_page();
+			$this->endpoint->maybe_flush_pending_rewrite();
+
+			$this->assertSame( array(), (array) get_option( 'rewrite_rules' ), 'Installing mode should not persist rewrite rules.' );
+			$this->assertSame( 'no', get_option( 'woocommerce_queue_flush_rewrite_rules' ), 'A soft endpoint flush should not use the shared hard-flush queue.' );
+
+			wp_installing( false );
+			$this->assertSame( 'yes', get_option( 'woocommerce_review_order_flush_rewrite_pending' ), 'The next normal request should see the queued flush through the option cache.' );
+			$this->endpoint->add_rewrite_rule();
+			$this->endpoint->maybe_flush_pending_rewrite();
+
+			$review_order_rules = array_filter(
+				(array) get_option( 'rewrite_rules' ),
+				static function ( $query ): bool {
+					return str_contains( $query, Endpoint::QUERY_VAR . '=' );
+				}
+			);
+
+			$this->assertNotEmpty( $review_order_rules, 'The next normal request should persist the endpoint registered on init.' );
+			$this->assertFalse( get_option( 'woocommerce_review_order_flush_rewrite_pending', false ), 'The normal request should consume the endpoint trigger.' );
+		} finally {
+			wp_installing( false );
+			remove_action( 'wp_loaded', array( $wp_rewrite, 'flush_rules' ) );
+			remove_filter( 'flush_rewrite_rules_hard', '__return_false' );
+			delete_option( 'woocommerce_review_order_flush_rewrite_pending' );
+			delete_option( 'woocommerce_queue_flush_rewrite_rules' );
+			delete_option( 'rewrite_rules' );
+			if ( null !== $original_pending ) {
+				add_option( 'woocommerce_review_order_flush_rewrite_pending', $original_pending, '', array_key_exists( 'woocommerce_review_order_flush_rewrite_pending', $original_alloptions ) );
+			}
+			if ( null !== $original_queue ) {
+				add_option( 'woocommerce_queue_flush_rewrite_rules', $original_queue, '', array_key_exists( 'woocommerce_queue_flush_rewrite_rules', $original_alloptions ) );
+			}
+			if ( null !== $original_rules ) {
+				add_option( 'rewrite_rules', $original_rules, '', array_key_exists( 'rewrite_rules', $original_alloptions ) );
+			}
+			$wp_rewrite->set_permalink_structure( $original_permalink );
+			$wp_rewrite->extra_rules_top = $original_extra;
+			$wp_rewrite->rules           = $original_generated;
+			if ( null === $original_wp_loaded ) {
+				unset( $wp_actions['wp_loaded'] );
+			} else {
+				$wp_actions['wp_loaded'] = $original_wp_loaded; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Restore the original action count.
+			}
+			wp_installing( $original_installing );
+		}
+	}
+
+	/**
+	 * Pending rewrite option cache locations.
+	 *
+	 * @return array<string, array{bool}>
+	 */
+	public function provide_pending_rewrite_option_autoload_cases(): array {
+		return array(
+			'autoloaded in alloptions' => array( true ),
+			'cached by option name'    => array( false ),
+		);
+	}
+
 	/**
 	 * @testdox maybe_create_host_page() republishes a draft host page and queues a rewrite flush.
 	 */