diff --git a/features/plugin.feature b/features/plugin.feature index b513af6c..79cacb31 100644 --- a/features/plugin.feature +++ b/features/plugin.feature @@ -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: This update requires WordPress version + """ + + @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: This update requires PHP version + """ + diff --git a/features/theme-update.feature b/features/theme-update.feature index c5bf42ed..d9fda895 100644 --- a/features/theme-update.feature +++ b/features/theme-update.feature @@ -32,6 +32,7 @@ Feature: Update WordPress themes | name | version | | twentytwelve | 4.0 | + @require-wp-4.5 Scenario: Not giving a slug on update should throw an error unless --all given Given a WP install And I run `wp theme path` diff --git a/features/theme.feature b/features/theme.feature index fa15a95c..169901b0 100644 --- a/features/theme.feature +++ b/features/theme.feature @@ -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: This update requires WordPress version + """ diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 55adf856..58ceaec7 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -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'], ); @@ -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' => '', ]; @@ -740,6 +744,8 @@ public function update( $args, $assoc_args ) { } protected function get_item_list() { + global $wp_version; + $items = []; $duplicate_names = []; @@ -760,29 +766,63 @@ 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(); } + $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 = sprintf( + 'This update requires PHP version %s, but the version installed is %s.', + $requires_php, + 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'] ) { @@ -817,9 +857,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 = "This update requires WordPress version $plugin_update_info->requires, but the version installed is $wp_version."; + + $items[ $file ]['update_unavailable_reason'] = $reason; + } } } @@ -1397,6 +1453,8 @@ public function delete( $args, $assoc_args = array() ) { * * file * * author * * tested_up_to + * * requires + * * requires_php * * wporg_status * * wporg_last_updated * @@ -1488,7 +1546,6 @@ protected function get_status( $file ) { if ( is_plugin_active_for_network( $file ) ) { return 'active-network'; } - if ( is_plugin_active( $file ) ) { return 'active'; } diff --git a/src/WP_CLI/CommandWithUpgrade.php b/src/WP_CLI/CommandWithUpgrade.php index d925aeb2..a1953eab 100755 --- a/src/WP_CLI/CommandWithUpgrade.php +++ b/src/WP_CLI/CommandWithUpgrade.php @@ -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 = ' '; @@ -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'; } @@ -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 ); @@ -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' ) ) { @@ -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 ) { diff --git a/src/WP_CLI/ParseThemeNameInput.php b/src/WP_CLI/ParseThemeNameInput.php index 6a30a386..84017f4b 100644 --- a/src/WP_CLI/ParseThemeNameInput.php +++ b/src/WP_CLI/ParseThemeNameInput.php @@ -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(); @@ -76,22 +81,62 @@ 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( + 'This update requires PHP version %s, but the version installed is %s.', + $requires_php, + PHP_VERSION + ); + } else { + $update = $update_info ? 'available' : 'none'; + } + + if ( ! $compatible_wp ) { + $update = 'unavailable'; + + $update_unavailable_reason = "This update requires WordPress version $requires, but the version installed is $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.