Skip to content
Merged
55 changes: 55 additions & 0 deletions features/plugin.feature
Original file line number Diff line number Diff line change
Expand Up @@ -810,3 +810,58 @@ Feature: Manage WordPress plugins
"""
5.5
"""

@require-php-7
Scenario: Show plugin update as unavailable if it doesn't meet WordPress requirements
Given a WP install

When I run `wp core download --version=6.4 --force`
And I run `rm -r wp-content/themes/*`
And I run `wp plugin install wp-super-cache --version=1.9.4`

When I run `wp plugin list --name=wp-super-cache --field=update_version`
And save STDOUT as {UPDATE_VERSION}

When I run `wp plugin list --name=wp-super-cache --field=requires`
And save STDOUT as {REQUIRES}

When I run `wp plugin list --name=wp-super-cache --field=requires_php`
And save STDOUT as {REQUIRES_PHP}

And I run `wp plugin list`
Then STDOUT should be a table containing rows:
| name | status | update | version | update_version | auto_update | requires | requires_php |
| wp-super-cache | inactive | unavailable | 1.9.4 | {UPDATE_VERSION} | off | {REQUIRES} | {REQUIRES_PHP} |

When I try `wp plugin update wp-super-cache`
Then STDERR should contain:
"""
Warning: wp-super-cache: Requires a newer version of WordPress
"""

@less-than-php-8.0 @require-wp-5.6
Scenario: Show plugin update as unavailable if it doesn't meet PHP requirements
Given a WP install

And I run `wp plugin install edit-flow --version=0.9.8`

When I run `wp plugin list --name=edit-flow --field=update_version`
And save STDOUT as {UPDATE_VERSION}

When I run `wp plugin list --name=edit-flow --field=requires`
And save STDOUT as {REQUIRES}

When I run `wp plugin list --name=edit-flow --field=requires_php`
And save STDOUT as {REQUIRES_PHP}

And I run `wp plugin list`
Then STDOUT should be a table containing rows:
| name | status | update | version | update_version | auto_update | requires | requires_php |
| edit-flow | inactive | unavailable | 0.9.8 | {UPDATE_VERSION} | off | {REQUIRES} | {REQUIRES_PHP} |

When I try `wp plugin update edit-flow`
Then STDERR should contain:
"""
Warning: edit-flow: Requires a newer version of PHP
"""

28 changes: 28 additions & 0 deletions features/theme.feature
Original file line number Diff line number Diff line change
Expand Up @@ -620,3 +620,31 @@ Feature: Manage WordPress themes
Then STDOUT should be a table containing rows:
| auto_update |
| on |

@require-php-7
Scenario: Show theme update as unavailable if it doesn't meet WordPress requirements
Given a WP install

When I run `wp core download --version=6.2 --force`
And I run `rm -r wp-content/themes/*`
And I run `wp theme install kadence --version=1.1.1`

When I run `wp theme list --name=kadence --field=update_version`
And save STDOUT as {UPDATE_VERSION}

When I run `wp theme list --name=kadence --field=requires`
And save STDOUT as {REQUIRES}

When I run `wp theme list --name=kadence --field=requires_php`
And save STDOUT as {REQUIRES_PHP}

And I run `wp theme list`
Then STDOUT should be a table containing rows:
| name | status | update | version | update_version | auto_update | requires | requires_php |
| kadence | inactive | unavailable | 1.1.1 | {UPDATE_VERSION} | off | {REQUIRES} | {REQUIRES_PHP} |

When I try `wp theme update kadence`
Then STDERR should contain:
"""
Warning: kadence: Requires a newer version of WordPress
"""
94 changes: 75 additions & 19 deletions src/Plugin_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ protected function get_all_items() {
'file' => $file,
'auto_update' => false,
'tested_up_to' => '',
'requires' => '',
'requires_php' => '',
'wporg_status' => $wporg_info['status'],
'wporg_last_updated' => $wporg_info['last_updated'],
);
Expand All @@ -293,6 +295,8 @@ protected function get_all_items() {
'auto_update' => false,
'author' => $item_data['Author'],
'tested_up_to' => '',
'requires' => '',
'requires_php' => '',
'wporg_status' => '',
'wporg_last_updated' => '',
];
Expand Down Expand Up @@ -740,6 +744,8 @@ public function update( $args, $assoc_args ) {
}

