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
12 use Zend\Http\Client\Adapter\Curl
;
13 use Zend\Http\Client\Adapter\Socket
;
15 use Zend\Stdlib\ArrayUtils
;
16 use Zend\Stdlib\ErrorHandler
;
22 class Client
implements Stdlib\DispatchableInterface
25 * @const string Supported HTTP Authentication methods
27 const AUTH_BASIC
= 'basic';
28 const AUTH_DIGEST
= 'digest';
31 * @const string POST data encoding methods
33 const ENC_URLENCODED
= 'application/x-www-form-urlencoded';
34 const ENC_FORMDATA
= 'multipart/form-data';
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';
57 * @var Client\Adapter\AdapterInterface
69 protected $streamName;
74 protected $streamHandle = null;
77 * @var array of Header\SetCookie
79 protected $cookies = [];
84 protected $encType = '';
89 protected $lastRawRequest;
94 protected $lastRawResponse;
99 protected $redirectCounter = 0;
102 * Configuration array, set using the constructor or using ::setOptions()
106 protected $config = [
108 'strictredirects' => false,
109 'useragent' => Client
::class,
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,
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
132 protected static $fileInfoDb;
138 * @param array|Traversable $options
140 public function __construct($uri = null, $options = null)
145 if ($options !== null) {
146 $this->setOptions($options);
151 * Set configuration parameters for this HTTP client
153 * @param array|Traversable $options
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);
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
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);
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
;
228 * @param Request $request
231 public function setRequest(Request
$request)
233 $this->request
= $request;
242 public function getRequest()
244 if (empty($this->request
)) {
245 $this->request
= new Request();
246 $this->request
->setAllowCustomMethods(false);
248 return $this->request
;
254 * @param Response $response
257 public function setResponse(Response
$response)
259 $this->response
= $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)
281 public function getLastRawRequest()
283 return $this->lastRawRequest
;
287 * Get the last response (as a string)
291 public function getLastRawResponse()
293 return $this->lastRawResponse
;
297 * Get the redirections count
301 public function getRedirectionsCount()
303 return $this->redirectCounter
;
307 * Set Uri (to the request)
309 * @param string|Http $uri
312 public function setUri($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)) {
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));
341 * Get uri (from the request)
345 public function getUri()
347 return $this->getRequest()->getUri();
351 * Set the HTTP method (to the request)
353 * @param string $method
356 public function setMethod($method)
358 $method = $this->getRequest()->setMethod($method)->getMethod();
360 if (empty($this->encType
)
364 Request
::METHOD_POST
,
366 Request
::METHOD_DELETE
,
367 Request
::METHOD_PATCH
,
368 Request
::METHOD_OPTIONS
,
373 $this->setEncType(self
::ENC_URLENCODED
);
380 * Get the HTTP method
384 public function getMethod()
386 return $this->getRequest()->getMethod();
390 * Set the query string argument separator
392 * @param string $argSeparator
395 public function setArgSeparator($argSeparator)
397 $this->setOptions(['argseparator' => $argSeparator]);
402 * Get the query string argument separator
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
423 public function setEncType($encType, $boundary = null)
425 if (null === $encType ||
empty($encType)) {
426 $this->encType
= null;
430 if (! empty($boundary)) {
431 $encType .= sprintf('; boundary=%s', $boundary);
434 $this->encType
= $encType;
439 * Get the encoding type
443 public function getEncType()
445 return $this->encType
;
449 * Set raw body (for advanced use cases)
451 * @param string $body
454 public function setRawBody($body)
456 $this->getRequest()->setContent($body);
461 * Set the POST parameters
466 public function setParameterPost(array $post)
468 $this->getRequest()->getPost()->fromArray($post);
473 * Set the GET parameters
475 * @param array $query
478 public function setParameterGet(array $query)
480 $this->getRequest()->getQuery()->fromArray($query);
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)
491 public function resetParameters($clearCookies = false /*, $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;
510 $this->clearCookies();
521 * Return the current cookies
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();
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
559 public function addCookie(
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;
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(
590 $this->cookies
[$this->getCookieId($setCookie)] = $setCookie;
591 } elseif ($cookie instanceof Header\SetCookie
) {
592 $this->cookies
[$this->getCookieId($cookie)] = $cookie;
594 throw new Exception\
InvalidArgumentException('Invalid parameter type passed as Cookie');
600 * Set an array of cookies
602 * @param array $cookies
603 * @throws Exception\InvalidArgumentException
606 public function setCookies($cookies)
608 if (is_array($cookies)) {
609 $this->clearCookies();
610 foreach ($cookies as $name => $value) {
611 $this->addCookie($name, $value);
614 throw new Exception\
InvalidArgumentException('Invalid cookies passed as parameter, it must be an array');
620 * Clear all the cookies
622 public function clearCookies()
628 * Set the headers (for the request)
630 * @param Headers|array $headers
631 * @throws Exception\InvalidArgumentException
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);
643 throw new Exception\
InvalidArgumentException('Invalid parameter headers passed');
649 * Check if exists the header type specified
651 * @param string $name
654 public function hasHeader($name)
656 $headers = $this->getRequest()->getHeaders();
658 if ($headers instanceof Headers
) {
659 return $headers->has($name);
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();
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]);
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
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(),
726 ErrorHandler
::start();
727 $fp = fopen($this->streamName
, 'w+b');
728 $error = ErrorHandler
::stop();
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);
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
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\'',
759 throw new Exception\
InvalidArgumentException('The username cannot be empty');
764 'password' => $password,
772 * Clear http authentication
774 public function clearAuth()
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\'',
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);
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\'',
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);
836 $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $digest['nc']
837 . ':' . $digest['cnonce'] . ':' . $digest['qoc'] . ':' . $ha2);
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);
860 * @param Request $request
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.
878 $uri = $this->getUri();
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;
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);
909 $method = $this->getRequest()->getMethod();
911 // this is so the correct Encoding Type is set
912 $this->setMethod($method);
915 $body = $this->prepareBody();
918 $headers = $this->prepareHeaders($body, $uri);
920 $secure = $uri->getScheme() == 'https';
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;
941 if ($stream !== null) {
944 throw new Exception\
RuntimeException('Unable to read response, or response is empty');
947 if ($this->config
['storeresponse']) {
948 $this->lastRawResponse
= $response;
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']) {
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);
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);
1005 // Split into path and query and set the query
1006 if (strpos($location, '?') !== false) {
1007 list($location, $query) = explode('?', $location, 2);
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
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
;
1026 // If we didn't get any location, stop redirecting
1029 } while ($this->redirectCounter
<= $this->config
['maxredirects']);
1031 $this->response
= $response;
1036 * Fully reset the HTTP client (auth, cookies, request, response, etc.)
1040 public function reset()
1042 $this->resetParameters();
1044 $this->clearCookies();
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)
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',
1082 $ctype = $this->detectFileMimeType($filename);
1086 $this->getRequest()->getFiles()->set($filename, [
1087 'formname' => $formname,
1088 'filename' => basename($filename),
1097 * Remove a file to upload
1099 * @param string $filename
1102 public function removeFileUpload($filename)
1104 $file = $this->getRequest()->getFiles()->get($filename);
1105 if (! empty($file)) {
1106 $this->getRequest()->getFiles()->set($filename, null);
1115 * @param string $domain
1116 * @param string $path
1117 * @param bool $secure
1118 * @return Header\Cookie|bool
1120 protected function prepareCookies($domain, $path, $secure)
1124 if (! empty($this->cookies
)) {
1125 foreach ($this->cookies
as $id => $cookie) {
1126 if ($cookie->isExpired()) {
1127 unset($this->cookies
[$id]);
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']);
1145 * Prepare the request headers
1147 * @param resource|string $body
1149 * @throws Exception\RuntimeException
1152 protected function prepareHeaders($body, $uri)
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';
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;
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)',
1208 $this->adapter
->setCurlOption(CURLOPT_HTTPAUTH
, CURLAUTH_DIGEST
);
1209 $this->adapter
->setCurlOption(CURLOPT_USERPWD
, $this->auth
['user'] . ':' . $this->auth
['password']);
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'];
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();
1238 * Prepare the request body (for PATCH, POST and PUT requests)
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()) {
1250 $rawBody = $this->getRequest()->getContent();
1251 if (! empty($rawBody)) {
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
1262 $this->setEncType(self
::ENC_FORMDATA
);
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]);
1281 foreach ($this->getRequest()->getFiles()->toArray() as $file) {
1282 $fhead = ['Content-Type' => $file['ctype']];
1283 $body .= $this->encodeFormData(
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, '&');
1296 throw new Client\Exception\
RuntimeException(sprintf(
1297 'Cannot handle content type \'%s\' automatically',
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
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)
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
1342 $type = 'application/octet-stream';
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")
1358 public function encodeFormData($boundary, $name, $value, $filename = null, $headers = [])
1360 $ret = '--' . $boundary . "\r\n"
1361 . 'Content-Disposition: form-data; name="' . $name . '"';
1364 $ret .= '; filename="' . $filename . '"';
1368 foreach ($headers as $hname => $hvalue) {
1369 $ret .= $hname . ': ' . $hvalue . "\r\n";
1372 $ret .= $value . "\r\n";
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.
1387 * @param array $parray
1388 * @param string $prefix
1391 protected function flattenParametersArray($parray, $prefix = null)
1393 if (! is_array($parray)) {
1399 foreach ($parray as $name => $value) {
1400 // Calculate array key
1402 if (is_int($name)) {
1403 $key = $prefix . '[]';
1405 $key = $prefix . sprintf('[%s]', $name);
1411 if (is_array($value)) {
1412 $parameters = array_merge($parameters, $this->flattenParametersArray($value, $key));
1414 $parameters[] = [$key, $value];
1422 * Separating this from send method allows subclasses to wrap
1423 * the interaction with the adapter
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
);
1443 throw new Exception\
RuntimeException('Adapter does not support streaming');
1447 $this->lastRawRequest
= $this->adapter
->write(
1450 $this->config
['httpversion'],
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
1467 * @throws Client\Exception\InvalidArgumentException
1469 public static function encodeAuthHeader($user, $password, $type = self
::AUTH_BASIC
)
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
1489 throw new Client\Exception\
InvalidArgumentException(sprintf(
1490 'Not a supported HTTP authentication type: \'%s\'',