From 23e7a41305903804dbe16a75820521671a1c9dbc Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Tue, 7 Mar 2023 16:30:43 -0500 Subject: [PATCH 01/15] Change URL formatting for ACF, GF, WPML Use `http_build_query()` to improve readability and reduce risk of errors. --- src/Plugins/AcfPro.php | 7 ++++++- src/Plugins/GravityForms.php | 6 +++++- src/Plugins/Wpml.php | 7 ++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Plugins/AcfPro.php b/src/Plugins/AcfPro.php index b84f9bf..4ba084a 100644 --- a/src/Plugins/AcfPro.php +++ b/src/Plugins/AcfPro.php @@ -18,7 +18,12 @@ 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; + return 'https://connect.advancedcustomfields.com/index.php?' . http_build_query( array( + 'p' => 'pro', + 'a' => 'download', + 'k' => getenv( 'ACF_PRO_KEY' ), + 't' => $this->version, + ), '', '&' ); } } diff --git a/src/Plugins/GravityForms.php b/src/Plugins/GravityForms.php index 7d3ec56..e3159a3 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -32,7 +32,11 @@ public function __construct( $version = '', $slug = 'gravityforms' ) { */ 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' ) ) ); + $response = unserialize( $http->post( 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php', array( + 'op' => 'get_plugin', + 'slug' => $this->slug, + 'key' => getenv( 'GRAVITY_FORMS_KEY' ), + ) ) ); if ( empty( $response['download_url_latest'] ) || ! is_string( $response['download_url_latest'] ) ) { throw new UnexpectedValueException( sprintf( diff --git a/src/Plugins/Wpml.php b/src/Plugins/Wpml.php index b1ad222..05d815c 100644 --- a/src/Plugins/Wpml.php +++ b/src/Plugins/Wpml.php @@ -57,7 +57,12 @@ public function getDownloadUrl() { ) ); } - return 'https://wpml.org/?download=' . $packages[ $this->slug ] . '&user_id=' . getenv( 'WPML_USER_ID' ) . '&subscription_key=' . getenv( 'WPML_KEY' ) . '&version=' . $this->version; + return 'https://wpml.org/?' . http_build_query( array( + 'download' => $packages[ $this->slug ], + 'user_id' => getenv( 'WPML_USER_ID' ), + 'subscription_key' => getenv( 'WPML_KEY' ), + 'version' => $this->version, + ), '', '&' ); } } From 281034026512ff05e388b3b219460e9c0d83d8be Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Tue, 7 Mar 2023 16:32:53 -0500 Subject: [PATCH 02/15] Change HTTP request method for ACF-E, GF, PLL Use GET instead of POST to better match the verb to the request and for consistency between downloaders. --- src/Plugins/AcfExtendedPro.php | 2 +- src/Plugins/GravityForms.php | 2 +- src/Plugins/PolylangPro.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Plugins/AcfExtendedPro.php b/src/Plugins/AcfExtendedPro.php index e5766ff..ba7db8c 100644 --- a/src/Plugins/AcfExtendedPro.php +++ b/src/Plugins/AcfExtendedPro.php @@ -21,7 +21,7 @@ class AcfExtendedPro extends AbstractEddPlugin { */ public function getDownloadUrl() { $http = new Http(); - $response = json_decode( $http->post( 'https://acf-extended.com', array( + $response = json_decode( $http->get( 'https://acf-extended.com', array( 'edd_action' => 'get_version', 'license' => getenv( 'ACFE_PRO_KEY' ), 'item_name' => 'ACF Extended Pro', diff --git a/src/Plugins/GravityForms.php b/src/Plugins/GravityForms.php index e3159a3..6c7a271 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -32,7 +32,7 @@ public function __construct( $version = '', $slug = 'gravityforms' ) { */ public function getDownloadUrl() { $http = new Http(); - $response = unserialize( $http->post( 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php', array( + $response = unserialize( $http->get( 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php', array( 'op' => 'get_plugin', 'slug' => $this->slug, 'key' => getenv( 'GRAVITY_FORMS_KEY' ), diff --git a/src/Plugins/PolylangPro.php b/src/Plugins/PolylangPro.php index 66df2cb..c035da6 100644 --- a/src/Plugins/PolylangPro.php +++ b/src/Plugins/PolylangPro.php @@ -21,7 +21,7 @@ class PolylangPro extends AbstractEddPlugin { */ public function getDownloadUrl() { $http = new Http(); - $response = json_decode( $http->post( 'https://polylang.pro', array( + $response = json_decode( $http->get( 'https://polylang.pro', array( 'edd_action' => 'get_version', 'license' => getenv( 'POLYLANG_PRO_KEY' ), 'item_name' => 'Polylang Pro', From 7d3aa9efa4c25db6a405e1d29eef56960ff2982e Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Wed, 15 Mar 2023 13:59:26 -0400 Subject: [PATCH 03/15] Clean-up usage of exceptions Changed: - Throw `InvalidArgumentException` instead of `UnexpectedValueException` when dealing with an unsupported package in Ninja Forms, PublishPress Pro, and WPML. - Throw `UnexpectedValueException` if the decoded/unserialized API response is not an array. - Improved messages to clarify errors from API. - Fixed and improved PHPDoc tags to document exceptions. --- src/Plugins/AbstractEddPlugin.php | 7 ++++--- src/Plugins/AcfExtendedPro.php | 9 +++++++++ src/Plugins/GravityForms.php | 8 ++++++++ src/Plugins/NinjaForms.php | 12 +++++++++++- src/Plugins/PolylangPro.php | 9 +++++++++ src/Plugins/PublishPressPro.php | 12 +++++++++++- src/Plugins/WpAiPro.php | 9 +++++++++ src/Plugins/Wpml.php | 5 +++-- 8 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/Plugins/AbstractEddPlugin.php b/src/Plugins/AbstractEddPlugin.php index cacf139..bb87023 100644 --- a/src/Plugins/AbstractEddPlugin.php +++ b/src/Plugins/AbstractEddPlugin.php @@ -19,26 +19,27 @@ abstract class AbstractEddPlugin extends AbstractPlugin { * Get the download URL for this plugin. * * @param array $response The EDD API response. + * @throws UnexpectedValueException If the response is invalid or versions do not match. * @return string */ protected function extractDownloadUrl( array $response ) { if ( empty( $response['download_link'] ) || ! is_string( $response['download_link'] ) ) { throw new UnexpectedValueException( sprintf( - 'Expected a valid download URL for package %s', + 'Expected a valid download URL from API for package %s', 'junaidbhura/' . $this->slug ) ); } if ( empty( $response['new_version'] ) || ! is_scalar( $response['new_version'] ) ) { throw new UnexpectedValueException( sprintf( - 'Expected a valid download version number for package %s', + 'Expected a valid download version number from API for package %s', 'junaidbhura/' . $this->slug ) ); } if ( ! Semver::satisfies( $response['new_version'], $this->version ) ) { throw new UnexpectedValueException( sprintf( - 'Expected download version (%s) to match installed version (%s) of package %s', + 'Expected download version from API (%s) to match installed version (%s) of package %s', $response['new_version'], $this->version, 'junaidbhura/' . $this->slug diff --git a/src/Plugins/AcfExtendedPro.php b/src/Plugins/AcfExtendedPro.php index ba7db8c..b95f1f4 100644 --- a/src/Plugins/AcfExtendedPro.php +++ b/src/Plugins/AcfExtendedPro.php @@ -8,6 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; +use UnexpectedValueException; /** * AcfExtendedPro class. @@ -17,6 +18,7 @@ class AcfExtendedPro extends AbstractEddPlugin { /** * Get the download URL for this plugin. * + * @throws UnexpectedValueException If the response is invalid. * @return string */ public function getDownloadUrl() { @@ -29,6 +31,13 @@ public function getDownloadUrl() { 'version' => $this->version, ) ), true ); + if ( ! is_array( $response ) ) { + throw new UnexpectedValueException( sprintf( + 'Expected a JSON object from API for package %s', + 'junaidbhura/' . $this->slug + ) ); + } + return $this->extractDownloadUrl( $response ); } diff --git a/src/Plugins/GravityForms.php b/src/Plugins/GravityForms.php index 6c7a271..04a5228 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -28,6 +28,7 @@ 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() { @@ -38,6 +39,13 @@ public function getDownloadUrl() { 'key' => getenv( 'GRAVITY_FORMS_KEY' ), ) ) ); + if ( ! is_array( $response ) ) { + throw new UnexpectedValueException( sprintf( + 'Expected a serialized object from API for package %s', + 'junaidbhura/' . $this->slug + ) ); + } + if ( empty( $response['download_url_latest'] ) || ! is_string( $response['download_url_latest'] ) ) { throw new UnexpectedValueException( sprintf( 'Expected a valid download URL for package %s', diff --git a/src/Plugins/NinjaForms.php b/src/Plugins/NinjaForms.php index 9dca8bc..666d649 100644 --- a/src/Plugins/NinjaForms.php +++ b/src/Plugins/NinjaForms.php @@ -8,6 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; +use InvalidArgumentException; use UnexpectedValueException; /** @@ -18,6 +19,8 @@ class NinjaForms extends AbstractEddPlugin { /** * Get the download URL for this plugin. * + * @throws InvalidArgumentException If the package is unsupported. + * @throws UnexpectedValueException If the response is invalid. * @return string */ public function getDownloadUrl() { @@ -296,7 +299,7 @@ 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 ) ); @@ -319,6 +322,13 @@ public function getDownloadUrl() { 'version' => $this->version, ) ), true ); + if ( ! is_array( $response ) ) { + throw new UnexpectedValueException( sprintf( + 'Expected a JSON object from API for package %s', + 'junaidbhura/' . $this->slug + ) ); + } + return $this->extractDownloadUrl( $response ); } diff --git a/src/Plugins/PolylangPro.php b/src/Plugins/PolylangPro.php index c035da6..7797820 100644 --- a/src/Plugins/PolylangPro.php +++ b/src/Plugins/PolylangPro.php @@ -8,6 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; +use UnexpectedValueException; /** * PolylangPro class. @@ -17,6 +18,7 @@ class PolylangPro extends AbstractEddPlugin { /** * Get the download URL for this plugin. * + * @throws UnexpectedValueException If the response is invalid. * @return string */ public function getDownloadUrl() { @@ -29,6 +31,13 @@ public function getDownloadUrl() { 'version' => $this->version, ) ), true ); + if ( ! is_array( $response ) ) { + throw new UnexpectedValueException( sprintf( + 'Expected a JSON object from API for package %s', + 'junaidbhura/' . $this->slug + ) ); + } + return $this->extractDownloadUrl( $response ); } diff --git a/src/Plugins/PublishPressPro.php b/src/Plugins/PublishPressPro.php index 81bd86c..976d89f 100644 --- a/src/Plugins/PublishPressPro.php +++ b/src/Plugins/PublishPressPro.php @@ -8,6 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; +use InvalidArgumentException; use UnexpectedValueException; /** @@ -28,6 +29,8 @@ public function __construct( $version = '', $slug = 'publishpress-planner-pro' ) /** * Get the download URL for this plugin. * + * @throws InvalidArgumentException If the package is unsupported. + * @throws UnexpectedValueException If the response is invalid. * @return string */ public function getDownloadUrl() { @@ -89,7 +92,7 @@ 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 ) ); @@ -112,6 +115,13 @@ public function getDownloadUrl() { 'version' => $this->version, ) ), true ); + if ( ! is_array( $response ) ) { + throw new UnexpectedValueException( sprintf( + 'Expected a JSON object from API for package %s', + 'junaidbhura/' . $this->slug + ) ); + } + return $this->extractDownloadUrl( $response ); } diff --git a/src/Plugins/WpAiPro.php b/src/Plugins/WpAiPro.php index 6041f56..065275d 100644 --- a/src/Plugins/WpAiPro.php +++ b/src/Plugins/WpAiPro.php @@ -8,6 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; +use UnexpectedValueException; /** * WpAiPro class. @@ -27,6 +28,7 @@ public function __construct( $version = '', $slug = 'wp-all-import-pro' ) { /** * Get the download URL for this plugin. * + * @throws UnexpectedValueException If the response is invalid. * @return string */ public function getDownloadUrl() { @@ -84,6 +86,13 @@ public function getDownloadUrl() { 'version' => $this->version, ) ), true ); + if ( ! is_array( $response ) ) { + throw new UnexpectedValueException( sprintf( + 'Expected a JSON object from API for package %s', + 'junaidbhura/' . $this->slug + ) ); + } + return $this->extractDownloadUrl( $response ); } diff --git a/src/Plugins/Wpml.php b/src/Plugins/Wpml.php index 05d815c..78fdc64 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,7 +52,7 @@ 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 ) ); From 0b1c8af2e29945d37c3774c0b693d59785aa2d42 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Wed, 15 Mar 2023 18:18:19 -0400 Subject: [PATCH 04/15] Clean-up `Http` class Changed: - Use booleans intead of integers for cURL options. - Compile URL in GET request earlier to organize cURL options similarly to POST request. --- src/Http.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Http.php b/src/Http.php index e78d71e..feff084 100644 --- a/src/Http.php +++ b/src/Http.php @@ -22,14 +22,15 @@ class Http { 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_RETURNTRANSFER, true ); 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 ); @@ -44,15 +45,15 @@ public function post( $url = '', $args = array() ) { * @return mixed */ public function get( $url = '', $args = array() ) { - $query_string = ''; + 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 ); + 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 ); + $response = curl_exec( $curl_handle ); curl_close( $curl_handle ); From fb0c1ab6e8226a4ffb7346457f7d81f37ee62218 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Wed, 15 Mar 2023 14:01:36 -0400 Subject: [PATCH 05/15] Improve `Http` class request and response handling Changed: - Added protected method `request()` to aggregate the execution of the request, handling of the response, and closing of the cURL handler. - Added cURL option `CURLOPT_FAILONERROR` to fail if the response is >= 400. - Throw `RuntimeException ` if the request failed. - Fixed and improved PHPDoc tags. --- src/Http.php | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Http.php b/src/Http.php index feff084..a9f6578 100644 --- a/src/Http.php +++ b/src/Http.php @@ -7,6 +7,8 @@ namespace Junaidbhura\Composer\WPProPlugins; +use RuntimeException; + /** * Http class. */ @@ -15,15 +17,16 @@ 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() ) { $curl_handle = curl_init(); curl_setopt( $curl_handle, CURLOPT_URL, $url ); curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true ); + curl_setopt( $curl_handle, CURLOPT_FAILONERROR, true ); curl_setopt( $curl_handle, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt( $curl_handle, CURLOPT_SSL_VERIFYHOST, false ); curl_setopt( $curl_handle, CURLOPT_CUSTOMREQUEST, 'POST' ); @@ -31,18 +34,15 @@ public function post( $url = '', $args = array() ) { 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() ) { if ( ! empty( $args ) ) { @@ -53,10 +53,30 @@ public function get( $url = '', $args = array() ) { curl_setopt( $curl_handle, CURLOPT_URL, $url ); curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true ); + curl_setopt( $curl_handle, CURLOPT_FAILONERROR, true ); + 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 + */ + protected function request( $curl_handle ) { $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; } From cee1cf5833d77317b946d37471c1552742946e4a Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Wed, 15 Mar 2023 20:24:30 -0400 Subject: [PATCH 06/15] Refactor plugin abstraction and Http usage Changed: - Replaced `getDownloadUrl()` in EDD plugins with a new protected method `getDownloadUrlFromApi()` that only handles the request for the download URL response object. - Replaced `extractDownloadUrl()` with `getDownloadUrl()` in `AbstractEddPlugin` that retrieves the response to parse from the plugin's `getDownloadUrlFromApi()`. - Wrapped requests with `Http` in a try/catch to intercept cURL exceptions and wrap them in a plugin-aware exception. --- src/Plugins/AbstractEddPlugin.php | 34 ++++++++++++++++++++++++++++--- src/Plugins/AcfExtendedPro.php | 22 ++++++-------------- src/Plugins/GravityForms.php | 26 +++++++++++++++++------ src/Plugins/NinjaForms.php | 22 ++++++-------------- src/Plugins/PolylangPro.php | 22 ++++++-------------- src/Plugins/PublishPressPro.php | 22 ++++++-------------- src/Plugins/WpAiPro.php | 22 ++++++-------------- 7 files changed, 81 insertions(+), 89 deletions(-) diff --git a/src/Plugins/AbstractEddPlugin.php b/src/Plugins/AbstractEddPlugin.php index bb87023..f8a0581 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 RuntimeException; use UnexpectedValueException; /** @@ -15,14 +16,41 @@ */ 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 or versions do not match. + * @throws UnexpectedValueException If the response is invalid. * @return string */ - protected function extractDownloadUrl( array $response ) { + public function getDownloadUrl() { + try { + $response = json_decode( $this->getDownloadUrlFromApi(), true ); + } catch ( RuntimeException $e ) { + $details = $e->getMessage(); + if ( $details ) { + $details = PHP_EOL . $details; + } + + throw new UnexpectedValueException( sprintf( + 'Could not query API for package %s. Please try again later.' . $details, + 'junaidbhura/' . $this->slug + ) ); + } + + if ( ! is_array( $response ) ) { + throw new UnexpectedValueException( sprintf( + 'Expected a JSON object from API for package %s', + 'junaidbhura/' . $this->slug + ) ); + } + if ( empty( $response['download_link'] ) || ! is_string( $response['download_link'] ) ) { throw new UnexpectedValueException( sprintf( 'Expected a valid download URL from API for package %s', diff --git a/src/Plugins/AcfExtendedPro.php b/src/Plugins/AcfExtendedPro.php index b95f1f4..ae3f1a6 100644 --- a/src/Plugins/AcfExtendedPro.php +++ b/src/Plugins/AcfExtendedPro.php @@ -8,7 +8,6 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; -use UnexpectedValueException; /** * AcfExtendedPro class. @@ -16,29 +15,20 @@ class AcfExtendedPro extends AbstractEddPlugin { /** - * Get the download URL for this plugin. + * Get the download URL for this plugin from its API. * - * @throws UnexpectedValueException If the response is invalid. * @return string */ - public function getDownloadUrl() { - $http = new Http(); - $response = json_decode( $http->get( 'https://acf-extended.com', array( + protected function getDownloadUrlFromApi() { + $http = new Http(); + + return $http->post( 'https://acf-extended.com', 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 ( ! is_array( $response ) ) { - throw new UnexpectedValueException( sprintf( - 'Expected a JSON object from API for package %s', - 'junaidbhura/' . $this->slug - ) ); - } - - return $this->extractDownloadUrl( $response ); + ) ); } } diff --git a/src/Plugins/GravityForms.php b/src/Plugins/GravityForms.php index 04a5228..35731d2 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -8,6 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; +use RuntimeException; use UnexpectedValueException; /** @@ -32,12 +33,25 @@ public function __construct( $version = '', $slug = 'gravityforms' ) { * @return string */ public function getDownloadUrl() { - $http = new Http(); - $response = unserialize( $http->get( 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php', array( - 'op' => 'get_plugin', - 'slug' => $this->slug, - 'key' => getenv( 'GRAVITY_FORMS_KEY' ), - ) ) ); + $http = new Http(); + + try { + $response = unserialize( $http->get( 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php', array( + 'op' => 'get_plugin', + 'slug' => $this->slug, + 'key' => getenv( 'GRAVITY_FORMS_KEY' ), + ) ) ); + } catch ( RuntimeException $e ) { + $details = $e->getMessage(); + if ( $details ) { + $details = PHP_EOL . $details; + } + + throw new UnexpectedValueException( sprintf( + 'Could not query API for package %s. Please try again later.' . $details, + 'junaidbhura/' . $this->slug + ) ); + } if ( ! is_array( $response ) ) { throw new UnexpectedValueException( sprintf( diff --git a/src/Plugins/NinjaForms.php b/src/Plugins/NinjaForms.php index 666d649..949650c 100644 --- a/src/Plugins/NinjaForms.php +++ b/src/Plugins/NinjaForms.php @@ -9,7 +9,6 @@ use Junaidbhura\Composer\WPProPlugins\Http; use InvalidArgumentException; -use UnexpectedValueException; /** * NinjaForms class. @@ -17,13 +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. - * @throws UnexpectedValueException If the response is invalid. * @return string */ - public function getDownloadUrl() { + protected function getDownloadUrlFromApi() { $name = ''; $env = null; /** @@ -313,23 +311,15 @@ 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(); + + return $http->get( 'https://ninjaforms.com', array( 'edd_action' => 'get_version', 'license' => $license, 'item_name' => $name, 'url' => $url, 'version' => $this->version, - ) ), true ); - - if ( ! is_array( $response ) ) { - throw new UnexpectedValueException( sprintf( - 'Expected a JSON object from API for package %s', - 'junaidbhura/' . $this->slug - ) ); - } - - return $this->extractDownloadUrl( $response ); + ) ); } } diff --git a/src/Plugins/PolylangPro.php b/src/Plugins/PolylangPro.php index 7797820..14b64ac 100644 --- a/src/Plugins/PolylangPro.php +++ b/src/Plugins/PolylangPro.php @@ -8,7 +8,6 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; -use UnexpectedValueException; /** * PolylangPro class. @@ -16,29 +15,20 @@ class PolylangPro extends AbstractEddPlugin { /** - * Get the download URL for this plugin. + * Get the download URL for this plugin from its API. * - * @throws UnexpectedValueException If the response is invalid. * @return string */ - public function getDownloadUrl() { - $http = new Http(); - $response = json_decode( $http->get( 'https://polylang.pro', array( + protected function getDownloadUrlFromApi() { + $http = new Http(); + + return $http->get( 'https://polylang.pro', array( 'edd_action' => 'get_version', 'license' => getenv( 'POLYLANG_PRO_KEY' ), 'item_name' => 'Polylang Pro', 'url' => getenv( 'POLYLANG_PRO_URL' ), 'version' => $this->version, - ) ), true ); - - if ( ! is_array( $response ) ) { - throw new UnexpectedValueException( sprintf( - 'Expected a JSON object from API for package %s', - 'junaidbhura/' . $this->slug - ) ); - } - - return $this->extractDownloadUrl( $response ); + ) ); } } diff --git a/src/Plugins/PublishPressPro.php b/src/Plugins/PublishPressPro.php index 976d89f..be4660b 100644 --- a/src/Plugins/PublishPressPro.php +++ b/src/Plugins/PublishPressPro.php @@ -9,7 +9,6 @@ use Junaidbhura\Composer\WPProPlugins\Http; use InvalidArgumentException; -use UnexpectedValueException; /** * PublishPressPro class. @@ -27,13 +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. - * @throws UnexpectedValueException If the response is invalid. * @return string */ - public function getDownloadUrl() { + protected function getDownloadUrlFromApi() { $id = 0; $env = null; /** @@ -106,23 +104,15 @@ 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(); + + return $http->get( 'https://publishpress.com', array( 'edd_action' => 'get_version', 'license' => $license, 'item_id' => $id, 'url' => $url, 'version' => $this->version, - ) ), true ); - - if ( ! is_array( $response ) ) { - throw new UnexpectedValueException( sprintf( - 'Expected a JSON object from API for package %s', - 'junaidbhura/' . $this->slug - ) ); - } - - return $this->extractDownloadUrl( $response ); + ) ); } } diff --git a/src/Plugins/WpAiPro.php b/src/Plugins/WpAiPro.php index 065275d..38692d3 100644 --- a/src/Plugins/WpAiPro.php +++ b/src/Plugins/WpAiPro.php @@ -8,7 +8,6 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Junaidbhura\Composer\WPProPlugins\Http; -use UnexpectedValueException; /** * WpAiPro class. @@ -26,12 +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. * - * @throws UnexpectedValueException If the response is invalid. * @return string */ - public function getDownloadUrl() { + protected function getDownloadUrlFromApi() { $url = ''; $name = ''; $license = ''; @@ -77,23 +75,15 @@ public function getDownloadUrl() { } } - $http = new Http(); - $response = json_decode( $http->get( 'https://www.wpallimport.com', array( + $http = new Http(); + + return $http->get( 'https://www.wpallimport.com', array( 'edd_action' => 'get_version', 'license' => $license, 'item_name' => $name, 'url' => $url, 'version' => $this->version, - ) ), true ); - - if ( ! is_array( $response ) ) { - throw new UnexpectedValueException( sprintf( - 'Expected a JSON object from API for package %s', - 'junaidbhura/' . $this->slug - ) ); - } - - return $this->extractDownloadUrl( $response ); + ) ); } } From 24eb2391d0269cca0772bda4ef2451d829aac79d Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 11:17:42 -0400 Subject: [PATCH 07/15] Remove default value for `$url` arguments in `Http` class A URL is necessary for requests. --- src/Http.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http.php b/src/Http.php index a9f6578..d5230a6 100644 --- a/src/Http.php +++ b/src/Http.php @@ -21,7 +21,7 @@ class Http { * @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, true ); @@ -44,7 +44,7 @@ public function post( $url = '', $args = array() ) { * @param array $args Arguments to add to request. * @return string */ - public function get( $url = '', $args = array() ) { + public function get( $url, $args = array() ) { if ( ! empty( $args ) ) { $url .= '?' . http_build_query( $args, '', '&' ); } From 1e8f5281b703735d9ce783a80de4873f3b093fb4 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 11:22:35 -0400 Subject: [PATCH 08/15] Update block comments in `AbstractPlugin` Changed: - Removed extraneous property descriptions. - Rephrased description of `$slug` property to be more specific. - Added missing `$slug` method parameter. --- src/Plugins/AbstractPlugin.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Plugins/AbstractPlugin.php b/src/Plugins/AbstractPlugin.php index 0bab616..e10819f 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; From 378ade7833b7240f0eb0a694f5c8a9fb5ed8bde7 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 11:42:25 -0400 Subject: [PATCH 09/15] Fix HTTP request method for ACF-Extended Amends cee1cf5833d77317b946d37471c1552742946e4a Accidentally reverted back from GET to POST. --- src/Plugins/AcfExtendedPro.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/AcfExtendedPro.php b/src/Plugins/AcfExtendedPro.php index ae3f1a6..738e9b8 100644 --- a/src/Plugins/AcfExtendedPro.php +++ b/src/Plugins/AcfExtendedPro.php @@ -22,7 +22,7 @@ class AcfExtendedPro extends AbstractEddPlugin { protected function getDownloadUrlFromApi() { $http = new Http(); - return $http->post( 'https://acf-extended.com', array( + return $http->get( 'https://acf-extended.com', array( 'edd_action' => 'get_version', 'license' => getenv( 'ACFE_PRO_KEY' ), 'item_name' => 'ACF Extended Pro', From efdb917413b3480c380505f5fb920012baf04e1f Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 11:47:31 -0400 Subject: [PATCH 10/15] Decouple API URL and query parameters Decoupled API URL and API URL query parameters from its HTTP query building and URL concatenation, and HTTP request call. This allows the base URL and query parameters to be more readable from their final concatenated URL or HTTP request call. --- src/Plugins/AcfExtendedPro.php | 8 ++++++-- src/Plugins/AcfPro.php | 8 ++++++-- src/Plugins/GravityForms.php | 14 +++++++++----- src/Plugins/NinjaForms.php | 8 ++++++-- src/Plugins/PolylangPro.php | 8 ++++++-- src/Plugins/PublishPressPro.php | 8 ++++++-- src/Plugins/WpAiPro.php | 8 ++++++-- src/Plugins/Wpml.php | 8 ++++++-- 8 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/Plugins/AcfExtendedPro.php b/src/Plugins/AcfExtendedPro.php index 738e9b8..b69d92d 100644 --- a/src/Plugins/AcfExtendedPro.php +++ b/src/Plugins/AcfExtendedPro.php @@ -22,13 +22,17 @@ class AcfExtendedPro extends AbstractEddPlugin { protected function getDownloadUrlFromApi() { $http = new Http(); - return $http->get( 'https://acf-extended.com', array( + $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, - ) ); + ); + + $api_url = 'https://acf-extended.com'; + + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/AcfPro.php b/src/Plugins/AcfPro.php index 4ba084a..ec650cd 100644 --- a/src/Plugins/AcfPro.php +++ b/src/Plugins/AcfPro.php @@ -18,12 +18,16 @@ class AcfPro extends AbstractPlugin { * @return string */ public function getDownloadUrl() { - return 'https://connect.advancedcustomfields.com/index.php?' . http_build_query( array( + $api_query = array( 'p' => 'pro', 'a' => 'download', 'k' => getenv( 'ACF_PRO_KEY' ), '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 35731d2..be6cd1c 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -35,12 +35,16 @@ public function __construct( $version = '', $slug = 'gravityforms' ) { public function getDownloadUrl() { $http = new Http(); + $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 = unserialize( $http->get( 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php', array( - 'op' => 'get_plugin', - 'slug' => $this->slug, - 'key' => getenv( 'GRAVITY_FORMS_KEY' ), - ) ) ); + $response = unserialize( $http->get( $api_url, $api_query ) ); } catch ( RuntimeException $e ) { $details = $e->getMessage(); if ( $details ) { diff --git a/src/Plugins/NinjaForms.php b/src/Plugins/NinjaForms.php index 949650c..26c000f 100644 --- a/src/Plugins/NinjaForms.php +++ b/src/Plugins/NinjaForms.php @@ -313,13 +313,17 @@ protected function getDownloadUrlFromApi() { $http = new Http(); - return $http->get( 'https://ninjaforms.com', array( + $api_query = array( 'edd_action' => 'get_version', 'license' => $license, 'item_name' => $name, 'url' => $url, 'version' => $this->version, - ) ); + ); + + $api_url = 'https://ninjaforms.com'; + + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/PolylangPro.php b/src/Plugins/PolylangPro.php index 14b64ac..139d860 100644 --- a/src/Plugins/PolylangPro.php +++ b/src/Plugins/PolylangPro.php @@ -22,13 +22,17 @@ class PolylangPro extends AbstractEddPlugin { protected function getDownloadUrlFromApi() { $http = new Http(); - return $http->get( 'https://polylang.pro', array( + $api_query = array( 'edd_action' => 'get_version', 'license' => getenv( 'POLYLANG_PRO_KEY' ), 'item_name' => 'Polylang Pro', 'url' => getenv( 'POLYLANG_PRO_URL' ), 'version' => $this->version, - ) ); + ); + + $api_url = 'https://polylang.pro'; + + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/PublishPressPro.php b/src/Plugins/PublishPressPro.php index be4660b..72ed498 100644 --- a/src/Plugins/PublishPressPro.php +++ b/src/Plugins/PublishPressPro.php @@ -106,13 +106,17 @@ protected function getDownloadUrlFromApi() { $http = new Http(); - return $http->get( 'https://publishpress.com', array( + $api_query = array( 'edd_action' => 'get_version', 'license' => $license, 'item_id' => $id, 'url' => $url, 'version' => $this->version, - ) ); + ); + + $api_url = 'https://publishpress.com'; + + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/WpAiPro.php b/src/Plugins/WpAiPro.php index 38692d3..e30d00f 100644 --- a/src/Plugins/WpAiPro.php +++ b/src/Plugins/WpAiPro.php @@ -77,13 +77,17 @@ protected function getDownloadUrlFromApi() { $http = new Http(); - return $http->get( 'https://www.wpallimport.com', array( + $api_query = array( 'edd_action' => 'get_version', 'license' => $license, 'item_name' => $name, 'url' => $url, 'version' => $this->version, - ) ); + ); + + $api_url = 'https://www.wpallimport.com'; + + return $http->get( $api_url, $api_query ); } } diff --git a/src/Plugins/Wpml.php b/src/Plugins/Wpml.php index 78fdc64..d442027 100644 --- a/src/Plugins/Wpml.php +++ b/src/Plugins/Wpml.php @@ -58,12 +58,16 @@ public function getDownloadUrl() { ) ); } - return 'https://wpml.org/?' . http_build_query( array( + $api_query = array( 'download' => $packages[ $this->slug ], 'user_id' => getenv( 'WPML_USER_ID' ), 'subscription_key' => getenv( 'WPML_KEY' ), 'version' => $this->version, - ), '', '&' ); + ); + + $api_url = 'https://wpml.org/'; + + return $api_url . '?' . http_build_query( $api_query, '', '&' ); } } From e6675136fd01621e36b3af081d7baf73eac543f8 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 11:50:51 -0400 Subject: [PATCH 11/15] Decouple version from API URL query parameters and version check This ensures compatibility with the classes' `$version` property being potentially empty, presuming a request for the latest version. --- src/Plugins/AbstractEddPlugin.php | 3 ++- src/Plugins/AcfExtendedPro.php | 6 +++++- src/Plugins/AcfPro.php | 6 +++++- src/Plugins/NinjaForms.php | 6 +++++- src/Plugins/PolylangPro.php | 6 +++++- src/Plugins/PublishPressPro.php | 6 +++++- src/Plugins/WpAiPro.php | 6 +++++- src/Plugins/Wpml.php | 6 +++++- 8 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/Plugins/AbstractEddPlugin.php b/src/Plugins/AbstractEddPlugin.php index f8a0581..b9118a0 100644 --- a/src/Plugins/AbstractEddPlugin.php +++ b/src/Plugins/AbstractEddPlugin.php @@ -65,7 +65,8 @@ public function getDownloadUrl() { ) ); } - if ( ! Semver::satisfies( $response['new_version'], $this->version ) ) { + // If no version is specified, we are fetching the latest version. + if ( $this->version && ! Semver::satisfies( $response['new_version'], $this->version ) ) { throw new UnexpectedValueException( sprintf( 'Expected download version from API (%s) to match installed version (%s) of package %s', $response['new_version'], diff --git a/src/Plugins/AcfExtendedPro.php b/src/Plugins/AcfExtendedPro.php index b69d92d..25f819c 100644 --- a/src/Plugins/AcfExtendedPro.php +++ b/src/Plugins/AcfExtendedPro.php @@ -27,9 +27,13 @@ protected function getDownloadUrlFromApi() { 'license' => getenv( 'ACFE_PRO_KEY' ), 'item_name' => 'ACF Extended Pro', 'url' => getenv( 'ACFE_PRO_URL' ), - 'version' => $this->version, ); + // 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 $http->get( $api_url, $api_query ); diff --git a/src/Plugins/AcfPro.php b/src/Plugins/AcfPro.php index ec650cd..4b23bb7 100644 --- a/src/Plugins/AcfPro.php +++ b/src/Plugins/AcfPro.php @@ -22,9 +22,13 @@ public function getDownloadUrl() { 'p' => 'pro', 'a' => 'download', 'k' => getenv( 'ACF_PRO_KEY' ), - 't' => $this->version, ); + // 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/NinjaForms.php b/src/Plugins/NinjaForms.php index 26c000f..1a8c590 100644 --- a/src/Plugins/NinjaForms.php +++ b/src/Plugins/NinjaForms.php @@ -318,9 +318,13 @@ protected function getDownloadUrlFromApi() { 'license' => $license, 'item_name' => $name, 'url' => $url, - 'version' => $this->version, ); + // 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 $http->get( $api_url, $api_query ); diff --git a/src/Plugins/PolylangPro.php b/src/Plugins/PolylangPro.php index 139d860..f6bff57 100644 --- a/src/Plugins/PolylangPro.php +++ b/src/Plugins/PolylangPro.php @@ -27,9 +27,13 @@ protected function getDownloadUrlFromApi() { 'license' => getenv( 'POLYLANG_PRO_KEY' ), 'item_name' => 'Polylang Pro', 'url' => getenv( 'POLYLANG_PRO_URL' ), - 'version' => $this->version, ); + // 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 $http->get( $api_url, $api_query ); diff --git a/src/Plugins/PublishPressPro.php b/src/Plugins/PublishPressPro.php index 72ed498..53fadb2 100644 --- a/src/Plugins/PublishPressPro.php +++ b/src/Plugins/PublishPressPro.php @@ -111,9 +111,13 @@ protected function getDownloadUrlFromApi() { 'license' => $license, 'item_id' => $id, 'url' => $url, - 'version' => $this->version, ); + // 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 $http->get( $api_url, $api_query ); diff --git a/src/Plugins/WpAiPro.php b/src/Plugins/WpAiPro.php index e30d00f..6ddbcde 100644 --- a/src/Plugins/WpAiPro.php +++ b/src/Plugins/WpAiPro.php @@ -82,9 +82,13 @@ protected function getDownloadUrlFromApi() { 'license' => $license, 'item_name' => $name, 'url' => $url, - 'version' => $this->version, ); + // 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 $http->get( $api_url, $api_query ); diff --git a/src/Plugins/Wpml.php b/src/Plugins/Wpml.php index d442027..6b24999 100644 --- a/src/Plugins/Wpml.php +++ b/src/Plugins/Wpml.php @@ -62,9 +62,13 @@ public function getDownloadUrl() { 'download' => $packages[ $this->slug ], 'user_id' => getenv( 'WPML_USER_ID' ), 'subscription_key' => getenv( 'WPML_KEY' ), - 'version' => $this->version, ); + // 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, '', '&' ); From f4ad7eccdd144f33a95cb674b5a19c24617e47c2 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 12:07:49 -0400 Subject: [PATCH 12/15] Add `getPackageName()` method to `AbstractPlugin` To reduce repetition, and risk of mistakes, when formatting the plugin's Composer package name. The package name is used in the messaging of most exceptions thrown. Storing vendor name as a namespace --- src/Plugins/AbstractEddPlugin.php | 10 +++++----- src/Plugins/AbstractPlugin.php | 9 +++++++++ src/Plugins/GravityForms.php | 6 +++--- src/Plugins/NinjaForms.php | 2 +- src/Plugins/PublishPressPro.php | 2 +- src/Plugins/Wpml.php | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Plugins/AbstractEddPlugin.php b/src/Plugins/AbstractEddPlugin.php index b9118a0..450ca00 100644 --- a/src/Plugins/AbstractEddPlugin.php +++ b/src/Plugins/AbstractEddPlugin.php @@ -40,28 +40,28 @@ public function getDownloadUrl() { throw new UnexpectedValueException( sprintf( 'Could not query API for package %s. Please try again later.' . $details, - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } if ( ! is_array( $response ) ) { throw new UnexpectedValueException( sprintf( 'Expected a JSON object from API for package %s', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } if ( empty( $response['download_link'] ) || ! is_string( $response['download_link'] ) ) { throw new UnexpectedValueException( sprintf( 'Expected a valid download URL from API for package %s', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } if ( empty( $response['new_version'] ) || ! is_scalar( $response['new_version'] ) ) { throw new UnexpectedValueException( sprintf( 'Expected a valid download version number from API for package %s', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } @@ -71,7 +71,7 @@ public function getDownloadUrl() { 'Expected download version from API (%s) to match installed version (%s) of package %s', $response['new_version'], $this->version, - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } diff --git a/src/Plugins/AbstractPlugin.php b/src/Plugins/AbstractPlugin.php index e10819f..b1524a6 100644 --- a/src/Plugins/AbstractPlugin.php +++ b/src/Plugins/AbstractPlugin.php @@ -44,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/GravityForms.php b/src/Plugins/GravityForms.php index be6cd1c..72f70bd 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -53,21 +53,21 @@ public function getDownloadUrl() { throw new UnexpectedValueException( sprintf( 'Could not query API for package %s. Please try again later.' . $details, - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } if ( ! is_array( $response ) ) { throw new UnexpectedValueException( sprintf( 'Expected a serialized object from API for package %s', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } if ( empty( $response['download_url_latest'] ) || ! is_string( $response['download_url_latest'] ) ) { throw new UnexpectedValueException( sprintf( 'Expected a valid download URL for package %s', - 'junaidbhura/' . $this->slug + $this->getPackageName() ) ); } diff --git a/src/Plugins/NinjaForms.php b/src/Plugins/NinjaForms.php index 1a8c590..4b95b74 100644 --- a/src/Plugins/NinjaForms.php +++ b/src/Plugins/NinjaForms.php @@ -299,7 +299,7 @@ protected function getDownloadUrlFromApi() { default: 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() ) ); } diff --git a/src/Plugins/PublishPressPro.php b/src/Plugins/PublishPressPro.php index 53fadb2..2252383 100644 --- a/src/Plugins/PublishPressPro.php +++ b/src/Plugins/PublishPressPro.php @@ -92,7 +92,7 @@ protected function getDownloadUrlFromApi() { default: 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() ) ); } diff --git a/src/Plugins/Wpml.php b/src/Plugins/Wpml.php index 6b24999..984f7d9 100644 --- a/src/Plugins/Wpml.php +++ b/src/Plugins/Wpml.php @@ -54,7 +54,7 @@ public function getDownloadUrl() { if ( ! array_key_exists( $this->slug, $packages ) ) { 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() ) ); } From fa45ae8ef3ae3eb886f20a1d52135c61bf955b25 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 14:32:57 -0400 Subject: [PATCH 13/15] Improve `Http` class Changed: - Moved shared cURL options to `request()` method. - Catch any exception from `Http` requests. --- src/Http.php | 13 ++++++------- src/Plugins/AbstractEddPlugin.php | 4 ++-- src/Plugins/GravityForms.php | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Http.php b/src/Http.php index d5230a6..93bbdfd 100644 --- a/src/Http.php +++ b/src/Http.php @@ -24,9 +24,6 @@ class Http { public function post( $url, $args = array() ) { $curl_handle = curl_init(); curl_setopt( $curl_handle, CURLOPT_URL, $url ); - curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, true ); - curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true ); - curl_setopt( $curl_handle, CURLOPT_FAILONERROR, true ); curl_setopt( $curl_handle, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt( $curl_handle, CURLOPT_SSL_VERIFYHOST, false ); curl_setopt( $curl_handle, CURLOPT_CUSTOMREQUEST, 'POST' ); @@ -51,9 +48,6 @@ public function get( $url, $args = array() ) { $curl_handle = curl_init(); curl_setopt( $curl_handle, CURLOPT_URL, $url ); - curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, true ); - curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true ); - curl_setopt( $curl_handle, CURLOPT_FAILONERROR, true ); return $this->request( $curl_handle ); } @@ -61,10 +55,15 @@ public function get( $url, $args = array() ) { /** * @param \CurlHandle|resource $curl_handle The cURL handler. * @throws RuntimeException If the request failed or the response is invalid. - * @return string + * @return string The response body. */ protected function request( $curl_handle ) { + curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true ); + 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 ); diff --git a/src/Plugins/AbstractEddPlugin.php b/src/Plugins/AbstractEddPlugin.php index 450ca00..7d41f3d 100644 --- a/src/Plugins/AbstractEddPlugin.php +++ b/src/Plugins/AbstractEddPlugin.php @@ -8,7 +8,7 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; use Composer\Semver\Semver; -use RuntimeException; +use Exception; use UnexpectedValueException; /** @@ -32,7 +32,7 @@ abstract protected function getDownloadUrlFromApi(); public function getDownloadUrl() { try { $response = json_decode( $this->getDownloadUrlFromApi(), true ); - } catch ( RuntimeException $e ) { + } catch ( Exception $e ) { $details = $e->getMessage(); if ( $details ) { $details = PHP_EOL . $details; diff --git a/src/Plugins/GravityForms.php b/src/Plugins/GravityForms.php index 72f70bd..62d1df6 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -7,8 +7,8 @@ namespace Junaidbhura\Composer\WPProPlugins\Plugins; +use Exception; use Junaidbhura\Composer\WPProPlugins\Http; -use RuntimeException; use UnexpectedValueException; /** @@ -45,7 +45,7 @@ public function getDownloadUrl() { try { $response = unserialize( $http->get( $api_url, $api_query ) ); - } catch ( RuntimeException $e ) { + } catch ( Exception $e ) { $details = $e->getMessage(); if ( $details ) { $details = PHP_EOL . $details; From 134926bbb205f530e36c2200297c191f99700ded Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 14:55:17 -0400 Subject: [PATCH 14/15] Improve error handling for EDD plugins Separated JSON decoding error handling from HTTP request/response error handling. By using `json_last_error()`, we can provide a more precise error for end users as opposed to just checking if its an array. Output a truncated excerpt of the response body if JSON decoding fails to give a hint of what the issue might be. --- src/Plugins/AbstractEddPlugin.php | 75 ++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/src/Plugins/AbstractEddPlugin.php b/src/Plugins/AbstractEddPlugin.php index 7d41f3d..d575b3c 100644 --- a/src/Plugins/AbstractEddPlugin.php +++ b/src/Plugins/AbstractEddPlugin.php @@ -31,34 +31,77 @@ abstract protected function getDownloadUrlFromApi(); */ public function getDownloadUrl() { try { - $response = json_decode( $this->getDownloadUrlFromApi(), true ); + $response = $this->getDownloadUrlFromApi(); } catch ( Exception $e ) { - $details = $e->getMessage(); - if ( $details ) { - $details = PHP_EOL . $details; + $details = []; + + $error = $e->getMessage(); + if ( $error ) { + $details[] = 'HTTP Error: ' . $error; } - throw new UnexpectedValueException( sprintf( - 'Could not query API for package %s. Please try again later.' . $details, + $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 ); } - if ( ! is_array( $response ) ) { - throw new UnexpectedValueException( sprintf( - 'Expected a JSON object from API for package %s', + 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( $response['download_link'] ) || ! is_string( $response['download_link'] ) ) { + if ( empty( $data['download_link'] ) || ! is_string( $data['download_link'] ) ) { throw new UnexpectedValueException( sprintf( '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 from API for package %s', $this->getPackageName() @@ -66,16 +109,16 @@ public function getDownloadUrl() { } // If no version is specified, we are fetching the latest version. - if ( $this->version && ! Semver::satisfies( $response['new_version'], $this->version ) ) { + if ( $this->version && ! Semver::satisfies( $data['new_version'], $this->version ) ) { throw new UnexpectedValueException( sprintf( 'Expected download version from API (%s) to match installed version (%s) of package %s', - $response['new_version'], + $data['new_version'], $this->version, $this->getPackageName() ) ); } - return $response['download_link']; + return $data['download_link']; } } From ab73a246d1a67445f68f033acc940f013a0be4bf Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Fri, 9 Jun 2023 14:58:22 -0400 Subject: [PATCH 15/15] Improve error handling for Gravity Forms Separated unserialization error handling from HTTP request/response error handling. Wrapped unserialization in try/catch to intercept any throwables from unexpected objects in their unserialization handlers. Gravity Forms is supposed to return an associative array but could be compromised. Output a truncated excerpt of the response body if unserialization fails to give a hint of what the issue might be. Ideally, add an error handler to catch the PHP notice that is raised by `unserialize()`. --- src/Plugins/GravityForms.php | 64 ++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/Plugins/GravityForms.php b/src/Plugins/GravityForms.php index 62d1df6..0a17039 100644 --- a/src/Plugins/GravityForms.php +++ b/src/Plugins/GravityForms.php @@ -44,34 +44,72 @@ public function getDownloadUrl() { $api_url = 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php'; try { - $response = unserialize( $http->get( $api_url, $api_query ) ); + $response = $http->get( $api_url, $api_query ); } catch ( Exception $e ) { - $details = $e->getMessage(); - if ( $details ) { - $details = PHP_EOL . $details; + $details = []; + + $error = $e->getMessage(); + if ( $error ) { + $details[] = 'HTTP Error: ' . $error; } - throw new UnexpectedValueException( sprintf( - 'Could not query API for package %s. Please try again later.' . $details, + $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 ); } - if ( ! is_array( $response ) ) { - throw new UnexpectedValueException( sprintf( - 'Expected a serialized object from API for package %s', + // 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( $response['download_url_latest'] ) || ! is_string( $response['download_url_latest'] ) ) { + if ( empty( $data['download_url_latest'] ) || ! is_string( $data['download_url_latest'] ) ) { throw new UnexpectedValueException( sprintf( 'Expected a valid download URL for package %s', $this->getPackageName() ) ); } - return str_replace( 'http://', 'https://', $response['download_url_latest'] ); + return str_replace( 'http://', 'https://', $data['download_url_latest'] ); } }