protected function get_item_list() {
global $wp_version;

$items = [];
$duplicate_names = [];

Expand All @@ -760,29 +766,62 @@ protected function get_item_list() {
$update_info = ( isset( $all_update_info->response[ $file ] ) && null !== $all_update_info->response[ $file ] ) ? (array) $all_update_info->response[ $file ] : null;
$name = Utils\get_plugin_name( $file );
$wporg_info = $this->get_wporg_data( $name );
$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false );

if ( ! isset( $duplicate_names[ $name ] ) ) {
$duplicate_names[ $name ] = array();
}

$php_version = PHP_VERSION;

$requires = isset( $update_info ) && isset( $update_info['requires'] ) ? $update_info['requires'] : null;
$requires_php = isset( $update_info ) && isset( $update_info['requires_php'] ) ? $update_info['requires_php'] : null;

// If an update has requires_php set, check to see if the local version of PHP meets that requirement
// The plugins update API already filters out plugins that don't meet WordPress requirements, but does not
// filter out plugins based on PHP requirements -- so we must do that here
$compatible_php = empty( $requires_php ) || version_compare( PHP_VERSION, $requires_php, '>=' );

if ( ! $compatible_php ) {
$update = 'unavailable';
$update_unavailable_reason = "Requires a newer version of PHP [$requires_php] than available [$php_version]";
} else {
$update = $update_info ? 'available' : 'none';
}

// requires and requires_php are only provided by the plugins update API in the case of an update available.
// For display consistency, get these values from the current plugin file if they aren't in this response
if ( null === $requires ) {
$requires = ! empty( $plugin_data['RequiresWP'] ) ? $plugin_data['RequiresWP'] : '';
}

if ( null === $requires_php ) {
$requires_php = ! empty( $plugin_data['RequiresPHP'] ) ? $plugin_data['RequiresPHP'] : '';
}

$duplicate_names[ $name ][] = $file;
$items[ $file ] = [
'name' => $name,
'status' => $this->get_status( $file ),
'update' => (bool) $update_info,
'update_version' => isset( $update_info ) && isset( $update_info['new_version'] ) ? $update_info['new_version'] : null,
'update_package' => isset( $update_info ) && isset( $update_info['package'] ) ? $update_info['package'] : null,
'version' => $details['Version'],
'update_id' => $file,
'title' => $details['Name'],
'description' => wordwrap( $details['Description'] ),
'file' => $file,
'auto_update' => in_array( $file, $auto_updates, true ),
'author' => $details['Author'],
'tested_up_to' => '',
'wporg_status' => $wporg_info['status'],
'wporg_last_updated' => $wporg_info['last_updated'],
'recently_active' => in_array( $file, array_keys( $recently_active ), true ),
'name' => $name,
'status' => $this->get_status( $file ),
'update' => $update,
'update_version' => isset( $update_info ) && isset( $update_info['new_version'] ) ? $update_info['new_version'] : null,
'update_package' => isset( $update_info ) && isset( $update_info['package'] ) ? $update_info['package'] : null,
'version' => $details['Version'],
'update_id' => $file,
'title' => $details['Name'],
'description' => wordwrap( $details['Description'] ),
'file' => $file,
'auto_update' => in_array( $file, $auto_updates, true ),
'author' => $details['Author'],
'tested_up_to' => '',
'requires' => $requires,
'requires_php' => $requires_php,
'wporg_status' => $wporg_info['status'],
'wporg_last_updated' => $wporg_info['last_updated'],

'recently_active' => in_array( $file, array_keys( $recently_active ), true ),

'update_unavailable_reason' => isset( $update_unavailable_reason ) ? $update_unavailable_reason : '',
];

if ( $this->check_headers['tested_up_to'] ) {
Expand Down Expand Up @@ -817,9 +856,25 @@ protected function get_item_list() {
// Get info for all plugins that don't have an update.
$plugin_update_info = isset( $all_update_info->no_update[ $file ] ) ? $all_update_info->no_update[ $file ] : null;

// Compare version and update information in plugin list.
// Check if local version is newer than what is listed upstream.
if ( null !== $plugin_update_info && version_compare( $details['Version'], $plugin_update_info->new_version, '>' ) ) {
$items[ $file ]['update'] = static::INVALID_VERSION_MESSAGE;
$items[ $file ]['update'] = static::INVALID_VERSION_MESSAGE;
$items[ $file ]['requires'] = isset( $plugin_update_info->requires ) ? $plugin_update_info->requires : null;
$items[ $file ]['requires_php'] = isset( $plugin_update_info->requires_php ) ? $plugin_update_info->requires_php : null;
}

// If there is a plugin in no_update with a newer version than the local copy, it is because the plugins update api
// has already filtered it because the local WordPress version is too low
if ( null !== $plugin_update_info && version_compare( $details['Version'], $plugin_update_info->new_version, '<' ) ) {
$items[ $file ]['update'] = 'unavailable';
$items[ $file ]['update_version'] = $plugin_update_info->new_version;
$items[ $file ]['requires'] = isset( $plugin_update_info->requires ) ? $plugin_update_info->requires : null;
$items[ $file ]['requires_php'] = isset( $plugin_update_info->requires_php ) ? $plugin_update_info->requires_php : null;

$reason = "Requires a newer version of WordPress [$plugin_update_info->requires] than installed [$wp_version]";

$items[ $file ]['update_unavailable_reason'] = $reason;

}
}
}
Expand Down Expand Up @@ -1397,6 +1452,8 @@ public function delete( $args, $assoc_args = array() ) {
* * file
* * author
* * tested_up_to
* * requires
* * requires_php
* * wporg_status
* * wporg_last_updated
*
Expand Down Expand Up @@ -1488,7 +1545,6 @@ protected function get_status( $file ) {
if ( is_plugin_active_for_network( $file ) ) {
return 'active-network';
}

if ( is_plugin_active( $file ) ) {
return 'active';
}
Expand Down
29 changes: 21 additions & 8 deletions src/WP_CLI/CommandWithUpgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ private function status_all() {
$padding = $this->get_padding( $items );

foreach ( $items as $file => $details ) {
if ( $details['update'] ) {
if ( 'available' === $details['update'] ) {
$line = ' %yU%n';
} else {
$line = ' ';
Expand Down Expand Up @@ -150,8 +150,7 @@ private function show_legend( $items ) {
$this->map['long'][ $status ]
);
}

if ( in_array( true, wp_list_pluck( $items, 'update' ), true ) ) {
if ( in_array( 'available', wp_list_pluck( $items, 'update' ), true ) ) {
$legend_line[] = '%yU = Update Available%n';
}

Expand Down Expand Up @@ -375,7 +374,12 @@ protected function update_many( $args, $assoc_args ) {
$errors = count( $args ) - count( $items );
}

$items_to_update = wp_list_filter( $items, [ 'update' => true ] );
$items_to_update = array_filter(
$items,
function ( $item ) {
return isset( $item['update'] ) && 'none' !== $item['update'];
}
);

$minor = (bool) Utils\get_flag_value( $assoc_args, 'minor', false );
$patch = (bool) Utils\get_flag_value( $assoc_args, 'patch', false );
Expand Down Expand Up @@ -417,6 +421,11 @@ protected function update_many( $args, $assoc_args ) {
++$skipped;
unset( $items_to_update[ $item_key ] );
}
if ( 'unavailable' === $item_info['update'] ) {
WP_CLI::warning( "{$item_info['name']}: {$item_info['update_unavailable_reason']}" );
++$skipped;
unset( $items_to_update[ $item_key ] );
}
}

if ( Utils\get_flag_value( $assoc_args, 'dry-run' ) ) {
Expand Down Expand Up @@ -564,10 +573,14 @@ function ( $value ) {

foreach ( $item as $field => &$value ) {
if ( 'update' === $field ) {
if ( true === $value ) {
$value = 'available';
} elseif ( false === $value ) {
$value = 'none';
// If an update is unavailable, make sure to also show these fields which will explain why
if ( 'unavailable' === $value ) {
if ( ! in_array( 'requires', $this->obj_fields, true ) ) {
array_push( $this->obj_fields, 'requires' );
}
if ( ! in_array( 'requires_php', $this->obj_fields, true ) ) {
array_push( $this->obj_fields, 'requires_php' );
}
}
} elseif ( 'auto_update' === $field ) {
if ( true === $value ) {
Expand Down
69 changes: 56 additions & 13 deletions src/WP_CLI/ParseThemeNameInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ protected function check_optional_args_and_all( $args, $all, $verb = 'install' )
* @return array
*/
private function get_all_themes() {
global $wp_version;
// Extract the major WordPress version (e.g., "6.3") from the full version string
list($wp_core_version) = explode( '-', $wp_version );
$wp_core_version = implode( '.', array_slice( explode( '.', $wp_core_version ), 0, 2 ) );

$items = array();
$theme_version_info = array();

Expand Down Expand Up @@ -76,22 +81,60 @@ private function get_all_themes() {
}

foreach ( wp_get_themes() as $key => $theme ) {
$stylesheet = $theme->get_stylesheet();

$stylesheet = $theme->get_stylesheet();
$update_info = ( isset( $all_update_info->response[ $stylesheet ] ) && null !== $all_update_info->response[ $theme->get_stylesheet() ] ) ? (array) $all_update_info->response[ $theme->get_stylesheet() ] : null;

// Unlike plugin update responses, the wordpress.org API does not seem to check and filter themes that don't meet
// WordPress version requirements into a separate no_updates array
// Also unlike plugin update responses, the wordpress.org API seems to always include requires AND requires_php
$requires = isset( $update_info ) && isset( $update_info['requires'] ) ? $update_info['requires'] : null;
$requires_php = isset( $update_info ) && isset( $update_info['requires_php'] ) ? $update_info['requires_php'] : null;

$compatible_php = empty( $requires_php ) || version_compare( PHP_VERSION, $requires_php, '>=' );
$compatible_wp = empty( $requires ) || version_compare( $wp_version, $requires, '>=' );

if ( ! $compatible_php ) {
$update = 'unavailable';
$update_unavailable_reason = sprintf(
'Requires a newer version of PHP [%s] than installed [%s]',
$requires_php,
PHP_VERSION
);
} else {
$update = $update_info ? 'available' : 'none';
}

if ( ! $compatible_wp ) {
$update = 'unavailable';
$update_unavailable_reason = "Requires a newer version of WordPress [$requires] than installed [$wp_version]";
} else {
$update = $update_info ? 'available' : 'none';
}

// For display consistency, get these values from the current plugin file if they aren't in this response
if ( null === $requires ) {
$requires = ! empty( $theme->get( 'RequiresWP' ) ) ? $theme->get( 'RequiresWP' ) : '';
}

if ( null === $requires_php ) {
$requires_php = ! empty( $theme->get( 'RequiresPHP' ) ) ? $theme->get( 'RequiresPHP' ) : '';
}

$items[ $stylesheet ] = [
'name' => $key,
'status' => $this->get_status( $theme ),
'update' => (bool) $update_info,
'update_version' => isset( $update_info['new_version'] ) ? $update_info['new_version'] : null,
'update_package' => isset( $update_info['package'] ) ? $update_info['package'] : null,
'version' => $theme->get( 'Version' ),
'update_id' => $stylesheet,
'title' => $theme->get( 'Name' ),
'description' => wordwrap( $theme->get( 'Description' ) ),
'author' => $theme->get( 'Author' ),
'auto_update' => in_array( $stylesheet, $auto_updates, true ),
'name' => $key,
'status' => $this->get_status( $theme ),
'update' => $update,
'update_version' => isset( $update_info['new_version'] ) ? $update_info['new_version'] : null,
'update_package' => isset( $update_info['package'] ) ? $update_info['package'] : null,
'version' => $theme->get( 'Version' ),
'update_id' => $stylesheet,
'title' => $theme->get( 'Name' ),
'description' => wordwrap( $theme->get( 'Description' ) ),
'author' => $theme->get( 'Author' ),
'auto_update' => in_array( $stylesheet, $auto_updates, true ),
'requires' => $requires,
'requires_php' => $requires_php,
'update_unavailable_reason' => isset( $update_unavailable_reason ) ? $update_unavailable_reason : '',
];

// Compare version and update information in theme list.
Expand Down
Loading