switch to Symfony HTTPClient (#2901)

This commit is contained in:
Andy Miller
2020-04-29 15:36:51 -06:00
committed by GitHub
parent daf7e66ec3
commit 24db65cd90
3 changed files with 145 additions and 349 deletions

View File

@@ -55,7 +55,8 @@
"phive/twig-extensions-deferred": "^1.0",
"willdurand/negotiation": "2.x-dev",
"itsgoingd/clockwork": "@beta",
"enshrined/svg-sanitize": "~0.1"
"enshrined/svg-sanitize": "~0.1",
"symfony/http-client": "^4.4"
},
"require-dev": {
"codeception/codeception": "^2.4",

84
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3deb19274879ec4b3566d70163f0e4f3",
"content-hash": "993f670b144e15558dd801ffcc7dfc81",
"packages": [
{
"name": "antoligy/dom-string-iterators",
@@ -2178,6 +2178,88 @@
"homepage": "https://symfony.com",
"time": "2020-03-27T16:54:36+00:00"
},
{
"name": "symfony/http-client",
"version": "v4.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "88d1745f4095727b8bf0574a0f414331f4ec229c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/88d1745f4095727b8bf0574a0f414331f4ec229c",
"reference": "88d1745f4095727b8bf0574a0f414331f4ec229c",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"psr/log": "^1.0",
"symfony/http-client-contracts": "^1.1.8|^2",
"symfony/polyfill-php73": "^1.11",
"symfony/service-contracts": "^1.0|^2"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "1.1"
},
"require-dev": {
"guzzlehttp/promises": "^1.3.1",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^4.3|^5.0",
"symfony/http-kernel": "^4.4",
"symfony/process": "^4.2|^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony HttpClient component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-04-12T16:14:02+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.15.0",

View File

@@ -11,81 +11,35 @@ namespace Grav\Common\GPM;
use Grav\Common\Utils;
use Grav\Common\Grav;
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\HttpClient\NativeHttpClient;
class Response
{
/** @var callable The callback for the progress, either a function or callback in array notation */
public static $callback = null;
/** @var string Which method to use for HTTP calls, can be 'curl', 'fopen' or 'auto'. Auto is default and fopen is the preferred method */
private static $method = 'auto';
/** @var array Default parameters for `curl` and `fopen` */
private static $defaults = [
'curl' => [
CURLOPT_REFERER => 'Grav GPM',
CURLOPT_USERAGENT => 'Grav GPM',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_FAILONERROR => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HEADER => false,
//CURLOPT_SSL_VERIFYPEER => true, // this is set in the constructor since it's a setting
/**
* Example of callback parameters from within your own class
*/
//CURLOPT_NOPROGRESS => false,
//CURLOPT_PROGRESSFUNCTION => [$this, 'progress']
],
'fopen' => [
'method' => 'GET',
'user_agent' => 'Grav GPM',
'max_redirects' => 5,
'follow_location' => 1,
'timeout' => 15,
/* // this is set in the constructor since it's a setting
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
*/
/**
* Example of callback parameters from within your own class
*/
//'notification' => [$this, 'progress']
]
private static $headers = [
'Referer' => 'Grav CMS',
'User-Agent' => 'Grav CMS'
];
/**
* Sets the preferred method to use for making HTTP calls.
*
* @param string $method Default is `auto`
* @return Response
*/
public static function setMethod($method = 'auto')
{
if (!\in_array($method, ['auto', 'curl', 'fopen'], true)) {
$method = 'auto';
}
self::$method = $method;
return new self();
}
/**
* Makes a request to the URL by using the preferred method
*
* @param string $uri URL to call
* @param array $options An array of parameters for both `curl` and `fopen`
* @param callable|null $callback Either a function or callback in array notation
* @param string $uri URL to call
* @param array $overrides An array of parameters for both `curl` and `fopen`
* @param callable|null $callback Either a function or callback in array notation
* @return string The response of the request
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
*/
public static function get($uri = '', $options = [], $callback = null)
public static function get($uri = '', $overrides = [], $callback = null)
{
if (!self::isCurlAvailable() && !self::isFopenAvailable()) {
throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available');
if (empty($uri)) {
throw new TransportException('missing URI');
}
// check if this function is available, if so use it to stop any timeouts
@@ -97,95 +51,50 @@ class Response
}
$config = Grav::instance()['config'];
$overrides = [];
$options = new HttpOptions();
// Override CA Bundle
$caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
if (is_dir($caPathOrFile) || (is_link($caPathOrFile) && is_dir(readlink($caPathOrFile)))) {
$overrides['curl'][CURLOPT_CAPATH] = $caPathOrFile;
$overrides['fopen']['ssl']['capath'] = $caPathOrFile;
} else {
$overrides['curl'][CURLOPT_CAINFO] = $caPathOrFile;
$overrides['fopen']['ssl']['cafile'] = $caPathOrFile;
// Set default Headers
$options->setHeaders(self::$headers);
// Disable verify Peer if required
$verify_peer = $config->get('system.gpm.verify_peer', true);
if ($verify_peer !== true) {
$options->verifyPeer($verify_peer);
}
// SSL Verify Peer and Proxy Setting
$settings = [
'method' => $config->get('system.gpm.method', self::$method),
'verify_peer' => $config->get('system.gpm.verify_peer', true),
// `system.proxy_url` is for fallback
// introduced with 1.1.0-beta.1 probably safe to remove at some point
'proxy_url' => $config->get('system.gpm.proxy_url', $config->get('system.proxy_url', false)),
];
if (!$settings['verify_peer']) {
$overrides = array_replace_recursive([], $overrides, [
'curl' => [
CURLOPT_SSL_VERIFYPEER => $settings['verify_peer'],
CURLOPT_SSL_VERIFYHOST => false
],
'fopen' => [
'ssl' => [
'verify_peer' => $settings['verify_peer'],
'verify_peer_name' => $settings['verify_peer'],
]
]
]);
// Set proxy url if provided
$proxy_url = $config->get('system.gpm.proxy_url', false);
if ($proxy_url) {
$options->setProxy($proxy_url);
}
// Proxy Setting
if ($settings['proxy_url']) {
$proxy = parse_url($settings['proxy_url']);
$fopen_proxy = ($proxy['scheme'] ?: 'http') . '://' . $proxy['host'] . (isset($proxy['port']) ? ':' . $proxy['port'] : '');
$overrides = array_replace_recursive([], $overrides, [
'curl' => [
CURLOPT_PROXY => $proxy['host'],
CURLOPT_PROXYTYPE => 'HTTP'
],
'fopen' => [
'proxy' => $fopen_proxy,
'request_fulluri' => true
]
]);
if (isset($proxy['port'])) {
$overrides['curl'][CURLOPT_PROXYPORT] = $proxy['port'];
}
if (isset($proxy['user'], $proxy['pass'])) {
$fopen_auth = $auth = base64_encode($proxy['user'] . ':' . $proxy['pass']);
$overrides['curl'][CURLOPT_PROXYUSERPWD] = $proxy['user'] . ':' . $proxy['pass'];
$overrides['fopen']['header'] = "Proxy-Authorization: Basic $fopen_auth";
}
// Use callback if provided
if ($callback) {
self::$callback = $callback;
$options->setOnProgress(['Grav\Common\GPM\Response', 'progress']);
}
$options = array_replace_recursive(self::$defaults, $options, $overrides);
$method = 'get' . ucfirst(strtolower($settings['method']));
$preferred_method = $config->get('system.gpm.method', 'auto');
self::$callback = $callback;
return static::$method($uri, $options, $callback);
$settings = array_merge_recursive($options->toArray(), $overrides);
switch ($preferred_method) {
case 'curl':
$client = new CurlHttpClient($settings);
break;
case 'fopen':
case 'native':
$client = new NativeHttpClient($settings);
break;
default:
$client = HttpClient::create($settings);
}
$response = $client->request('GET', $uri);
return $response->getContent();
}
/**
* Checks if cURL is available
*
* @return bool
*/
public static function isCurlAvailable()
{
return function_exists('curl_version');
}
/**
* Checks if the remote fopen request is enabled in PHP
*
* @return bool
*/
public static function isFopenAvailable()
{
return preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
}
/**
* Is this a remote file or not
@@ -202,219 +111,23 @@ class Response
* Progress normalized for cURL and Fopen
* Accepts a variable length of arguments passed in by stream method
*/
public static function progress()
public static function progress(int $bytes_transferred, int $filesize, array $info)
{
static $filesize = null;
$args = func_get_args();
$isCurlResource = is_resource($args[0]) && get_resource_type($args[0]) === 'curl';
$notification_code = !$isCurlResource ? $args[0] : false;
$bytes_transferred = $isCurlResource ? $args[2] : $args[4];
if ($isCurlResource) {
$filesize = $args[1];
} elseif ($notification_code == STREAM_NOTIFY_FILE_SIZE_IS) {
$filesize = $args[5];
}
if ($bytes_transferred > 0) {
if ($notification_code == STREAM_NOTIFY_PROGRESS | STREAM_NOTIFY_COMPLETED || $isCurlResource) {
$percent = $filesize <= 0 ? 0 : round(($bytes_transferred * 100) / $filesize, 1);
$progress = [
'code' => $notification_code,
'filesize' => $filesize,
'transferred' => $bytes_transferred,
'percent' => $percent < 100 ? $percent : 100
];
$percent = $filesize <= 0 ? 0 : intval(($bytes_transferred * 100) / $filesize);
if (self::$callback !== null) {
call_user_func(self::$callback, $progress);
}
$progress = [
'code' => $info['http_code'],
'filesize' => $filesize,
'transferred' => $bytes_transferred,
'percent' => $percent < 100 ? $percent : 100
];
if (self::$callback !== null) {
call_user_func(self::$callback, $progress);
}
}
}
/**
* Automatically picks the preferred method
*
* @return string|null The response of the request
*/
private static function getAuto()
{
if (!ini_get('open_basedir') && self::isFopenAvailable()) {
return self::getFopen(func_get_args());
}
if (self::isCurlAvailable()) {
return self::getCurl(func_get_args());
}
return null;
}
/**
* Starts a HTTP request via fopen
*
* @return string The response of the request
*/
private static function getFopen()
{
if (\count($args = func_get_args()) === 1) {
$args = $args[0];
}
$uri = $args[0];
$options = $args[1] ?? [];
$callback = $args[2] ?? null;
if ($callback) {
$options['fopen']['notification'] = ['self', 'progress'];
}
if (isset($options['fopen']['ssl'])) {
$ssl = $options['fopen']['ssl'];
unset($options['fopen']['ssl']);
$stream = stream_context_create([
'http' => $options['fopen'],
'ssl' => $ssl
], $options['fopen']);
} else {
$stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
}
$content = @file_get_contents($uri, false, $stream);
if ($content === false) {
$code = null;
// Function file_get_contents() creates local variable $http_response_header, check it
if (isset($http_response_header)) {
$code = explode(' ', $http_response_header[0] ?? '')[1] ?? null;
}
switch ($code) {
case '404':
throw new \RuntimeException('Page not found');
case '401':
throw new \RuntimeException('Invalid LICENSE');
default:
throw new \RuntimeException("Error while trying to download (code: {$code}): {$uri}\n");
}
}
return $content;
}
/**
* Starts a HTTP request via cURL
*
* @return string The response of the request
*/
private static function getCurl()
{
$args = func_get_args();
$args = count($args) > 1 ? $args : array_shift($args);
$uri = $args[0];
$options = $args[1] ?? [];
$callback = $args[2] ?? null;
$ch = curl_init($uri);
$response = static::curlExecFollow($ch, $options, $callback);
$errno = curl_errno($ch);
if ($errno) {
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error_message = curl_strerror($errno) . "\n" . curl_error($ch);
switch ($code) {
case '404':
throw new \RuntimeException('Page not found');
case '401':
throw new \RuntimeException('Invalid LICENSE');
default:
throw new \RuntimeException("Error while trying to download (code: $code): $uri \nMessage: $error_message");
}
}
curl_close($ch);
return $response;
}
/**
* @param resource $ch
* @param array $options
* @param bool $callback
* @return bool|mixed
*/
private static function curlExecFollow($ch, $options, $callback)
{
if ($callback) {
curl_setopt_array(
$ch,
[
CURLOPT_NOPROGRESS => false,
CURLOPT_PROGRESSFUNCTION => ['self', 'progress']
]
);
}
// no open_basedir set, we can proceed normally
if (!ini_get('open_basedir')) {
curl_setopt_array($ch, $options['curl']);
return curl_exec($ch);
}
$max_redirects = $options['curl'][CURLOPT_MAXREDIRS] ?? 5;
$options['curl'][CURLOPT_FOLLOWLOCATION] = false;
// open_basedir set but no redirects to follow, we can disable followlocation and proceed normally
curl_setopt_array($ch, $options['curl']);
if ($max_redirects <= 0) {
return curl_exec($ch);
}
$uri = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
$rch = curl_copy_handle($ch);
curl_setopt($rch, CURLOPT_HEADER, true);
curl_setopt($rch, CURLOPT_NOBODY, true);
curl_setopt($rch, CURLOPT_FORBID_REUSE, false);
curl_setopt($rch, CURLOPT_RETURNTRANSFER, true);
do {
curl_setopt($rch, CURLOPT_URL, $uri);
$header = curl_exec($rch);
if (curl_errno($rch)) {
$code = 0;
} else {
$code = (int)curl_getinfo($rch, CURLINFO_HTTP_CODE);
if ($code === 301 || $code === 302 || $code === 303) {
preg_match('/Location:(.*?)\n/', $header, $matches);
$uri = trim(array_pop($matches));
} else {
$code = 0;
}
}
} while ($code && --$max_redirects);
curl_close($rch);
if (!$max_redirects) {
if ($max_redirects === null) {
trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING);
}
return false;
}
curl_setopt($ch, CURLOPT_URL, $uri);
return curl_exec($ch);
}
}