PluginProbe
Gmail SMTP / trunk
Gmail SMTP vtrunk
1.2.3.21 1.2.3.20 trunk 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.9 1.2.0 1.2.3.14 1.2.3.15 1.2.3.16 1.2.3.18 1.2.3.5
gmail-smtp / google-api-php-client / vendor / guzzlehttp / guzzle / src / Client.php

Client.php in Gmail SMTP trunk, at google-api-php-client/vendor/guzzlehttp/guzzle/src/Client.php

1,270 lines 48.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace GuzzleHttp;
4
5 use GuzzleHttp\Cookie\CookieJar;
6 use GuzzleHttp\Exception\GuzzleException;
7 use GuzzleHttp\Exception\InvalidArgumentException;
8 use GuzzleHttp\Handler\CurlShareHandleState;
9 use GuzzleHttp\Promise as P;
10 use GuzzleHttp\Promise\PromiseInterface;
11 use Psr\Http\Message\RequestInterface;
12 use Psr\Http\Message\ResponseInterface;
13 use Psr\Http\Message\StreamInterface;
14 use Psr\Http\Message\UriInterface;
15
16 /**
17 * @final
18 */
19 class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
20 {
21 use ClientTrait;
22
23 /**
24 * @var array Default request options
25 */
26 private $config;
27
28 /**
29 * Clients accept an array of constructor parameters.
30 *
31 * Here's an example of creating a client using a base_uri and an array of
32 * default request options to apply to each request:
33 *
34 * $client = new Client([
35 * 'base_uri' => 'http://www.foo.com/1.0/',
36 * 'timeout' => 0,
37 * 'allow_redirects' => false,
38 * 'proxy' => '192.168.16.1:10'
39 * ]);
40 *
41 * Client configuration settings include the following options:
42 *
43 * - handler: (callable) Function that transfers HTTP requests over the
44 * wire. The function is called with a Psr7\Http\Message\RequestInterface
45 * and array of transfer options, and must return a
46 * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a
47 * Psr7\Http\Message\ResponseInterface on success.
48 * If no handler is provided, a default handler will be created
49 * that enables all of the request options below by attaching all of the
50 * default middleware to the handler.
51 * - base_uri: (string|UriInterface) Base URI of the client that is merged
52 * into relative URIs. Can be a string or instance of UriInterface.
53 * - transport_sharing: (string|null) Transport sharing mode for the
54 * default handler. Accepts TransportSharing::* or null. Defaults to null.
55 * - **: any request option
56 *
57 * @param array $config Client configuration settings.
58 *
59 * @see RequestOptions for a list of available request options.
60 */
61 public function __construct(array $config = [])
62 {
63 $transportSharing = \array_key_exists('transport_sharing', $config) ? $config['transport_sharing'] : null;
64 $transportSharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
65 unset($config['transport_sharing']);
66
67 if (!isset($config['handler'])) {
68 $config['handler'] = $transportSharingMode === TransportSharing::NONE
69 ? HandlerStack::create()
70 : HandlerStack::create(Utils::chooseHandler(['transport_sharing' => $transportSharingMode]));
71 } elseif (!\is_callable($config['handler'])) {
72 throw new InvalidArgumentException('handler must be a callable');
73 } elseif ($transportSharingMode === TransportSharing::HANDLER_REQUIRE) {
74 throw new InvalidArgumentException('The "transport_sharing" client option can only require sharing when Guzzle creates the default handler. Configure the "transport_sharing" option on CurlHandler or CurlMultiHandler when providing a custom cURL handler.');
75 }
76
77 // Convert the base_uri to a UriInterface
78 if (isset($config['base_uri'])) {
79 $config['base_uri'] = Psr7\Utils::uriFor($config['base_uri']);
80 }
81
82 $this->configureDefaults($config);
83 }
84
85 /**
86 * @param string $method
87 * @param array $args
88 *
89 * @return PromiseInterface|ResponseInterface
90 *
91 * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0.
92 */
93 public function __call($method, $args)
94 {
95 if (\count($args) < 1) {
96 throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
97 }
98
99 $uri = $args[0];
100 $opts = $args[1] ?? [];
101
102 $isAsync = \substr($method, -5) === 'Async';
103 $method = $isAsync ? \substr($method, 0, -5) : $method;
104 $method = \strtoupper($method);
105
106 return $isAsync
107 ? $this->requestAsync($method, $uri, $opts)
108 : $this->request($method, $uri, $opts);
109 }
110
111 /**
112 * Asynchronously send an HTTP request.
113 *
114 * @param array $options Request options to apply to the given
115 * request and to the transfer. See {@see RequestOptions}.
116 */
117 public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
118 {
119 // Merge the base URI into the request URI if needed.
120 $options = $this->prepareDefaults($options);
121
122 return $this->transfer(
123 $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')),
124 $options
125 );
126 }
127
128 /**
129 * Send an HTTP request.
130 *
131 * @param array $options Request options to apply to the given
132 * request and to the transfer. See {@see RequestOptions}.
133 *
134 * @throws GuzzleException
135 */
136 public function send(RequestInterface $request, array $options = []): ResponseInterface
137 {
138 $options[RequestOptions::SYNCHRONOUS] = true;
139
140 return $this->sendAsync($request, $options)->wait();
141 }
142
143 /**
144 * The HttpClient PSR (PSR-18) specify this method.
145 *
146 * {@inheritDoc}
147 */
148 public function sendRequest(RequestInterface $request): ResponseInterface
149 {
150 $options[RequestOptions::SYNCHRONOUS] = true;
151 $options[RequestOptions::ALLOW_REDIRECTS] = false;
152 $options[RequestOptions::HTTP_ERRORS] = false;
153
154 return $this->sendAsync($request, $options)->wait();
155 }
156
157 /**
158 * Create and send an asynchronous HTTP request.
159 *
160 * Use an absolute path to override the base path of the client, or a
161 * relative path to append to the base path of the client. The URL can
162 * contain the query string as well. Use an array to provide a URL
163 * template and additional variables to use in the URL template expansion.
164 *
165 * @param string $method HTTP method
166 * @param string|UriInterface $uri URI object or string.
167 * @param array $options Request options to apply. See {@see RequestOptions}.
168 */
169 public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
170 {
171 $normalizedMethod = \strtoupper($method);
172 if ($method !== $normalizedMethod) {
173 \trigger_deprecation(
174 'guzzlehttp/guzzle',
175 '7.11',
176 'Passing a non-uppercase HTTP method to Client::requestAsync() is deprecated; guzzlehttp/guzzle 8.0 will preserve HTTP method casing. Pass an uppercase method explicitly if uppercase is required.'
177 );
178 $method = $normalizedMethod;
179 }
180
181 $options = $this->prepareDefaults($options);
182 // Remove request modifying parameter because it can be done up-front.
183 $headers = $options['headers'] ?? [];
184 $droppedHeaderNames = self::castDeprecatedHeaderOptionValues($headers);
185 if ($droppedHeaderNames !== [] && isset($options['_conditional'])) {
186 $options['_conditional'] = Psr7\Utils::caselessRemove($droppedHeaderNames, $options['_conditional']);
187 }
188 $body = $options['body'] ?? null;
189 $version = self::normalizeProtocolVersion($options['version'] ?? '1.1');
190 // Merge the URI into the base URI.
191 $uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options);
192 if (\is_array($body)) {
193 throw $this->invalidBody();
194 }
195 $body = self::createBodyStream($body);
196 $request = new Psr7\Request($method, $uri, $headers, $body, $version);
197 // Remove the option so that they are not doubly-applied.
198 unset($options['headers'], $options['body'], $options['version']);
199
200 return $this->transfer($request, $options);
201 }
202
203 /**
204 * Create and send an HTTP request.
205 *
206 * Use an absolute path to override the base path of the client, or a
207 * relative path to append to the base path of the client. The URL can
208 * contain the query string as well.
209 *
210 * @param string $method HTTP method.
211 * @param string|UriInterface $uri URI object or string.
212 * @param array $options Request options to apply. See {@see RequestOptions}.
213 *
214 * @throws GuzzleException
215 */
216 public function request(string $method, $uri = '', array $options = []): ResponseInterface
217 {
218 $normalizedMethod = \strtoupper($method);
219 if ($method !== $normalizedMethod) {
220 \trigger_deprecation(
221 'guzzlehttp/guzzle',
222 '7.11',
223 'Passing a non-uppercase HTTP method to Client::request() is deprecated; guzzlehttp/guzzle 8.0 will preserve HTTP method casing. Pass an uppercase method explicitly if uppercase is required.'
224 );
225 $method = $normalizedMethod;
226 }
227
228 $options[RequestOptions::SYNCHRONOUS] = true;
229
230 return $this->requestAsync($method, $uri, $options)->wait();
231 }
232
233 /**
234 * Get a client configuration option.
235 *
236 * These options include default request options of the client, a "handler"
237 * (if utilized by the concrete client), and a "base_uri" if utilized by
238 * the concrete client.
239 *
240 * @param string|null $option The config option to retrieve.
241 *
242 * @return mixed
243 */
244 public function getConfig(?string $option = null)
245 {
246 return $option === null
247 ? $this->config
248 : ($this->config[$option] ?? null);
249 }
250
251 private function buildUri(UriInterface $uri, array $config): UriInterface
252 {
253 if (isset($config['base_uri'])) {
254 $uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri);
255 }
256
257 $idnOptions = Utils::normalizeIdnConversionOption($config['idn_conversion'] ?? null);
258 if ($idnOptions !== null) {
259 $uri = Utils::idnUriConvert($uri, $idnOptions);
260 }
261
262 return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri;
263 }
264
265 /**
266 * Configures the default options for a client.
267 */
268 private function configureDefaults(array $config): void
269 {
270 $defaults = [
271 'allow_redirects' => RedirectMiddleware::$defaultSettings,
272 'http_errors' => true,
273 'decode_content' => true,
274 'verify' => true,
275 'cookies' => false,
276 'idn_conversion' => false,
277 'protocols' => ['http', 'https'],
278 ];
279
280 // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set.
281
282 // We can only trust the HTTP_PROXY environment variable in a CLI
283 // process due to the fact that PHP has no reliable mechanism to
284 // get environment variables that start with "HTTP_".
285 if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) {
286 $defaults['proxy']['http'] = $proxy;
287 }
288
289 if ($proxy = Utils::getenv('HTTPS_PROXY')) {
290 $defaults['proxy']['https'] = $proxy;
291 }
292
293 if ($noProxy = Utils::getenv('NO_PROXY')) {
294 $cleanedNoProxy = \str_replace(' ', '', $noProxy);
295 $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy);
296 }
297
298 $this->config = $config + $defaults;
299
300 if (!empty($config['cookies']) && $config['cookies'] === true) {
301 $this->config['cookies'] = new CookieJar();
302 }
303
304 // Add the default user-agent header.
305 if (!isset($this->config['headers'])) {
306 $this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()];
307 } else {
308 // Add the User-Agent header if one was not already set.
309 $hasUserAgent = false;
310 foreach (\array_keys($this->config['headers']) as $name) {
311 if (\strtolower((string) $name) === 'user-agent') {
312 $hasUserAgent = true;
313 break;
314 }
315 }
316
317 if (!$hasUserAgent) {
318 $this->config['headers']['User-Agent'] = Utils::defaultUserAgent();
319 }
320 }
321
322 if (\is_array($this->config['headers'])) {
323 self::warnAboutInvalidHeaderOptionTypes($this->config['headers']);
324 self::castDeprecatedHeaderOptionValues($this->config['headers']);
325 }
326 }
327
328 /**
329 * Merges default options into the array.
330 *
331 * @param array $options Options to modify by reference
332 */
333 private function prepareDefaults(array $options): array
334 {
335 self::warnAboutRequestLevelHandler($options);
336
337 $defaults = $this->config;
338
339 if (!empty($defaults['headers'])) {
340 // Default headers are only added if they are not present.
341 $defaults['_conditional'] = $defaults['headers'];
342 unset($defaults['headers']);
343 }
344
345 // Special handling for headers is required as they are added as
346 // conditional headers and as headers passed to a request ctor.
347 if (\array_key_exists('headers', $options)) {
348 // Allows default headers to be unset.
349 if ($options['headers'] === null) {
350 $defaults['_conditional'] = [];
351 unset($options['headers']);
352 } elseif (!\is_array($options['headers'])) {
353 throw new InvalidArgumentException('headers must be an array');
354 }
355 }
356
357 // Shallow merge defaults underneath options.
358 $result = $options + $defaults;
359
360 // Remove null values.
361 foreach ($result as $k => $v) {
362 if ($v === null) {
363 unset($result[$k]);
364 }
365 }
366
367 self::warnAboutInvalidRequestOptionTypes($result);
368
369 return self::normalizeDeprecatedRequestOptionValues($result);
370 }
371
372 /**
373 * Normalize values that guzzlehttp/guzzle 8.0 rejects only after the
374 * corresponding 7.x deprecation has already been emitted.
375 *
376 * @param array<string, mixed> $options
377 *
378 * @return array<string, mixed>
379 */
380 private static function normalizeDeprecatedRequestOptionValues(array $options): array
381 {
382 self::normalizeDeprecatedAuthOptionValues($options);
383 self::normalizeDeprecatedTlsFileOptionValues($options, 'cert');
384 self::normalizeDeprecatedTlsFileOptionValues($options, 'ssl_key');
385 self::normalizeDeprecatedStringOptionValues($options);
386 self::normalizeDeprecatedNumericOptionValues($options);
387 self::normalizeDeprecatedIntegerOptionValues($options);
388
389 return $options;
390 }
391
392 /**
393 * @param mixed $value
394 */
395 private static function canStringifyDeprecatedValue($value): bool
396 {
397 return $value === null
398 || \is_scalar($value)
399 || (\is_object($value) && \method_exists($value, '__toString'));
400 }
401
402 /**
403 * @param mixed $value
404 */
405 private static function stringifyDeprecatedValue($value): string
406 {
407 if (\is_float($value) && !\is_finite($value)) {
408 return \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
409 }
410
411 if ($value === null) {
412 return '';
413 }
414
415 if (\is_scalar($value)) {
416 return (string) $value;
417 }
418
419 if (\is_object($value) && \method_exists($value, '__toString')) {
420 return $value->__toString();
421 }
422
423 throw new \LogicException('Value is not stringable.');
424 }
425
426 /**
427 * @param array<string, mixed> $options
428 */
429 private static function normalizeDeprecatedAuthOptionValues(array &$options): void
430 {
431 if (!isset($options['auth']) || !\is_array($options['auth']) || $options['auth'] === []) {
432 return;
433 }
434
435 foreach ([0, 1] as $index) {
436 if (
437 \array_key_exists($index, $options['auth'])
438 && !\is_string($options['auth'][$index])
439 && self::canStringifyDeprecatedValue($options['auth'][$index])
440 ) {
441 $options['auth'][$index] = self::stringifyDeprecatedValue($options['auth'][$index]);
442 }
443 }
444
445 if (
446 \array_key_exists(2, $options['auth'])
447 && $options['auth'][2] !== null
448 && !\is_string($options['auth'][2])
449 && self::canStringifyDeprecatedValue($options['auth'][2])
450 ) {
451 $options['auth'][2] = self::stringifyDeprecatedValue($options['auth'][2]);
452 }
453 }
454
455 /**
456 * @param array<string, mixed> $options
457 */
458 private static function normalizeDeprecatedTlsFileOptionValues(array &$options, string $option): void
459 {
460 if (!isset($options[$option]) || !\is_array($options[$option])) {
461 return;
462 }
463
464 foreach ([0, 1] as $index) {
465 if (
466 \array_key_exists($index, $options[$option])
467 && $options[$option][$index] !== null
468 && !\is_string($options[$option][$index])
469 && self::canStringifyDeprecatedValue($options[$option][$index])
470 ) {
471 $options[$option][$index] = self::stringifyDeprecatedValue($options[$option][$index]);
472 }
473 }
474 }
475
476 /**
477 * @param array<string, mixed> $options
478 */
479 private static function normalizeDeprecatedStringOptionValues(array &$options): void
480 {
481 foreach (['cert_type', 'force_ip_resolve', 'ssl_key_type'] as $option) {
482 if (
483 \array_key_exists($option, $options)
484 && !\is_string($options[$option])
485 && self::canStringifyDeprecatedValue($options[$option])
486 ) {
487 $options[$option] = self::stringifyDeprecatedValue($options[$option]);
488 }
489 }
490 }
491
492 /**
493 * @param array<string, mixed> $options
494 */
495 private static function normalizeDeprecatedNumericOptionValues(array &$options): void
496 {
497 foreach (['connect_timeout', 'delay', 'read_timeout', 'timeout'] as $option) {
498 if (
499 \array_key_exists($option, $options)
500 && \is_string($options[$option])
501 && \is_numeric($options[$option])
502 ) {
503 $options[$option] = $options[$option] + 0;
504 }
505 }
506 }
507
508 /**
509 * @param array<string, mixed> $options
510 */
511 private static function normalizeDeprecatedIntegerOptionValues(array &$options): void
512 {
513 foreach (['crypto_method', 'retries'] as $option) {
514 if (!\array_key_exists($option, $options)) {
515 continue;
516 }
517
518 if (\is_string($options[$option]) && \preg_match('/^-?\d+$/D', $options[$option]) === 1) {
519 $options[$option] = (int) $options[$option];
520 } elseif (
521 \is_float($options[$option])
522 && \is_finite($options[$option])
523 && $options[$option] === (float) (int) $options[$option]
524 ) {
525 $options[$option] = (int) $options[$option];
526 }
527 }
528 }
529
530 private static function warnAboutRequestLevelHandler(array $options): void
531 {
532 if (!\array_key_exists('handler', $options)) {
533 return;
534 }
535
536 \trigger_deprecation(
537 'guzzlehttp/guzzle',
538 '7.12',
539 'Passing the "handler" request option is deprecated; guzzlehttp/guzzle 8.0 will ignore request-level handlers. Configure the handler when creating the Client, or use a separate Client instance for requests that need a different handler.'
540 );
541 }
542
543 private static function warnAboutInvalidRequestOptionTypes(array $options): void
544 {
545 if (isset($options['handler']) && !\is_callable($options['handler'])) {
546 self::warnInvalidRequestOptionType('handler', 'callable', $options['handler']);
547 }
548
549 if (isset($options['allow_redirects']) && \is_array($options['allow_redirects'])) {
550 self::warnAboutInvalidAllowRedirectsOptionTypes($options['allow_redirects']);
551 }
552
553 if (isset($options['auth'])) {
554 self::warnAboutInvalidAuthOptionTypes($options['auth']);
555 }
556
557 if (isset($options['body']) && \is_array($options['body'])) {
558 self::warnInvalidRequestOptionType('body', 'resource|string|null|int|float|bool|StreamInterface|(callable&object)|\Iterator|\Stringable', $options['body']);
559 }
560
561 self::warnAboutInvalidTlsFileOptionTypes($options, 'cert');
562 self::warnIfPresentAndNotString($options, 'cert_type');
563 self::warnIfPresentAndNotNumber($options, 'connect_timeout');
564 self::warnIfPresentAndNotInt($options, 'crypto_method');
565 self::warnIfPresentAndNotBoolOrResource($options, 'debug');
566 self::warnIfPresentAndNotBoolOrString($options, 'decode_content');
567 self::warnIfPresentAndNotNumber($options, 'delay');
568 self::warnIfPresentAndNotBoolOrInt($options, 'expect');
569
570 if (isset($options['form_params'])) {
571 self::warnAboutInvalidFormParamTypes($options['form_params']);
572 }
573
574 if (isset($options['force_ip_resolve']) && !\is_string($options['force_ip_resolve'])) {
575 self::warnInvalidRequestOptionType('force_ip_resolve', 'string', $options['force_ip_resolve']);
576 }
577
578 if (isset($options['headers'])) {
579 self::warnAboutInvalidHeaderOptionTypes($options['headers']);
580 }
581
582 self::warnIfPresentAndNotBool($options, 'http_errors');
583
584 if (isset($options['multipart'])) {
585 self::warnAboutInvalidMultipartOptionTypes($options['multipart']);
586 }
587
588 self::warnIfPresentAndNotCallable($options, 'on_headers');
589 self::warnIfPresentAndNotCallable($options, 'on_stats');
590 self::warnIfPresentAndNotCallable($options, 'progress');
591 self::warnIfPresentAndNotStringArray($options, 'protocols', true);
592 self::warnAboutInvalidProxyOptionTypes($options);
593
594 self::warnIfPresentAndNotNumber($options, 'read_timeout');
595 self::warnIfPresentAndNotInt($options, 'retries');
596
597 if (isset($options['sink']) && !\is_bool($options['sink']) && !\is_resource($options['sink']) && !\is_string($options['sink']) && !$options['sink'] instanceof StreamInterface) {
598 self::warnInvalidRequestOptionType('sink', 'resource|string|StreamInterface', $options['sink']);
599 }
600
601 self::warnAboutInvalidTlsFileOptionTypes($options, 'ssl_key');
602 self::warnIfPresentAndNotString($options, 'ssl_key_type');
603 self::warnIfPresentAndNotBool($options, 'stream');
604 self::warnIfPresentAndNotArray($options, 'stream_context', 'array<array-key, mixed>');
605 self::warnIfPresentAndNotBool($options, 'synchronous');
606 self::warnIfPresentAndNotNumber($options, 'timeout');
607 self::warnIfPresentAndNotBoolOrString($options, 'verify');
608 self::warnIfPresentAndNotStringOrNumber($options, 'version');
609 self::warnIfPresentAndNotArray($options, 'curl', 'array<int|string, mixed>');
610
611 if (isset($options['cookies']) && $options['cookies'] === true) {
612 self::warnInvalidRequestOptionType('cookies', 'false|CookieJarInterface', $options['cookies']);
613 }
614 }
615
616 private static function warnAboutInvalidAllowRedirectsOptionTypes(array $allowRedirects): void
617 {
618 self::warnIfPresentAndNotInt($allowRedirects, 'max', 'allow_redirects.max');
619 self::warnIfPresentAndNotBool($allowRedirects, 'strict', 'allow_redirects.strict');
620 self::warnIfPresentAndNotBool($allowRedirects, 'referer', 'allow_redirects.referer');
621 self::warnIfPresentAndNotStringArray($allowRedirects, 'protocols', true, 'allow_redirects.protocols');
622 self::warnIfPresentAndNotCallable($allowRedirects, 'on_redirect', 'allow_redirects.on_redirect');
623 self::warnIfPresentAndNotBool($allowRedirects, 'track_redirects', 'allow_redirects.track_redirects');
624 }
625
626 /**
627 * @param mixed $auth
628 */
629 private static function warnAboutInvalidAuthOptionTypes($auth): void
630 {
631 if ($auth === false || \is_string($auth) || $auth === []) {
632 return;
633 }
634
635 if (!\is_array($auth)) {
636 self::warnInvalidRequestOptionType('auth', 'array{0: string, 1: string, 2?: string|null}|string|false|null', $auth);
637
638 return;
639 }
640
641 if (!\array_key_exists(0, $auth) || !\is_string($auth[0])) {
642 self::warnInvalidRequestOptionType('auth.0', 'string', $auth[0] ?? null);
643 }
644
645 if (!\array_key_exists(1, $auth) || !\is_string($auth[1])) {
646 self::warnInvalidRequestOptionType('auth.1', 'string', $auth[1] ?? null);
647 }
648
649 if (\array_key_exists(2, $auth) && $auth[2] !== null && !\is_string($auth[2])) {
650 self::warnInvalidRequestOptionType('auth.2', 'string|null', $auth[2]);
651 }
652 }
653
654 /**
655 * @param mixed $value
656 */
657 private static function warnAboutInvalidFormParamTypes($value): void
658 {
659 if (!\is_array($value)) {
660 self::warnInvalidRequestOptionType('form_params', 'array<array-key, string|int|float|bool|null|array>', $value);
661
662 return;
663 }
664
665 self::warnAboutInvalidFormParamArray($value, 'form_params');
666 }
667
668 private static function warnAboutInvalidFormParamArray(array $values, string $path): bool
669 {
670 foreach ($values as $key => $item) {
671 $itemPath = $path.'.'.(string) $key;
672 if (\is_array($item)) {
673 if (!self::warnAboutInvalidFormParamArray($item, $itemPath)) {
674 return false;
675 }
676
677 continue;
678 }
679
680 if ($item !== null && !\is_scalar($item)) {
681 self::warnInvalidRequestOptionType($itemPath, 'string|int|float|bool|null|array', $item);
682
683 return false;
684 }
685 }
686
687 return true;
688 }
689
690 /**
691 * @param mixed $headers
692 */
693 private static function warnAboutInvalidHeaderOptionTypes($headers): void
694 {
695 if (!\is_array($headers)) {
696 self::warnInvalidRequestOptionType('headers', 'array<array-key, string|non-empty-array<array-key, string>>|null', $headers);
697
698 return;
699 }
700
701 foreach ($headers as $name => $value) {
702 $path = 'headers.'.(string) $name;
703 if (\is_array($value)) {
704 if ($value === []) {
705 self::warnInvalidRequestOptionType($path, 'string|non-empty-array<array-key, string>', $value);
706
707 break;
708 }
709
710 foreach ($value as $index => $item) {
711 if (!\is_string($item)) {
712 self::warnInvalidRequestOptionType($path.'.'.(string) $index, 'string', $item);
713
714 break 2;
715 }
716 }
717 } elseif (!\is_string($value)) {
718 self::warnInvalidRequestOptionType($path, 'string|non-empty-array<array-key, string>', $value);
719
720 break;
721 }
722 }
723 }
724
725 /**
726 * @param mixed $multipart
727 */
728 private static function warnAboutInvalidMultipartOptionTypes($multipart): void
729 {
730 if (!\is_array($multipart)) {
731 self::warnInvalidRequestOptionType('multipart', 'array<array-key, array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}>', $multipart);
732
733 return;
734 }
735
736 foreach ($multipart as $index => $part) {
737 $path = 'multipart.'.(string) $index;
738 if (!\is_array($part)) {
739 self::warnInvalidRequestOptionType($path, 'array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}', $part);
740
741 return;
742 }
743
744 if (!\array_key_exists('name', $part) || (!\is_string($part['name']) && !\is_int($part['name']))) {
745 self::warnInvalidRequestOptionType($path.'.name', 'string|int', $part['name'] ?? null);
746 }
747
748 if (!\array_key_exists('contents', $part)) {
749 self::warnInvalidRequestOptionType($path, 'array{name: string|int, contents: mixed, headers?: array<array-key, string>, filename?: string}', $part);
750 }
751
752 if (\array_key_exists('headers', $part)) {
753 if (!\is_array($part['headers'])) {
754 self::warnInvalidRequestOptionType($path.'.headers', 'array<array-key, string>', $part['headers']);
755 } else {
756 foreach ($part['headers'] as $name => $value) {
757 if (!\is_string($value)) {
758 self::warnInvalidRequestOptionType($path.'.headers.'.(string) $name, 'string', $value);
759
760 break 2;
761 }
762 }
763 }
764 }
765
766 if (\array_key_exists('filename', $part) && !\is_string($part['filename'])) {
767 self::warnInvalidRequestOptionType($path.'.filename', 'string', $part['filename']);
768 }
769 }
770 }
771
772 private static function warnAboutInvalidProxyOptionTypes(array $options): void
773 {
774 if (!isset($options['proxy'])) {
775 return;
776 }
777
778 if (!\is_string($options['proxy']) && !\is_array($options['proxy'])) {
779 self::warnInvalidRequestOptionType('proxy', 'string|array{http?: string|null, https?: string|null, no?: string|array<array-key, string>|null}', $options['proxy']);
780
781 return;
782 }
783
784 if (!\is_array($options['proxy'])) {
785 return;
786 }
787
788 foreach (['http', 'https'] as $scheme) {
789 if (\array_key_exists($scheme, $options['proxy']) && $options['proxy'][$scheme] !== null && !\is_string($options['proxy'][$scheme])) {
790 self::warnInvalidRequestOptionType('proxy.'.$scheme, 'string|null', $options['proxy'][$scheme]);
791 }
792 }
793
794 if (!\array_key_exists('no', $options['proxy']) || $options['proxy']['no'] === null) {
795 return;
796 }
797
798 if (\is_string($options['proxy']['no'])) {
799 return;
800 }
801
802 if (!\is_array($options['proxy']['no'])) {
803 self::warnInvalidRequestOptionType('proxy.no', 'string|array<array-key, string>|null', $options['proxy']['no']);
804
805 return;
806 }
807
808 foreach ($options['proxy']['no'] as $index => $noProxy) {
809 if (!\is_string($noProxy)) {
810 self::warnInvalidRequestOptionType('proxy.no.'.(string) $index, 'string', $noProxy);
811
812 return;
813 }
814 }
815 }
816
817 private static function warnAboutInvalidTlsFileOptionTypes(array $options, string $option): void
818 {
819 if (!isset($options[$option])) {
820 return;
821 }
822
823 if (\is_string($options[$option])) {
824 return;
825 }
826
827 if (!\is_array($options[$option])) {
828 self::warnInvalidRequestOptionType($option, 'string|array{0: string, 1?: string}', $options[$option]);
829
830 return;
831 }
832
833 if (!\array_key_exists(0, $options[$option]) || !\is_string($options[$option][0])) {
834 self::warnInvalidRequestOptionType($option.'.0', 'string', $options[$option][0] ?? null);
835 }
836
837 if (\array_key_exists(1, $options[$option]) && $options[$option][1] !== null && !\is_string($options[$option][1])) {
838 self::warnInvalidRequestOptionType($option.'.1', 'string|null', $options[$option][1]);
839 }
840 }
841
842 private static function warnIfPresentAndNotArray(array $options, string $option, string $expected): void
843 {
844 if (\array_key_exists($option, $options) && !\is_array($options[$option])) {
845 self::warnInvalidRequestOptionType($option, $expected, $options[$option]);
846 }
847 }
848
849 private static function warnIfPresentAndNotBool(array $options, string $option, ?string $path = null): void
850 {
851 if (\array_key_exists($option, $options) && !\is_bool($options[$option])) {
852 self::warnInvalidRequestOptionType($path ?? $option, 'bool', $options[$option]);
853 }
854 }
855
856 private static function warnIfPresentAndNotBoolOrInt(array $options, string $option): void
857 {
858 if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_int($options[$option])) {
859 self::warnInvalidRequestOptionType($option, 'bool|int', $options[$option]);
860 }
861 }
862
863 private static function warnIfPresentAndNotBoolOrResource(array $options, string $option): void
864 {
865 if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_resource($options[$option])) {
866 self::warnInvalidRequestOptionType($option, 'bool|resource', $options[$option]);
867 }
868 }
869
870 private static function warnIfPresentAndNotBoolOrString(array $options, string $option): void
871 {
872 if (\array_key_exists($option, $options) && !\is_bool($options[$option]) && !\is_string($options[$option])) {
873 self::warnInvalidRequestOptionType($option, 'bool|string', $options[$option]);
874 }
875 }
876
877 private static function warnIfPresentAndNotCallable(array $options, string $option, ?string $path = null): void
878 {
879 if (\array_key_exists($option, $options) && !\is_callable($options[$option])) {
880 self::warnInvalidRequestOptionType($path ?? $option, 'callable', $options[$option]);
881 }
882 }
883
884 private static function warnIfPresentAndNotInt(array $options, string $option, ?string $path = null): void
885 {
886 if (\array_key_exists($option, $options) && !\is_int($options[$option])) {
887 self::warnInvalidRequestOptionType($path ?? $option, 'int', $options[$option]);
888 }
889 }
890
891 private static function warnIfPresentAndNotNumber(array $options, string $option): void
892 {
893 if (\array_key_exists($option, $options) && !\is_int($options[$option]) && !\is_float($options[$option])) {
894 self::warnInvalidRequestOptionType($option, 'int|float', $options[$option]);
895 }
896 }
897
898 private static function warnIfPresentAndNotString(array $options, string $option): void
899 {
900 if (\array_key_exists($option, $options) && !\is_string($options[$option])) {
901 self::warnInvalidRequestOptionType($option, 'string', $options[$option]);
902 }
903 }
904
905 private static function warnIfPresentAndNotStringArray(array $options, string $option, bool $nonEmpty, ?string $path = null): void
906 {
907 if (!\array_key_exists($option, $options)) {
908 return;
909 }
910
911 $path = $path ?? $option;
912 $expected = ($nonEmpty ? 'non-empty-' : '').'array<array-key, string>';
913
914 if (!\is_array($options[$option]) || ($nonEmpty && $options[$option] === [])) {
915 self::warnInvalidRequestOptionType($path, $expected, $options[$option]);
916
917 return;
918 }
919
920 foreach ($options[$option] as $index => $item) {
921 if (!\is_string($item)) {
922 self::warnInvalidRequestOptionType($path.'.'.(string) $index, 'string', $item);
923
924 return;
925 }
926 }
927 }
928
929 private static function warnIfPresentAndNotStringOrNumber(array $options, string $option): void
930 {
931 if (
932 \array_key_exists($option, $options)
933 && !\is_string($options[$option])
934 && !\is_int($options[$option])
935 && !\is_float($options[$option])
936 ) {
937 self::warnInvalidRequestOptionType($option, 'string|int|float', $options[$option]);
938 }
939 }
940
941 /**
942 * @param mixed $value
943 */
944 private static function warnInvalidRequestOptionType(string $option, string $expected, $value): void
945 {
946 \trigger_deprecation(
947 'guzzlehttp/guzzle',
948 '7.11',
949 'Passing %s to request option "%s" is deprecated; guzzlehttp/guzzle 8.0 requires %s.',
950 \get_debug_type($value),
951 $option,
952 $expected
953 );
954 }
955
956 /**
957 * Transfers the given request and applies request options.
958 *
959 * The URI of the request is not modified and the request options are used
960 * as-is without merging in default options.
961 *
962 * @param array $options See {@see RequestOptions}.
963 */
964 private function transfer(RequestInterface $request, array $options): PromiseInterface
965 {
966 $request = $this->applyOptions($request, $options);
967
968 $protocolVersion = $request->getProtocolVersion();
969
970 if ('' === $protocolVersion) {
971 \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.');
972
973 $request = Psr7\Utils::modifyRequest($request, ['version' => '1.1']);
974 } elseif (!self::isProtocolVersionValid($protocolVersion)) {
975 \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.');
976 }
977
978 /** @var HandlerStack $handler */
979 $handler = $options['handler'];
980
981 try {
982 return P\Create::promiseFor($handler($request, $options));
983 } catch (\Exception $e) {
984 return P\Create::rejectionFor($e);
985 }
986 }
987
988 /**
989 * Applies the array of request options to a request.
990 */
991 private function applyOptions(RequestInterface $request, array &$options): RequestInterface
992 {
993 $modify = [
994 'set_headers' => [],
995 ];
996
997 if (isset($options['headers'])) {
998 if (array_keys($options['headers']) === range(0, count($options['headers']) - 1)) {
999 throw new InvalidArgumentException('The headers array must have header name as keys.');
1000 }
1001 $headers = $options['headers'];
1002 $droppedHeaderNames = self::castDeprecatedHeaderOptionValues($headers);
1003 if ($droppedHeaderNames !== [] && isset($options['_conditional'])) {
1004 $options['_conditional'] = Psr7\Utils::caselessRemove($droppedHeaderNames, $options['_conditional']);
1005 }
1006 $modify['set_headers'] = $headers;
1007 unset($options['headers']);
1008 }
1009
1010 if (isset($options['form_params'])) {
1011 if (isset($options['multipart'])) {
1012 throw new InvalidArgumentException('You cannot use '
1013 .'form_params and multipart at the same time. Use the '
1014 .'form_params option if you want to send application/'
1015 .'x-www-form-urlencoded requests, and the multipart '
1016 .'option to send multipart/form-data requests.');
1017 }
1018 $options['body'] = \http_build_query(self::normalizeNonFiniteFloats($options['form_params'], 'form_params'), '', '&');
1019 unset($options['form_params']);
1020 // Ensure that we don't have the header in different case and set the new value.
1021 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
1022 $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
1023 }
1024
1025 if (isset($options['multipart'])) {
1026 $options['body'] = new Psr7\MultipartStream($options['multipart']);
1027 unset($options['multipart']);
1028 }
1029
1030 if (isset($options['json'])) {
1031 $options['body'] = Utils::jsonEncode($options['json']);
1032 unset($options['json']);
1033 // Ensure that we don't have the header in different case and set the new value.
1034 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
1035 $options['_conditional']['Content-Type'] = 'application/json';
1036 }
1037
1038 if (isset($options['decode_content']) && \is_string($options['decode_content'])) {
1039 // Ensure that we don't have the header in different case and set the new value.
1040 $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']);
1041 $modify['set_headers']['Accept-Encoding'] = (string) $options['decode_content'];
1042 }
1043
1044 if (isset($options['body'])) {
1045 if (\is_array($options['body'])) {
1046 throw $this->invalidBody();
1047 }
1048 $modify['body'] = self::createBodyStream($options['body']);
1049 unset($options['body']);
1050 }
1051
1052 if (!empty($options['auth']) && \is_array($options['auth'])) {
1053 $value = $options['auth'];
1054 $type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
1055 switch ($type) {
1056 case 'basic':
1057 // Ensure that we don't have the header in different case and set the new value.
1058 $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']);
1059 $modify['set_headers']['Authorization'] = 'Basic '
1060 .\base64_encode("$value[0]:$value[1]");
1061 break;
1062 case 'digest':
1063 // @todo: Do not rely on curl
1064 $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST;
1065 $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
1066 break;
1067 case 'ntlm':
1068 \trigger_deprecation(
1069 'guzzlehttp/guzzle',
1070 '7.12',
1071 'Passing "ntlm" as the built-in auth type is deprecated; guzzlehttp/guzzle 8.0 will no longer apply NTLM through the "auth" request option. Configure NTLM with cURL HTTP authentication options instead.'
1072 );
1073 $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
1074 $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
1075 break;
1076 }
1077 }
1078
1079 if (isset($options['query'])) {
1080 $value = $options['query'];
1081 if (\is_array($value)) {
1082 $value = \http_build_query(self::normalizeNonFiniteFloats($value, 'query'), '', '&', \PHP_QUERY_RFC3986);
1083 }
1084 if (!\is_string($value)) {
1085 throw new InvalidArgumentException('query must be a string or array');
1086 }
1087 $modify['query'] = $value;
1088 unset($options['query']);
1089 }
1090
1091 // Ensure that sink is not an invalid value.
1092 if (isset($options['sink'])) {
1093 // TODO: Add more sink validation?
1094 if (\is_bool($options['sink'])) {
1095 throw new InvalidArgumentException('sink must not be a boolean');
1096 }
1097 }
1098
1099 if (isset($options['version'])) {
1100 $modify['version'] = self::normalizeProtocolVersion($options['version']);
1101 }
1102
1103 $request = Psr7\Utils::modifyRequest($request, $modify);
1104 if ($request->getBody() instanceof Psr7\MultipartStream) {
1105 // Use a multipart/form-data POST if a Content-Type is not set.
1106 // Ensure that we don't have the header in different case and set the new value.
1107 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
1108 $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary='
1109 .$request->getBody()->getBoundary();
1110 }
1111
1112 // Merge in conditional headers if they are not present.
1113 if (isset($options['_conditional'])) {
1114 // Build up the changes so it's in a single clone of the message.
1115 $modify = [];
1116 foreach ($options['_conditional'] as $k => $v) {
1117 $name = (string) $k;
1118 if (!$request->hasHeader($name)) {
1119 $modify['set_headers'][$name] = $v;
1120 }
1121 }
1122 $request = Psr7\Utils::modifyRequest($request, $modify);
1123 // Don't pass this internal value along to middleware/handlers.
1124 unset($options['_conditional']);
1125 }
1126
1127 return $request;
1128 }
1129
1130 /**
1131 * @param array<array-key, mixed> $headers
1132 *
1133 * @return list<string>
1134 */
1135 private static function castDeprecatedHeaderOptionValues(array &$headers): array
1136 {
1137 $droppedHeaderNames = [];
1138
1139 foreach ($headers as $name => $value) {
1140 if (\is_array($value)) {
1141 if ($value === []) {
1142 $droppedHeaderNames[] = (string) $name;
1143 unset($headers[$name]);
1144
1145 continue;
1146 }
1147
1148 foreach ($value as $index => $item) {
1149 if ($item === null || (!\is_string($item) && \is_scalar($item))) {
1150 if (\is_float($item) && !\is_finite($item)) {
1151 $item = \is_nan($item) ? 'NAN' : ($item > 0 ? 'INF' : '-INF');
1152 }
1153 $value[$index] = (string) $item;
1154 }
1155 }
1156
1157 $headers[$name] = $value;
1158
1159 continue;
1160 }
1161
1162 if ($value === null || (!\is_string($value) && \is_scalar($value))) {
1163 if (\is_float($value) && !\is_finite($value)) {
1164 $value = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
1165 }
1166 $headers[$name] = (string) $value;
1167 }
1168 }
1169
1170 return $droppedHeaderNames;
1171 }
1172
1173 /**
1174 * @param mixed $body
1175 */
1176 private static function createBodyStream($body): StreamInterface
1177 {
1178 if ($body instanceof StreamInterface) {
1179 return $body;
1180 }
1181
1182 if (\is_resource($body) || $body === null || \is_string($body) || $body instanceof \Iterator) {
1183 return Psr7\Utils::streamFor($body);
1184 }
1185
1186 if (\is_scalar($body)) {
1187 \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.');
1188
1189 return Psr7\Utils::streamFor(self::stringifyScalar($body));
1190 }
1191
1192 if (\is_object($body) && \method_exists($body, '__toString')) {
1193 return Psr7\Utils::streamFor((string) $body);
1194 }
1195
1196 if (\is_callable($body)) {
1197 return Psr7\Utils::streamFor($body);
1198 }
1199
1200 throw new InvalidArgumentException(\sprintf(
1201 'Passing %s to request option "body" is invalid; expected resource|string|null|int|float|bool|StreamInterface|callable&object|Iterator|Stringable.',
1202 \get_debug_type($body)
1203 ));
1204 }
1205
1206 /**
1207 * @param bool|float|int|string $value
1208 */
1209 private static function stringifyScalar($value): string
1210 {
1211 // Normalize non-finite floats to dodge PHP 8.5's (string) NAN
1212 // coercion warning while the value is still accepted.
1213 if (\is_float($value) && !\is_finite($value)) {
1214 $value = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
1215 }
1216
1217 return (string) $value;
1218 }
1219
1220 /**
1221 * Converts non-finite floats in the array to the strings PHP coerces
1222 * them to, as implicit coercion of NAN emits a warning on PHP 8.5.
1223 */
1224 private static function normalizeNonFiniteFloats(array $values, string $option): array
1225 {
1226 foreach ($values as $key => $value) {
1227 if (\is_array($value)) {
1228 $values[$key] = self::normalizeNonFiniteFloats($value, $option);
1229 } elseif (\is_float($value) && !\is_finite($value)) {
1230 \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);
1231
1232 $values[$key] = \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
1233 }
1234 }
1235
1236 return $values;
1237 }
1238
1239 /**
1240 * @param string|int|float $version
1241 */
1242 private static function normalizeProtocolVersion($version): string
1243 {
1244 if ('' === $version) {
1245 \trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing an empty "version" request option is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.');
1246
1247 return '1.1';
1248 }
1249
1250 return \is_float($version) ? \number_format($version, 1, '.', '') : (string) $version;
1251 }
1252
1253 private static function isProtocolVersionValid(string $version): bool
1254 {
1255 return 1 === \preg_match('/^\d+(?:\.\d+)?$/D', $version);
1256 }
1257
1258 /**
1259 * Return an InvalidArgumentException with pre-set message.
1260 */
1261 private function invalidBody(): InvalidArgumentException
1262 {
1263 return new InvalidArgumentException('Passing in the "body" request '
1264 .'option as an array to send a request is not supported. '
1265 .'Please use the "form_params" request option to send a '
1266 .'application/x-www-form-urlencoded request, or the "multipart" '
1267 .'request option to send a multipart/form-data request.');
1268 }
1269 }
1270