diff --git a/features/core-download.feature b/features/core-download.feature index 5c7fbb57..7ae4d866 100644 --- a/features/core-download.feature +++ b/features/core-download.feature @@ -484,3 +484,24 @@ Feature: Download WordPress Success: """ + @require-php-7.4 + Scenario: Installing beta version using --version=beta + Given an empty directory + And an empty cache + + When I try `wp core download --version=beta` + Then the wp-settings.php file should exist + And STDOUT should contain: + """ + Downloading WordPress + """ + And STDOUT should contain: + """ + (en_US)... + """ + And STDOUT should contain: + """ + Success: WordPress downloaded. + """ + And the return code should be 0 + diff --git a/features/core-update.feature b/features/core-update.feature index 021fddfc..2289948c 100644 --- a/features/core-update.feature +++ b/features/core-update.feature @@ -538,3 +538,22 @@ Feature: Update WordPress core """ Success: """ + + @require-php-7.4 + Scenario: Use `--version=beta` to update to the latest beta version + Given a WP install + + # Using `try` since checksums might not be available for beta/RC. + When I try `wp core update --version=beta --force` + Then STDOUT should contain: + """ + Updating to version + """ + And STDOUT should contain: + """ + (en_US)... + """ + And STDOUT should contain: + """ + Success: WordPress updated successfully. + """ diff --git a/src/Core_Command.php b/src/Core_Command.php index a5ae0f18..f3b156ea 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -135,7 +135,7 @@ public function check_update( $args, $assoc_args ) { * : Select which language you want to download. * * [--version=] - * : Select which version you want to download. Accepts a version number, 'latest' or 'nightly'. + * : Select which version you want to download. Accepts a version number, 'latest', 'nightly', or 'beta'. * * [--skip-content] * : Download WP without the default themes and plugins. @@ -226,12 +226,21 @@ public function download( $args, $assoc_args ) { $version = 'nightly'; } - // Nightly builds and skip content are only available in .zip format. - $extension = ( ( 'nightly' === $version ) || $skip_content ) - ? 'zip' - : 'tar.gz'; + if ( 'beta' === strtolower( $assoc_args['version'] ) ) { + $offer = $this->get_beta_download_offer( $locale, $insecure ); + $version = $offer['current']; + $download_url = $offer['download']; + if ( ! $skip_content ) { + $download_url = str_replace( '.zip', '.tar.gz', $download_url ); + } + } else { + // Nightly builds and skip content are only available in .zip format. + $extension = ( ( 'nightly' === $version ) || $skip_content ) + ? 'zip' + : 'tar.gz'; - $download_url = $this->get_download_url( $version, $locale, $extension ); + $download_url = $this->get_download_url( $version, $locale, $extension ); + } } else { try { $offer = ( new WpOrgApi( [ 'insecure' => $insecure ] ) ) @@ -1095,7 +1104,7 @@ private static function get_core_checksums( $version, $locale, $insecure ) { * : Only perform updates for minor releases (e.g. update from WP 4.3 to 4.3.3 instead of 4.4.2). * * [--version=] - * : Update to a specific version, instead of to the latest version. Alternatively accepts 'nightly'. + * : Update to a specific version, instead of to the latest version. Alternatively accepts 'nightly' or 'beta'. * * [--force] * : Update even when installed WP version is greater than the requested version. @@ -1229,6 +1238,7 @@ public function update( $args, $assoc_args ) { } } elseif ( Utils\wp_version_compare( $assoc_args['version'], '<' ) || 'nightly' === $assoc_args['version'] + || 'beta' === $assoc_args['version'] || Utils\get_flag_value( $assoc_args, 'force' ) || ! empty( $assoc_args['locale'] ) ) { @@ -1240,22 +1250,45 @@ public function update( $args, $assoc_args ) { */ $locale = Utils\get_flag_value( $assoc_args, 'locale', get_locale() ); - $new_package = $this->get_download_url( $version, $locale ); - - $update = (object) [ - 'response' => 'upgrade', - 'current' => $assoc_args['version'], - 'download' => $new_package, - 'packages' => (object) [ - 'partial' => null, - 'new_bundled' => null, - 'no_content' => null, - 'full' => $new_package, - ], - 'version' => $version, - 'locale' => $locale, - ]; - + if ( 'beta' === $version ) { + $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); + $offer = $this->get_beta_download_offer( $locale, $insecure ); + $version = $offer['current']; + + /** @var array{full?: string, no_content?: string|null, new_bundled?: string|null, partial?: string|null} $packages */ + $packages = isset( $offer['packages'] ) && is_array( $offer['packages'] ) ? $offer['packages'] : []; + $new_package = ! empty( $packages['no_content'] ) ? $packages['no_content'] : $offer['download']; + + $update = (object) [ + 'response' => 'upgrade', + 'current' => $version, + 'download' => $new_package, + 'packages' => (object) [ + 'partial' => $packages['partial'] ?? null, + 'new_bundled' => $packages['new_bundled'] ?? null, + 'no_content' => $packages['no_content'] ?? null, + 'full' => $packages['full'] ?? $offer['download'], + ], + 'version' => $version, + 'locale' => $offer['locale'] ?? $locale, + ]; + } else { + $new_package = $this->get_download_url( $version, $locale ); + + $update = (object) [ + 'response' => 'upgrade', + 'current' => $assoc_args['version'], + 'download' => $new_package, + 'packages' => (object) [ + 'partial' => null, + 'new_bundled' => null, + 'no_content' => null, + 'full' => $new_package, + ], + 'version' => $version, + 'locale' => $locale, + ]; + } } if ( ! empty( $update ) @@ -1605,6 +1638,55 @@ private function get_download_url( $version, $locale = 'en_US', $file_type = 'zi return "https://{$locale_subdomain}wordpress.org/wordpress-{$version}{$locale_suffix}.{$file_type}"; } + /** + * Gets the download offer for the latest WordPress beta or RC version. + * + * Queries the WordPress.org version-check API with `channel=beta` to retrieve + * the current beta or release candidate download offer. + * + * @param string $locale Locale to request. Defaults to 'en_US'. + * @param bool $insecure Whether to retry without certificate validation on TLS handshake failure. + * @return array{current: string, download: string, locale: string, packages: array{full: string, no_content: string|null, new_bundled: string|null, partial: string|null}} Associative array with download offer data. + */ + private function get_beta_download_offer( $locale = 'en_US', $insecure = false ) { + $url = 'https://api.wordpress.org/core/version-check/1.7/?' . http_build_query( + [ + 'channel' => 'beta', + 'locale' => $locale, + ], + '', + '&' + ); + + $options = [ + 'insecure' => $insecure, + 'halt_on_error' => false, + ]; + + /** @var \WpOrg\Requests\Response $response */ + $response = Utils\http_request( 'GET', $url, null, [ 'Accept' => 'application/json' ], $options ); + + if ( ! $response->success || (int) $response->status_code < 200 || (int) $response->status_code >= 300 ) { + WP_CLI::error( "Couldn't fetch response from {$url} (HTTP code {$response->status_code})." ); + } + + /** @var array{offers: array}|null $data */ + $data = json_decode( $response->body, true ); + + if ( ! is_array( $data ) || empty( $data['offers'] ) ) { + WP_CLI::error( 'No beta version found.' ); + } + + /** @var array{current: string, download: string, locale: string, packages: array{full: string, no_content: string|null, new_bundled: string|null, partial: string|null}} $offer */ + $offer = $data['offers'][0]; + + if ( ! is_array( $offer ) || empty( $offer['current'] ) || empty( $offer['download'] ) ) { + WP_CLI::error( 'Failed to parse beta version information.' ); + } + + return $offer; + } + /** * Returns update information. *