Skip to content

Commit

Permalink
Check if HTTP response is OK
Browse files Browse the repository at this point in the history
Refactored `Http` class to store last response body and headers, allow parsing of body through callback, and finding of status code and message (based on Composer 2's `Response`, `RemoteFileSystem`, and `HttpDownloader`).

Changed `AbstractEddPlugin::extractDownloadUrl()` to check the HTTP response status code and response body and throw exceptions if not 200 or not an array, respectively.
  • Loading branch information
mcaskill committed Mar 8, 2023
1 parent f7ee853 commit 5b3a2d9
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 55 deletions.
171 changes: 156 additions & 15 deletions Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,138 @@
*/
class Http {

/**
* The body of the last response.
*
* @var mixed
*/
protected $body;

/**
* The headers of the last response.
*
* @var string[]
*/
protected $headers = [];

/**
* @param string[] $headers array of returned headers like from getHeaders()
* @param string $name header name (case insensitive)
* @return string|null
*/
public static function findHeaderValue( array $headers, $name ) {
$value = null;
foreach ( $headers as $header ) {
if ( preg_match( '{^' . preg_quote( $name ) . ':\s*(.+?)\s*$}i', $header, $match ) ) {
// In case of redirects, the response contains the headers
// of all responses so we can not return directly and need
// to keep iterating.
$value = $match[1];
}
}

return $value;
}

/**
* @param string[] $headers Array of headers.
* @return int|null
*/
public static function findStatusCode( array $headers ) {
$value = null;
foreach ( $headers as $header ) {
if ( preg_match( '{^HTTP/\S+ (\d+)}i', $header, $matches ) ) {
// In case of redirects, the response contains the headers
// of all responses so we can not return directly and need
// to keep iterating.
$value = (int) $matches[1];
}
}

return $value;
}

/**
* @param string[] $headers Array of headers.
* @return string|null
*/
public static function findStatusMessage( array $headers ) {
$value = null;
foreach ( $headers as $header ) {
if ( preg_match( '{^HTTP/\S+ \d+}i', $header ) ) {
// In case of redirects, the response contains the headers
// of all responses so we can not return directly and need
// to keep iterating.
$value = trim( $header );
}
}

return $value;
}

/**
* @return mixed
*/
public function getBody()
{
return $this->body;
}

/**
* Get an excerpt of the last response body.
*
* @param int $length
* @return string
*/
public function getExcerpt( $length = 200 )
{
$body = $this->getBody();
$body = is_scalar( $body ) ? var_export( $body, true ) : json_encode( $body );

return substr( $body, 0, $length ) . ( strlen( $body ) > $length ? '...' : '' );
}

/**
* @return int|null
*/
public function getStatusCode()
{
return self::findStatusCode( $this->getHeaders() );
}

/**
* @return string|null
*/
public function getStatusMessage()
{
return self::findStatusMessage( $this->getHeaders() );
}

/**
* @return string|null
*/
public function getHeader( $name )
{
return self::findHeaderValue( $this->getHeaders(), $name );
}

/**
* Returns the headers of the last response.
*
* @return string[]
*/
public function getHeaders() {
return $this->headers;
}

/**
* POST request.
*
* @param string $url Url to POST.
* @param string $url URL to POST.
* @param array $args Arguments to POST.
* @return mixed
* @return mixed The response body.
*/
public function post( $url = '', $args = array() ) {
public function post( $url = '', $args = array(), callable $parse_body = null ) {
$curl_handle = curl_init();
curl_setopt( $curl_handle, CURLOPT_URL, $url );
curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, 1 );
Expand All @@ -30,33 +154,50 @@ public function post( $url = '', $args = array() ) {
if ( ! empty( $args ) ) {
curl_setopt( $curl_handle, CURLOPT_POSTFIELDS, http_build_query( $args, '', '&' ) );
}
$response = curl_exec( $curl_handle );
$this->headers = [];
curl_setopt( $curl_handle, CURLOPT_HEADERFUNCTION, array( $this, 'curlHeaderCallback' ) );
$body = curl_exec( $curl_handle );
$this->body = $parse_body ? $parse_body( $body ) : $body;
curl_close( $curl_handle );

return $response;
return $this->body;
}

/**
* GET request.
*
* @param string $url Base URL for requeset (without params)
* @param array $args Arguments to add to request
* @return mixed
* @param string $url Base URL for requeset (without params).
* @param array $args Arguments to add to request.
* @return mixed The response body.
*/
public function get( $url = '', $args = array() ) {
$query_string = '';

public function get( $url = '', $args = array(), callable $parse_body = null ) {
$curl_handle = curl_init();
curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true );
$query_string = '';
if ( ! empty( $args ) ) {
$query_string = http_build_query( $args, '', '&' );
}
curl_setopt( $curl_handle, CURLOPT_URL, $url . '?' . $query_string );
$response = curl_exec( $curl_handle );
curl_setopt( $curl_handle, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $curl_handle, CURLOPT_FOLLOWLOCATION, true );
$this->headers = [];
curl_setopt( $curl_handle, CURLOPT_HEADERFUNCTION, array( $this, 'curlHeaderCallback' ) );
$body = curl_exec( $curl_handle );
$this->body = $parse_body ? $parse_body( $body ) : $body;
curl_close( $curl_handle );

return $response;
return $this->body;
}

/**
* Accumulates the request's response headers.
*
* @param \CurlHandle|resource $handle The cURL handler.
* @param string $header The header data to be written.
* @return int The number of bytes written.
*/
protected function curlHeaderCallback( $handle, $header ) {
$this->headers[] = $header;
return strlen( $header );
}

}
42 changes: 32 additions & 10 deletions plugins/AbstractEddPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,56 @@ abstract class AbstractEddPlugin extends AbstractPlugin {
/**
* Get the download URL for this plugin.
*
* @param array<string, mixed> $response The EDD API response.
* @param Http $response The HTTP API response.
* @throws UnexpectedValueException If the response is not OK, invalid, or malformed.
* @return string
*/
protected function extractDownloadUrl( array $response ) {
if ( empty( $response['download_link'] ) || ! is_string( $response['download_link'] ) ) {
protected function extractDownloadUrl( Http $response ) {
$status_code = $response->getStatusCode();
if ($status_code !== 200) {
$details = PHP_EOL . $response->getExcerpt();

throw new UnexpectedValueException( sprintf(
'Could not query API (HTTP %s) for package %s. Please try again later.' . $details,
$response->getStatusMessage(),
'junaidbhura/' . $this->slug
) );
}

$body = $response->getBody();
if ( ! is_array( $body ) ) {
$details = PHP_EOL . $response->getExcerpt();

throw new UnexpectedValueException( sprintf(
'API response is malformed for package %s.' . $details,
'junaidbhura/' . $this->slug
) );
}

if ( empty( $body['download_link'] ) || ! is_string( $body['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'] ) ) {
if ( empty( $body['new_version'] ) || ! is_scalar( $body['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 ) ) {
if ( ! Semver::satisfies( $body['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.',
$body['new_version'],
$this->version,
'junaidbhura/' . $this->slug
) );
}

return $response['download_link'];
return $body['download_link'];
}

}
10 changes: 6 additions & 4 deletions plugins/AcfExtendedPro.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ class AcfExtendedPro extends AbstractEddPlugin {
* @return string
*/
public function getDownloadUrl() {
$http = new Http();
$response = json_decode( $http->get( 'https://acf-extended.com', array(
$http = new Http();
$http->get( '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 );
), function ( $body ) {
return json_decode( $body, true );
} );

return $this->extractDownloadUrl( $response );
return $this->extractDownloadUrl( $http );
}

}
46 changes: 36 additions & 10 deletions plugins/GravityForms.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Junaidbhura\Composer\WPProPlugins\Plugins;

use Composer\Semver\Semver;
use Junaidbhura\Composer\WPProPlugins\Http;
use UnexpectedValueException;

Expand All @@ -31,12 +32,35 @@ 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(
$http = new Http();
$http->get( 'https://gravityapi.com/wp-content/plugins/gravitymanager/api.php', array(
'op' => 'get_plugin',
'slug' => $this->slug,
'key' => getenv( 'GRAVITY_FORMS_KEY' ),
) ) );
), function ( $body ) {
return unserialize( $body );
} );

$status_code = $http->getStatusCode();
if ($status_code !== 200) {
$details = PHP_EOL . $http->getExcerpt();

throw new UnexpectedValueException( sprintf(
'Could not query API (HTTP %s) for package %s. Please try again later.' . $details,
$http->getStatusMessage(),
'junaidbhura/' . $this->slug
) );
}

$body = $response->getBody();
if ( ! is_array( $body ) ) {
$details = PHP_EOL . $response->getExcerpt();

throw new UnexpectedValueException( sprintf(
'API response is malformed for package %s.' . $details,
'junaidbhura/' . $this->slug
) );
}

$candidates = array(
array( 'version', 'download_url' ),
Expand All @@ -46,7 +70,7 @@ public function getDownloadUrl() {
list( $version_key, $download_key ) = $candidate;

try {
return $this->extractDownloadUrl( $response, $version_key, $download_key );
return $this->extractDownloadUrl( $http, $version_key, $download_key );
} catch ( UnexpectedValueException $e ) {
// throw it after foreach
}
Expand All @@ -56,15 +80,17 @@ public function getDownloadUrl() {
}

/**
* @param array<string, mixed> $response The EDD API response.
* @param string $version_key The API field key that holds the version.
* @param string $download_key The API field key that holds the download URL.
* @param Http $response The HTTP API response.
* @param string $version_key The API field key that holds the version.
* @param string $download_key The API field key that holds the download URL.
* @throws UnexpectedValueException If the response is not OK, invalid, or malformed.
* @return string
*/
protected function extractDownloadUrl( array $response, $version_key, $download_key ) {
$version = array_key_exists( $version_key, $response ) ? $response[ $version_key ] : null;
$download_url = array_key_exists( $download_key, $response ) ? $response[ $download_key ] : null;
protected function extractDownloadUrl( Http $response, $version_key, $download_key ) {
$body = unserialize( $response->getBody() );

$version = array_key_exists( $version_key, $body ) ? $body[ $version_key ] : null;
$download_url = array_key_exists( $download_key, $body ) ? $body[ $download_key ] : null;

if ( false === $version ) {
// If FALSE, the package does not exist / is not available
Expand Down
10 changes: 6 additions & 4 deletions plugins/NinjaForms.php
Original file line number Diff line number Diff line change
Expand Up @@ -310,16 +310,18 @@ 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();
$http->get( 'https://ninjaforms.com', array(
'edd_action' => 'get_version',
'license' => $license,
'item_name' => $name,
'url' => $url,
'version' => $this->version,
) ), true );
), function ( $body ) {
return json_decode( $body, true );
} );

return $this->extractDownloadUrl( $response );
return $this->extractDownloadUrl( $http );
}

}
Loading

0 comments on commit 5b3a2d9

Please sign in to comment.