Commit 1ccb853d400 for woocommerce

commit 1ccb853d4005aa04c0c8ab7ceabfce1d4dd204f8
Author: Néstor Soriano <konamiman@konamiman.com>
Date:   Thu Jul 9 18:50:15 2026 +0200

    Remove experimental-feature user meta on full uninstall (#66027)

    * Remove experimental-feature user meta on full uninstall

    The WC_REMOVE_ALL_DATA cleanup deletes WooCommerce user meta via an
    explicit allowlist of wc_ / _wc_ keys (a blanket wc_% / _wc_% wildcard
    is unsafe). That allowlist was missing the per-user meta written by
    three feature data stores through Users::update_site_user_meta(), so it
    was left behind on uninstall even when the site owner opted into full
    data removal:

    - wc_push_notification_preferences (NotificationPreferencesDataStore)
    - _wc_shopper_list_<slug>           (ShopperList)
    - _wc_email_verified,
      _wc_email_verification_key,
      _wc_email_verification_attempts   (EmailVerificationService)

    Each stored key carries a _<table_prefix> suffix, so the added LIKE
    patterns use a trailing % and escape literal underscores, matching the
    convention of the existing allowlist entries.

    * Add regression test for uninstall user-meta cleanup

    Cover the WC_REMOVE_ALL_DATA user-meta sweep with a test asserting the
    push-notification, shopper-list, and customer email-verification meta are
    removed while WordPress core role/capability meta and unrelated third-party
    meta survive. A negative control (widening a pattern to wc_%) confirms the
    test fails on the over-deletion regression it guards against.

    Also document in uninstall.php which class constants the hardcoded key
    prefixes mirror, and why the constants cannot be referenced directly (the
    script runs before PSR-4 autoloading is available).

    ---------

    Co-authored-by: Brandon Kraft <public@brandonkraft.com>

diff --git a/plugins/woocommerce/changelog/fix-65946-uninstall-experimental-user-meta b/plugins/woocommerce/changelog/fix-65946-uninstall-experimental-user-meta
new file mode 100644
index 00000000000..271f86650ad
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-65946-uninstall-experimental-user-meta
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Remove the push notification preferences, shopper list, and customer email verification user meta when uninstalling with WC_REMOVE_ALL_DATA.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/util/install.php b/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
index 70c15e4f320..db00bbe4788 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
@@ -130,6 +130,65 @@ class WC_Tests_Install extends WC_Unit_Test_Case {
 		$this->assertNull( get_role( 'shop_manager' ) );
 	}

+	/**
+	 * @testdox Uninstalling with WC_REMOVE_ALL_DATA removes WooCommerce experimental-feature user meta without matching core-style or third-party meta keys.
+	 */
+	public function test_uninstall_removes_experimental_user_meta_but_preserves_other_meta() {
+		global $wpdb;
+
+		$user_id     = $this->factory()->user->create();
+		$blog_suffix = '_' . rtrim( $wpdb->get_blog_prefix( get_current_blog_id() ), '_' );
+
+		// WooCommerce user meta that a full-data-removal uninstall must delete. Seeded through the same
+		// site-aware helper the features use so the stored key format (..._<blog_prefix>) matches production,
+		// and via the owning class constants where public so a constant rename would surface here. The two
+		// email keys mirror EmailVerificationService::VERIFIED_META / ::KEY_META, which are private consts and
+		// are therefore spelled out as the same literals uninstall.php carries.
+		$wc_base_keys = array(
+			\Automattic\WooCommerce\Internal\ShopperLists\ShopperList::META_KEY_PREFIX . 'saved-for-later',
+			'_wc_email_verified',
+			'_wc_email_verification_key',
+			\Automattic\WooCommerce\Internal\PushNotifications\DataStores\NotificationPreferencesDataStore::META_KEY,
+		);
+		foreach ( $wc_base_keys as $base_key ) {
+			\Automattic\WooCommerce\Internal\Utilities\Users::update_site_user_meta( $user_id, $base_key, 'seeded-value' );
+		}
+
+		// Meta that must survive. wc_capabilities / wc_user_level reproduce the exact key names WordPress core
+		// writes on a site whose table prefix is "wc_" (the collision uninstall.php's comment guards against):
+		// widening any wc_ pattern to wc_% would delete them, so they are the regression tripwire. An unrelated
+		// third-party key rounds out the seeded control group.
+		$seeded_preserved_keys = array( 'wc_capabilities', 'wc_user_level', 'another_plugin_pref' );
+		foreach ( $seeded_preserved_keys as $meta_key ) {
+			update_user_meta( $user_id, $meta_key, 'preserve-me' );
+		}
+
+		// The user's real capability row (created by the factory) must also survive, or the uninstall would
+		// strip roles and could lock the site out.
+		$real_capabilities_key = $wpdb->get_blog_prefix( get_current_blog_id() ) . 'capabilities';
+
+		self::uninstall();
+
+		// uninstall.php deletes via raw SQL, which does not invalidate the user-meta object cache, so drop the
+		// cache before reading meta existence back from the database.
+		wp_cache_delete( $user_id, 'user_meta' );
+
+		foreach ( $wc_base_keys as $base_key ) {
+			$stored_key = $base_key . $blog_suffix;
+			$this->assertFalse( metadata_exists( 'user', $user_id, $stored_key ), "Uninstall should have removed WooCommerce user meta '{$stored_key}'." );
+		}
+
+		foreach ( array_merge( $seeded_preserved_keys, array( $real_capabilities_key ) ) as $meta_key ) {
+			$this->assertTrue( metadata_exists( 'user', $user_id, $meta_key ), "Uninstall must not remove non-WooCommerce user meta '{$meta_key}'." );
+		}
+
+		if ( is_multisite() ) {
+			wpmu_delete_user( $user_id );
+		} else {
+			wp_delete_user( $user_id );
+		}
+	}
+
 	/**
 	 * Make sure the list of tables returned by WC_Install::get_tables() and used when uninstalling the plugin
 	 * or deleting a site in a multi site install is not missing any of the WC tables. If a table is added to
diff --git a/plugins/woocommerce/uninstall.php b/plugins/woocommerce/uninstall.php
index d586b16e7c3..eefb5287b8e 100644
--- a/plugins/woocommerce/uninstall.php
+++ b/plugins/woocommerce/uninstall.php
@@ -114,9 +114,17 @@ if ( defined( 'WC_REMOVE_ALL_DATA' ) && true === WC_REMOVE_ALL_DATA ) {
 	 * other plugins' user meta and, critically, WordPress core's own role/capability meta (the
 	 * {prefix}capabilities and {prefix}user_level keys) on any site whose database table prefix is "wc_",
 	 * which would strip every user's roles and could lock the site out. We therefore match only
-	 * WooCommerce's own known wc_ / _wc_ user meta keys (including the wc_admin_ legacy prefix, the
-	 * _wc_egg_ easter-egg meta, and the per-site customer lookup meta, whose keys are suffixed with the
-	 * site's table prefix) rather than a blanket wildcard.
+	 * WooCommerce's own known wc_ / _wc_ user meta keys (the wc_admin_ legacy prefix; the _wc_egg_
+	 * easter-egg meta; the per-site customer lookup, push notification preferences, shopper-list, and
+	 * email-verification meta, whose keys are suffixed with the site's table prefix; and the exact
+	 * wc_last_active and wc_marketplace_suggestions_dismissed_suggestions keys) rather than a blanket
+	 * wildcard.
+	 *
+	 * The push-notification-preferences, shopper-list, and email-verification prefixes below mirror,
+	 * respectively, NotificationPreferencesDataStore::META_KEY, ShopperList::META_KEY_PREFIX, and
+	 * EmailVerificationService::VERIFIED_META / ::KEY_META. This script runs before PSR-4 autoloading is
+	 * available, so those constants cannot be referenced directly here; keep the literals below in sync if
+	 * the constants ever change.
 	 *
 	 * Note: wp_usermeta is shared across a multisite network while this uninstall runs per site, so the
 	 * matching meta is removed network-wide, consistent with the woocommerce_ option/meta cleanup above.
@@ -127,9 +135,13 @@ if ( defined( 'WC_REMOVE_ALL_DATA' ) && true === WC_REMOVE_ALL_DATA ) {
 			OR meta_key LIKE '\_woocommerce\_%'
 			OR meta_key LIKE 'wc\_admin\_%'
 			OR meta_key LIKE '\_wc\_egg\_%'
+			OR meta_key LIKE '\_wc\_shopper\_list\_%'
+			OR meta_key LIKE '\_wc\_email\_verified\_%'
+			OR meta_key LIKE '\_wc\_email\_verification\_%'
 			OR meta_key LIKE 'wc\_last\_order\_%'
 			OR meta_key LIKE 'wc\_order\_count\_%'
 			OR meta_key LIKE 'wc\_money\_spent\_%'
+			OR meta_key LIKE 'wc\_push\_notification\_preferences\_%'
 			OR meta_key IN ( 'wc_last_active', 'wc_marketplace_suggestions_dismissed_suggestions' );"
 	);