Commit 37f05098d91 for woocommerce

commit 37f05098d9130496021c66655b7aed51947e5698
Author: Sergey Mitroshin <sergeymitr@gmail.com>
Date:   Wed Nov 4 21:58:12 2026 -0500

    Fix UCP discovery missing Allow header and stale rewrite rules (#70679)

    Fix UCP discovery missing Allow header and stale rewrite rules.

diff --git a/plugins/woocommerce/changelog/fix-ucp-discovery-allow-header-and-stale-rewrite b/plugins/woocommerce/changelog/fix-ucp-discovery-allow-header-and-stale-rewrite
new file mode 100644
index 00000000000..3e9d4bcf250
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-ucp-discovery-allow-header-and-stale-rewrite
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+UCP: Send an Allow header with 405 responses from /.well-known/ucp and recover the endpoint when the flag changes without a rewrite flush.
diff --git a/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php b/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
index 87752e4ed7b..89d293d6642 100644
--- a/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
+++ b/plugins/woocommerce/src/Internal/Ucp/UcpDiscovery.php
@@ -38,13 +38,20 @@ final class UcpDiscovery implements RegisterHooksInterface {
 	public function register_feature_hooks(): void {
 		global $wp_rewrite;

-		if ( ! UcpUtils::is_enabled() || ! $wp_rewrite->using_permalinks() ) {
-			return;
+		$active = UcpUtils::is_enabled() && $wp_rewrite->using_permalinks();
+
+		if ( $active ) {
+			add_rewrite_rule( self::RULE, 'index.php?' . self::QUERY_VAR . '=1', 'top' );
+			add_filter( 'query_vars', array( $this, 'add_query_var' ), 0 );
+			add_action( 'parse_request', array( $this, 'maybe_render' ), 0 );
 		}

-		add_rewrite_rule( self::RULE, 'index.php?' . self::QUERY_VAR . '=1', 'top' );
-		add_filter( 'query_vars', array( $this, 'add_query_var' ), 0 );
-		add_action( 'parse_request', array( $this, 'maybe_render' ), 0 );
+		// The flag can change without FEATURE_ENABLED_CHANGED_ACTION firing (direct DB write, option import),
+		// so flush when the stored rules disagree; empty stored rules are regenerated by WP on their own.
+		$rules = get_option( 'rewrite_rules' );
+		if ( is_array( $rules ) && ! empty( $rules ) && isset( $rules[ self::RULE ] ) !== $active ) {
+			update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' );
+		}
 	}

 	/**
@@ -82,6 +89,8 @@ final class UcpDiscovery implements RegisterHooksInterface {

 		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
 		if ( ! in_array( $_SERVER['REQUEST_METHOD'] ?? '', array( 'GET', 'HEAD' ), true ) ) {
+			// RFC 9110 requires a 405 to name the allowed methods.
+			header( 'Allow: GET, HEAD' );
 			status_header( 405 );
 			exit;
 		}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php b/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
index 62958bc5acb..d9298eeb7ba 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Ucp/UcpDiscoveryTest.php
@@ -35,6 +35,7 @@ class UcpDiscoveryTest extends \WC_Unit_Test_Case {
 		unset( $wp_rewrite->extra_rules_top[ self::RULE ] );
 		remove_filter( 'query_vars', array( $this->sut, 'add_query_var' ), 0 );
 		remove_action( 'parse_request', array( $this->sut, 'maybe_render' ), 0 );
+		delete_option( 'woocommerce_queue_flush_rewrite_rules' );
 		parent::tearDown();
 	}

@@ -77,6 +78,32 @@ class UcpDiscoveryTest extends \WC_Unit_Test_Case {
 		$this->assertSame( $expected ? 0 : false, has_action( 'parse_request', array( $this->sut, 'maybe_render' ) ) );
 	}

+	/**
+	 * @testdox register_feature_hooks queues a rewrite flush when the stored rules disagree with the feature flag.
+	 * @testWith ["yes", "present", false]
+	 *           ["yes", "missing", true]
+	 *           ["yes", "empty", false]
+	 *           ["no", "present", true]
+	 *
+	 * @param string $enabled  Feature option value.
+	 * @param string $stored   Whether the stored rules contain the rule, lack it, or are empty.
+	 * @param bool   $expected Whether a flush should be queued.
+	 */
+	public function test_register_feature_hooks_heals_stale_rewrite_rules( string $enabled, string $stored, bool $expected ): void {
+		update_option( 'woocommerce_feature_ucp_enabled', $enabled );
+		$this->set_permalink_structure( '/%postname%/' );
+		$rules = array( '^shop/?$' => 'index.php?post_type=product' );
+		if ( 'present' === $stored ) {
+			$rules[ self::RULE ] = 'index.php?' . UcpDiscovery::QUERY_VAR . '=1';
+		}
+		update_option( 'rewrite_rules', 'empty' === $stored ? array() : $rules );
+		update_option( 'woocommerce_queue_flush_rewrite_rules', 'no' );
+
+		$this->sut->register_feature_hooks();
+
+		$this->assertSame( $expected ? 'yes' : 'no', get_option( 'woocommerce_queue_flush_rewrite_rules' ) );
+	}
+
 	/**
 	 * @testdox Toggling the UCP feature queues a rewrite rules flush.
 	 */