Skip to content

10: Notices queue - #10

Open
nikolaystrikhar wants to merge 3 commits into
09-loader-resolvefrom
10-notices-queue
Open

10: Notices queue#10
nikolaystrikhar wants to merge 3 commits into
09-loader-resolvefrom
10-notices-queue

Conversation

@nikolaystrikhar

Copy link
Copy Markdown
Contributor

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:

Loader::notices()->queue_dependency_notice( $sub_plugin );
Loader::notices()->render();   // wired to all_admin_notices by boot()

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 with if ( 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 — and wp_cache_flush(), which deploy scripts and every "purge cache" button call, destroys it. Under allkeys-lru a 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_wide to deactivate_plugins() — that is what is_standalone_plugin_network_active() exists for — which removes the plugin from every site in the network. But set_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 in wp_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 loading profile.php would print the warning to themselves and destroy it; the administrator would never learn their plugin was auto-deactivated. It checks activate_plugins first, which on multisite correctly resolves to superadmins because that cap maps through manage_network_plugins there.

That last one also fixes the hook. Task 11's plan wired render() to admin_notices, but core dispatches admin_notices, network_admin_notices and user_admin_notices as 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 uses all_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 private message_or_default() helper is gone in favour of the $default parameter added to Sub_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 across wp_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.

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.
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