Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions features/plugin-install-github-latest.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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
"""
And STDOUT should contain:
"""
package from https://api.github.com/repos/danielbachhuber/one-time-login/zipball/
"""
And STDOUT should contain:
"""
Plugin installed successfully.
Success: Installed 1 of 1 plugins.
"""
119 changes: 118 additions & 1 deletion src/WP_CLI/CommandWithUpgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ abstract class CommandWithUpgrade extends \WP_CLI_Command {

protected $chained_command = false;

/**
* The GitHub Releases public api endpoint.
*
* @var string
*/
private $github_releases_api_endpoint = 'https://api.github.com/repos/%s/releases';

/**
* The GitHub latest release url format.
*
* @var string
*/
private $github_latest_release_url = '/^https:\/\/github\.com\/(.*)\/releases\/latest\/?$/';

// Invalid version message.
const INVALID_VERSION_MESSAGE = 'version higher than expected';

Expand Down Expand Up @@ -145,7 +159,6 @@ private function show_legend( $items ) {
}

public function install( $args, $assoc_args ) {

$successes = 0;
$errors = 0;
foreach ( $args as $slug ) {
Expand All @@ -159,6 +172,25 @@ public function install( $args, $assoc_args ) {

$is_remote = false !== strpos( $slug, '://' );

if ( $is_remote ) {
$github_repo = $this->get_github_repo_from_releases_url( $slug );

if ( $github_repo ) {
$version = $this->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.
Expand Down Expand Up @@ -771,4 +803,89 @@ private function parse_url_host_component( $url, $component ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url.
return function_exists( 'wp_parse_url' ) ? wp_parse_url( $url, $component ) : parse_url( $url, $component );
}

/**
* Get the latest package version based on a given repo slug.
*
* @param string $repo_slug
*
* @return array{ name: string, url: string }|\WP_Error
*/
public function get_the_latest_github_version( $repo_slug ) {
$api_url = sprintf( $this->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 );

// WP_Http::FORBIDDEN doesn't exist in WordPress 3.7
if ( 403 === wp_remote_retrieve_response_code( $response ) ) {
return new \WP_Error(
403,
$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.
*/
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_releases_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.';
}
}