Skip to content

fix: clean every site's data on multisite uninstall#584

Merged
galbus merged 1 commit into
mainfrom
claude/great-colden-aeee20
Jul 19, 2026
Merged

fix: clean every site's data on multisite uninstall#584
galbus merged 1 commit into
mainfrom
claude/great-colden-aeee20

Conversation

@galbus

@galbus galbus commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What & why

On a multisite network, deleting the plugin left all of its data in the database — including the beyondwords_api_key secret and beyondwords_project_id.

Uninstaller::cleanup_plugin_options() used is_multisite() ? delete_site_option() : delete_option(). But the plugin stores every option per-site via update_option() / register_setting() — there are no update_site_option() writes anywhere in src/. So on multisite the uninstall loop deleted wp_sitemeta rows that were never created and left every real option in wp_options / wp_N_options. Retaining an API secret the user believes was deleted is a data-hygiene/security concern, and the method's 0 return value silently masked the failure.

There is also a second layer: WordPress runs uninstall.php once, in the network's main-site context. So even a corrected delete_option() — plus the raw-SQL transient and post-meta cleanups, which operate on the per-site $wpdb->options / $wpdb->postmeta tables — would only ever clean the main site. Subsites were never iterated.

Changes

  • cleanup_plugin_options()delete_option() is now the unconditional primary path (runs on both single-site and multisite), with a defensive delete_site_option() sweep retained on multisite for any legacy network option.
  • Uninstaller::run() (new) — iterates get_sites(['number' => 0]) + switch_to_blog(), cleans each site's options, transients and post-meta, then restore_current_blog(). uninstall.php now calls run(). The individual cleanup_* methods stay public with unchanged contracts.

Notes for reviewers

  • number => 0 lifts get_sites()' default 100-site cap so no site is skipped on large networks.
  • No phpcs:ignore or ruleset change was needed. switch_to_blog() is a WordPress-VIP-Go warning, but that ruleset already pins it to severity 3 — below the default reporting threshold of 5 — so phpcs stays clean.
  • The same either/or pattern also exists in Updater::delete_deprecated_options() (src/core/class-updater.php). That is a separate finding and is intentionally out of scope here.

Testing

  • npm run phpcs — clean (exit 0, full project scan).
  • UninstallerTest (single-site): 50 tests, 366 assertions, all green.
  • UninstallerTest under real multisite (WP_MULTISITE=1): the new run_cleans_every_site_on_multisite regression test passes — the API key, a transient and post-meta are removed on both the main site and a subsite (6 assertions across 2 sites). This test fails against the pre-fix code.

🤖 Generated with Claude Code

cleanup_plugin_options() called delete_site_option() on multisite, but
the plugin stores every option per-site via update_option(). The loop
therefore deleted nothing real and left the beyondwords_api_key secret
and all settings behind in wp_N_options after the plugin was removed.

Make delete_option() the unconditional primary path (with a defensive
delete_site_option() sweep for legacy network options), and add
Uninstaller::run() to iterate get_sites() + switch_to_blog() so every
site's options, transients and post-meta are cleaned -- not only the
network main site, which is the sole context uninstall.php runs in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@galbus
galbus marked this pull request as ready for review July 19, 2026 07:06
@galbus
galbus requested a review from gouravkhunger July 19, 2026 07:06
@galbus
galbus merged commit 73d259b into main Jul 19, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants