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' );"
);