12: Conflict resolution - #12
Conversation
59ad699 to
f44d91b
Compare
7d4435a to
4d1c627
Compare
f44d91b to
db11979
Compare
4d1c627 to
fe01f47
Compare
0e4b982 to
4aa12bd
Compare
fe01f47 to
4d56787
Compare
4aa12bd to
cbf226e
Compare
cbf226e to
3c6ee34
Compare
Conflict\Resolver takes its four collaborators as required constructor arguments -- the checker, the deactivator, the notice queue and the redirector -- because the container is now mandatory and nothing has to be constructible without one. The nullable peers and the accessors that fell back to a static are gone with the class they fell back to. Conflict\Gatekeeper owns who may resolve: an interactive admin GET, the activate_plugins capability, and a hook prefix. plugins_loaded runs on every request and fires before auth_redirect(), so an unauthenticated GET of an admin URL reaches this code, and the capability gate covers every policy rather than only the destructive one -- the other branches queue a notice the same user could not render anyway. The priority-1 step asks the gatekeeper before it resolves Resolver_Interface at all, so a host binding its own resolver cannot drop either gate by omission. Conflict\Destination becomes Conflict\Redirector. It decides where to send the user and never goes there; wp_safe_redirect() and the exit after it stay in the resolver, so the policy action and the admin-URL knowledge change for separate reasons. A class that returns a URL from a filter still earns the agent noun. The boot barrier now measures from the lowest priority in the sequence rather than from the load priority, or a host booting between conflict resolution and the load would be told nothing while half its wiring silently failed.
3c6ee34 to
d0e0a6c
Compare
b28c21e to
c7760ef
Compare
d0e0a6c to
7059031
Compare
| private const LOAD_PRIORITY = 2; | ||
|
|
||
| /** | ||
| * plugins_loaded priority conflict resolution runs at, ahead of the load pass. | ||
| * | ||
| * A standalone that survives the conflict defines the guard constant as it loads, and the load | ||
| * pass has to see that, so resolution cannot share a priority with it. | ||
| * | ||
| * @since 1.0.0 | ||
| * | ||
| * @var int | ||
| */ | ||
| private const RESOLVE_PRIORITY = 1; |
There was a problem hiding this comment.
Something to maybe note/consider:
1 and 2 are based on LearnDash/MemberDash loading its Container at plugins_loaded:0, right? For instance, the implementation in Shop Kit was able to handle this logic a bit later due to it not having as strong conflict resolution (it didn't need it) and its Container was set at plugins_loaded:1 instead of plugins_loaded:0.
Our main constraints are:
LOAD_PRIORITYneeds to be beforeplugins_loaded:10- Mainly an assumption that the earliest an absorbed plugin would be hooking is to
plugins_loaded:10as hooking before10should only be done in special cases like what we're doing here
- Mainly an assumption that the earliest an absorbed plugin would be hooking is to
RESOLVE_PRIORITYneeds to be beforeLOAD_PRIORITYLOAD_PRIORITYandRESOLVE_PRIORITYboth need to be after the Host Plugin has set up their Container- Likely on
plugins_loadedsomewhere, but hopefully beforeplugins_loaded:10
- Likely on
I do think the earlier the better for this type of thing, but maybe we could scoot these priorities a little later to more easily allow other code to run before our hooks if needed. With RESOLVE_PRIORITY = 1, if LearnDash had to hook in before us, for instance, almost the only way to safely ensure this would be to simply run the code before ever running Plugin Absorber without utilizing hooks at all.
The main downside I see currently is that we are basically forcing Plugin Absorber to be set up at plugins_loaded:0 because if it is done any later, it won't run properly. While there are checks for that in the code added in #11 and there's the inline loader to try to account for it, it could still potentially be a pain point.
|
Superseded by #19 and #20, which split this in two along the line between the classes that only answer a question and the one that acts on the answers:
Both are rebased onto @d4mation — your note on the priorities is in #20. Taking your three constraints: resolution now runs at One thing that argues against going later still, which is worth recording since it is invisible from the host side: every priority below the load is a band of the bundled plugin's own |
What: conflict resolution becomes the first step of
Boot\Scheduler's sequence —plugins_loadedpriority 1, ahead of the load pass at 2 — askingConflict\Gatekeeper::may_resolve()and only thenResolver_Interface::resolve_all(), withResolver,GatekeeperandRedirectorbound inProviderand the resolver reachable asLoader::resolver().Usage:
The standalone is deactivated silently, the merge notice is queued, and the admin lands back on the screen they were on.
Why this way:
Resolution takes a priority of its own, and the boot barrier moves with it. A standalone that survives defines the guard constant as it loads and the load pass has to see that, so the two can't share a priority;
wiring_window_has_closed()now measures against the lowest priority in the sequence rather than the load's, which catches a host booting into the gap between them.The gatekeeper is a separate binding, resolved before the resolver. Who may have a conflict resolved is our invariant; what a conflict means is the host's policy — split that way, a host's
Resolver_Interfaceis never even built on a request that fails a gate.Both gates, and the capability one covers every policy.
plugins_loadedfires on every request, so an ungated resolve turns a visitor's checkout POST into a 302 and exits a WP-CLI run silently; and it runs beforeauth_redirect(), so an unauthenticated GET of an admin URL would otherwise reach a site-wide deactivation. GatingNOTICE_ONLYtoo is free, sinceNotices\Queue::render()won't render or clear for that user anyway.An unrecognised policy is normalised before the switch, not caught by a
default:after it.Conflict_Policy::is_valid()maps anything unknown toNOTICE_ONLY, so a typo like'defered'produces a sentence rather than a deactivation.Where to send the user isn't what to do about the conflict.
Redirector::after_deactivation()returns a destination orfalseand theexitstays in the resolver, so every destination is assertable — and it matches on screen basename, sincewp_get_referer()prefers the bare_wp_http_refererpath thatadmin_url()comparisons miss behind a proxy or in the network admin.