Commit 326695026c7 for woocommerce

commit 326695026c79d5ec1913edf4a73331ea42299d6c
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date:   Mon Jul 27 11:35:10 2026 +0300

    Fix: Persist nav-menu hidden snapshot only for the current user (#66977)

    fix: persist nav-menu hidden snapshot only for the current user

diff --git a/plugins/woocommerce/changelog/66976-fix-nav-menu-hidden-snapshot-current-user b/plugins/woocommerce/changelog/66976-fix-nav-menu-hidden-snapshot-current-user
new file mode 100644
index 00000000000..0b0e5fc20e3
--- /dev/null
+++ b/plugins/woocommerce/changelog/66976-fix-nav-menu-hidden-snapshot-current-user
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Persist the default nav-menu hidden meta box snapshot only for the current user on the Appearance > Menus screen, matching WordPress core, so a read of another user's option (e.g. user-switching or admin tooling) can no longer overwrite that user's own first-visit snapshot.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php
index 8bcef39f77b..c0ec14282c4 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php
@@ -444,7 +444,9 @@ class WC_Admin_Menus {
 	 * across the multiple reads in a single page load and, crucially, across
 	 * later requests: boxes registered by plugins installed after the first
 	 * visit stay visible, matching vanilla WordPress instead of defaulting to
-	 * hidden on every recompute.
+	 * hidden on every recompute. As in core, only the current user's snapshot is
+	 * persisted, so reads on behalf of another user never overwrite that user's
+	 * own first-visit snapshot.
 	 *
 	 * @since 11.1.0
 	 *
@@ -511,7 +513,16 @@ class WC_Admin_Menus {
 		// Persist the snapshot, mirroring core's wp_initial_nav_menu_meta_boxes(),
 		// so later reads in this request and in future requests return this same
 		// list instead of recomputing it against a changed meta box registry.
-		update_user_meta( $user->ID, 'metaboxhidden_nav-menus', $hidden );
+		//
+		// Like core, only the current user's snapshot is persisted. This filter
+		// runs for whichever user's option is being read, so a read performed on
+		// behalf of another user (user-switching or admin tooling) still receives
+		// the computed default below for display, but must not consume that user's
+		// durable first-visit snapshot from this request's meta box registry.
+		$current_user_id = get_current_user_id();
+		if ( $current_user_id && (int) $user->ID === $current_user_id ) {
+			update_user_meta( $current_user_id, 'metaboxhidden_nav-menus', $hidden );
+		}

 		return $hidden;
 	}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-admin-menus-test.php b/plugins/woocommerce/tests/php/includes/class-wc-admin-menus-test.php
index 85e3f284e2f..052de06d8ba 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-admin-menus-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-admin-menus-test.php
@@ -208,6 +208,7 @@ class WC_Admin_Menus_Test extends WC_Unit_Test_Case {
 	public function test_filter_keeps_endpoints_box_visible_after_late_registration() {
 		$GLOBALS['wp_meta_boxes'] = $this->get_nav_meta_boxes();  // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
 		$user_id                  = $this->factory->user->create();
+		wp_set_current_user( $user_id );

 		$menus = new WC_Admin_Menus();
 		$menus->register_default_nav_menu_meta_boxes_filter();
@@ -240,6 +241,7 @@ class WC_Admin_Menus_Test extends WC_Unit_Test_Case {
 	public function test_filter_keeps_late_registered_third_party_box_out_of_snapshot() {
 		$GLOBALS['wp_meta_boxes'] = $this->get_nav_meta_boxes();  // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
 		$user_id                  = $this->factory->user->create();
+		wp_set_current_user( $user_id );

 		$menus = new WC_Admin_Menus();
 		$menus->register_default_nav_menu_meta_boxes_filter();
@@ -267,6 +269,7 @@ class WC_Admin_Menus_Test extends WC_Unit_Test_Case {
 	public function test_filter_persists_hidden_snapshot_on_first_visit() {
 		$GLOBALS['wp_meta_boxes'] = $this->get_nav_meta_boxes();  // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
 		$user_id                  = $this->factory->user->create();
+		wp_set_current_user( $user_id );

 		$menus = new WC_Admin_Menus();
 		$menus->register_default_nav_menu_meta_boxes_filter();
@@ -289,6 +292,7 @@ class WC_Admin_Menus_Test extends WC_Unit_Test_Case {
 	public function test_snapshot_persists_so_later_registered_third_party_box_stays_visible() {
 		$GLOBALS['wp_meta_boxes'] = $this->get_nav_meta_boxes();  // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
 		$user_id                  = $this->factory->user->create();
+		wp_set_current_user( $user_id );

 		// First request: user visits the nav-menus screen for the first time.
 		$first_visit = new WC_Admin_Menus();
@@ -313,4 +317,33 @@ class WC_Admin_Menus_Test extends WC_Unit_Test_Case {
 		$this->assertIsArray( $hidden );
 		$this->assertNotContains( 'add-later-plugin-box', $hidden );
 	}
+
+	/**
+	 * A read performed on behalf of another user (e.g. user-switching or admin
+	 * tooling) during the nav-menus request must not persist a snapshot for that
+	 * user. Core's wp_initial_nav_menu_meta_boxes() only writes for the current
+	 * user, so the other user's genuine first-visit snapshot stays intact and
+	 * self-healing. The computed default is still returned for display.
+	 */
+	public function test_filter_does_not_persist_snapshot_for_a_non_current_user() {
+		$GLOBALS['wp_meta_boxes'] = $this->get_nav_meta_boxes();  // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		$current_user_id          = $this->factory->user->create();
+		$other_user_id            = $this->factory->user->create();
+		wp_set_current_user( $current_user_id );
+
+		$menus = new WC_Admin_Menus();
+		$menus->register_default_nav_menu_meta_boxes_filter();
+
+		// The current user reads another user's option (the pathological path).
+		$hidden = get_user_option( 'metaboxhidden_nav-menus', $other_user_id );
+
+		// The computed default is still returned for display...
+		$this->assertIsArray( $hidden );
+		$this->assertContains( 'add-post-type-news', $hidden );
+
+		// ...but nothing is persisted for either user, so the other user's genuine
+		// first-visit snapshot is never consumed by this request.
+		$this->assertSame( '', get_user_meta( $other_user_id, 'metaboxhidden_nav-menus', true ) );
+		$this->assertSame( '', get_user_meta( $current_user_id, 'metaboxhidden_nav-menus', true ) );
+	}
 }