composer package updates
[openemr.git] / vendor / zendframework / zend-http / src / Client.php
blobdab67b8f7477b4a37ecd82d464973abe06a8fdbb
1 <?php
2 /**
3 * @see https://github.com/zendframework/zend-http for the canonical source repository
4 * @copyright Copyright (c) 2005-2017 Zend Technologies USA Inc. (http://www.zend.com)
5 * @license https://github.com/zendframework/zend-http/blob/master/LICENSE.md New BSD License
6 */
8 namespace Zend\Http;
10 use ArrayIterator;
11 use Traversable;
12 use Zend\Http\Client\Adapter\Curl;
13 use Zend\Http\Client\Adapter\Socket;
14 use Zend\Stdlib;
15 use Zend\Stdlib\ArrayUtils;
16 use Zend\Stdlib\ErrorHandler;
17 use Zend\Uri\Http;
19 /**
20 * Http client
22 class Client implements Stdlib\DispatchableInterface
24 /**
25 * @const string Supported HTTP Authentication methods
27 const AUTH_BASIC = 'basic';
28 const AUTH_DIGEST = 'digest';
30 /**
31 * @const string POST data encoding methods
33 const ENC_URLENCODED = 'application/x-www-form-urlencoded';
34 const ENC_FORMDATA = 'multipart/form-data';
36 /**
37 * @const string DIGEST Authentication
39 const DIGEST_REALM = 'realm';
40 const DIGEST_QOP = 'qop';
41 const DIGEST_NONCE = 'nonce';
42 const DIGEST_OPAQUE = 'opaque';
43 const DIGEST_NC = 'nc';
44 const DIGEST_CNONCE = 'cnonce';
46 /**
47 * @var Response
49 protected $response;
51 /**
52 * @var Request
54 protected $request;
56 /**
57 * @var Client\Adapter\AdapterInterface
59 protected $adapter;
61 /**
62 * @var array
64 protected $auth = [];
66 /**
67 * @var string
69 protected $streamName;
71 /**
72 * @var resource|null
74 protected $streamHandle = null;
76 /**
77 * @var array of Header\SetCookie
79 protected $cookies = [];
81 /**
82 * @var string
84 protected $encType = '';
86 /**
87 * @var Request
89 protected $lastRawRequest;
91 /**
92 * @var Response
94 protected $lastRawResponse;
96 /**
97 * @var int
99 protected $redirectCounter = 0;
102 * Configuration array, set using the constructor or using ::setOptions()
104 * @var array
106 protected $config = [
107 'maxredirects' => 5,
108 'strictredirects' => false,
109 'useragent' => Client::class,
110 'timeout' => 10,
111 'connecttimeout' => null,
112 'adapter' => Socket::class,
113 'httpversion' => Request::VERSION_11,
114 'storeresponse' => true,
115 'keepalive' => false,
116 'outputstream' => false,
117 'encodecookies' => true,
118 'argseparator' => null,
119 'rfc3986strict' => false,
120 'sslcafile' => null,
121 'sslcapath' => null,
125 * Fileinfo magic database resource
127 * This variable is populated the first time _detectFileMimeType is called
128 * and is then reused on every call to this method
130 * @var resource
132 protected static $fileInfoDb;
135 * Constructor
137 * @param string $uri
138 * @param array|Traversable $options
140 public function __construct($uri = null, $options = null)
142 if ($uri !== null) {
143 $this->setUri($uri);
145 if ($options !== null) {
146 $this->setOptions($options);
151 * Set configuration parameters for this HTTP client
153 * @param array|Traversable $options
154 * @return Client
155 * @throws Client\Exception\InvalidArgumentException
157 public function setOptions($options = [])
159 if ($options instanceof Traversable) {
160 $options = ArrayUtils::iteratorToArray($options);
162 if (! is_array($options)) {
163 throw new Client\Exception\InvalidArgumentException('Config parameter is not valid');
166 /** Config Key Normalization */
167 foreach ($options as $k => $v) {
168 $this->config[str_replace(['-', '_', ' ', '.'], '', strtolower($k))] = $v; // replace w/ normalized
171 // Pass configuration options to the adapter if it exists
172 if ($this->adapter instanceof Client\Adapter\AdapterInterface) {
173 $this->adapter->setOptions($options);
176 return $this;
180 * Load the connection adapter
182 * While this method is not called more than one for a client, it is
183 * separated from ->request() to preserve logic and readability
185 * @param Client\Adapter\AdapterInterface|string $adapter
186 * @return Client
187 * @throws Client\Exception\InvalidArgumentException
189 public function setAdapter($adapter)
191 if (is_string($adapter)) {
192 if (! class_exists($adapter)) {
193 throw new Client\Exception\InvalidArgumentException(
194 'Unable to locate adapter class "' . $adapter . '"'
197 $adapter = new $adapter;
200 if (! $adapter instanceof Client\Adapter\AdapterInterface) {
201 throw new Client\Exception\InvalidArgumentException('Passed adapter is not a HTTP connection adapter');
204 $this->adapter = $adapter;
205 $config = $this->config;
206 unset($config['adapter']);
207 $this->adapter->setOptions($config);
208 return $this;
212 * Load the connection adapter
214 * @return Client\Adapter\AdapterInterface $adapter
216 public function getAdapter()
218 if (! $this->adapter) {
219 $this->setAdapter($this->config['adapter']);
222 return $this->adapter;
226 * Set request
228 * @param Request $request
229 * @return Client
231 public function setRequest(Request $request)
233 $this->request = $request;
234 return $this;
238 * Get Request
240 * @return Request
242 public function getRequest()
244 if (empty($this->request)) {
245 $this->request = new Request();
246 $this->request->setAllowCustomMethods(false);
248 return $this->request;
252 * Set response
254 * @param Response $response
255 * @return Client
257 public function setResponse(Response $response)
259 $this->response = $response;
260 return $this;
264 * Get Response
266 * @return Response
268 public function getResponse()
270 if (empty($this->response)) {
271 $this->response = new Response();
273 return $this->response;
277 * Get the last request (as a string)
279 * @return string
281 public function getLastRawRequest()
283 return $this->lastRawRequest;
287 * Get the last response (as a string)
289 * @return string
291 public function getLastRawResponse()
293 return $this->lastRawResponse;
297 * Get the redirections count
299 * @return int
301 public function getRedirectionsCount()
303 return $this->redirectCounter;
307 * Set Uri (to the request)
309 * @param string|Http $uri
310 * @return Client
312 public function setUri($uri)
314 if (! empty($uri)) {
315 // remember host of last request
316 $lastHost = $this->getRequest()->getUri()->getHost();
317 $this->getRequest()->setUri($uri);
319 // if host changed, the HTTP authentication should be cleared for security
320 // reasons, see #4215 for a discussion - currently authentication is also
321 // cleared for peer subdomains due to technical limits
322 $nextHost = $this->getRequest()->getUri()->getHost();
323 if (! preg_match('/' . preg_quote($lastHost, '/') . '$/i', $nextHost)) {
324 $this->clearAuth();
327 // Set auth if username and password has been specified in the uri
328 if ($this->getUri()->getUser() && $this->getUri()->getPassword()) {
329 $this->setAuth($this->getUri()->getUser(), $this->getUri()->getPassword());
332 // We have no ports, set the defaults
333 if (! $this->getUri()->getPort()) {
334 $this->getUri()->setPort(($this->getUri()->getScheme() == 'https' ? 443 : 80));
337 return $this;
341 * Get uri (from the request)
343 * @return Http
345 public function getUri()
347 return $this->getRequest()->getUri();
351 * Set the HTTP method (to the request)
353 * @param string $method
354 * @return Client
356 public function setMethod($method)
358 $method = $this->getRequest()->setMethod($method)->getMethod();
360 if (empty($this->encType)
361 && in_array(
362 $method,
364 Request::METHOD_POST,
365 Request::METHOD_PUT,
366 Request::METHOD_DELETE,
367 Request::METHOD_PATCH,
368 Request::METHOD_OPTIONS,
370 true
373 $this->setEncType(self::ENC_URLENCODED);
376 return $this;
380 * Get the HTTP method
382 * @return string
384 public function getMethod()
386 return $this->getRequest()->getMethod();
390 * Set the query string argument separator
392 * @param string $argSeparator
393 * @return Client
395 public function setArgSeparator($argSeparator)
397 $this->setOptions(['argseparator' => $argSeparator]);
398 return $this;
402 * Get the query string argument separator
404 * @return string
406 public function getArgSeparator()
408 $argSeparator = $this->config['argseparator'];
409 if (empty($argSeparator)) {
410 $argSeparator = ini_get('arg_separator.output');
411 $this->setArgSeparator($argSeparator);
413 return $argSeparator;
417 * Set the encoding type and the boundary (if any)
419 * @param string $encType
420 * @param string $boundary
421 * @return Client
423 public function setEncType($encType, $boundary = null)
425 if (null === $encType || empty($encType)) {
426 $this->encType = null;
427 return $this;
430 if (! empty($boundary)) {
431 $encType .= sprintf('; boundary=%s', $boundary);
434 $this->encType = $encType;
435 return $this;
439 * Get the encoding type
441 * @return string
443 public function getEncType()
445 return $this->encType;
449 * Set raw body (for advanced use cases)
451 * @param string $body
452 * @return Client
454 public function setRawBody($body)
456 $this->getRequest()->setContent($body);
457 return $this;
461 * Set the POST parameters
463 * @param array $post
464 * @return Client
466 public function setParameterPost(array $post)
468 $this->getRequest()->getPost()->fromArray($post);
469 return $this;
473 * Set the GET parameters
475 * @param array $query
476 * @return Client
478 public function setParameterGet(array $query)
480 $this->getRequest()->getQuery()->fromArray($query);
481 return $this;
485 * Reset all the HTTP parameters (request, response, etc)
487 * @param bool $clearCookies Also clear all valid cookies? (defaults to false)
488 * @param bool $clearAuth Also clear http authentication? (defaults to true)
489 * @return Client
491 public function resetParameters($clearCookies = false /*, $clearAuth = true */)
493 $clearAuth = true;
494 if (func_num_args() > 1) {
495 $clearAuth = func_get_arg(1);
498 $uri = $this->getUri();
500 $this->streamName = null;
501 $this->encType = null;
502 $this->request = null;
503 $this->response = null;
504 $this->lastRawRequest = null;
505 $this->lastRawResponse = null;
507 $this->setUri($uri);
509 if ($clearCookies) {
510 $this->clearCookies();
513 if ($clearAuth) {
514 $this->clearAuth();
517 return $this;
521 * Return the current cookies
523 * @return array
525 public function getCookies()
527 return $this->cookies;
531 * Get the cookie Id (name+domain+path)
533 * @param Header\SetCookie|Header\Cookie $cookie
534 * @return string|bool
536 protected function getCookieId($cookie)
538 if (($cookie instanceof Header\SetCookie) || ($cookie instanceof Header\Cookie)) {
539 return $cookie->getName() . $cookie->getDomain() . $cookie->getPath();
541 return false;
545 * Add a cookie
547 * @param array|ArrayIterator|Header\SetCookie|string $cookie
548 * @param string $value
549 * @param string $expire
550 * @param string $path
551 * @param string $domain
552 * @param bool $secure
553 * @param bool $httponly
554 * @param string $maxAge
555 * @param string $version
556 * @throws Exception\InvalidArgumentException
557 * @return Client
559 public function addCookie(
560 $cookie,
561 $value = null,
562 $expire = null,
563 $path = null,
564 $domain = null,
565 $secure = false,
566 $httponly = true,
567 $maxAge = null,
568 $version = null
570 if (is_array($cookie) || $cookie instanceof ArrayIterator) {
571 foreach ($cookie as $setCookie) {
572 if ($setCookie instanceof Header\SetCookie) {
573 $this->cookies[$this->getCookieId($setCookie)] = $setCookie;
574 } else {
575 throw new Exception\InvalidArgumentException('The cookie parameter is not a valid Set-Cookie type');
578 } elseif (is_string($cookie) && $value !== null) {
579 $setCookie = new Header\SetCookie(
580 $cookie,
581 $value,
582 $expire,
583 $path,
584 $domain,
585 $secure,
586 $httponly,
587 $maxAge,
588 $version
590 $this->cookies[$this->getCookieId($setCookie)] = $setCookie;
591 } elseif ($cookie instanceof Header\SetCookie) {
592 $this->cookies[$this->getCookieId($cookie)] = $cookie;
593 } else {
594 throw new Exception\InvalidArgumentException('Invalid parameter type passed as Cookie');
596 return $this;
600 * Set an array of cookies
602 * @param array $cookies
603 * @throws Exception\InvalidArgumentException
604 * @return Client
606 public function setCookies($cookies)
608 if (is_array($cookies)) {
609 $this->clearCookies();
610 foreach ($cookies as $name => $value) {
611 $this->addCookie($name, $value);
613 } else {
614 throw new Exception\InvalidArgumentException('Invalid cookies passed as parameter, it must be an array');
616 return $this;
620 * Clear all the cookies
622 public function clearCookies()
624 $this->cookies = [];
628 * Set the headers (for the request)
630 * @param Headers|array $headers
631 * @throws Exception\InvalidArgumentException
632 * @return Client
634 public function setHeaders($headers)
636 if (is_array($headers)) {
637 $newHeaders = new Headers();
638 $newHeaders->addHeaders($headers);
639 $this->getRequest()->setHeaders($newHeaders);
640 } elseif ($headers instanceof Headers) {
641 $this->getRequest()->setHeaders($headers);
642 } else {
643 throw new Exception\InvalidArgumentException('Invalid parameter headers passed');
645 return $this;
649 * Check if exists the header type specified
651 * @param string $name
652 * @return bool
654 public function hasHeader($name)
656 $headers = $this->getRequest()->getHeaders();
658 if ($headers instanceof Headers) {
659 return $headers->has($name);
662 return false;
666 * Get the header value of the request
668 * @param string $name
669 * @return string|bool
671 public function getHeader($name)
673 $headers = $this->getRequest()->getHeaders();
675 if ($headers instanceof Headers) {
676 if ($headers->get($name)) {
677 return $headers->get($name)->getFieldValue();
680 return false;
684 * Set streaming for received data
686 * @param string|bool $streamfile Stream file, true for temp file, false/null for no streaming
687 * @return \Zend\Http\Client
689 public function setStream($streamfile = true)
691 $this->setOptions(['outputstream' => $streamfile]);
692 return $this;
696 * Get status of streaming for received data
697 * @return bool|string
699 public function getStream()
701 if (null !== $this->streamName) {
702 return $this->streamName;
705 return $this->config['outputstream'];
709 * Create temporary stream
711 * @throws Exception\RuntimeException
712 * @return resource
714 protected function openTempStream()
716 $this->streamName = $this->config['outputstream'];
718 if (! is_string($this->streamName)) {
719 // If name is not given, create temp name
720 $this->streamName = tempnam(
721 isset($this->config['streamtmpdir']) ? $this->config['streamtmpdir'] : sys_get_temp_dir(),
722 Client::class
726 ErrorHandler::start();
727 $fp = fopen($this->streamName, 'w+b');
728 $error = ErrorHandler::stop();
729 if (false === $fp) {
730 if ($this->adapter instanceof Client\Adapter\AdapterInterface) {
731 $this->adapter->close();
733 throw new Exception\RuntimeException(sprintf('Could not open temp file %s', $this->streamName), 0, $error);
736 return $fp;
740 * Create a HTTP authentication "Authorization:" header according to the
741 * specified user, password and authentication method.
743 * @param string $user
744 * @param string $password
745 * @param string $type
746 * @throws Exception\InvalidArgumentException
747 * @return Client
749 public function setAuth($user, $password, $type = self::AUTH_BASIC)
751 if (! defined('static::AUTH_' . strtoupper($type))) {
752 throw new Exception\InvalidArgumentException(sprintf(
753 'Invalid or not supported authentication type: \'%s\'',
754 $type
758 if (empty($user)) {
759 throw new Exception\InvalidArgumentException('The username cannot be empty');
762 $this->auth = [
763 'user' => $user,
764 'password' => $password,
765 'type' => $type,
768 return $this;
772 * Clear http authentication
774 public function clearAuth()
776 $this->auth = [];
780 * Calculate the response value according to the HTTP authentication type
782 * @see http://www.faqs.org/rfcs/rfc2617.html
783 * @param string $user
784 * @param string $password
785 * @param string $type
786 * @param array $digest
787 * @param null|string $entityBody
788 * @throws Exception\InvalidArgumentException
789 * @return string|bool
791 protected function calcAuthDigest($user, $password, $type = self::AUTH_BASIC, $digest = [], $entityBody = null)
793 if (! defined('self::AUTH_' . strtoupper($type))) {
794 throw new Exception\InvalidArgumentException(sprintf(
795 'Invalid or not supported authentication type: \'%s\'',
796 $type
799 $response = false;
800 switch (strtolower($type)) {
801 case self::AUTH_BASIC:
802 // In basic authentication, the user name cannot contain ":"
803 if (strpos($user, ':') !== false) {
804 throw new Exception\InvalidArgumentException(
805 'The user name cannot contain \':\' in Basic HTTP authentication'
808 $response = base64_encode($user . ':' . $password);
809 break;
810 case self::AUTH_DIGEST:
811 if (empty($digest)) {
812 throw new Exception\InvalidArgumentException('The digest cannot be empty');
814 foreach ($digest as $key => $value) {
815 if (! defined('self::DIGEST_' . strtoupper($key))) {
816 throw new Exception\InvalidArgumentException(sprintf(
817 'Invalid or not supported digest authentication parameter: \'%s\'',
818 $key
822 $ha1 = md5($user . ':' . $digest['realm'] . ':' . $password);
823 if (empty($digest['qop']) || strtolower($digest['qop']) == 'auth') {
824 $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath());
825 } elseif (strtolower($digest['qop']) == 'auth-int') {
826 if (empty($entityBody)) {
827 throw new Exception\InvalidArgumentException(
828 'I cannot use the auth-int digest authentication without the entity body'
831 $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath() . ':' . md5($entityBody));
833 if (empty($digest['qop'])) {
834 $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $ha2);
835 } else {
836 $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $digest['nc']
837 . ':' . $digest['cnonce'] . ':' . $digest['qoc'] . ':' . $ha2);
839 break;
841 return $response;
845 * Dispatch
847 * @param Stdlib\RequestInterface $request
848 * @param Stdlib\ResponseInterface $response
849 * @return Stdlib\ResponseInterface
851 public function dispatch(Stdlib\RequestInterface $request, Stdlib\ResponseInterface $response = null)
853 $response = $this->send($request);
854 return $response;
858 * Send HTTP request
860 * @param Request $request
861 * @return Response
862 * @throws Exception\RuntimeException
863 * @throws Client\Exception\RuntimeException
865 public function send(Request $request = null)
867 if ($request !== null) {
868 $this->setRequest($request);
871 $this->redirectCounter = 0;
873 $adapter = $this->getAdapter();
875 // Send the first request. If redirected, continue.
876 do {
877 // uri
878 $uri = $this->getUri();
880 // query
881 $query = $this->getRequest()->getQuery();
883 if (! empty($query)) {
884 $queryArray = $query->toArray();
886 if (! empty($queryArray)) {
887 $newUri = $uri->toString();
888 $queryString = http_build_query($queryArray, null, $this->getArgSeparator());
890 if ($this->config['rfc3986strict']) {
891 $queryString = str_replace('+', '%20', $queryString);
894 if (strpos($newUri, '?') !== false) {
895 $newUri .= $this->getArgSeparator() . $queryString;
896 } else {
897 $newUri .= '?' . $queryString;
900 $uri = new Http($newUri);
903 // If we have no ports, set the defaults
904 if (! $uri->getPort()) {
905 $uri->setPort($uri->getScheme() == 'https' ? 443 : 80);
908 // method
909 $method = $this->getRequest()->getMethod();
911 // this is so the correct Encoding Type is set
912 $this->setMethod($method);
914 // body
915 $body = $this->prepareBody();
917 // headers
918 $headers = $this->prepareHeaders($body, $uri);
920 $secure = $uri->getScheme() == 'https';
922 // cookies
923 $cookie = $this->prepareCookies($uri->getHost(), $uri->getPath(), $secure);
924 if ($cookie->getFieldValue()) {
925 $headers['Cookie'] = $cookie->getFieldValue();
928 // check that adapter supports streaming before using it
929 if (is_resource($body) && ! ($adapter instanceof Client\Adapter\StreamInterface)) {
930 throw new Client\Exception\RuntimeException('Adapter does not support streaming');
933 $this->streamHandle = null;
934 // calling protected method to allow extending classes
935 // to wrap the interaction with the adapter
936 $response = $this->doRequest($uri, $method, $secure, $headers, $body);
937 $stream = $this->streamHandle;
938 $this->streamHandle = null;
940 if (! $response) {
941 if ($stream !== null) {
942 fclose($stream);
944 throw new Exception\RuntimeException('Unable to read response, or response is empty');
947 if ($this->config['storeresponse']) {
948 $this->lastRawResponse = $response;
949 } else {
950 $this->lastRawResponse = null;
953 if ($this->config['outputstream']) {
954 if ($stream === null) {
955 $stream = $this->getStream();
956 if (! is_resource($stream) && is_string($stream)) {
957 $stream = fopen($stream, 'r');
960 $streamMetaData = stream_get_meta_data($stream);
961 if ($streamMetaData['seekable']) {
962 rewind($stream);
964 // cleanup the adapter
965 $adapter->setOutputStream(null);
966 $response = Response\Stream::fromStream($response, $stream);
967 $response->setStreamName($this->streamName);
968 if (! is_string($this->config['outputstream'])) {
969 // we used temp name, will need to clean up
970 $response->setCleanup(true);
972 } else {
973 $response = $this->getResponse()->fromString($response);
976 // Get the cookies from response (if any)
977 $setCookies = $response->getCookie();
978 if (! empty($setCookies)) {
979 $this->addCookie($setCookies);
982 // If we got redirected, look for the Location header
983 if ($response->isRedirect() && ($response->getHeaders()->has('Location'))) {
984 // Avoid problems with buggy servers that add whitespace at the
985 // end of some headers
986 $location = trim($response->getHeaders()->get('Location')->getFieldValue());
988 // Check whether we send the exact same request again, or drop the parameters
989 // and send a GET request
990 if ($response->getStatusCode() == 303
991 || ((! $this->config['strictredirects'])
992 && ($response->getStatusCode() == 302 || $response->getStatusCode() == 301))
994 $this->resetParameters(false, false);
995 $this->setMethod(Request::METHOD_GET);
998 // If we got a well formed absolute URI
999 if (($scheme = substr($location, 0, 6))
1000 && ($scheme == 'http:/' || $scheme == 'https:')
1002 // setURI() clears parameters if host changed, see #4215
1003 $this->setUri($location);
1004 } else {
1005 // Split into path and query and set the query
1006 if (strpos($location, '?') !== false) {
1007 list($location, $query) = explode('?', $location, 2);
1008 } else {
1009 $query = '';
1011 $this->getUri()->setQuery($query);
1013 // Else, if we got just an absolute path, set it
1014 if (strpos($location, '/') === 0) {
1015 $this->getUri()->setPath($location);
1016 // Else, assume we have a relative path
1017 } else {
1018 // Get the current path directory, removing any trailing slashes
1019 $path = $this->getUri()->getPath();
1020 $path = rtrim(substr($path, 0, strrpos($path, '/')), '/');
1021 $this->getUri()->setPath($path . '/' . $location);
1024 ++$this->redirectCounter;
1025 } else {
1026 // If we didn't get any location, stop redirecting
1027 break;
1029 } while ($this->redirectCounter <= $this->config['maxredirects']);
1031 $this->response = $response;
1032 return $response;
1036 * Fully reset the HTTP client (auth, cookies, request, response, etc.)
1038 * @return Client
1040 public function reset()
1042 $this->resetParameters();
1043 $this->clearAuth();
1044 $this->clearCookies();
1046 return $this;
1050 * Set a file to upload (using a POST request)
1052 * Can be used in two ways:
1054 * 1. $data is null (default): $filename is treated as the name if a local file which
1055 * will be read and sent. Will try to guess the content type using mime_content_type().
1056 * 2. $data is set - $filename is sent as the file name, but $data is sent as the file
1057 * contents and no file is read from the file system. In this case, you need to
1058 * manually set the Content-Type ($ctype) or it will default to
1059 * application/octet-stream.
1061 * @param string $filename Name of file to upload, or name to save as
1062 * @param string $formname Name of form element to send as
1063 * @param string $data Data to send (if null, $filename is read and sent)
1064 * @param string $ctype Content type to use (if $data is set and $ctype is
1065 * null, will be application/octet-stream)
1066 * @return Client
1067 * @throws Exception\RuntimeException
1069 public function setFileUpload($filename, $formname, $data = null, $ctype = null)
1071 if ($data === null) {
1072 ErrorHandler::start();
1073 $data = file_get_contents($filename);
1074 $error = ErrorHandler::stop();
1075 if ($data === false) {
1076 throw new Exception\RuntimeException(sprintf(
1077 'Unable to read file \'%s\' for upload',
1078 $filename
1079 ), 0, $error);
1081 if (! $ctype) {
1082 $ctype = $this->detectFileMimeType($filename);
1086 $this->getRequest()->getFiles()->set($filename, [
1087 'formname' => $formname,
1088 'filename' => basename($filename),
1089 'ctype' => $ctype,
1090 'data' => $data,
1093 return $this;
1097 * Remove a file to upload
1099 * @param string $filename
1100 * @return bool
1102 public function removeFileUpload($filename)
1104 $file = $this->getRequest()->getFiles()->get($filename);
1105 if (! empty($file)) {
1106 $this->getRequest()->getFiles()->set($filename, null);
1107 return true;
1109 return false;
1113 * Prepare Cookies
1115 * @param string $domain
1116 * @param string $path
1117 * @param bool $secure
1118 * @return Header\Cookie|bool
1120 protected function prepareCookies($domain, $path, $secure)
1122 $validCookies = [];
1124 if (! empty($this->cookies)) {
1125 foreach ($this->cookies as $id => $cookie) {
1126 if ($cookie->isExpired()) {
1127 unset($this->cookies[$id]);
1128 continue;
1131 if ($cookie->isValidForRequest($domain, $path, $secure)) {
1132 // OAM hack some domains try to set the cookie multiple times
1133 $validCookies[$cookie->getName()] = $cookie;
1138 $cookies = Header\Cookie::fromSetCookieArray($validCookies);
1139 $cookies->setEncodeValue($this->config['encodecookies']);
1141 return $cookies;
1145 * Prepare the request headers
1147 * @param resource|string $body
1148 * @param Http $uri
1149 * @throws Exception\RuntimeException
1150 * @return array
1152 protected function prepareHeaders($body, $uri)
1154 $headers = [];
1156 // Set the host header
1157 if ($this->config['httpversion'] == Request::VERSION_11) {
1158 $host = $uri->getHost();
1159 // If the port is not default, add it
1160 if (! (($uri->getScheme() == 'http' && $uri->getPort() == 80)
1161 || ($uri->getScheme() == 'https' && $uri->getPort() == 443))
1163 $host .= ':' . $uri->getPort();
1166 $headers['Host'] = $host;
1169 // Set the connection header
1170 if (! $this->getRequest()->getHeaders()->has('Connection')) {
1171 if (! $this->config['keepalive']) {
1172 $headers['Connection'] = 'close';
1176 // Set the Accept-encoding header if not set - depending on whether
1177 // zlib is available or not.
1178 if (! $this->getRequest()->getHeaders()->has('Accept-Encoding')) {
1179 if (function_exists('gzinflate')) {
1180 $headers['Accept-Encoding'] = 'gzip, deflate';
1181 } else {
1182 $headers['Accept-Encoding'] = 'identity';
1186 // Set the user agent header
1187 if (! $this->getRequest()->getHeaders()->has('User-Agent') && isset($this->config['useragent'])) {
1188 $headers['User-Agent'] = $this->config['useragent'];
1191 // Set HTTP authentication if needed
1192 if (! empty($this->auth)) {
1193 switch ($this->auth['type']) {
1194 case self::AUTH_BASIC:
1195 $auth = $this->calcAuthDigest($this->auth['user'], $this->auth['password'], $this->auth['type']);
1196 if ($auth !== false) {
1197 $headers['Authorization'] = 'Basic ' . $auth;
1199 break;
1200 case self::AUTH_DIGEST:
1201 if (! $this->adapter instanceof Client\Adapter\Curl) {
1202 throw new Exception\RuntimeException(sprintf(
1203 'The digest authentication is only available for curl adapters (%s)',
1204 Curl::class
1208 $this->adapter->setCurlOption(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
1209 $this->adapter->setCurlOption(CURLOPT_USERPWD, $this->auth['user'] . ':' . $this->auth['password']);
1213 // Content-type
1214 $encType = $this->getEncType();
1215 if (! empty($encType)) {
1216 $headers['Content-Type'] = $encType;
1219 if (! empty($body)) {
1220 if (is_resource($body)) {
1221 $fstat = fstat($body);
1222 $headers['Content-Length'] = $fstat['size'];
1223 } else {
1224 $headers['Content-Length'] = strlen($body);
1228 // Merge the headers of the request (if any)
1229 // here we need right 'http field' and not lowercase letters
1230 $requestHeaders = $this->getRequest()->getHeaders();
1231 foreach ($requestHeaders as $requestHeaderElement) {
1232 $headers[$requestHeaderElement->getFieldName()] = $requestHeaderElement->getFieldValue();
1234 return $headers;
1238 * Prepare the request body (for PATCH, POST and PUT requests)
1240 * @return string
1241 * @throws \Zend\Http\Client\Exception\RuntimeException
1243 protected function prepareBody()
1245 // According to RFC2616, a TRACE request should not have a body.
1246 if ($this->getRequest()->isTrace()) {
1247 return '';
1250 $rawBody = $this->getRequest()->getContent();
1251 if (! empty($rawBody)) {
1252 return $rawBody;
1255 $body = '';
1256 $hasFiles = false;
1258 if (! $this->getRequest()->getHeaders()->has('Content-Type')) {
1259 $hasFiles = ! empty($this->getRequest()->getFiles()->toArray());
1260 // If we have files to upload, force encType to multipart/form-data
1261 if ($hasFiles) {
1262 $this->setEncType(self::ENC_FORMDATA);
1264 } else {
1265 $this->setEncType($this->getHeader('Content-Type'));
1268 // If we have POST parameters or files, encode and add them to the body
1269 if (! empty($this->getRequest()->getPost()->toArray()) || $hasFiles) {
1270 if (stripos($this->getEncType(), self::ENC_FORMDATA) === 0) {
1271 $boundary = '---ZENDHTTPCLIENT-' . md5(microtime());
1272 $this->setEncType(self::ENC_FORMDATA, $boundary);
1274 // Get POST parameters and encode them
1275 $params = self::flattenParametersArray($this->getRequest()->getPost()->toArray());
1276 foreach ($params as $pp) {
1277 $body .= $this->encodeFormData($boundary, $pp[0], $pp[1]);
1280 // Encode files
1281 foreach ($this->getRequest()->getFiles()->toArray() as $file) {
1282 $fhead = ['Content-Type' => $file['ctype']];
1283 $body .= $this->encodeFormData(
1284 $boundary,
1285 $file['formname'],
1286 $file['data'],
1287 $file['filename'],
1288 $fhead
1291 $body .= '--' . $boundary . '--' . "\r\n";
1292 } elseif (stripos($this->getEncType(), self::ENC_URLENCODED) === 0) {
1293 // Encode body as application/x-www-form-urlencoded
1294 $body = http_build_query($this->getRequest()->getPost()->toArray(), null, '&');
1295 } else {
1296 throw new Client\Exception\RuntimeException(sprintf(
1297 'Cannot handle content type \'%s\' automatically',
1298 $this->encType
1303 return $body;
1308 * Attempt to detect the MIME type of a file using available extensions
1310 * This method will try to detect the MIME type of a file. If the fileinfo
1311 * extension is available, it will be used. If not, the mime_magic
1312 * extension which is deprecated but is still available in many PHP setups
1313 * will be tried.
1315 * If neither extension is available, the default application/octet-stream
1316 * MIME type will be returned
1318 * @param string $file File path
1319 * @return string MIME type
1321 protected function detectFileMimeType($file)
1323 $type = null;
1325 // First try with fileinfo functions
1326 if (function_exists('finfo_open')) {
1327 if (static::$fileInfoDb === null) {
1328 ErrorHandler::start();
1329 static::$fileInfoDb = finfo_open(FILEINFO_MIME);
1330 ErrorHandler::stop();
1333 if (static::$fileInfoDb) {
1334 $type = finfo_file(static::$fileInfoDb, $file);
1336 } elseif (function_exists('mime_content_type')) {
1337 $type = mime_content_type($file);
1340 // Fallback to the default application/octet-stream
1341 if (! $type) {
1342 $type = 'application/octet-stream';
1345 return $type;
1349 * Encode data to a multipart/form-data part suitable for a POST request.
1351 * @param string $boundary
1352 * @param string $name
1353 * @param mixed $value
1354 * @param string $filename
1355 * @param array $headers Associative array of optional headers @example ("Content-Transfer-Encoding" => "binary")
1356 * @return string
1358 public function encodeFormData($boundary, $name, $value, $filename = null, $headers = [])
1360 $ret = '--' . $boundary . "\r\n"
1361 . 'Content-Disposition: form-data; name="' . $name . '"';
1363 if ($filename) {
1364 $ret .= '; filename="' . $filename . '"';
1366 $ret .= "\r\n";
1368 foreach ($headers as $hname => $hvalue) {
1369 $ret .= $hname . ': ' . $hvalue . "\r\n";
1371 $ret .= "\r\n";
1372 $ret .= $value . "\r\n";
1374 return $ret;
1378 * Convert an array of parameters into a flat array of (key, value) pairs
1380 * Will flatten a potentially multi-dimentional array of parameters (such
1381 * as POST parameters) into a flat array of (key, value) paris. In case
1382 * of multi-dimentional arrays, square brackets ([]) will be added to the
1383 * key to indicate an array.
1385 * @since 1.9
1387 * @param array $parray
1388 * @param string $prefix
1389 * @return array
1391 protected function flattenParametersArray($parray, $prefix = null)
1393 if (! is_array($parray)) {
1394 return $parray;
1397 $parameters = [];
1399 foreach ($parray as $name => $value) {
1400 // Calculate array key
1401 if ($prefix) {
1402 if (is_int($name)) {
1403 $key = $prefix . '[]';
1404 } else {
1405 $key = $prefix . sprintf('[%s]', $name);
1407 } else {
1408 $key = $name;
1411 if (is_array($value)) {
1412 $parameters = array_merge($parameters, $this->flattenParametersArray($value, $key));
1413 } else {
1414 $parameters[] = [$key, $value];
1418 return $parameters;
1422 * Separating this from send method allows subclasses to wrap
1423 * the interaction with the adapter
1425 * @param Http $uri
1426 * @param string $method
1427 * @param bool $secure
1428 * @param array $headers
1429 * @param string $body
1430 * @return string the raw response
1431 * @throws Exception\RuntimeException
1433 protected function doRequest(Http $uri, $method, $secure = false, $headers = [], $body = '')
1435 // Open the connection, send the request and read the response
1436 $this->adapter->connect($uri->getHost(), $uri->getPort(), $secure);
1438 if ($this->config['outputstream']) {
1439 if ($this->adapter instanceof Client\Adapter\StreamInterface) {
1440 $this->streamHandle = $this->openTempStream();
1441 $this->adapter->setOutputStream($this->streamHandle);
1442 } else {
1443 throw new Exception\RuntimeException('Adapter does not support streaming');
1446 // HTTP connection
1447 $this->lastRawRequest = $this->adapter->write(
1448 $method,
1449 $uri,
1450 $this->config['httpversion'],
1451 $headers,
1452 $body
1455 return $this->adapter->read();
1459 * Create a HTTP authentication "Authorization:" header according to the
1460 * specified user, password and authentication method.
1462 * @see http://www.faqs.org/rfcs/rfc2617.html
1463 * @param string $user
1464 * @param string $password
1465 * @param string $type
1466 * @return string
1467 * @throws Client\Exception\InvalidArgumentException
1469 public static function encodeAuthHeader($user, $password, $type = self::AUTH_BASIC)
1471 switch ($type) {
1472 case self::AUTH_BASIC:
1473 // In basic authentication, the user name cannot contain ":"
1474 if (strpos($user, ':') !== false) {
1475 throw new Client\Exception\InvalidArgumentException(
1476 'The user name cannot contain \':\' in \'Basic\' HTTP authentication'
1480 return 'Basic ' . base64_encode($user . ':' . $password);
1482 //case self::AUTH_DIGEST:
1484 * @todo Implement digest authentication
1486 // break;
1488 default:
1489 throw new Client\Exception\InvalidArgumentException(sprintf(
1490 'Not a supported HTTP authentication type: \'%s\'',
1491 $type
1495 return;