1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * Redistributions of files must retain the above copyright notice.
8: *
9: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
10: * @link https://cakephp.org CakePHP(tm) Project
11: * @since 3.0.0
12: * @license https://opensource.org/licenses/mit-license.php MIT License
13: */
14: namespace Cake\Http;
15:
16: use Cake\Core\App;
17: use Cake\Core\Exception\Exception;
18: use Cake\Core\InstanceConfigTrait;
19: use Cake\Http\Client\AdapterInterface;
20: use Cake\Http\Client\Adapter\Curl;
21: use Cake\Http\Client\Adapter\Stream;
22: use Cake\Http\Client\Request;
23: use Cake\Http\Cookie\CookieCollection;
24: use Cake\Http\Cookie\CookieInterface;
25: use Cake\Utility\Hash;
26: use InvalidArgumentException;
27: use Zend\Diactoros\Uri;
28:
29: /**
30: * The end user interface for doing HTTP requests.
31: *
32: * ### Scoped clients
33: *
34: * If you're doing multiple requests to the same hostname it's often convenient
35: * to use the constructor arguments to create a scoped client. This allows you
36: * to keep your code DRY and not repeat hostnames, authentication, and other options.
37: *
38: * ### Doing requests
39: *
40: * Once you've created an instance of Client you can do requests
41: * using several methods. Each corresponds to a different HTTP method.
42: *
43: * - get()
44: * - post()
45: * - put()
46: * - delete()
47: * - patch()
48: *
49: * ### Cookie management
50: *
51: * Client will maintain cookies from the responses done with
52: * a client instance. These cookies will be automatically added
53: * to future requests to matching hosts. Cookies will respect the
54: * `Expires`, `Path` and `Domain` attributes. You can get the client's
55: * CookieCollection using cookies()
56: *
57: * You can use the 'cookieJar' constructor option to provide a custom
58: * cookie jar instance you've restored from cache/disk. By default
59: * an empty instance of Cake\Http\Client\CookieCollection will be created.
60: *
61: * ### Sending request bodies
62: *
63: * By default any POST/PUT/PATCH/DELETE request with $data will
64: * send their data as `application/x-www-form-urlencoded` unless
65: * there are attached files. In that case `multipart/form-data`
66: * will be used.
67: *
68: * When sending request bodies you can use the `type` option to
69: * set the Content-Type for the request:
70: *
71: * ```
72: * $http->get('/users', [], ['type' => 'json']);
73: * ```
74: *
75: * The `type` option sets both the `Content-Type` and `Accept` header, to
76: * the same mime type. When using `type` you can use either a full mime
77: * type or an alias. If you need different types in the Accept and Content-Type
78: * headers you should set them manually and not use `type`
79: *
80: * ### Using authentication
81: *
82: * By using the `auth` key you can use authentication. The type sub option
83: * can be used to specify which authentication strategy you want to use.
84: * CakePHP comes with a few built-in strategies:
85: *
86: * - Basic
87: * - Digest
88: * - Oauth
89: *
90: * ### Using proxies
91: *
92: * By using the `proxy` key you can set authentication credentials for
93: * a proxy if you need to use one. The type sub option can be used to
94: * specify which authentication strategy you want to use.
95: * CakePHP comes with built-in support for basic authentication.
96: */
97: class Client
98: {
99: use InstanceConfigTrait;
100:
101: /**
102: * Default configuration for the client.
103: *
104: * @var array
105: */
106: protected $_defaultConfig = [
107: 'adapter' => null,
108: 'host' => null,
109: 'port' => null,
110: 'scheme' => 'http',
111: 'timeout' => 30,
112: 'ssl_verify_peer' => true,
113: 'ssl_verify_peer_name' => true,
114: 'ssl_verify_depth' => 5,
115: 'ssl_verify_host' => true,
116: 'redirect' => false,
117: ];
118:
119: /**
120: * List of cookies from responses made with this client.
121: *
122: * Cookies are indexed by the cookie's domain or
123: * request host name.
124: *
125: * @var \Cake\Http\Cookie\CookieCollection
126: */
127: protected $_cookies;
128:
129: /**
130: * Adapter for sending requests.
131: *
132: * @var \Cake\Http\Client\AdapterInterface
133: */
134: protected $_adapter;
135:
136: /**
137: * Create a new HTTP Client.
138: *
139: * ### Config options
140: *
141: * You can set the following options when creating a client:
142: *
143: * - host - The hostname to do requests on.
144: * - port - The port to use.
145: * - scheme - The default scheme/protocol to use. Defaults to http.
146: * - timeout - The timeout in seconds. Defaults to 30
147: * - ssl_verify_peer - Whether or not SSL certificates should be validated.
148: * Defaults to true.
149: * - ssl_verify_peer_name - Whether or not peer names should be validated.
150: * Defaults to true.
151: * - ssl_verify_depth - The maximum certificate chain depth to traverse.
152: * Defaults to 5.
153: * - ssl_verify_host - Verify that the certificate and hostname match.
154: * Defaults to true.
155: * - redirect - Number of redirects to follow. Defaults to false.
156: * - adapter - The adapter class name or instance. Defaults to
157: * \Cake\Http\Client\Adapter\Curl if `curl` extension is loaded else
158: * \Cake\Http\Client\Adapter\Stream.
159: *
160: * @param array $config Config options for scoped clients.
161: * @throws \InvalidArgumentException
162: */
163: public function __construct($config = [])
164: {
165: $this->setConfig($config);
166:
167: $adapter = $this->_config['adapter'];
168: if ($adapter === null) {
169: $adapter = Curl::class;
170:
171: if (!extension_loaded('curl')) {
172: $adapter = Stream::class;
173: }
174: } else {
175: $this->setConfig('adapter', null);
176: }
177:
178: if (is_string($adapter)) {
179: $adapter = new $adapter();
180: }
181:
182: if (!$adapter instanceof AdapterInterface) {
183: throw new InvalidArgumentException('Adapter must be an instance of Cake\Http\Client\AdapterInterface');
184: }
185: $this->_adapter = $adapter;
186:
187: if (!empty($this->_config['cookieJar'])) {
188: $this->_cookies = $this->_config['cookieJar'];
189: $this->setConfig('cookieJar', null);
190: } else {
191: $this->_cookies = new CookieCollection();
192: }
193: }
194:
195: /**
196: * Get the cookies stored in the Client.
197: *
198: * @return \Cake\Http\Cookie\CookieCollection
199: */
200: public function cookies()
201: {
202: return $this->_cookies;
203: }
204:
205: /**
206: * Adds a cookie to the Client collection.
207: *
208: * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object.
209: * @return $this
210: * @throws \InvalidArgumentException
211: */
212: public function addCookie(CookieInterface $cookie)
213: {
214: if (!$cookie->getDomain() || !$cookie->getPath()) {
215: throw new InvalidArgumentException('Cookie must have a domain and a path set.');
216: }
217: $this->_cookies = $this->_cookies->add($cookie);
218:
219: return $this;
220: }
221:
222: /**
223: * Do a GET request.
224: *
225: * The $data argument supports a special `_content` key
226: * for providing a request body in a GET request. This is
227: * generally not used, but services like ElasticSearch use
228: * this feature.
229: *
230: * @param string $url The url or path you want to request.
231: * @param array $data The query data you want to send.
232: * @param array $options Additional options for the request.
233: * @return \Cake\Http\Client\Response
234: */
235: public function get($url, $data = [], array $options = [])
236: {
237: $options = $this->_mergeOptions($options);
238: $body = null;
239: if (isset($data['_content'])) {
240: $body = $data['_content'];
241: unset($data['_content']);
242: }
243: $url = $this->buildUrl($url, $data, $options);
244:
245: return $this->_doRequest(
246: Request::METHOD_GET,
247: $url,
248: $body,
249: $options
250: );
251: }
252:
253: /**
254: * Do a POST request.
255: *
256: * @param string $url The url or path you want to request.
257: * @param mixed $data The post data you want to send.
258: * @param array $options Additional options for the request.
259: * @return \Cake\Http\Client\Response
260: */
261: public function post($url, $data = [], array $options = [])
262: {
263: $options = $this->_mergeOptions($options);
264: $url = $this->buildUrl($url, [], $options);
265:
266: return $this->_doRequest(Request::METHOD_POST, $url, $data, $options);
267: }
268:
269: /**
270: * Do a PUT request.
271: *
272: * @param string $url The url or path you want to request.
273: * @param mixed $data The request data you want to send.
274: * @param array $options Additional options for the request.
275: * @return \Cake\Http\Client\Response
276: */
277: public function put($url, $data = [], array $options = [])
278: {
279: $options = $this->_mergeOptions($options);
280: $url = $this->buildUrl($url, [], $options);
281:
282: return $this->_doRequest(Request::METHOD_PUT, $url, $data, $options);
283: }
284:
285: /**
286: * Do a PATCH request.
287: *
288: * @param string $url The url or path you want to request.
289: * @param mixed $data The request data you want to send.
290: * @param array $options Additional options for the request.
291: * @return \Cake\Http\Client\Response
292: */
293: public function patch($url, $data = [], array $options = [])
294: {
295: $options = $this->_mergeOptions($options);
296: $url = $this->buildUrl($url, [], $options);
297:
298: return $this->_doRequest(Request::METHOD_PATCH, $url, $data, $options);
299: }
300:
301: /**
302: * Do an OPTIONS request.
303: *
304: * @param string $url The url or path you want to request.
305: * @param mixed $data The request data you want to send.
306: * @param array $options Additional options for the request.
307: * @return \Cake\Http\Client\Response
308: */
309: public function options($url, $data = [], array $options = [])
310: {
311: $options = $this->_mergeOptions($options);
312: $url = $this->buildUrl($url, [], $options);
313:
314: return $this->_doRequest(Request::METHOD_OPTIONS, $url, $data, $options);
315: }
316:
317: /**
318: * Do a TRACE request.
319: *
320: * @param string $url The url or path you want to request.
321: * @param mixed $data The request data you want to send.
322: * @param array $options Additional options for the request.
323: * @return \Cake\Http\Client\Response
324: */
325: public function trace($url, $data = [], array $options = [])
326: {
327: $options = $this->_mergeOptions($options);
328: $url = $this->buildUrl($url, [], $options);
329:
330: return $this->_doRequest(Request::METHOD_TRACE, $url, $data, $options);
331: }
332:
333: /**
334: * Do a DELETE request.
335: *
336: * @param string $url The url or path you want to request.
337: * @param mixed $data The request data you want to send.
338: * @param array $options Additional options for the request.
339: * @return \Cake\Http\Client\Response
340: */
341: public function delete($url, $data = [], array $options = [])
342: {
343: $options = $this->_mergeOptions($options);
344: $url = $this->buildUrl($url, [], $options);
345:
346: return $this->_doRequest(Request::METHOD_DELETE, $url, $data, $options);
347: }
348:
349: /**
350: * Do a HEAD request.
351: *
352: * @param string $url The url or path you want to request.
353: * @param array $data The query string data you want to send.
354: * @param array $options Additional options for the request.
355: * @return \Cake\Http\Client\Response
356: */
357: public function head($url, array $data = [], array $options = [])
358: {
359: $options = $this->_mergeOptions($options);
360: $url = $this->buildUrl($url, $data, $options);
361:
362: return $this->_doRequest(Request::METHOD_HEAD, $url, '', $options);
363: }
364:
365: /**
366: * Helper method for doing non-GET requests.
367: *
368: * @param string $method HTTP method.
369: * @param string $url URL to request.
370: * @param mixed $data The request body.
371: * @param array $options The options to use. Contains auth, proxy, etc.
372: * @return \Cake\Http\Client\Response
373: */
374: protected function _doRequest($method, $url, $data, $options)
375: {
376: $request = $this->_createRequest(
377: $method,
378: $url,
379: $data,
380: $options
381: );
382:
383: return $this->send($request, $options);
384: }
385:
386: /**
387: * Does a recursive merge of the parameter with the scope config.
388: *
389: * @param array $options Options to merge.
390: * @return array Options merged with set config.
391: */
392: protected function _mergeOptions($options)
393: {
394: return Hash::merge($this->_config, $options);
395: }
396:
397: /**
398: * Send a request.
399: *
400: * Used internally by other methods, but can also be used to send
401: * handcrafted Request objects.
402: *
403: * @param \Cake\Http\Client\Request $request The request to send.
404: * @param array $options Additional options to use.
405: * @return \Cake\Http\Client\Response
406: */
407: public function send(Request $request, $options = [])
408: {
409: $redirects = 0;
410: if (isset($options['redirect'])) {
411: $redirects = (int)$options['redirect'];
412: unset($options['redirect']);
413: }
414:
415: do {
416: $response = $this->_sendRequest($request, $options);
417:
418: $handleRedirect = $response->isRedirect() && $redirects-- > 0;
419: if ($handleRedirect) {
420: $url = $request->getUri();
421:
422: $location = $response->getHeaderLine('Location');
423: $locationUrl = $this->buildUrl($location, [], [
424: 'host' => $url->getHost(),
425: 'port' => $url->getPort(),
426: 'scheme' => $url->getScheme(),
427: 'protocolRelative' => true
428: ]);
429: $request = $request->withUri(new Uri($locationUrl));
430: $request = $this->_cookies->addToRequest($request, []);
431: }
432: } while ($handleRedirect);
433:
434: return $response;
435: }
436:
437: /**
438: * Send a request without redirection.
439: *
440: * @param \Cake\Http\Client\Request $request The request to send.
441: * @param array $options Additional options to use.
442: * @return \Cake\Http\Client\Response
443: */
444: protected function _sendRequest(Request $request, $options)
445: {
446: $responses = $this->_adapter->send($request, $options);
447: $url = $request->getUri();
448: foreach ($responses as $response) {
449: $this->_cookies = $this->_cookies->addFromResponse($response, $request);
450: }
451:
452: return array_pop($responses);
453: }
454:
455: /**
456: * Generate a URL based on the scoped client options.
457: *
458: * @param string $url Either a full URL or just the path.
459: * @param string|array $query The query data for the URL.
460: * @param array $options The config options stored with Client::config()
461: * @return string A complete url with scheme, port, host, and path.
462: */
463: public function buildUrl($url, $query = [], $options = [])
464: {
465: if (empty($options) && empty($query)) {
466: return $url;
467: }
468: if ($query) {
469: $q = (strpos($url, '?') === false) ? '?' : '&';
470: $url .= $q;
471: $url .= is_string($query) ? $query : http_build_query($query);
472: }
473: $defaults = [
474: 'host' => null,
475: 'port' => null,
476: 'scheme' => 'http',
477: 'protocolRelative' => false
478: ];
479: $options += $defaults;
480:
481: if ($options['protocolRelative'] && preg_match('#^//#', $url)) {
482: $url = $options['scheme'] . ':' . $url;
483: }
484: if (preg_match('#^https?://#', $url)) {
485: return $url;
486: }
487:
488: $defaultPorts = [
489: 'http' => 80,
490: 'https' => 443
491: ];
492: $out = $options['scheme'] . '://' . $options['host'];
493: if ($options['port'] && $options['port'] != $defaultPorts[$options['scheme']]) {
494: $out .= ':' . $options['port'];
495: }
496: $out .= '/' . ltrim($url, '/');
497:
498: return $out;
499: }
500:
501: /**
502: * Creates a new request object based on the parameters.
503: *
504: * @param string $method HTTP method name.
505: * @param string $url The url including query string.
506: * @param mixed $data The request body.
507: * @param array $options The options to use. Contains auth, proxy, etc.
508: * @return \Cake\Http\Client\Request
509: */
510: protected function _createRequest($method, $url, $data, $options)
511: {
512: $headers = isset($options['headers']) ? (array)$options['headers'] : [];
513: if (isset($options['type'])) {
514: $headers = array_merge($headers, $this->_typeHeaders($options['type']));
515: }
516: if (is_string($data) && !isset($headers['Content-Type']) && !isset($headers['content-type'])) {
517: $headers['Content-Type'] = 'application/x-www-form-urlencoded';
518: }
519:
520: $request = new Request($url, $method, $headers, $data);
521: $cookies = isset($options['cookies']) ? $options['cookies'] : [];
522: /** @var \Cake\Http\Client\Request $request */
523: $request = $this->_cookies->addToRequest($request, $cookies);
524: if (isset($options['auth'])) {
525: $request = $this->_addAuthentication($request, $options);
526: }
527: if (isset($options['proxy'])) {
528: $request = $this->_addProxy($request, $options);
529: }
530:
531: return $request;
532: }
533:
534: /**
535: * Returns headers for Accept/Content-Type based on a short type
536: * or full mime-type.
537: *
538: * @param string $type short type alias or full mimetype.
539: * @return array Headers to set on the request.
540: * @throws \Cake\Core\Exception\Exception When an unknown type alias is used.
541: */
542: protected function _typeHeaders($type)
543: {
544: if (strpos($type, '/') !== false) {
545: return [
546: 'Accept' => $type,
547: 'Content-Type' => $type
548: ];
549: }
550: $typeMap = [
551: 'json' => 'application/json',
552: 'xml' => 'application/xml',
553: ];
554: if (!isset($typeMap[$type])) {
555: throw new Exception("Unknown type alias '$type'.");
556: }
557:
558: return [
559: 'Accept' => $typeMap[$type],
560: 'Content-Type' => $typeMap[$type],
561: ];
562: }
563:
564: /**
565: * Add authentication headers to the request.
566: *
567: * Uses the authentication type to choose the correct strategy
568: * and use its methods to add headers.
569: *
570: * @param \Cake\Http\Client\Request $request The request to modify.
571: * @param array $options Array of options containing the 'auth' key.
572: * @return \Cake\Http\Client\Request The updated request object.
573: */
574: protected function _addAuthentication(Request $request, $options)
575: {
576: $auth = $options['auth'];
577: $adapter = $this->_createAuth($auth, $options);
578: $result = $adapter->authentication($request, $options['auth']);
579:
580: return $result ?: $request;
581: }
582:
583: /**
584: * Add proxy authentication headers.
585: *
586: * Uses the authentication type to choose the correct strategy
587: * and use its methods to add headers.
588: *
589: * @param \Cake\Http\Client\Request $request The request to modify.
590: * @param array $options Array of options containing the 'proxy' key.
591: * @return \Cake\Http\Client\Request The updated request object.
592: */
593: protected function _addProxy(Request $request, $options)
594: {
595: $auth = $options['proxy'];
596: $adapter = $this->_createAuth($auth, $options);
597: $result = $adapter->proxyAuthentication($request, $options['proxy']);
598:
599: return $result ?: $request;
600: }
601:
602: /**
603: * Create the authentication strategy.
604: *
605: * Use the configuration options to create the correct
606: * authentication strategy handler.
607: *
608: * @param array $auth The authentication options to use.
609: * @param array $options The overall request options to use.
610: * @return object Authentication strategy instance.
611: * @throws \Cake\Core\Exception\Exception when an invalid strategy is chosen.
612: */
613: protected function _createAuth($auth, $options)
614: {
615: if (empty($auth['type'])) {
616: $auth['type'] = 'basic';
617: }
618: $name = ucfirst($auth['type']);
619: $class = App::className($name, 'Http/Client/Auth');
620: if (!$class) {
621: throw new Exception(
622: sprintf('Invalid authentication type %s', $name)
623: );
624: }
625:
626: return new $class($this, $options);
627: }
628: }
629: // @deprecated 3.4.0 Backwards compatibility with earler 3.x versions.
630: class_alias('Cake\Http\Client', 'Cake\Network\Http\Client');
631: