Bug summary
wp plugin list and wp theme list expose an update_unavailable_reason field. The reason string is set inside the per-item loop when an update fails the PHP or WordPress requirement, but the variable is never reset for the next item. After one incompatible item, later plugins or themes that do not set their own reason can show the first item's reason, even when their own update column says none or available. Anyone consuming --format=json or --format=csv gets contradictory rows.
Both list builders have the same pattern from the same commit:
Plugin_Command::get_item_list() (plugin side, PHP requirement branch)
- the
ParseThemeNameInput trait used by wp theme list (theme side, both the PHP and the WordPress requirement branches)
Introduced in 730da5d (Support requires and requires_php in plugin/theme list and update commands), so the leak exists since v2.1.24 and is still in v3.0.0 and main.
Steps to reproduce
On an install with two plugins (akismet and hello here), add a temporary mu-plugin that gives only akismet an update requiring an impossible PHP version:
<?php
add_filter( 'site_transient_update_plugins', function ( $transient ) {
if ( ! is_object( $transient ) ) {
$transient = new stdClass();
}
if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
$transient->response = array();
}
$transient->response['akismet/akismet.php'] = (object) array(
'slug' => 'akismet',
'plugin' => 'akismet/akismet.php',
'new_version' => '99.0',
'package' => 'https://downloads.wordpress.org/plugin/akismet.99.0.zip',
'requires' => '5.0',
'requires_php' => '99.0',
);
return $transient;
} );
Then:
$ wp plugin list --fields=name,update,update_unavailable_reason --format=csv
name,update,update_unavailable_reason
akismet,unavailable,"This update requires PHP version 99.0, but the version installed is 8.5.10."
hello,none,"This update requires PHP version 99.0, but the version installed is 8.5.10."
hello has no update at all, but reports akismet's reason. Expected: an empty reason for hello.
The theme side behaves the same with the equivalent site_transient_update_themes filter: injecting an incompatible update for twentytwentyfive makes twentytwentyfour and twentytwentythree both carry its reason. The leak is strictly forward: injecting the update on the last item of the list leaves all earlier rows clean.
The real world trigger is any site whose PHP is older than what one installed plugin's or theme's update requires, which is exactly the situation this field exists to report.
Environment
Reproduced against extension-command main (8b288e9) on WordPress 7.1 with PHP 8.5.10. Code inspected at the v2.1.24 and v3.0.0 tags, same pattern at both.
I have a fix ready (reset the variable per item in both loops) with Behat scenarios for both sides, PR follows.
Bug summary
wp plugin listandwp theme listexpose anupdate_unavailable_reasonfield. The reason string is set inside the per-item loop when an update fails the PHP or WordPress requirement, but the variable is never reset for the next item. After one incompatible item, later plugins or themes that do not set their own reason can show the first item's reason, even when their own update column saysnoneoravailable. Anyone consuming--format=jsonor--format=csvgets contradictory rows.Both list builders have the same pattern from the same commit:
Plugin_Command::get_item_list()(plugin side, PHP requirement branch)ParseThemeNameInputtrait used bywp theme list(theme side, both the PHP and the WordPress requirement branches)Introduced in 730da5d (Support requires and requires_php in plugin/theme list and update commands), so the leak exists since v2.1.24 and is still in v3.0.0 and main.
Steps to reproduce
On an install with two plugins (akismet and hello here), add a temporary mu-plugin that gives only akismet an update requiring an impossible PHP version:
Then:
hello has no update at all, but reports akismet's reason. Expected: an empty reason for hello.
The theme side behaves the same with the equivalent
site_transient_update_themesfilter: injecting an incompatible update for twentytwentyfive makes twentytwentyfour and twentytwentythree both carry its reason. The leak is strictly forward: injecting the update on the last item of the list leaves all earlier rows clean.The real world trigger is any site whose PHP is older than what one installed plugin's or theme's update requires, which is exactly the situation this field exists to report.
Environment
Reproduced against extension-command main (8b288e9) on WordPress 7.1 with PHP 8.5.10. Code inspected at the v2.1.24 and v3.0.0 tags, same pattern at both.
I have a fix ready (reset the variable per item in both loops) with Behat scenarios for both sides, PR follows.