10: Notices queue - #10
Open
nikolaystrikhar wants to merge 3 commits into
Open
Conversation
Three defects, each of which loses the one warning a site owner gets that their plugin was deactivated. Storage was a transient. Verified in core: set_transient() short-circuits to wp_cache_set() and never touches the database when an external object cache is present, so on a Redis or Memcached site the queue lived only in the cache -- where wp_cache_flush(), which deploy scripts and every purge button call, destroys it. The merge notice is raised once and never re-queued. It is an option now. The queue was per-site while the deactivation is network-wide. The resolver passes $network_wide to deactivate_plugins(), removing the plugin from every site, but the explanation landed in whichever site's options table served the request. On a fifty-site network the superadmin would never find it. Multisite uses network options now. render() consumes the queue and had no capability check, and it is wired to a hook that fires for anyone who can reach wp-admin. A subscriber loading profile.php silently swallowed the notice, and nothing re-queues it. It checks activate_plugins first, which on multisite correctly resolves to superadmins. Also drop non-string entries instead of printing them, expose option_name() so a host can render the same queue without replacing the implementation, and cover the corrupted-queue, capability, cache-flush and missing-prefix paths.
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: the notice queue and its interface — merge, conflict and dependency notices — plus
Loader::notices(). Lands before the load path and the resolver because both call into it.Stacked on #9.
Usage:
Why this way: entries are keyed
"{$slug}:{$type}"rather than by slug, because one sub-plugin can legitimately earn a merge notice while its conflict is resolved and a dependency notice while its load is attempted, in the same request — keying by slug alone would silently drop one.The review found three defects, and they share a failure mode: each one loses the single warning a site owner ever gets that their plugin was deactivated. The merge notice is raised exactly once and nothing re-queues it.
Storage was a transient, which is not durable. Verified against core:
set_transient()opens withif ( wp_using_ext_object_cache() ) { wp_cache_set(...); }and returns without touching the database. On any Redis or Memcached site the queue would live only in the cache — andwp_cache_flush(), which deploy scripts and every "purge cache" button call, destroys it. Underallkeys-lrua zero-TTL key is also evictable. It is an option now. On a plain site this changes nothing (core already stored a no-expiry transient as an autoloaded option); on an object-cache site it is the difference between working and not. The Global Constraint that specified a transient is amended with the reasoning.The queue was per-site while the deactivation is network-wide.
Resolver::deactivate()passes$network_widetodeactivate_plugins()— that is whatis_standalone_plugin_network_active()exists for — which removes the plugin from every site in the network. Butset_transient()writes to the current blog's options table. On a fifty-site network, a front-end request to site 37 would deactivate the plugin everywhere and file the explanation inwp_37_options, where the superadmin never looks. Multisite uses network options now.render()consumed the queue with no capability check. It is wired to a hook that fires for every user who can reach wp-admin, and rendering deletes the queue. A subscriber loadingprofile.phpwould print the warning to themselves and destroy it; the administrator would never learn their plugin was auto-deactivated. It checksactivate_pluginsfirst, which on multisite correctly resolves to superadmins because that cap maps throughmanage_network_pluginsthere.That last one also fixes the hook. Task 11's plan wired
render()toadmin_notices, but core dispatchesadmin_notices,network_admin_noticesanduser_admin_noticesas mutually exclusive branches — so a superadmin working in the network admin, exactly where a network-wide deactivation gets noticed, would never see the queue render. The plan now usesall_admin_notices.Also: non-string entries are dropped rather than printed, and the cleaned array is written back so a corrupted queue heals itself;
Notices::option_name()is public so a host can render the same queue without replacing the implementation; and the planned privatemessage_or_default()helper is gone in favour of the$defaultparameter added toSub_Plugin::get_conflict_notice_message()in PR 7 — identical semantics, one less duplicated method.Verify:
slic run unit— 125 tests, 186 assertions, green.slic run unit --env multisite— green, 1 deliberate skip (the autoload assertion is meaningless for network options).composer test:analysis→[OK] No errors, exit 0. New coverage for the capability gate, the logged-out path, survival acrosswp_cache_flush(), a corrupted queue, and the missing-hook-prefix throw.Not covered: concurrent read-modify-write on the queue. Two simultaneous requests queueing different notices can lose one; the dependency notice re-queues next request, the merge notice does not. Closing it would mean one option per notice — noted rather than fixed, since the window is a few milliseconds inside
plugins_loaded.