diff --git a/src/Http.php b/src/Http.php index e78d71e..93bbdfd 100644 --- a/src/Http.php +++ b/src/Http.php @@ -7,6 +7,8 @@ namespace Junaidbhura\Composer\WPProPlugins; +use RuntimeException; + /** * Http class. */ @@ -15,47 +17,65 @@ class Http { /** * POST request. * - * @param string $url Url to POST. - * @param array $args Arguments to POST. - * @return mixed + * @param string $url URL to POST. + * @param array $args Arguments to POST. + * @return string */ - public function post( $url = '', $args = array() ) { + public function post( $url, $args = array() ) { $curl_handle = curl_init(); curl_setopt( $curl_handle, CURLOPT_URL, $url ); - curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, 1 ); - curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true ); - curl_setopt( $curl_handle, CURLOPT_SSL_VERIFYPEER, 0 ); - curl_setopt( $curl_handle, CURLOPT_SSL_VERIFYHOST, 0 ); + curl_setopt( $curl_handle, CURLOPT_SSL_VERIFYPEER, false ); + curl_setopt( $curl_handle, CURLOPT_SSL_VERIFYHOST, false ); curl_setopt( $curl_handle, CURLOPT_CUSTOMREQUEST, 'POST' ); if ( ! empty( $args ) ) { curl_setopt( $curl_handle, CURLOPT_POSTFIELDS, http_build_query( $args, '', '&' ) ); } - $response = curl_exec( $curl_handle ); - curl_close( $curl_handle ); - return $response; + return $this->request( $curl_handle ); } /** * GET request. * - * @param string $url Base URL for requeset (without params) - * @param array $args Arguments to add to request - * @return mixed + * @param string $url URL to GET (without params). + * @param array $args Arguments to add to request. + * @return string */ - public function get( $url = '', $args = array() ) { - $query_string = ''; + public function get( $url, $args = array() ) { + if ( ! empty( $args ) ) { + $url .= '?' . http_build_query( $args, '', '&' ); + } $curl_handle = curl_init(); - curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, 1 ); + curl_setopt( $curl_handle, CURLOPT_URL, $url ); + + return $this->request( $curl_handle ); + } + + /** + * @param \CurlHandle|resource $curl_handle The cURL handler. + * @throws RuntimeException If the request failed or the response is invalid. + * @return string The response body. + */ + protected function request( $curl_handle ) { + curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true ); - if ( ! empty( $args ) ) { - $query_string = http_build_query( $args, '', '&' ); - } - curl_setopt( $curl_handle, CURLOPT_URL, $url . '?' . $query_string ); + curl_setopt( $curl_handle, CURLOPT_FAILONERROR, true ); + $response = curl_exec( $curl_handle ); + + $curl_errno = curl_errno( $curl_handle ); + $curl_error = curl_error( $curl_handle ); curl_close( $curl_handle ); + if ( false === $response ) { + throw new RuntimeException( sprintf( + 'cURL error (%d): %s', + $curl_errno, + $curl_error + ), $curl_errno ); + } + return $response; } diff --git a/src/Plugins/AbstractEddPlugin.php b/src/Plugins/AbstractEddPlugin.php index cacf139..d575b3c 100644 --- a/src/Plugins/AbstractEddPlugin.php +++ b/src/Plugins/AbstractEddPlugin.php @@ -8,6 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Composer\Semver\Semver; +use Exception; use UnexpectedValueException; /** @@ -15,37 +16,109 @@ */ abstract class AbstractEddPlugin extends AbstractPlugin { + /** + * Get the download URL for this plugin from its API. + * + * @return string + */ + abstract protected function getDownloadUrlFromApi(); + /** * Get the download URL for this plugin. * - * @param array $response The EDD API response. + * @throws UnexpectedValueException If the response is invalid. * @return string */ - protected function extractDownloadUrl( array $response ) { - if ( empty( $response['download_link'] ) || ! is_string( $response['download_link'] ) ) { + public function getDownloadUrl() { + try { + $response = $this->getDownloadUrlFromApi(); + } catch ( Exception $e ) { + $details = []; + + $error = $e->getMessage(); + if ( $error ) { + $details[] = 'HTTP Error: ' . $error; + } + + $message = sprintf( + 'Could not query API for package %s. Please try again later.', + $this->getPackageName() + ); + + if ( $details ) { + $message .= PHP_EOL . PHP_EOL . implode( PHP_EOL . PHP_EOL, $details ); + } + + throw new UnexpectedValueException( $message ); + } + + try { + /** + * @todo When the Composer plugin drops support for PHP 5, + * use the `json_decode()` function's `JSON_THROW_ON_ERROR` flag, + * introduced in PHP 7.3, to simplify error handling. + */ + $data = json_decode( $response, true ); + + if ( json_last_error() !== JSON_ERROR_NONE ) { + throw new Exception( + json_last_error_msg(), + json_last_error() + ); + } + + if ( ! is_array( $data ) ) { + throw new UnexpectedValueException( + 'Expected a data structure' + ); + } + } catch ( Exception $e ) { + $details = [ + 'json_decode(): ' . $e->getMessage(), + ]; + + $response_length = mb_strlen( $response ); + if ( $response_length > 0 ) { + $details[] = ' ' . mb_substr( $response, 0, 100 ) . ( $response_length > 100 ? '...' : '' ); + } + + $message = sprintf( + 'Expected a data structure from API for package %s. Please try again later.', + $this->getPackageName() + ); + + if ( $details ) { + $message .= PHP_EOL . PHP_EOL . implode( PHP_EOL . PHP_EOL, $details ); + } + + throw new UnexpectedValueException( $message ); + } + + if ( empty( $data['download_link'] ) || ! is_string( $data['download_link'] ) ) { throw new UnexpectedValueException( sprintf( - 'Expected a valid download URL for package %s', - 'junaidbhura/' . $this->slug + 'Expected a valid download URL from API for package %s', + $this->getPackageName() ) ); } - if ( empty( $response['new_version'] ) || ! is_scalar( $response['new_version'] ) ) { + if ( empty( $data['new_version'] ) || ! is_scalar( $data['new_version'] ) ) { throw new UnexpectedValueException( sprintf( - 'Expected a valid download version number for package %s', - 'junaidbhura/' . $this->slug + 'Expected a valid download version number from API for package %s', + $this->getPackageName() ) ); } - if ( ! Semver::satisfies( $response['new_version'], $this->version ) ) { + // If no version is specified, we are fetching the latest version. + if ( $this->version && ! Semver::satisfies( $data['new_version'], $this->version ) ) { throw new UnexpectedValueException( sprintf( - 'Expected download version (%s) to match installed version (%s) of package %s', - $response['new_version'], + 'Expected download version from API (%s) to match installed version (%s) of package %s', + $data['new_version'], $this->version, - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } - return $response['download_link']; + return $data['download_link']; } } diff --git a/src/Plugins/AbstractPlugin.php b/src/Plugins/AbstractPlugin.php index 0bab616..b1524a6 100644 --- a/src/Plugins/AbstractPlugin.php +++ b/src/Plugins/AbstractPlugin.php @@ -15,14 +15,14 @@ abstract class AbstractPlugin { /** * The version number of the plugin to download. * - * @var string Version number. + * @var string */ protected $version = ''; /** - * The slug of which plugin to download. + * The name of the plugin to download. * - * @var string Plugin slug. + * @var string */ protected $slug = ''; @@ -30,6 +30,7 @@ abstract class AbstractPlugin { * AbstractPlugin constructor. * * @param string $version + * @param string $slug */ public function __construct( $version = '', $slug = '' ) { $this->version = $version; @@ -43,4 +44,13 @@ public function __construct( $version = '', $slug = '' ) { */ abstract public function getDownloadUrl(); + /** + * Get the plugin's Composer package name with vendor. + * + * @return string + */ + protected function getPackageName() { + return 'junaidbhura/' . $this->slug; + } + } diff --git a/src/Plugins/AcfExtendedPro.php b/src/Plugins/AcfExtendedPro.php index e5766ff..25f819c 100644 --- a/src/Plugins/AcfExtendedPro.php +++ b/src/Plugins/AcfExtendedPro.php @@ -15,21 +15,28 @@ class AcfExtendedPro extends AbstractEddPlugin { /** - * Get the download URL for this plugin. + * Get the download URL for this plugin from its API. * * @return string */ - public function getDownloadUrl() { - $http = new Http(); - $response = json_decode( $http->post( 'https://acf-extended.com', array( + protected function getDownloadUrlFromApi() { + $http = new Http(); + + $api_query = array( 'edd_action' => 'get_version', 'license' => getenv( 'ACFE_PRO_KEY' ), 'item_name' => 'ACF Extended Pro', 'url' => getenv( 'ACFE_PRO_URL' ), - 'version' => $this->version, - ) ), true ); + ); + + // If no version is specified, we are fetching the latest version. + if ( $this->version ) { + $api_query['version'] = $this->version; + } + + $api_url = 'https://acf-extended.com'; - return $this->extractDownloadUrl( $response ); + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/AcfPro.php b/src/Plugins/AcfPro.php index b84f9bf..4b23bb7 100644 --- a/src/Plugins/AcfPro.php +++ b/src/Plugins/AcfPro.php @@ -18,7 +18,20 @@ class AcfPro extends AbstractPlugin { * @return string */ public function getDownloadUrl() { - return 'https://connect.advancedcustomfields.com/index.php?p=pro&a=download&k=' . getenv( 'ACF_PRO_KEY' ) . '&t=' . $this->version; + $api_query = array( + 'p' => 'pro', + 'a' => 'download', + 'k' => getenv( 'ACF_PRO_KEY' ), + ); + + // If no version is specified, we are fetching the latest version. + if ( $this->version ) { + $api_query['t'] = $this->version; + } + + $api_url = 'https://connect.advancedcustomfields.com/index.php'; + + return $api_url . '?' . http_build_query( $api_query, '', '&' ); } } diff --git a/src/Plugins/GravityForms.php b/src/Plugins/GravityForms.php index 7d3ec56..0a17039 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -7,6 +7,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; +use Exception; use Junaidbhura\Composer\WPProPlugins\Http; use UnexpectedValueException; @@ -28,20 +29,87 @@ public function __construct( $version = '', $slug = 'gravityforms' ) { /** * Get the download URL for this plugin. * + * @throws UnexpectedValueException If the response is invalid. * @return string */ public function getDownloadUrl() { - $http = new Http(); - $response = unserialize( $http->post( 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php?op=get_plugin&slug=' . $this->slug . '&key=' . getenv( 'GRAVITY_FORMS_KEY' ) ) ); + $http = new Http(); - if ( empty( $response['download_url_latest'] ) || ! is_string( $response['download_url_latest'] ) ) { + $api_query = array( + 'op' => 'get_plugin', + 'slug' => $this->slug, + 'key' => getenv( 'GRAVITY_FORMS_KEY' ), + ); + + $api_url = 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php'; + + try { + $response = $http->get( $api_url, $api_query ); + } catch ( Exception $e ) { + $details = []; + + $error = $e->getMessage(); + if ( $error ) { + $details[] = 'HTTP Error: ' . $error; + } + + $message = sprintf( + 'Could not query API for package %s. Please try again later.', + $this->getPackageName() + ); + + if ( $details ) { + $message .= PHP_EOL . PHP_EOL . implode( PHP_EOL . PHP_EOL, $details ); + } + + throw new UnexpectedValueException( $message ); + } + + // Catch any throwables from objects in their unserialization handlers. + // Composer itself handles converting PHP notices into exceptions. + try { + /** + * @todo When the Composer plugin drops support for PHP 5, + * use the `unserialize()` function's `allowed_classes` option, + * introduced in PHP 7, to disallow all classes. + */ + $data = unserialize( $response ); + + if ( $data !== false && ! is_array( $data ) ) { + throw new UnexpectedValueException( + 'unserialize(): Expected a data structure' + ); + } + } catch ( Exception $e ) { + $details = [ + $e->getMessage(), + ]; + + $response_length = mb_strlen( $response ); + if ( $response_length > 0 ) { + $details[] = ' ' . mb_substr( $response, 0, 100 ) . ( $response_length > 100 ? '...' : '' ); + } + + $message = sprintf( + 'Expected a data structure from API for package %s.', + $this->getPackageName() + ); + + if ( $details ) { + $message .= PHP_EOL . PHP_EOL . implode( PHP_EOL . PHP_EOL, $details ); + } + + throw new UnexpectedValueException( $message ); + } + + if ( empty( $data['download_url_latest'] ) || ! is_string( $data['download_url_latest'] ) ) { throw new UnexpectedValueException( sprintf( 'Expected a valid download URL for package %s', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } - return str_replace( 'http://', 'https://', $response['download_url_latest'] ); + return str_replace( 'http://', 'https://', $data['download_url_latest'] ); } } diff --git a/src/Plugins/NinjaForms.php b/src/Plugins/NinjaForms.php index 9dca8bc..4b95b74 100644 --- a/src/Plugins/NinjaForms.php +++ b/src/Plugins/NinjaForms.php @@ -8,7 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; -use UnexpectedValueException; +use InvalidArgumentException; /** * NinjaForms class. @@ -16,11 +16,12 @@ class NinjaForms extends AbstractEddPlugin { /** - * Get the download URL for this plugin. + * Get the download URL for this plugin from its API. * + * @throws InvalidArgumentException If the package is unsupported. * @return string */ - public function getDownloadUrl() { + protected function getDownloadUrlFromApi() { $name = ''; $env = null; /** @@ -296,9 +297,9 @@ public function getDownloadUrl() { break; default: - throw new UnexpectedValueException( sprintf( + throw new InvalidArgumentException( sprintf( 'Could not find a matching package for %s. Check the package spelling and that the package is supported', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } @@ -310,16 +311,23 @@ public function getDownloadUrl() { $url = ( getenv( "NINJA_FORMS_{$env}_URL" ) ?: $url ); } - $http = new Http(); - $response = json_decode( $http->get( 'https://ninjaforms.com', array( + $http = new Http(); + + $api_query = array( 'edd_action' => 'get_version', 'license' => $license, 'item_name' => $name, 'url' => $url, - 'version' => $this->version, - ) ), true ); + ); + + // If no version is specified, we are fetching the latest version. + if ( $this->version ) { + $api_query['version'] = $this->version; + } + + $api_url = 'https://ninjaforms.com'; - return $this->extractDownloadUrl( $response ); + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/PolylangPro.php b/src/Plugins/PolylangPro.php index 66df2cb..f6bff57 100644 --- a/src/Plugins/PolylangPro.php +++ b/src/Plugins/PolylangPro.php @@ -15,21 +15,28 @@ class PolylangPro extends AbstractEddPlugin { /** - * Get the download URL for this plugin. + * Get the download URL for this plugin from its API. * * @return string */ - public function getDownloadUrl() { - $http = new Http(); - $response = json_decode( $http->post( 'https://polylang.pro', array( + protected function getDownloadUrlFromApi() { + $http = new Http(); + + $api_query = array( 'edd_action' => 'get_version', 'license' => getenv( 'POLYLANG_PRO_KEY' ), 'item_name' => 'Polylang Pro', 'url' => getenv( 'POLYLANG_PRO_URL' ), - 'version' => $this->version, - ) ), true ); + ); + + // If no version is specified, we are fetching the latest version. + if ( $this->version ) { + $api_query['version'] = $this->version; + } + + $api_url = 'https://polylang.pro'; - return $this->extractDownloadUrl( $response ); + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/PublishPressPro.php b/src/Plugins/PublishPressPro.php index 81bd86c..2252383 100644 --- a/src/Plugins/PublishPressPro.php +++ b/src/Plugins/PublishPressPro.php @@ -8,7 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; -use UnexpectedValueException; +use InvalidArgumentException; /** * PublishPressPro class. @@ -26,11 +26,12 @@ public function __construct( $version = '', $slug = 'publishpress-planner-pro' ) } /** - * Get the download URL for this plugin. + * Get the download URL for this plugin from its API. * + * @throws InvalidArgumentException If the package is unsupported. * @return string */ - public function getDownloadUrl() { + protected function getDownloadUrlFromApi() { $id = 0; $env = null; /** @@ -89,9 +90,9 @@ public function getDownloadUrl() { break; default: - throw new UnexpectedValueException( sprintf( + throw new InvalidArgumentException( sprintf( 'Could not find a matching package for %s. Check the package spelling and that the package is supported', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } @@ -103,16 +104,23 @@ public function getDownloadUrl() { $url = ( getenv( "PUBLISHPRESS_{$env}_PRO_URL" ) ?: $url ); } - $http = new Http(); - $response = json_decode( $http->get( 'https://publishpress.com', array( + $http = new Http(); + + $api_query = array( 'edd_action' => 'get_version', 'license' => $license, 'item_id' => $id, 'url' => $url, - 'version' => $this->version, - ) ), true ); + ); + + // If no version is specified, we are fetching the latest version. + if ( $this->version ) { + $api_query['version'] = $this->version; + } + + $api_url = 'https://publishpress.com'; - return $this->extractDownloadUrl( $response ); + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/WpAiPro.php b/src/Plugins/WpAiPro.php index 6041f56..6ddbcde 100644 --- a/src/Plugins/WpAiPro.php +++ b/src/Plugins/WpAiPro.php @@ -25,11 +25,11 @@ public function __construct( $version = '', $slug = 'wp-all-import-pro' ) { } /** - * Get the download URL for this plugin. + * Get the download URL for this plugin from its API. * * @return string */ - public function getDownloadUrl() { + protected function getDownloadUrlFromApi() { $url = ''; $name = ''; $license = ''; @@ -75,16 +75,23 @@ public function getDownloadUrl() { } } - $http = new Http(); - $response = json_decode( $http->get( 'https://www.wpallimport.com', array( + $http = new Http(); + + $api_query = array( 'edd_action' => 'get_version', 'license' => $license, 'item_name' => $name, 'url' => $url, - 'version' => $this->version, - ) ), true ); + ); + + // If no version is specified, we are fetching the latest version. + if ( $this->version ) { + $api_query['version'] = $this->version; + } + + $api_url = 'https://www.wpallimport.com'; - return $this->extractDownloadUrl( $response ); + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/Wpml.php b/src/Plugins/Wpml.php index b1ad222..984f7d9 100644 --- a/src/Plugins/Wpml.php +++ b/src/Plugins/Wpml.php @@ -7,7 +7,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; -use UnexpectedValueException; +use InvalidArgumentException; /** * Wpml class. @@ -27,6 +27,7 @@ public function __construct( $version = '', $slug = 'wpml-sitepress-multilingual /** * Get the download URL for this plugin. * + * @throws InvalidArgumentException If the package is unsupported. * @return string */ public function getDownloadUrl() { @@ -51,13 +52,26 @@ public function getDownloadUrl() { ); if ( ! array_key_exists( $this->slug, $packages ) ) { - throw new UnexpectedValueException( sprintf( + throw new InvalidArgumentException( sprintf( 'Could not find a matching package for %s. Check the package spelling and that the package is supported', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } - return 'https://wpml.org/?download=' . $packages[ $this->slug ] . '&user_id=' . getenv( 'WPML_USER_ID' ) . '&subscription_key=' . getenv( 'WPML_KEY' ) . '&version=' . $this->version; + $api_query = array( + 'download' => $packages[ $this->slug ], + 'user_id' => getenv( 'WPML_USER_ID' ), + 'subscription_key' => getenv( 'WPML_KEY' ), + ); + + // If no version is specified, we are fetching the latest version. + if ( $this->version ) { + $api_query['version'] = $this->version; + } + + $api_url = 'https://wpml.org/'; + + return $api_url . '?' . http_build_query( $api_query, '', '&' ); } }