diff --git a/features/plugin-install-github-latest.feature b/features/plugin-install-github-latest.feature new file mode 100644 index 00000000..35d80886 --- /dev/null +++ b/features/plugin-install-github-latest.feature @@ -0,0 +1,15 @@ +Feature: Install WordPress plugins + + Scenario: Verify that providing a plugin releases/latest GitHub URL will get the latest ZIP + Given a WP install + When I run `wp plugin install https://github.com/danielbachhuber/one-time-login/releases/latest` + Then STDOUT should contain: + """ + Latest release resolved to Version 0.4.0 + Downloading installation package from + """ + And STDOUT should contain: + """ + Plugin installed successfully. + Success: Installed 1 of 1 plugins. + """ diff --git a/src/WP_CLI/CommandWithUpgrade.php b/src/WP_CLI/CommandWithUpgrade.php index 53c45df7..0cb7c061 100755 --- a/src/WP_CLI/CommandWithUpgrade.php +++ b/src/WP_CLI/CommandWithUpgrade.php @@ -145,7 +145,6 @@ private function show_legend( $items ) { } public function install( $args, $assoc_args ) { - $successes = 0; $errors = 0; foreach ( $args as $slug ) { @@ -159,6 +158,27 @@ public function install( $args, $assoc_args ) { $is_remote = false !== strpos( $slug, '://' ); + $github_parser = new ParseGithubUrlInput(); + + if ( $is_remote ) { + $github_repo = $github_parser->get_github_repo_from_url( $slug ); + + if ( $github_repo ) { + $version = $github_parser->get_the_latest_github_version( $github_repo ); + + if ( is_wp_error( $version ) ) { + WP_CLI::error( $version->get_error_message() ); + } + + /** + * Sets the $slug that will trigger the installation based on a zip file. + */ + $slug = $version['url']; + + WP_CLI::log( 'Latest release resolved to ' . $version['name'] ); + } + } + // Check if a URL to a remote or local zip has been specified. if ( $is_remote || ( pathinfo( $slug, PATHINFO_EXTENSION ) === 'zip' && is_file( $slug ) ) ) { // Install from local or remote zip file. diff --git a/src/WP_CLI/ParseGithubUrlInput.php b/src/WP_CLI/ParseGithubUrlInput.php new file mode 100644 index 00000000..14ded787 --- /dev/null +++ b/src/WP_CLI/ParseGithubUrlInput.php @@ -0,0 +1,108 @@ +github_releases_api_endpoint, $repo_slug ); + $token = getenv( 'GITHUB_TOKEN' ); + + $request_arguments = $token ? [ 'headers' => 'Authorization: Bearer ' . getenv( 'GITHUB_TOKEN' ) ] : []; + + $response = \wp_remote_get( $api_url, $request_arguments ); + + if ( \is_wp_error( $response ) ) { + return $response; + } + + $body = \wp_remote_retrieve_body( $response ); + $decoded_body = json_decode( $body ); + + if ( wp_remote_retrieve_response_code( $response ) === \WP_Http::FORBIDDEN ) { + return new \WP_Error( + \WP_Http::FORBIDDEN, + $this->build_rate_limiting_error_message( $decoded_body ) + ); + } + + if ( null === $decoded_body ) { + return new \WP_Error( 500, 'Empty response received from GitHub.com API' ); + } + + if ( ! isset( $decoded_body[0] ) ) { + return new \WP_Error( '400', 'The given Github repository does not have any releases' ); + } + + $latest_release = $decoded_body[0]; + + return [ + 'name' => $latest_release->name, + 'url' => $this->get_asset_url_from_release( $latest_release ), + ]; + } + + /** + * Get the asset URL from the release array. When the asset is not present, we fallback to the zipball_url (source code) property. + * + * @param array $release + * + * @return string|null + */ + private function get_asset_url_from_release( $release ) { + if ( isset( $release->assets[0]->browser_download_url ) ) { + return $release->assets[0]->browser_download_url; + } + + if ( isset( $release->zipball_url ) ) { + return $release->zipball_url; + } + + return null; + } + + /** + * Get the GitHub repo from the URL. + * + * @param string $url + * + * @return string|null + */ + public function get_github_repo_from_url( $url ) { + preg_match( $this->github_latest_release_url, $url, $matches ); + + return isset( $matches[1] ) ? $matches[1] : null; + } + + /** + * Build the error message we display in WP-CLI for the API Rate limiting error response. + * + * @param $decoded_body + * + * @return string + */ + private function build_rate_limiting_error_message( $decoded_body ) { + return $decoded_body->message . PHP_EOL . $decoded_body->documentation_url . PHP_EOL . 'In order to pass the token to WP-CLI, you need to use the GITHUB_TOKEN environment variable.'; + } +}