Skip to content

12A: Answer the conflict questions before anything acts on them - #19

Open
nikolaystrikhar wants to merge 3 commits into
mainfrom
12A-conflict-gates
Open

12A: Answer the conflict questions before anything acts on them#19
nikolaystrikhar wants to merge 3 commits into
mainfrom
12A-conflict-gates

Conversation

@nikolaystrikhar

@nikolaystrikhar nikolaystrikhar commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What: adds Conflict\Detector, Conflict\Gatekeeper and Conflict\Redirector and binds all three in the container. No hook is wired here and nothing calls them yet; the step that does arrives in the PR above this one.

Usage:

$container->get( Detector::class )->has_conflict();

$gatekeeper = $container->get( Gatekeeper::class );
$gatekeeper->request_may_resolve() && $gatekeeper->user_may_resolve();

$container->get( Redirector::class )->after_deactivation( $_SERVER['REQUEST_URI'] ?? '' );

Why this way:

Detection is a query, so it stays off the resolver's contract — a host binding its own could otherwise answer "no conflict" and quietly disable the pass.

Two gate methods, because user_may_resolve() pins the current user: asked unconditionally, an SSO plugin's visitors read as signed out on every admin request, so the cheap request gate turns away cron, WP-CLI and every POST first.

On multisite the capability is manage_network_plugins, by namedeactivate_plugins() acts network-wide, and core only widens activate_plugins into it while the Plugins menu is off for sites.

A GET carrying an action is refused along with every POST, since plugins.php?action=activate is the request plugin_sandbox_scrape() replays. Any action arg at all, never a known-safe list.

The redirector answers with the screen requested, not the referrer — only a validated basename and a re-encoded query leave it, with admin_url(), network_admin_url() or user_admin_url() in front.

Adds three classes that only report: Conflict\Detector (is a registered
sub-plugin's standalone active), Conflict\Gatekeeper (may this request, and
this user, have a conflict resolved) and Conflict\Redirector (which admin
screen a resolved request should land on). None of them deactivates,
queues or redirects; each is bound in the container and nothing calls them
yet.

Kept off Resolver_Interface deliberately: detection is a query, and a host
rebinding the resolver should not be able to drop it by omission.
@nikolaystrikhar nikolaystrikhar changed the title Answer the conflict questions before anything acts on them 12A: Answer the conflict questions before anything acts on them Aug 12, 2026
sanitize_key() narrowed "any action arg at all" to "any action that survives
sanitize_key": admin.php dispatches admin_action_{$action} on the raw value, so
an action named outside a-z0-9_- emptied to '' and the gate admitted the one
request it is most confident it refuses.

admin_url_for() branched on is_network_admin() alone while its own comment
promised all three admins, so a conflict resolved under /wp-admin/user/ threw
the user out onto a site's own screens.

The Gatekeeper docblock still measured the SSO race against plugins_loaded
priority 1, which its own test had already moved to 5.
Every new PHP file declares strict_types, so the rule arrives with the file
rather than as a sweep over it afterwards. PR 21 covers everything that already
existed on main.
Comment on lines +174 to +176
if ( is_user_admin() ) {
return user_admin_url( $screen );
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this would ever actually be used in practice, but it is harmless and could account for a weird edge case we aren't considering I suppose.

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