From cdd7ad020e165fe680703b6d3319b908b682fb7a Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Fri, 20 Oct 2023 17:12:29 +0300 Subject: jaeger-client -> opentelemetry --- .../guzzlehttp/guzzle/src/Handler/CurlFactory.php | 638 +++++++++++++++++++++ .../guzzle/src/Handler/CurlFactoryInterface.php | 25 + .../guzzlehttp/guzzle/src/Handler/CurlHandler.php | 49 ++ .../guzzle/src/Handler/CurlMultiHandler.php | 267 +++++++++ .../guzzlehttp/guzzle/src/Handler/EasyHandle.php | 112 ++++ .../guzzle/src/Handler/HeaderProcessor.php | 42 ++ .../guzzlehttp/guzzle/src/Handler/MockHandler.php | 212 +++++++ vendor/guzzlehttp/guzzle/src/Handler/Proxy.php | 51 ++ .../guzzle/src/Handler/StreamHandler.php | 615 ++++++++++++++++++++ 9 files changed, 2011 insertions(+) create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/CurlHandler.php create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/MockHandler.php create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/Proxy.php create mode 100644 vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php (limited to 'vendor/guzzlehttp/guzzle/src/Handler') diff --git a/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php b/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php new file mode 100644 index 000000000..be88d9e49 --- /dev/null +++ b/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php @@ -0,0 +1,638 @@ +maxHandles = $maxHandles; + } + + public function create(RequestInterface $request, array $options): EasyHandle + { + if (isset($options['curl']['body_as_string'])) { + $options['_body_as_string'] = $options['curl']['body_as_string']; + unset($options['curl']['body_as_string']); + } + + $easy = new EasyHandle(); + $easy->request = $request; + $easy->options = $options; + $conf = $this->getDefaultConf($easy); + $this->applyMethod($easy, $conf); + $this->applyHandlerOptions($easy, $conf); + $this->applyHeaders($easy, $conf); + unset($conf['_headers']); + + // Add handler options from the request configuration options + if (isset($options['curl'])) { + $conf = \array_replace($conf, $options['curl']); + } + + $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); + $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init(); + curl_setopt_array($easy->handle, $conf); + + return $easy; + } + + public function release(EasyHandle $easy): void + { + $resource = $easy->handle; + unset($easy->handle); + + if (\count($this->handles) >= $this->maxHandles) { + \curl_close($resource); + } else { + // Remove all callback functions as they can hold onto references + // and are not cleaned up by curl_reset. Using curl_setopt_array + // does not work for some reason, so removing each one + // individually. + \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null); + \curl_setopt($resource, \CURLOPT_READFUNCTION, null); + \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null); + \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null); + \curl_reset($resource); + $this->handles[] = $resource; + } + } + + /** + * Completes a cURL transaction, either returning a response promise or a + * rejected promise. + * + * @param callable(RequestInterface, array): PromiseInterface $handler + * @param CurlFactoryInterface $factory Dictates how the handle is released + */ + public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface + { + if (isset($easy->options['on_stats'])) { + self::invokeStats($easy); + } + + if (!$easy->response || $easy->errno) { + return self::finishError($handler, $easy, $factory); + } + + // Return the response if it is present and there is no error. + $factory->release($easy); + + // Rewind the body of the response if possible. + $body = $easy->response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + + return new FulfilledPromise($easy->response); + } + + private static function invokeStats(EasyHandle $easy): void + { + $curlStats = \curl_getinfo($easy->handle); + $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME); + $stats = new TransferStats( + $easy->request, + $easy->response, + $curlStats['total_time'], + $easy->errno, + $curlStats + ); + ($easy->options['on_stats'])($stats); + } + + /** + * @param callable(RequestInterface, array): PromiseInterface $handler + */ + private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface + { + // Get error information and release the handle to the factory. + $ctx = [ + 'errno' => $easy->errno, + 'error' => \curl_error($easy->handle), + 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME), + ] + \curl_getinfo($easy->handle); + $ctx[self::CURL_VERSION_STR] = \curl_version()['version']; + $factory->release($easy); + + // Retry when nothing is present or when curl failed to rewind. + if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) { + return self::retryFailedRewind($handler, $easy, $ctx); + } + + return self::createRejection($easy, $ctx); + } + + private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface + { + static $connectionErrors = [ + \CURLE_OPERATION_TIMEOUTED => true, + \CURLE_COULDNT_RESOLVE_HOST => true, + \CURLE_COULDNT_CONNECT => true, + \CURLE_SSL_CONNECT_ERROR => true, + \CURLE_GOT_NOTHING => true, + ]; + + if ($easy->createResponseException) { + return P\Create::rejectionFor( + new RequestException( + 'An error was encountered while creating the response', + $easy->request, + $easy->response, + $easy->createResponseException, + $ctx + ) + ); + } + + // If an exception was encountered during the onHeaders event, then + // return a rejected promise that wraps that exception. + if ($easy->onHeadersException) { + return P\Create::rejectionFor( + new RequestException( + 'An error was encountered during the on_headers event', + $easy->request, + $easy->response, + $easy->onHeadersException, + $ctx + ) + ); + } + + $message = \sprintf( + 'cURL error %s: %s (%s)', + $ctx['errno'], + $ctx['error'], + 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html' + ); + $uriString = (string) $easy->request->getUri(); + if ($uriString !== '' && false === \strpos($ctx['error'], $uriString)) { + $message .= \sprintf(' for %s', $uriString); + } + + // Create a connection exception if it was a specific error code. + $error = isset($connectionErrors[$easy->errno]) + ? new ConnectException($message, $easy->request, null, $ctx) + : new RequestException($message, $easy->request, $easy->response, null, $ctx); + + return P\Create::rejectionFor($error); + } + + /** + * @return array + */ + private function getDefaultConf(EasyHandle $easy): array + { + $conf = [ + '_headers' => $easy->request->getHeaders(), + \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), + \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), + \CURLOPT_RETURNTRANSFER => false, + \CURLOPT_HEADER => false, + \CURLOPT_CONNECTTIMEOUT => 300, + ]; + + if (\defined('CURLOPT_PROTOCOLS')) { + $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS; + } + + $version = $easy->request->getProtocolVersion(); + if ($version == 1.1) { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1; + } elseif ($version == 2.0) { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0; + } else { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0; + } + + return $conf; + } + + private function applyMethod(EasyHandle $easy, array &$conf): void + { + $body = $easy->request->getBody(); + $size = $body->getSize(); + + if ($size === null || $size > 0) { + $this->applyBody($easy->request, $easy->options, $conf); + + return; + } + + $method = $easy->request->getMethod(); + if ($method === 'PUT' || $method === 'POST') { + // See https://tools.ietf.org/html/rfc7230#section-3.3.2 + if (!$easy->request->hasHeader('Content-Length')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; + } + } elseif ($method === 'HEAD') { + $conf[\CURLOPT_NOBODY] = true; + unset( + $conf[\CURLOPT_WRITEFUNCTION], + $conf[\CURLOPT_READFUNCTION], + $conf[\CURLOPT_FILE], + $conf[\CURLOPT_INFILE] + ); + } + } + + private function applyBody(RequestInterface $request, array $options, array &$conf): void + { + $size = $request->hasHeader('Content-Length') + ? (int) $request->getHeaderLine('Content-Length') + : null; + + // Send the body as a string if the size is less than 1MB OR if the + // [curl][body_as_string] request value is set. + if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) { + $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody(); + // Don't duplicate the Content-Length header + $this->removeHeader('Content-Length', $conf); + $this->removeHeader('Transfer-Encoding', $conf); + } else { + $conf[\CURLOPT_UPLOAD] = true; + if ($size !== null) { + $conf[\CURLOPT_INFILESIZE] = $size; + $this->removeHeader('Content-Length', $conf); + } + $body = $request->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) { + return $body->read($length); + }; + } + + // If the Expect header is not present, prevent curl from adding it + if (!$request->hasHeader('Expect')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Expect:'; + } + + // cURL sometimes adds a content-type by default. Prevent this. + if (!$request->hasHeader('Content-Type')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:'; + } + } + + private function applyHeaders(EasyHandle $easy, array &$conf): void + { + foreach ($conf['_headers'] as $name => $values) { + foreach ($values as $value) { + $value = (string) $value; + if ($value === '') { + // cURL requires a special format for empty headers. + // See https://github.com/guzzle/guzzle/issues/1882 for more details. + $conf[\CURLOPT_HTTPHEADER][] = "$name;"; + } else { + $conf[\CURLOPT_HTTPHEADER][] = "$name: $value"; + } + } + } + + // Remove the Accept header if one was not set + if (!$easy->request->hasHeader('Accept')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Accept:'; + } + } + + /** + * Remove a header from the options array. + * + * @param string $name Case-insensitive header to remove + * @param array $options Array of options to modify + */ + private function removeHeader(string $name, array &$options): void + { + foreach (\array_keys($options['_headers']) as $key) { + if (!\strcasecmp($key, $name)) { + unset($options['_headers'][$key]); + + return; + } + } + } + + private function applyHandlerOptions(EasyHandle $easy, array &$conf): void + { + $options = $easy->options; + if (isset($options['verify'])) { + if ($options['verify'] === false) { + unset($conf[\CURLOPT_CAINFO]); + $conf[\CURLOPT_SSL_VERIFYHOST] = 0; + $conf[\CURLOPT_SSL_VERIFYPEER] = false; + } else { + $conf[\CURLOPT_SSL_VERIFYHOST] = 2; + $conf[\CURLOPT_SSL_VERIFYPEER] = true; + if (\is_string($options['verify'])) { + // Throw an error if the file/folder/link path is not valid or doesn't exist. + if (!\file_exists($options['verify'])) { + throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}"); + } + // If it's a directory or a link to a directory use CURLOPT_CAPATH. + // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. + if ( + \is_dir($options['verify']) + || ( + \is_link($options['verify']) === true + && ($verifyLink = \readlink($options['verify'])) !== false + && \is_dir($verifyLink) + ) + ) { + $conf[\CURLOPT_CAPATH] = $options['verify']; + } else { + $conf[\CURLOPT_CAINFO] = $options['verify']; + } + } + } + } + + if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) { + $accept = $easy->request->getHeaderLine('Accept-Encoding'); + if ($accept) { + $conf[\CURLOPT_ENCODING] = $accept; + } else { + // The empty string enables all available decoders and implicitly + // sets a matching 'Accept-Encoding' header. + $conf[\CURLOPT_ENCODING] = ''; + // But as the user did not specify any acceptable encodings we need + // to overwrite this implicit header with an empty one. + $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; + } + } + + if (!isset($options['sink'])) { + // Use a default temp stream if no sink was set. + $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+'); + } + $sink = $options['sink']; + if (!\is_string($sink)) { + $sink = \GuzzleHttp\Psr7\Utils::streamFor($sink); + } elseif (!\is_dir(\dirname($sink))) { + // Ensure that the directory exists before failing in curl. + throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink)); + } else { + $sink = new LazyOpenStream($sink, 'w+'); + } + $easy->sink = $sink; + $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int { + return $sink->write($write); + }; + + $timeoutRequiresNoSignal = false; + if (isset($options['timeout'])) { + $timeoutRequiresNoSignal |= $options['timeout'] < 1; + $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; + } + + // CURL default value is CURL_IPRESOLVE_WHATEVER + if (isset($options['force_ip_resolve'])) { + if ('v4' === $options['force_ip_resolve']) { + $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; + } elseif ('v6' === $options['force_ip_resolve']) { + $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6; + } + } + + if (isset($options['connect_timeout'])) { + $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; + $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; + } + + if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') { + $conf[\CURLOPT_NOSIGNAL] = true; + } + + if (isset($options['proxy'])) { + if (!\is_array($options['proxy'])) { + $conf[\CURLOPT_PROXY] = $options['proxy']; + } else { + $scheme = $easy->request->getUri()->getScheme(); + if (isset($options['proxy'][$scheme])) { + $host = $easy->request->getUri()->getHost(); + if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) { + unset($conf[\CURLOPT_PROXY]); + } else { + $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme]; + } + } + } + } + + if (isset($options['crypto_method'])) { + if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) { + if (!defined('CURL_SSLVERSION_TLSv1_0')) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.0 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0; + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) { + if (!defined('CURL_SSLVERSION_TLSv1_1')) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.1 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1; + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { + if (!defined('CURL_SSLVERSION_TLSv1_2')) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; + } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { + if (!defined('CURL_SSLVERSION_TLSv1_3')) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; + } else { + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); + } + } + + if (isset($options['cert'])) { + $cert = $options['cert']; + if (\is_array($cert)) { + $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1]; + $cert = $cert[0]; + } + if (!\file_exists($cert)) { + throw new \InvalidArgumentException("SSL certificate not found: {$cert}"); + } + // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files. + // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html + $ext = pathinfo($cert, \PATHINFO_EXTENSION); + if (preg_match('#^(der|p12)$#i', $ext)) { + $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext); + } + $conf[\CURLOPT_SSLCERT] = $cert; + } + + if (isset($options['ssl_key'])) { + if (\is_array($options['ssl_key'])) { + if (\count($options['ssl_key']) === 2) { + [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key']; + } else { + [$sslKey] = $options['ssl_key']; + } + } + + $sslKey = $sslKey ?? $options['ssl_key']; + + if (!\file_exists($sslKey)) { + throw new \InvalidArgumentException("SSL private key not found: {$sslKey}"); + } + $conf[\CURLOPT_SSLKEY] = $sslKey; + } + + if (isset($options['progress'])) { + $progress = $options['progress']; + if (!\is_callable($progress)) { + throw new \InvalidArgumentException('progress client option must be callable'); + } + $conf[\CURLOPT_NOPROGRESS] = false; + $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) { + $progress($downloadSize, $downloaded, $uploadSize, $uploaded); + }; + } + + if (!empty($options['debug'])) { + $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']); + $conf[\CURLOPT_VERBOSE] = true; + } + } + + /** + * This function ensures that a response was set on a transaction. If one + * was not set, then the request is retried if possible. This error + * typically means you are sending a payload, curl encountered a + * "Connection died, retrying a fresh connect" error, tried to rewind the + * stream, and then encountered a "necessary data rewind wasn't possible" + * error, causing the request to be sent through curl_multi_info_read() + * without an error status. + * + * @param callable(RequestInterface, array): PromiseInterface $handler + */ + private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface + { + try { + // Only rewind if the body has been read from. + $body = $easy->request->getBody(); + if ($body->tell() > 0) { + $body->rewind(); + } + } catch (\RuntimeException $e) { + $ctx['error'] = 'The connection unexpectedly failed without ' + .'providing an error. The request would have been retried, ' + .'but attempting to rewind the request body failed. ' + .'Exception: '.$e; + + return self::createRejection($easy, $ctx); + } + + // Retry no more than 3 times before giving up. + if (!isset($easy->options['_curl_retries'])) { + $easy->options['_curl_retries'] = 1; + } elseif ($easy->options['_curl_retries'] == 2) { + $ctx['error'] = 'The cURL request was retried 3 times ' + .'and did not succeed. The most likely reason for the failure ' + .'is that cURL was unable to rewind the body of the request ' + .'and subsequent retries resulted in the same error. Turn on ' + .'the debug option to see what went wrong. See ' + .'https://bugs.php.net/bug.php?id=47204 for more information.'; + + return self::createRejection($easy, $ctx); + } else { + ++$easy->options['_curl_retries']; + } + + return $handler($easy->request, $easy->options); + } + + private function createHeaderFn(EasyHandle $easy): callable + { + if (isset($easy->options['on_headers'])) { + $onHeaders = $easy->options['on_headers']; + + if (!\is_callable($onHeaders)) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + } else { + $onHeaders = null; + } + + return static function ($ch, $h) use ( + $onHeaders, + $easy, + &$startingResponse + ) { + $value = \trim($h); + if ($value === '') { + $startingResponse = true; + try { + $easy->createResponse(); + } catch (\Exception $e) { + $easy->createResponseException = $e; + + return -1; + } + if ($onHeaders !== null) { + try { + $onHeaders($easy->response); + } catch (\Exception $e) { + // Associate the exception with the handle and trigger + // a curl header write error by returning 0. + $easy->onHeadersException = $e; + + return -1; + } + } + } elseif ($startingResponse) { + $startingResponse = false; + $easy->headers = [$value]; + } else { + $easy->headers[] = $value; + } + + return \strlen($h); + }; + } + + public function __destruct() + { + foreach ($this->handles as $id => $handle) { + \curl_close($handle); + unset($this->handles[$id]); + } + } +} diff --git a/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php b/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php new file mode 100644 index 000000000..fe57ed5d5 --- /dev/null +++ b/vendor/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php @@ -0,0 +1,25 @@ +factory = $options['handle_factory'] + ?? new CurlFactory(3); + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + if (isset($options['delay'])) { + \usleep($options['delay'] * 1000); + } + + $easy = $this->factory->create($request, $options); + \curl_exec($easy->handle); + $easy->errno = \curl_errno($easy->handle); + + return CurlFactory::finish($this, $easy, $this->factory); + } +} diff --git a/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php b/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php new file mode 100644 index 000000000..a64e1821a --- /dev/null +++ b/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php @@ -0,0 +1,267 @@ + An array of delay times, indexed by handle id in `addRequest`. + * + * @see CurlMultiHandler::addRequest + */ + private $delays = []; + + /** + * @var array An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt() + */ + private $options = []; + + /** @var resource|\CurlMultiHandle */ + private $_mh; + + /** + * This handler accepts the following options: + * + * - handle_factory: An optional factory used to create curl handles + * - select_timeout: Optional timeout (in seconds) to block before timing + * out while selecting curl handles. Defaults to 1 second. + * - options: An associative array of CURLMOPT_* options and + * corresponding values for curl_multi_setopt() + */ + public function __construct(array $options = []) + { + $this->factory = $options['handle_factory'] ?? new CurlFactory(50); + + if (isset($options['select_timeout'])) { + $this->selectTimeout = $options['select_timeout']; + } elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { + @trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED); + $this->selectTimeout = (int) $selectTimeout; + } else { + $this->selectTimeout = 1; + } + + $this->options = $options['options'] ?? []; + + // unsetting the property forces the first access to go through + // __get(). + unset($this->_mh); + } + + /** + * @param string $name + * + * @return resource|\CurlMultiHandle + * + * @throws \BadMethodCallException when another field as `_mh` will be gotten + * @throws \RuntimeException when curl can not initialize a multi handle + */ + public function __get($name) + { + if ($name !== '_mh') { + throw new \BadMethodCallException("Can not get other property as '_mh'."); + } + + $multiHandle = \curl_multi_init(); + + if (false === $multiHandle) { + throw new \RuntimeException('Can not initialize curl multi handle.'); + } + + $this->_mh = $multiHandle; + + foreach ($this->options as $option => $value) { + // A warning is raised in case of a wrong option. + curl_multi_setopt($this->_mh, $option, $value); + } + + return $this->_mh; + } + + public function __destruct() + { + if (isset($this->_mh)) { + \curl_multi_close($this->_mh); + unset($this->_mh); + } + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $easy = $this->factory->create($request, $options); + $id = (int) $easy->handle; + + $promise = new Promise( + [$this, 'execute'], + function () use ($id) { + return $this->cancel($id); + } + ); + + $this->addRequest(['easy' => $easy, 'deferred' => $promise]); + + return $promise; + } + + /** + * Ticks the curl event loop. + */ + public function tick(): void + { + // Add any delayed handles if needed. + if ($this->delays) { + $currentTime = Utils::currentTime(); + foreach ($this->delays as $id => $delay) { + if ($currentTime >= $delay) { + unset($this->delays[$id]); + \curl_multi_add_handle( + $this->_mh, + $this->handles[$id]['easy']->handle + ); + } + } + } + + // Step through the task queue which may add additional requests. + P\Utils::queue()->run(); + + if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) { + // Perform a usleep if a select returns -1. + // See: https://bugs.php.net/bug.php?id=61141 + \usleep(250); + } + + while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { + } + + $this->processMessages(); + } + + /** + * Runs until all outstanding connections have completed. + */ + public function execute(): void + { + $queue = P\Utils::queue(); + + while ($this->handles || !$queue->isEmpty()) { + // If there are no transfers, then sleep for the next delay + if (!$this->active && $this->delays) { + \usleep($this->timeToNext()); + } + $this->tick(); + } + } + + private function addRequest(array $entry): void + { + $easy = $entry['easy']; + $id = (int) $easy->handle; + $this->handles[$id] = $entry; + if (empty($easy->options['delay'])) { + \curl_multi_add_handle($this->_mh, $easy->handle); + } else { + $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000); + } + } + + /** + * Cancels a handle from sending and removes references to it. + * + * @param int $id Handle ID to cancel and remove. + * + * @return bool True on success, false on failure. + */ + private function cancel($id): bool + { + if (!is_int($id)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + // Cannot cancel if it has been processed. + if (!isset($this->handles[$id])) { + return false; + } + + $handle = $this->handles[$id]['easy']->handle; + unset($this->delays[$id], $this->handles[$id]); + \curl_multi_remove_handle($this->_mh, $handle); + \curl_close($handle); + + return true; + } + + private function processMessages(): void + { + while ($done = \curl_multi_info_read($this->_mh)) { + if ($done['msg'] !== \CURLMSG_DONE) { + // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216 + continue; + } + $id = (int) $done['handle']; + \curl_multi_remove_handle($this->_mh, $done['handle']); + + if (!isset($this->handles[$id])) { + // Probably was cancelled. + continue; + } + + $entry = $this->handles[$id]; + unset($this->handles[$id], $this->delays[$id]); + $entry['easy']->errno = $done['result']; + $entry['deferred']->resolve( + CurlFactory::finish($this, $entry['easy'], $this->factory) + ); + } + } + + private function timeToNext(): int + { + $currentTime = Utils::currentTime(); + $nextTime = \PHP_INT_MAX; + foreach ($this->delays as $time) { + if ($time < $nextTime) { + $nextTime = $time; + } + } + + return ((int) \max(0, $nextTime - $currentTime)) * 1000000; + } +} diff --git a/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php b/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php new file mode 100644 index 000000000..1bc39f4b4 --- /dev/null +++ b/vendor/guzzlehttp/guzzle/src/Handler/EasyHandle.php @@ -0,0 +1,112 @@ +headers); + + $normalizedKeys = Utils::normalizeHeaderKeys($headers); + + if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) { + $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; + unset($headers[$normalizedKeys['content-encoding']]); + if (isset($normalizedKeys['content-length'])) { + $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; + + $bodyLength = (int) $this->sink->getSize(); + if ($bodyLength) { + $headers[$normalizedKeys['content-length']] = $bodyLength; + } else { + unset($headers[$normalizedKeys['content-length']]); + } + } + } + + // Attach a response to the easy handle with the parsed headers. + $this->response = new Response( + $status, + $headers, + $this->sink, + $ver, + $reason + ); + } + + /** + * @param string $name + * + * @return void + * + * @throws \BadMethodCallException + */ + public function __get($name) + { + $msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: '.$name; + throw new \BadMethodCallException($msg); + } +} diff --git a/vendor/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php b/vendor/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php new file mode 100644 index 000000000..5554b8fa9 --- /dev/null +++ b/vendor/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php @@ -0,0 +1,42 @@ +|null $queue The parameters to be passed to the append function, as an indexed array. + * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. + * @param callable|null $onRejected Callback to invoke when the return value is rejected. + */ + public function __construct(array $queue = null, callable $onFulfilled = null, callable $onRejected = null) + { + $this->onFulfilled = $onFulfilled; + $this->onRejected = $onRejected; + + if ($queue) { + // array_values included for BC + $this->append(...array_values($queue)); + } + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + if (!$this->queue) { + throw new \OutOfBoundsException('Mock queue is empty'); + } + + if (isset($options['delay']) && \is_numeric($options['delay'])) { + \usleep((int) $options['delay'] * 1000); + } + + $this->lastRequest = $request; + $this->lastOptions = $options; + $response = \array_shift($this->queue); + + if (isset($options['on_headers'])) { + if (!\is_callable($options['on_headers'])) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + try { + $options['on_headers']($response); + } catch (\Exception $e) { + $msg = 'An error was encountered during the on_headers event'; + $response = new RequestException($msg, $request, $response, $e); + } + } + + if (\is_callable($response)) { + $response = $response($request, $options); + } + + $response = $response instanceof \Throwable + ? P\Create::rejectionFor($response) + : P\Create::promiseFor($response); + + return $response->then( + function (?ResponseInterface $value) use ($request, $options) { + $this->invokeStats($request, $options, $value); + if ($this->onFulfilled) { + ($this->onFulfilled)($value); + } + + if ($value !== null && isset($options['sink'])) { + $contents = (string) $value->getBody(); + $sink = $options['sink']; + + if (\is_resource($sink)) { + \fwrite($sink, $contents); + } elseif (\is_string($sink)) { + \file_put_contents($sink, $contents); + } elseif ($sink instanceof StreamInterface) { + $sink->write($contents); + } + } + + return $value; + }, + function ($reason) use ($request, $options) { + $this->invokeStats($request, $options, null, $reason); + if ($this->onRejected) { + ($this->onRejected)($reason); + } + + return P\Create::rejectionFor($reason); + } + ); + } + + /** + * Adds one or more variadic requests, exceptions, callables, or promises + * to the queue. + * + * @param mixed ...$values + */ + public function append(...$values): void + { + foreach ($values as $value) { + if ($value instanceof ResponseInterface + || $value instanceof \Throwable + || $value instanceof PromiseInterface + || \is_callable($value) + ) { + $this->queue[] = $value; + } else { + throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.Utils::describeType($value)); + } + } + } + + /** + * Get the last received request. + */ + public function getLastRequest(): ?RequestInterface + { + return $this->lastRequest; + } + + /** + * Get the last received request options. + */ + public function getLastOptions(): array + { + return $this->lastOptions; + } + + /** + * Returns the number of remaining items in the queue. + */ + public function count(): int + { + return \count($this->queue); + } + + public function reset(): void + { + $this->queue = []; + } + + /** + * @param mixed $reason Promise or reason. + */ + private function invokeStats( + RequestInterface $request, + array $options, + ResponseInterface $response = null, + $reason = null + ): void { + if (isset($options['on_stats'])) { + $transferTime = $options['transfer_time'] ?? 0; + $stats = new TransferStats($request, $response, $transferTime, $reason); + ($options['on_stats'])($stats); + } + } +} diff --git a/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php b/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php new file mode 100644 index 000000000..f045b526c --- /dev/null +++ b/vendor/guzzlehttp/guzzle/src/Handler/Proxy.php @@ -0,0 +1,51 @@ +withoutHeader('Expect'); + + // Append a content-length header if body size is zero to match + // cURL's behavior. + if (0 === $request->getBody()->getSize()) { + $request = $request->withHeader('Content-Length', '0'); + } + + return $this->createResponse( + $request, + $options, + $this->createStream($request, $options), + $startTime + ); + } catch (\InvalidArgumentException $e) { + throw $e; + } catch (\Exception $e) { + // Determine if the error was a networking error. + $message = $e->getMessage(); + // This list can probably get more comprehensive. + if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed + || false !== \strpos($message, 'Connection refused') + || false !== \strpos($message, "couldn't connect to host") // error on HHVM + || false !== \strpos($message, 'connection attempt failed') + ) { + $e = new ConnectException($e->getMessage(), $request, $e); + } else { + $e = RequestException::wrapException($request, $e); + } + $this->invokeStats($options, $request, $startTime, null, $e); + + return P\Create::rejectionFor($e); + } + } + + private function invokeStats( + array $options, + RequestInterface $request, + ?float $startTime, + ResponseInterface $response = null, + \Throwable $error = null + ): void { + if (isset($options['on_stats'])) { + $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []); + ($options['on_stats'])($stats); + } + } + + /** + * @param resource $stream + */ + private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime): PromiseInterface + { + $hdrs = $this->lastHeaders; + $this->lastHeaders = []; + + try { + [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered while creating the response', $request, null, $e) + ); + } + + [$stream, $headers] = $this->checkDecode($options, $headers, $stream); + $stream = Psr7\Utils::streamFor($stream); + $sink = $stream; + + if (\strcasecmp('HEAD', $request->getMethod())) { + $sink = $this->createSink($stream, $options); + } + + try { + $response = new Psr7\Response($status, $headers, $sink, $ver, $reason); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered while creating the response', $request, null, $e) + ); + } + + if (isset($options['on_headers'])) { + try { + $options['on_headers']($response); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered during the on_headers event', $request, $response, $e) + ); + } + } + + // Do not drain when the request is a HEAD request because they have + // no body. + if ($sink !== $stream) { + $this->drain($stream, $sink, $response->getHeaderLine('Content-Length')); + } + + $this->invokeStats($options, $request, $startTime, $response, null); + + return new FulfilledPromise($response); + } + + private function createSink(StreamInterface $stream, array $options): StreamInterface + { + if (!empty($options['stream'])) { + return $stream; + } + + $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+'); + + return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink); + } + + /** + * @param resource $stream + */ + private function checkDecode(array $options, array $headers, $stream): array + { + // Automatically decode responses when instructed. + if (!empty($options['decode_content'])) { + $normalizedKeys = Utils::normalizeHeaderKeys($headers); + if (isset($normalizedKeys['content-encoding'])) { + $encoding = $headers[$normalizedKeys['content-encoding']]; + if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { + $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream)); + $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; + + // Remove content-encoding header + unset($headers[$normalizedKeys['content-encoding']]); + + // Fix content-length header + if (isset($normalizedKeys['content-length'])) { + $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; + $length = (int) $stream->getSize(); + if ($length === 0) { + unset($headers[$normalizedKeys['content-length']]); + } else { + $headers[$normalizedKeys['content-length']] = [$length]; + } + } + } + } + } + + return [$stream, $headers]; + } + + /** + * Drains the source stream into the "sink" client option. + * + * @param string $contentLength Header specifying the amount of + * data to read. + * + * @throws \RuntimeException when the sink option is invalid. + */ + private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength): StreamInterface + { + // If a content-length header is provided, then stop reading once + // that number of bytes has been read. This can prevent infinitely + // reading from a stream when dealing with servers that do not honor + // Connection: Close headers. + Psr7\Utils::copyToStream( + $source, + $sink, + (\strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 + ); + + $sink->seek(0); + $source->close(); + + return $sink; + } + + /** + * Create a resource and check to ensure it was created successfully + * + * @param callable $callback Callable that returns stream resource + * + * @return resource + * + * @throws \RuntimeException on error + */ + private function createResource(callable $callback) + { + $errors = []; + \set_error_handler(static function ($_, $msg, $file, $line) use (&$errors): bool { + $errors[] = [ + 'message' => $msg, + 'file' => $file, + 'line' => $line, + ]; + + return true; + }); + + try { + $resource = $callback(); + } finally { + \restore_error_handler(); + } + + if (!$resource) { + $message = 'Error creating resource: '; + foreach ($errors as $err) { + foreach ($err as $key => $value) { + $message .= "[$key] $value".\PHP_EOL; + } + } + throw new \RuntimeException(\trim($message)); + } + + return $resource; + } + + /** + * @return resource + */ + private function createStream(RequestInterface $request, array $options) + { + static $methods; + if (!$methods) { + $methods = \array_flip(\get_class_methods(__CLASS__)); + } + + if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) { + throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request); + } + + // HTTP/1.1 streams using the PHP stream wrapper require a + // Connection: close header + if ($request->getProtocolVersion() == '1.1' + && !$request->hasHeader('Connection') + ) { + $request = $request->withHeader('Connection', 'close'); + } + + // Ensure SSL is verified by default + if (!isset($options['verify'])) { + $options['verify'] = true; + } + + $params = []; + $context = $this->getDefaultContext($request); + + if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + + if (!empty($options)) { + foreach ($options as $key => $value) { + $method = "add_{$key}"; + if (isset($methods[$method])) { + $this->{$method}($request, $context, $value, $params); + } + } + } + + if (isset($options['stream_context'])) { + if (!\is_array($options['stream_context'])) { + throw new \InvalidArgumentException('stream_context must be an array'); + } + $context = \array_replace_recursive($context, $options['stream_context']); + } + + // Microsoft NTLM authentication only supported with curl handler + if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) { + throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); + } + + $uri = $this->resolveHost($request, $options); + + $contextResource = $this->createResource( + static function () use ($context, $params) { + return \stream_context_create($context, $params); + } + ); + + return $this->createResource( + function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) { + $resource = @\fopen((string) $uri, 'r', false, $contextResource); + $this->lastHeaders = $http_response_header ?? []; + + if (false === $resource) { + throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context); + } + + if (isset($options['read_timeout'])) { + $readTimeout = $options['read_timeout']; + $sec = (int) $readTimeout; + $usec = ($readTimeout - $sec) * 100000; + \stream_set_timeout($resource, $sec, $usec); + } + + return $resource; + } + ); + } + + private function resolveHost(RequestInterface $request, array $options): UriInterface + { + $uri = $request->getUri(); + + if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) { + if ('v4' === $options['force_ip_resolve']) { + $records = \dns_get_record($uri->getHost(), \DNS_A); + if (false === $records || !isset($records[0]['ip'])) { + throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); + } + + return $uri->withHost($records[0]['ip']); + } + if ('v6' === $options['force_ip_resolve']) { + $records = \dns_get_record($uri->getHost(), \DNS_AAAA); + if (false === $records || !isset($records[0]['ipv6'])) { + throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); + } + + return $uri->withHost('['.$records[0]['ipv6'].']'); + } + } + + return $uri; + } + + private function getDefaultContext(RequestInterface $request): array + { + $headers = ''; + foreach ($request->getHeaders() as $name => $value) { + foreach ($value as $val) { + $headers .= "$name: $val\r\n"; + } + } + + $context = [ + 'http' => [ + 'method' => $request->getMethod(), + 'header' => $headers, + 'protocol_version' => $request->getProtocolVersion(), + 'ignore_errors' => true, + 'follow_location' => 0, + ], + 'ssl' => [ + 'peer_name' => $request->getUri()->getHost(), + ], + ]; + + $body = (string) $request->getBody(); + + if ('' !== $body) { + $context['http']['content'] = $body; + // Prevent the HTTP handler from adding a Content-Type header. + if (!$request->hasHeader('Content-Type')) { + $context['http']['header'] .= "Content-Type:\r\n"; + } + } + + $context['http']['header'] = \rtrim($context['http']['header']); + + return $context; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void + { + $uri = null; + + if (!\is_array($value)) { + $uri = $value; + } else { + $scheme = $request->getUri()->getScheme(); + if (isset($value[$scheme])) { + if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) { + $uri = $value[$scheme]; + } + } + } + + if (!$uri) { + return; + } + + $parsed = $this->parse_proxy($uri); + $options['http']['proxy'] = $parsed['proxy']; + + if ($parsed['auth']) { + if (!isset($options['http']['header'])) { + $options['http']['header'] = []; + } + $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}"; + } + } + + /** + * Parses the given proxy URL to make it compatible with the format PHP's stream context expects. + */ + private function parse_proxy(string $url): array + { + $parsed = \parse_url($url); + + if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') { + if (isset($parsed['host']) && isset($parsed['port'])) { + $auth = null; + if (isset($parsed['user']) && isset($parsed['pass'])) { + $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}"); + } + + return [ + 'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", + 'auth' => $auth ? "Basic {$auth}" : null, + ]; + } + } + + // Return proxy as-is. + return [ + 'proxy' => $url, + 'auth' => null, + ]; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value > 0) { + $options['http']['timeout'] = $value; + } + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params): void + { + if ( + $value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT + || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT + || (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) + ) { + $options['http']['crypto_method'] = $value; + + return; + } + + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value === false) { + $options['ssl']['verify_peer'] = false; + $options['ssl']['verify_peer_name'] = false; + + return; + } + + if (\is_string($value)) { + $options['ssl']['cafile'] = $value; + if (!\file_exists($value)) { + throw new \RuntimeException("SSL CA bundle not found: $value"); + } + } elseif ($value !== true) { + throw new \InvalidArgumentException('Invalid verify request option'); + } + + $options['ssl']['verify_peer'] = true; + $options['ssl']['verify_peer_name'] = true; + $options['ssl']['allow_self_signed'] = false; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void + { + if (\is_array($value)) { + $options['ssl']['passphrase'] = $value[1]; + $value = $value[0]; + } + + if (!\file_exists($value)) { + throw new \RuntimeException("SSL certificate not found: {$value}"); + } + + $options['ssl']['local_cert'] = $value; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void + { + self::addNotification( + $params, + static function ($code, $a, $b, $c, $transferred, $total) use ($value) { + if ($code == \STREAM_NOTIFY_PROGRESS) { + // The upload progress cannot be determined. Use 0 for cURL compatibility: + // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html + $value($total, $transferred, 0, 0); + } + } + ); + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value === false) { + return; + } + + static $map = [ + \STREAM_NOTIFY_CONNECT => 'CONNECT', + \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', + \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', + \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', + \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', + \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', + \STREAM_NOTIFY_PROGRESS => 'PROGRESS', + \STREAM_NOTIFY_FAILURE => 'FAILURE', + \STREAM_NOTIFY_COMPLETED => 'COMPLETED', + \STREAM_NOTIFY_RESOLVE => 'RESOLVE', + ]; + static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; + + $value = Utils::debugResource($value); + $ident = $request->getMethod().' '.$request->getUri()->withFragment(''); + self::addNotification( + $params, + static function (int $code, ...$passed) use ($ident, $value, $map, $args): void { + \fprintf($value, '<%s> [%s] ', $ident, $map[$code]); + foreach (\array_filter($passed) as $i => $v) { + \fwrite($value, $args[$i].': "'.$v.'" '); + } + \fwrite($value, "\n"); + } + ); + } + + private static function addNotification(array &$params, callable $notify): void + { + // Wrap the existing function if needed. + if (!isset($params['notification'])) { + $params['notification'] = $notify; + } else { + $params['notification'] = self::callArray([ + $params['notification'], + $notify, + ]); + } + } + + private static function callArray(array $functions): callable + { + return static function (...$args) use ($functions) { + foreach ($functions as $fn) { + $fn(...$args); + } + }; + } +} -- cgit v1.2.3