From 24db65cd90f5e2872e2e84dce48623506799e787 Mon Sep 17 00:00:00 2001 From: Andy Miller <1084697+rhukster@users.noreply.github.com> Date: Wed, 29 Apr 2020 15:36:51 -0600 Subject: [PATCH] switch to Symfony HTTPClient (#2901) --- composer.json | 3 +- composer.lock | 84 ++++- system/src/Grav/Common/GPM/Response.php | 407 ++++-------------------- 3 files changed, 145 insertions(+), 349 deletions(-) diff --git a/composer.json b/composer.json index 468f881d5..2efa2cde1 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 4b6e4724f..a27ee9a00 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/system/src/Grav/Common/GPM/Response.php b/system/src/Grav/Common/GPM/Response.php index 77aa91822..b43496f34 100644 --- a/system/src/Grav/Common/GPM/Response.php +++ b/system/src/Grav/Common/GPM/Response.php @@ -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); - } }