fix: clean every site's data on multisite uninstall#584
Merged
Conversation
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>
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
galbus
marked this pull request as ready for review
July 19, 2026 07:06
gouravkhunger
approved these changes
Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
On a multisite network, deleting the plugin left all of its data in the database — including the
beyondwords_api_keysecret andbeyondwords_project_id.Uninstaller::cleanup_plugin_options()usedis_multisite() ? delete_site_option() : delete_option(). But the plugin stores every option per-site viaupdate_option()/register_setting()— there are noupdate_site_option()writes anywhere insrc/. So on multisite the uninstall loop deletedwp_sitemetarows that were never created and left every real option inwp_options/wp_N_options. Retaining an API secret the user believes was deleted is a data-hygiene/security concern, and the method's0return value silently masked the failure.There is also a second layer: WordPress runs
uninstall.phponce, in the network's main-site context. So even a correcteddelete_option()— plus the raw-SQL transient and post-meta cleanups, which operate on the per-site$wpdb->options/$wpdb->postmetatables — 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 defensivedelete_site_option()sweep retained on multisite for any legacy network option.Uninstaller::run()(new) — iteratesget_sites(['number' => 0])+switch_to_blog(), cleans each site's options, transients and post-meta, thenrestore_current_blog().uninstall.phpnow callsrun(). The individualcleanup_*methods stay public with unchanged contracts.Notes for reviewers
number => 0liftsget_sites()' default 100-site cap so no site is skipped on large networks.phpcs:ignoreor ruleset change was needed.switch_to_blog()is aWordPress-VIP-Gowarning, but that ruleset already pins it to severity 3 — below the default reporting threshold of 5 — sophpcsstays clean.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.UninstallerTestunder real multisite (WP_MULTISITE=1): the newrun_cleans_every_site_on_multisiteregression 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