lOrString($options, 'decode_content'); self::warnIfPresentAndNotNumber($options, 'delay'); if (isset($options['delay']) && \is_numeric($options['delay'])) { $delay = (float) $options['delay']; if (!\is_finite($delay) || $delay < 0.0) { self::warnInvalidRequestOptionType('delay', 'finite int|float greater than or equal to 0', $options['delay'], '7.13'); } } self::warnIfPresentAndNotBoolOrInt($options, 'expect'); if (isset($options['form_params'])) { self::warnAboutInvalidFormParamTypes($options['form_params']); } if (isset($options['force_ip_resolve']) && !\is_string($options['force_ip_resolve'])) { self::warnInvalidRequestOptionType('force_ip_resolve', 'string', $options['force_ip_resolve']); } if ( isset($options['force_ip_resolve']) && \is_string($options['force_ip_resolve']) && $options['force_ip_resolve'] !== 'v4' && $options['force_ip_resolve'] !== 'v6' ) { self::warnInvalidRequestOptionType('force_ip_resolve', '"v4"|"v6"', $options['force_ip_resolve'], '7.13'); } if (isset($options['headers'])) { self::warnAboutInvalidHeaderOptionTypes($options['headers']); } self::warnIfPresentAndNotBool($options, 'http_errors'); if (isset($options['multipart'])) { self::warnAboutInvalidMultipartOptionTypes($options['multipart']); } self::warnIfPresentAndNotCallable($options, 'on_headers'); self::warnIfPresentAndNotCallable($options, 'on_stats'); self::warnIfPresentAndNotCallable($options, 'on_trailers', null, '7.14'); self::warnIfPresentAndNotCallable($options, 'progress'); self::warnIfPresentAndNotStringArray($options, 'protocols', true); self::warnAboutInvalidProtocolValues($options, 'protocols'); self::warnAboutInvalidProxyOptionTypes($options); self::warnIfPresentAndNotNumber($options, 'read_timeout'); self::warnIfPresentAndNotInt($options, 'retries'); if (isset($options['sink']) && !\is_bool($options['sink']) && !\is_resource($options['sink']) && !\is_string($options['sink']) && !$options['sink'] instanceof StreamInterface) { self::warnInvalidRequestOptionType('sink', 'resource|string|StreamInterface', $options['sink']); } self::warnAboutInvalidTlsFileOptionTypes($options, 'ssl_key'); self::warnIfPresentAndNotString($options, 'ssl_key_type'); self::warnIfPresentAndNotBool($options, 'stream'); self::warnIfPresentAndNotArray($options, 'stream_context', 'array'); self::warnIfPresentAndNotBool($options, 'synchronous'); self::warnIfPresentAndNotNumber($options, 'timeout'); self::warnIfPresentAndNotBoolOrString($options, 'verify'); self::warnIfPresentAndNotStringOrNumber($options, 'version'); self::warnIfPresentAndNotArray($options, 'curl', 'array'); if (isset($options['cookies']) && $options['cookies'] === true) { self::warnInvalidRequestOptionType('cookies', 'false|CookieJarInterface', $options['cookies']); } if ( isset($options['cookies']) && $options['cookies'] !== false && $options['cookies'] !== true && !($options['cookies'] instanceof CookieJarInterface) ) { self::warnInvalidRequestOptionType('cookies', 'false|CookieJarInterface', $options['cookies'], '7.13'); } } private static function warnAboutInvalidAllowRedirectsOptionTypes(array $allowRedirects): void { self::warnIfPresentAndNotInt($allowRedirects, 'max', 'allow_redirects.max'); self::warnIfPresentAndNotBool($allowRedirects, 'strict', 'allow_redirects.strict'); self::warnIfPresentAndNotBool($allowRedirects, 'referer', 'allow_redirects.referer'); self::warnIfPresentAndNotStringArray($allowRedirects, 'protocols', true, 'allow_redirects.protocols'); self::warnAboutInvalidProtocolValues($allowRedirects, 'protocols', 'allow_redirects.protocols'); self::warnIfPresentAndNotCallable($allowRedirects, 'on_redirect', 'allow_redirects.on_redirect'); self::warnIfPresentAndNotBool($allowRedirects, 'track_redirects', 'allow_redirects.track_redirects'); } /** * @param mixed $auth */ private static function warnAboutInvalidAuthOptionTypes($auth): void { if ($auth === false || \is_string($auth) || $auth === []) { return; } if (!\is_array($auth)) { self::warnInvalidRequestOptionType('auth', 'array{0: string, 1: string, 2?: string|null}|string|false|null', $auth); return; } if (!\array_key_exists(0, $auth) || !\is_string($auth[0])) { self::warnInvalidRequestOptionType('auth.0', 'string', $auth[0] ?? null); } if (!\array_key_exists(1, $auth) || !\is_string($auth[1])) { self::warnInvalidRequestOptionType('auth.1', 'string', $auth[1] ?? null); } if (\array_key_exists(2, $auth) && $auth[2] !== null && !\is_string($auth[2])) { self::warnInvalidRequestOptionType('auth.2', 'string|null', $auth[2]); } } /** * @param mixed $value */ private static function warnAboutInvalidFormParamTypes($value): void { if (!\is_array($value)) { self::warnInvalidRequestOptionType('form_params', 'array', $value); return; } self::warnAboutInvalidFormParamArray($value, 'form_params'); } private static function warnAboutInvalidFormParamArray(array $values, string $path): bool { foreach ($values as $key => $item) { $itemPath = $path.'.'.(string) $key; if (\is_array($item)) { if (!self::warnAboutInvalidFormParamArray($item, $itemPath)) { return false; } continue; } if ($item !== null && !\is_scalar($item)) { self::warnInvalidRequestOptionType($itemPath, 'string|int|float|bool|null|array', $item); return false; } } return true; } /** * @param mixed $headers */ private static function warnAboutInvalidHeaderOptionTypes($headers): void { if (!\is_array($headers)) { self::warnInvalidRequestOptionType('headers', 'array>|null', $headers); return; } foreach ($headers as $name => $value) { $path = 'headers.'.(string) $name; if (\is_array($value)) { if ($value === []) { self::warnInvalidRequestOptionType($path, 'string|non-empty-array', $value); break; } foreach ($value as $index => $item) { if (!\is_string($item)) { self::warnInvalidRequestOptionType($path.'.'.(string) $index, 'string', $item); break 2; } } } elseif (!\is_string($value)) { self::warnInvalidRequestOptionType($path, 'string|non-empty-array', $value); break; } } } /** * @param mixed $multipart */ private static function warnAboutInvalidMultipartOptionTypes($multipart): void { if (!\is_array($multipart)) { self::warnInvalidRequestOptionType('multipart', 'array, filename?: string}>', $multipart); return; } foreach ($multipart as $index => $part) { $path = 'multipart.'.(string) $index; if (!\is_array($part)) { self::warnInvalidRequestOptionType($path, 'array{name: string|int, contents: mixed, headers?: array, filename?: string}', $part); return; } if (!\array_key_exists('name', $part) || (!\is_string($part['name']) && !\is_int($part['name']))) { self::warnInvalidRequestOptionType($path.'.name', 'string|int', $part['name'] ?? null); } if (!\array_key_exists('contents', $part)) { self::warnInvalidRequestOptionType($path, 'array{name: string|int, contents: mixed, headers?: array, filename?: string}', $part); } if (\array_key_exists('headers', $part)) { if (!\is_array($part['headers'])) { self::warnInvalidRequestOptionType($path.'.headers', 'array', $part['headers']); } else { foreach ($part['headers'] as $name => $value) { if (!\is_string($value)) { self::warnInvalidRequestOptionType($path.'.headers.'.(string) $name, 'string', $value); break 2; } } } } if (\array_key_exists('filename', $part) && !\is_string($part['filename'])) { self::warnInvalidRequestOptionType($path.'.filename', 'string', $part['filename']); } } } private static function warnAboutInvalidProxyOptionTypes(array $options): void { if (!isset($options['proxy'])) { return; } if (!\is_string($options['proxy']) && !\is_array($options['proxy'])) { self::warnInvalidRequestOptionType('proxy', 'string|array{http?: string|null, https?: string|null, no?: string|array|null}', $options['proxy']); return; } if (!\is_array($options['proxy'])) { return; } foreach (['http', 'https'] as $scheme) { if (\array_key_exists($scheme, $options['proxy']) && $options['proxy'][$scheme] !== null && !\is_string($options['proxy'][$scheme])) { self::warnInvalidRequestOptionType('proxy.'.$scheme, 'string|null', $options['proxy'][$scheme]); } } if (!\array_key_exists('no', $options['proxy']) || $options['proxy']['no'] === null) { return; } if (\is_string($options['proxy']['no'])) { return; } if (!\is_array($options['proxy']['no'])) { self::warnInvalidRequestOptionType('proxy.no', 'string|array|null', $options['proxy']['no']); return; } foreach ($options['proxy']['no'] as $index => $noProxy) { if (!\is_string($noProxy)) { self::warnInvalidRequestOptionType('proxy.no.'.(string) $index, 'string', $noProxy); return; } } } private static function warnAboutInvalidTlsFileOptionTypes(array $options, string $option): void { if (!isset($options[$option])) { return; } if (\is_string($options[$option])) { return; } if (!\is_array($options[$option])) { self::warnInvalidRequestOptionType($option, 'string|array{0: string, 1?: string}', $options[$option]); return; } if (!\array_key_exists(0, $options[$option]) || !\is_string($options[$option][0])) { self::warnInvalidRequestOptionType($option.'.0', 'string', $options[$option][0] ?? null); } if (\array_key_exists(1, $options[$option]) && $options[$option][1] !== null && !\is_string($options[$option][1])) { self::warnInvalidRequestOptionType($option.'.1', 'string|null', $options[$option][1]); } } private static function warnIfPresentAndNotArray(array $options, string $option, string $expected): void { if (\array_key_exists($option, $options) && !\is_array($options[$option])) { self::warnInvalidRequestOptionType($option, $expected, $options[$option]); } } private static function warnIfPresentAndNotBool(array $options, string $option, ?string $path = null): void { if (\array_key_exists($option, $options) && !\is_bool($options[$option])) { self::warnInvalidRequestOptionType($path ?? $option, 'bool', $options[$option]); } } private static function warnIfPresentAndNotBoolOrInt(array $options, string $option): void { if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_int($options[$option])) { self::warnInvalidRequestOptionType($option, 'bool|int', $options[$option]); } } private static function warnIfPresentAndNotBoolOrResource(array $options, string $option): void { if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_resource($options[$option])) { self::warnInvalidRequestOptionType($option, 'bool|resource', $options[$option]); } } private static function warnIfPresentAndNotBoolOrString(array $options, string $option): void { if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_string($options[$option])) { self::warnInvalidRequestOptionType($option, 'bool|string', $options[$option]); } } private static function warnIfPresentAndNotCallable( array $options, string $option, ?string $path = null, string $since = '7.11' ): void { if (\array_key_exists($option, $options) && !\is_callable($options[$option])) { self::warnInvalidRequestOptionType($path ?? $option, 'callable', $options[$option], $since); } } private static function warnIfPresentAndNotInt( array $options, string $option, ?string $path = null, string $since = '7.11' ): void { if (\array_key_exists($option, $options) && !\is_int($options[$option])) { self::warnInvalidRequestOptionType($path ?? $option, 'int', $options[$option], $since); } } private static function warnIfPresentAndNotNumber(array $options, string $option): void { if (\array_key_exists($option, $options) && !\is_int($options[$option]) && !\is_float($options[$option])) { self::warnInvalidRequestOptionType($option, 'int|float', $options[$option]); } } private static function warnIfPresentAndNotString(array $options, string $option): void { if (\array_key_exists($option, $options) && !\is_string($options[$option])) { self::warnInvalidRequestOptionType($option, 'string', $options[$option]); } } private static function warnIfPresentAndNotStringArray(array $options, string $option, bool $nonEmpty, ?string $path = null): void { if (!\array_key_exists($option, $options)) { return; } $path = $path ?? $option; $expected = ($nonEmpty ? 'non-empty-' : '').'array'; if (!\is_array($options[$option]) || ($nonEmpty && $options[$option] === [])) { self::warnInvalidRequestOptionType($path, $expected, $options[$option]); return; } foreach ($options[$option] as $index => $item) { if (!\is_string($item)) { self::warnInvalidRequestOptionType($path.'.'.(string) $index, 'string', $item); return; } } } /** * @param array $options */ private static function warnAboutInvalidProtocolValues(array $options, string $option, ?string $path = null): void { if (!isset($options[$option]) || !\is_array($options[$option])) { return; } $path = $path ?? $option; foreach ($options[$option] as $index => $protocol) { if (\is_string($protocol) && $protocol !== 'http' && $protocol !== 'https') { self::warnInvalidRequestOptionType($path.'.'.(string) $index, '"http"|"https"', $protocol, '7.13'); } } } private static function warnIfPresentAndNotStringOrNumber(array $options, string $option): void { if ( \array_key_exists($option, $options) && !\is_string($options[$option]) && !\is_int($options[$option]) && !\is_float($options[$option]) ) { self::warnInvalidRequestOptionType($option, 'string|int|float', $options[$option]); } } /** * @param mixed $value */ private static function warnInvalidRequestOptionType(string $option, string $expected, $value, string $since = '7.11'): void { \trigger_deprecation( 'guzzlehttp/guzzle', $since, 'Passing %s to request option "%s" is deprecated; guzzlehttp/guzzle 8.0 requires %s.', \get_debug_type($value), $option, $expected ); } /** * Transfers the given request and applies request options. * * The URI of the request is not modified and the request options are used * as-is without merging in default options. * * @param array $options See {@see RequestOptions}. */ private function transfer(RequestInterface $request, array $options): PromiseInterface { $request = $this->applyOptions($request, $options); $protocolVersion = $request->getProtocolVersion(); if ('' === $protocolVersion) { \trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with an empty protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.'); $request = Psr7\Utils::modifyRequest($request, ['version' => '1.1']); } elseif (!self::isProtocolVersionValid($protocolVersion)) { \trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with a malformed protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject malformed protocol versions.'); } /** @var HandlerStack $handler */ $handler = $options['handler']; try { return P\Create::promiseFor($handler($request, $options)); } catch (\Exception $e) { return P\Create::rejectionFor($e); } } /** * Applies the array of request options to a request. */ private function applyOptions(RequestInterface $request, array &$options): RequestInterface { $modify = [ 'set_headers' => [], ]; if (isset($options['headers'])) { if (array_keys($options['headers']) === range(0, count($options['headers']) - 1)) { throw new InvalidArgumentException('The headers array must have header name as keys.'); } $headers = $options['headers']; $droppedHeaderNames = self::castDeprecatedHeaderOptionValues($headers); if ($droppedHeaderNames !== [] && isset($options['_conditional'])) { $options['_conditional'] = Psr7\Utils::caselessRemove($droppedHeaderNames, $options['_conditional']); } $modify['set_headers'] = $headers; unset($options['headers']); } if (isset($options['form_params'])) { if (isset($options['multipart'])) { throw new InvalidArgumentException('You cannot use ' .'form_params and multipart at the same time. Use the ' .'form_params option if you want to send application/' .'x-www-form-urlencoded requests, and the multipart ' .'option to send multipart/form-data requests.'); } $options['body'] = \http_build_query(self::normalizeNonFiniteFloats($options['form_params'], 'form_params'), '', '&'); unset($options['form_params']); // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; } if (isset($options['multipart'])) { $options['body'] = new Psr7\MultipartStream($options['multipart']); unset($options['multipart']); } if (isset($options['json'])) { $json = \json_encode($options['json']); if (\JSON_ERROR_NONE !== \json_last_error()) { throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg()); } /** @var non-empty-string $json */ $options['body'] = $json; unset($options['json']); // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'application/json'; } if (isset($options['decode_content']) && \is_string($options['decode_content'])) { // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']); $modify['set_headers']['Accept-Encoding'] = (string) $options['decode_content']; } if (isset($options['body'])) { if (\is_array($options['body'])) { throw $this->invalidBody(); } $modify['body'] = self::createBodyStream($options['body']); unset($options['body']); } if (!empty($options['auth']) && \is_array($options['auth'])) { $value = $options['auth']; $type = isset($value[2]) ? Psr7\Utils::asciiToLower($value[2]) : 'basic'; switch ($type) { case 'basic': // Ensure that we don't have the header in different case and set the new value. $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']); $modify['set_headers']['Authorization'] = 'Basic ' .\base64_encode("$value[0]:$value[1]"); break; case 'digest': // @todo: Do not rely on curl $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST; $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; break; case 'ntlm': \trigger_deprecation( 'guzzlehttp/guzzle', '7.12', 'Passing "ntlm" as the built-in auth type is deprecated; guzzlehttp/guzzle 8.0 will no longer apply NTLM through the "auth" request option. NTLM is also deprecated by curl/libcurl and may be unavailable in current or future libcurl builds. Avoid NTLM; if you must use it temporarily, configure cURL HTTP authentication options directly with a libcurl build that still supports NTLM.' ); if (!CurlVersion::supportsNtlm()) { throw new InvalidArgumentException('NTLM authentication is not available because the installed curl/libcurl build does not provide NTLM support.'); } $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM; $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; break; } } if (isset($options['query'])) { $value = $options['query']; if (\is_array($value)) { $value = \http_build_query(self::normalizeNonFiniteFloats($value, 'query'), '', '&', \PHP_QUERY_RFC3986); } if (!\is_string($value)) { throw new InvalidArgumentException('query must be a string or array'); } $modify['query'] = $value; unset($options['query']); } // Ensure that sink is not an invalid value. if (isset($options['sink'])) { // TODO: Add more sink validation? if (\is_bool($options['sink'])) { throw new InvalidArgumentException('sink must not be a boolean'); } } if (isset($options['version'])) { $modify['version'] = self::normalizeProtocolVersion($options['version']); } $request = Psr7\Utils::modifyRequest($request, $modify); if ($request->getBody() instanceof Psr7\MultipartStream) { // Use a multipart/form-data POST if a Content-Type is not set. // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' .$request->getBody()->getBoundary(); } // Merge in conditional headers if they are not present. if (isset($options['_conditional'])) { // Build up the changes so it's in a single clone of the message. $modify = []; foreach ($options['_conditional'] as $k => $v) { $name = (string) $k; if (!$request->hasHeader($name)) { $modify['set_headers'][$name] = $v; } } $request = Psr7\Utils::modifyRequest($request, $modify); // Don't pass this internal value along to middleware/handlers. unset($options['_conditional']); } return $request; } /** * @param array $headers * * @return list */ private static function castDeprecatedHeaderOptionValues(array &$headers): array { $droppedHeaderNames = []; foreach ($headers as $name => $value) { if (\is_array($value)) { if ($value === []) { $droppedHeaderNames[] = (string) $name; unset($headers[$name]); continue; } foreach ($value as $index => $item) { if ($item === null || (!\is_string($item) && \is_scalar($item))) { if (\is_float($item) && !\is_finite($item)) { $item = \is_nan($item) ? 'NAN' : ($item > 0 ? 'INF' : '-INF'); } $value[$index] = (string) $item; } } $headers[$name] = $value; continue; } if ($value === null || (!\is_string($value) && \is_scalar($value))) { if (\is_float($value) && !\is_finite($value)) { $value = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); } $headers[$name] = (string) $value; } } return $droppedHeaderNames; } /** * @param mixed $body */ private static function createBodyStream($body): StreamInterface { if ($body instanceof StreamInterface) { return $body; } if (\is_resource($body) || $body === null || \is_string($body) || $body instanceof \Iterator) { return Psr7\Utils::streamFor($body); } if (\is_scalar($body)) { \trigger_deprecation('guzzlehttp/guzzle', '7.12', 'Passing a non-string scalar to the "body" request option is deprecated; guzzlehttp/guzzle 8.0 will reject non-string scalar bodies.'); return Psr7\Utils::streamFor(self::stringifyScalar($body)); } if (\is_object($body) && \method_exists($body, '__toString')) { return Psr7\Utils::streamFor((string) $body); } if (\is_callable($body)) { return Psr7\Utils::streamFor($body); } throw new InvalidArgumentException(\sprintf( 'Passing %s to request option "body" is invalid; expected resource|string|null|int|float|bool|StreamInterface|callable&object|Iterator|Stringable.', \get_debug_type($body) )); } /** * @param bool|float|int|string $value */ private static function stringifyScalar($value): string { // Normalize non-finite floats to dodge PHP 8.5's (string) NAN // coercion warning while the value is still accepted. if (\is_float($value) && !\is_finite($value)) { $value = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); } return (string) $value; } /** * Converts non-finite floats in the array to the strings PHP coerces * them to, as implicit coercion of NAN emits a warning on PHP 8.5. */ private static function normalizeNonFiniteFloats(array $values, string $option): array { foreach ($values as $key => $value) { if (\is_array($value)) { $values[$key] = self::normalizeNonFiniteFloats($value, $option); } elseif (\is_float($value) && !\is_finite($value)) { \trigger_deprecation('guzzlehttp/guzzle', '7.12', 'Passing a non-finite float in the "%s" request option is deprecated; guzzlehttp/guzzle 8.0 will reject non-finite floats.', $option); $values[$key] = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); } } return $values; } /** * @param string|int|float $version */ private static function normalizeProtocolVersion($version): string { if ('' === $version) { \trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing an empty "version" request option is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.'); return '1.1'; } return \is_float($version) ? \number_format($version, 1, '.', '') : (string) $version; } private static function isProtocolVersionValid(string $version): bool { return 1 === \preg_match('/^\d+(?:\.\d+)?$/D', $version); } /** * Return an InvalidArgumentException with pre-set message. */ private function invalidBody(): InvalidArgumentException { return new InvalidArgumentException('Passing in the "body" request ' .'option as an array to send a request is not supported. ' .'Please use the "form_params" request option to send a ' .'application/x-www-form-urlencoded request, or the "multipart" ' .'request option to send a multipart/form-data request.'); } }