1<?php 2 3namespace GuzzleHttp; 4 5use GuzzleHttp\Cookie\CookieJar; 6use GuzzleHttp\Exception\GuzzleException; 7use GuzzleHttp\Exception\InvalidArgumentException; 8use GuzzleHttp\Promise as P; 9use GuzzleHttp\Promise\PromiseInterface; 10use Psr\Http\Message\RequestInterface; 11use Psr\Http\Message\ResponseInterface; 12use Psr\Http\Message\UriInterface; 13 14/** 15 * @final 16 */ 17class Client implements ClientInterface, \Psr\Http\Client\ClientInterface 18{ 19 use ClientTrait; 20 21 /** 22 * @var array Default request options 23 */ 24 private $config; 25 26 /** 27 * Clients accept an array of constructor parameters. 28 * 29 * Here's an example of creating a client using a base_uri and an array of 30 * default request options to apply to each request: 31 * 32 * $client = new Client([ 33 * 'base_uri' => 'http://www.foo.com/1.0/', 34 * 'timeout' => 0, 35 * 'allow_redirects' => false, 36 * 'proxy' => '192.168.16.1:10' 37 * ]); 38 * 39 * Client configuration settings include the following options: 40 * 41 * - handler: (callable) Function that transfers HTTP requests over the 42 * wire. The function is called with a Psr7\Http\Message\RequestInterface 43 * and array of transfer options, and must return a 44 * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a 45 * Psr7\Http\Message\ResponseInterface on success. 46 * If no handler is provided, a default handler will be created 47 * that enables all of the request options below by attaching all of the 48 * default middleware to the handler. 49 * - base_uri: (string|UriInterface) Base URI of the client that is merged 50 * into relative URIs. Can be a string or instance of UriInterface. 51 * - **: any request option 52 * 53 * @param array $config Client configuration settings. 54 * 55 * @see \GuzzleHttp\RequestOptions for a list of available request options. 56 */ 57 public function __construct(array $config = []) 58 { 59 if (!isset($config['handler'])) { 60 $config['handler'] = HandlerStack::create(); 61 } elseif (!\is_callable($config['handler'])) { 62 throw new InvalidArgumentException('handler must be a callable'); 63 } 64 65 // Convert the base_uri to a UriInterface 66 if (isset($config['base_uri'])) { 67 $config['base_uri'] = Psr7\Utils::uriFor($config['base_uri']); 68 } 69 70 $this->configureDefaults($config); 71 } 72 73 /** 74 * @param string $method 75 * @param array $args 76 * 77 * @return PromiseInterface|ResponseInterface 78 * 79 * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0. 80 */ 81 public function __call($method, $args) 82 { 83 if (\count($args) < 1) { 84 throw new InvalidArgumentException('Magic request methods require a URI and optional options array'); 85 } 86 87 $uri = $args[0]; 88 $opts = $args[1] ?? []; 89 90 return \substr($method, -5) === 'Async' 91 ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts) 92 : $this->request($method, $uri, $opts); 93 } 94 95 /** 96 * Asynchronously send an HTTP request. 97 * 98 * @param array $options Request options to apply to the given 99 * request and to the transfer. See \GuzzleHttp\RequestOptions. 100 */ 101 public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface 102 { 103 // Merge the base URI into the request URI if needed. 104 $options = $this->prepareDefaults($options); 105 106 return $this->transfer( 107 $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), 108 $options 109 ); 110 } 111 112 /** 113 * Send an HTTP request. 114 * 115 * @param array $options Request options to apply to the given 116 * request and to the transfer. See \GuzzleHttp\RequestOptions. 117 * 118 * @throws GuzzleException 119 */ 120 public function send(RequestInterface $request, array $options = []): ResponseInterface 121 { 122 $options[RequestOptions::SYNCHRONOUS] = true; 123 124 return $this->sendAsync($request, $options)->wait(); 125 } 126 127 /** 128 * The HttpClient PSR (PSR-18) specify this method. 129 * 130 * {@inheritDoc} 131 */ 132 public function sendRequest(RequestInterface $request): ResponseInterface 133 { 134 $options[RequestOptions::SYNCHRONOUS] = true; 135 $options[RequestOptions::ALLOW_REDIRECTS] = false; 136 $options[RequestOptions::HTTP_ERRORS] = false; 137 138 return $this->sendAsync($request, $options)->wait(); 139 } 140 141 /** 142 * Create and send an asynchronous HTTP request. 143 * 144 * Use an absolute path to override the base path of the client, or a 145 * relative path to append to the base path of the client. The URL can 146 * contain the query string as well. Use an array to provide a URL 147 * template and additional variables to use in the URL template expansion. 148 * 149 * @param string $method HTTP method 150 * @param string|UriInterface $uri URI object or string. 151 * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. 152 */ 153 public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface 154 { 155 $options = $this->prepareDefaults($options); 156 // Remove request modifying parameter because it can be done up-front. 157 $headers = $options['headers'] ?? []; 158 $body = $options['body'] ?? null; 159 $version = $options['version'] ?? '1.1'; 160 // Merge the URI into the base URI. 161 $uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options); 162 if (\is_array($body)) { 163 throw $this->invalidBody(); 164 } 165 $request = new Psr7\Request($method, $uri, $headers, $body, $version); 166 // Remove the option so that they are not doubly-applied. 167 unset($options['headers'], $options['body'], $options['version']); 168 169 return $this->transfer($request, $options); 170 } 171 172 /** 173 * Create and send an HTTP request. 174 * 175 * Use an absolute path to override the base path of the client, or a 176 * relative path to append to the base path of the client. The URL can 177 * contain the query string as well. 178 * 179 * @param string $method HTTP method. 180 * @param string|UriInterface $uri URI object or string. 181 * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. 182 * 183 * @throws GuzzleException 184 */ 185 public function request(string $method, $uri = '', array $options = []): ResponseInterface 186 { 187 $options[RequestOptions::SYNCHRONOUS] = true; 188 189 return $this->requestAsync($method, $uri, $options)->wait(); 190 } 191 192 /** 193 * Get a client configuration option. 194 * 195 * These options include default request options of the client, a "handler" 196 * (if utilized by the concrete client), and a "base_uri" if utilized by 197 * the concrete client. 198 * 199 * @param string|null $option The config option to retrieve. 200 * 201 * @return mixed 202 * 203 * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0. 204 */ 205 public function getConfig(string $option = null) 206 { 207 return $option === null 208 ? $this->config 209 : ($this->config[$option] ?? null); 210 } 211 212 private function buildUri(UriInterface $uri, array $config): UriInterface 213 { 214 if (isset($config['base_uri'])) { 215 $uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri); 216 } 217 218 if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) { 219 $idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion']; 220 $uri = Utils::idnUriConvert($uri, $idnOptions); 221 } 222 223 return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; 224 } 225 226 /** 227 * Configures the default options for a client. 228 */ 229 private function configureDefaults(array $config): void 230 { 231 $defaults = [ 232 'allow_redirects' => RedirectMiddleware::$defaultSettings, 233 'http_errors' => true, 234 'decode_content' => true, 235 'verify' => true, 236 'cookies' => false, 237 'idn_conversion' => false, 238 ]; 239 240 // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. 241 242 // We can only trust the HTTP_PROXY environment variable in a CLI 243 // process due to the fact that PHP has no reliable mechanism to 244 // get environment variables that start with "HTTP_". 245 if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) { 246 $defaults['proxy']['http'] = $proxy; 247 } 248 249 if ($proxy = Utils::getenv('HTTPS_PROXY')) { 250 $defaults['proxy']['https'] = $proxy; 251 } 252 253 if ($noProxy = Utils::getenv('NO_PROXY')) { 254 $cleanedNoProxy = \str_replace(' ', '', $noProxy); 255 $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy); 256 } 257 258 $this->config = $config + $defaults; 259 260 if (!empty($config['cookies']) && $config['cookies'] === true) { 261 $this->config['cookies'] = new CookieJar(); 262 } 263 264 // Add the default user-agent header. 265 if (!isset($this->config['headers'])) { 266 $this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()]; 267 } else { 268 // Add the User-Agent header if one was not already set. 269 foreach (\array_keys($this->config['headers']) as $name) { 270 if (\strtolower($name) === 'user-agent') { 271 return; 272 } 273 } 274 $this->config['headers']['User-Agent'] = Utils::defaultUserAgent(); 275 } 276 } 277 278 /** 279 * Merges default options into the array. 280 * 281 * @param array $options Options to modify by reference 282 */ 283 private function prepareDefaults(array $options): array 284 { 285 $defaults = $this->config; 286 287 if (!empty($defaults['headers'])) { 288 // Default headers are only added if they are not present. 289 $defaults['_conditional'] = $defaults['headers']; 290 unset($defaults['headers']); 291 } 292 293 // Special handling for headers is required as they are added as 294 // conditional headers and as headers passed to a request ctor. 295 if (\array_key_exists('headers', $options)) { 296 // Allows default headers to be unset. 297 if ($options['headers'] === null) { 298 $defaults['_conditional'] = []; 299 unset($options['headers']); 300 } elseif (!\is_array($options['headers'])) { 301 throw new InvalidArgumentException('headers must be an array'); 302 } 303 } 304 305 // Shallow merge defaults underneath options. 306 $result = $options + $defaults; 307 308 // Remove null values. 309 foreach ($result as $k => $v) { 310 if ($v === null) { 311 unset($result[$k]); 312 } 313 } 314 315 return $result; 316 } 317 318 /** 319 * Transfers the given request and applies request options. 320 * 321 * The URI of the request is not modified and the request options are used 322 * as-is without merging in default options. 323 * 324 * @param array $options See \GuzzleHttp\RequestOptions. 325 */ 326 private function transfer(RequestInterface $request, array $options): PromiseInterface 327 { 328 $request = $this->applyOptions($request, $options); 329 /** @var HandlerStack $handler */ 330 $handler = $options['handler']; 331 332 try { 333 return P\Create::promiseFor($handler($request, $options)); 334 } catch (\Exception $e) { 335 return P\Create::rejectionFor($e); 336 } 337 } 338 339 /** 340 * Applies the array of request options to a request. 341 */ 342 private function applyOptions(RequestInterface $request, array &$options): RequestInterface 343 { 344 $modify = [ 345 'set_headers' => [], 346 ]; 347 348 if (isset($options['headers'])) { 349 if (array_keys($options['headers']) === range(0, count($options['headers']) - 1)) { 350 throw new InvalidArgumentException('The headers array must have header name as keys.'); 351 } 352 $modify['set_headers'] = $options['headers']; 353 unset($options['headers']); 354 } 355 356 if (isset($options['form_params'])) { 357 if (isset($options['multipart'])) { 358 throw new InvalidArgumentException('You cannot use ' 359 .'form_params and multipart at the same time. Use the ' 360 .'form_params option if you want to send application/' 361 .'x-www-form-urlencoded requests, and the multipart ' 362 .'option to send multipart/form-data requests.'); 363 } 364 $options['body'] = \http_build_query($options['form_params'], '', '&'); 365 unset($options['form_params']); 366 // Ensure that we don't have the header in different case and set the new value. 367 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); 368 $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; 369 } 370 371 if (isset($options['multipart'])) { 372 $options['body'] = new Psr7\MultipartStream($options['multipart']); 373 unset($options['multipart']); 374 } 375 376 if (isset($options['json'])) { 377 $options['body'] = Utils::jsonEncode($options['json']); 378 unset($options['json']); 379 // Ensure that we don't have the header in different case and set the new value. 380 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); 381 $options['_conditional']['Content-Type'] = 'application/json'; 382 } 383 384 if (!empty($options['decode_content']) 385 && $options['decode_content'] !== true 386 ) { 387 // Ensure that we don't have the header in different case and set the new value. 388 $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']); 389 $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; 390 } 391 392 if (isset($options['body'])) { 393 if (\is_array($options['body'])) { 394 throw $this->invalidBody(); 395 } 396 $modify['body'] = Psr7\Utils::streamFor($options['body']); 397 unset($options['body']); 398 } 399 400 if (!empty($options['auth']) && \is_array($options['auth'])) { 401 $value = $options['auth']; 402 $type = isset($value[2]) ? \strtolower($value[2]) : 'basic'; 403 switch ($type) { 404 case 'basic': 405 // Ensure that we don't have the header in different case and set the new value. 406 $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']); 407 $modify['set_headers']['Authorization'] = 'Basic ' 408 .\base64_encode("$value[0]:$value[1]"); 409 break; 410 case 'digest': 411 // @todo: Do not rely on curl 412 $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST; 413 $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; 414 break; 415 case 'ntlm': 416 $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM; 417 $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; 418 break; 419 } 420 } 421 422 if (isset($options['query'])) { 423 $value = $options['query']; 424 if (\is_array($value)) { 425 $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986); 426 } 427 if (!\is_string($value)) { 428 throw new InvalidArgumentException('query must be a string or array'); 429 } 430 $modify['query'] = $value; 431 unset($options['query']); 432 } 433 434 // Ensure that sink is not an invalid value. 435 if (isset($options['sink'])) { 436 // TODO: Add more sink validation? 437 if (\is_bool($options['sink'])) { 438 throw new InvalidArgumentException('sink must not be a boolean'); 439 } 440 } 441 442 if (isset($options['version'])) { 443 $modify['version'] = $options['version']; 444 } 445 446 $request = Psr7\Utils::modifyRequest($request, $modify); 447 if ($request->getBody() instanceof Psr7\MultipartStream) { 448 // Use a multipart/form-data POST if a Content-Type is not set. 449 // Ensure that we don't have the header in different case and set the new value. 450 $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); 451 $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' 452 .$request->getBody()->getBoundary(); 453 } 454 455 // Merge in conditional headers if they are not present. 456 if (isset($options['_conditional'])) { 457 // Build up the changes so it's in a single clone of the message. 458 $modify = []; 459 foreach ($options['_conditional'] as $k => $v) { 460 if (!$request->hasHeader($k)) { 461 $modify['set_headers'][$k] = $v; 462 } 463 } 464 $request = Psr7\Utils::modifyRequest($request, $modify); 465 // Don't pass this internal value along to middleware/handlers. 466 unset($options['_conditional']); 467 } 468 469 return $request; 470 } 471 472 /** 473 * Return an InvalidArgumentException with pre-set message. 474 */ 475 private function invalidBody(): InvalidArgumentException 476 { 477 return new InvalidArgumentException('Passing in the "body" request ' 478 .'option as an array to send a request is not supported. ' 479 .'Please use the "form_params" request option to send a ' 480 .'application/x-www-form-urlencoded request, or the "multipart" ' 481 .'request option to send a multipart/form-data request.'); 482 } 483} 484