Update website
This commit is contained in:
parent
4413528994
commit
1d90fbf296
6865 changed files with 1091082 additions and 0 deletions
181
vendor/symfony/http-client/AmpHttpClient.php
vendored
Normal file
181
vendor/symfony/http-client/AmpHttpClient.php
vendored
Normal file
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Amp\CancelledException;
|
||||
use Amp\Http\Client\DelegateHttpClient;
|
||||
use Amp\Http\Client\InterceptedHttpClient;
|
||||
use Amp\Http\Client\PooledHttpClient;
|
||||
use Amp\Http\Client\Request;
|
||||
use Amp\Http\Tunnel\Http1TunnelConnector;
|
||||
use Amp\Promise;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\Internal\AmpClientState;
|
||||
use Symfony\Component\HttpClient\Response\AmpResponse;
|
||||
use Symfony\Component\HttpClient\Response\ResponseStream;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
if (!interface_exists(DelegateHttpClient::class)) {
|
||||
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^4.2.1".');
|
||||
}
|
||||
|
||||
if (!interface_exists(Promise::class)) {
|
||||
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the installed "amphp/http-client" is not compatible with this version of "symfony/http-client". Try downgrading "amphp/http-client" to "^4.2.1".');
|
||||
}
|
||||
|
||||
/**
|
||||
* A portable implementation of the HttpClientInterface contracts based on Amp's HTTP client.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
|
||||
{
|
||||
use HttpClientTrait;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private $defaultOptions = self::OPTIONS_DEFAULTS;
|
||||
private static $emptyDefaults = self::OPTIONS_DEFAULTS;
|
||||
|
||||
/** @var AmpClientState */
|
||||
private $multi;
|
||||
|
||||
/**
|
||||
* @param array $defaultOptions Default requests' options
|
||||
* @param callable|null $clientConfigurator A callable that builds a {@see DelegateHttpClient} from a {@see PooledHttpClient};
|
||||
* passing null builds an {@see InterceptedHttpClient} with 2 retries on failures
|
||||
* @param int $maxHostConnections The maximum number of connections to a single host
|
||||
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
|
||||
*
|
||||
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
|
||||
*/
|
||||
public function __construct(array $defaultOptions = [], ?callable $clientConfigurator = null, int $maxHostConnections = 6, int $maxPendingPushes = 50)
|
||||
{
|
||||
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
|
||||
|
||||
if ($defaultOptions) {
|
||||
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
|
||||
}
|
||||
|
||||
$this->multi = new AmpClientState($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
|
||||
|
||||
$options['proxy'] = self::getProxy($options['proxy'], $url, $options['no_proxy']);
|
||||
|
||||
if (null !== $options['proxy'] && !class_exists(Http1TunnelConnector::class)) {
|
||||
throw new \LogicException('You cannot use the "proxy" option as the "amphp/http-tunnel" package is not installed. Try running "composer require amphp/http-tunnel".');
|
||||
}
|
||||
|
||||
if ($options['bindto']) {
|
||||
if (0 === strpos($options['bindto'], 'if!')) {
|
||||
throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
|
||||
}
|
||||
if (0 === strpos($options['bindto'], 'host!')) {
|
||||
$options['bindto'] = substr($options['bindto'], 5);
|
||||
}
|
||||
}
|
||||
|
||||
if (('' !== $options['body'] || 'POST' === $method || isset($options['normalized_headers']['content-length'])) && !isset($options['normalized_headers']['content-type'])) {
|
||||
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
if (!isset($options['normalized_headers']['user-agent'])) {
|
||||
$options['headers'][] = 'User-Agent: Symfony HttpClient/Amp';
|
||||
}
|
||||
|
||||
if (0 < $options['max_duration']) {
|
||||
$options['timeout'] = min($options['max_duration'], $options['timeout']);
|
||||
}
|
||||
|
||||
if ($options['resolve']) {
|
||||
$this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
|
||||
}
|
||||
|
||||
if ($options['peer_fingerprint'] && !isset($options['peer_fingerprint']['pin-sha256'])) {
|
||||
throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
|
||||
}
|
||||
|
||||
$request = new Request(implode('', $url), $method);
|
||||
|
||||
if ($options['http_version']) {
|
||||
switch ((float) $options['http_version']) {
|
||||
case 1.0: $request->setProtocolVersions(['1.0']); break;
|
||||
case 1.1: $request->setProtocolVersions(['1.1', '1.0']); break;
|
||||
default: $request->setProtocolVersions(['2', '1.1', '1.0']); break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($options['headers'] as $v) {
|
||||
$h = explode(': ', $v, 2);
|
||||
$request->addHeader($h[0], $h[1]);
|
||||
}
|
||||
|
||||
$request->setTcpConnectTimeout(1000 * $options['timeout']);
|
||||
$request->setTlsHandshakeTimeout(1000 * $options['timeout']);
|
||||
$request->setTransferTimeout(1000 * $options['max_duration']);
|
||||
if (method_exists($request, 'setInactivityTimeout')) {
|
||||
$request->setInactivityTimeout(0);
|
||||
}
|
||||
|
||||
if ('' !== $request->getUri()->getUserInfo() && !$request->hasHeader('authorization')) {
|
||||
$auth = explode(':', $request->getUri()->getUserInfo(), 2);
|
||||
$auth = array_map('rawurldecode', $auth) + [1 => ''];
|
||||
$request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth)));
|
||||
}
|
||||
|
||||
return new AmpResponse($this->multi, $request, $options, $this->logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
if ($responses instanceof AmpResponse) {
|
||||
$responses = [$responses];
|
||||
} elseif (!is_iterable($responses)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AmpResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
|
||||
}
|
||||
|
||||
return new ResponseStream(AmpResponse::stream($responses, $timeout));
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->multi->dnsCache = [];
|
||||
|
||||
foreach ($this->multi->pushedResponses as $authority => $pushedResponses) {
|
||||
foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) {
|
||||
$pushDeferred->fail(new CancelledException());
|
||||
|
||||
if ($this->logger) {
|
||||
$this->logger->debug(sprintf('Unused pushed response: "%s"', $pushedUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->multi->pushedResponses = [];
|
||||
}
|
||||
}
|
48
vendor/symfony/http-client/AsyncDecoratorTrait.php
vendored
Normal file
48
vendor/symfony/http-client/AsyncDecoratorTrait.php
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Symfony\Component\HttpClient\Response\AsyncResponse;
|
||||
use Symfony\Component\HttpClient\Response\ResponseStream;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
|
||||
/**
|
||||
* Eases with processing responses while streaming them.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
trait AsyncDecoratorTrait
|
||||
{
|
||||
use DecoratorTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return AsyncResponse
|
||||
*/
|
||||
abstract public function request(string $method, string $url, array $options = []): ResponseInterface;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
if ($responses instanceof AsyncResponse) {
|
||||
$responses = [$responses];
|
||||
} elseif (!is_iterable($responses)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
|
||||
}
|
||||
|
||||
return new ResponseStream(AsyncResponse::stream($responses, $timeout, static::class));
|
||||
}
|
||||
}
|
54
vendor/symfony/http-client/CHANGELOG.md
vendored
Normal file
54
vendor/symfony/http-client/CHANGELOG.md
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.4
|
||||
---
|
||||
|
||||
* Add `MockHttpClient::setResponseFactory()` method to be able to set response factory after client creating
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Implement `HttpClientInterface::withOptions()` from `symfony/contracts` v2.4
|
||||
* Add `DecoratorTrait` to ease writing simple decorators
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* added `AsyncDecoratorTrait` to ease processing responses without breaking async
|
||||
* added support for pausing responses with a new `pause_handler` callable exposed as an info item
|
||||
* added `StreamableInterface` to ease turning responses into PHP streams
|
||||
* added `MockResponse::getRequestMethod()` and `getRequestUrl()` to allow inspecting which request has been sent
|
||||
* added `EventSourceHttpClient` a Server-Sent events stream implementing the [EventSource specification](https://www.w3.org/TR/eventsource/#eventsource)
|
||||
* added option "extra.curl" to allow setting additional curl options in `CurlHttpClient`
|
||||
* added `RetryableHttpClient` to automatically retry failed HTTP requests.
|
||||
* added `extra.trace_content` option to `TraceableHttpClient` to prevent it from keeping the content in memory
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* added `NoPrivateNetworkHttpClient` decorator
|
||||
* added `AmpHttpClient`, a portable HTTP/2 implementation based on Amp
|
||||
* added `LoggerAwareInterface` to `ScopingHttpClient` and `TraceableHttpClient`
|
||||
* made `HttpClient::create()` return an `AmpHttpClient` when `amphp/http-client` is found but curl is not or too old
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* added `canceled` to `ResponseInterface::getInfo()`
|
||||
* added `HttpClient::createForBaseUri()`
|
||||
* added `HttplugClient` with support for sync and async requests
|
||||
* added `max_duration` option
|
||||
* added support for NTLM authentication
|
||||
* added `StreamWrapper` to cast any `ResponseInterface` instances to PHP streams.
|
||||
* added `$response->toStream()` to cast responses to regular PHP streams
|
||||
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
|
||||
* added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler
|
||||
* allow enabling buffering conditionally with a Closure
|
||||
* allow option "buffer" to be a stream resource
|
||||
* allow arbitrary values for the "json" option
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* added the component
|
152
vendor/symfony/http-client/CachingHttpClient.php
vendored
Normal file
152
vendor/symfony/http-client/CachingHttpClient.php
vendored
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
use Symfony\Component\HttpClient\Response\ResponseStream;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
|
||||
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
|
||||
use Symfony\Component\HttpKernel\HttpClientKernel;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* Adds caching on top of an HTTP client.
|
||||
*
|
||||
* The implementation buffers responses in memory and doesn't stream directly from the network.
|
||||
* You can disable/enable this layer by setting option "no_cache" under "extra" to true/false.
|
||||
* By default, caching is enabled unless the "buffer" option is set to false.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class CachingHttpClient implements HttpClientInterface, ResetInterface
|
||||
{
|
||||
use HttpClientTrait;
|
||||
|
||||
private $client;
|
||||
private $cache;
|
||||
private $defaultOptions = self::OPTIONS_DEFAULTS;
|
||||
|
||||
public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = [])
|
||||
{
|
||||
if (!class_exists(HttpClientKernel::class)) {
|
||||
throw new \LogicException(sprintf('Using "%s" requires that the HttpKernel component version 4.3 or higher is installed, try running "composer require symfony/http-kernel:^5.4".', __CLASS__));
|
||||
}
|
||||
|
||||
$this->client = $client;
|
||||
$kernel = new HttpClientKernel($client);
|
||||
$this->cache = new HttpCache($kernel, $store, null, $defaultOptions);
|
||||
|
||||
unset($defaultOptions['debug']);
|
||||
unset($defaultOptions['default_ttl']);
|
||||
unset($defaultOptions['private_headers']);
|
||||
unset($defaultOptions['allow_reload']);
|
||||
unset($defaultOptions['allow_revalidate']);
|
||||
unset($defaultOptions['stale_while_revalidate']);
|
||||
unset($defaultOptions['stale_if_error']);
|
||||
unset($defaultOptions['trace_level']);
|
||||
unset($defaultOptions['trace_header']);
|
||||
|
||||
if ($defaultOptions) {
|
||||
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
|
||||
$url = implode('', $url);
|
||||
|
||||
if (!empty($options['body']) || !empty($options['extra']['no_cache']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
|
||||
return $this->client->request($method, $url, $options);
|
||||
}
|
||||
|
||||
$request = Request::create($url, $method);
|
||||
$request->attributes->set('http_client_options', $options);
|
||||
|
||||
foreach ($options['normalized_headers'] as $name => $values) {
|
||||
if ('cookie' !== $name) {
|
||||
foreach ($values as $value) {
|
||||
$request->headers->set($name, substr($value, 2 + \strlen($name)), false);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($values as $cookies) {
|
||||
foreach (explode('; ', substr($cookies, \strlen('Cookie: '))) as $cookie) {
|
||||
if ('' !== $cookie) {
|
||||
$cookie = explode('=', $cookie, 2);
|
||||
$request->cookies->set($cookie[0], $cookie[1] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->cache->handle($request);
|
||||
$response = new MockResponse($response->getContent(), [
|
||||
'http_code' => $response->getStatusCode(),
|
||||
'response_headers' => $response->headers->allPreserveCase(),
|
||||
]);
|
||||
|
||||
return MockResponse::fromRequest($method, $url, $options, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
if ($responses instanceof ResponseInterface) {
|
||||
$responses = [$responses];
|
||||
} elseif (!is_iterable($responses)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of ResponseInterface objects, "%s" given.', __METHOD__, get_debug_type($responses)));
|
||||
}
|
||||
|
||||
$mockResponses = [];
|
||||
$clientResponses = [];
|
||||
|
||||
foreach ($responses as $response) {
|
||||
if ($response instanceof MockResponse) {
|
||||
$mockResponses[] = $response;
|
||||
} else {
|
||||
$clientResponses[] = $response;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$mockResponses) {
|
||||
return $this->client->stream($clientResponses, $timeout);
|
||||
}
|
||||
|
||||
if (!$clientResponses) {
|
||||
return new ResponseStream(MockResponse::stream($mockResponses, $timeout));
|
||||
}
|
||||
|
||||
return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) {
|
||||
yield from MockResponse::stream($mockResponses, $timeout);
|
||||
yield $this->client->stream($clientResponses, $timeout);
|
||||
})());
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if ($this->client instanceof ResetInterface) {
|
||||
$this->client->reset();
|
||||
}
|
||||
}
|
||||
}
|
87
vendor/symfony/http-client/Chunk/DataChunk.php
vendored
Normal file
87
vendor/symfony/http-client/Chunk/DataChunk.php
vendored
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Chunk;
|
||||
|
||||
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class DataChunk implements ChunkInterface
|
||||
{
|
||||
private $offset = 0;
|
||||
private $content = '';
|
||||
|
||||
public function __construct(int $offset = 0, string $content = '')
|
||||
{
|
||||
$this->offset = $offset;
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isTimeout(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isFirst(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isLast(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInformationalStatus(): ?array
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOffset(): int
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getError(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
140
vendor/symfony/http-client/Chunk/ErrorChunk.php
vendored
Normal file
140
vendor/symfony/http-client/Chunk/ErrorChunk.php
vendored
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Chunk;
|
||||
|
||||
use Symfony\Component\HttpClient\Exception\TimeoutException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ErrorChunk implements ChunkInterface
|
||||
{
|
||||
private $didThrow = false;
|
||||
private $offset;
|
||||
private $errorMessage;
|
||||
private $error;
|
||||
|
||||
/**
|
||||
* @param \Throwable|string $error
|
||||
*/
|
||||
public function __construct(int $offset, $error)
|
||||
{
|
||||
$this->offset = $offset;
|
||||
|
||||
if (\is_string($error)) {
|
||||
$this->errorMessage = $error;
|
||||
} else {
|
||||
$this->error = $error;
|
||||
$this->errorMessage = $error->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isTimeout(): bool
|
||||
{
|
||||
$this->didThrow = true;
|
||||
|
||||
if (null !== $this->error) {
|
||||
throw new TransportException($this->errorMessage, 0, $this->error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isFirst(): bool
|
||||
{
|
||||
$this->didThrow = true;
|
||||
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isLast(): bool
|
||||
{
|
||||
$this->didThrow = true;
|
||||
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInformationalStatus(): ?array
|
||||
{
|
||||
$this->didThrow = true;
|
||||
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
$this->didThrow = true;
|
||||
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOffset(): int
|
||||
{
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getError(): ?string
|
||||
{
|
||||
return $this->errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether the wrapped error has been thrown or not
|
||||
*/
|
||||
public function didThrow(?bool $didThrow = null): bool
|
||||
{
|
||||
if (null !== $didThrow && $this->didThrow !== $didThrow) {
|
||||
return !$this->didThrow = $didThrow;
|
||||
}
|
||||
|
||||
return $this->didThrow;
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (!$this->didThrow) {
|
||||
$this->didThrow = true;
|
||||
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
28
vendor/symfony/http-client/Chunk/FirstChunk.php
vendored
Normal file
28
vendor/symfony/http-client/Chunk/FirstChunk.php
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Chunk;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FirstChunk extends DataChunk
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isFirst(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
35
vendor/symfony/http-client/Chunk/InformationalChunk.php
vendored
Normal file
35
vendor/symfony/http-client/Chunk/InformationalChunk.php
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Chunk;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class InformationalChunk extends DataChunk
|
||||
{
|
||||
private $status;
|
||||
|
||||
public function __construct(int $statusCode, array $headers)
|
||||
{
|
||||
$this->status = [$statusCode, $headers];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInformationalStatus(): ?array
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
}
|
28
vendor/symfony/http-client/Chunk/LastChunk.php
vendored
Normal file
28
vendor/symfony/http-client/Chunk/LastChunk.php
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Chunk;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LastChunk extends DataChunk
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isLast(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
79
vendor/symfony/http-client/Chunk/ServerSentEvent.php
vendored
Normal file
79
vendor/symfony/http-client/Chunk/ServerSentEvent.php
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Chunk;
|
||||
|
||||
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||
|
||||
/**
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class ServerSentEvent extends DataChunk implements ChunkInterface
|
||||
{
|
||||
private $data = '';
|
||||
private $id = '';
|
||||
private $type = 'message';
|
||||
private $retry = 0;
|
||||
|
||||
public function __construct(string $content)
|
||||
{
|
||||
parent::__construct(-1, $content);
|
||||
|
||||
// remove BOM
|
||||
if (0 === strpos($content, "\xEF\xBB\xBF")) {
|
||||
$content = substr($content, 3);
|
||||
}
|
||||
|
||||
foreach (preg_split("/(?:\r\n|[\r\n])/", $content) as $line) {
|
||||
if (0 === $i = strpos($line, ':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$i = false === $i ? \strlen($line) : $i;
|
||||
$field = substr($line, 0, $i);
|
||||
$i += 1 + (' ' === ($line[1 + $i] ?? ''));
|
||||
|
||||
switch ($field) {
|
||||
case 'id': $this->id = substr($line, $i); break;
|
||||
case 'event': $this->type = substr($line, $i); break;
|
||||
case 'data': $this->data .= ('' === $this->data ? '' : "\n").substr($line, $i); break;
|
||||
case 'retry':
|
||||
$retry = substr($line, $i);
|
||||
|
||||
if ('' !== $retry && \strlen($retry) === strspn($retry, '0123456789')) {
|
||||
$this->retry = $retry / 1000.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getData(): string
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function getRetry(): float
|
||||
{
|
||||
return $this->retry;
|
||||
}
|
||||
}
|
575
vendor/symfony/http-client/CurlHttpClient.php
vendored
Normal file
575
vendor/symfony/http-client/CurlHttpClient.php
vendored
Normal file
|
@ -0,0 +1,575 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\Internal\CurlClientState;
|
||||
use Symfony\Component\HttpClient\Internal\PushedResponse;
|
||||
use Symfony\Component\HttpClient\Response\CurlResponse;
|
||||
use Symfony\Component\HttpClient\Response\ResponseStream;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* A performant implementation of the HttpClientInterface contracts based on the curl extension.
|
||||
*
|
||||
* This provides fully concurrent HTTP requests, with transparent
|
||||
* HTTP/2 push when a curl version that supports it is installed.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
|
||||
{
|
||||
use HttpClientTrait;
|
||||
|
||||
private $defaultOptions = self::OPTIONS_DEFAULTS + [
|
||||
'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
|
||||
// password as the second one; or string like username:password - enabling NTLM auth
|
||||
'extra' => [
|
||||
'curl' => [], // A list of extra curl options indexed by their corresponding CURLOPT_*
|
||||
],
|
||||
];
|
||||
private static $emptyDefaults = self::OPTIONS_DEFAULTS + ['auth_ntlm' => null];
|
||||
|
||||
/**
|
||||
* @var LoggerInterface|null
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
private $maxHostConnections;
|
||||
private $maxPendingPushes;
|
||||
|
||||
/**
|
||||
* An internal object to share state between the client and its responses.
|
||||
*
|
||||
* @var CurlClientState
|
||||
*/
|
||||
private $multi;
|
||||
|
||||
/**
|
||||
* @param array $defaultOptions Default request's options
|
||||
* @param int $maxHostConnections The maximum number of connections to a single host
|
||||
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
|
||||
*
|
||||
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
|
||||
*/
|
||||
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 0)
|
||||
{
|
||||
if (!\extension_loaded('curl')) {
|
||||
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
|
||||
}
|
||||
|
||||
$this->maxHostConnections = $maxHostConnections;
|
||||
$this->maxPendingPushes = $maxPendingPushes;
|
||||
|
||||
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
|
||||
|
||||
if ($defaultOptions) {
|
||||
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
if (isset($this->multi)) {
|
||||
$this->multi->logger = $logger;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$multi = $this->ensureState();
|
||||
|
||||
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
|
||||
$scheme = $url['scheme'];
|
||||
$authority = $url['authority'];
|
||||
$host = parse_url($authority, \PHP_URL_HOST);
|
||||
$proxy = self::getProxyUrl($options['proxy'], $url);
|
||||
$url = implode('', $url);
|
||||
|
||||
if (!isset($options['normalized_headers']['user-agent'])) {
|
||||
$options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
|
||||
}
|
||||
|
||||
$curlopts = [
|
||||
\CURLOPT_URL => $url,
|
||||
\CURLOPT_TCP_NODELAY => true,
|
||||
\CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
|
||||
\CURLOPT_REDIR_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
|
||||
\CURLOPT_FOLLOWLOCATION => true,
|
||||
\CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
|
||||
\CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
|
||||
\CURLOPT_TIMEOUT => 0,
|
||||
\CURLOPT_PROXY => $proxy,
|
||||
\CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
|
||||
\CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
|
||||
\CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
|
||||
\CURLOPT_CAINFO => $options['cafile'],
|
||||
\CURLOPT_CAPATH => $options['capath'],
|
||||
\CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
|
||||
\CURLOPT_SSLCERT => $options['local_cert'],
|
||||
\CURLOPT_SSLKEY => $options['local_pk'],
|
||||
\CURLOPT_KEYPASSWD => $options['passphrase'],
|
||||
\CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
|
||||
];
|
||||
|
||||
if (1.0 === (float) $options['http_version']) {
|
||||
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
|
||||
} elseif (1.1 === (float) $options['http_version']) {
|
||||
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
|
||||
} elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 & CurlClientState::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
|
||||
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
|
||||
}
|
||||
|
||||
if (isset($options['auth_ntlm'])) {
|
||||
$curlopts[\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
|
||||
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
|
||||
|
||||
if (\is_array($options['auth_ntlm'])) {
|
||||
$count = \count($options['auth_ntlm']);
|
||||
if ($count <= 0 || $count > 2) {
|
||||
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must contain 1 or 2 elements, %d given.', $count));
|
||||
}
|
||||
|
||||
$options['auth_ntlm'] = implode(':', $options['auth_ntlm']);
|
||||
}
|
||||
|
||||
if (!\is_string($options['auth_ntlm'])) {
|
||||
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must be a string or an array, "%s" given.', get_debug_type($options['auth_ntlm'])));
|
||||
}
|
||||
|
||||
$curlopts[\CURLOPT_USERPWD] = $options['auth_ntlm'];
|
||||
}
|
||||
|
||||
if (!\ZEND_THREAD_SAFE) {
|
||||
$curlopts[\CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
|
||||
}
|
||||
|
||||
if (\defined('CURLOPT_HEADEROPT') && \defined('CURLHEADER_SEPARATE')) {
|
||||
$curlopts[\CURLOPT_HEADEROPT] = \CURLHEADER_SEPARATE;
|
||||
}
|
||||
|
||||
// curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map
|
||||
if (isset($multi->dnsCache->hostnames[$host])) {
|
||||
$options['resolve'] += [$host => $multi->dnsCache->hostnames[$host]];
|
||||
}
|
||||
|
||||
if ($options['resolve'] || $multi->dnsCache->evictions) {
|
||||
// First reset any old DNS cache entries then add the new ones
|
||||
$resolve = $multi->dnsCache->evictions;
|
||||
$multi->dnsCache->evictions = [];
|
||||
$port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
|
||||
|
||||
if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
|
||||
// DNS cache removals require curl 7.42 or higher
|
||||
$multi->reset();
|
||||
}
|
||||
|
||||
foreach ($options['resolve'] as $resolveHost => $ip) {
|
||||
$resolve[] = null === $ip ? "-$resolveHost:$port" : "$resolveHost:$port:$ip";
|
||||
$multi->dnsCache->hostnames[$resolveHost] = $ip;
|
||||
$multi->dnsCache->removals["-$resolveHost:$port"] = "-$resolveHost:$port";
|
||||
}
|
||||
|
||||
$curlopts[\CURLOPT_RESOLVE] = $resolve;
|
||||
}
|
||||
|
||||
if ('POST' === $method) {
|
||||
// Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
|
||||
$curlopts[\CURLOPT_POST] = true;
|
||||
} elseif ('HEAD' === $method) {
|
||||
$curlopts[\CURLOPT_NOBODY] = true;
|
||||
} else {
|
||||
$curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
|
||||
}
|
||||
|
||||
if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) {
|
||||
$curlopts[\CURLOPT_NOSIGNAL] = true;
|
||||
}
|
||||
|
||||
if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
|
||||
$options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided
|
||||
}
|
||||
$body = $options['body'];
|
||||
|
||||
foreach ($options['headers'] as $i => $header) {
|
||||
if (\is_string($body) && '' !== $body && 0 === stripos($header, 'Content-Length: ')) {
|
||||
// Let curl handle Content-Length headers
|
||||
unset($options['headers'][$i]);
|
||||
continue;
|
||||
}
|
||||
if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
|
||||
// curl requires a special syntax to send empty headers
|
||||
$curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
|
||||
} else {
|
||||
$curlopts[\CURLOPT_HTTPHEADER][] = $header;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent curl from sending its default Accept and Expect headers
|
||||
foreach (['accept', 'expect'] as $header) {
|
||||
if (!isset($options['normalized_headers'][$header][0])) {
|
||||
$curlopts[\CURLOPT_HTTPHEADER][] = $header.':';
|
||||
}
|
||||
}
|
||||
|
||||
if (!\is_string($body)) {
|
||||
if (\is_resource($body)) {
|
||||
$curlopts[\CURLOPT_INFILE] = $body;
|
||||
} else {
|
||||
$eof = false;
|
||||
$buffer = '';
|
||||
$curlopts[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body, &$buffer, &$eof) {
|
||||
return self::readRequestBody($length, $body, $buffer, $eof);
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($options['normalized_headers']['content-length'][0])) {
|
||||
$curlopts[\CURLOPT_INFILESIZE] = (int) substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
|
||||
}
|
||||
if (!isset($options['normalized_headers']['transfer-encoding'])) {
|
||||
$curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding:'.(isset($curlopts[\CURLOPT_INFILESIZE]) ? '' : ' chunked');
|
||||
}
|
||||
|
||||
if ('POST' !== $method) {
|
||||
$curlopts[\CURLOPT_UPLOAD] = true;
|
||||
|
||||
if (!isset($options['normalized_headers']['content-type']) && 0 !== ($curlopts[\CURLOPT_INFILESIZE] ?? null)) {
|
||||
$curlopts[\CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
|
||||
}
|
||||
}
|
||||
} elseif ('' !== $body || 'POST' === $method) {
|
||||
$curlopts[\CURLOPT_POSTFIELDS] = $body;
|
||||
}
|
||||
|
||||
if ($options['peer_fingerprint']) {
|
||||
if (!isset($options['peer_fingerprint']['pin-sha256'])) {
|
||||
throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
|
||||
}
|
||||
|
||||
$curlopts[\CURLOPT_PINNEDPUBLICKEY] = 'sha256//'.implode(';sha256//', $options['peer_fingerprint']['pin-sha256']);
|
||||
}
|
||||
|
||||
if ($options['bindto']) {
|
||||
if (file_exists($options['bindto'])) {
|
||||
$curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
|
||||
} elseif (!str_starts_with($options['bindto'], 'if!') && preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) {
|
||||
$curlopts[\CURLOPT_INTERFACE] = $matches[1];
|
||||
$curlopts[\CURLOPT_LOCALPORT] = $matches[2];
|
||||
} else {
|
||||
$curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
|
||||
}
|
||||
}
|
||||
|
||||
if (0 < $options['max_duration']) {
|
||||
$curlopts[\CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
|
||||
}
|
||||
|
||||
if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
|
||||
$this->validateExtraCurlOptions($options['extra']['curl']);
|
||||
$curlopts += $options['extra']['curl'];
|
||||
}
|
||||
|
||||
if ($pushedResponse = $multi->pushedResponses[$url] ?? null) {
|
||||
unset($multi->pushedResponses[$url]);
|
||||
|
||||
if (self::acceptPushForRequest($method, $options, $pushedResponse)) {
|
||||
$this->logger && $this->logger->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url));
|
||||
|
||||
// Reinitialize the pushed response with request's options
|
||||
$ch = $pushedResponse->handle;
|
||||
$pushedResponse = $pushedResponse->response;
|
||||
$pushedResponse->__construct($multi, $url, $options, $this->logger);
|
||||
} else {
|
||||
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s"', $url));
|
||||
$pushedResponse = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$pushedResponse) {
|
||||
$ch = curl_init();
|
||||
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, $url));
|
||||
$curlopts += [\CURLOPT_SHARE => $multi->share];
|
||||
}
|
||||
|
||||
foreach ($curlopts as $opt => $value) {
|
||||
if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) {
|
||||
$constantName = $this->findConstantName($opt);
|
||||
throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
|
||||
}
|
||||
}
|
||||
|
||||
return $pushedResponse ?? new CurlResponse($multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host), CurlClientState::$curlVersion['version_number']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
if ($responses instanceof CurlResponse) {
|
||||
$responses = [$responses];
|
||||
} elseif (!is_iterable($responses)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of CurlResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
|
||||
}
|
||||
|
||||
$multi = $this->ensureState();
|
||||
|
||||
if (\is_resource($multi->handle) || $multi->handle instanceof \CurlMultiHandle) {
|
||||
$active = 0;
|
||||
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle, $active)) {
|
||||
}
|
||||
}
|
||||
|
||||
return new ResponseStream(CurlResponse::stream($responses, $timeout));
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if (isset($this->multi)) {
|
||||
$this->multi->reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts pushed responses only if their headers related to authentication match the request.
|
||||
*/
|
||||
private static function acceptPushForRequest(string $method, array $options, PushedResponse $pushedResponse): bool
|
||||
{
|
||||
if ('' !== $options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (['proxy', 'no_proxy', 'bindto', 'local_cert', 'local_pk'] as $k) {
|
||||
if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
|
||||
$normalizedHeaders = $options['normalized_headers'][$k] ?? [];
|
||||
foreach ($normalizedHeaders as $i => $v) {
|
||||
$normalizedHeaders[$i] = substr($v, \strlen($k) + 2);
|
||||
}
|
||||
|
||||
if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the request's body callback to allow it to return strings longer than curl requested.
|
||||
*/
|
||||
private static function readRequestBody(int $length, \Closure $body, string &$buffer, bool &$eof): string
|
||||
{
|
||||
if (!$eof && \strlen($buffer) < $length) {
|
||||
if (!\is_string($data = $body($length))) {
|
||||
throw new TransportException(sprintf('The return value of the "body" option callback must be a string, "%s" returned.', get_debug_type($data)));
|
||||
}
|
||||
|
||||
$buffer .= $data;
|
||||
$eof = '' === $data;
|
||||
}
|
||||
|
||||
$data = substr($buffer, 0, $length);
|
||||
$buffer = substr($buffer, $length);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves relative URLs on redirects and deals with authentication headers.
|
||||
*
|
||||
* Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
|
||||
*/
|
||||
private static function createRedirectResolver(array $options, string $host): \Closure
|
||||
{
|
||||
$redirectHeaders = [];
|
||||
if (0 < $options['max_redirects']) {
|
||||
$redirectHeaders['host'] = $host;
|
||||
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
|
||||
return 0 !== stripos($h, 'Host:');
|
||||
});
|
||||
|
||||
if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) {
|
||||
$redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
|
||||
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) {
|
||||
try {
|
||||
$location = self::parseUrl($location);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($noContent && $redirectHeaders) {
|
||||
$filterContentHeaders = static function ($h) {
|
||||
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
|
||||
};
|
||||
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
|
||||
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
|
||||
}
|
||||
|
||||
if ($redirectHeaders && $host = parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
|
||||
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
|
||||
curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
|
||||
} elseif ($noContent && $redirectHeaders) {
|
||||
curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']);
|
||||
}
|
||||
|
||||
$url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
|
||||
$url = self::resolveUrl($location, $url);
|
||||
|
||||
curl_setopt($ch, \CURLOPT_PROXY, self::getProxyUrl($options['proxy'], $url));
|
||||
|
||||
return implode('', $url);
|
||||
};
|
||||
}
|
||||
|
||||
private function ensureState(): CurlClientState
|
||||
{
|
||||
if (!isset($this->multi)) {
|
||||
$this->multi = new CurlClientState($this->maxHostConnections, $this->maxPendingPushes);
|
||||
$this->multi->logger = $this->logger;
|
||||
}
|
||||
|
||||
return $this->multi;
|
||||
}
|
||||
|
||||
private function findConstantName(int $opt): ?string
|
||||
{
|
||||
$constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
|
||||
return $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_'));
|
||||
}, \ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
return key($constants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents overriding options that are set internally throughout the request.
|
||||
*/
|
||||
private function validateExtraCurlOptions(array $options): void
|
||||
{
|
||||
$curloptsToConfig = [
|
||||
// options used in CurlHttpClient
|
||||
\CURLOPT_HTTPAUTH => 'auth_ntlm',
|
||||
\CURLOPT_USERPWD => 'auth_ntlm',
|
||||
\CURLOPT_RESOLVE => 'resolve',
|
||||
\CURLOPT_NOSIGNAL => 'timeout',
|
||||
\CURLOPT_HTTPHEADER => 'headers',
|
||||
\CURLOPT_INFILE => 'body',
|
||||
\CURLOPT_READFUNCTION => 'body',
|
||||
\CURLOPT_INFILESIZE => 'body',
|
||||
\CURLOPT_POSTFIELDS => 'body',
|
||||
\CURLOPT_UPLOAD => 'body',
|
||||
\CURLOPT_INTERFACE => 'bindto',
|
||||
\CURLOPT_TIMEOUT_MS => 'max_duration',
|
||||
\CURLOPT_TIMEOUT => 'max_duration',
|
||||
\CURLOPT_MAXREDIRS => 'max_redirects',
|
||||
\CURLOPT_POSTREDIR => 'max_redirects',
|
||||
\CURLOPT_PROXY => 'proxy',
|
||||
\CURLOPT_NOPROXY => 'no_proxy',
|
||||
\CURLOPT_SSL_VERIFYPEER => 'verify_peer',
|
||||
\CURLOPT_SSL_VERIFYHOST => 'verify_host',
|
||||
\CURLOPT_CAINFO => 'cafile',
|
||||
\CURLOPT_CAPATH => 'capath',
|
||||
\CURLOPT_SSL_CIPHER_LIST => 'ciphers',
|
||||
\CURLOPT_SSLCERT => 'local_cert',
|
||||
\CURLOPT_SSLKEY => 'local_pk',
|
||||
\CURLOPT_KEYPASSWD => 'passphrase',
|
||||
\CURLOPT_CERTINFO => 'capture_peer_cert_chain',
|
||||
\CURLOPT_USERAGENT => 'normalized_headers',
|
||||
\CURLOPT_REFERER => 'headers',
|
||||
// options used in CurlResponse
|
||||
\CURLOPT_NOPROGRESS => 'on_progress',
|
||||
\CURLOPT_PROGRESSFUNCTION => 'on_progress',
|
||||
];
|
||||
|
||||
if (\defined('CURLOPT_UNIX_SOCKET_PATH')) {
|
||||
$curloptsToConfig[\CURLOPT_UNIX_SOCKET_PATH] = 'bindto';
|
||||
}
|
||||
|
||||
if (\defined('CURLOPT_PINNEDPUBLICKEY')) {
|
||||
$curloptsToConfig[\CURLOPT_PINNEDPUBLICKEY] = 'peer_fingerprint';
|
||||
}
|
||||
|
||||
$curloptsToCheck = [
|
||||
\CURLOPT_PRIVATE,
|
||||
\CURLOPT_HEADERFUNCTION,
|
||||
\CURLOPT_WRITEFUNCTION,
|
||||
\CURLOPT_VERBOSE,
|
||||
\CURLOPT_STDERR,
|
||||
\CURLOPT_RETURNTRANSFER,
|
||||
\CURLOPT_URL,
|
||||
\CURLOPT_FOLLOWLOCATION,
|
||||
\CURLOPT_HEADER,
|
||||
\CURLOPT_CONNECTTIMEOUT,
|
||||
\CURLOPT_CONNECTTIMEOUT_MS,
|
||||
\CURLOPT_HTTP_VERSION,
|
||||
\CURLOPT_PORT,
|
||||
\CURLOPT_DNS_USE_GLOBAL_CACHE,
|
||||
\CURLOPT_PROTOCOLS,
|
||||
\CURLOPT_REDIR_PROTOCOLS,
|
||||
\CURLOPT_COOKIEFILE,
|
||||
\CURLINFO_REDIRECT_COUNT,
|
||||
];
|
||||
|
||||
if (\defined('CURLOPT_HTTP09_ALLOWED')) {
|
||||
$curloptsToCheck[] = \CURLOPT_HTTP09_ALLOWED;
|
||||
}
|
||||
|
||||
if (\defined('CURLOPT_HEADEROPT')) {
|
||||
$curloptsToCheck[] = \CURLOPT_HEADEROPT;
|
||||
}
|
||||
|
||||
$methodOpts = [
|
||||
\CURLOPT_POST,
|
||||
\CURLOPT_PUT,
|
||||
\CURLOPT_CUSTOMREQUEST,
|
||||
\CURLOPT_HTTPGET,
|
||||
\CURLOPT_NOBODY,
|
||||
];
|
||||
|
||||
foreach ($options as $opt => $optValue) {
|
||||
if (isset($curloptsToConfig[$opt])) {
|
||||
$constName = $this->findConstantName($opt) ?? $opt;
|
||||
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt]));
|
||||
}
|
||||
|
||||
if (\in_array($opt, $methodOpts)) {
|
||||
throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
|
||||
}
|
||||
|
||||
if (\in_array($opt, $curloptsToCheck)) {
|
||||
$constName = $this->findConstantName($opt) ?? $opt;
|
||||
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
vendor/symfony/http-client/DataCollector/HttpClientDataCollector.php
vendored
Normal file
176
vendor/symfony/http-client/DataCollector/HttpClientDataCollector.php
vendored
Normal file
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\DataCollector;
|
||||
|
||||
use Symfony\Component\HttpClient\TraceableHttpClient;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
|
||||
use Symfony\Component\VarDumper\Caster\ImgStub;
|
||||
|
||||
/**
|
||||
* @author Jérémy Romey <jeremy@free-agent.fr>
|
||||
*/
|
||||
final class HttpClientDataCollector extends DataCollector implements LateDataCollectorInterface
|
||||
{
|
||||
/**
|
||||
* @var TraceableHttpClient[]
|
||||
*/
|
||||
private $clients = [];
|
||||
|
||||
public function registerClient(string $name, TraceableHttpClient $client)
|
||||
{
|
||||
$this->clients[$name] = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function collect(Request $request, Response $response, ?\Throwable $exception = null)
|
||||
{
|
||||
$this->lateCollect();
|
||||
}
|
||||
|
||||
public function lateCollect()
|
||||
{
|
||||
$this->data['request_count'] = $this->data['request_count'] ?? 0;
|
||||
$this->data['error_count'] = $this->data['error_count'] ?? 0;
|
||||
$this->data += ['clients' => []];
|
||||
|
||||
foreach ($this->clients as $name => $client) {
|
||||
[$errorCount, $traces] = $this->collectOnClient($client);
|
||||
|
||||
$this->data['clients'] += [
|
||||
$name => [
|
||||
'traces' => [],
|
||||
'error_count' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
$this->data['clients'][$name]['traces'] = array_merge($this->data['clients'][$name]['traces'], $traces);
|
||||
$this->data['request_count'] += \count($traces);
|
||||
$this->data['error_count'] += $errorCount;
|
||||
$this->data['clients'][$name]['error_count'] += $errorCount;
|
||||
|
||||
$client->reset();
|
||||
}
|
||||
}
|
||||
|
||||
public function getClients(): array
|
||||
{
|
||||
return $this->data['clients'] ?? [];
|
||||
}
|
||||
|
||||
public function getRequestCount(): int
|
||||
{
|
||||
return $this->data['request_count'] ?? 0;
|
||||
}
|
||||
|
||||
public function getErrorCount(): int
|
||||
{
|
||||
return $this->data['error_count'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'http_client';
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->data = [
|
||||
'clients' => [],
|
||||
'request_count' => 0,
|
||||
'error_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function collectOnClient(TraceableHttpClient $client): array
|
||||
{
|
||||
$traces = $client->getTracedRequests();
|
||||
$errorCount = 0;
|
||||
$baseInfo = [
|
||||
'response_headers' => 1,
|
||||
'retry_count' => 1,
|
||||
'redirect_count' => 1,
|
||||
'redirect_url' => 1,
|
||||
'user_data' => 1,
|
||||
'error' => 1,
|
||||
'url' => 1,
|
||||
];
|
||||
|
||||
foreach ($traces as $i => $trace) {
|
||||
if (400 <= ($trace['info']['http_code'] ?? 0)) {
|
||||
++$errorCount;
|
||||
}
|
||||
|
||||
$info = $trace['info'];
|
||||
$traces[$i]['http_code'] = $info['http_code'] ?? 0;
|
||||
|
||||
unset($info['filetime'], $info['http_code'], $info['ssl_verify_result'], $info['content_type']);
|
||||
|
||||
if (($info['http_method'] ?? null) === $trace['method']) {
|
||||
unset($info['http_method']);
|
||||
}
|
||||
|
||||
if (($info['url'] ?? null) === $trace['url']) {
|
||||
unset($info['url']);
|
||||
}
|
||||
|
||||
foreach ($info as $k => $v) {
|
||||
if (!$v || (is_numeric($v) && 0 > $v)) {
|
||||
unset($info[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
if (\is_string($content = $trace['content'])) {
|
||||
$contentType = 'application/octet-stream';
|
||||
|
||||
foreach ($info['response_headers'] ?? [] as $h) {
|
||||
if (0 === stripos($h, 'content-type: ')) {
|
||||
$contentType = substr($h, \strlen('content-type: '));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === strpos($contentType, 'image/') && class_exists(ImgStub::class)) {
|
||||
$content = new ImgStub($content, $contentType, '');
|
||||
} else {
|
||||
$content = [$content];
|
||||
}
|
||||
|
||||
$content = ['response_content' => $content];
|
||||
} elseif (\is_array($content)) {
|
||||
$content = ['response_json' => $content];
|
||||
} else {
|
||||
$content = [];
|
||||
}
|
||||
|
||||
if (isset($info['retry_count'])) {
|
||||
$content['retries'] = $info['previous_info'];
|
||||
unset($info['previous_info']);
|
||||
}
|
||||
|
||||
$debugInfo = array_diff_key($info, $baseInfo);
|
||||
$info = ['info' => $debugInfo] + array_diff_key($info, $debugInfo) + $content;
|
||||
unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
|
||||
$traces[$i]['info'] = $this->cloneVar($info);
|
||||
$traces[$i]['options'] = $this->cloneVar($trace['options']);
|
||||
}
|
||||
|
||||
return [$errorCount, $traces];
|
||||
}
|
||||
}
|
66
vendor/symfony/http-client/DecoratorTrait.php
vendored
Normal file
66
vendor/symfony/http-client/DecoratorTrait.php
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* Eases with writing decorators.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
trait DecoratorTrait
|
||||
{
|
||||
private $client;
|
||||
|
||||
public function __construct(?HttpClientInterface $client = null)
|
||||
{
|
||||
$this->client = $client ?? HttpClient::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->client->request($method, $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->client->stream($responses, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function withOptions(array $options): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->client = $this->client->withOptions($options);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if ($this->client instanceof ResetInterface) {
|
||||
$this->client->reset();
|
||||
}
|
||||
}
|
||||
}
|
51
vendor/symfony/http-client/DependencyInjection/HttpClientPass.php
vendored
Normal file
51
vendor/symfony/http-client/DependencyInjection/HttpClientPass.php
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpClient\TraceableHttpClient;
|
||||
|
||||
final class HttpClientPass implements CompilerPassInterface
|
||||
{
|
||||
private $clientTag;
|
||||
|
||||
public function __construct(string $clientTag = 'http_client.client')
|
||||
{
|
||||
if (0 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/http-client', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->clientTag = $clientTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition('data_collector.http_client')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
|
||||
$container->register('.debug.'.$id, TraceableHttpClient::class)
|
||||
->setArguments([new Reference('.debug.'.$id.'.inner'), new Reference('debug.stopwatch', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])
|
||||
->addTag('kernel.reset', ['method' => 'reset'])
|
||||
->setDecoratedService($id, null, 5);
|
||||
$container->getDefinition('data_collector.http_client')
|
||||
->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]);
|
||||
}
|
||||
}
|
||||
}
|
162
vendor/symfony/http-client/EventSourceHttpClient.php
vendored
Normal file
162
vendor/symfony/http-client/EventSourceHttpClient.php
vendored
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Symfony\Component\HttpClient\Chunk\DataChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
|
||||
use Symfony\Component\HttpClient\Exception\EventSourceException;
|
||||
use Symfony\Component\HttpClient\Response\AsyncContext;
|
||||
use Symfony\Component\HttpClient\Response\AsyncResponse;
|
||||
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* @author Antoine Bluchet <soyuka@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class EventSourceHttpClient implements HttpClientInterface, ResetInterface
|
||||
{
|
||||
use AsyncDecoratorTrait, HttpClientTrait {
|
||||
AsyncDecoratorTrait::withOptions insteadof HttpClientTrait;
|
||||
}
|
||||
|
||||
private $reconnectionTime;
|
||||
|
||||
public function __construct(?HttpClientInterface $client = null, float $reconnectionTime = 10.0)
|
||||
{
|
||||
$this->client = $client ?? HttpClient::create();
|
||||
$this->reconnectionTime = $reconnectionTime;
|
||||
}
|
||||
|
||||
public function connect(string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('GET', $url, self::mergeDefaultOptions($options, [
|
||||
'buffer' => false,
|
||||
'headers' => [
|
||||
'Accept' => 'text/event-stream',
|
||||
'Cache-Control' => 'no-cache',
|
||||
],
|
||||
], true));
|
||||
}
|
||||
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$state = new class() {
|
||||
public $buffer = null;
|
||||
public $lastEventId = null;
|
||||
public $reconnectionTime;
|
||||
public $lastError = null;
|
||||
};
|
||||
$state->reconnectionTime = $this->reconnectionTime;
|
||||
|
||||
if ($accept = self::normalizeHeaders($options['headers'] ?? [])['accept'] ?? []) {
|
||||
$state->buffer = \in_array($accept, [['Accept: text/event-stream'], ['accept: text/event-stream']], true) ? '' : null;
|
||||
|
||||
if (null !== $state->buffer) {
|
||||
$options['extra']['trace_content'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use ($state, $method, $url, $options) {
|
||||
if (null !== $state->buffer) {
|
||||
$context->setInfo('reconnection_time', $state->reconnectionTime);
|
||||
$isTimeout = false;
|
||||
}
|
||||
$lastError = $state->lastError;
|
||||
$state->lastError = null;
|
||||
|
||||
try {
|
||||
$isTimeout = $chunk->isTimeout();
|
||||
|
||||
if (null !== $chunk->getInformationalStatus() || $context->getInfo('canceled')) {
|
||||
yield $chunk;
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$state->lastError = $lastError ?? microtime(true);
|
||||
|
||||
if (null === $state->buffer || ($isTimeout && microtime(true) - $state->lastError < $state->reconnectionTime)) {
|
||||
yield $chunk;
|
||||
} else {
|
||||
$options['headers']['Last-Event-ID'] = $state->lastEventId;
|
||||
$state->buffer = '';
|
||||
$state->lastError = microtime(true);
|
||||
$context->getResponse()->cancel();
|
||||
$context->replaceRequest($method, $url, $options);
|
||||
if ($isTimeout) {
|
||||
yield $chunk;
|
||||
} else {
|
||||
$context->pause($state->reconnectionTime);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($chunk->isFirst()) {
|
||||
if (preg_match('/^text\/event-stream(;|$)/i', $context->getHeaders()['content-type'][0] ?? '')) {
|
||||
$state->buffer = '';
|
||||
} elseif (null !== $lastError || (null !== $state->buffer && 200 === $context->getStatusCode())) {
|
||||
throw new EventSourceException(sprintf('Response content-type is "%s" while "text/event-stream" was expected for "%s".', $context->getHeaders()['content-type'][0] ?? '', $context->getInfo('url')));
|
||||
} else {
|
||||
$context->passthru();
|
||||
}
|
||||
|
||||
if (null === $lastError) {
|
||||
yield $chunk;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($chunk->isLast()) {
|
||||
if ('' !== $content = $state->buffer) {
|
||||
$state->buffer = '';
|
||||
yield new DataChunk(-1, $content);
|
||||
}
|
||||
|
||||
yield $chunk;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $state->buffer.$chunk->getContent();
|
||||
$events = preg_split('/((?:\r\n){2,}|\r{2,}|\n{2,})/', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
|
||||
$state->buffer = array_pop($events);
|
||||
|
||||
for ($i = 0; isset($events[$i]); $i += 2) {
|
||||
$content = $events[$i].$events[1 + $i];
|
||||
if (!preg_match('/(?:^|\r\n|[\r\n])[^:\r\n]/', $content)) {
|
||||
yield new DataChunk(-1, $content);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$event = new ServerSentEvent($content);
|
||||
|
||||
if ('' !== $event->getId()) {
|
||||
$context->setInfo('last_event_id', $state->lastEventId = $event->getId());
|
||||
}
|
||||
|
||||
if ($event->getRetry()) {
|
||||
$context->setInfo('reconnection_time', $state->reconnectionTime = $event->getRetry());
|
||||
}
|
||||
|
||||
yield $event;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
24
vendor/symfony/http-client/Exception/ClientException.php
vendored
Normal file
24
vendor/symfony/http-client/Exception/ClientException.php
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
|
||||
/**
|
||||
* Represents a 4xx response.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class ClientException extends \RuntimeException implements ClientExceptionInterface
|
||||
{
|
||||
use HttpExceptionTrait;
|
||||
}
|
21
vendor/symfony/http-client/Exception/EventSourceException.php
vendored
Normal file
21
vendor/symfony/http-client/Exception/EventSourceException.php
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class EventSourceException extends \RuntimeException implements DecodingExceptionInterface
|
||||
{
|
||||
}
|
78
vendor/symfony/http-client/Exception/HttpExceptionTrait.php
vendored
Normal file
78
vendor/symfony/http-client/Exception/HttpExceptionTrait.php
vendored
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait HttpExceptionTrait
|
||||
{
|
||||
private $response;
|
||||
|
||||
public function __construct(ResponseInterface $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
$code = $response->getInfo('http_code');
|
||||
$url = $response->getInfo('url');
|
||||
$message = sprintf('HTTP %d returned for "%s".', $code, $url);
|
||||
|
||||
$httpCodeFound = false;
|
||||
$isJson = false;
|
||||
foreach (array_reverse($response->getInfo('response_headers')) as $h) {
|
||||
if (str_starts_with($h, 'HTTP/')) {
|
||||
if ($httpCodeFound) {
|
||||
break;
|
||||
}
|
||||
|
||||
$message = sprintf('%s returned for "%s".', $h, $url);
|
||||
$httpCodeFound = true;
|
||||
}
|
||||
|
||||
if (0 === stripos($h, 'content-type:')) {
|
||||
if (preg_match('/\bjson\b/i', $h)) {
|
||||
$isJson = true;
|
||||
}
|
||||
|
||||
if ($httpCodeFound) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to guess a better error message using common API error formats
|
||||
// The MIME type isn't explicitly checked because some formats inherit from others
|
||||
// Ex: JSON:API follows RFC 7807 semantics, Hydra can be used in any JSON-LD-compatible format
|
||||
if ($isJson && $body = json_decode($response->getContent(false), true)) {
|
||||
if (isset($body['hydra:title']) || isset($body['hydra:description'])) {
|
||||
// see http://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors
|
||||
$separator = isset($body['hydra:title'], $body['hydra:description']) ? "\n\n" : '';
|
||||
$message = ($body['hydra:title'] ?? '').$separator.($body['hydra:description'] ?? '');
|
||||
} elseif ((isset($body['title']) || isset($body['detail']))
|
||||
&& (\is_scalar($body['title'] ?? '') && \is_scalar($body['detail'] ?? ''))) {
|
||||
// see RFC 7807 and https://jsonapi.org/format/#error-objects
|
||||
$separator = isset($body['title'], $body['detail']) ? "\n\n" : '';
|
||||
$message = ($body['title'] ?? '').$separator.($body['detail'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($message, $code);
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
21
vendor/symfony/http-client/Exception/InvalidArgumentException.php
vendored
Normal file
21
vendor/symfony/http-client/Exception/InvalidArgumentException.php
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class InvalidArgumentException extends \InvalidArgumentException implements TransportExceptionInterface
|
||||
{
|
||||
}
|
23
vendor/symfony/http-client/Exception/JsonException.php
vendored
Normal file
23
vendor/symfony/http-client/Exception/JsonException.php
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
|
||||
/**
|
||||
* Thrown by responses' toArray() method when their content cannot be JSON-decoded.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class JsonException extends \JsonException implements DecodingExceptionInterface
|
||||
{
|
||||
}
|
24
vendor/symfony/http-client/Exception/RedirectionException.php
vendored
Normal file
24
vendor/symfony/http-client/Exception/RedirectionException.php
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
|
||||
/**
|
||||
* Represents a 3xx response.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class RedirectionException extends \RuntimeException implements RedirectionExceptionInterface
|
||||
{
|
||||
use HttpExceptionTrait;
|
||||
}
|
24
vendor/symfony/http-client/Exception/ServerException.php
vendored
Normal file
24
vendor/symfony/http-client/Exception/ServerException.php
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
|
||||
/**
|
||||
* Represents a 5xx response.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class ServerException extends \RuntimeException implements ServerExceptionInterface
|
||||
{
|
||||
use HttpExceptionTrait;
|
||||
}
|
21
vendor/symfony/http-client/Exception/TimeoutException.php
vendored
Normal file
21
vendor/symfony/http-client/Exception/TimeoutException.php
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class TimeoutException extends TransportException implements TimeoutExceptionInterface
|
||||
{
|
||||
}
|
21
vendor/symfony/http-client/Exception/TransportException.php
vendored
Normal file
21
vendor/symfony/http-client/Exception/TransportException.php
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class TransportException extends \RuntimeException implements TransportExceptionInterface
|
||||
{
|
||||
}
|
79
vendor/symfony/http-client/HttpClient.php
vendored
Normal file
79
vendor/symfony/http-client/HttpClient.php
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Amp\Http\Client\Connection\ConnectionLimitingPool;
|
||||
use Amp\Promise;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* A factory to instantiate the best possible HTTP client for the runtime.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class HttpClient
|
||||
{
|
||||
/**
|
||||
* @param array $defaultOptions Default request's options
|
||||
* @param int $maxHostConnections The maximum number of connections to a single host
|
||||
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
|
||||
*
|
||||
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
|
||||
*/
|
||||
public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
|
||||
{
|
||||
if ($amp = class_exists(ConnectionLimitingPool::class) && interface_exists(Promise::class)) {
|
||||
if (!\extension_loaded('curl')) {
|
||||
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
|
||||
}
|
||||
|
||||
// Skip curl when HTTP/2 push is unsupported or buggy, see https://bugs.php.net/77535
|
||||
if (\PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) || !\defined('CURLMOPT_PUSHFUNCTION')) {
|
||||
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
|
||||
}
|
||||
|
||||
static $curlVersion = null;
|
||||
$curlVersion = $curlVersion ?? curl_version();
|
||||
|
||||
// HTTP/2 push crashes before curl 7.61
|
||||
if (0x073D00 > $curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & $curlVersion['features'])) {
|
||||
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
|
||||
}
|
||||
}
|
||||
|
||||
if (\extension_loaded('curl')) {
|
||||
if ('\\' !== \DIRECTORY_SEPARATOR || isset($defaultOptions['cafile']) || isset($defaultOptions['capath']) || \ini_get('curl.cainfo') || \ini_get('openssl.cafile') || \ini_get('openssl.capath')) {
|
||||
return new CurlHttpClient($defaultOptions, $maxHostConnections, $maxPendingPushes);
|
||||
}
|
||||
|
||||
@trigger_error('Configure the "curl.cainfo", "openssl.cafile" or "openssl.capath" php.ini setting to enable the CurlHttpClient', \E_USER_WARNING);
|
||||
}
|
||||
|
||||
if ($amp) {
|
||||
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
|
||||
}
|
||||
|
||||
@trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^4.2.1" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
|
||||
|
||||
return new NativeHttpClient($defaultOptions, $maxHostConnections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a client that adds options (e.g. authentication headers) only when the request URL matches the provided base URI.
|
||||
*/
|
||||
public static function createForBaseUri(string $baseUri, array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
|
||||
{
|
||||
$client = self::create([], $maxHostConnections, $maxPendingPushes);
|
||||
|
||||
return ScopingHttpClient::forBaseUri($client, $baseUri, $defaultOptions);
|
||||
}
|
||||
}
|
719
vendor/symfony/http-client/HttpClientTrait.php
vendored
Normal file
719
vendor/symfony/http-client/HttpClientTrait.php
vendored
Normal file
|
@ -0,0 +1,719 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
|
||||
/**
|
||||
* Provides the common logic from writing HttpClientInterface implementations.
|
||||
*
|
||||
* All private methods are static to prevent implementers from creating memory leaks via circular references.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
trait HttpClientTrait
|
||||
{
|
||||
private static $CHUNK_SIZE = 16372;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function withOptions(array $options): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and normalizes method, URL and options, and merges them with defaults.
|
||||
*
|
||||
* @throws InvalidArgumentException When a not-supported option is found
|
||||
*/
|
||||
private static function prepareRequest(?string $method, ?string $url, array $options, array $defaultOptions = [], bool $allowExtraOptions = false): array
|
||||
{
|
||||
if (null !== $method) {
|
||||
if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid HTTP method "%s", only uppercase letters are accepted.', $method));
|
||||
}
|
||||
if (!$method) {
|
||||
throw new InvalidArgumentException('The HTTP method cannot be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
$options = self::mergeDefaultOptions($options, $defaultOptions, $allowExtraOptions);
|
||||
|
||||
$buffer = $options['buffer'] ?? true;
|
||||
|
||||
if ($buffer instanceof \Closure) {
|
||||
$options['buffer'] = static function (array $headers) use ($buffer) {
|
||||
if (!\is_bool($buffer = $buffer($headers))) {
|
||||
if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
|
||||
throw new \LogicException(sprintf('The closure passed as option "buffer" must return bool or stream resource, got "%s".', get_debug_type($buffer)));
|
||||
}
|
||||
|
||||
if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
|
||||
throw new \LogicException(sprintf('The stream returned by the closure passed as option "buffer" must be writeable, got mode "%s".', $bufferInfo['mode']));
|
||||
}
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
};
|
||||
} elseif (!\is_bool($buffer)) {
|
||||
if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
|
||||
throw new InvalidArgumentException(sprintf('Option "buffer" must be bool, stream resource or Closure, "%s" given.', get_debug_type($buffer)));
|
||||
}
|
||||
|
||||
if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
|
||||
throw new InvalidArgumentException(sprintf('The stream in option "buffer" must be writeable, mode "%s" given.', $bufferInfo['mode']));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['json'])) {
|
||||
if (isset($options['body']) && '' !== $options['body']) {
|
||||
throw new InvalidArgumentException('Define either the "json" or the "body" option, setting both is not supported.');
|
||||
}
|
||||
$options['body'] = self::jsonEncode($options['json']);
|
||||
unset($options['json']);
|
||||
|
||||
if (!isset($options['normalized_headers']['content-type'])) {
|
||||
$options['normalized_headers']['content-type'] = ['Content-Type: application/json'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($options['normalized_headers']['accept'])) {
|
||||
$options['normalized_headers']['accept'] = ['Accept: */*'];
|
||||
}
|
||||
|
||||
if (isset($options['body'])) {
|
||||
$options['body'] = self::normalizeBody($options['body']);
|
||||
|
||||
if (\is_string($options['body'])
|
||||
&& (string) \strlen($options['body']) !== substr($h = $options['normalized_headers']['content-length'][0] ?? '', 16)
|
||||
&& ('' !== $h || '' !== $options['body'])
|
||||
) {
|
||||
if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
|
||||
unset($options['normalized_headers']['transfer-encoding']);
|
||||
$options['body'] = self::dechunk($options['body']);
|
||||
}
|
||||
|
||||
$options['normalized_headers']['content-length'] = [substr_replace($h ?: 'Content-Length: ', \strlen($options['body']), 16)];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['peer_fingerprint'])) {
|
||||
$options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']);
|
||||
}
|
||||
|
||||
// Validate on_progress
|
||||
if (isset($options['on_progress']) && !\is_callable($onProgress = $options['on_progress'])) {
|
||||
throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress)));
|
||||
}
|
||||
|
||||
if (\is_array($options['auth_basic'] ?? null)) {
|
||||
$count = \count($options['auth_basic']);
|
||||
if ($count <= 0 || $count > 2) {
|
||||
throw new InvalidArgumentException(sprintf('Option "auth_basic" must contain 1 or 2 elements, "%s" given.', $count));
|
||||
}
|
||||
|
||||
$options['auth_basic'] = implode(':', $options['auth_basic']);
|
||||
}
|
||||
|
||||
if (!\is_string($options['auth_basic'] ?? '')) {
|
||||
throw new InvalidArgumentException(sprintf('Option "auth_basic" must be string or an array, "%s" given.', get_debug_type($options['auth_basic'])));
|
||||
}
|
||||
|
||||
if (isset($options['auth_bearer'])) {
|
||||
if (!\is_string($options['auth_bearer'])) {
|
||||
throw new InvalidArgumentException(sprintf('Option "auth_bearer" must be a string, "%s" given.', get_debug_type($options['auth_bearer'])));
|
||||
}
|
||||
if (preg_match('{[^\x21-\x7E]}', $options['auth_bearer'])) {
|
||||
throw new InvalidArgumentException('Invalid character found in option "auth_bearer": '.json_encode($options['auth_bearer']).'.');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['auth_basic'], $options['auth_bearer'])) {
|
||||
throw new InvalidArgumentException('Define either the "auth_basic" or the "auth_bearer" option, setting both is not supported.');
|
||||
}
|
||||
|
||||
if (null !== $url) {
|
||||
// Merge auth with headers
|
||||
if (($options['auth_basic'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
|
||||
$options['normalized_headers']['authorization'] = ['Authorization: Basic '.base64_encode($options['auth_basic'])];
|
||||
}
|
||||
// Merge bearer with headers
|
||||
if (($options['auth_bearer'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
|
||||
$options['normalized_headers']['authorization'] = ['Authorization: Bearer '.$options['auth_bearer']];
|
||||
}
|
||||
|
||||
unset($options['auth_basic'], $options['auth_bearer']);
|
||||
|
||||
// Parse base URI
|
||||
if (\is_string($options['base_uri'])) {
|
||||
$options['base_uri'] = self::parseUrl($options['base_uri']);
|
||||
}
|
||||
|
||||
// Validate and resolve URL
|
||||
$url = self::parseUrl($url, $options['query']);
|
||||
$url = self::resolveUrl($url, $options['base_uri'], $defaultOptions['query'] ?? []);
|
||||
}
|
||||
|
||||
// Finalize normalization of options
|
||||
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
|
||||
if (0 > $options['timeout'] = (float) ($options['timeout'] ?? \ini_get('default_socket_timeout'))) {
|
||||
$options['timeout'] = 172800.0; // 2 days
|
||||
}
|
||||
|
||||
$options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : 0;
|
||||
$options['headers'] = array_merge(...array_values($options['normalized_headers']));
|
||||
|
||||
return [$url, $options];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException When an invalid option is found
|
||||
*/
|
||||
private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array
|
||||
{
|
||||
$options['normalized_headers'] = self::normalizeHeaders($options['headers'] ?? []);
|
||||
|
||||
if ($defaultOptions['headers'] ?? false) {
|
||||
$options['normalized_headers'] += self::normalizeHeaders($defaultOptions['headers']);
|
||||
}
|
||||
|
||||
$options['headers'] = array_merge(...array_values($options['normalized_headers']) ?: [[]]);
|
||||
|
||||
if ($resolve = $options['resolve'] ?? false) {
|
||||
$options['resolve'] = [];
|
||||
foreach ($resolve as $k => $v) {
|
||||
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = (string) $v;
|
||||
}
|
||||
}
|
||||
|
||||
// Option "query" is never inherited from defaults
|
||||
$options['query'] = $options['query'] ?? [];
|
||||
|
||||
$options += $defaultOptions;
|
||||
|
||||
if (isset(self::$emptyDefaults)) {
|
||||
foreach (self::$emptyDefaults as $k => $v) {
|
||||
if (!isset($options[$k])) {
|
||||
$options[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($defaultOptions['extra'])) {
|
||||
$options['extra'] += $defaultOptions['extra'];
|
||||
}
|
||||
|
||||
if ($resolve = $defaultOptions['resolve'] ?? false) {
|
||||
foreach ($resolve as $k => $v) {
|
||||
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => (string) $v];
|
||||
}
|
||||
}
|
||||
|
||||
if ($allowExtraOptions || !$defaultOptions) {
|
||||
return $options;
|
||||
}
|
||||
|
||||
// Look for unsupported options
|
||||
foreach ($options as $name => $v) {
|
||||
if (\array_key_exists($name, $defaultOptions) || 'normalized_headers' === $name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('auth_ntlm' === $name) {
|
||||
if (!\extension_loaded('curl')) {
|
||||
$msg = 'try installing the "curl" extension to use "%s" instead.';
|
||||
} else {
|
||||
$msg = 'try using "%s" instead.';
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", '.$msg, __CLASS__, CurlHttpClient::class));
|
||||
}
|
||||
|
||||
$alternatives = [];
|
||||
|
||||
foreach ($defaultOptions as $k => $v) {
|
||||
if (levenshtein($name, $k) <= \strlen($name) / 3 || str_contains($k, $name)) {
|
||||
$alternatives[] = $k;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to "%s", did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions))));
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*
|
||||
* @throws InvalidArgumentException When an invalid header is found
|
||||
*/
|
||||
private static function normalizeHeaders(array $headers): array
|
||||
{
|
||||
$normalizedHeaders = [];
|
||||
|
||||
foreach ($headers as $name => $values) {
|
||||
if (\is_object($values) && method_exists($values, '__toString')) {
|
||||
$values = (string) $values;
|
||||
}
|
||||
|
||||
if (\is_int($name)) {
|
||||
if (!\is_string($values)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
|
||||
}
|
||||
[$name, $values] = explode(':', $values, 2);
|
||||
$values = [ltrim($values)];
|
||||
} elseif (!is_iterable($values)) {
|
||||
if (\is_object($values)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
|
||||
}
|
||||
|
||||
$values = (array) $values;
|
||||
}
|
||||
|
||||
$lcName = strtolower($name);
|
||||
$normalizedHeaders[$lcName] = [];
|
||||
|
||||
foreach ($values as $value) {
|
||||
$normalizedHeaders[$lcName][] = $value = $name.': '.$value;
|
||||
|
||||
if (\strlen($value) !== strcspn($value, "\r\n\0")) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid header: CR/LF/NUL found in "%s".', $value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $normalizedHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string|resource|\Traversable|\Closure $body
|
||||
*
|
||||
* @return string|resource|\Closure
|
||||
*
|
||||
* @throws InvalidArgumentException When an invalid body is passed
|
||||
*/
|
||||
private static function normalizeBody($body)
|
||||
{
|
||||
if (\is_array($body)) {
|
||||
array_walk_recursive($body, $caster = static function (&$v) use (&$caster) {
|
||||
if (\is_object($v)) {
|
||||
if ($vars = get_object_vars($v)) {
|
||||
array_walk_recursive($vars, $caster);
|
||||
$v = $vars;
|
||||
} elseif (method_exists($v, '__toString')) {
|
||||
$v = (string) $v;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return http_build_query($body, '', '&');
|
||||
}
|
||||
|
||||
if (\is_string($body)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$generatorToCallable = static function (\Generator $body): \Closure {
|
||||
return static function () use ($body) {
|
||||
while ($body->valid()) {
|
||||
$chunk = $body->current();
|
||||
$body->next();
|
||||
|
||||
if ('' !== $chunk) {
|
||||
return $chunk;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
};
|
||||
|
||||
if ($body instanceof \Generator) {
|
||||
return $generatorToCallable($body);
|
||||
}
|
||||
|
||||
if ($body instanceof \Traversable) {
|
||||
return $generatorToCallable((static function ($body) { yield from $body; })($body));
|
||||
}
|
||||
|
||||
if ($body instanceof \Closure) {
|
||||
$r = new \ReflectionFunction($body);
|
||||
$body = $r->getClosure();
|
||||
|
||||
if ($r->isGenerator()) {
|
||||
$body = $body(self::$CHUNK_SIZE);
|
||||
|
||||
return $generatorToCallable($body);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
if (!\is_array(@stream_get_meta_data($body))) {
|
||||
throw new InvalidArgumentException(sprintf('Option "body" must be string, stream resource, iterable or callable, "%s" given.', get_debug_type($body)));
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private static function dechunk(string $body): string
|
||||
{
|
||||
$h = fopen('php://temp', 'w+');
|
||||
stream_filter_append($h, 'dechunk', \STREAM_FILTER_WRITE);
|
||||
fwrite($h, $body);
|
||||
$body = stream_get_contents($h, -1, 0);
|
||||
rewind($h);
|
||||
ftruncate($h, 0);
|
||||
|
||||
if (fwrite($h, '-') && '' !== stream_get_contents($h, -1, 0)) {
|
||||
throw new TransportException('Request body has broken chunked encoding.');
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $fingerprint
|
||||
*
|
||||
* @throws InvalidArgumentException When an invalid fingerprint is passed
|
||||
*/
|
||||
private static function normalizePeerFingerprint($fingerprint): array
|
||||
{
|
||||
if (\is_string($fingerprint)) {
|
||||
switch (\strlen($fingerprint = str_replace(':', '', $fingerprint))) {
|
||||
case 32: $fingerprint = ['md5' => $fingerprint]; break;
|
||||
case 40: $fingerprint = ['sha1' => $fingerprint]; break;
|
||||
case 44: $fingerprint = ['pin-sha256' => [$fingerprint]]; break;
|
||||
case 64: $fingerprint = ['sha256' => $fingerprint]; break;
|
||||
default: throw new InvalidArgumentException(sprintf('Cannot auto-detect fingerprint algorithm for "%s".', $fingerprint));
|
||||
}
|
||||
} elseif (\is_array($fingerprint)) {
|
||||
foreach ($fingerprint as $algo => $hash) {
|
||||
$fingerprint[$algo] = 'pin-sha256' === $algo ? (array) $hash : str_replace(':', '', $hash);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException(sprintf('Option "peer_fingerprint" must be string or array, "%s" given.', get_debug_type($fingerprint)));
|
||||
}
|
||||
|
||||
return $fingerprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidArgumentException When the value cannot be json-encoded
|
||||
*/
|
||||
private static function jsonEncode($value, ?int $flags = null, int $maxDepth = 512): string
|
||||
{
|
||||
$flags = $flags ?? (\JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_PRESERVE_ZERO_FRACTION);
|
||||
|
||||
try {
|
||||
$value = json_encode($value, $flags | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0), $maxDepth);
|
||||
} catch (\JsonException $e) {
|
||||
throw new InvalidArgumentException('Invalid value for "json" option: '.$e->getMessage());
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error() && (false === $value || !($flags & \JSON_PARTIAL_OUTPUT_ON_ERROR))) {
|
||||
throw new InvalidArgumentException('Invalid value for "json" option: '.json_last_error_msg());
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a URL against a base URI.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-5.2.2
|
||||
*
|
||||
* @throws InvalidArgumentException When an invalid URL is passed
|
||||
*/
|
||||
private static function resolveUrl(array $url, ?array $base, array $queryDefaults = []): array
|
||||
{
|
||||
$givenUrl = $url;
|
||||
|
||||
if (null !== $base && '' === ($base['scheme'] ?? '').($base['authority'] ?? '')) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid "base_uri" option: host or scheme is missing in "%s".', implode('', $base)));
|
||||
}
|
||||
|
||||
if (null === $url['scheme'] && (null === $base || null === $base['scheme'])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid URL: scheme is missing in "%s". Did you forget to add "http(s)://"?', implode('', $base ?? $url)));
|
||||
}
|
||||
|
||||
if (null === $base && '' === $url['scheme'].$url['authority']) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid URL: no "base_uri" option was provided and host or scheme is missing in "%s".', implode('', $url)));
|
||||
}
|
||||
|
||||
if (null !== $url['scheme']) {
|
||||
$url['path'] = self::removeDotSegments($url['path'] ?? '');
|
||||
} else {
|
||||
if (null !== $url['authority']) {
|
||||
$url['path'] = self::removeDotSegments($url['path'] ?? '');
|
||||
} else {
|
||||
if (null === $url['path']) {
|
||||
$url['path'] = $base['path'];
|
||||
$url['query'] = $url['query'] ?? $base['query'];
|
||||
} else {
|
||||
if ('/' !== $url['path'][0]) {
|
||||
if (null === $base['path']) {
|
||||
$url['path'] = '/'.$url['path'];
|
||||
} else {
|
||||
$segments = explode('/', $base['path']);
|
||||
array_splice($segments, -1, 1, [$url['path']]);
|
||||
$url['path'] = implode('/', $segments);
|
||||
}
|
||||
}
|
||||
|
||||
$url['path'] = self::removeDotSegments($url['path']);
|
||||
}
|
||||
|
||||
$url['authority'] = $base['authority'];
|
||||
|
||||
if ($queryDefaults) {
|
||||
$url['query'] = '?'.self::mergeQueryString(substr($url['query'] ?? '', 1), $queryDefaults, false);
|
||||
}
|
||||
}
|
||||
|
||||
$url['scheme'] = $base['scheme'];
|
||||
}
|
||||
|
||||
if ('' === ($url['path'] ?? '')) {
|
||||
$url['path'] = '/';
|
||||
}
|
||||
|
||||
if ('?' === ($url['query'] ?? '')) {
|
||||
$url['query'] = null;
|
||||
}
|
||||
|
||||
if (null !== $url['scheme'] && null === $url['authority']) {
|
||||
throw new InvalidArgumentException(\sprintf('Invalid URL: host is missing in "%s".', implode('', $givenUrl)));
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a URL and fixes its encoding if needed.
|
||||
*
|
||||
* @throws InvalidArgumentException When an invalid URL is passed
|
||||
*/
|
||||
private static function parseUrl(string $url, array $query = [], array $allowedSchemes = ['http' => 80, 'https' => 443]): array
|
||||
{
|
||||
if (false === $parts = parse_url($url)) {
|
||||
if ('/' !== ($url[0] ?? '') || false === $parts = parse_url($url.'#')) {
|
||||
throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
|
||||
}
|
||||
unset($parts['fragment']);
|
||||
}
|
||||
|
||||
if ($query) {
|
||||
$parts['query'] = self::mergeQueryString($parts['query'] ?? null, $query, true);
|
||||
}
|
||||
|
||||
$port = $parts['port'] ?? 0;
|
||||
|
||||
if (null !== $scheme = $parts['scheme'] ?? null) {
|
||||
if (!isset($allowedSchemes[$scheme = strtolower($scheme)])) {
|
||||
throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s".', $url));
|
||||
}
|
||||
|
||||
$port = $allowedSchemes[$scheme] === $port ? 0 : $port;
|
||||
$scheme .= ':';
|
||||
}
|
||||
|
||||
if (null !== $host = $parts['host'] ?? null) {
|
||||
if (!\defined('INTL_IDNA_VARIANT_UTS46') && preg_match('/[\x80-\xFF]/', $host)) {
|
||||
throw new InvalidArgumentException(sprintf('Unsupported IDN "%s", try enabling the "intl" PHP extension or running "composer require symfony/polyfill-intl-idn".', $host));
|
||||
}
|
||||
|
||||
$host = \defined('INTL_IDNA_VARIANT_UTS46') ? idn_to_ascii($host, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46) ?: strtolower($host) : strtolower($host);
|
||||
$host .= $port ? ':'.$port : '';
|
||||
}
|
||||
|
||||
foreach (['user', 'pass', 'path', 'query', 'fragment'] as $part) {
|
||||
if (!isset($parts[$part])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($parts[$part], '%')) {
|
||||
// https://tools.ietf.org/html/rfc3986#section-2.3
|
||||
$parts[$part] = preg_replace_callback('/%(?:2[DE]|3[0-9]|[46][1-9A-F]|5F|[57][0-9A]|7E)++/i', function ($m) { return rawurldecode($m[0]); }, $parts[$part]);
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc3986#section-3.3
|
||||
$parts[$part] = preg_replace_callback("#[^-A-Za-z0-9._~!$&/'()[\]*+,;=:@{}%]++#", function ($m) { return rawurlencode($m[0]); }, $parts[$part]);
|
||||
}
|
||||
|
||||
return [
|
||||
'scheme' => $scheme,
|
||||
'authority' => null !== $host ? '//'.(isset($parts['user']) ? $parts['user'].(isset($parts['pass']) ? ':'.$parts['pass'] : '').'@' : '').$host : null,
|
||||
'path' => isset($parts['path'][0]) ? $parts['path'] : null,
|
||||
'query' => isset($parts['query']) ? '?'.$parts['query'] : null,
|
||||
'fragment' => isset($parts['fragment']) ? '#'.$parts['fragment'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes dot-segments from a path.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc3986#section-5.2.4
|
||||
*/
|
||||
private static function removeDotSegments(string $path)
|
||||
{
|
||||
$result = '';
|
||||
|
||||
while (!\in_array($path, ['', '.', '..'], true)) {
|
||||
if ('.' === $path[0] && (str_starts_with($path, $p = '../') || str_starts_with($path, $p = './'))) {
|
||||
$path = substr($path, \strlen($p));
|
||||
} elseif ('/.' === $path || str_starts_with($path, '/./')) {
|
||||
$path = substr_replace($path, '/', 0, 3);
|
||||
} elseif ('/..' === $path || str_starts_with($path, '/../')) {
|
||||
$i = strrpos($result, '/');
|
||||
$result = $i ? substr($result, 0, $i) : '';
|
||||
$path = substr_replace($path, '/', 0, 4);
|
||||
} else {
|
||||
$i = strpos($path, '/', 1) ?: \strlen($path);
|
||||
$result .= substr($path, 0, $i);
|
||||
$path = substr($path, $i);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges and encodes a query array with a query string.
|
||||
*
|
||||
* @throws InvalidArgumentException When an invalid query-string value is passed
|
||||
*/
|
||||
private static function mergeQueryString(?string $queryString, array $queryArray, bool $replace): ?string
|
||||
{
|
||||
if (!$queryArray) {
|
||||
return $queryString;
|
||||
}
|
||||
|
||||
$query = [];
|
||||
|
||||
if (null !== $queryString) {
|
||||
foreach (explode('&', $queryString) as $v) {
|
||||
if ('' !== $v) {
|
||||
$k = urldecode(explode('=', $v, 2)[0]);
|
||||
$query[$k] = (isset($query[$k]) ? $query[$k].'&' : '').$v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($replace) {
|
||||
foreach ($queryArray as $k => $v) {
|
||||
if (null === $v) {
|
||||
unset($query[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$queryString = http_build_query($queryArray, '', '&', \PHP_QUERY_RFC3986);
|
||||
$queryArray = [];
|
||||
|
||||
if ($queryString) {
|
||||
if (str_contains($queryString, '%')) {
|
||||
// https://tools.ietf.org/html/rfc3986#section-2.3 + some chars not encoded by browsers
|
||||
$queryString = strtr($queryString, [
|
||||
'%21' => '!',
|
||||
'%24' => '$',
|
||||
'%28' => '(',
|
||||
'%29' => ')',
|
||||
'%2A' => '*',
|
||||
'%2F' => '/',
|
||||
'%3A' => ':',
|
||||
'%3B' => ';',
|
||||
'%40' => '@',
|
||||
'%5B' => '[',
|
||||
'%5D' => ']',
|
||||
]);
|
||||
}
|
||||
|
||||
foreach (explode('&', $queryString) as $v) {
|
||||
$queryArray[rawurldecode(explode('=', $v, 2)[0])] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return implode('&', $replace ? array_replace($query, $queryArray) : ($query + $queryArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads proxy configuration from the same environment variables as curl when no proxy is explicitly set.
|
||||
*/
|
||||
private static function getProxy(?string $proxy, array $url, ?string $noProxy): ?array
|
||||
{
|
||||
if (null === $proxy = self::getProxyUrl($proxy, $url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$proxy = (parse_url($proxy) ?: []) + ['scheme' => 'http'];
|
||||
|
||||
if (!isset($proxy['host'])) {
|
||||
throw new TransportException('Invalid HTTP proxy: host is missing.');
|
||||
}
|
||||
|
||||
if ('http' === $proxy['scheme']) {
|
||||
$proxyUrl = 'tcp://'.$proxy['host'].':'.($proxy['port'] ?? '80');
|
||||
} elseif ('https' === $proxy['scheme']) {
|
||||
$proxyUrl = 'ssl://'.$proxy['host'].':'.($proxy['port'] ?? '443');
|
||||
} else {
|
||||
throw new TransportException(sprintf('Unsupported proxy scheme "%s": "http" or "https" expected.', $proxy['scheme']));
|
||||
}
|
||||
|
||||
$noProxy = $noProxy ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '';
|
||||
$noProxy = $noProxy ? preg_split('/[\s,]+/', $noProxy) : [];
|
||||
|
||||
return [
|
||||
'url' => $proxyUrl,
|
||||
'auth' => isset($proxy['user']) ? 'Basic '.base64_encode(rawurldecode($proxy['user']).':'.rawurldecode($proxy['pass'] ?? '')) : null,
|
||||
'no_proxy' => $noProxy,
|
||||
];
|
||||
}
|
||||
|
||||
private static function getProxyUrl(?string $proxy, array $url): ?string
|
||||
{
|
||||
if (null !== $proxy) {
|
||||
return $proxy;
|
||||
}
|
||||
|
||||
// Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities
|
||||
$proxy = $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null;
|
||||
|
||||
if ('https:' === $url['scheme']) {
|
||||
$proxy = $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? $proxy;
|
||||
}
|
||||
|
||||
return $proxy;
|
||||
}
|
||||
|
||||
private static function shouldBuffer(array $headers): bool
|
||||
{
|
||||
if (null === $contentType = $headers['content-type'][0] ?? null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false !== $i = strpos($contentType, ';')) {
|
||||
$contentType = substr($contentType, 0, $i);
|
||||
}
|
||||
|
||||
return $contentType && preg_match('#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', $contentType);
|
||||
}
|
||||
}
|
331
vendor/symfony/http-client/HttpOptions.php
vendored
Normal file
331
vendor/symfony/http-client/HttpOptions.php
vendored
Normal file
|
@ -0,0 +1,331 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* A helper providing autocompletion for available options.
|
||||
*
|
||||
* @see HttpClientInterface for a description of each options.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class HttpOptions
|
||||
{
|
||||
private $options = [];
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setAuthBasic(string $user, string $password = '')
|
||||
{
|
||||
$this->options['auth_basic'] = $user;
|
||||
|
||||
if ('' !== $password) {
|
||||
$this->options['auth_basic'] .= ':'.$password;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setAuthBearer(string $token)
|
||||
{
|
||||
$this->options['auth_bearer'] = $token;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setQuery(array $query)
|
||||
{
|
||||
$this->options['query'] = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setHeaders(iterable $headers)
|
||||
{
|
||||
$this->options['headers'] = $headers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string|resource|\Traversable|\Closure $body
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBody($body)
|
||||
{
|
||||
$this->options['body'] = $body;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $json
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setJson($json)
|
||||
{
|
||||
$this->options['json'] = $json;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setUserData($data)
|
||||
{
|
||||
$this->options['user_data'] = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setMaxRedirects(int $max)
|
||||
{
|
||||
$this->options['max_redirects'] = $max;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setHttpVersion(string $version)
|
||||
{
|
||||
$this->options['http_version'] = $version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setBaseUri(string $uri)
|
||||
{
|
||||
$this->options['base_uri'] = $uri;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function buffer(bool $buffer)
|
||||
{
|
||||
$this->options['buffer'] = $buffer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setOnProgress(callable $callback)
|
||||
{
|
||||
$this->options['on_progress'] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function resolve(array $hostIps)
|
||||
{
|
||||
$this->options['resolve'] = $hostIps;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setProxy(string $proxy)
|
||||
{
|
||||
$this->options['proxy'] = $proxy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setNoProxy(string $noProxy)
|
||||
{
|
||||
$this->options['no_proxy'] = $noProxy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setTimeout(float $timeout)
|
||||
{
|
||||
$this->options['timeout'] = $timeout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setMaxDuration(float $maxDuration)
|
||||
{
|
||||
$this->options['max_duration'] = $maxDuration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function bindTo(string $bindto)
|
||||
{
|
||||
$this->options['bindto'] = $bindto;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function verifyPeer(bool $verify)
|
||||
{
|
||||
$this->options['verify_peer'] = $verify;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function verifyHost(bool $verify)
|
||||
{
|
||||
$this->options['verify_host'] = $verify;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCaFile(string $cafile)
|
||||
{
|
||||
$this->options['cafile'] = $cafile;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCaPath(string $capath)
|
||||
{
|
||||
$this->options['capath'] = $capath;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocalCert(string $cert)
|
||||
{
|
||||
$this->options['local_cert'] = $cert;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocalPk(string $pk)
|
||||
{
|
||||
$this->options['local_pk'] = $pk;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setPassphrase(string $passphrase)
|
||||
{
|
||||
$this->options['passphrase'] = $passphrase;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCiphers(string $ciphers)
|
||||
{
|
||||
$this->options['ciphers'] = $ciphers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $fingerprint
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPeerFingerprint($fingerprint)
|
||||
{
|
||||
$this->options['peer_fingerprint'] = $fingerprint;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function capturePeerCertChain(bool $capture)
|
||||
{
|
||||
$this->options['capture_peer_cert_chain'] = $capture;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setExtra(string $name, $value)
|
||||
{
|
||||
$this->options['extra'][$name] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
276
vendor/symfony/http-client/HttplugClient.php
vendored
Normal file
276
vendor/symfony/http-client/HttplugClient.php
vendored
Normal file
|
@ -0,0 +1,276 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use GuzzleHttp\Promise\Promise as GuzzlePromise;
|
||||
use GuzzleHttp\Promise\RejectedPromise;
|
||||
use GuzzleHttp\Promise\Utils;
|
||||
use Http\Client\Exception\NetworkException;
|
||||
use Http\Client\Exception\RequestException;
|
||||
use Http\Client\HttpAsyncClient;
|
||||
use Http\Client\HttpClient as HttplugInterface;
|
||||
use Http\Discovery\Exception\NotFoundException;
|
||||
use Http\Discovery\Psr17FactoryDiscovery;
|
||||
use Http\Message\RequestFactory;
|
||||
use Http\Message\StreamFactory;
|
||||
use Http\Message\UriFactory;
|
||||
use Http\Promise\Promise;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Nyholm\Psr7\Request;
|
||||
use Nyholm\Psr7\Uri;
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriFactoryInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
|
||||
use Symfony\Component\HttpClient\Response\HttplugPromise;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
if (!interface_exists(HttplugInterface::class)) {
|
||||
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/httplug" package is not installed. Try running "composer require php-http/httplug".');
|
||||
}
|
||||
|
||||
if (!interface_exists(RequestFactory::class)) {
|
||||
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory".');
|
||||
}
|
||||
|
||||
/**
|
||||
* An adapter to turn a Symfony HttpClientInterface into an Httplug client.
|
||||
*
|
||||
* Run "composer require nyholm/psr7" to install an efficient implementation of response
|
||||
* and stream factories with flex-provided autowiring aliases.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestFactory, StreamFactory, UriFactory, ResetInterface
|
||||
{
|
||||
private $client;
|
||||
private $responseFactory;
|
||||
private $streamFactory;
|
||||
|
||||
/**
|
||||
* @var \SplObjectStorage<ResponseInterface, array{RequestInterface, Promise}>|null
|
||||
*/
|
||||
private $promisePool;
|
||||
|
||||
private $waitLoop;
|
||||
|
||||
public function __construct(?HttpClientInterface $client = null, ?ResponseFactoryInterface $responseFactory = null, ?StreamFactoryInterface $streamFactory = null)
|
||||
{
|
||||
$this->client = $client ?? HttpClient::create();
|
||||
$this->responseFactory = $responseFactory;
|
||||
$this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
|
||||
$this->promisePool = class_exists(Utils::class) ? new \SplObjectStorage() : null;
|
||||
|
||||
if (null === $this->responseFactory || null === $this->streamFactory) {
|
||||
if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
|
||||
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
|
||||
}
|
||||
|
||||
try {
|
||||
$psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
|
||||
$this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
|
||||
$this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
|
||||
} catch (NotFoundException $e) {
|
||||
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
$this->waitLoop = new HttplugWaitLoop($this->client, $this->promisePool, $this->responseFactory, $this->streamFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function sendRequest(RequestInterface $request): Psr7ResponseInterface
|
||||
{
|
||||
try {
|
||||
return HttplugWaitLoop::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $this->sendPsr7Request($request), true);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new NetworkException($e->getMessage(), $request, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return HttplugPromise
|
||||
*/
|
||||
public function sendAsyncRequest(RequestInterface $request): Promise
|
||||
{
|
||||
if (!$promisePool = $this->promisePool) {
|
||||
throw new \LogicException(sprintf('You cannot use "%s()" as the "guzzlehttp/promises" package is not installed. Try running "composer require guzzlehttp/promises".', __METHOD__));
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->sendPsr7Request($request, true);
|
||||
} catch (NetworkException $e) {
|
||||
return new HttplugPromise(new RejectedPromise($e));
|
||||
}
|
||||
|
||||
$waitLoop = $this->waitLoop;
|
||||
|
||||
$promise = new GuzzlePromise(static function () use ($response, $waitLoop) {
|
||||
$waitLoop->wait($response);
|
||||
}, static function () use ($response, $promisePool) {
|
||||
$response->cancel();
|
||||
unset($promisePool[$response]);
|
||||
});
|
||||
|
||||
$promisePool[$response] = [$request, $promise];
|
||||
|
||||
return new HttplugPromise($promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves pending promises that complete before the timeouts are reached.
|
||||
*
|
||||
* When $maxDuration is null and $idleTimeout is reached, promises are rejected.
|
||||
*
|
||||
* @return int The number of remaining pending promises
|
||||
*/
|
||||
public function wait(?float $maxDuration = null, ?float $idleTimeout = null): int
|
||||
{
|
||||
return $this->waitLoop->wait(null, $maxDuration, $idleTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface
|
||||
{
|
||||
if ($this->responseFactory instanceof RequestFactoryInterface) {
|
||||
$request = $this->responseFactory->createRequest($method, $uri);
|
||||
} elseif (class_exists(Request::class)) {
|
||||
$request = new Request($method, $uri);
|
||||
} elseif (class_exists(Psr17FactoryDiscovery::class)) {
|
||||
$request = Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
|
||||
} else {
|
||||
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
|
||||
}
|
||||
|
||||
$request = $request
|
||||
->withProtocolVersion($protocolVersion)
|
||||
->withBody($this->createStream($body))
|
||||
;
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
$request = $request->withAddedHeader($name, $value);
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createStream($body = null): StreamInterface
|
||||
{
|
||||
if ($body instanceof StreamInterface) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
if (\is_string($body ?? '')) {
|
||||
$stream = $this->streamFactory->createStream($body ?? '');
|
||||
} elseif (\is_resource($body)) {
|
||||
$stream = $this->streamFactory->createStreamFromResource($body);
|
||||
} else {
|
||||
throw new \InvalidArgumentException(sprintf('"%s()" expects string, resource or StreamInterface, "%s" given.', __METHOD__, get_debug_type($body)));
|
||||
}
|
||||
|
||||
if ($stream->isSeekable()) {
|
||||
$stream->seek(0);
|
||||
}
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createUri($uri): UriInterface
|
||||
{
|
||||
if ($uri instanceof UriInterface) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
if ($this->responseFactory instanceof UriFactoryInterface) {
|
||||
return $this->responseFactory->createUri($uri);
|
||||
}
|
||||
|
||||
if (class_exists(Uri::class)) {
|
||||
return new Uri($uri);
|
||||
}
|
||||
|
||||
if (class_exists(Psr17FactoryDiscovery::class)) {
|
||||
return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
|
||||
}
|
||||
|
||||
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->wait();
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if ($this->client instanceof ResetInterface) {
|
||||
$this->client->reset();
|
||||
}
|
||||
}
|
||||
|
||||
private function sendPsr7Request(RequestInterface $request, ?bool $buffer = null): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$body = $request->getBody();
|
||||
|
||||
if ($body->isSeekable()) {
|
||||
$body->seek(0);
|
||||
}
|
||||
|
||||
$options = [
|
||||
'headers' => $request->getHeaders(),
|
||||
'body' => $body->getContents(),
|
||||
'buffer' => $buffer,
|
||||
];
|
||||
|
||||
if ('1.0' === $request->getProtocolVersion()) {
|
||||
$options['http_version'] = '1.0';
|
||||
}
|
||||
|
||||
return $this->client->request($request->getMethod(), (string) $request->getUri(), $options);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new RequestException($e->getMessage(), $request, $e);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
throw new NetworkException($e->getMessage(), $request, $e);
|
||||
}
|
||||
}
|
||||
}
|
142
vendor/symfony/http-client/Internal/AmpBody.php
vendored
Normal file
142
vendor/symfony/http-client/Internal/AmpBody.php
vendored
Normal file
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
use Amp\ByteStream\InputStream;
|
||||
use Amp\ByteStream\ResourceInputStream;
|
||||
use Amp\Http\Client\RequestBody;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AmpBody implements RequestBody, InputStream
|
||||
{
|
||||
private $body;
|
||||
private $info;
|
||||
private $onProgress;
|
||||
private $offset = 0;
|
||||
private $length = -1;
|
||||
private $uploaded;
|
||||
|
||||
public function __construct($body, &$info, \Closure $onProgress)
|
||||
{
|
||||
$this->body = $body;
|
||||
$this->info = &$info;
|
||||
$this->onProgress = $onProgress;
|
||||
|
||||
if (\is_resource($body)) {
|
||||
$this->offset = ftell($body);
|
||||
$this->length = fstat($body)['size'];
|
||||
$this->body = new ResourceInputStream($body);
|
||||
} elseif (\is_string($body)) {
|
||||
$this->length = \strlen($body);
|
||||
}
|
||||
}
|
||||
|
||||
public function createBodyStream(): InputStream
|
||||
{
|
||||
if (null !== $this->uploaded) {
|
||||
$this->uploaded = null;
|
||||
|
||||
if (\is_string($this->body)) {
|
||||
$this->offset = 0;
|
||||
} elseif ($this->body instanceof ResourceInputStream) {
|
||||
fseek($this->body->getResource(), $this->offset);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeaders(): Promise
|
||||
{
|
||||
return new Success([]);
|
||||
}
|
||||
|
||||
public function getBodyLength(): Promise
|
||||
{
|
||||
return new Success($this->length - $this->offset);
|
||||
}
|
||||
|
||||
public function read(): Promise
|
||||
{
|
||||
$this->info['size_upload'] += $this->uploaded;
|
||||
$this->uploaded = 0;
|
||||
($this->onProgress)();
|
||||
|
||||
$chunk = $this->doRead();
|
||||
$chunk->onResolve(function ($e, $data) {
|
||||
if (null !== $data) {
|
||||
$this->uploaded = \strlen($data);
|
||||
} else {
|
||||
$this->info['upload_content_length'] = $this->info['size_upload'];
|
||||
}
|
||||
});
|
||||
|
||||
return $chunk;
|
||||
}
|
||||
|
||||
public static function rewind(RequestBody $body): RequestBody
|
||||
{
|
||||
if (!$body instanceof self) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$body->uploaded = null;
|
||||
|
||||
if ($body->body instanceof ResourceInputStream) {
|
||||
fseek($body->body->getResource(), $body->offset);
|
||||
|
||||
return new $body($body->body, $body->info, $body->onProgress);
|
||||
}
|
||||
|
||||
if (\is_string($body->body)) {
|
||||
$body->offset = 0;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function doRead(): Promise
|
||||
{
|
||||
if ($this->body instanceof ResourceInputStream) {
|
||||
return $this->body->read();
|
||||
}
|
||||
|
||||
if (null === $this->offset || !$this->length) {
|
||||
return new Success();
|
||||
}
|
||||
|
||||
if (\is_string($this->body)) {
|
||||
$this->offset = null;
|
||||
|
||||
return new Success($this->body);
|
||||
}
|
||||
|
||||
if ('' === $data = ($this->body)(16372)) {
|
||||
$this->offset = null;
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
if (!\is_string($data)) {
|
||||
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
|
||||
}
|
||||
|
||||
return new Success($data);
|
||||
}
|
||||
}
|
217
vendor/symfony/http-client/Internal/AmpClientState.php
vendored
Normal file
217
vendor/symfony/http-client/Internal/AmpClientState.php
vendored
Normal file
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
use Amp\CancellationToken;
|
||||
use Amp\Deferred;
|
||||
use Amp\Http\Client\Connection\ConnectionLimitingPool;
|
||||
use Amp\Http\Client\Connection\DefaultConnectionFactory;
|
||||
use Amp\Http\Client\InterceptedHttpClient;
|
||||
use Amp\Http\Client\Interceptor\RetryRequests;
|
||||
use Amp\Http\Client\PooledHttpClient;
|
||||
use Amp\Http\Client\Request;
|
||||
use Amp\Http\Client\Response;
|
||||
use Amp\Http\Tunnel\Http1TunnelConnector;
|
||||
use Amp\Http\Tunnel\Https1TunnelConnector;
|
||||
use Amp\Promise;
|
||||
use Amp\Socket\Certificate;
|
||||
use Amp\Socket\ClientTlsContext;
|
||||
use Amp\Socket\ConnectContext;
|
||||
use Amp\Socket\Connector;
|
||||
use Amp\Socket\DnsConnector;
|
||||
use Amp\Socket\SocketAddress;
|
||||
use Amp\Success;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Internal representation of the Amp client's state.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class AmpClientState extends ClientState
|
||||
{
|
||||
public $dnsCache = [];
|
||||
public $responseCount = 0;
|
||||
public $pushedResponses = [];
|
||||
|
||||
private $clients = [];
|
||||
private $clientConfigurator;
|
||||
private $maxHostConnections;
|
||||
private $maxPendingPushes;
|
||||
private $logger;
|
||||
|
||||
public function __construct(?callable $clientConfigurator, int $maxHostConnections, int $maxPendingPushes, ?LoggerInterface &$logger)
|
||||
{
|
||||
$this->clientConfigurator = $clientConfigurator ?? static function (PooledHttpClient $client) {
|
||||
return new InterceptedHttpClient($client, new RetryRequests(2));
|
||||
};
|
||||
$this->maxHostConnections = $maxHostConnections;
|
||||
$this->maxPendingPushes = $maxPendingPushes;
|
||||
$this->logger = &$logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Promise<Response>
|
||||
*/
|
||||
public function request(array $options, Request $request, CancellationToken $cancellation, array &$info, \Closure $onProgress, &$handle): Promise
|
||||
{
|
||||
if ($options['proxy']) {
|
||||
if ($request->hasHeader('proxy-authorization')) {
|
||||
$options['proxy']['auth'] = $request->getHeader('proxy-authorization');
|
||||
}
|
||||
|
||||
// Matching "no_proxy" should follow the behavior of curl
|
||||
$host = $request->getUri()->getHost();
|
||||
foreach ($options['proxy']['no_proxy'] as $rule) {
|
||||
$dotRule = '.'.ltrim($rule, '.');
|
||||
|
||||
if ('*' === $rule || $host === $rule || substr($host, -\strlen($dotRule)) === $dotRule) {
|
||||
$options['proxy'] = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$request = clone $request;
|
||||
|
||||
if ($request->hasHeader('proxy-authorization')) {
|
||||
$request->removeHeader('proxy-authorization');
|
||||
}
|
||||
|
||||
if ($options['capture_peer_cert_chain']) {
|
||||
$info['peer_certificate_chain'] = [];
|
||||
}
|
||||
|
||||
$request->addEventListener(new AmpListener($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle));
|
||||
$request->setPushHandler(function ($request, $response) use ($options): Promise {
|
||||
return $this->handlePush($request, $response, $options);
|
||||
});
|
||||
|
||||
($request->hasHeader('content-length') ? new Success((int) $request->getHeader('content-length')) : $request->getBody()->getBodyLength())
|
||||
->onResolve(static function ($e, $bodySize) use (&$info) {
|
||||
if (null !== $bodySize && 0 <= $bodySize) {
|
||||
$info['upload_content_length'] = ((1 + $info['upload_content_length']) ?? 1) - 1 + $bodySize;
|
||||
}
|
||||
});
|
||||
|
||||
[$client, $connector] = $this->getClient($options);
|
||||
$response = $client->request($request, $cancellation);
|
||||
$response->onResolve(static function ($e) use ($connector, &$handle) {
|
||||
if (null === $e) {
|
||||
$handle = $connector->handle;
|
||||
}
|
||||
});
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function getClient(array $options): array
|
||||
{
|
||||
$options = [
|
||||
'bindto' => $options['bindto'] ?: '0',
|
||||
'verify_peer' => $options['verify_peer'],
|
||||
'capath' => $options['capath'],
|
||||
'cafile' => $options['cafile'],
|
||||
'local_cert' => $options['local_cert'],
|
||||
'local_pk' => $options['local_pk'],
|
||||
'ciphers' => $options['ciphers'],
|
||||
'capture_peer_cert_chain' => $options['capture_peer_cert_chain'] || $options['peer_fingerprint'],
|
||||
'proxy' => $options['proxy'],
|
||||
];
|
||||
|
||||
$key = md5(serialize($options));
|
||||
|
||||
if (isset($this->clients[$key])) {
|
||||
return $this->clients[$key];
|
||||
}
|
||||
|
||||
$context = new ClientTlsContext('');
|
||||
$options['verify_peer'] || $context = $context->withoutPeerVerification();
|
||||
$options['cafile'] && $context = $context->withCaFile($options['cafile']);
|
||||
$options['capath'] && $context = $context->withCaPath($options['capath']);
|
||||
$options['local_cert'] && $context = $context->withCertificate(new Certificate($options['local_cert'], $options['local_pk']));
|
||||
$options['ciphers'] && $context = $context->withCiphers($options['ciphers']);
|
||||
$options['capture_peer_cert_chain'] && $context = $context->withPeerCapturing();
|
||||
|
||||
$connector = $handleConnector = new class() implements Connector {
|
||||
public $connector;
|
||||
public $uri;
|
||||
public $handle;
|
||||
|
||||
public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null): Promise
|
||||
{
|
||||
$result = $this->connector->connect($this->uri ?? $uri, $context, $token);
|
||||
$result->onResolve(function ($e, $socket) {
|
||||
$this->handle = null !== $socket ? $socket->getResource() : false;
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
};
|
||||
$connector->connector = new DnsConnector(new AmpResolver($this->dnsCache));
|
||||
|
||||
$context = (new ConnectContext())
|
||||
->withTcpNoDelay()
|
||||
->withTlsContext($context);
|
||||
|
||||
if ($options['bindto']) {
|
||||
if (file_exists($options['bindto'])) {
|
||||
$connector->uri = 'unix://'.$options['bindto'];
|
||||
} else {
|
||||
$context = $context->withBindTo($options['bindto']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($options['proxy']) {
|
||||
$proxyUrl = parse_url($options['proxy']['url']);
|
||||
$proxySocket = new SocketAddress($proxyUrl['host'], $proxyUrl['port']);
|
||||
$proxyHeaders = $options['proxy']['auth'] ? ['Proxy-Authorization' => $options['proxy']['auth']] : [];
|
||||
|
||||
if ('ssl' === $proxyUrl['scheme']) {
|
||||
$connector = new Https1TunnelConnector($proxySocket, $context->getTlsContext(), $proxyHeaders, $connector);
|
||||
} else {
|
||||
$connector = new Http1TunnelConnector($proxySocket, $proxyHeaders, $connector);
|
||||
}
|
||||
}
|
||||
|
||||
$maxHostConnections = 0 < $this->maxHostConnections ? $this->maxHostConnections : \PHP_INT_MAX;
|
||||
$pool = new DefaultConnectionFactory($connector, $context);
|
||||
$pool = ConnectionLimitingPool::byAuthority($maxHostConnections, $pool);
|
||||
|
||||
return $this->clients[$key] = [($this->clientConfigurator)(new PooledHttpClient($pool)), $handleConnector];
|
||||
}
|
||||
|
||||
private function handlePush(Request $request, Promise $response, array $options): Promise
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
$authority = $request->getUri()->getAuthority();
|
||||
|
||||
if ($this->maxPendingPushes <= \count($this->pushedResponses[$authority] ?? [])) {
|
||||
$fifoUrl = key($this->pushedResponses[$authority]);
|
||||
unset($this->pushedResponses[$authority][$fifoUrl]);
|
||||
$this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
|
||||
}
|
||||
|
||||
$url = (string) $request->getUri();
|
||||
$this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
|
||||
$this->pushedResponses[$authority][] = [$url, $deferred, $request, $response, [
|
||||
'proxy' => $options['proxy'],
|
||||
'bindto' => $options['bindto'],
|
||||
'local_cert' => $options['local_cert'],
|
||||
'local_pk' => $options['local_pk'],
|
||||
]];
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
183
vendor/symfony/http-client/Internal/AmpListener.php
vendored
Normal file
183
vendor/symfony/http-client/Internal/AmpListener.php
vendored
Normal file
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
use Amp\Http\Client\Connection\Stream;
|
||||
use Amp\Http\Client\EventListener;
|
||||
use Amp\Http\Client\Request;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AmpListener implements EventListener
|
||||
{
|
||||
private $info;
|
||||
private $pinSha256;
|
||||
private $onProgress;
|
||||
private $handle;
|
||||
|
||||
public function __construct(array &$info, array $pinSha256, \Closure $onProgress, &$handle)
|
||||
{
|
||||
$info += [
|
||||
'connect_time' => 0.0,
|
||||
'pretransfer_time' => 0.0,
|
||||
'starttransfer_time' => 0.0,
|
||||
'total_time' => 0.0,
|
||||
'namelookup_time' => 0.0,
|
||||
'primary_ip' => '',
|
||||
'primary_port' => 0,
|
||||
];
|
||||
|
||||
$this->info = &$info;
|
||||
$this->pinSha256 = $pinSha256;
|
||||
$this->onProgress = $onProgress;
|
||||
$this->handle = &$handle;
|
||||
}
|
||||
|
||||
public function startRequest(Request $request): Promise
|
||||
{
|
||||
$this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function startDnsResolution(Request $request): Promise
|
||||
{
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function startConnectionCreation(Request $request): Promise
|
||||
{
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function startTlsNegotiation(Request $request): Promise
|
||||
{
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function startSendingRequest(Request $request, Stream $stream): Promise
|
||||
{
|
||||
$host = $stream->getRemoteAddress()->getHost();
|
||||
|
||||
if (false !== strpos($host, ':')) {
|
||||
$host = '['.$host.']';
|
||||
}
|
||||
|
||||
$this->info['primary_ip'] = $host;
|
||||
$this->info['primary_port'] = $stream->getRemoteAddress()->getPort();
|
||||
$this->info['pretransfer_time'] = microtime(true) - $this->info['start_time'];
|
||||
$this->info['debug'] .= sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']);
|
||||
|
||||
if ((isset($this->info['peer_certificate_chain']) || $this->pinSha256) && null !== $tlsInfo = $stream->getTlsInfo()) {
|
||||
foreach ($tlsInfo->getPeerCertificates() as $cert) {
|
||||
$this->info['peer_certificate_chain'][] = openssl_x509_read($cert->toPem());
|
||||
}
|
||||
|
||||
if ($this->pinSha256) {
|
||||
$pin = openssl_pkey_get_public($this->info['peer_certificate_chain'][0]);
|
||||
$pin = openssl_pkey_get_details($pin)['key'];
|
||||
$pin = \array_slice(explode("\n", $pin), 1, -2);
|
||||
$pin = base64_decode(implode('', $pin));
|
||||
$pin = base64_encode(hash('sha256', $pin, true));
|
||||
|
||||
if (!\in_array($pin, $this->pinSha256, true)) {
|
||||
throw new TransportException(sprintf('SSL public key does not match pinned public key for "%s".', $this->info['url']));
|
||||
}
|
||||
}
|
||||
}
|
||||
($this->onProgress)();
|
||||
|
||||
$uri = $request->getUri();
|
||||
$requestUri = $uri->getPath() ?: '/';
|
||||
|
||||
if ('' !== $query = $uri->getQuery()) {
|
||||
$requestUri .= '?'.$query;
|
||||
}
|
||||
|
||||
if ('CONNECT' === $method = $request->getMethod()) {
|
||||
$requestUri = $uri->getHost().': '.($uri->getPort() ?? ('https' === $uri->getScheme() ? 443 : 80));
|
||||
}
|
||||
|
||||
$this->info['debug'] .= sprintf("> %s %s HTTP/%s \r\n", $method, $requestUri, $request->getProtocolVersions()[0]);
|
||||
|
||||
foreach ($request->getRawHeaders() as [$name, $value]) {
|
||||
$this->info['debug'] .= $name.': '.$value."\r\n";
|
||||
}
|
||||
$this->info['debug'] .= "\r\n";
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function completeSendingRequest(Request $request, Stream $stream): Promise
|
||||
{
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function startReceivingResponse(Request $request, Stream $stream): Promise
|
||||
{
|
||||
$this->info['starttransfer_time'] = microtime(true) - $this->info['start_time'];
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function completeReceivingResponse(Request $request, Stream $stream): Promise
|
||||
{
|
||||
$this->handle = null;
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function completeDnsResolution(Request $request): Promise
|
||||
{
|
||||
$this->info['namelookup_time'] = microtime(true) - $this->info['start_time'];
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function completeConnectionCreation(Request $request): Promise
|
||||
{
|
||||
$this->info['connect_time'] = microtime(true) - $this->info['start_time'];
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function completeTlsNegotiation(Request $request): Promise
|
||||
{
|
||||
($this->onProgress)();
|
||||
|
||||
return new Success();
|
||||
}
|
||||
|
||||
public function abort(Request $request, \Throwable $cause): Promise
|
||||
{
|
||||
return new Success();
|
||||
}
|
||||
}
|
52
vendor/symfony/http-client/Internal/AmpResolver.php
vendored
Normal file
52
vendor/symfony/http-client/Internal/AmpResolver.php
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\Dns\Record;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
|
||||
/**
|
||||
* Handles local overrides for the DNS resolver.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AmpResolver implements Dns\Resolver
|
||||
{
|
||||
private $dnsMap;
|
||||
|
||||
public function __construct(array &$dnsMap)
|
||||
{
|
||||
$this->dnsMap = &$dnsMap;
|
||||
}
|
||||
|
||||
public function resolve(string $name, ?int $typeRestriction = null): Promise
|
||||
{
|
||||
if (!isset($this->dnsMap[$name]) || !\in_array($typeRestriction, [Record::A, null], true)) {
|
||||
return Dns\resolver()->resolve($name, $typeRestriction);
|
||||
}
|
||||
|
||||
return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
|
||||
}
|
||||
|
||||
public function query(string $name, int $type): Promise
|
||||
{
|
||||
if (!isset($this->dnsMap[$name]) || Record::A !== $type) {
|
||||
return Dns\resolver()->query($name, $type);
|
||||
}
|
||||
|
||||
return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
|
||||
}
|
||||
}
|
40
vendor/symfony/http-client/Internal/Canary.php
vendored
Normal file
40
vendor/symfony/http-client/Internal/Canary.php
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Canary
|
||||
{
|
||||
private $canceller;
|
||||
|
||||
public function __construct(\Closure $canceller)
|
||||
{
|
||||
$this->canceller = $canceller;
|
||||
}
|
||||
|
||||
public function cancel()
|
||||
{
|
||||
if (($canceller = $this->canceller) instanceof \Closure) {
|
||||
$this->canceller = null;
|
||||
$canceller();
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->cancel();
|
||||
}
|
||||
}
|
26
vendor/symfony/http-client/Internal/ClientState.php
vendored
Normal file
26
vendor/symfony/http-client/Internal/ClientState.php
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
/**
|
||||
* Internal representation of the client state.
|
||||
*
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ClientState
|
||||
{
|
||||
public $handlesActivity = [];
|
||||
public $openHandles = [];
|
||||
public $lastTimeout;
|
||||
}
|
149
vendor/symfony/http-client/Internal/CurlClientState.php
vendored
Normal file
149
vendor/symfony/http-client/Internal/CurlClientState.php
vendored
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Response\CurlResponse;
|
||||
|
||||
/**
|
||||
* Internal representation of the cURL client's state.
|
||||
*
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CurlClientState extends ClientState
|
||||
{
|
||||
/** @var \CurlMultiHandle|resource|null */
|
||||
public $handle;
|
||||
/** @var \CurlShareHandle|resource|null */
|
||||
public $share;
|
||||
/** @var PushedResponse[] */
|
||||
public $pushedResponses = [];
|
||||
/** @var DnsCache */
|
||||
public $dnsCache;
|
||||
/** @var float[] */
|
||||
public $pauseExpiries = [];
|
||||
public $execCounter = \PHP_INT_MIN;
|
||||
/** @var LoggerInterface|null */
|
||||
public $logger;
|
||||
public $performing = false;
|
||||
|
||||
public static $curlVersion;
|
||||
|
||||
public function __construct(int $maxHostConnections, int $maxPendingPushes)
|
||||
{
|
||||
self::$curlVersion = self::$curlVersion ?? curl_version();
|
||||
|
||||
$this->handle = curl_multi_init();
|
||||
$this->dnsCache = new DnsCache();
|
||||
$this->reset();
|
||||
|
||||
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
|
||||
if (\defined('CURLPIPE_MULTIPLEX')) {
|
||||
curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
|
||||
}
|
||||
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS') && 0 < $maxHostConnections) {
|
||||
$maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? 4294967295 : $maxHostConnections;
|
||||
}
|
||||
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
|
||||
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
|
||||
}
|
||||
|
||||
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
|
||||
if (0 >= $maxPendingPushes || \PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// HTTP/2 push crashes before curl 7.61
|
||||
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > self::$curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone to prevent a circular reference
|
||||
$multi = clone $this;
|
||||
$multi->handle = null;
|
||||
$multi->share = null;
|
||||
$multi->pushedResponses = &$this->pushedResponses;
|
||||
$multi->logger = &$this->logger;
|
||||
$multi->handlesActivity = &$this->handlesActivity;
|
||||
$multi->openHandles = &$this->openHandles;
|
||||
|
||||
curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, static function ($parent, $pushed, array $requestHeaders) use ($multi, $maxPendingPushes) {
|
||||
return $multi->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
|
||||
});
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
foreach ($this->pushedResponses as $url => $response) {
|
||||
$this->logger && $this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
|
||||
curl_multi_remove_handle($this->handle, $response->handle);
|
||||
curl_close($response->handle);
|
||||
}
|
||||
|
||||
$this->pushedResponses = [];
|
||||
$this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals;
|
||||
$this->dnsCache->removals = $this->dnsCache->hostnames = [];
|
||||
|
||||
$this->share = curl_share_init();
|
||||
|
||||
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
|
||||
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);
|
||||
|
||||
if (\defined('CURL_LOCK_DATA_CONNECT') && \PHP_VERSION_ID >= 80000) {
|
||||
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
|
||||
}
|
||||
}
|
||||
|
||||
private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int
|
||||
{
|
||||
$headers = [];
|
||||
$origin = curl_getinfo($parent, \CURLINFO_EFFECTIVE_URL);
|
||||
|
||||
foreach ($requestHeaders as $h) {
|
||||
if (false !== $i = strpos($h, ':', 1)) {
|
||||
$headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
|
||||
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin));
|
||||
|
||||
return \CURL_PUSH_DENY;
|
||||
}
|
||||
|
||||
$url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
|
||||
|
||||
// curl before 7.65 doesn't validate the pushed ":authority" header,
|
||||
// but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
|
||||
// ignoring domains mentioned as alt-name in the certificate for now (same as curl).
|
||||
if (!str_starts_with($origin, $url.'/')) {
|
||||
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url));
|
||||
|
||||
return \CURL_PUSH_DENY;
|
||||
}
|
||||
|
||||
if ($maxPendingPushes <= \count($this->pushedResponses)) {
|
||||
$fifoUrl = key($this->pushedResponses);
|
||||
unset($this->pushedResponses[$fifoUrl]);
|
||||
$this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
|
||||
}
|
||||
|
||||
$url .= $headers[':path'][0];
|
||||
$this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
|
||||
|
||||
$this->pushedResponses[$url] = new PushedResponse(new CurlResponse($this, $pushed), $headers, $this->openHandles[(int) $parent][1] ?? [], $pushed);
|
||||
|
||||
return \CURL_PUSH_OK;
|
||||
}
|
||||
}
|
39
vendor/symfony/http-client/Internal/DnsCache.php
vendored
Normal file
39
vendor/symfony/http-client/Internal/DnsCache.php
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
/**
|
||||
* Cache for resolved DNS queries.
|
||||
*
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DnsCache
|
||||
{
|
||||
/**
|
||||
* Resolved hostnames (hostname => IP address).
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $hostnames = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $removals = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $evictions = [];
|
||||
}
|
153
vendor/symfony/http-client/Internal/HttplugWaitLoop.php
vendored
Normal file
153
vendor/symfony/http-client/Internal/HttplugWaitLoop.php
vendored
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
use Http\Client\Exception\NetworkException;
|
||||
use Http\Promise\Promise;
|
||||
use Psr\Http\Message\RequestInterface as Psr7RequestInterface;
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Symfony\Component\HttpClient\Response\StreamableInterface;
|
||||
use Symfony\Component\HttpClient\Response\StreamWrapper;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class HttplugWaitLoop
|
||||
{
|
||||
private $client;
|
||||
private $promisePool;
|
||||
private $responseFactory;
|
||||
private $streamFactory;
|
||||
|
||||
/**
|
||||
* @param \SplObjectStorage<ResponseInterface, array{Psr7RequestInterface, Promise}>|null $promisePool
|
||||
*/
|
||||
public function __construct(HttpClientInterface $client, ?\SplObjectStorage $promisePool, ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->promisePool = $promisePool;
|
||||
$this->responseFactory = $responseFactory;
|
||||
$this->streamFactory = $streamFactory;
|
||||
}
|
||||
|
||||
public function wait(?ResponseInterface $pendingResponse, ?float $maxDuration = null, ?float $idleTimeout = null): int
|
||||
{
|
||||
if (!$this->promisePool) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$guzzleQueue = \GuzzleHttp\Promise\Utils::queue();
|
||||
|
||||
if (0.0 === $remainingDuration = $maxDuration) {
|
||||
$idleTimeout = 0.0;
|
||||
} elseif (null !== $maxDuration) {
|
||||
$startTime = microtime(true);
|
||||
$idleTimeout = max(0.0, min($maxDuration / 5, $idleTimeout ?? $maxDuration));
|
||||
}
|
||||
|
||||
do {
|
||||
foreach ($this->client->stream($this->promisePool, $idleTimeout) as $response => $chunk) {
|
||||
try {
|
||||
if (null !== $maxDuration && $chunk->isTimeout()) {
|
||||
goto check_duration;
|
||||
}
|
||||
|
||||
if ($chunk->isFirst()) {
|
||||
// Deactivate throwing on 3/4/5xx
|
||||
$response->getStatusCode();
|
||||
}
|
||||
|
||||
if (!$chunk->isLast()) {
|
||||
goto check_duration;
|
||||
}
|
||||
|
||||
if ([, $promise] = $this->promisePool[$response] ?? null) {
|
||||
unset($this->promisePool[$response]);
|
||||
$promise->resolve(self::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $response, true));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
if ([$request, $promise] = $this->promisePool[$response] ?? null) {
|
||||
unset($this->promisePool[$response]);
|
||||
|
||||
if ($e instanceof TransportExceptionInterface) {
|
||||
$e = new NetworkException($e->getMessage(), $request, $e);
|
||||
}
|
||||
|
||||
$promise->reject($e);
|
||||
}
|
||||
}
|
||||
|
||||
$guzzleQueue->run();
|
||||
|
||||
if ($pendingResponse === $response) {
|
||||
return $this->promisePool->count();
|
||||
}
|
||||
|
||||
check_duration:
|
||||
if (null !== $maxDuration && $idleTimeout && $idleTimeout > $remainingDuration = max(0.0, $maxDuration - microtime(true) + $startTime)) {
|
||||
$idleTimeout = $remainingDuration / 5;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$count = $this->promisePool->count()) {
|
||||
return 0;
|
||||
}
|
||||
} while (null === $maxDuration || 0 < $remainingDuration);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public static function createPsr7Response(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory, HttpClientInterface $client, ResponseInterface $response, bool $buffer): Psr7ResponseInterface
|
||||
{
|
||||
$responseParameters = [$response->getStatusCode()];
|
||||
|
||||
foreach ($response->getInfo('response_headers') as $h) {
|
||||
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? (?:\d\d\d) (.+)#', $h, $m)) {
|
||||
$responseParameters[1] = $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
$psrResponse = $responseFactory->createResponse(...$responseParameters);
|
||||
|
||||
foreach ($response->getHeaders(false) as $name => $values) {
|
||||
foreach ($values as $value) {
|
||||
try {
|
||||
$psrResponse = $psrResponse->withAddedHeader($name, $value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// ignore invalid header
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($response instanceof StreamableInterface) {
|
||||
$body = $streamFactory->createStreamFromResource($response->toStream(false));
|
||||
} elseif (!$buffer) {
|
||||
$body = $streamFactory->createStreamFromResource(StreamWrapper::createResource($response, $client));
|
||||
} else {
|
||||
$body = $streamFactory->createStream($response->getContent(false));
|
||||
}
|
||||
|
||||
if ($body->isSeekable()) {
|
||||
$body->seek(0);
|
||||
}
|
||||
|
||||
return $psrResponse->withBody($body);
|
||||
}
|
||||
}
|
47
vendor/symfony/http-client/Internal/NativeClientState.php
vendored
Normal file
47
vendor/symfony/http-client/Internal/NativeClientState.php
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
/**
|
||||
* Internal representation of the native client's state.
|
||||
*
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class NativeClientState extends ClientState
|
||||
{
|
||||
/** @var int */
|
||||
public $id;
|
||||
/** @var int */
|
||||
public $maxHostConnections = \PHP_INT_MAX;
|
||||
/** @var int */
|
||||
public $responseCount = 0;
|
||||
/** @var string[] */
|
||||
public $dnsCache = [];
|
||||
/** @var bool */
|
||||
public $sleep = false;
|
||||
/** @var int[] */
|
||||
public $hosts = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = random_int(\PHP_INT_MIN, \PHP_INT_MAX);
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->responseCount = 0;
|
||||
$this->dnsCache = [];
|
||||
$this->hosts = [];
|
||||
}
|
||||
}
|
41
vendor/symfony/http-client/Internal/PushedResponse.php
vendored
Normal file
41
vendor/symfony/http-client/Internal/PushedResponse.php
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Internal;
|
||||
|
||||
use Symfony\Component\HttpClient\Response\CurlResponse;
|
||||
|
||||
/**
|
||||
* A pushed response with its request headers.
|
||||
*
|
||||
* @author Alexander M. Turek <me@derrabus.de>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class PushedResponse
|
||||
{
|
||||
public $response;
|
||||
|
||||
/** @var string[] */
|
||||
public $requestHeaders;
|
||||
|
||||
public $parentOptions = [];
|
||||
|
||||
public $handle;
|
||||
|
||||
public function __construct(CurlResponse $response, array $requestHeaders, array $parentOptions, $handle)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->requestHeaders = $requestHeaders;
|
||||
$this->parentOptions = $parentOptions;
|
||||
$this->handle = $handle;
|
||||
}
|
||||
}
|
19
vendor/symfony/http-client/LICENSE
vendored
Normal file
19
vendor/symfony/http-client/LICENSE
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2018-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
124
vendor/symfony/http-client/MockHttpClient.php
vendored
Normal file
124
vendor/symfony/http-client/MockHttpClient.php
vendored
Normal file
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
use Symfony\Component\HttpClient\Response\ResponseStream;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* A test-friendly HttpClient that doesn't make actual HTTP requests.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class MockHttpClient implements HttpClientInterface, ResetInterface
|
||||
{
|
||||
use HttpClientTrait;
|
||||
|
||||
private $responseFactory;
|
||||
private $requestsCount = 0;
|
||||
private $defaultOptions = [];
|
||||
|
||||
/**
|
||||
* @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
|
||||
*/
|
||||
public function __construct($responseFactory = null, ?string $baseUri = 'https://example.com')
|
||||
{
|
||||
$this->setResponseFactory($responseFactory);
|
||||
$this->defaultOptions['base_uri'] = $baseUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
|
||||
*/
|
||||
public function setResponseFactory($responseFactory): void
|
||||
{
|
||||
if ($responseFactory instanceof ResponseInterface) {
|
||||
$responseFactory = [$responseFactory];
|
||||
}
|
||||
|
||||
if (!$responseFactory instanceof \Iterator && null !== $responseFactory && !\is_callable($responseFactory)) {
|
||||
$responseFactory = (static function () use ($responseFactory) {
|
||||
yield from $responseFactory;
|
||||
})();
|
||||
}
|
||||
|
||||
$this->responseFactory = $responseFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
|
||||
$url = implode('', $url);
|
||||
|
||||
if (null === $this->responseFactory) {
|
||||
$response = new MockResponse();
|
||||
} elseif (\is_callable($this->responseFactory)) {
|
||||
$response = ($this->responseFactory)($method, $url, $options);
|
||||
} elseif (!$this->responseFactory->valid()) {
|
||||
throw new TransportException('The response factory iterator passed to MockHttpClient is empty.');
|
||||
} else {
|
||||
$responseFactory = $this->responseFactory->current();
|
||||
$response = \is_callable($responseFactory) ? $responseFactory($method, $url, $options) : $responseFactory;
|
||||
$this->responseFactory->next();
|
||||
}
|
||||
++$this->requestsCount;
|
||||
|
||||
if (!$response instanceof ResponseInterface) {
|
||||
throw new TransportException(sprintf('The response factory passed to MockHttpClient must return/yield an instance of ResponseInterface, "%s" given.', \is_object($response) ? \get_class($response) : \gettype($response)));
|
||||
}
|
||||
|
||||
return MockResponse::fromRequest($method, $url, $options, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
if ($responses instanceof ResponseInterface) {
|
||||
$responses = [$responses];
|
||||
} elseif (!is_iterable($responses)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of MockResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
|
||||
}
|
||||
|
||||
return new ResponseStream(MockResponse::stream($responses, $timeout));
|
||||
}
|
||||
|
||||
public function getRequestsCount(): int
|
||||
{
|
||||
return $this->requestsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function withOptions(array $options): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions, true);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->requestsCount = 0;
|
||||
}
|
||||
}
|
472
vendor/symfony/http-client/NativeHttpClient.php
vendored
Normal file
472
vendor/symfony/http-client/NativeHttpClient.php
vendored
Normal file
|
@ -0,0 +1,472 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\Internal\NativeClientState;
|
||||
use Symfony\Component\HttpClient\Response\NativeResponse;
|
||||
use Symfony\Component\HttpClient\Response\ResponseStream;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* A portable implementation of the HttpClientInterface contracts based on PHP stream wrappers.
|
||||
*
|
||||
* PHP stream wrappers are able to fetch response bodies concurrently,
|
||||
* but each request is opened synchronously.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
|
||||
{
|
||||
use HttpClientTrait;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private $defaultOptions = self::OPTIONS_DEFAULTS;
|
||||
private static $emptyDefaults = self::OPTIONS_DEFAULTS;
|
||||
|
||||
/** @var NativeClientState */
|
||||
private $multi;
|
||||
|
||||
/**
|
||||
* @param array $defaultOptions Default request's options
|
||||
* @param int $maxHostConnections The maximum number of connections to open
|
||||
*
|
||||
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
|
||||
*/
|
||||
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
|
||||
{
|
||||
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
|
||||
|
||||
if ($defaultOptions) {
|
||||
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
|
||||
}
|
||||
|
||||
$this->multi = new NativeClientState();
|
||||
$this->multi->maxHostConnections = 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
|
||||
|
||||
if ($options['bindto']) {
|
||||
if (file_exists($options['bindto'])) {
|
||||
throw new TransportException(__CLASS__.' cannot bind to local Unix sockets, use e.g. CurlHttpClient instead.');
|
||||
}
|
||||
if (str_starts_with($options['bindto'], 'if!')) {
|
||||
throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
|
||||
}
|
||||
if (str_starts_with($options['bindto'], 'host!')) {
|
||||
$options['bindto'] = substr($options['bindto'], 5);
|
||||
}
|
||||
}
|
||||
|
||||
$hasContentLength = isset($options['normalized_headers']['content-length']);
|
||||
$hasBody = '' !== $options['body'] || 'POST' === $method || $hasContentLength;
|
||||
|
||||
$options['body'] = self::getBodyAsString($options['body']);
|
||||
|
||||
if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
|
||||
unset($options['normalized_headers']['transfer-encoding']);
|
||||
$options['headers'] = array_merge(...array_values($options['normalized_headers']));
|
||||
$options['body'] = self::dechunk($options['body']);
|
||||
}
|
||||
if ('' === $options['body'] && $hasBody && !$hasContentLength) {
|
||||
$options['headers'][] = 'Content-Length: 0';
|
||||
}
|
||||
if ($hasBody && !isset($options['normalized_headers']['content-type'])) {
|
||||
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
|
||||
// gzip is the most widely available algo, no need to deal with deflate
|
||||
$options['headers'][] = 'Accept-Encoding: gzip';
|
||||
}
|
||||
|
||||
if ($options['peer_fingerprint']) {
|
||||
if (isset($options['peer_fingerprint']['pin-sha256']) && 1 === \count($options['peer_fingerprint'])) {
|
||||
throw new TransportException(__CLASS__.' cannot verify "pin-sha256" fingerprints, please provide a "sha256" one.');
|
||||
}
|
||||
|
||||
unset($options['peer_fingerprint']['pin-sha256']);
|
||||
}
|
||||
|
||||
$info = [
|
||||
'response_headers' => [],
|
||||
'url' => $url,
|
||||
'error' => null,
|
||||
'canceled' => false,
|
||||
'http_method' => $method,
|
||||
'http_code' => 0,
|
||||
'redirect_count' => 0,
|
||||
'start_time' => 0.0,
|
||||
'connect_time' => 0.0,
|
||||
'redirect_time' => 0.0,
|
||||
'pretransfer_time' => 0.0,
|
||||
'starttransfer_time' => 0.0,
|
||||
'total_time' => 0.0,
|
||||
'namelookup_time' => 0.0,
|
||||
'size_upload' => 0,
|
||||
'size_download' => 0,
|
||||
'size_body' => \strlen($options['body']),
|
||||
'primary_ip' => '',
|
||||
'primary_port' => 'http:' === $url['scheme'] ? 80 : 443,
|
||||
'debug' => \extension_loaded('curl') ? '' : "* Enable the curl extension for better performance\n",
|
||||
];
|
||||
|
||||
if ($onProgress = $options['on_progress']) {
|
||||
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
|
||||
$lastProgress = [0, 0];
|
||||
$maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF;
|
||||
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
|
||||
if ($info['total_time'] >= $maxDuration) {
|
||||
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
|
||||
}
|
||||
|
||||
$progressInfo = $info;
|
||||
$progressInfo['url'] = implode('', $info['url']);
|
||||
unset($progressInfo['size_body']);
|
||||
|
||||
if ($progress && -1 === $progress[0]) {
|
||||
// Response completed
|
||||
$lastProgress[0] = max($lastProgress);
|
||||
} else {
|
||||
$lastProgress = $progress ?: $lastProgress;
|
||||
}
|
||||
|
||||
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
|
||||
};
|
||||
} elseif (0 < $options['max_duration']) {
|
||||
$maxDuration = $options['max_duration'];
|
||||
$onProgress = static function () use (&$info, $maxDuration): void {
|
||||
if ($info['total_time'] >= $maxDuration) {
|
||||
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Always register a notification callback to compute live stats about the response
|
||||
$notification = static function (int $code, int $severity, ?string $msg, int $msgCode, int $dlNow, int $dlSize) use ($onProgress, &$info) {
|
||||
$info['total_time'] = microtime(true) - $info['start_time'];
|
||||
|
||||
if (\STREAM_NOTIFY_PROGRESS === $code) {
|
||||
$info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
|
||||
$info['size_upload'] += $dlNow ? 0 : $info['size_body'];
|
||||
$info['size_download'] = $dlNow;
|
||||
} elseif (\STREAM_NOTIFY_CONNECT === $code) {
|
||||
$info['connect_time'] = $info['total_time'];
|
||||
$info['debug'] .= $info['request_header'];
|
||||
unset($info['request_header']);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($onProgress) {
|
||||
$onProgress($dlNow, $dlSize);
|
||||
}
|
||||
};
|
||||
|
||||
if ($options['resolve']) {
|
||||
$this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
|
||||
}
|
||||
|
||||
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, implode('', $url)));
|
||||
|
||||
if (!isset($options['normalized_headers']['user-agent'])) {
|
||||
$options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
|
||||
}
|
||||
|
||||
if (0 < $options['max_duration']) {
|
||||
$options['timeout'] = min($options['max_duration'], $options['timeout']);
|
||||
}
|
||||
|
||||
$bindto = $options['bindto'];
|
||||
if (!$bindto && (70322 === \PHP_VERSION_ID || 70410 === \PHP_VERSION_ID)) {
|
||||
$bindto = '0:0';
|
||||
}
|
||||
|
||||
$context = [
|
||||
'http' => [
|
||||
'protocol_version' => min($options['http_version'] ?: '1.1', '1.1'),
|
||||
'method' => $method,
|
||||
'content' => $options['body'],
|
||||
'ignore_errors' => true,
|
||||
'curl_verify_ssl_peer' => $options['verify_peer'],
|
||||
'curl_verify_ssl_host' => $options['verify_host'],
|
||||
'auto_decode' => false, // Disable dechunk filter, it's incompatible with stream_select()
|
||||
'timeout' => $options['timeout'],
|
||||
'follow_location' => false, // We follow redirects ourselves - the native logic is too limited
|
||||
],
|
||||
'ssl' => array_filter([
|
||||
'verify_peer' => $options['verify_peer'],
|
||||
'verify_peer_name' => $options['verify_host'],
|
||||
'cafile' => $options['cafile'],
|
||||
'capath' => $options['capath'],
|
||||
'local_cert' => $options['local_cert'],
|
||||
'local_pk' => $options['local_pk'],
|
||||
'passphrase' => $options['passphrase'],
|
||||
'ciphers' => $options['ciphers'],
|
||||
'peer_fingerprint' => $options['peer_fingerprint'],
|
||||
'capture_peer_cert_chain' => $options['capture_peer_cert_chain'],
|
||||
'allow_self_signed' => (bool) $options['peer_fingerprint'],
|
||||
'SNI_enabled' => true,
|
||||
'disable_compression' => true,
|
||||
], static function ($v) { return null !== $v; }),
|
||||
'socket' => [
|
||||
'bindto' => $bindto,
|
||||
'tcp_nodelay' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$context = stream_context_create($context, ['notification' => $notification]);
|
||||
|
||||
$resolver = static function ($multi) use ($context, $options, $url, &$info, $onProgress) {
|
||||
[$host, $port] = self::parseHostPort($url, $info);
|
||||
|
||||
if (!isset($options['normalized_headers']['host'])) {
|
||||
$options['headers'][] = 'Host: '.$host.$port;
|
||||
}
|
||||
|
||||
$proxy = self::getProxy($options['proxy'], $url, $options['no_proxy']);
|
||||
|
||||
if (!self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, 'https:' === $url['scheme'])) {
|
||||
$ip = self::dnsResolve($host, $multi, $info, $onProgress);
|
||||
$url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
|
||||
}
|
||||
|
||||
return [self::createRedirectResolver($options, $host, $proxy, $info, $onProgress), implode('', $url)];
|
||||
};
|
||||
|
||||
return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolver, $onProgress, $this->logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
if ($responses instanceof NativeResponse) {
|
||||
$responses = [$responses];
|
||||
} elseif (!is_iterable($responses)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of NativeResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
|
||||
}
|
||||
|
||||
return new ResponseStream(NativeResponse::stream($responses, $timeout));
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->multi->reset();
|
||||
}
|
||||
|
||||
private static function getBodyAsString($body): string
|
||||
{
|
||||
if (\is_resource($body)) {
|
||||
return stream_get_contents($body);
|
||||
}
|
||||
|
||||
if (!$body instanceof \Closure) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$result = '';
|
||||
|
||||
while ('' !== $data = $body(self::$CHUNK_SIZE)) {
|
||||
if (!\is_string($data)) {
|
||||
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
|
||||
}
|
||||
|
||||
$result .= $data;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the host and the port from the URL.
|
||||
*/
|
||||
private static function parseHostPort(array $url, array &$info): array
|
||||
{
|
||||
if ($port = parse_url($url['authority'], \PHP_URL_PORT) ?: '') {
|
||||
$info['primary_port'] = $port;
|
||||
$port = ':'.$port;
|
||||
} else {
|
||||
$info['primary_port'] = 'http:' === $url['scheme'] ? 80 : 443;
|
||||
}
|
||||
|
||||
return [parse_url($url['authority'], \PHP_URL_HOST), $port];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the IP of the host using the local DNS cache if possible.
|
||||
*/
|
||||
private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string
|
||||
{
|
||||
if (null === $ip = $multi->dnsCache[$host] ?? null) {
|
||||
$info['debug'] .= "* Hostname was NOT found in DNS cache\n";
|
||||
$now = microtime(true);
|
||||
|
||||
if (!$ip = gethostbynamel($host)) {
|
||||
throw new TransportException(sprintf('Could not resolve host "%s".', $host));
|
||||
}
|
||||
|
||||
$info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
|
||||
$multi->dnsCache[$host] = $ip = $ip[0];
|
||||
$info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n";
|
||||
} else {
|
||||
$info['debug'] .= "* Hostname was found in DNS cache\n";
|
||||
}
|
||||
|
||||
$info['primary_ip'] = $ip;
|
||||
|
||||
if ($onProgress) {
|
||||
// Notify DNS resolution
|
||||
$onProgress();
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles redirects - the native logic is too buggy to be used.
|
||||
*/
|
||||
private static function createRedirectResolver(array $options, string $host, ?array $proxy, array &$info, ?\Closure $onProgress): \Closure
|
||||
{
|
||||
$redirectHeaders = [];
|
||||
if (0 < $maxRedirects = $options['max_redirects']) {
|
||||
$redirectHeaders = ['host' => $host];
|
||||
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
|
||||
return 0 !== stripos($h, 'Host:');
|
||||
});
|
||||
|
||||
if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
|
||||
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
|
||||
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return static function (NativeClientState $multi, ?string $location, $context) use (&$redirectHeaders, $proxy, &$info, $maxRedirects, $onProgress): ?string {
|
||||
if (null === $location || $info['http_code'] < 300 || 400 <= $info['http_code']) {
|
||||
$info['redirect_url'] = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$url = self::parseUrl($location);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$info['redirect_url'] = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = self::resolveUrl($url, $info['url']);
|
||||
$info['redirect_url'] = implode('', $url);
|
||||
|
||||
if ($info['redirect_count'] >= $maxRedirects) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$info['url'] = $url;
|
||||
++$info['redirect_count'];
|
||||
$info['redirect_time'] = microtime(true) - $info['start_time'];
|
||||
|
||||
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
|
||||
if (\in_array($info['http_code'], [301, 302, 303], true)) {
|
||||
$options = stream_context_get_options($context)['http'];
|
||||
|
||||
if ('POST' === $options['method'] || 303 === $info['http_code']) {
|
||||
$info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
|
||||
$options['content'] = '';
|
||||
$filterContentHeaders = static function ($h) {
|
||||
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
|
||||
};
|
||||
$options['header'] = array_filter($options['header'], $filterContentHeaders);
|
||||
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
|
||||
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
|
||||
|
||||
if (\PHP_VERSION_ID >= 80300) {
|
||||
stream_context_set_options($context, ['http' => $options]);
|
||||
} else {
|
||||
stream_context_set_option($context, ['http' => $options]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[$host, $port] = self::parseHostPort($url, $info);
|
||||
|
||||
if (false !== (parse_url($location.'#', \PHP_URL_HOST) ?? false)) {
|
||||
// Authorization and Cookie headers MUST NOT follow except for the initial host name
|
||||
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
|
||||
$requestHeaders[] = 'Host: '.$host.$port;
|
||||
$dnsResolve = !self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, 'https:' === $url['scheme']);
|
||||
} else {
|
||||
$dnsResolve = isset(stream_context_get_options($context)['ssl']['peer_name']);
|
||||
}
|
||||
|
||||
if ($dnsResolve) {
|
||||
$ip = self::dnsResolve($host, $multi, $info, $onProgress);
|
||||
$url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
|
||||
}
|
||||
|
||||
return implode('', $url);
|
||||
};
|
||||
}
|
||||
|
||||
private static function configureHeadersAndProxy($context, string $host, array $requestHeaders, ?array $proxy, bool $isSsl): bool
|
||||
{
|
||||
if (null === $proxy) {
|
||||
stream_context_set_option($context, 'http', 'header', $requestHeaders);
|
||||
stream_context_set_option($context, 'ssl', 'peer_name', $host);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Matching "no_proxy" should follow the behavior of curl
|
||||
|
||||
foreach ($proxy['no_proxy'] as $rule) {
|
||||
$dotRule = '.'.ltrim($rule, '.');
|
||||
|
||||
if ('*' === $rule || $host === $rule || str_ends_with($host, $dotRule)) {
|
||||
stream_context_set_option($context, 'http', 'proxy', null);
|
||||
stream_context_set_option($context, 'http', 'request_fulluri', false);
|
||||
stream_context_set_option($context, 'http', 'header', $requestHeaders);
|
||||
stream_context_set_option($context, 'ssl', 'peer_name', $host);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $proxy['auth']) {
|
||||
$requestHeaders[] = 'Proxy-Authorization: '.$proxy['auth'];
|
||||
}
|
||||
|
||||
stream_context_set_option($context, 'http', 'proxy', $proxy['url']);
|
||||
stream_context_set_option($context, 'http', 'request_fulluri', !$isSsl);
|
||||
stream_context_set_option($context, 'http', 'header', $requestHeaders);
|
||||
stream_context_set_option($context, 'ssl', 'peer_name', null);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
132
vendor/symfony/http-client/NoPrivateNetworkHttpClient.php
vendored
Normal file
132
vendor/symfony/http-client/NoPrivateNetworkHttpClient.php
vendored
Normal file
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* Decorator that blocks requests to private networks by default.
|
||||
*
|
||||
* @author Hallison Boaventura <hallisonboaventura@gmail.com>
|
||||
*/
|
||||
final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
|
||||
{
|
||||
use HttpClientTrait;
|
||||
|
||||
private const PRIVATE_SUBNETS = [
|
||||
'127.0.0.0/8',
|
||||
'10.0.0.0/8',
|
||||
'192.168.0.0/16',
|
||||
'172.16.0.0/12',
|
||||
'169.254.0.0/16',
|
||||
'0.0.0.0/8',
|
||||
'240.0.0.0/4',
|
||||
'::1/128',
|
||||
'fc00::/7',
|
||||
'fe80::/10',
|
||||
'::ffff:0:0/96',
|
||||
'::/128',
|
||||
];
|
||||
|
||||
private $client;
|
||||
private $subnets;
|
||||
|
||||
/**
|
||||
* @param string|array|null $subnets String or array of subnets using CIDR notation that will be used by IpUtils.
|
||||
* If null is passed, the standard private subnets will be used.
|
||||
*/
|
||||
public function __construct(HttpClientInterface $client, $subnets = null)
|
||||
{
|
||||
if (!(\is_array($subnets) || \is_string($subnets) || null === $subnets)) {
|
||||
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be of the type array, string or null. "%s" given.', __METHOD__, get_debug_type($subnets)));
|
||||
}
|
||||
|
||||
if (!class_exists(IpUtils::class)) {
|
||||
throw new \LogicException(sprintf('You cannot use "%s" if the HttpFoundation component is not installed. Try running "composer require symfony/http-foundation".', __CLASS__));
|
||||
}
|
||||
|
||||
$this->client = $client;
|
||||
$this->subnets = $subnets;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$onProgress = $options['on_progress'] ?? null;
|
||||
if (null !== $onProgress && !\is_callable($onProgress)) {
|
||||
throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress)));
|
||||
}
|
||||
|
||||
$subnets = $this->subnets;
|
||||
$lastPrimaryIp = '';
|
||||
|
||||
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, &$lastPrimaryIp): void {
|
||||
if ($info['primary_ip'] !== $lastPrimaryIp) {
|
||||
if ($info['primary_ip'] && IpUtils::checkIp($info['primary_ip'], $subnets ?? self::PRIVATE_SUBNETS)) {
|
||||
throw new TransportException(sprintf('IP "%s" is blocked for "%s".', $info['primary_ip'], $info['url']));
|
||||
}
|
||||
|
||||
$lastPrimaryIp = $info['primary_ip'];
|
||||
}
|
||||
|
||||
null !== $onProgress && $onProgress($dlNow, $dlSize, $info);
|
||||
};
|
||||
|
||||
return $this->client->request($method, $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->client->stream($responses, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
if ($this->client instanceof LoggerAwareInterface) {
|
||||
$this->client->setLogger($logger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function withOptions(array $options): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->client = $this->client->withOptions($options);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if ($this->client instanceof ResetInterface) {
|
||||
$this->client->reset();
|
||||
}
|
||||
}
|
||||
}
|
231
vendor/symfony/http-client/Psr18Client.php
vendored
Normal file
231
vendor/symfony/http-client/Psr18Client.php
vendored
Normal file
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Http\Discovery\Exception\NotFoundException;
|
||||
use Http\Discovery\Psr17FactoryDiscovery;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Nyholm\Psr7\Request;
|
||||
use Nyholm\Psr7\Uri;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Client\NetworkExceptionInterface;
|
||||
use Psr\Http\Client\RequestExceptionInterface;
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriFactoryInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
|
||||
use Symfony\Component\HttpClient\Response\StreamableInterface;
|
||||
use Symfony\Component\HttpClient\Response\StreamWrapper;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface as HttpClientResponseInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
if (!interface_exists(RequestFactoryInterface::class)) {
|
||||
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as the "psr/http-factory" package is not installed. Try running "composer require nyholm/psr7".');
|
||||
}
|
||||
|
||||
if (!interface_exists(ClientInterface::class)) {
|
||||
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as the "psr/http-client" package is not installed. Try running "composer require psr/http-client".');
|
||||
}
|
||||
|
||||
/**
|
||||
* An adapter to turn a Symfony HttpClientInterface into a PSR-18 ClientInterface.
|
||||
*
|
||||
* Run "composer require psr/http-client" to install the base ClientInterface. Run
|
||||
* "composer require nyholm/psr7" to install an efficient implementation of response
|
||||
* and stream factories with flex-provided autowiring aliases.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class Psr18Client implements ClientInterface, RequestFactoryInterface, StreamFactoryInterface, UriFactoryInterface, ResetInterface
|
||||
{
|
||||
private $client;
|
||||
private $responseFactory;
|
||||
private $streamFactory;
|
||||
|
||||
public function __construct(?HttpClientInterface $client = null, ?ResponseFactoryInterface $responseFactory = null, ?StreamFactoryInterface $streamFactory = null)
|
||||
{
|
||||
$this->client = $client ?? HttpClient::create();
|
||||
$this->responseFactory = $responseFactory;
|
||||
$this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
|
||||
|
||||
if (null !== $this->responseFactory && null !== $this->streamFactory) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
|
||||
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
|
||||
}
|
||||
|
||||
try {
|
||||
$psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
|
||||
$this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
|
||||
$this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
|
||||
} catch (NotFoundException $e) {
|
||||
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function sendRequest(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$body = $request->getBody();
|
||||
|
||||
if ($body->isSeekable()) {
|
||||
$body->seek(0);
|
||||
}
|
||||
|
||||
$options = [
|
||||
'headers' => $request->getHeaders(),
|
||||
'body' => $body->getContents(),
|
||||
];
|
||||
|
||||
if ('1.0' === $request->getProtocolVersion()) {
|
||||
$options['http_version'] = '1.0';
|
||||
}
|
||||
|
||||
$response = $this->client->request($request->getMethod(), (string) $request->getUri(), $options);
|
||||
|
||||
return HttplugWaitLoop::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $response, false);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
if ($e instanceof \InvalidArgumentException) {
|
||||
throw new Psr18RequestException($e, $request);
|
||||
}
|
||||
|
||||
throw new Psr18NetworkException($e, $request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createRequest(string $method, $uri): RequestInterface
|
||||
{
|
||||
if ($this->responseFactory instanceof RequestFactoryInterface) {
|
||||
return $this->responseFactory->createRequest($method, $uri);
|
||||
}
|
||||
|
||||
if (class_exists(Request::class)) {
|
||||
return new Request($method, $uri);
|
||||
}
|
||||
|
||||
if (class_exists(Psr17FactoryDiscovery::class)) {
|
||||
return Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
|
||||
}
|
||||
|
||||
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createStream(string $content = ''): StreamInterface
|
||||
{
|
||||
$stream = $this->streamFactory->createStream($content);
|
||||
|
||||
if ($stream->isSeekable()) {
|
||||
$stream->seek(0);
|
||||
}
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
|
||||
{
|
||||
return $this->streamFactory->createStreamFromFile($filename, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createStreamFromResource($resource): StreamInterface
|
||||
{
|
||||
return $this->streamFactory->createStreamFromResource($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createUri(string $uri = ''): UriInterface
|
||||
{
|
||||
if ($this->responseFactory instanceof UriFactoryInterface) {
|
||||
return $this->responseFactory->createUri($uri);
|
||||
}
|
||||
|
||||
if (class_exists(Uri::class)) {
|
||||
return new Uri($uri);
|
||||
}
|
||||
|
||||
if (class_exists(Psr17FactoryDiscovery::class)) {
|
||||
return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
|
||||
}
|
||||
|
||||
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if ($this->client instanceof ResetInterface) {
|
||||
$this->client->reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Psr18NetworkException extends \RuntimeException implements NetworkExceptionInterface
|
||||
{
|
||||
private $request;
|
||||
|
||||
public function __construct(TransportExceptionInterface $e, RequestInterface $request)
|
||||
{
|
||||
parent::__construct($e->getMessage(), 0, $e);
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function getRequest(): RequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Psr18RequestException extends \InvalidArgumentException implements RequestExceptionInterface
|
||||
{
|
||||
private $request;
|
||||
|
||||
public function __construct(TransportExceptionInterface $e, RequestInterface $request)
|
||||
{
|
||||
parent::__construct($e->getMessage(), 0, $e);
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function getRequest(): RequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
27
vendor/symfony/http-client/README.md
vendored
Normal file
27
vendor/symfony/http-client/README.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
HttpClient component
|
||||
====================
|
||||
|
||||
The HttpClient component provides powerful methods to fetch HTTP resources synchronously or asynchronously.
|
||||
|
||||
Sponsor
|
||||
-------
|
||||
|
||||
The Httpclient component for Symfony 5.4/6.0 is [backed][1] by [Klaxoon][2].
|
||||
|
||||
Klaxoon is a platform that empowers organizations to run effective and
|
||||
productive workshops easily in a hybrid environment. Anytime, Anywhere.
|
||||
|
||||
Help Symfony by [sponsoring][3] its development!
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/current/components/http_client.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
|
||||
[1]: https://symfony.com/backers
|
||||
[2]: https://klaxoon.com
|
||||
[3]: https://symfony.com/sponsor
|
460
vendor/symfony/http-client/Response/AmpResponse.php
vendored
Normal file
460
vendor/symfony/http-client/Response/AmpResponse.php
vendored
Normal file
|
@ -0,0 +1,460 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Amp\ByteStream\StreamException;
|
||||
use Amp\CancellationTokenSource;
|
||||
use Amp\Coroutine;
|
||||
use Amp\Deferred;
|
||||
use Amp\Http\Client\HttpException;
|
||||
use Amp\Http\Client\Request;
|
||||
use Amp\Http\Client\Response;
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\HttpClientTrait;
|
||||
use Symfony\Component\HttpClient\Internal\AmpBody;
|
||||
use Symfony\Component\HttpClient\Internal\AmpClientState;
|
||||
use Symfony\Component\HttpClient\Internal\Canary;
|
||||
use Symfony\Component\HttpClient\Internal\ClientState;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class AmpResponse implements ResponseInterface, StreamableInterface
|
||||
{
|
||||
use CommonResponseTrait;
|
||||
use TransportResponseTrait;
|
||||
|
||||
private static $nextId = 'a';
|
||||
|
||||
private $multi;
|
||||
private $options;
|
||||
private $onProgress;
|
||||
|
||||
private static $delay;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(AmpClientState $multi, Request $request, array $options, ?LoggerInterface $logger)
|
||||
{
|
||||
$this->multi = $multi;
|
||||
$this->options = &$options;
|
||||
$this->logger = $logger;
|
||||
$this->timeout = $options['timeout'];
|
||||
$this->shouldBuffer = $options['buffer'];
|
||||
|
||||
if ($this->inflate = \extension_loaded('zlib') && !$request->hasHeader('accept-encoding')) {
|
||||
$request->setHeader('Accept-Encoding', 'gzip');
|
||||
}
|
||||
|
||||
$this->initializer = static function (self $response) {
|
||||
return null !== $response->options;
|
||||
};
|
||||
|
||||
$info = &$this->info;
|
||||
$headers = &$this->headers;
|
||||
$canceller = new CancellationTokenSource();
|
||||
$handle = &$this->handle;
|
||||
|
||||
$info['url'] = (string) $request->getUri();
|
||||
$info['http_method'] = $request->getMethod();
|
||||
$info['start_time'] = null;
|
||||
$info['redirect_url'] = null;
|
||||
$info['redirect_time'] = 0.0;
|
||||
$info['redirect_count'] = 0;
|
||||
$info['size_upload'] = 0.0;
|
||||
$info['size_download'] = 0.0;
|
||||
$info['upload_content_length'] = -1.0;
|
||||
$info['download_content_length'] = -1.0;
|
||||
$info['user_data'] = $options['user_data'];
|
||||
$info['max_duration'] = $options['max_duration'];
|
||||
$info['debug'] = '';
|
||||
|
||||
$onProgress = $options['on_progress'] ?? static function () {};
|
||||
$onProgress = $this->onProgress = static function () use (&$info, $onProgress) {
|
||||
$info['total_time'] = microtime(true) - $info['start_time'];
|
||||
$onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info);
|
||||
};
|
||||
|
||||
$pauseDeferred = new Deferred();
|
||||
$pause = new Success();
|
||||
|
||||
$throttleWatcher = null;
|
||||
|
||||
$this->id = $id = self::$nextId++;
|
||||
Loop::defer(static function () use ($request, $multi, &$id, &$info, &$headers, $canceller, &$options, $onProgress, &$handle, $logger, &$pause) {
|
||||
return new Coroutine(self::generateResponse($request, $multi, $id, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause));
|
||||
});
|
||||
|
||||
$info['pause_handler'] = static function (float $duration) use (&$throttleWatcher, &$pauseDeferred, &$pause) {
|
||||
if (null !== $throttleWatcher) {
|
||||
Loop::cancel($throttleWatcher);
|
||||
}
|
||||
|
||||
$pause = $pauseDeferred->promise();
|
||||
|
||||
if ($duration <= 0) {
|
||||
$deferred = $pauseDeferred;
|
||||
$pauseDeferred = new Deferred();
|
||||
$deferred->resolve();
|
||||
} else {
|
||||
$throttleWatcher = Loop::delay(ceil(1000 * $duration), static function () use (&$pauseDeferred) {
|
||||
$deferred = $pauseDeferred;
|
||||
$pauseDeferred = new Deferred();
|
||||
$deferred->resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$multi->lastTimeout = null;
|
||||
$multi->openHandles[$id] = $id;
|
||||
++$multi->responseCount;
|
||||
|
||||
$this->canary = new Canary(static function () use ($canceller, $multi, $id) {
|
||||
$canceller->cancel();
|
||||
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInfo(?string $type = null)
|
||||
{
|
||||
return null !== $type ? $this->info[$type] ?? null : $this->info;
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
try {
|
||||
$this->doDestruct();
|
||||
} finally {
|
||||
// Clear the DNS cache when all requests completed
|
||||
if (0 >= --$this->multi->responseCount) {
|
||||
$this->multi->responseCount = 0;
|
||||
$this->multi->dnsCache = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
private static function schedule(self $response, array &$runningResponses): void
|
||||
{
|
||||
if (isset($runningResponses[0])) {
|
||||
$runningResponses[0][1][$response->id] = $response;
|
||||
} else {
|
||||
$runningResponses[0] = [$response->multi, [$response->id => $response]];
|
||||
}
|
||||
|
||||
if (!isset($response->multi->openHandles[$response->id])) {
|
||||
$response->multi->handlesActivity[$response->id][] = null;
|
||||
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param AmpClientState $multi
|
||||
*/
|
||||
private static function perform(ClientState $multi, ?array &$responses = null): void
|
||||
{
|
||||
if ($responses) {
|
||||
foreach ($responses as $response) {
|
||||
try {
|
||||
if ($response->info['start_time']) {
|
||||
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
|
||||
($response->onProgress)();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$multi->handlesActivity[$response->id][] = null;
|
||||
$multi->handlesActivity[$response->id][] = $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param AmpClientState $multi
|
||||
*/
|
||||
private static function select(ClientState $multi, float $timeout): int
|
||||
{
|
||||
$timeout += microtime(true);
|
||||
self::$delay = Loop::defer(static function () use ($timeout) {
|
||||
if (0 < $timeout -= microtime(true)) {
|
||||
self::$delay = Loop::delay(ceil(1000 * $timeout), [Loop::class, 'stop']);
|
||||
} else {
|
||||
Loop::stop();
|
||||
}
|
||||
});
|
||||
|
||||
Loop::run();
|
||||
|
||||
return null === self::$delay ? 1 : 0;
|
||||
}
|
||||
|
||||
private static function generateResponse(Request $request, AmpClientState $multi, string $id, array &$info, array &$headers, CancellationTokenSource $canceller, array &$options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
|
||||
{
|
||||
$request->setInformationalResponseHandler(static function (Response $response) use ($multi, $id, &$info, &$headers) {
|
||||
self::addResponseHeaders($response, $info, $headers);
|
||||
$multi->handlesActivity[$id][] = new InformationalChunk($response->getStatus(), $response->getHeaders());
|
||||
self::stopLoop();
|
||||
});
|
||||
|
||||
try {
|
||||
/* @var Response $response */
|
||||
if (null === $response = yield from self::getPushedResponse($request, $multi, $info, $headers, $options, $logger)) {
|
||||
$logger && $logger->info(sprintf('Request: "%s %s"', $info['http_method'], $info['url']));
|
||||
|
||||
$response = yield from self::followRedirects($request, $multi, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause);
|
||||
}
|
||||
|
||||
$options = null;
|
||||
|
||||
$multi->handlesActivity[$id][] = new FirstChunk();
|
||||
|
||||
if ('HEAD' === $response->getRequest()->getMethod() || \in_array($info['http_code'], [204, 304], true)) {
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
self::stopLoop();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($response->hasHeader('content-length')) {
|
||||
$info['download_content_length'] = (float) $response->getHeader('content-length');
|
||||
}
|
||||
|
||||
$body = $response->getBody();
|
||||
|
||||
while (true) {
|
||||
self::stopLoop();
|
||||
|
||||
yield $pause;
|
||||
|
||||
if (null === $data = yield $body->read()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$info['size_download'] += \strlen($data);
|
||||
$multi->handlesActivity[$id][] = $data;
|
||||
}
|
||||
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
} catch (\Throwable $e) {
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = $e;
|
||||
} finally {
|
||||
$info['download_content_length'] = $info['size_download'];
|
||||
}
|
||||
|
||||
self::stopLoop();
|
||||
}
|
||||
|
||||
private static function followRedirects(Request $originRequest, AmpClientState $multi, array &$info, array &$headers, CancellationTokenSource $canceller, array $options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
|
||||
{
|
||||
yield $pause;
|
||||
|
||||
$originRequest->setBody(new AmpBody($options['body'], $info, $onProgress));
|
||||
$response = yield $multi->request($options, $originRequest, $canceller->getToken(), $info, $onProgress, $handle);
|
||||
$previousUrl = null;
|
||||
|
||||
while (true) {
|
||||
self::addResponseHeaders($response, $info, $headers);
|
||||
$status = $response->getStatus();
|
||||
|
||||
if (!\in_array($status, [301, 302, 303, 307, 308], true) || null === $location = $response->getHeader('location')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$urlResolver = new class() {
|
||||
use HttpClientTrait {
|
||||
parseUrl as public;
|
||||
resolveUrl as public;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
$previousUrl = $previousUrl ?? $urlResolver::parseUrl($info['url']);
|
||||
$location = $urlResolver::parseUrl($location);
|
||||
$location = $urlResolver::resolveUrl($location, $previousUrl);
|
||||
$info['redirect_url'] = implode('', $location);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (0 >= $options['max_redirects'] || $info['redirect_count'] >= $options['max_redirects']) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$logger && $logger->info(sprintf('Redirecting: "%s %s"', $status, $info['url']));
|
||||
|
||||
try {
|
||||
// Discard body of redirects
|
||||
while (null !== yield $response->getBody()->read()) {
|
||||
}
|
||||
} catch (HttpException|StreamException $e) {
|
||||
// Ignore streaming errors on previous responses
|
||||
}
|
||||
|
||||
++$info['redirect_count'];
|
||||
$info['url'] = $info['redirect_url'];
|
||||
$info['redirect_url'] = null;
|
||||
$previousUrl = $location;
|
||||
|
||||
$request = new Request($info['url'], $info['http_method']);
|
||||
$request->setProtocolVersions($originRequest->getProtocolVersions());
|
||||
$request->setTcpConnectTimeout($originRequest->getTcpConnectTimeout());
|
||||
$request->setTlsHandshakeTimeout($originRequest->getTlsHandshakeTimeout());
|
||||
$request->setTransferTimeout($originRequest->getTransferTimeout());
|
||||
|
||||
if (\in_array($status, [301, 302, 303], true)) {
|
||||
$originRequest->removeHeader('transfer-encoding');
|
||||
$originRequest->removeHeader('content-length');
|
||||
$originRequest->removeHeader('content-type');
|
||||
|
||||
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
|
||||
if ('POST' === $response->getRequest()->getMethod() || 303 === $status) {
|
||||
$info['http_method'] = 'HEAD' === $response->getRequest()->getMethod() ? 'HEAD' : 'GET';
|
||||
$request->setMethod($info['http_method']);
|
||||
}
|
||||
} else {
|
||||
$request->setBody(AmpBody::rewind($response->getRequest()->getBody()));
|
||||
}
|
||||
|
||||
foreach ($originRequest->getRawHeaders() as [$name, $value]) {
|
||||
$request->addHeader($name, $value);
|
||||
}
|
||||
|
||||
if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) {
|
||||
$request->removeHeader('authorization');
|
||||
$request->removeHeader('cookie');
|
||||
$request->removeHeader('host');
|
||||
}
|
||||
|
||||
yield $pause;
|
||||
|
||||
$response = yield $multi->request($options, $request, $canceller->getToken(), $info, $onProgress, $handle);
|
||||
$info['redirect_time'] = microtime(true) - $info['start_time'];
|
||||
}
|
||||
}
|
||||
|
||||
private static function addResponseHeaders(Response $response, array &$info, array &$headers): void
|
||||
{
|
||||
$info['http_code'] = $response->getStatus();
|
||||
|
||||
if ($headers) {
|
||||
$info['debug'] .= "< \r\n";
|
||||
$headers = [];
|
||||
}
|
||||
|
||||
$h = sprintf('HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatus(), $response->getReason());
|
||||
$info['debug'] .= "< {$h}\r\n";
|
||||
$info['response_headers'][] = $h;
|
||||
|
||||
foreach ($response->getRawHeaders() as [$name, $value]) {
|
||||
$headers[strtolower($name)][] = $value;
|
||||
$h = $name.': '.$value;
|
||||
$info['debug'] .= "< {$h}\r\n";
|
||||
$info['response_headers'][] = $h;
|
||||
}
|
||||
|
||||
$info['debug'] .= "< \r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts pushed responses only if their headers related to authentication match the request.
|
||||
*/
|
||||
private static function getPushedResponse(Request $request, AmpClientState $multi, array &$info, array &$headers, array $options, ?LoggerInterface $logger)
|
||||
{
|
||||
if ('' !== $options['body']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$authority = $request->getUri()->getAuthority();
|
||||
|
||||
foreach ($multi->pushedResponses[$authority] ?? [] as $i => [$pushedUrl, $pushDeferred, $pushedRequest, $pushedResponse, $parentOptions]) {
|
||||
if ($info['url'] !== $pushedUrl || $info['http_method'] !== $pushedRequest->getMethod()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($parentOptions as $k => $v) {
|
||||
if ($options[$k] !== $v) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
|
||||
if ($pushedRequest->getHeaderArray($k) !== $request->getHeaderArray($k)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$response = yield $pushedResponse;
|
||||
|
||||
foreach ($response->getHeaderArray('vary') as $vary) {
|
||||
foreach (preg_split('/\s*+,\s*+/', $vary) as $v) {
|
||||
if ('*' === $v || ($pushedRequest->getHeaderArray($v) !== $request->getHeaderArray($v) && 'accept-encoding' !== strtolower($v))) {
|
||||
$logger && $logger->debug(sprintf('Skipping pushed response: "%s"', $info['url']));
|
||||
continue 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pushDeferred->resolve();
|
||||
$logger && $logger->debug(sprintf('Accepting pushed response: "%s %s"', $info['http_method'], $info['url']));
|
||||
self::addResponseHeaders($response, $info, $headers);
|
||||
unset($multi->pushedResponses[$authority][$i]);
|
||||
|
||||
if (!$multi->pushedResponses[$authority]) {
|
||||
unset($multi->pushedResponses[$authority]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
private static function stopLoop(): void
|
||||
{
|
||||
if (null !== self::$delay) {
|
||||
Loop::cancel(self::$delay);
|
||||
self::$delay = null;
|
||||
}
|
||||
|
||||
Loop::defer([Loop::class, 'stop']);
|
||||
}
|
||||
}
|
195
vendor/symfony/http-client/Response/AsyncContext.php
vendored
Normal file
195
vendor/symfony/http-client/Response/AsyncContext.php
vendored
Normal file
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Component\HttpClient\Chunk\DataChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\LastChunk;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* A DTO to work with AsyncResponse.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class AsyncContext
|
||||
{
|
||||
private $passthru;
|
||||
private $client;
|
||||
private $response;
|
||||
private $info = [];
|
||||
private $content;
|
||||
private $offset;
|
||||
|
||||
public function __construct(&$passthru, HttpClientInterface $client, ResponseInterface &$response, array &$info, $content, int $offset)
|
||||
{
|
||||
$this->passthru = &$passthru;
|
||||
$this->client = $client;
|
||||
$this->response = &$response;
|
||||
$this->info = &$info;
|
||||
$this->content = $content;
|
||||
$this->offset = $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP status without consuming the response.
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->response->getInfo('http_code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the headers without consuming the response.
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
$headers = [];
|
||||
|
||||
foreach ($this->response->getInfo('response_headers') as $h) {
|
||||
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([123456789]\d\d)(?: |$)#', $h, $m)) {
|
||||
$headers = [];
|
||||
} elseif (2 === \count($m = explode(':', $h, 2))) {
|
||||
$headers[strtolower($m[0])][] = ltrim($m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource|null The PHP stream resource where the content is buffered, if it is
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new chunk of content.
|
||||
*/
|
||||
public function createChunk(string $data): ChunkInterface
|
||||
{
|
||||
return new DataChunk($this->offset, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the request for the given number of seconds.
|
||||
*/
|
||||
public function pause(float $duration): void
|
||||
{
|
||||
if (\is_callable($pause = $this->response->getInfo('pause_handler'))) {
|
||||
$pause($duration);
|
||||
} elseif (0 < $duration) {
|
||||
usleep((int) (1E6 * $duration));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the request and returns the last chunk to yield.
|
||||
*/
|
||||
public function cancel(): ChunkInterface
|
||||
{
|
||||
$this->info['canceled'] = true;
|
||||
$this->info['error'] = 'Response has been canceled.';
|
||||
$this->response->cancel();
|
||||
|
||||
return new LastChunk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current info of the response.
|
||||
*/
|
||||
public function getInfo(?string $type = null)
|
||||
{
|
||||
if (null !== $type) {
|
||||
return $this->info[$type] ?? $this->response->getInfo($type);
|
||||
}
|
||||
|
||||
return $this->info + $this->response->getInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches an info to the response.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setInfo(string $type, $value): self
|
||||
{
|
||||
if ('canceled' === $type && $value !== $this->info['canceled']) {
|
||||
throw new \LogicException('You cannot set the "canceled" info directly.');
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
unset($this->info[$type]);
|
||||
} else {
|
||||
$this->info[$type] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently processed response.
|
||||
*/
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the currently processed response by doing a new request.
|
||||
*/
|
||||
public function replaceRequest(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$this->info['previous_info'][] = $info = $this->response->getInfo();
|
||||
if (null !== $onProgress = $options['on_progress'] ?? null) {
|
||||
$thisInfo = &$this->info;
|
||||
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
|
||||
$onProgress($dlNow, $dlSize, $thisInfo + $info);
|
||||
};
|
||||
}
|
||||
if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) {
|
||||
if (0 >= $options['max_duration'] = $info['max_duration'] - $info['total_time']) {
|
||||
throw new TransportException(sprintf('Max duration was reached for "%s".', $info['url']));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response = $this->client->request($method, $url, ['buffer' => false] + $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the currently processed response by another one.
|
||||
*/
|
||||
public function replaceResponse(ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$this->info['previous_info'][] = $this->response->getInfo();
|
||||
|
||||
return $this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces or removes the chunk filter iterator.
|
||||
*
|
||||
* @param ?callable(ChunkInterface, self): ?\Iterator $passthru
|
||||
*/
|
||||
public function passthru(?callable $passthru = null): void
|
||||
{
|
||||
$this->passthru = $passthru ?? static function ($chunk, $context) {
|
||||
$context->passthru = null;
|
||||
|
||||
yield $chunk;
|
||||
};
|
||||
}
|
||||
}
|
473
vendor/symfony/http-client/Response/AsyncResponse.php
vendored
Normal file
473
vendor/symfony/http-client/Response/AsyncResponse.php
vendored
Normal file
|
@ -0,0 +1,473 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\LastChunk;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Provides a single extension point to process a response's content stream.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class AsyncResponse implements ResponseInterface, StreamableInterface
|
||||
{
|
||||
use CommonResponseTrait;
|
||||
|
||||
private const FIRST_CHUNK_YIELDED = 1;
|
||||
private const LAST_CHUNK_YIELDED = 2;
|
||||
|
||||
private $client;
|
||||
private $response;
|
||||
private $info = ['canceled' => false];
|
||||
private $passthru;
|
||||
private $stream;
|
||||
private $yieldedState;
|
||||
|
||||
/**
|
||||
* @param ?callable(ChunkInterface, AsyncContext): ?\Iterator $passthru
|
||||
*/
|
||||
public function __construct(HttpClientInterface $client, string $method, string $url, array $options, ?callable $passthru = null)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->shouldBuffer = $options['buffer'] ?? true;
|
||||
|
||||
if (null !== $onProgress = $options['on_progress'] ?? null) {
|
||||
$thisInfo = &$this->info;
|
||||
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
|
||||
$onProgress($dlNow, $dlSize, $thisInfo + $info);
|
||||
};
|
||||
}
|
||||
$this->response = $client->request($method, $url, ['buffer' => false] + $options);
|
||||
$this->passthru = $passthru;
|
||||
$this->initializer = static function (self $response, ?float $timeout = null) {
|
||||
if (null === $response->shouldBuffer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
foreach (self::stream([$response], $timeout) as $chunk) {
|
||||
if ($chunk->isTimeout() && $response->passthru) {
|
||||
// Timeouts thrown during initialization are transport errors
|
||||
foreach (self::passthru($response->client, $response, new ErrorChunk($response->offset, new TransportException($chunk->getError()))) as $chunk) {
|
||||
if ($chunk->isFirst()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if ($chunk->isFirst()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if (\array_key_exists('user_data', $options)) {
|
||||
$this->info['user_data'] = $options['user_data'];
|
||||
}
|
||||
if (\array_key_exists('max_duration', $options)) {
|
||||
$this->info['max_duration'] = $options['max_duration'];
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
if ($this->initializer) {
|
||||
self::initialize($this);
|
||||
}
|
||||
|
||||
return $this->response->getStatusCode();
|
||||
}
|
||||
|
||||
public function getHeaders(bool $throw = true): array
|
||||
{
|
||||
if ($this->initializer) {
|
||||
self::initialize($this);
|
||||
}
|
||||
|
||||
$headers = $this->response->getHeaders(false);
|
||||
|
||||
if ($throw) {
|
||||
$this->checkStatusCode();
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
public function getInfo(?string $type = null)
|
||||
{
|
||||
if (null !== $type) {
|
||||
return $this->info[$type] ?? $this->response->getInfo($type);
|
||||
}
|
||||
|
||||
return $this->info + $this->response->getInfo();
|
||||
}
|
||||
|
||||
public function toStream(bool $throw = true)
|
||||
{
|
||||
if ($throw) {
|
||||
// Ensure headers arrived
|
||||
$this->getHeaders(true);
|
||||
}
|
||||
|
||||
$handle = function () {
|
||||
$stream = $this->response instanceof StreamableInterface ? $this->response->toStream(false) : StreamWrapper::createResource($this->response);
|
||||
|
||||
return stream_get_meta_data($stream)['wrapper_data']->stream_cast(\STREAM_CAST_FOR_SELECT);
|
||||
};
|
||||
|
||||
$stream = StreamWrapper::createResource($this);
|
||||
stream_get_meta_data($stream)['wrapper_data']
|
||||
->bindHandles($handle, $this->content);
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
public function cancel(): void
|
||||
{
|
||||
if ($this->info['canceled']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info['canceled'] = true;
|
||||
$this->info['error'] = 'Response has been canceled.';
|
||||
$this->close();
|
||||
$client = $this->client;
|
||||
$this->client = null;
|
||||
|
||||
if (!$this->passthru) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach (self::passthru($client, $this, new LastChunk()) as $chunk) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
$this->passthru = null;
|
||||
} catch (ExceptionInterface $e) {
|
||||
// ignore any errors when canceling
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$httpException = null;
|
||||
|
||||
if ($this->initializer && null === $this->getInfo('error')) {
|
||||
try {
|
||||
self::initialize($this, -0.0);
|
||||
$this->getHeaders(true);
|
||||
} catch (HttpExceptionInterface $httpException) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->passthru && null === $this->getInfo('error')) {
|
||||
$this->info['canceled'] = true;
|
||||
|
||||
try {
|
||||
foreach (self::passthru($this->client, $this, new LastChunk()) as $chunk) {
|
||||
// no-op
|
||||
}
|
||||
} catch (ExceptionInterface $e) {
|
||||
// ignore any errors when destructing
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $httpException) {
|
||||
throw $httpException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function stream(iterable $responses, ?float $timeout = null, ?string $class = null): \Generator
|
||||
{
|
||||
while ($responses) {
|
||||
$wrappedResponses = [];
|
||||
$asyncMap = new \SplObjectStorage();
|
||||
$client = null;
|
||||
|
||||
foreach ($responses as $r) {
|
||||
if (!$r instanceof self) {
|
||||
throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', $class ?? static::class, get_debug_type($r)));
|
||||
}
|
||||
|
||||
if (null !== $e = $r->info['error'] ?? null) {
|
||||
yield $r => $chunk = new ErrorChunk($r->offset, new TransportException($e));
|
||||
$chunk->didThrow() ?: $chunk->getContent();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null === $client) {
|
||||
$client = $r->client;
|
||||
} elseif ($r->client !== $client) {
|
||||
throw new TransportException('Cannot stream AsyncResponse objects with many clients.');
|
||||
}
|
||||
|
||||
$asyncMap[$r->response] = $r;
|
||||
$wrappedResponses[] = $r->response;
|
||||
|
||||
if ($r->stream) {
|
||||
yield from self::passthruStream($response = $r->response, $r, new FirstChunk(), $asyncMap);
|
||||
|
||||
if (!isset($asyncMap[$response])) {
|
||||
array_pop($wrappedResponses);
|
||||
}
|
||||
|
||||
if ($r->response !== $response && !isset($asyncMap[$r->response])) {
|
||||
$asyncMap[$r->response] = $r;
|
||||
$wrappedResponses[] = $r->response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$client || !$wrappedResponses) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) {
|
||||
$r = $asyncMap[$response];
|
||||
|
||||
if (null === $chunk->getError()) {
|
||||
if ($chunk->isFirst()) {
|
||||
// Ensure no exception is thrown on destruct for the wrapped response
|
||||
$r->response->getStatusCode();
|
||||
} elseif (0 === $r->offset && null === $r->content && $chunk->isLast()) {
|
||||
$r->content = fopen('php://memory', 'w+');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$r->passthru) {
|
||||
if (null !== $chunk->getError() || $chunk->isLast()) {
|
||||
unset($asyncMap[$response]);
|
||||
} elseif (null !== $r->content && '' !== ($content = $chunk->getContent()) && \strlen($content) !== fwrite($r->content, $content)) {
|
||||
$chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
|
||||
$r->info['error'] = $chunk->getError();
|
||||
$r->response->cancel();
|
||||
}
|
||||
|
||||
yield $r => $chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (null !== $chunk->getError()) {
|
||||
// no-op
|
||||
} elseif ($chunk->isFirst()) {
|
||||
$r->yieldedState = self::FIRST_CHUNK_YIELDED;
|
||||
} elseif (self::FIRST_CHUNK_YIELDED !== $r->yieldedState && null === $chunk->getInformationalStatus()) {
|
||||
throw new \LogicException(sprintf('Instance of "%s" is already consumed and cannot be managed by "%s". A decorated client should not call any of the response\'s methods in its "request()" method.', get_debug_type($response), $class ?? static::class));
|
||||
}
|
||||
|
||||
foreach (self::passthru($r->client, $r, $chunk, $asyncMap) as $chunk) {
|
||||
yield $r => $chunk;
|
||||
}
|
||||
|
||||
if ($r->response !== $response && isset($asyncMap[$response])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $chunk->getError() && $chunk->isLast()) {
|
||||
$r->yieldedState = self::LAST_CHUNK_YIELDED;
|
||||
}
|
||||
if (null === $chunk->getError() && self::LAST_CHUNK_YIELDED !== $r->yieldedState && $r->response === $response && null !== $r->client) {
|
||||
throw new \LogicException('A chunk passthru must yield an "isLast()" chunk before ending a stream.');
|
||||
}
|
||||
|
||||
$responses = [];
|
||||
foreach ($asyncMap as $response) {
|
||||
$r = $asyncMap[$response];
|
||||
|
||||
if (null !== $r->client) {
|
||||
$responses[] = $asyncMap[$response];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SplObjectStorage<ResponseInterface, AsyncResponse>|null $asyncMap
|
||||
*/
|
||||
private static function passthru(HttpClientInterface $client, self $r, ChunkInterface $chunk, ?\SplObjectStorage $asyncMap = null): \Generator
|
||||
{
|
||||
$r->stream = null;
|
||||
$response = $r->response;
|
||||
$context = new AsyncContext($r->passthru, $client, $r->response, $r->info, $r->content, $r->offset);
|
||||
if (null === $stream = ($r->passthru)($chunk, $context)) {
|
||||
if ($r->response === $response && (null !== $chunk->getError() || $chunk->isLast())) {
|
||||
throw new \LogicException('A chunk passthru cannot swallow the last chunk.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$stream instanceof \Iterator) {
|
||||
throw new \LogicException(sprintf('A chunk passthru must return an "Iterator", "%s" returned.', get_debug_type($stream)));
|
||||
}
|
||||
$r->stream = $stream;
|
||||
|
||||
yield from self::passthruStream($response, $r, null, $asyncMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \SplObjectStorage<ResponseInterface, AsyncResponse>|null $asyncMap
|
||||
*/
|
||||
private static function passthruStream(ResponseInterface $response, self $r, ?ChunkInterface $chunk, ?\SplObjectStorage $asyncMap): \Generator
|
||||
{
|
||||
while (true) {
|
||||
try {
|
||||
if (null !== $chunk && $r->stream) {
|
||||
$r->stream->next();
|
||||
}
|
||||
|
||||
if (!$r->stream || !$r->stream->valid() || !$r->stream) {
|
||||
$r->stream = null;
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
unset($asyncMap[$response]);
|
||||
$r->stream = null;
|
||||
$r->info['error'] = $e->getMessage();
|
||||
$r->response->cancel();
|
||||
|
||||
yield $r => $chunk = new ErrorChunk($r->offset, $e);
|
||||
$chunk->didThrow() ?: $chunk->getContent();
|
||||
break;
|
||||
}
|
||||
|
||||
$chunk = $r->stream->current();
|
||||
|
||||
if (!$chunk instanceof ChunkInterface) {
|
||||
throw new \LogicException(sprintf('A chunk passthru must yield instances of "%s", "%s" yielded.', ChunkInterface::class, get_debug_type($chunk)));
|
||||
}
|
||||
|
||||
if (null !== $chunk->getError()) {
|
||||
// no-op
|
||||
} elseif ($chunk->isFirst()) {
|
||||
$e = $r->openBuffer();
|
||||
|
||||
yield $r => $chunk;
|
||||
|
||||
if ($r->initializer && null === $r->getInfo('error')) {
|
||||
// Ensure the HTTP status code is always checked
|
||||
$r->getHeaders(true);
|
||||
}
|
||||
|
||||
if (null === $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$r->response->cancel();
|
||||
$chunk = new ErrorChunk($r->offset, $e);
|
||||
} elseif ('' !== $content = $chunk->getContent()) {
|
||||
if (null !== $r->shouldBuffer) {
|
||||
throw new \LogicException('A chunk passthru must yield an "isFirst()" chunk before any content chunk.');
|
||||
}
|
||||
|
||||
if (null !== $r->content && \strlen($content) !== fwrite($r->content, $content)) {
|
||||
$chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
|
||||
$r->info['error'] = $chunk->getError();
|
||||
$r->response->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $chunk->getError() || $chunk->isLast()) {
|
||||
$stream = $r->stream;
|
||||
$r->stream = null;
|
||||
unset($asyncMap[$response]);
|
||||
}
|
||||
|
||||
if (null === $chunk->getError()) {
|
||||
$r->offset += \strlen($content);
|
||||
|
||||
yield $r => $chunk;
|
||||
|
||||
if (!$chunk->isLast()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stream->next();
|
||||
|
||||
if ($stream->valid()) {
|
||||
throw new \LogicException('A chunk passthru cannot yield after an "isLast()" chunk.');
|
||||
}
|
||||
|
||||
$r->passthru = null;
|
||||
} else {
|
||||
if ($chunk instanceof ErrorChunk) {
|
||||
$chunk->didThrow(false);
|
||||
} else {
|
||||
try {
|
||||
$chunk = new ErrorChunk($chunk->getOffset(), !$chunk->isTimeout() ?: $chunk->getError());
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$chunk = new ErrorChunk($chunk->getOffset(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
yield $r => $chunk;
|
||||
$chunk->didThrow() ?: $chunk->getContent();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function openBuffer(): ?\Throwable
|
||||
{
|
||||
if (null === $shouldBuffer = $this->shouldBuffer) {
|
||||
throw new \LogicException('A chunk passthru cannot yield more than one "isFirst()" chunk.');
|
||||
}
|
||||
|
||||
$e = $this->shouldBuffer = null;
|
||||
|
||||
if ($shouldBuffer instanceof \Closure) {
|
||||
try {
|
||||
$shouldBuffer = $shouldBuffer($this->getHeaders(false));
|
||||
|
||||
if (null !== $e = $this->response->getInfo('error')) {
|
||||
throw new TransportException($e);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->info['error'] = $e->getMessage();
|
||||
$this->response->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
if (true === $shouldBuffer) {
|
||||
$this->content = fopen('php://temp', 'w+');
|
||||
} elseif (\is_resource($shouldBuffer)) {
|
||||
$this->content = $shouldBuffer;
|
||||
}
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
private function close(): void
|
||||
{
|
||||
$this->response->cancel();
|
||||
}
|
||||
}
|
185
vendor/symfony/http-client/Response/CommonResponseTrait.php
vendored
Normal file
185
vendor/symfony/http-client/Response/CommonResponseTrait.php
vendored
Normal file
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\HttpClient\Exception\JsonException;
|
||||
use Symfony\Component\HttpClient\Exception\RedirectionException;
|
||||
use Symfony\Component\HttpClient\Exception\ServerException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
|
||||
/**
|
||||
* Implements common logic for response classes.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait CommonResponseTrait
|
||||
{
|
||||
/**
|
||||
* @var callable|null A callback that tells whether we're waiting for response headers
|
||||
*/
|
||||
private $initializer;
|
||||
private $shouldBuffer;
|
||||
private $content;
|
||||
private $offset = 0;
|
||||
private $jsonData;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContent(bool $throw = true): string
|
||||
{
|
||||
if ($this->initializer) {
|
||||
self::initialize($this);
|
||||
}
|
||||
|
||||
if ($throw) {
|
||||
$this->checkStatusCode();
|
||||
}
|
||||
|
||||
if (null === $this->content) {
|
||||
$content = null;
|
||||
|
||||
foreach (self::stream([$this]) as $chunk) {
|
||||
if (!$chunk->isLast()) {
|
||||
$content .= $chunk->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $content) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if (null === $this->content) {
|
||||
throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
|
||||
}
|
||||
} else {
|
||||
foreach (self::stream([$this]) as $chunk) {
|
||||
// Chunks are buffered in $this->content already
|
||||
}
|
||||
}
|
||||
|
||||
rewind($this->content);
|
||||
|
||||
return stream_get_contents($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toArray(bool $throw = true): array
|
||||
{
|
||||
if ('' === $content = $this->getContent($throw)) {
|
||||
throw new JsonException('Response body is empty.');
|
||||
}
|
||||
|
||||
if (null !== $this->jsonData) {
|
||||
return $this->jsonData;
|
||||
}
|
||||
|
||||
try {
|
||||
$content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
|
||||
} catch (\JsonException $e) {
|
||||
throw new JsonException($e->getMessage().sprintf(' for "%s".', $this->getInfo('url')), $e->getCode());
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) {
|
||||
throw new JsonException(json_last_error_msg().sprintf(' for "%s".', $this->getInfo('url')), json_last_error());
|
||||
}
|
||||
|
||||
if (!\is_array($content)) {
|
||||
throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".', get_debug_type($content), $this->getInfo('url')));
|
||||
}
|
||||
|
||||
if (null !== $this->content) {
|
||||
// Option "buffer" is true
|
||||
return $this->jsonData = $content;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toStream(bool $throw = true)
|
||||
{
|
||||
if ($throw) {
|
||||
// Ensure headers arrived
|
||||
$this->getHeaders($throw);
|
||||
}
|
||||
|
||||
$stream = StreamWrapper::createResource($this);
|
||||
stream_get_meta_data($stream)['wrapper_data']
|
||||
->bindHandles($this->handle, $this->content);
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the response and all its network handles.
|
||||
*/
|
||||
abstract protected function close(): void;
|
||||
|
||||
private static function initialize(self $response): void
|
||||
{
|
||||
if (null !== $response->getInfo('error')) {
|
||||
throw new TransportException($response->getInfo('error'));
|
||||
}
|
||||
|
||||
try {
|
||||
if (($response->initializer)($response, -0.0)) {
|
||||
foreach (self::stream([$response], -0.0) as $chunk) {
|
||||
if ($chunk->isFirst()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Persist timeouts thrown during initialization
|
||||
$response->info['error'] = $e->getMessage();
|
||||
$response->close();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$response->initializer = null;
|
||||
}
|
||||
|
||||
private function checkStatusCode()
|
||||
{
|
||||
$code = $this->getInfo('http_code');
|
||||
|
||||
if (500 <= $code) {
|
||||
throw new ServerException($this);
|
||||
}
|
||||
|
||||
if (400 <= $code) {
|
||||
throw new ClientException($this);
|
||||
}
|
||||
|
||||
if (300 <= $code) {
|
||||
throw new RedirectionException($this);
|
||||
}
|
||||
}
|
||||
}
|
472
vendor/symfony/http-client/Response/CurlResponse.php
vendored
Normal file
472
vendor/symfony/http-client/Response/CurlResponse.php
vendored
Normal file
|
@ -0,0 +1,472 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\Internal\Canary;
|
||||
use Symfony\Component\HttpClient\Internal\ClientState;
|
||||
use Symfony\Component\HttpClient\Internal\CurlClientState;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CurlResponse implements ResponseInterface, StreamableInterface
|
||||
{
|
||||
use CommonResponseTrait {
|
||||
getContent as private doGetContent;
|
||||
}
|
||||
use TransportResponseTrait;
|
||||
|
||||
private $multi;
|
||||
private $debugBuffer;
|
||||
|
||||
/**
|
||||
* @param \CurlHandle|resource|string $ch
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(CurlClientState $multi, $ch, ?array $options = null, ?LoggerInterface $logger = null, string $method = 'GET', ?callable $resolveRedirect = null, ?int $curlVersion = null)
|
||||
{
|
||||
$this->multi = $multi;
|
||||
|
||||
if (\is_resource($ch) || $ch instanceof \CurlHandle) {
|
||||
$this->handle = $ch;
|
||||
$this->debugBuffer = fopen('php://temp', 'w+');
|
||||
if (0x074000 === $curlVersion) {
|
||||
fwrite($this->debugBuffer, 'Due to a bug in curl 7.64.0, the debug log is disabled; use another version to work around the issue.');
|
||||
} else {
|
||||
curl_setopt($ch, \CURLOPT_VERBOSE, true);
|
||||
curl_setopt($ch, \CURLOPT_STDERR, $this->debugBuffer);
|
||||
}
|
||||
} else {
|
||||
$this->info['url'] = $ch;
|
||||
$ch = $this->handle;
|
||||
}
|
||||
|
||||
$this->id = $id = (int) $ch;
|
||||
$this->logger = $logger;
|
||||
$this->shouldBuffer = $options['buffer'] ?? true;
|
||||
$this->timeout = $options['timeout'] ?? null;
|
||||
$this->info['http_method'] = $method;
|
||||
$this->info['user_data'] = $options['user_data'] ?? null;
|
||||
$this->info['max_duration'] = $options['max_duration'] ?? null;
|
||||
$this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
|
||||
$info = &$this->info;
|
||||
$headers = &$this->headers;
|
||||
$debugBuffer = $this->debugBuffer;
|
||||
|
||||
if (!$info['response_headers']) {
|
||||
// Used to keep track of what we're waiting for
|
||||
curl_setopt($ch, \CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter
|
||||
}
|
||||
|
||||
curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int {
|
||||
return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
|
||||
});
|
||||
|
||||
if (null === $options) {
|
||||
// Pushed response: buffer until requested
|
||||
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
|
||||
$multi->handlesActivity[$id][] = $data;
|
||||
curl_pause($ch, \CURLPAUSE_RECV);
|
||||
|
||||
return \strlen($data);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$execCounter = $multi->execCounter;
|
||||
$this->info['pause_handler'] = static function (float $duration) use ($ch, $multi, $execCounter) {
|
||||
if (0 < $duration) {
|
||||
if ($execCounter === $multi->execCounter) {
|
||||
curl_multi_remove_handle($multi->handle, $ch);
|
||||
}
|
||||
|
||||
$lastExpiry = end($multi->pauseExpiries);
|
||||
$multi->pauseExpiries[(int) $ch] = $duration += microtime(true);
|
||||
if (false !== $lastExpiry && $lastExpiry > $duration) {
|
||||
asort($multi->pauseExpiries);
|
||||
}
|
||||
curl_pause($ch, \CURLPAUSE_ALL);
|
||||
} else {
|
||||
unset($multi->pauseExpiries[(int) $ch]);
|
||||
curl_pause($ch, \CURLPAUSE_CONT);
|
||||
curl_multi_add_handle($multi->handle, $ch);
|
||||
}
|
||||
};
|
||||
|
||||
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
|
||||
curl_pause($ch, \CURLPAUSE_CONT);
|
||||
|
||||
if ($onProgress = $options['on_progress']) {
|
||||
$url = isset($info['url']) ? ['url' => $info['url']] : [];
|
||||
curl_setopt($ch, \CURLOPT_NOPROGRESS, false);
|
||||
curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) {
|
||||
try {
|
||||
rewind($debugBuffer);
|
||||
$debug = ['debug' => stream_get_contents($debugBuffer)];
|
||||
$onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug);
|
||||
} catch (\Throwable $e) {
|
||||
$multi->handlesActivity[(int) $ch][] = null;
|
||||
$multi->handlesActivity[(int) $ch][] = $e;
|
||||
|
||||
return 1; // Abort the request
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
|
||||
if ('H' === (curl_getinfo($ch, \CURLINFO_PRIVATE)[0] ?? null)) {
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = new TransportException(sprintf('Unsupported protocol for "%s"', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
|
||||
$multi->handlesActivity[$id][] = $data;
|
||||
|
||||
return \strlen($data);
|
||||
});
|
||||
|
||||
$multi->handlesActivity[$id][] = $data;
|
||||
|
||||
return \strlen($data);
|
||||
});
|
||||
|
||||
$this->initializer = static function (self $response) {
|
||||
$waitFor = curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE);
|
||||
|
||||
return 'H' === $waitFor[0];
|
||||
};
|
||||
|
||||
// Schedule the request in a non-blocking way
|
||||
$multi->lastTimeout = null;
|
||||
$multi->openHandles[$id] = [$ch, $options];
|
||||
curl_multi_add_handle($multi->handle, $ch);
|
||||
|
||||
$this->canary = new Canary(static function () use ($ch, $multi, $id) {
|
||||
unset($multi->pauseExpiries[$id], $multi->openHandles[$id], $multi->handlesActivity[$id]);
|
||||
curl_setopt($ch, \CURLOPT_PRIVATE, '_0');
|
||||
|
||||
if ($multi->performing) {
|
||||
return;
|
||||
}
|
||||
|
||||
curl_multi_remove_handle($multi->handle, $ch);
|
||||
curl_setopt_array($ch, [
|
||||
\CURLOPT_NOPROGRESS => true,
|
||||
\CURLOPT_PROGRESSFUNCTION => null,
|
||||
\CURLOPT_HEADERFUNCTION => null,
|
||||
\CURLOPT_WRITEFUNCTION => null,
|
||||
\CURLOPT_READFUNCTION => null,
|
||||
\CURLOPT_INFILE => null,
|
||||
]);
|
||||
|
||||
if (!$multi->openHandles) {
|
||||
// Schedule DNS cache eviction for the next request
|
||||
$multi->dnsCache->evictions = $multi->dnsCache->evictions ?: $multi->dnsCache->removals;
|
||||
$multi->dnsCache->removals = $multi->dnsCache->hostnames = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInfo(?string $type = null)
|
||||
{
|
||||
if (!$info = $this->finalInfo) {
|
||||
$info = array_merge($this->info, curl_getinfo($this->handle));
|
||||
$info['url'] = $this->info['url'] ?? $info['url'];
|
||||
$info['redirect_url'] = $this->info['redirect_url'] ?? null;
|
||||
|
||||
// workaround curl not subtracting the time offset for pushed responses
|
||||
if (isset($this->info['url']) && $info['start_time'] / 1000 < $info['total_time']) {
|
||||
$info['total_time'] -= $info['starttransfer_time'] ?: $info['total_time'];
|
||||
$info['starttransfer_time'] = 0.0;
|
||||
}
|
||||
|
||||
rewind($this->debugBuffer);
|
||||
$info['debug'] = stream_get_contents($this->debugBuffer);
|
||||
$waitFor = curl_getinfo($this->handle, \CURLINFO_PRIVATE);
|
||||
|
||||
if ('H' !== $waitFor[0] && 'C' !== $waitFor[0]) {
|
||||
curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
|
||||
rewind($this->debugBuffer);
|
||||
ftruncate($this->debugBuffer, 0);
|
||||
$this->finalInfo = $info;
|
||||
}
|
||||
}
|
||||
|
||||
return null !== $type ? $info[$type] ?? null : $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContent(bool $throw = true): string
|
||||
{
|
||||
$performing = $this->multi->performing;
|
||||
$this->multi->performing = $performing || '_0' === curl_getinfo($this->handle, \CURLINFO_PRIVATE);
|
||||
|
||||
try {
|
||||
return $this->doGetContent($throw);
|
||||
} finally {
|
||||
$this->multi->performing = $performing;
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
try {
|
||||
if (null === $this->timeout) {
|
||||
return; // Unused pushed response
|
||||
}
|
||||
|
||||
$this->doDestruct();
|
||||
} finally {
|
||||
if (\is_resource($this->handle) || $this->handle instanceof \CurlHandle) {
|
||||
curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
private static function schedule(self $response, array &$runningResponses): void
|
||||
{
|
||||
if (isset($runningResponses[$i = (int) $response->multi->handle])) {
|
||||
$runningResponses[$i][1][$response->id] = $response;
|
||||
} else {
|
||||
$runningResponses[$i] = [$response->multi, [$response->id => $response]];
|
||||
}
|
||||
|
||||
if ('_0' === curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE)) {
|
||||
// Response already completed
|
||||
$response->multi->handlesActivity[$response->id][] = null;
|
||||
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param CurlClientState $multi
|
||||
*/
|
||||
private static function perform(ClientState $multi, ?array &$responses = null): void
|
||||
{
|
||||
if ($multi->performing) {
|
||||
if ($responses) {
|
||||
$response = current($responses);
|
||||
$multi->handlesActivity[(int) $response->handle][] = null;
|
||||
$multi->handlesActivity[(int) $response->handle][] = new TransportException(sprintf('Userland callback cannot use the client nor the response while processing "%s".', curl_getinfo($response->handle, \CURLINFO_EFFECTIVE_URL)));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$multi->performing = true;
|
||||
++$multi->execCounter;
|
||||
$active = 0;
|
||||
while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($multi->handle, $active))) {
|
||||
}
|
||||
|
||||
if (\CURLM_OK !== $err) {
|
||||
throw new TransportException(curl_multi_strerror($err));
|
||||
}
|
||||
|
||||
while ($info = curl_multi_info_read($multi->handle)) {
|
||||
if (\CURLMSG_DONE !== $info['msg']) {
|
||||
continue;
|
||||
}
|
||||
$result = $info['result'];
|
||||
$id = (int) $ch = $info['handle'];
|
||||
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
|
||||
|
||||
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /* CURLE_HTTP2 */ 16, /* CURLE_HTTP2_STREAM */ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
|
||||
curl_multi_remove_handle($multi->handle, $ch);
|
||||
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
|
||||
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
|
||||
curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);
|
||||
|
||||
if (0 === curl_multi_add_handle($multi->handle, $ch)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
|
||||
$multi->handlesActivity[$id][] = new FirstChunk();
|
||||
}
|
||||
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
|
||||
}
|
||||
} finally {
|
||||
$multi->performing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param CurlClientState $multi
|
||||
*/
|
||||
private static function select(ClientState $multi, float $timeout): int
|
||||
{
|
||||
if (\PHP_VERSION_ID < 70211) {
|
||||
// workaround https://bugs.php.net/76480
|
||||
$timeout = min($timeout, 0.01);
|
||||
}
|
||||
|
||||
if ($multi->pauseExpiries) {
|
||||
$now = microtime(true);
|
||||
|
||||
foreach ($multi->pauseExpiries as $id => $pauseExpiry) {
|
||||
if ($now < $pauseExpiry) {
|
||||
$timeout = min($timeout, $pauseExpiry - $now);
|
||||
break;
|
||||
}
|
||||
|
||||
unset($multi->pauseExpiries[$id]);
|
||||
curl_pause($multi->openHandles[$id][0], \CURLPAUSE_CONT);
|
||||
curl_multi_add_handle($multi->handle, $multi->openHandles[$id][0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (0 !== $selected = curl_multi_select($multi->handle, $timeout)) {
|
||||
return $selected;
|
||||
}
|
||||
|
||||
if ($multi->pauseExpiries && 0 < $timeout -= microtime(true) - $now) {
|
||||
usleep((int) (1E6 * $timeout));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses header lines as curl yields them to us.
|
||||
*/
|
||||
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int
|
||||
{
|
||||
if (!str_ends_with($data, "\r\n")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
|
||||
|
||||
if ('H' !== $waitFor[0]) {
|
||||
return \strlen($data); // Ignore HTTP trailers
|
||||
}
|
||||
|
||||
$statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE);
|
||||
|
||||
if ($statusCode !== $info['http_code'] && !preg_match("#^HTTP/\d+(?:\.\d+)? {$statusCode}(?: |\r\n$)#", $data)) {
|
||||
return \strlen($data); // Ignore headers from responses to CONNECT requests
|
||||
}
|
||||
|
||||
if ("\r\n" !== $data) {
|
||||
// Regular header line: add it to the list
|
||||
self::addResponseHeaders([substr($data, 0, -2)], $info, $headers);
|
||||
|
||||
if (!str_starts_with($data, 'HTTP/')) {
|
||||
if (0 === stripos($data, 'Location:')) {
|
||||
$location = trim(substr($data, 9, -2));
|
||||
}
|
||||
|
||||
return \strlen($data);
|
||||
}
|
||||
|
||||
if (\function_exists('openssl_x509_read') && $certinfo = curl_getinfo($ch, \CURLINFO_CERTINFO)) {
|
||||
$info['peer_certificate_chain'] = array_map('openssl_x509_read', array_column($certinfo, 'Cert'));
|
||||
}
|
||||
|
||||
if (300 <= $info['http_code'] && $info['http_code'] < 400) {
|
||||
if (curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
|
||||
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
|
||||
} elseif (303 === $info['http_code'] || ('POST' === $info['http_method'] && \in_array($info['http_code'], [301, 302], true))) {
|
||||
curl_setopt($ch, \CURLOPT_POSTFIELDS, '');
|
||||
}
|
||||
}
|
||||
|
||||
return \strlen($data);
|
||||
}
|
||||
|
||||
// End of headers: handle informational responses, redirects, etc.
|
||||
|
||||
if (200 > $statusCode) {
|
||||
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
|
||||
$location = null;
|
||||
|
||||
return \strlen($data);
|
||||
}
|
||||
|
||||
$info['redirect_url'] = null;
|
||||
|
||||
if (300 <= $statusCode && $statusCode < 400 && null !== $location) {
|
||||
if ($noContent = 303 === $statusCode || ('POST' === $info['http_method'] && \in_array($statusCode, [301, 302], true))) {
|
||||
$info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
|
||||
curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']);
|
||||
}
|
||||
|
||||
if (null === $info['redirect_url'] = $resolveRedirect($ch, $location, $noContent)) {
|
||||
$options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
|
||||
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
|
||||
curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);
|
||||
} else {
|
||||
$url = parse_url($location ?? ':');
|
||||
|
||||
if (isset($url['host']) && null !== $ip = $multi->dnsCache->hostnames[$url['host'] = strtolower($url['host'])] ?? null) {
|
||||
// Populate DNS cache for redirects if needed
|
||||
$port = $url['port'] ?? ('http' === ($url['scheme'] ?? parse_url(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL), \PHP_URL_SCHEME)) ? 80 : 443);
|
||||
curl_setopt($ch, \CURLOPT_RESOLVE, ["{$url['host']}:$port:$ip"]);
|
||||
$multi->dnsCache->removals["-{$url['host']}:$port"] = "-{$url['host']}:$port";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (401 === $statusCode && isset($options['auth_ntlm']) && 0 === strncasecmp($headers['www-authenticate'][0] ?? '', 'NTLM ', 5)) {
|
||||
// Continue with NTLM auth
|
||||
} elseif ($statusCode < 300 || 400 <= $statusCode || null === $location || curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
|
||||
// Headers and redirects completed, time to get the response's content
|
||||
$multi->handlesActivity[$id][] = new FirstChunk();
|
||||
|
||||
if ('HEAD' === $info['http_method'] || \in_array($statusCode, [204, 304], true)) {
|
||||
$waitFor = '_0'; // no content expected
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
} else {
|
||||
$waitFor[0] = 'C'; // C = content
|
||||
}
|
||||
|
||||
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
|
||||
} elseif (null !== $info['redirect_url'] && $logger) {
|
||||
$logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url']));
|
||||
}
|
||||
|
||||
$location = null;
|
||||
|
||||
return \strlen($data);
|
||||
}
|
||||
}
|
80
vendor/symfony/http-client/Response/HttplugPromise.php
vendored
Normal file
80
vendor/symfony/http-client/Response/HttplugPromise.php
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use GuzzleHttp\Promise\Create;
|
||||
use GuzzleHttp\Promise\PromiseInterface as GuzzlePromiseInterface;
|
||||
use Http\Promise\Promise as HttplugPromiseInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class HttplugPromise implements HttplugPromiseInterface
|
||||
{
|
||||
private $promise;
|
||||
|
||||
public function __construct(GuzzlePromiseInterface $promise)
|
||||
{
|
||||
$this->promise = $promise;
|
||||
}
|
||||
|
||||
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
|
||||
{
|
||||
return new self($this->promise->then(
|
||||
$this->wrapThenCallback($onFulfilled),
|
||||
$this->wrapThenCallback($onRejected)
|
||||
));
|
||||
}
|
||||
|
||||
public function cancel(): void
|
||||
{
|
||||
$this->promise->cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getState(): string
|
||||
{
|
||||
return $this->promise->getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return Psr7ResponseInterface|mixed
|
||||
*/
|
||||
public function wait($unwrap = true)
|
||||
{
|
||||
$result = $this->promise->wait($unwrap);
|
||||
|
||||
while ($result instanceof HttplugPromiseInterface || $result instanceof GuzzlePromiseInterface) {
|
||||
$result = $result->wait($unwrap);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function wrapThenCallback(?callable $callback): ?callable
|
||||
{
|
||||
if (null === $callback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static function ($value) use ($callback) {
|
||||
return Create::promiseFor($callback($value));
|
||||
};
|
||||
}
|
||||
}
|
343
vendor/symfony/http-client/Response/MockResponse.php
vendored
Normal file
343
vendor/symfony/http-client/Response/MockResponse.php
vendored
Normal file
|
@ -0,0 +1,343 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\Internal\ClientState;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* A test-friendly response.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class MockResponse implements ResponseInterface, StreamableInterface
|
||||
{
|
||||
use CommonResponseTrait;
|
||||
use TransportResponseTrait {
|
||||
doDestruct as public __destruct;
|
||||
}
|
||||
|
||||
private $body;
|
||||
private $requestOptions = [];
|
||||
private $requestUrl;
|
||||
private $requestMethod;
|
||||
|
||||
private static $mainMulti;
|
||||
private static $idSequence = 0;
|
||||
|
||||
/**
|
||||
* @param string|string[]|iterable $body The response body as a string or an iterable of strings,
|
||||
* yielding an empty string simulates an idle timeout,
|
||||
* throwing an exception yields an ErrorChunk
|
||||
*
|
||||
* @see ResponseInterface::getInfo() for possible info, e.g. "response_headers"
|
||||
*/
|
||||
public function __construct($body = '', array $info = [])
|
||||
{
|
||||
$this->body = is_iterable($body) ? $body : (string) $body;
|
||||
$this->info = $info + ['http_code' => 200] + $this->info;
|
||||
|
||||
if (!isset($info['response_headers'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$responseHeaders = [];
|
||||
|
||||
foreach ($info['response_headers'] as $k => $v) {
|
||||
foreach ((array) $v as $v) {
|
||||
$responseHeaders[] = (\is_string($k) ? $k.': ' : '').$v;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info['response_headers'] = [];
|
||||
self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the options used when doing the request.
|
||||
*/
|
||||
public function getRequestOptions(): array
|
||||
{
|
||||
return $this->requestOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL used when doing the request.
|
||||
*/
|
||||
public function getRequestUrl(): string
|
||||
{
|
||||
return $this->requestUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the method used when doing the request.
|
||||
*/
|
||||
public function getRequestMethod(): string
|
||||
{
|
||||
return $this->requestMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInfo(?string $type = null)
|
||||
{
|
||||
return null !== $type ? $this->info[$type] ?? null : $this->info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cancel(): void
|
||||
{
|
||||
$this->info['canceled'] = true;
|
||||
$this->info['error'] = 'Response has been canceled.';
|
||||
try {
|
||||
$this->body = null;
|
||||
} catch (TransportException $e) {
|
||||
// ignore errors when canceling
|
||||
}
|
||||
|
||||
$onProgress = $this->requestOptions['on_progress'] ?? static function () {};
|
||||
$dlSize = isset($this->headers['content-encoding']) || 'HEAD' === ($this->info['http_method'] ?? null) || \in_array($this->info['http_code'], [204, 304], true) ? 0 : (int) ($this->headers['content-length'][0] ?? 0);
|
||||
$onProgress($this->offset, $dlSize, $this->info);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function close(): void
|
||||
{
|
||||
$this->inflate = null;
|
||||
$this->body = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function fromRequest(string $method, string $url, array $options, ResponseInterface $mock): self
|
||||
{
|
||||
$response = new self([]);
|
||||
$response->requestOptions = $options;
|
||||
$response->id = ++self::$idSequence;
|
||||
$response->shouldBuffer = $options['buffer'] ?? true;
|
||||
$response->initializer = static function (self $response) {
|
||||
return \is_array($response->body[0] ?? null);
|
||||
};
|
||||
|
||||
$response->info['redirect_count'] = 0;
|
||||
$response->info['redirect_url'] = null;
|
||||
$response->info['start_time'] = microtime(true);
|
||||
$response->info['http_method'] = $method;
|
||||
$response->info['http_code'] = 0;
|
||||
$response->info['user_data'] = $options['user_data'] ?? null;
|
||||
$response->info['max_duration'] = $options['max_duration'] ?? null;
|
||||
$response->info['url'] = $url;
|
||||
|
||||
if ($mock instanceof self) {
|
||||
$mock->requestOptions = $response->requestOptions;
|
||||
$mock->requestMethod = $method;
|
||||
$mock->requestUrl = $url;
|
||||
}
|
||||
|
||||
self::writeRequest($response, $options, $mock);
|
||||
$response->body[] = [$options, $mock];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function schedule(self $response, array &$runningResponses): void
|
||||
{
|
||||
if (!$response->id) {
|
||||
throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.');
|
||||
}
|
||||
|
||||
$multi = self::$mainMulti ?? self::$mainMulti = new ClientState();
|
||||
|
||||
if (!isset($runningResponses[0])) {
|
||||
$runningResponses[0] = [$multi, []];
|
||||
}
|
||||
|
||||
$runningResponses[0][1][$response->id] = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function perform(ClientState $multi, array &$responses): void
|
||||
{
|
||||
foreach ($responses as $response) {
|
||||
$id = $response->id;
|
||||
|
||||
if (null === $response->body) {
|
||||
// Canceled response
|
||||
$response->body = [];
|
||||
} elseif ([] === $response->body) {
|
||||
// Error chunk
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
|
||||
} elseif (null === $chunk = array_shift($response->body)) {
|
||||
// Last chunk
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = array_shift($response->body);
|
||||
} elseif (\is_array($chunk)) {
|
||||
// First chunk
|
||||
try {
|
||||
$offset = 0;
|
||||
$chunk[1]->getStatusCode();
|
||||
$chunk[1]->getHeaders(false);
|
||||
self::readResponse($response, $chunk[0], $chunk[1], $offset);
|
||||
$multi->handlesActivity[$id][] = new FirstChunk();
|
||||
$buffer = $response->requestOptions['buffer'] ?? null;
|
||||
|
||||
if ($buffer instanceof \Closure && $response->content = $buffer($response->headers) ?: null) {
|
||||
$response->content = \is_resource($response->content) ? $response->content : fopen('php://temp', 'w+');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = $e;
|
||||
}
|
||||
} elseif ($chunk instanceof \Throwable) {
|
||||
$multi->handlesActivity[$id][] = null;
|
||||
$multi->handlesActivity[$id][] = $chunk;
|
||||
} else {
|
||||
// Data or timeout chunk
|
||||
$multi->handlesActivity[$id][] = $chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function select(ClientState $multi, float $timeout): int
|
||||
{
|
||||
return 42;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates sending the request.
|
||||
*/
|
||||
private static function writeRequest(self $response, array $options, ResponseInterface $mock)
|
||||
{
|
||||
$onProgress = $options['on_progress'] ?? static function () {};
|
||||
$response->info += $mock->getInfo() ?: [];
|
||||
|
||||
// simulate "size_upload" if it is set
|
||||
if (isset($response->info['size_upload'])) {
|
||||
$response->info['size_upload'] = 0.0;
|
||||
}
|
||||
|
||||
// simulate "total_time" if it is not set
|
||||
if (!isset($response->info['total_time'])) {
|
||||
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
|
||||
}
|
||||
|
||||
// "notify" DNS resolution
|
||||
$onProgress(0, 0, $response->info);
|
||||
|
||||
// consume the request body
|
||||
if (\is_resource($body = $options['body'] ?? '')) {
|
||||
$data = stream_get_contents($body);
|
||||
if (isset($response->info['size_upload'])) {
|
||||
$response->info['size_upload'] += \strlen($data);
|
||||
}
|
||||
} elseif ($body instanceof \Closure) {
|
||||
while ('' !== $data = $body(16372)) {
|
||||
if (!\is_string($data)) {
|
||||
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
|
||||
}
|
||||
|
||||
// "notify" upload progress
|
||||
if (isset($response->info['size_upload'])) {
|
||||
$response->info['size_upload'] += \strlen($data);
|
||||
}
|
||||
|
||||
$onProgress(0, 0, $response->info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates reading the response.
|
||||
*/
|
||||
private static function readResponse(self $response, array $options, ResponseInterface $mock, int &$offset)
|
||||
{
|
||||
$onProgress = $options['on_progress'] ?? static function () {};
|
||||
|
||||
// populate info related to headers
|
||||
$info = $mock->getInfo() ?: [];
|
||||
$response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode() ?: 200;
|
||||
$response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers);
|
||||
$dlSize = isset($response->headers['content-encoding']) || 'HEAD' === $response->info['http_method'] || \in_array($response->info['http_code'], [204, 304], true) ? 0 : (int) ($response->headers['content-length'][0] ?? 0);
|
||||
|
||||
$response->info = [
|
||||
'start_time' => $response->info['start_time'],
|
||||
'user_data' => $response->info['user_data'],
|
||||
'max_duration' => $response->info['max_duration'],
|
||||
'http_code' => $response->info['http_code'],
|
||||
] + $info + $response->info;
|
||||
|
||||
if (null !== $response->info['error']) {
|
||||
throw new TransportException($response->info['error']);
|
||||
}
|
||||
|
||||
if (!isset($response->info['total_time'])) {
|
||||
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
|
||||
}
|
||||
|
||||
// "notify" headers arrival
|
||||
$onProgress(0, $dlSize, $response->info);
|
||||
|
||||
// cast response body to activity list
|
||||
$body = $mock instanceof self ? $mock->body : $mock->getContent(false);
|
||||
|
||||
if (!\is_string($body)) {
|
||||
try {
|
||||
foreach ($body as $chunk) {
|
||||
if ('' === $chunk = (string) $chunk) {
|
||||
// simulate an idle timeout
|
||||
$response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
|
||||
} else {
|
||||
$response->body[] = $chunk;
|
||||
$offset += \strlen($chunk);
|
||||
// "notify" download progress
|
||||
$onProgress($offset, $dlSize, $response->info);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$response->body[] = $e;
|
||||
}
|
||||
} elseif ('' !== $body) {
|
||||
$response->body[] = $body;
|
||||
$offset = \strlen($body);
|
||||
}
|
||||
|
||||
if (!isset($response->info['total_time'])) {
|
||||
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
|
||||
}
|
||||
|
||||
// "notify" completion
|
||||
$onProgress($offset, $dlSize, $response->info);
|
||||
|
||||
if ($dlSize && $offset !== $dlSize) {
|
||||
throw new TransportException(sprintf('Transfer closed with %d bytes remaining to read.', $dlSize - $offset));
|
||||
}
|
||||
}
|
||||
}
|
376
vendor/symfony/http-client/Response/NativeResponse.php
vendored
Normal file
376
vendor/symfony/http-client/Response/NativeResponse.php
vendored
Normal file
|
@ -0,0 +1,376 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\Internal\Canary;
|
||||
use Symfony\Component\HttpClient\Internal\ClientState;
|
||||
use Symfony\Component\HttpClient\Internal\NativeClientState;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class NativeResponse implements ResponseInterface, StreamableInterface
|
||||
{
|
||||
use CommonResponseTrait;
|
||||
use TransportResponseTrait;
|
||||
|
||||
private $context;
|
||||
private $url;
|
||||
private $resolver;
|
||||
private $onProgress;
|
||||
private $remaining;
|
||||
private $buffer;
|
||||
private $multi;
|
||||
private $pauseExpiry = 0;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolver, ?callable $onProgress, ?LoggerInterface $logger)
|
||||
{
|
||||
$this->multi = $multi;
|
||||
$this->id = $id = (int) $context;
|
||||
$this->context = $context;
|
||||
$this->url = $url;
|
||||
$this->logger = $logger;
|
||||
$this->timeout = $options['timeout'];
|
||||
$this->info = &$info;
|
||||
$this->resolver = $resolver;
|
||||
$this->onProgress = $onProgress;
|
||||
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
|
||||
$this->shouldBuffer = $options['buffer'] ?? true;
|
||||
|
||||
// Temporary resource to dechunk the response stream
|
||||
$this->buffer = fopen('php://temp', 'w+');
|
||||
|
||||
$info['user_data'] = $options['user_data'];
|
||||
$info['max_duration'] = $options['max_duration'];
|
||||
++$multi->responseCount;
|
||||
|
||||
$this->initializer = static function (self $response) {
|
||||
return null === $response->remaining;
|
||||
};
|
||||
|
||||
$pauseExpiry = &$this->pauseExpiry;
|
||||
$info['pause_handler'] = static function (float $duration) use (&$pauseExpiry) {
|
||||
$pauseExpiry = 0 < $duration ? microtime(true) + $duration : 0;
|
||||
};
|
||||
|
||||
$this->canary = new Canary(static function () use ($multi, $id) {
|
||||
if (null !== ($host = $multi->openHandles[$id][6] ?? null) && 0 >= --$multi->hosts[$host]) {
|
||||
unset($multi->hosts[$host]);
|
||||
}
|
||||
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInfo(?string $type = null)
|
||||
{
|
||||
if (!$info = $this->finalInfo) {
|
||||
$info = $this->info;
|
||||
$info['url'] = implode('', $info['url']);
|
||||
unset($info['size_body'], $info['request_header']);
|
||||
|
||||
if (null === $this->buffer) {
|
||||
$this->finalInfo = $info;
|
||||
}
|
||||
}
|
||||
|
||||
return null !== $type ? $info[$type] ?? null : $info;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
try {
|
||||
$this->doDestruct();
|
||||
} finally {
|
||||
// Clear the DNS cache when all requests completed
|
||||
if (0 >= --$this->multi->responseCount) {
|
||||
$this->multi->responseCount = 0;
|
||||
$this->multi->dnsCache = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function open(): void
|
||||
{
|
||||
$url = $this->url;
|
||||
|
||||
set_error_handler(function ($type, $msg) use (&$url) {
|
||||
if (\E_NOTICE !== $type || 'fopen(): Content-type not specified assuming application/x-www-form-urlencoded' !== $msg) {
|
||||
throw new TransportException($msg);
|
||||
}
|
||||
|
||||
$this->logger && $this->logger->info(sprintf('%s for "%s".', $msg, $url ?? $this->url));
|
||||
});
|
||||
|
||||
try {
|
||||
$this->info['start_time'] = microtime(true);
|
||||
|
||||
[$resolver, $url] = ($this->resolver)($this->multi);
|
||||
|
||||
while (true) {
|
||||
$context = stream_context_get_options($this->context);
|
||||
|
||||
if ($proxy = $context['http']['proxy'] ?? null) {
|
||||
$this->info['debug'] .= "* Establish HTTP proxy tunnel to {$proxy}\n";
|
||||
$this->info['request_header'] = $url;
|
||||
} else {
|
||||
$this->info['debug'] .= "* Trying {$this->info['primary_ip']}...\n";
|
||||
$this->info['request_header'] = $this->info['url']['path'].$this->info['url']['query'];
|
||||
}
|
||||
|
||||
$this->info['request_header'] = sprintf("> %s %s HTTP/%s \r\n", $context['http']['method'], $this->info['request_header'], $context['http']['protocol_version']);
|
||||
$this->info['request_header'] .= implode("\r\n", $context['http']['header'])."\r\n\r\n";
|
||||
|
||||
if (\array_key_exists('peer_name', $context['ssl']) && null === $context['ssl']['peer_name']) {
|
||||
unset($context['ssl']['peer_name']);
|
||||
$this->context = stream_context_create([], ['options' => $context] + stream_context_get_params($this->context));
|
||||
}
|
||||
|
||||
// Send request and follow redirects when needed
|
||||
$this->handle = $h = fopen($url, 'r', false, $this->context);
|
||||
self::addResponseHeaders(stream_get_meta_data($h)['wrapper_data'], $this->info, $this->headers, $this->info['debug']);
|
||||
$url = $resolver($this->multi, $this->headers['location'][0] ?? null, $this->context);
|
||||
|
||||
if (null === $url) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->logger && $this->logger->info(sprintf('Redirecting: "%s %s"', $this->info['http_code'], $url ?? $this->url));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->close();
|
||||
$this->multi->handlesActivity[$this->id][] = null;
|
||||
$this->multi->handlesActivity[$this->id][] = $e;
|
||||
|
||||
return;
|
||||
} finally {
|
||||
$this->info['pretransfer_time'] = $this->info['total_time'] = microtime(true) - $this->info['start_time'];
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
if (isset($context['ssl']['capture_peer_cert_chain']) && isset(($context = stream_context_get_options($this->context))['ssl']['peer_certificate_chain'])) {
|
||||
$this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain'];
|
||||
}
|
||||
|
||||
stream_set_blocking($h, false);
|
||||
$this->context = $this->resolver = null;
|
||||
|
||||
// Create dechunk buffers
|
||||
if (isset($this->headers['content-length'])) {
|
||||
$this->remaining = (int) $this->headers['content-length'][0];
|
||||
} elseif ('chunked' === ($this->headers['transfer-encoding'][0] ?? null)) {
|
||||
stream_filter_append($this->buffer, 'dechunk', \STREAM_FILTER_WRITE);
|
||||
$this->remaining = -1;
|
||||
} else {
|
||||
$this->remaining = -2;
|
||||
}
|
||||
|
||||
$this->multi->handlesActivity[$this->id] = [new FirstChunk()];
|
||||
|
||||
if ('HEAD' === $context['http']['method'] || \in_array($this->info['http_code'], [204, 304], true)) {
|
||||
$this->multi->handlesActivity[$this->id][] = null;
|
||||
$this->multi->handlesActivity[$this->id][] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$host = parse_url($this->info['redirect_url'] ?? $this->url, \PHP_URL_HOST);
|
||||
$this->multi->lastTimeout = null;
|
||||
$this->multi->openHandles[$this->id] = [&$this->pauseExpiry, $h, $this->buffer, $this->onProgress, &$this->remaining, &$this->info, $host];
|
||||
$this->multi->hosts[$host] = 1 + ($this->multi->hosts[$host] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
private function close(): void
|
||||
{
|
||||
$this->canary->cancel();
|
||||
$this->handle = $this->buffer = $this->inflate = $this->onProgress = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
private static function schedule(self $response, array &$runningResponses): void
|
||||
{
|
||||
if (!isset($runningResponses[$i = $response->multi->id])) {
|
||||
$runningResponses[$i] = [$response->multi, []];
|
||||
}
|
||||
|
||||
$runningResponses[$i][1][$response->id] = $response;
|
||||
|
||||
if (null === $response->buffer) {
|
||||
// Response already completed
|
||||
$response->multi->handlesActivity[$response->id][] = null;
|
||||
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param NativeClientState $multi
|
||||
*/
|
||||
private static function perform(ClientState $multi, ?array &$responses = null): void
|
||||
{
|
||||
foreach ($multi->openHandles as $i => [$pauseExpiry, $h, $buffer, $onProgress]) {
|
||||
if ($pauseExpiry) {
|
||||
if (microtime(true) < $pauseExpiry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$multi->openHandles[$i][0] = 0;
|
||||
}
|
||||
|
||||
$hasActivity = false;
|
||||
$remaining = &$multi->openHandles[$i][4];
|
||||
$info = &$multi->openHandles[$i][5];
|
||||
$e = null;
|
||||
|
||||
// Read incoming buffer and write it to the dechunk one
|
||||
try {
|
||||
if ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) {
|
||||
fwrite($buffer, $data);
|
||||
$hasActivity = true;
|
||||
$multi->sleep = false;
|
||||
|
||||
if (-1 !== $remaining) {
|
||||
$remaining -= \strlen($data);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$hasActivity = $onProgress = false;
|
||||
}
|
||||
|
||||
if (!$hasActivity) {
|
||||
if ($onProgress) {
|
||||
try {
|
||||
// Notify the progress callback so that it can e.g. cancel
|
||||
// the request if the stream is inactive for too long
|
||||
$info['total_time'] = microtime(true) - $info['start_time'];
|
||||
$onProgress();
|
||||
} catch (\Throwable $e) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
} elseif ('' !== $data = stream_get_contents($buffer, -1, 0)) {
|
||||
rewind($buffer);
|
||||
ftruncate($buffer, 0);
|
||||
|
||||
if (null === $e) {
|
||||
$multi->handlesActivity[$i][] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $e || !$remaining || feof($h)) {
|
||||
// Stream completed
|
||||
$info['total_time'] = microtime(true) - $info['start_time'];
|
||||
$info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
|
||||
|
||||
if ($onProgress) {
|
||||
try {
|
||||
$onProgress(-1);
|
||||
} catch (\Throwable $e) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $e) {
|
||||
if (0 < $remaining) {
|
||||
$e = new TransportException(sprintf('Transfer closed with %s bytes remaining to read.', $remaining));
|
||||
} elseif (-1 === $remaining && fwrite($buffer, '-') && '' !== stream_get_contents($buffer, -1, 0)) {
|
||||
$e = new TransportException('Transfer closed with outstanding data remaining from chunked response.');
|
||||
}
|
||||
}
|
||||
|
||||
$multi->handlesActivity[$i][] = null;
|
||||
$multi->handlesActivity[$i][] = $e;
|
||||
if (null !== ($host = $multi->openHandles[$i][6] ?? null) && 0 >= --$multi->hosts[$host]) {
|
||||
unset($multi->hosts[$host]);
|
||||
}
|
||||
unset($multi->openHandles[$i]);
|
||||
$multi->sleep = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $responses) {
|
||||
return;
|
||||
}
|
||||
|
||||
$maxHosts = $multi->maxHostConnections;
|
||||
|
||||
foreach ($responses as $i => $response) {
|
||||
if (null !== $response->remaining || null === $response->buffer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($response->pauseExpiry && microtime(true) < $response->pauseExpiry) {
|
||||
// Create empty open handles to tell we still have pending requests
|
||||
$multi->openHandles[$i] = [\INF, null, null, null];
|
||||
} elseif ($maxHosts && $maxHosts > ($multi->hosts[parse_url($response->url, \PHP_URL_HOST)] ?? 0)) {
|
||||
// Open the next pending request - this is a blocking operation so we do only one of them
|
||||
$response->open();
|
||||
$multi->sleep = false;
|
||||
self::perform($multi);
|
||||
$maxHosts = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param NativeClientState $multi
|
||||
*/
|
||||
private static function select(ClientState $multi, float $timeout): int
|
||||
{
|
||||
if (!$multi->sleep = !$multi->sleep) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$_ = $handles = [];
|
||||
$now = null;
|
||||
|
||||
foreach ($multi->openHandles as [$pauseExpiry, $h]) {
|
||||
if (null === $h) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($pauseExpiry && ($now ?? $now = microtime(true)) < $pauseExpiry) {
|
||||
$timeout = min($timeout, $pauseExpiry - $now);
|
||||
continue;
|
||||
}
|
||||
|
||||
$handles[] = $h;
|
||||
}
|
||||
|
||||
if (!$handles) {
|
||||
usleep((int) (1E6 * $timeout));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return stream_select($handles, $_, $_, (int) $timeout, (int) (1E6 * ($timeout - (int) $timeout)));
|
||||
}
|
||||
}
|
54
vendor/symfony/http-client/Response/ResponseStream.php
vendored
Normal file
54
vendor/symfony/http-client/Response/ResponseStream.php
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class ResponseStream implements ResponseStreamInterface
|
||||
{
|
||||
private $generator;
|
||||
|
||||
public function __construct(\Generator $generator)
|
||||
{
|
||||
$this->generator = $generator;
|
||||
}
|
||||
|
||||
public function key(): ResponseInterface
|
||||
{
|
||||
return $this->generator->key();
|
||||
}
|
||||
|
||||
public function current(): ChunkInterface
|
||||
{
|
||||
return $this->generator->current();
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
$this->generator->next();
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->generator->rewind();
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->generator->valid();
|
||||
}
|
||||
}
|
313
vendor/symfony/http-client/Response/StreamWrapper.php
vendored
Normal file
313
vendor/symfony/http-client/Response/StreamWrapper.php
vendored
Normal file
|
@ -0,0 +1,313 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Allows turning ResponseInterface instances to PHP streams.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class StreamWrapper
|
||||
{
|
||||
/** @var resource|null */
|
||||
public $context;
|
||||
|
||||
/** @var HttpClientInterface */
|
||||
private $client;
|
||||
|
||||
/** @var ResponseInterface */
|
||||
private $response;
|
||||
|
||||
/** @var resource|string|null */
|
||||
private $content;
|
||||
|
||||
/** @var resource|null */
|
||||
private $handle;
|
||||
|
||||
private $blocking = true;
|
||||
private $timeout;
|
||||
private $eof = false;
|
||||
private $offset = 0;
|
||||
|
||||
/**
|
||||
* Creates a PHP stream resource from a ResponseInterface.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
public static function createResource(ResponseInterface $response, ?HttpClientInterface $client = null)
|
||||
{
|
||||
if ($response instanceof StreamableInterface) {
|
||||
$stack = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
||||
|
||||
if ($response !== ($stack[1]['object'] ?? null)) {
|
||||
return $response->toStream(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $client && !method_exists($response, 'stream')) {
|
||||
throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
|
||||
}
|
||||
|
||||
static $registered = false;
|
||||
|
||||
if (!$registered = $registered || stream_wrapper_register(strtr(__CLASS__, '\\', '-'), __CLASS__)) {
|
||||
throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
|
||||
}
|
||||
|
||||
$context = [
|
||||
'client' => $client ?? $response,
|
||||
'response' => $response,
|
||||
];
|
||||
|
||||
return fopen(strtr(__CLASS__, '\\', '-').'://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context]));
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource|callable|null $handle The resource handle that should be monitored when
|
||||
* stream_select() is used on the created stream
|
||||
* @param resource|null $content The seekable resource where the response body is buffered
|
||||
*/
|
||||
public function bindHandles(&$handle, &$content): void
|
||||
{
|
||||
$this->handle = &$handle;
|
||||
$this->content = &$content;
|
||||
$this->offset = null;
|
||||
}
|
||||
|
||||
public function stream_open(string $path, string $mode, int $options): bool
|
||||
{
|
||||
if ('r' !== $mode) {
|
||||
if ($options & \STREAM_REPORT_ERRORS) {
|
||||
trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), \E_USER_WARNING);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$context = stream_context_get_options($this->context)['symfony'] ?? null;
|
||||
$this->client = $context['client'] ?? null;
|
||||
$this->response = $context['response'] ?? null;
|
||||
$this->context = null;
|
||||
|
||||
if (null !== $this->client && null !== $this->response) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($options & \STREAM_REPORT_ERRORS) {
|
||||
trigger_error('Missing options "client" or "response" in "symfony" stream context.', \E_USER_WARNING);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_read(int $count)
|
||||
{
|
||||
if (\is_resource($this->content)) {
|
||||
// Empty the internal activity list
|
||||
foreach ($this->client->stream([$this->response], 0) as $chunk) {
|
||||
try {
|
||||
if (!$chunk->isTimeout() && $chunk->isFirst()) {
|
||||
$this->response->getStatusCode(); // ignore 3/4/5xx
|
||||
}
|
||||
} catch (ExceptionInterface $e) {
|
||||
trigger_error($e->getMessage(), \E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 !== fseek($this->content, $this->offset ?? 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('' !== $data = fread($this->content, $count)) {
|
||||
fseek($this->content, 0, \SEEK_END);
|
||||
$this->offset += \strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
if (\is_string($this->content)) {
|
||||
if (\strlen($this->content) <= $count) {
|
||||
$data = $this->content;
|
||||
$this->content = null;
|
||||
} else {
|
||||
$data = substr($this->content, 0, $count);
|
||||
$this->content = substr($this->content, $count);
|
||||
}
|
||||
$this->offset += \strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) {
|
||||
try {
|
||||
$this->eof = true;
|
||||
$this->eof = !$chunk->isTimeout();
|
||||
|
||||
if (!$this->eof && !$this->blocking) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->eof = $chunk->isLast();
|
||||
|
||||
if ($chunk->isFirst()) {
|
||||
$this->response->getStatusCode(); // ignore 3/4/5xx
|
||||
}
|
||||
|
||||
if ('' !== $data = $chunk->getContent()) {
|
||||
if (\strlen($data) > $count) {
|
||||
if (null === $this->content) {
|
||||
$this->content = substr($data, $count);
|
||||
}
|
||||
$data = substr($data, 0, $count);
|
||||
}
|
||||
$this->offset += \strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
} catch (ExceptionInterface $e) {
|
||||
trigger_error($e->getMessage(), \E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
|
||||
{
|
||||
if (\STREAM_OPTION_BLOCKING === $option) {
|
||||
$this->blocking = (bool) $arg1;
|
||||
} elseif (\STREAM_OPTION_READ_TIMEOUT === $option) {
|
||||
$this->timeout = $arg1 + $arg2 / 1e6;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function stream_tell(): int
|
||||
{
|
||||
return $this->offset ?? 0;
|
||||
}
|
||||
|
||||
public function stream_eof(): bool
|
||||
{
|
||||
return $this->eof && !\is_string($this->content);
|
||||
}
|
||||
|
||||
public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
|
||||
{
|
||||
if (null === $this->content && null === $this->offset) {
|
||||
$this->response->getStatusCode();
|
||||
$this->offset = 0;
|
||||
}
|
||||
|
||||
if (!\is_resource($this->content) || 0 !== fseek($this->content, 0, \SEEK_END)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$size = ftell($this->content);
|
||||
|
||||
if (\SEEK_CUR === $whence) {
|
||||
$offset += $this->offset ?? 0;
|
||||
}
|
||||
|
||||
if (\SEEK_END === $whence || $size < $offset) {
|
||||
foreach ($this->client->stream([$this->response]) as $chunk) {
|
||||
try {
|
||||
if ($chunk->isFirst()) {
|
||||
$this->response->getStatusCode(); // ignore 3/4/5xx
|
||||
}
|
||||
|
||||
// Chunks are buffered in $this->content already
|
||||
$size += \strlen($chunk->getContent());
|
||||
|
||||
if (\SEEK_END !== $whence && $offset <= $size) {
|
||||
break;
|
||||
}
|
||||
} catch (ExceptionInterface $e) {
|
||||
trigger_error($e->getMessage(), \E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (\SEEK_END === $whence) {
|
||||
$offset += $size;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 <= $offset && $offset <= $size) {
|
||||
$this->eof = false;
|
||||
$this->offset = $offset;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_cast(int $castAs)
|
||||
{
|
||||
if (\STREAM_CAST_FOR_SELECT === $castAs) {
|
||||
$this->response->getHeaders(false);
|
||||
|
||||
return (\is_callable($this->handle) ? ($this->handle)() : $this->handle) ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_stat(): array
|
||||
{
|
||||
try {
|
||||
$headers = $this->response->getHeaders(false);
|
||||
} catch (ExceptionInterface $e) {
|
||||
trigger_error($e->getMessage(), \E_USER_WARNING);
|
||||
$headers = [];
|
||||
}
|
||||
|
||||
return [
|
||||
'dev' => 0,
|
||||
'ino' => 0,
|
||||
'mode' => 33060,
|
||||
'nlink' => 0,
|
||||
'uid' => 0,
|
||||
'gid' => 0,
|
||||
'rdev' => 0,
|
||||
'size' => (int) ($headers['content-length'][0] ?? -1),
|
||||
'atime' => 0,
|
||||
'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0,
|
||||
'ctime' => 0,
|
||||
'blksize' => 0,
|
||||
'blocks' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
35
vendor/symfony/http-client/Response/StreamableInterface.php
vendored
Normal file
35
vendor/symfony/http-client/Response/StreamableInterface.php
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
interface StreamableInterface
|
||||
{
|
||||
/**
|
||||
* Casts the response to a PHP stream resource.
|
||||
*
|
||||
* @return resource
|
||||
*
|
||||
* @throws TransportExceptionInterface When a network error occurs
|
||||
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
|
||||
* @throws ClientExceptionInterface On a 4xx when $throw is true
|
||||
* @throws ServerExceptionInterface On a 5xx when $throw is true
|
||||
*/
|
||||
public function toStream(bool $throw = true);
|
||||
}
|
221
vendor/symfony/http-client/Response/TraceableResponse.php
vendored
Normal file
221
vendor/symfony/http-client/Response/TraceableResponse.php
vendored
Normal file
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\HttpClient\Exception\RedirectionException;
|
||||
use Symfony\Component\HttpClient\Exception\ServerException;
|
||||
use Symfony\Component\HttpClient\TraceableHttpClient;
|
||||
use Symfony\Component\Stopwatch\StopwatchEvent;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TraceableResponse implements ResponseInterface, StreamableInterface
|
||||
{
|
||||
private $client;
|
||||
private $response;
|
||||
private $content;
|
||||
private $event;
|
||||
|
||||
public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content, ?StopwatchEvent $event = null)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->response = $response;
|
||||
$this->content = &$content;
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
try {
|
||||
if (method_exists($this->response, '__destruct')) {
|
||||
$this->response->__destruct();
|
||||
}
|
||||
} finally {
|
||||
if ($this->event && $this->event->isStarted()) {
|
||||
$this->event->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
try {
|
||||
return $this->response->getStatusCode();
|
||||
} finally {
|
||||
if ($this->event && $this->event->isStarted()) {
|
||||
$this->event->lap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getHeaders(bool $throw = true): array
|
||||
{
|
||||
try {
|
||||
return $this->response->getHeaders($throw);
|
||||
} finally {
|
||||
if ($this->event && $this->event->isStarted()) {
|
||||
$this->event->lap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getContent(bool $throw = true): string
|
||||
{
|
||||
try {
|
||||
if (false === $this->content) {
|
||||
return $this->response->getContent($throw);
|
||||
}
|
||||
|
||||
return $this->content = $this->response->getContent(false);
|
||||
} finally {
|
||||
if ($this->event && $this->event->isStarted()) {
|
||||
$this->event->stop();
|
||||
}
|
||||
if ($throw) {
|
||||
$this->checkStatusCode($this->response->getStatusCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function toArray(bool $throw = true): array
|
||||
{
|
||||
try {
|
||||
if (false === $this->content) {
|
||||
return $this->response->toArray($throw);
|
||||
}
|
||||
|
||||
return $this->content = $this->response->toArray(false);
|
||||
} finally {
|
||||
if ($this->event && $this->event->isStarted()) {
|
||||
$this->event->stop();
|
||||
}
|
||||
if ($throw) {
|
||||
$this->checkStatusCode($this->response->getStatusCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function cancel(): void
|
||||
{
|
||||
$this->response->cancel();
|
||||
|
||||
if ($this->event && $this->event->isStarted()) {
|
||||
$this->event->stop();
|
||||
}
|
||||
}
|
||||
|
||||
public function getInfo(?string $type = null)
|
||||
{
|
||||
return $this->response->getInfo($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the response to a PHP stream resource.
|
||||
*
|
||||
* @return resource
|
||||
*
|
||||
* @throws TransportExceptionInterface When a network error occurs
|
||||
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
|
||||
* @throws ClientExceptionInterface On a 4xx when $throw is true
|
||||
* @throws ServerExceptionInterface On a 5xx when $throw is true
|
||||
*/
|
||||
public function toStream(bool $throw = true)
|
||||
{
|
||||
if ($throw) {
|
||||
// Ensure headers arrived
|
||||
$this->response->getHeaders(true);
|
||||
}
|
||||
|
||||
if ($this->response instanceof StreamableInterface) {
|
||||
return $this->response->toStream(false);
|
||||
}
|
||||
|
||||
return StreamWrapper::createResource($this->response, $this->client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function stream(HttpClientInterface $client, iterable $responses, ?float $timeout): \Generator
|
||||
{
|
||||
$wrappedResponses = [];
|
||||
$traceableMap = new \SplObjectStorage();
|
||||
|
||||
foreach ($responses as $r) {
|
||||
if (!$r instanceof self) {
|
||||
throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', TraceableHttpClient::class, get_debug_type($r)));
|
||||
}
|
||||
|
||||
$traceableMap[$r->response] = $r;
|
||||
$wrappedResponses[] = $r->response;
|
||||
if ($r->event && !$r->event->isStarted()) {
|
||||
$r->event->start();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($client->stream($wrappedResponses, $timeout) as $r => $chunk) {
|
||||
if ($traceableMap[$r]->event && $traceableMap[$r]->event->isStarted()) {
|
||||
try {
|
||||
if ($chunk->isTimeout() || !$chunk->isLast()) {
|
||||
$traceableMap[$r]->event->lap();
|
||||
} else {
|
||||
$traceableMap[$r]->event->stop();
|
||||
}
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$traceableMap[$r]->event->stop();
|
||||
if ($chunk instanceof ErrorChunk) {
|
||||
$chunk->didThrow(false);
|
||||
} else {
|
||||
$chunk = new ErrorChunk($chunk->getOffset(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
yield $traceableMap[$r] => $chunk;
|
||||
}
|
||||
}
|
||||
|
||||
private function checkStatusCode(int $code)
|
||||
{
|
||||
if (500 <= $code) {
|
||||
throw new ServerException($this);
|
||||
}
|
||||
|
||||
if (400 <= $code) {
|
||||
throw new ClientException($this);
|
||||
}
|
||||
|
||||
if (300 <= $code) {
|
||||
throw new RedirectionException($this);
|
||||
}
|
||||
}
|
||||
}
|
312
vendor/symfony/http-client/Response/TransportResponseTrait.php
vendored
Normal file
312
vendor/symfony/http-client/Response/TransportResponseTrait.php
vendored
Normal file
|
@ -0,0 +1,312 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Response;
|
||||
|
||||
use Symfony\Component\HttpClient\Chunk\DataChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||
use Symfony\Component\HttpClient\Chunk\LastChunk;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Component\HttpClient\Internal\ClientState;
|
||||
|
||||
/**
|
||||
* Implements common logic for transport-level response classes.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait TransportResponseTrait
|
||||
{
|
||||
private $canary;
|
||||
private $headers = [];
|
||||
private $info = [
|
||||
'response_headers' => [],
|
||||
'http_code' => 0,
|
||||
'error' => null,
|
||||
'canceled' => false,
|
||||
];
|
||||
|
||||
/** @var object|resource */
|
||||
private $handle;
|
||||
private $id;
|
||||
private $timeout = 0;
|
||||
private $inflate;
|
||||
private $finalInfo;
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
if ($this->initializer) {
|
||||
self::initialize($this);
|
||||
}
|
||||
|
||||
return $this->info['http_code'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getHeaders(bool $throw = true): array
|
||||
{
|
||||
if ($this->initializer) {
|
||||
self::initialize($this);
|
||||
}
|
||||
|
||||
if ($throw) {
|
||||
$this->checkStatusCode();
|
||||
}
|
||||
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cancel(): void
|
||||
{
|
||||
$this->info['canceled'] = true;
|
||||
$this->info['error'] = 'Response has been canceled.';
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the response and all its network handles.
|
||||
*/
|
||||
protected function close(): void
|
||||
{
|
||||
$this->canary->cancel();
|
||||
$this->inflate = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds pending responses to the activity list.
|
||||
*/
|
||||
abstract protected static function schedule(self $response, array &$runningResponses): void;
|
||||
|
||||
/**
|
||||
* Performs all pending non-blocking operations.
|
||||
*/
|
||||
abstract protected static function perform(ClientState $multi, array &$responses): void;
|
||||
|
||||
/**
|
||||
* Waits for network activity.
|
||||
*/
|
||||
abstract protected static function select(ClientState $multi, float $timeout): int;
|
||||
|
||||
private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void
|
||||
{
|
||||
foreach ($responseHeaders as $h) {
|
||||
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? (\d\d\d)(?: |$)#', $h, $m)) {
|
||||
if ($headers) {
|
||||
$debug .= "< \r\n";
|
||||
$headers = [];
|
||||
}
|
||||
$info['http_code'] = (int) $m[1];
|
||||
} elseif (2 === \count($m = explode(':', $h, 2))) {
|
||||
$headers[strtolower($m[0])][] = ltrim($m[1]);
|
||||
}
|
||||
|
||||
$debug .= "< {$h}\r\n";
|
||||
$info['response_headers'][] = $h;
|
||||
}
|
||||
|
||||
$debug .= "< \r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the request is always sent and that the response code was checked.
|
||||
*/
|
||||
private function doDestruct()
|
||||
{
|
||||
$this->shouldBuffer = true;
|
||||
|
||||
if ($this->initializer && null === $this->info['error']) {
|
||||
self::initialize($this);
|
||||
$this->checkStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements an event loop based on a buffer activity queue.
|
||||
*
|
||||
* @param iterable<array-key, self> $responses
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function stream(iterable $responses, ?float $timeout = null): \Generator
|
||||
{
|
||||
$runningResponses = [];
|
||||
|
||||
foreach ($responses as $response) {
|
||||
self::schedule($response, $runningResponses);
|
||||
}
|
||||
|
||||
$lastActivity = microtime(true);
|
||||
$elapsedTimeout = 0;
|
||||
|
||||
if ($fromLastTimeout = 0.0 === $timeout && '-0' === (string) $timeout) {
|
||||
$timeout = null;
|
||||
} elseif ($fromLastTimeout = 0 > $timeout) {
|
||||
$timeout = -$timeout;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
$hasActivity = false;
|
||||
$timeoutMax = 0;
|
||||
$timeoutMin = $timeout ?? \INF;
|
||||
|
||||
/** @var ClientState $multi */
|
||||
foreach ($runningResponses as $i => [$multi]) {
|
||||
$responses = &$runningResponses[$i][1];
|
||||
self::perform($multi, $responses);
|
||||
|
||||
foreach ($responses as $j => $response) {
|
||||
$timeoutMax = $timeout ?? max($timeoutMax, $response->timeout);
|
||||
$timeoutMin = min($timeoutMin, $response->timeout, 1);
|
||||
$chunk = false;
|
||||
|
||||
if ($fromLastTimeout && null !== $multi->lastTimeout) {
|
||||
$elapsedTimeout = microtime(true) - $multi->lastTimeout;
|
||||
}
|
||||
|
||||
if (isset($multi->handlesActivity[$j])) {
|
||||
$multi->lastTimeout = null;
|
||||
} elseif (!isset($multi->openHandles[$j])) {
|
||||
unset($responses[$j]);
|
||||
continue;
|
||||
} elseif ($elapsedTimeout >= $timeoutMax) {
|
||||
$multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))];
|
||||
$multi->lastTimeout ?? $multi->lastTimeout = $lastActivity;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
while ($multi->handlesActivity[$j] ?? false) {
|
||||
$hasActivity = true;
|
||||
$elapsedTimeout = 0;
|
||||
|
||||
if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) {
|
||||
if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) {
|
||||
$multi->handlesActivity[$j] = [null, new TransportException(sprintf('Error while processing content unencoding for "%s".', $response->getInfo('url')))];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content, $chunk)) {
|
||||
$multi->handlesActivity[$j] = [null, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($chunk)))];
|
||||
continue;
|
||||
}
|
||||
|
||||
$chunkLen = \strlen($chunk);
|
||||
$chunk = new DataChunk($response->offset, $chunk);
|
||||
$response->offset += $chunkLen;
|
||||
} elseif (null === $chunk) {
|
||||
$e = $multi->handlesActivity[$j][0];
|
||||
unset($responses[$j], $multi->handlesActivity[$j]);
|
||||
$response->close();
|
||||
|
||||
if (null !== $e) {
|
||||
$response->info['error'] = $e->getMessage();
|
||||
|
||||
if ($e instanceof \Error) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$chunk = new ErrorChunk($response->offset, $e);
|
||||
} else {
|
||||
if (0 === $response->offset && null === $response->content) {
|
||||
$response->content = fopen('php://memory', 'w+');
|
||||
}
|
||||
|
||||
$chunk = new LastChunk($response->offset);
|
||||
}
|
||||
} elseif ($chunk instanceof ErrorChunk) {
|
||||
unset($responses[$j]);
|
||||
$elapsedTimeout = $timeoutMax;
|
||||
} elseif ($chunk instanceof FirstChunk) {
|
||||
if ($response->logger) {
|
||||
$info = $response->getInfo();
|
||||
$response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url']));
|
||||
}
|
||||
|
||||
$response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(\ZLIB_ENCODING_GZIP) : null;
|
||||
|
||||
if ($response->shouldBuffer instanceof \Closure) {
|
||||
try {
|
||||
$response->shouldBuffer = ($response->shouldBuffer)($response->headers);
|
||||
|
||||
if (null !== $response->info['error']) {
|
||||
throw new TransportException($response->info['error']);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$response->close();
|
||||
$multi->handlesActivity[$j] = [null, $e];
|
||||
}
|
||||
}
|
||||
|
||||
if (true === $response->shouldBuffer) {
|
||||
$response->content = fopen('php://temp', 'w+');
|
||||
} elseif (\is_resource($response->shouldBuffer)) {
|
||||
$response->content = $response->shouldBuffer;
|
||||
}
|
||||
$response->shouldBuffer = null;
|
||||
|
||||
yield $response => $chunk;
|
||||
|
||||
if ($response->initializer && null === $response->info['error']) {
|
||||
// Ensure the HTTP status code is always checked
|
||||
$response->getHeaders(true);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $response => $chunk;
|
||||
}
|
||||
|
||||
unset($multi->handlesActivity[$j]);
|
||||
|
||||
if ($chunk instanceof ErrorChunk && !$chunk->didThrow()) {
|
||||
// Ensure transport exceptions are always thrown
|
||||
$chunk->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$responses) {
|
||||
unset($runningResponses[$i]);
|
||||
}
|
||||
|
||||
// Prevent memory leaks
|
||||
$multi->handlesActivity = $multi->handlesActivity ?: [];
|
||||
$multi->openHandles = $multi->openHandles ?: [];
|
||||
}
|
||||
|
||||
if (!$runningResponses) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($hasActivity) {
|
||||
$lastActivity = microtime(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (-1 === self::select($multi, min($timeoutMin, $timeoutMax - $elapsedTimeout))) {
|
||||
usleep((int) min(500, 1E6 * $timeoutMin));
|
||||
}
|
||||
|
||||
$elapsedTimeout = microtime(true) - $lastActivity;
|
||||
}
|
||||
}
|
||||
}
|
115
vendor/symfony/http-client/Retry/GenericRetryStrategy.php
vendored
Normal file
115
vendor/symfony/http-client/Retry/GenericRetryStrategy.php
vendored
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Retry;
|
||||
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\Response\AsyncContext;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
/**
|
||||
* Decides to retry the request when HTTP status codes belong to the given list of codes.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class GenericRetryStrategy implements RetryStrategyInterface
|
||||
{
|
||||
public const IDEMPOTENT_METHODS = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
|
||||
public const DEFAULT_RETRY_STATUS_CODES = [
|
||||
0 => self::IDEMPOTENT_METHODS, // for transport exceptions
|
||||
423,
|
||||
425,
|
||||
429,
|
||||
500 => self::IDEMPOTENT_METHODS,
|
||||
502,
|
||||
503,
|
||||
504 => self::IDEMPOTENT_METHODS,
|
||||
507 => self::IDEMPOTENT_METHODS,
|
||||
510 => self::IDEMPOTENT_METHODS,
|
||||
];
|
||||
|
||||
private $statusCodes;
|
||||
private $delayMs;
|
||||
private $multiplier;
|
||||
private $maxDelayMs;
|
||||
private $jitter;
|
||||
|
||||
/**
|
||||
* @param array $statusCodes List of HTTP status codes that trigger a retry
|
||||
* @param int $delayMs Amount of time to delay (or the initial value when multiplier is used)
|
||||
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
|
||||
* @param int $maxDelayMs Maximum delay to allow (0 means no maximum)
|
||||
* @param float $jitter Probability of randomness int delay (0 = none, 1 = 100% random)
|
||||
*/
|
||||
public function __construct(array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES, int $delayMs = 1000, float $multiplier = 2.0, int $maxDelayMs = 0, float $jitter = 0.1)
|
||||
{
|
||||
$this->statusCodes = $statusCodes;
|
||||
|
||||
if ($delayMs < 0) {
|
||||
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMs));
|
||||
}
|
||||
$this->delayMs = $delayMs;
|
||||
|
||||
if ($multiplier < 1) {
|
||||
throw new InvalidArgumentException(sprintf('Multiplier must be greater than or equal to one: "%s" given.', $multiplier));
|
||||
}
|
||||
$this->multiplier = $multiplier;
|
||||
|
||||
if ($maxDelayMs < 0) {
|
||||
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMs));
|
||||
}
|
||||
$this->maxDelayMs = $maxDelayMs;
|
||||
|
||||
if ($jitter < 0 || $jitter > 1) {
|
||||
throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter));
|
||||
}
|
||||
$this->jitter = $jitter;
|
||||
}
|
||||
|
||||
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
|
||||
{
|
||||
$statusCode = $context->getStatusCode();
|
||||
if (\in_array($statusCode, $this->statusCodes, true)) {
|
||||
return true;
|
||||
}
|
||||
if (isset($this->statusCodes[$statusCode]) && \is_array($this->statusCodes[$statusCode])) {
|
||||
return \in_array($context->getInfo('http_method'), $this->statusCodes[$statusCode], true);
|
||||
}
|
||||
if (null === $exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (\in_array(0, $this->statusCodes, true)) {
|
||||
return true;
|
||||
}
|
||||
if (isset($this->statusCodes[0]) && \is_array($this->statusCodes[0])) {
|
||||
return \in_array($context->getInfo('http_method'), $this->statusCodes[0], true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
|
||||
{
|
||||
$delay = $this->delayMs * $this->multiplier ** $context->getInfo('retry_count');
|
||||
|
||||
if ($this->jitter > 0) {
|
||||
$randomness = (int) ($delay * $this->jitter);
|
||||
$delay = $delay + random_int(-$randomness, +$randomness);
|
||||
}
|
||||
|
||||
if ($delay > $this->maxDelayMs && 0 !== $this->maxDelayMs) {
|
||||
return $this->maxDelayMs;
|
||||
}
|
||||
|
||||
return (int) $delay;
|
||||
}
|
||||
}
|
36
vendor/symfony/http-client/Retry/RetryStrategyInterface.php
vendored
Normal file
36
vendor/symfony/http-client/Retry/RetryStrategyInterface.php
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Retry;
|
||||
|
||||
use Symfony\Component\HttpClient\Response\AsyncContext;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
interface RetryStrategyInterface
|
||||
{
|
||||
/**
|
||||
* Returns whether the request should be retried.
|
||||
*
|
||||
* @param ?string $responseContent Null is passed when the body did not arrive yet
|
||||
*
|
||||
* @return bool|null Returns null to signal that the body is required to take a decision
|
||||
*/
|
||||
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool;
|
||||
|
||||
/**
|
||||
* Returns the time to wait in milliseconds.
|
||||
*/
|
||||
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int;
|
||||
}
|
171
vendor/symfony/http-client/RetryableHttpClient.php
vendored
Normal file
171
vendor/symfony/http-client/RetryableHttpClient.php
vendored
Normal file
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpClient\Response\AsyncContext;
|
||||
use Symfony\Component\HttpClient\Response\AsyncResponse;
|
||||
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
|
||||
use Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
|
||||
use Symfony\Contracts\HttpClient\ChunkInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* Automatically retries failing HTTP requests.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RetryableHttpClient implements HttpClientInterface, ResetInterface
|
||||
{
|
||||
use AsyncDecoratorTrait;
|
||||
|
||||
private $strategy;
|
||||
private $maxRetries;
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @param int $maxRetries The maximum number of times to retry
|
||||
*/
|
||||
public function __construct(HttpClientInterface $client, ?RetryStrategyInterface $strategy = null, int $maxRetries = 3, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->strategy = $strategy ?? new GenericRetryStrategy();
|
||||
$this->maxRetries = $maxRetries;
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
}
|
||||
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
if ($this->maxRetries <= 0) {
|
||||
return new AsyncResponse($this->client, $method, $url, $options);
|
||||
}
|
||||
|
||||
$retryCount = 0;
|
||||
$content = '';
|
||||
$firstChunk = null;
|
||||
|
||||
return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, &$retryCount, &$content, &$firstChunk) {
|
||||
$exception = null;
|
||||
try {
|
||||
if ($context->getInfo('canceled') || $chunk->isTimeout() || null !== $chunk->getInformationalStatus()) {
|
||||
yield $chunk;
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (TransportExceptionInterface $exception) {
|
||||
// catch TransportExceptionInterface to send it to the strategy
|
||||
}
|
||||
if (null !== $exception) {
|
||||
// always retry request that fail to resolve DNS
|
||||
if ('' !== $context->getInfo('primary_ip')) {
|
||||
$shouldRetry = $this->strategy->shouldRetry($context, null, $exception);
|
||||
if (null === $shouldRetry) {
|
||||
throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with an exception.', \get_class($this->strategy)));
|
||||
}
|
||||
|
||||
if (false === $shouldRetry) {
|
||||
yield from $this->passthru($context, $firstChunk, $content, $chunk);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} elseif ($chunk->isFirst()) {
|
||||
if (false === $shouldRetry = $this->strategy->shouldRetry($context, null, null)) {
|
||||
yield from $this->passthru($context, $firstChunk, $content, $chunk);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Body is needed to decide
|
||||
if (null === $shouldRetry) {
|
||||
$firstChunk = $chunk;
|
||||
$content = '';
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!$chunk->isLast()) {
|
||||
$content .= $chunk->getContent();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $shouldRetry = $this->strategy->shouldRetry($context, $content, null)) {
|
||||
throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with a body.', \get_class($this->strategy)));
|
||||
}
|
||||
|
||||
if (false === $shouldRetry) {
|
||||
yield from $this->passthru($context, $firstChunk, $content, $chunk);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$context->getResponse()->cancel();
|
||||
|
||||
$delay = $this->getDelayFromHeader($context->getHeaders()) ?? $this->strategy->getDelay($context, !$exception && $chunk->isLast() ? $content : null, $exception);
|
||||
++$retryCount;
|
||||
$content = '';
|
||||
$firstChunk = null;
|
||||
|
||||
$this->logger->info('Try #{count} after {delay}ms'.($exception ? ': '.$exception->getMessage() : ', status code: '.$context->getStatusCode()), [
|
||||
'count' => $retryCount,
|
||||
'delay' => $delay,
|
||||
]);
|
||||
|
||||
$context->setInfo('retry_count', $retryCount);
|
||||
$context->replaceRequest($method, $url, $options);
|
||||
$context->pause($delay / 1000);
|
||||
|
||||
if ($retryCount >= $this->maxRetries) {
|
||||
$context->passthru();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function getDelayFromHeader(array $headers): ?int
|
||||
{
|
||||
if (null !== $after = $headers['retry-after'][0] ?? null) {
|
||||
if (is_numeric($after)) {
|
||||
return (int) ($after * 1000);
|
||||
}
|
||||
|
||||
if (false !== $time = strtotime($after)) {
|
||||
return max(0, $time - time()) * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function passthru(AsyncContext $context, ?ChunkInterface $firstChunk, string &$content, ChunkInterface $lastChunk): \Generator
|
||||
{
|
||||
$context->passthru();
|
||||
|
||||
if (null !== $firstChunk) {
|
||||
yield $firstChunk;
|
||||
}
|
||||
|
||||
if ('' !== $content) {
|
||||
$chunk = $context->createChunk($content);
|
||||
$content = '';
|
||||
|
||||
yield $chunk;
|
||||
}
|
||||
|
||||
yield $lastChunk;
|
||||
}
|
||||
}
|
131
vendor/symfony/http-client/ScopingHttpClient.php
vendored
Normal file
131
vendor/symfony/http-client/ScopingHttpClient.php
vendored
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* Auto-configure the default options based on the requested URL.
|
||||
*
|
||||
* @author Anthony Martin <anthony.martin@sensiolabs.com>
|
||||
*/
|
||||
class ScopingHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
|
||||
{
|
||||
use HttpClientTrait;
|
||||
|
||||
private $client;
|
||||
private $defaultOptionsByRegexp;
|
||||
private $defaultRegexp;
|
||||
|
||||
public function __construct(HttpClientInterface $client, array $defaultOptionsByRegexp, ?string $defaultRegexp = null)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->defaultOptionsByRegexp = $defaultOptionsByRegexp;
|
||||
$this->defaultRegexp = $defaultRegexp;
|
||||
|
||||
if (null !== $defaultRegexp && !isset($defaultOptionsByRegexp[$defaultRegexp])) {
|
||||
throw new InvalidArgumentException(sprintf('No options are mapped to the provided "%s" default regexp.', $defaultRegexp));
|
||||
}
|
||||
}
|
||||
|
||||
public static function forBaseUri(HttpClientInterface $client, string $baseUri, array $defaultOptions = [], ?string $regexp = null): self
|
||||
{
|
||||
if (null === $regexp) {
|
||||
$regexp = preg_quote(implode('', self::resolveUrl(self::parseUrl('.'), self::parseUrl($baseUri))));
|
||||
}
|
||||
|
||||
$defaultOptions['base_uri'] = $baseUri;
|
||||
|
||||
return new self($client, [$regexp => $defaultOptions], $regexp);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$e = null;
|
||||
$url = self::parseUrl($url, $options['query'] ?? []);
|
||||
|
||||
if (\is_string($options['base_uri'] ?? null)) {
|
||||
$options['base_uri'] = self::parseUrl($options['base_uri']);
|
||||
}
|
||||
|
||||
try {
|
||||
$url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
if (null === $this->defaultRegexp) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$defaultOptions = $this->defaultOptionsByRegexp[$this->defaultRegexp];
|
||||
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
|
||||
if (\is_string($options['base_uri'] ?? null)) {
|
||||
$options['base_uri'] = self::parseUrl($options['base_uri']);
|
||||
}
|
||||
$url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null, $defaultOptions['query'] ?? []));
|
||||
}
|
||||
|
||||
foreach ($this->defaultOptionsByRegexp as $regexp => $defaultOptions) {
|
||||
if (preg_match("{{$regexp}}A", $url)) {
|
||||
if (null === $e || $regexp !== $this->defaultRegexp) {
|
||||
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->client->request($method, $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->client->stream($responses, $timeout);
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if ($this->client instanceof ResetInterface) {
|
||||
$this->client->reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
if ($this->client instanceof LoggerAwareInterface) {
|
||||
$this->client->setLogger($logger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function withOptions(array $options): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->client = $this->client->withOptions($options);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
120
vendor/symfony/http-client/TraceableHttpClient.php
vendored
Normal file
120
vendor/symfony/http-client/TraceableHttpClient.php
vendored
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Response\ResponseStream;
|
||||
use Symfony\Component\HttpClient\Response\TraceableResponse;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Romey <jeremy@free-agent.fr>
|
||||
*/
|
||||
final class TraceableHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
|
||||
{
|
||||
private $client;
|
||||
private $stopwatch;
|
||||
private $tracedRequests;
|
||||
|
||||
public function __construct(HttpClientInterface $client, ?Stopwatch $stopwatch = null)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->stopwatch = $stopwatch;
|
||||
$this->tracedRequests = new \ArrayObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$content = null;
|
||||
$traceInfo = [];
|
||||
$this->tracedRequests[] = [
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'options' => $options,
|
||||
'info' => &$traceInfo,
|
||||
'content' => &$content,
|
||||
];
|
||||
$onProgress = $options['on_progress'] ?? null;
|
||||
|
||||
if (false === ($options['extra']['trace_content'] ?? true)) {
|
||||
unset($content);
|
||||
$content = false;
|
||||
}
|
||||
|
||||
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) {
|
||||
$traceInfo = $info;
|
||||
|
||||
if (null !== $onProgress) {
|
||||
$onProgress($dlNow, $dlSize, $info);
|
||||
}
|
||||
};
|
||||
|
||||
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content, null === $this->stopwatch ? null : $this->stopwatch->start("$method $url", 'http_client'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
if ($responses instanceof TraceableResponse) {
|
||||
$responses = [$responses];
|
||||
} elseif (!is_iterable($responses)) {
|
||||
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
|
||||
}
|
||||
|
||||
return new ResponseStream(TraceableResponse::stream($this->client, $responses, $timeout));
|
||||
}
|
||||
|
||||
public function getTracedRequests(): array
|
||||
{
|
||||
return $this->tracedRequests->getArrayCopy();
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
if ($this->client instanceof ResetInterface) {
|
||||
$this->client->reset();
|
||||
}
|
||||
|
||||
$this->tracedRequests->exchangeArray([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
if ($this->client instanceof LoggerAwareInterface) {
|
||||
$this->client->setLogger($logger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function withOptions(array $options): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->client = $this->client->withOptions($options);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
55
vendor/symfony/http-client/composer.json
vendored
Normal file
55
vendor/symfony/http-client/composer.json
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "symfony/http-client",
|
||||
"type": "library",
|
||||
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
|
||||
"keywords": ["http"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"provide": {
|
||||
"php-http/async-client-implementation": "*",
|
||||
"php-http/client-implementation": "*",
|
||||
"psr/http-client-implementation": "1.0",
|
||||
"symfony/http-client-implementation": "2.4"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/deprecation-contracts": "^2.1|^3",
|
||||
"symfony/http-client-contracts": "^2.5.3",
|
||||
"symfony/polyfill-php73": "^1.11",
|
||||
"symfony/polyfill-php80": "^1.16",
|
||||
"symfony/service-contracts": "^1.0|^2|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/amp": "^2.5",
|
||||
"amphp/http-client": "^4.2.1",
|
||||
"amphp/http-tunnel": "^1.0",
|
||||
"amphp/socket": "^1.1",
|
||||
"guzzlehttp/promises": "^1.4|^2.0",
|
||||
"nyholm/psr7": "^1.0",
|
||||
"php-http/httplug": "^1.0|^2.0",
|
||||
"php-http/message-factory": "^1.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
|
||||
"symfony/http-kernel": "^4.4.13|^5.1.5|^6.0",
|
||||
"symfony/process": "^4.4|^5.0|^6.0",
|
||||
"symfony/stopwatch": "^4.4|^5.0|^6.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue