Skip to content

12: Conflict resolution - #12

Open
nikolaystrikhar wants to merge 1 commit into
11-loader-load-pathfrom
12-conflict-resolver
Open

12: Conflict resolution#12
nikolaystrikhar wants to merge 1 commit into
11-loader-load-pathfrom
12-conflict-resolver

Conversation

@nikolaystrikhar

@nikolaystrikhar nikolaystrikhar commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What: Conflict\Resolver finds a registered sub-plugin whose standalone is still active and applies its policy — defer, notify, or deactivate.

Usage:

Loader::register( [
    'slug'                       => 'give-recurring',
    'bundled_plugin_file'        => GIVE_PLUGIN_DIR . 'subs/give-recurring/give-recurring.php',
    'plugin_loaded_constant'     => 'GIVE_RECURRING_VERSION',
    'standalone_plugin_basename' => 'give-recurring/give-recurring.php',
    'conflict_policy'            => Conflict_Policy::DEACTIVATE, // the default
    'conflict_notice_message'    => static fn() => __( 'Recurring ships with Give now.', 'give' ),
] );

The standalone is deactivated, the merge notice is queued, and the admin lands back on the screen they were on with it gone.

Why this way: default: sits on the NOTICE_ONLY branch, not the DEACTIVATE one. Conflict_Policy::is_valid() normalises an unrecognised policy first, so default: is unreachable today — but it is one refactor from being live, and a fourth policy constant added without a matching case must not be read as consent to turn off a plugin the site owner deliberately activated. The two safety gates sit in different places on purpose: request-level gating (interactive admin GET only — not cron, not WP-CLI, not ajax, not a form POST) lives in the Loader trampoline where rebinding the resolver cannot drop it, because ungated this turns a visitor's checkout POST into a 302 that discards the order; the activate_plugins check lives in the resolver, because a host that binds its own resolver owns its own policy, and plugins_loaded fires long before auth_redirect() so an anonymous GET of an admin URL reaches this code. The redirect matches on the screen basename rather than a substring of admin_url(): wp_get_referer() prefers the _wp_http_referer field, which holds a bare path, so a URL comparison misses every admin form POST and the whole network admin.

Verify: slic run unit and slic run unit --env multisite — 298 tests green on both legs, plus composer test:analysis. exit is never mocked: wp_safe_redirect is stubbed to throw, and each test asserts both that it halted and the halt message, so "never redirected at all" cannot pass silently. Not covered here: the activation callback (#13) and the activation-error rewrite (#14). Rebased onto #11; the resolver interface now lives at Conflict\Contracts\Resolver_Interface per the folder-scoped convention, and the notices collaborator is merged main's Notices\Queue.

@nikolaystrikhar nikolaystrikhar changed the title 12: Conflict resolver 12: Conflict resolver (merge after #11 — last in the chain) Aug 11, 2026
Conflict\Resolver detects a registered sub-plugin whose standalone is still
active and applies the policy: DEFER stands down, NOTICE_ONLY explains, and
DEACTIVATE turns the standalone off, queues the merge notice and redirects so
the screen re-renders without it.

The switch puts default: on the branch that only talks. Conflict_Policy is_valid()
normalises an unrecognised policy to NOTICE_ONLY first, so default: is
unreachable today -- but a fourth policy added later without a matching case
must not be read as consent to turn off a plugin the site owner chose.

Two gates, deliberately in different places. Resolution runs only on an
interactive admin GET, in the Loader, where a host that rebinds the resolver
cannot drop it: unguarded, a visitor's checkout POST comes back as a 302 that
discards the order. Deactivation additionally needs activate_plugins, and that
lives in the resolver, because a replacement owns its own policy -- plugins_loaded
fires long before auth_redirect(), so an anonymous GET of an admin URL gets this
far.
@nikolaystrikhar nikolaystrikhar changed the title 12: Conflict resolver (merge after #11 — last in the chain) 12: Conflict resolution Aug 11, 2026
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.

1 participant