Make WordPress Core

Opened 4 weeks ago

Last modified 5 days ago

#66184 new defect (bug)

After uploading a plugin that "requires" other plugins, the "Activate Plugin" button leads to a wp_die() WSOD

Reported by: BusinessBloomer Owned by:
Priority: high Milestone: 7.2
Component: Plugins Version: 7.1.2
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses: ui, administration

Description

When a plugin is installed by uploading a zip, the result screen always shows an "Activate Plugin" button, even when the plugin's Requires Plugins header lists plugins that are not installed or not active. Clicking it takes the user to a bare wp_die() white screen with no admin menu and no way forward other than the browser's back button or the "Manage plugins" link.

The Plugins list table and the AJAX install flow already block activation in this case (the AJAX flow since #61294). The installer screen should behave the same way.

Steps to reproduce

  1. Create a plugin with this header and zip it:
<?php
/**
 * Plugin Name: Dep Test Plugin
 * Requires Plugins: classic-editor
 */
  1. Make sure the required plugin (Classic Editor in this example) is not installed on the site
  2. Go to Plugins > Add New Plugin > Upload Plugin and install the zip.
  3. On the result screen, click "Activate Plugin".

Actual result: a wp_die() white screen reading "Error: Dep Test Plugin requires 1 plugin to be installed and activated: Classic Editor. Manage plugins."

Expected result: the result screen does not offer "Activate Plugin" (or "Network Activate" on multisite) and tells the user which plugins are missing, inside the normal admin screen.

Cause

Uploads and non-JS installs run through update.php?action=upload-plugin and update.php?action=install-plugin, which render Plugin_Installer_Skin. Plugin_Installer_Skin::after() builds the activate links based only on the install result, the activate_plugin capability and whether the plugin is already active. It never checks the plugin's requirements, and update.php never initializes WP_Plugin_Dependencies.

The link goes to plugins.php?action=activate, where activate_plugin() calls validate_plugin_requirements(), gets a plugin_missing_dependencies error, and plugins.php passes it to wp_die().

Proposed fix

In Plugin_Installer_Skin::after(), when the install succeeded and the user can activate the plugin, call validate_plugin_requirements(), the same check activate_plugin() runs. If it returns a WP_Error, remove the activate and network activate links and show the error message as an admin notice.

validate_plugin_requirements() already calls WP_Plugin_Dependencies::initialize(), and Plugin_Upgrader::install() has already cleared the plugins cache, so the newly installed plugin's header is read correctly. Reusing this function also means the installer screen never offers a link that activate_plugin() would reject, including the WordPress and PHP version checks and the validate_plugin_requirements filter.

Patch attached.

Testing instructions

  1. Apply the patch.
  2. Repeat the steps to reproduce above.
  3. Confirm the result screen shows an error notice naming the missing plugin and no "Activate Plugin" button.
  4. Upload a plugin without a Requires Plugins header and confirm "Activate Plugin" still appears.
  5. Optional: repeat on multisite from Network Admin and confirm "Network Activate" is hidden only in the unmet-dependency case.
  • #61294: the same bug in the AJAX install flow, fixed in 6.5.4 by adding a missed initialize() call.
  • #64493: improving plugin dependency messages (7.2).
  • #22316: the original Plugin Dependencies feature, shipped in 6.5.

Attachments (4)

plugin-installer-skin-dependencies.diff​ (991 bytes ) - added by BusinessBloomer 4 weeks ago.
Hide the activate links in Plugin_Installer_Skin::after() and show the error notice when validate_plugin_requirements() fails, e.g. for unmet Requires Plugins dependencies
1-before-upload-shows-activate-button.jpg​ (28.7 KB ) - added by algorithmsunlocks 3 weeks ago.
Before: upload result offers "Activate Plugin" (trunk)
2-before-activate-leads-to-wp-die.jpg​ (10.4 KB ) - added by algorithmsunlocks 3 weeks ago.
Before: clicking Activate Plugin ends in wp_die() (trunk)
3-after-upload-shows-error-notice.jpg​ (31.0 KB ) - added by algorithmsunlocks 3 weeks ago.
After: no activation link, inline error notice (PR 13921)

Download all attachments as: .zip

Change History (17)

@BusinessBloomer
4 weeks ago

Hide the activate links in Plugin_Installer_Skin::after() and show the error notice when validate_plugin_requirements() fails, e.g. for unmet Requires Plugins dependencies

#1 @webdados
4 weeks ago

I can confirm this issue, helped @BusinessBloomer with the discovery, and wrote about it here ​https://blog.wonderm00n.com/2026/09/25/wordpress-plugins-should-a-pro-add-on-auto-install-its-free-version/

#2 @gunjanjaswal
4 weeks ago

Thanks for the detailed write-up and the patch. I can reproduce this: Plugin_Installer_Skin::after() builds the activate and network-activate links from only the install result, the activate_plugin capability and is_plugin_active(), so it never runs validate_plugin_requirements(). A plugin with an unmet Requires Plugins header still gets an Activate button, and plugins.php then rejects it with wp_die() -- the same failure the AJAX flow had before #61294.

Your fix (run the same check activate_plugin() uses, then drop the links and surface the error) looks right to me.

If it's useful, I'm happy to open a PR in the GitHub mirror carrying your fix plus unit tests, with props to you and webdados -- the installer-skin path has no coverage for the unmet-dependency case yet, and core will want a tested PR to land it. Equally happy to just add tests on top if you'd rather open the PR yourself. Either way, nice catch.

This ticket was mentioned in ​PR #13760 on ​WordPress/wordpress-develop by ​BusinessBloomer.


4 weeks ago
#3

  • Keywords has-patch added

Problem

After installing a plugin from a zip (Plugins > Add New Plugin > Upload Plugin), the result screen offers "Activate Plugin" even when the plugin's Requires Plugins header lists plugins that are not installed or not active. Clicking it goes to plugins.php?action=activate, where activate_plugin() returns the plugin_missing_dependencies error and plugins.php passes it to wp_die(). The user lands on a bare error page with no admin menu.

The Plugins list screen and the AJAX install flow (#61294) already avoid this. The installer screen should too.

Change

In Plugin_Installer_Skin::after(), when the install succeeded and the user can activate the plugin, run validate_plugin_requirements(), the same check activate_plugin() runs. If it returns a WP_Error, remove the activate and network activate links and show the error message as an inline admin notice.

Reusing validate_plugin_requirements() means the installer screen never offers a link that activate_plugin() would reject, including the WordPress and PHP version checks and the validate_plugin_requirements filter.

wp_clean_plugins_cache( false ) is called first. When I tested, get_plugins() did not yet list the newly uploaded plugin at this point of the request, so WP_Plugin_Dependencies did not know its Requires Plugins header and the check passed. Without the cache clear the link was still offered. I have not identified where the stale list comes from, so a reviewer who knows this part of the upgrader may have a better place for the fix.

Testing

Tested by hand on WordPress 7.1, not yet with PHPUnit.

  1. Create a plugin whose header contains Requires Plugins: classic-editor and zip it. Make sure Classic Editor is not installed.
  2. Upload it under Plugins > Add New Plugin > Upload Plugin.
  3. Before: the result screen shows "Activate Plugin", and clicking it ends in the wp_die() screen.
  4. After: no "Activate Plugin" link, and an inline error says "Dep Test Plugin requires 1 plugin to be installed and activated: classic-editor. Manage plugins."
  5. A plugin without a Requires Plugins header still shows "Activate Plugin" and activates normally.

Not tested: multisite (Network Activate), and the case where the dependency is installed but inactive.

Not changed here: plugins.php?action=activate still uses wp_die() when reached directly, for example from a stale tab.

AI assistance

The patch and this description were drafted with Claude Code (Anthropic), which also ran the manual test above on a local WordPress 7.1 site. I reproduced the original bug and reviewed the diff.

🤖 Generated with ​Claude Code

​@BusinessBloomer commented on ​PR #13760:


4 weeks ago
#4

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @rodolfomelogli@….

Contributors, please ​read how to link your accounts to ensure your work is properly credited in WordPress releases.

To understand the WordPress project's expectations around crediting contributors, please ​review the Contributor Attribution page in the Core Handbook.

Linked my GitHub and WordPress.org accounts (BusinessBloomer). Could the props list be refreshed? Thanks.

#5 @SergeyBiryukov
4 weeks ago

  • Component General → Plugins
  • Focuses administration added
  • Milestone Awaiting Review → 7.2

#6 @joedolson
3 weeks ago

  • Focuses accessibility removed

This ticket was mentioned in ​PR #13921 on ​WordPress/wordpress-develop by ​@algorithmsunlocks.


3 weeks ago
#7

  • Keywords has-unit-tests added

Trac ticket:

Use of AI Tools

​@algorithmsunlocks commented on ​PR #13921:


3 weeks ago
#8

The Upgrade Develop Version Tests failures are unrelated to this change: setup-php failed to install WP-CLI ("Could not setup wp-cli"), so wp core download exits with 127. PHPUnit, coding standards, and E2E all pass. Could someone re-run the failed jobs?

@algorithmsunlocks
3 weeks ago

Before: clicking Activate Plugin ends in wp_die() (trunk)

@algorithmsunlocks
3 weeks ago

After: no activation link, inline error notice (PR 13921)

#9 @algorithmsunlocks
3 weeks ago

Patch Testing Report

Patch tested: ​https://github.com/WordPress/wordpress-develop/pull/13921 (the fix from PR 13760 plus new PHPUnit tests)

Environment

  • WordPress: 7.2-alpha-63166-src (trunk)
  • PHP: 8.3.33
  • Server: wordpress-develop local Docker environment
  • Database: MySQL 9.7.2
  • Browser: Chrome 154
  • OS: macOS
  • Theme: Twenty Twenty-Five
  • Plugins: none (Classic Editor only for steps 4-5)

Steps

  1. Zip a plugin with Requires Plugins: classic-editor, with Classic Editor not installed.
  2. Plugins > Add New Plugin > Upload Plugin, and install the zip.
  3. Upload a plugin without Requires Plugins.
  4. Install Classic Editor but leave it inactive, then re-upload the first plugin.
  5. Activate Classic Editor, then re-upload the first plugin.

Actual Results

  1. Before the patch: "Activate Plugin" is offered and leads to a wp_die() screen.
  2. With the patch: no activation button, and an inline error notice names the missing plugin.
  3. A plugin without dependencies still gets a working "Activate Plugin" button.
  4. Inactive dependency: still no activation button.
  5. Active dependency: "Activate Plugin" is shown and works.

Additional Notes

  • New tests in tests/phpunit/tests/admin/pluginInstallerSkin.php fail on trunk and pass with the patch, on single site and multisite.
  • The failing "Upgrade Develop Version Tests" jobs on the PR are unrelated: setup-php could not install WP-CLI. Could someone with access re-run them?
  • Minor: the new notice names the dependency by slug (classic-editor) instead of its display name ("Classic Editor"), because the dependency API data isn't fetched during the upload request.
  • Edge case: if WP_Plugin_Dependencies::initialize() already ran earlier in the same request (only possible when a plugin triggers it), the new plugin isn't detected and the link is still shown. This is probably a follow-up.
  • @BusinessBloomer, PR 13921 keeps your commit unchanged. Feel free to cherry-pick the test commit into PR 13760 instead, and I'll close mine.

Supplemental Artifacts

Before: the upload result offers "Activate Plugin":
Before: upload result offers "Activate Plugin" (trunk)

Before: clicking it ends in wp_die():
Before: clicking Activate Plugin ends in wp_die() (trunk)

After: no activation link, and an inline error notice:
After: no activation link, inline error notice (PR 13921)

AI assistance: Yes. Claude Code (Claude Opus 5.5) was used for reproduction, root-cause tracing, writing the tests, and capturing the screenshots. I reviewed and manually verified everything.

​@huzaifaalmesbah commented on ​PR #13921:


2 weeks ago
#10

One suggestion for tests/phpunit/tests/admin/pluginInstallerSkin.php:

Currently, the test covers when the dependency is missing/not installed.

Could we also add a test case for when the dependency is installed on the site, but inactive (is_plugin_inactive())? That would ensure both branches of validate_plugin_requirements() are covered for this skin.

#11 @algorithmsunlocks
11 days ago

@huzaifaalmesbah thanks, added test_should_not_offer_activation_when_required_plugin_is_inactive() to PR ​https://github.com/WordPress/wordpress-develop/pull/13921, also rebased on current trunk. --group 66184 (3 tests): 2 fail on trunk, 3/3 pass with the patch on single site and multisite; --group plugins (156 tests) passes.

​@algorithmsunlocks commented on ​PR #13921:


11 days ago
#12

@huzaifaalmesbah thanks, added test_should_not_offer_activation_when_required_plugin_is_inactive() to PR ​https://github.com/WordPress/wordpress-develop/pull/13921, also rebased on current trunk. --group 66184 (3 tests): 2 fail on trunk, 3/3 pass with the patch on single site and multisite; --group plugins (156 tests) passes.

#13 @huzaifaalmesbah
5 days ago

Thanks @algorithmsunlocks, the new inactive-dependency test works: on current trunk, 2 of the 3 tests fail without the src change, and all 3 pass with it on single site and multisite.

On the open question from ​PR 13760: the wp_clean_plugins_cache( false ) call is needed, because WP_Upgrader::run() calls $this->skin->after() before upgrader_process_complete and before Plugin_Upgrader::install() clears the cache.

Minor, non-blocking: when the required plugin is not installed, the notice shows the slug (classic-editor) instead of the name shown on the wp_die() screen ("Classic Editor"), since no plugin API data is loaded on this screen.

Note: See TracTickets for help on using tickets.