12: Conflict resolver - #12
Open
nikolaystrikhar wants to merge 6 commits into
Open
Conversation
nikolaystrikhar
force-pushed
the
12-conflict-resolver
branch
from
August 6, 2026 07:36
59ad699 to
f44d91b
Compare
Conflict resolution deactivates a plugin and ends the request, and it ran at plugins_loaded on every request type with no gate. Known issue B logged this as a logged-out visitor bouncing to the login screen, which is the mildest case. The same path turns a visitor's checkout POST into a 302 that silently drops the order, bounces a login POST back to a blank form, aborts wp-cron before its event loop, and ends a WP-CLI command with status 0 and no output, since header() does nothing under the CLI SAPI. Now gated on is_admin() plus not cron, AJAX or WP-CLI -- is_admin() alone is not enough, because admin-ajax.php and admin-post.php both define WP_ADMIN. deactivate_plugins() is now silent and passes no $network_wide. Verified against core: the default is null, not false, and core takes the network branch on 'false !== $network_wide' and the blog branch on 'true !== $network_wide', so null covers both. The infinite-redirect justification for computing the flag never existed, and passing true skips the blog branch, stranding an entry that needs a second request and a second deactivation hook to clear. Silent because the standalone's own deactivation callback would otherwise run at plugins_loaded, where a routine flush_rewrite_rules() rebuilds the rules before any post type is registered and every custom permalink 404s. Core's automatic deactivations are silent for the same reason. redirect_destination() matches the screen rather than a substring of an absolute URL. wp_get_referer() prefers the _wp_http_referer field that every nonce-bearing admin form carries, and that holds a bare path -- so the 'never interrupt an inline update' guard missed every admin form POST, the whole network admin, and any site behind a TLS-terminating proxy. The shared resolve() test helper caught the halt exception without asserting it arrived, so four tests passed whether or not the redirect happened at all. Two network-flag tests now run against real core instead of a stub, because that claim is the only thing the argument rested on.
The context gate let every admin form submission through: admin-post.php and options.php define WP_ADMIN and never define DOING_AJAX, so a POST to either was deactivated and 302'd, and the browser followed with a GET that had no body. Resolution now requires a GET and waits for the next page view. boot()'s too-late guard still measured against the load priority, so booting at plugins_loaded priority 1 wired both hooks, ran the load loop, and dropped conflict resolution without reporting anything.
nikolaystrikhar
force-pushed
the
11-loader-load-path
branch
from
August 6, 2026 14:22
7d4435a to
4d1c627
Compare
nikolaystrikhar
force-pushed
the
12-conflict-resolver
branch
from
August 6, 2026 14:22
f44d91b to
db11979
Compare
…ugin Sub_Plugin named DEACTIVATE as its own fallback, which put "which policy applies when none is configured" in the object that only holds one sub-plugin's config. Conflict_Policy::default() states it now. The two fallbacks stay different, and say why: unconfigured means the sub-plugin accepted the default, whereas an unrecognised policy is a value nobody chose, and reading a typo as consent to deactivate is the outcome worth refusing. Plugin_State_Interface becomes the library's only route to WordPress's plugin functions. Sub_Plugin was a config value object that also queried global plugin state and required wp-admin/includes/plugin.php; Resolver required the same file guarded on a different function. One gateway, one include, one guard -- on deactivate_plugins, a function the library still calls, and still not is_plugin_active, whose third-party shims would short-circuit the require. is_standalone_plugin_active() is gone rather than delegated: its only caller already reaches collaborators through Loader, so forwarding would have bought Sub_Plugin a dependency on Loader to answer a question that was never about its configuration. is_standalone_plugin_network_active() had no production callers at all and is deleted. Sub_Plugin now makes no global WordPress calls beyond the defined() that is intrinsic to it, and its tests no longer stub is_plugin_active.
Also marks two PR 7 deviations superseded: the network-active predicate has moved off Sub_Plugin, and the include guard names deactivate_plugins now that the old guard function is one the library no longer calls.
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.
Adds
Conflict\Resolver(detect an active standalone, act on the policy),Loader::resolver(), and aplugins_loaded@1 hook ahead of the @2 load loop.Stacked on #11.
wp-login.phpPOSTs bounced to a blank form,wp-cron.phpaborted, WP-CLI ended silently.is_admin()pluswp_doing_ajax()was not enough:admin-post.phpandoptions.phpdefineWP_ADMIN, notDOING_AJAX.$network_wideargument is dropped. Core's defaultnulltakes both the network and blog branches; a computedtrueskips the blog branch, leaving a staleactive_pluginsentry.register_deactivation_hook()atplugins_loaded, where aflush_rewrite_rules()rebuilds rules before any post type is registered, 404ing every permalink.wp_get_referer()againstadmin_url()withstrpos(), but the referer is usually a path from_wp_http_referer, so plugins.php bulk actions missed it. It parses the URL now.boot()'s too-late guard trips at the earlier of the two hook priorities. Wiring is all-or-nothing.The
resolve()test helper never asserted the halt arrived; it takes an explicit expectation now.Verify:
slic run unit— 190 tests, 314 assertions, green (1 multisite-only skip).--env multisitegreen.composer test:analysis→[OK] No errors.Not covered: convergence with two conflicting standalones.