@@ -22,6 +22,20 @@ abstract class CommandWithUpgrade extends \WP_CLI_Command {
2222
2323 protected $ chained_command = false ;
2424
25+ /**
26+ * The GitHub Releases public api endpoint.
27+ *
28+ * @var string
29+ */
30+ private $ github_releases_api_endpoint = 'https://api.github.com/repos/%s/releases ' ;
31+
32+ /**
33+ * The GitHub latest release url format.
34+ *
35+ * @var string
36+ */
37+ private $ github_latest_release_url = '/^https:\/\/github\.com\/(.*)\/releases\/latest\/?$/ ' ;
38+
2539 // Invalid version message.
2640 const INVALID_VERSION_MESSAGE = 'version higher than expected ' ;
2741
@@ -145,7 +159,6 @@ private function show_legend( $items ) {
145159 }
146160
147161 public function install ( $ args , $ assoc_args ) {
148-
149162 $ successes = 0 ;
150163 $ errors = 0 ;
151164 foreach ( $ args as $ slug ) {
@@ -159,6 +172,25 @@ public function install( $args, $assoc_args ) {
159172
160173 $ is_remote = false !== strpos ( $ slug , ':// ' );
161174
175+ if ( $ is_remote ) {
176+ $ github_repo = $ this ->get_github_repo_from_releases_url ( $ slug );
177+
178+ if ( $ github_repo ) {
179+ $ version = $ this ->get_the_latest_github_version ( $ github_repo );
180+
181+ if ( is_wp_error ( $ version ) ) {
182+ WP_CLI ::error ( $ version ->get_error_message () );
183+ }
184+
185+ /**
186+ * Sets the $slug that will trigger the installation based on a zip file.
187+ */
188+ $ slug = $ version ['url ' ];
189+
190+ WP_CLI ::log ( 'Latest release resolved to ' . $ version ['name ' ] );
191+ }
192+ }
193+
162194 // Check if a URL to a remote or local zip has been specified.
163195 if ( $ is_remote || ( pathinfo ( $ slug , PATHINFO_EXTENSION ) === 'zip ' && is_file ( $ slug ) ) ) {
164196 // Install from local or remote zip file.
@@ -785,4 +817,89 @@ private function parse_url_host_component( $url, $component ) {
785817 // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url.
786818 return function_exists ( 'wp_parse_url ' ) ? wp_parse_url ( $ url , $ component ) : parse_url ( $ url , $ component );
787819 }
820+
821+ /**
822+ * Get the latest package version based on a given repo slug.
823+ *
824+ * @param string $repo_slug
825+ *
826+ * @return array{ name: string, url: string }|\WP_Error
827+ */
828+ public function get_the_latest_github_version ( $ repo_slug ) {
829+ $ api_url = sprintf ( $ this ->github_releases_api_endpoint , $ repo_slug );
830+ $ token = getenv ( 'GITHUB_TOKEN ' );
831+
832+ $ request_arguments = $ token ? [ 'headers ' => 'Authorization: Bearer ' . getenv ( 'GITHUB_TOKEN ' ) ] : [];
833+
834+ $ response = \wp_remote_get ( $ api_url , $ request_arguments );
835+
836+ if ( \is_wp_error ( $ response ) ) {
837+ return $ response ;
838+ }
839+
840+ $ body = \wp_remote_retrieve_body ( $ response );
841+ $ decoded_body = json_decode ( $ body );
842+
843+ // WP_Http::FORBIDDEN doesn't exist in WordPress 3.7
844+ if ( 403 === wp_remote_retrieve_response_code ( $ response ) ) {
845+ return new \WP_Error (
846+ 403 ,
847+ $ this ->build_rate_limiting_error_message ( $ decoded_body )
848+ );
849+ }
850+
851+ if ( null === $ decoded_body ) {
852+ return new \WP_Error ( 500 , 'Empty response received from GitHub.com API ' );
853+ }
854+
855+ if ( ! isset ( $ decoded_body [0 ] ) ) {
856+ return new \WP_Error ( '400 ' , 'The given Github repository does not have any releases ' );
857+ }
858+
859+ $ latest_release = $ decoded_body [0 ];
860+
861+ return [
862+ 'name ' => $ latest_release ->name ,
863+ 'url ' => $ this ->get_asset_url_from_release ( $ latest_release ),
864+ ];
865+ }
866+
867+ /**
868+ * Get the asset URL from the release array. When the asset is not present, we fallback to the zipball_url (source code) property.
869+ */
870+ private function get_asset_url_from_release ( $ release ) {
871+ if ( isset ( $ release ->assets [0 ]->browser_download_url ) ) {
872+ return $ release ->assets [0 ]->browser_download_url ;
873+ }
874+
875+ if ( isset ( $ release ->zipball_url ) ) {
876+ return $ release ->zipball_url ;
877+ }
878+
879+ return null ;
880+ }
881+
882+ /**
883+ * Get the GitHub repo from the URL.
884+ *
885+ * @param string $url
886+ *
887+ * @return string|null
888+ */
889+ public function get_github_repo_from_releases_url ( $ url ) {
890+ preg_match ( $ this ->github_latest_release_url , $ url , $ matches );
891+
892+ return isset ( $ matches [1 ] ) ? $ matches [1 ] : null ;
893+ }
894+
895+ /**
896+ * Build the error message we display in WP-CLI for the API Rate limiting error response.
897+ *
898+ * @param $decoded_body
899+ *
900+ * @return string
901+ */
902+ private function build_rate_limiting_error_message ( $ decoded_body ) {
903+ 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. ' ;
904+ }
788905}
0 commit comments