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