- <?php
- /**
- * Zend Framework (http://framework.zend.com/)
- *
- * @link http://github.com/zendframework/zf2 for the canonical source repository
- * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
- * @license http://framework.zend.com/license/new-bsd New BSD License
- */
-
- namespace Zend\Http;
-
- use ArrayIterator;
- use Traversable;
- use Zend\Stdlib;
- use Zend\Stdlib\ArrayUtils;
- use Zend\Stdlib\ErrorHandler;
- use Zend\Uri\Http;
-
- /**
- * Http client
- */
- class Client implements Stdlib\DispatchableInterface
- {
- /**
- * @const string Supported HTTP Authentication methods
- */
- const AUTH_BASIC = 'basic';
- const AUTH_DIGEST = 'digest'; // not implemented yet
-
- /**
- * @const string POST data encoding methods
- */
- const ENC_URLENCODED = 'application/x-www-form-urlencoded';
- const ENC_FORMDATA = 'multipart/form-data';
-
- /**
- * @const string DIGEST Authentication
- */
- const DIGEST_REALM = 'realm';
- const DIGEST_QOP = 'qop';
- const DIGEST_NONCE = 'nonce';
- const DIGEST_OPAQUE = 'opaque';
- const DIGEST_NC = 'nc';
- const DIGEST_CNONCE = 'cnonce';
-
- /**
- * @var Response
- */
- protected $response;
-
- /**
- * @var Request
- */
- protected $request;
-
- /**
- * @var Client/Adapter
- */
- protected $adapter;
-
- /**
- * @var array
- */
- protected $auth = array();
-
- /**
- * @var string
- */
- protected $streamName = null;
-
- /**
- * @var array of Header\SetCookie
- */
- protected $cookies = array();
-
- /**
- * @var string
- */
- protected $encType = '';
-
- /**
- * @var Request
- */
- protected $lastRawRequest = null;
-
- /**
- * @var Response
- */
- protected $lastRawResponse = null;
-
- /**
- * @var int
- */
- protected $redirectCounter = 0;
-
- /**
- * Configuration array, set using the constructor or using ::setOptions()
- *
- * @var array
- */
- protected $config = array(
- 'maxredirects' => 5,
- 'strictredirects' => false,
- 'useragent' => 'Zend\Http\Client',
- 'timeout' => 10,
- 'adapter' => 'Zend\Http\Client\Adapter\Socket',
- 'httpversion' => Request::VERSION_11,
- 'storeresponse' => true,
- 'keepalive' => false,
- 'outputstream' => false,
- 'encodecookies' => true,
- 'argseparator' => null,
- 'rfc3986strict' => false
- );
-
- /**
- * Fileinfo magic database resource
- *
- * This variable is populated the first time _detectFileMimeType is called
- * and is then reused on every call to this method
- *
- * @var resource
- */
- protected static $fileInfoDb = null;
-
- /**
- * Constructor
- *
- * @param string $uri
- * @param array|Traversable $options
- */
- public function __construct($uri = null, $options = null)
- {
- if ($uri !== null) {
- $this->setUri($uri);
- }
- if ($options !== null) {
- $this->setOptions($options);
- }
- }
-
- /**
- * Set configuration parameters for this HTTP client
- *
- * @param array|Traversable $options
- * @return Client
- * @throws Client\Exception\InvalidArgumentException
- */
- public function setOptions($options = array())
- {
- if ($options instanceof Traversable) {
- $options = ArrayUtils::iteratorToArray($options);
- }
- if (!is_array($options)) {
- throw new Client\Exception\InvalidArgumentException('Config parameter is not valid');
- }
-
- /** Config Key Normalization */
- foreach ($options as $k => $v) {
- $this->config[str_replace(array('-', '_', ' ', '.'), '', strtolower($k))] = $v; // replace w/ normalized
- }
-
- // Pass configuration options to the adapter if it exists
- if ($this->adapter instanceof Client\Adapter\AdapterInterface) {
- $this->adapter->setOptions($options);
- }
-
- return $this;
- }
-
- /**
- * Load the connection adapter
- *
- * While this method is not called more than one for a client, it is
- * separated from ->request() to preserve logic and readability
- *
- * @param Client\Adapter\AdapterInterface|string $adapter
- * @return Client
- * @throws Client\Exception\InvalidArgumentException
- */
- public function setAdapter($adapter)
- {
- if (is_string($adapter)) {
- if (!class_exists($adapter)) {
- throw new Client\Exception\InvalidArgumentException('Unable to locate adapter class "' . $adapter . '"');
- }
- $adapter = new $adapter;
- }
-
- if (! $adapter instanceof Client\Adapter\AdapterInterface) {
- throw new Client\Exception\InvalidArgumentException('Passed adapter is not a HTTP connection adapter');
- }
-
- $this->adapter = $adapter;
- $config = $this->config;
- unset($config['adapter']);
- $this->adapter->setOptions($config);
- return $this;
- }
-
- /**
- * Load the connection adapter
- *
- * @return Client\Adapter\AdapterInterface $adapter
- */
- public function getAdapter()
- {
- if (! $this->adapter) {
- $this->setAdapter($this->config['adapter']);
- }
-
- return $this->adapter;
- }
-
- /**
- * Set request
- *
- * @param Request $request
- * @return Client
- */
- public function setRequest(Request $request)
- {
- $this->request = $request;
- return $this;
- }
-
- /**
- * Get Request
- *
- * @return Request
- */
- public function getRequest()
- {
- if (empty($this->request)) {
- $this->request = new Request();
- }
- return $this->request;
- }
-
- /**
- * Set response
- *
- * @param Response $response
- * @return Client
- */
- public function setResponse(Response $response)
- {
- $this->response = $response;
- return $this;
- }
-
- /**
- * Get Response
- *
- * @return Response
- */
- public function getResponse()
- {
- if (empty($this->response)) {
- $this->response = new Response();
- }
- return $this->response;
- }
-
-
- /**
- * Get the last request (as a string)
- *
- * @return string
- */
- public function getLastRawRequest()
- {
- return $this->lastRawRequest;
- }
-
- /**
- * Get the last response (as a string)
- *
- * @return string
- */
- public function getLastRawResponse()
- {
- return $this->lastRawResponse;
- }
-
- /**
- * Get the redirections count
- *
- * @return int
- */
- public function getRedirectionsCount()
- {
- return $this->redirectCounter;
- }
-
- /**
- * Set Uri (to the request)
- *
- * @param string|Http $uri
- * @return Client
- */
- public function setUri($uri)
- {
- if (!empty($uri)) {
- // remember host of last request
- $lastHost = $this->getRequest()->getUri()->getHost();
- $this->getRequest()->setUri($uri);
-
- // if host changed, the HTTP authentication should be cleared for security
- // reasons, see #4215 for a discussion - currently authentication is also
- // cleared for peer subdomains due to technical limits
- $nextHost = $this->getRequest()->getUri()->getHost();
- if (!preg_match('/' . preg_quote($lastHost, '/') . '$/i', $nextHost)) {
- $this->clearAuth();
- }
-
- // Set auth if username and password has been specified in the uri
- if ($this->getUri()->getUser() && $this->getUri()->getPassword()) {
- $this->setAuth($this->getUri()->getUser(), $this->getUri()->getPassword());
- }
-
- // We have no ports, set the defaults
- if (! $this->getUri()->getPort()) {
- $this->getUri()->setPort(($this->getUri()->getScheme() == 'https' ? 443 : 80));
- }
- }
- return $this;
- }
-
- /**
- * Get uri (from the request)
- *
- * @return Http
- */
- public function getUri()
- {
- return $this->getRequest()->getUri();
- }
-
- /**
- * Set the HTTP method (to the request)
- *
- * @param string $method
- * @return Client
- */
- public function setMethod($method)
- {
- $method = $this->getRequest()->setMethod($method)->getMethod();
-
- if (($method == Request::METHOD_POST || $method == Request::METHOD_PUT ||
- $method == Request::METHOD_DELETE || $method == Request::METHOD_PATCH)
- && empty($this->encType)) {
- $this->setEncType(self::ENC_URLENCODED);
- }
-
- return $this;
- }
-
- /**
- * Get the HTTP method
- *
- * @return string
- */
- public function getMethod()
- {
- return $this->getRequest()->getMethod();
- }
-
- /**
- * Set the query string argument separator
- *
- * @param string $argSeparator
- * @return Client
- */
- public function setArgSeparator($argSeparator)
- {
- $this->setOptions(array("argseparator" => $argSeparator));
- return $this;
- }
-
- /**
- * Get the query string argument separator
- *
- * @return string
- */
- public function getArgSeparator()
- {
- $argSeparator = $this->config['argseparator'];
- if (empty($argSeparator)) {
- $argSeparator = ini_get('arg_separator.output');
- $this->setArgSeparator($argSeparator);
- }
- return $argSeparator;
- }
-
- /**
- * Set the encoding type and the boundary (if any)
- *
- * @param string $encType
- * @param string $boundary
- * @return Client
- */
- public function setEncType($encType, $boundary = null)
- {
- if (!empty($encType)) {
- if (!empty($boundary)) {
- $this->encType = $encType . "; boundary={$boundary}";
- } else {
- $this->encType = $encType;
- }
- }
- return $this;
- }
-
- /**
- * Get the encoding type
- *
- * @return string
- */
- public function getEncType()
- {
- return $this->encType;
- }
-
- /**
- * Set raw body (for advanced use cases)
- *
- * @param string $body
- * @return Client
- */
- public function setRawBody($body)
- {
- $this->getRequest()->setContent($body);
- return $this;
- }
-
- /**
- * Set the POST parameters
- *
- * @param array $post
- * @return Client
- */
- public function setParameterPost(array $post)
- {
- $this->getRequest()->getPost()->fromArray($post);
- return $this;
- }
-
- /**
- * Set the GET parameters
- *
- * @param array $query
- * @return Client
- */
- public function setParameterGet(array $query)
- {
- $this->getRequest()->getQuery()->fromArray($query);
- return $this;
- }
-
- /**
- * Reset all the HTTP parameters (request, response, etc)
- *
- * @param bool $clearCookies Also clear all valid cookies? (defaults to false)
- * @param bool $clearAuth Also clear http authentication? (defaults to true)
- * @return Client
- */
- public function resetParameters($clearCookies = false /*, $clearAuth = true */)
- {
- $clearAuth = true;
- if (func_num_args() > 1) {
- $clearAuth = func_get_arg(1);
- }
-
- $uri = $this->getUri();
-
- $this->streamName = null;
- $this->encType = null;
- $this->request = null;
- $this->response = null;
- $this->lastRawRequest = null;
- $this->lastRawResponse = null;
-
- $this->setUri($uri);
-
- if ($clearCookies) {
- $this->clearCookies();
- }
-
- if ($clearAuth) {
- $this->clearAuth();
- }
-
- return $this;
- }
-
- /**
- * Return the current cookies
- *
- * @return array
- */
- public function getCookies()
- {
- return $this->cookies;
- }
-
- /**
- * Get the cookie Id (name+domain+path)
- *
- * @param Header\SetCookie|Header\Cookie $cookie
- * @return string|bool
- */
- protected function getCookieId($cookie)
- {
- if (($cookie instanceof Header\SetCookie) || ($cookie instanceof Header\Cookie)) {
- return $cookie->getName() . $cookie->getDomain() . $cookie->getPath();
- }
- return false;
- }
-
- /**
- * Add a cookie
- *
- * @param array|ArrayIterator|Header\SetCookie|string $cookie
- * @param string $value
- * @param string $expire
- * @param string $path
- * @param string $domain
- * @param bool $secure
- * @param bool $httponly
- * @param string $maxAge
- * @param string $version
- * @throws Exception\InvalidArgumentException
- * @return Client
- */
- public function addCookie($cookie, $value = null, $expire = null, $path = null, $domain = null, $secure = false, $httponly = true, $maxAge = null, $version = null)
- {
- if (is_array($cookie) || $cookie instanceof ArrayIterator) {
- foreach ($cookie as $setCookie) {
- if ($setCookie instanceof Header\SetCookie) {
- $this->cookies[$this->getCookieId($setCookie)] = $setCookie;
- } else {
- throw new Exception\InvalidArgumentException('The cookie parameter is not a valid Set-Cookie type');
- }
- }
- } elseif (is_string($cookie) && $value !== null) {
- $setCookie = new Header\SetCookie($cookie, $value, $expire, $path, $domain, $secure, $httponly, $maxAge, $version);
- $this->cookies[$this->getCookieId($setCookie)] = $setCookie;
- } elseif ($cookie instanceof Header\SetCookie) {
- $this->cookies[$this->getCookieId($cookie)] = $cookie;
- } else {
- throw new Exception\InvalidArgumentException('Invalid parameter type passed as Cookie');
- }
- return $this;
- }
-
- /**
- * Set an array of cookies
- *
- * @param array $cookies
- * @throws Exception\InvalidArgumentException
- * @return Client
- */
- public function setCookies($cookies)
- {
- if (is_array($cookies)) {
- $this->clearCookies();
- foreach ($cookies as $name => $value) {
- $this->addCookie($name, $value);
- }
- } else {
- throw new Exception\InvalidArgumentException('Invalid cookies passed as parameter, it must be an array');
- }
- return $this;
- }
-
- /**
- * Clear all the cookies
- */
- public function clearCookies()
- {
- $this->cookies = array();
- }
-
- /**
- * Set the headers (for the request)
- *
- * @param Headers|array $headers
- * @throws Exception\InvalidArgumentException
- * @return Client
- */
- public function setHeaders($headers)
- {
- if (is_array($headers)) {
- $newHeaders = new Headers();
- $newHeaders->addHeaders($headers);
- $this->getRequest()->setHeaders($newHeaders);
- } elseif ($headers instanceof Headers) {
- $this->getRequest()->setHeaders($headers);
- } else {
- throw new Exception\InvalidArgumentException('Invalid parameter headers passed');
- }
- return $this;
- }
-
- /**
- * Check if exists the header type specified
- *
- * @param string $name
- * @return bool
- */
- public function hasHeader($name)
- {
- $headers = $this->getRequest()->getHeaders();
-
- if ($headers instanceof Headers) {
- return $headers->has($name);
- }
-
- return false;
- }
-
- /**
- * Get the header value of the request
- *
- * @param string $name
- * @return string|bool
- */
- public function getHeader($name)
- {
- $headers = $this->getRequest()->getHeaders();
-
- if ($headers instanceof Headers) {
- if ($headers->get($name)) {
- return $headers->get($name)->getFieldValue();
- }
- }
- return false;
- }
-
- /**
- * Set streaming for received data
- *
- * @param string|bool $streamfile Stream file, true for temp file, false/null for no streaming
- * @return \Zend\Http\Client
- */
- public function setStream($streamfile = true)
- {
- $this->setOptions(array("outputstream" => $streamfile));
- return $this;
- }
-
- /**
- * Get status of streaming for received data
- * @return bool|string
- */
- public function getStream()
- {
- if (null !== $this->streamName) {
- return $this->streamName;
- }
-
- return $this->config['outputstream'];
- }
-
- /**
- * Create temporary stream
- *
- * @throws Exception\RuntimeException
- * @return resource
- */
- protected function openTempStream()
- {
- $this->streamName = $this->config['outputstream'];
-
- if (!is_string($this->streamName)) {
- // If name is not given, create temp name
- $this->streamName = tempnam(
- isset($this->config['streamtmpdir']) ? $this->config['streamtmpdir'] : sys_get_temp_dir(),
- 'Zend\Http\Client'
- );
- }
-
- ErrorHandler::start();
- $fp = fopen($this->streamName, "w+b");
- $error = ErrorHandler::stop();
- if (false === $fp) {
- if ($this->adapter instanceof Client\Adapter\AdapterInterface) {
- $this->adapter->close();
- }
- throw new Exception\RuntimeException("Could not open temp file {$this->streamName}", 0, $error);
- }
-
- return $fp;
- }
-
- /**
- * Create a HTTP authentication "Authorization:" header according to the
- * specified user, password and authentication method.
- *
- * @param string $user
- * @param string $password
- * @param string $type
- * @throws Exception\InvalidArgumentException
- * @return Client
- */
- public function setAuth($user, $password, $type = self::AUTH_BASIC)
- {
- if (!defined('self::AUTH_' . strtoupper($type))) {
- throw new Exception\InvalidArgumentException("Invalid or not supported authentication type: '$type'");
- }
- if (empty($user)) {
- throw new Exception\InvalidArgumentException("The username cannot be empty");
- }
-
- $this->auth = array (
- 'user' => $user,
- 'password' => $password,
- 'type' => $type
-
- );
-
- return $this;
- }
-
- /**
- * Clear http authentication
- */
- public function clearAuth()
- {
- $this->auth = array();
- }
-
- /**
- * Calculate the response value according to the HTTP authentication type
- *
- * @see http://www.faqs.org/rfcs/rfc2617.html
- * @param string $user
- * @param string $password
- * @param string $type
- * @param array $digest
- * @param null|string $entityBody
- * @throws Exception\InvalidArgumentException
- * @return string|bool
- */
- protected function calcAuthDigest($user, $password, $type = self::AUTH_BASIC, $digest = array(), $entityBody = null)
- {
- if (!defined('self::AUTH_' . strtoupper($type))) {
- throw new Exception\InvalidArgumentException("Invalid or not supported authentication type: '$type'");
- }
- $response = false;
- switch (strtolower($type)) {
- case self::AUTH_BASIC :
- // In basic authentication, the user name cannot contain ":"
- if (strpos($user, ':') !== false) {
- throw new Exception\InvalidArgumentException("The user name cannot contain ':' in Basic HTTP authentication");
- }
- $response = base64_encode($user . ':' . $password);
- break;
- case self::AUTH_DIGEST :
- if (empty($digest)) {
- throw new Exception\InvalidArgumentException("The digest cannot be empty");
- }
- foreach ($digest as $key => $value) {
- if (!defined('self::DIGEST_' . strtoupper($key))) {
- throw new Exception\InvalidArgumentException("Invalid or not supported digest authentication parameter: '$key'");
- }
- }
- $ha1 = md5($user . ':' . $digest['realm'] . ':' . $password);
- if (empty($digest['qop']) || strtolower($digest['qop']) == 'auth') {
- $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath());
- } elseif (strtolower($digest['qop']) == 'auth-int') {
- if (empty($entityBody)) {
- throw new Exception\InvalidArgumentException("I cannot use the auth-int digest authentication without the entity body");
- }
- $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath() . ':' . md5($entityBody));
- }
- if (empty($digest['qop'])) {
- $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $ha2);
- } else {
- $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $digest['nc']
- . ':' . $digest['cnonce'] . ':' . $digest['qoc'] . ':' . $ha2);
- }
- break;
- }
- return $response;
- }
-
- /**
- * Dispatch
- *
- * @param Stdlib\RequestInterface $request
- * @param Stdlib\ResponseInterface $response
- * @return Stdlib\ResponseInterface
- */
- public function dispatch(Stdlib\RequestInterface $request, Stdlib\ResponseInterface $response = null)
- {
- $response = $this->send($request);
- return $response;
- }
-
- /**
- * Send HTTP request
- *
- * @param Request $request
- * @return Response
- * @throws Exception\RuntimeException
- * @throws Client\Exception\RuntimeException
- */
- public function send(Request $request = null)
- {
- if ($request !== null) {
- $this->setRequest($request);
- }
-
- $this->redirectCounter = 0;
- $response = null;
-
- $adapter = $this->getAdapter();
-
- // Send the first request. If redirected, continue.
- do {
- // uri
- $uri = $this->getUri();
-
- // query
- $query = $this->getRequest()->getQuery();
-
- if (!empty($query)) {
- $queryArray = $query->toArray();
-
- if (!empty($queryArray)) {
- $newUri = $uri->toString();
- $queryString = http_build_query($query, null, $this->getArgSeparator());
-
- if ($this->config['rfc3986strict']) {
- $queryString = str_replace('+', '%20', $queryString);
- }
-
- if (strpos($newUri, '?') !== false) {
- $newUri .= $this->getArgSeparator() . $queryString;
- } else {
- $newUri .= '?' . $queryString;
- }
-
- $uri = new Http($newUri);
- }
- }
- // If we have no ports, set the defaults
- if (!$uri->getPort()) {
- $uri->setPort($uri->getScheme() == 'https' ? 443 : 80);
- }
-
- // method
- $method = $this->getRequest()->getMethod();
-
- // body
- $body = $this->prepareBody();
-
- // headers
- $headers = $this->prepareHeaders($body, $uri);
-
- $secure = $uri->getScheme() == 'https';
-
- // cookies
- $cookie = $this->prepareCookies($uri->getHost(), $uri->getPath(), $secure);
- if ($cookie->getFieldValue()) {
- $headers['Cookie'] = $cookie->getFieldValue();
- }
-
- // check that adapter supports streaming before using it
- if (is_resource($body) && !($adapter instanceof Client\Adapter\StreamInterface)) {
- throw new Client\Exception\RuntimeException('Adapter does not support streaming');
- }
-
- // calling protected method to allow extending classes
- // to wrap the interaction with the adapter
- $response = $this->doRequest($uri, $method, $secure, $headers, $body);
-
- if (! $response) {
- throw new Exception\RuntimeException('Unable to read response, or response is empty');
- }
-
- if ($this->config['storeresponse']) {
- $this->lastRawResponse = $response;
- } else {
- $this->lastRawResponse = null;
- }
-
- if ($this->config['outputstream']) {
- $stream = $this->getStream();
- if (!is_resource($stream) && is_string($stream)) {
- $stream = fopen($stream, 'r');
- }
- $streamMetaData = stream_get_meta_data($stream);
- if ($streamMetaData['seekable']) {
- rewind($stream);
- }
- // cleanup the adapter
- $adapter->setOutputStream(null);
- $response = Response\Stream::fromStream($response, $stream);
- $response->setStreamName($this->streamName);
- if (!is_string($this->config['outputstream'])) {
- // we used temp name, will need to clean up
- $response->setCleanup(true);
- }
- } else {
- $response = $this->getResponse()->fromString($response);
- }
-
- // Get the cookies from response (if any)
- $setCookies = $response->getCookie();
- if (!empty($setCookies)) {
- $this->addCookie($setCookies);
- }
-
- // If we got redirected, look for the Location header
- if ($response->isRedirect() && ($response->getHeaders()->has('Location'))) {
-
- // Avoid problems with buggy servers that add whitespace at the
- // end of some headers
- $location = trim($response->getHeaders()->get('Location')->getFieldValue());
-
- // Check whether we send the exact same request again, or drop the parameters
- // and send a GET request
- if ($response->getStatusCode() == 303 ||
- ((! $this->config['strictredirects']) && ($response->getStatusCode() == 302 ||
- $response->getStatusCode() == 301))) {
-
- $this->resetParameters(false, false);
- $this->setMethod(Request::METHOD_GET);
- }
-
-
- // If we got a well formed absolute URI
- if (($scheme = substr($location, 0, 6)) &&
- ($scheme == 'http:/' || $scheme == 'https:')) {
- // setURI() clears parameters if host changed, see #4215
- $this->setUri($location);
- } else {
-
- // Split into path and query and set the query
- if (strpos($location, '?') !== false) {
- list($location, $query) = explode('?', $location, 2);
- } else {
- $query = '';
- }
- $this->getUri()->setQuery($query);
-
- // Else, if we got just an absolute path, set it
- if (strpos($location, '/') === 0) {
- $this->getUri()->setPath($location);
- // Else, assume we have a relative path
- } else {
- // Get the current path directory, removing any trailing slashes
- $path = $this->getUri()->getPath();
- $path = rtrim(substr($path, 0, strrpos($path, '/')), "/");
- $this->getUri()->setPath($path . '/' . $location);
- }
- }
- ++$this->redirectCounter;
-
- } else {
- // If we didn't get any location, stop redirecting
- break;
- }
-
- } while ($this->redirectCounter <= $this->config['maxredirects']);
-
- $this->response = $response;
- return $response;
- }
-
- /**
- * Fully reset the HTTP client (auth, cookies, request, response, etc.)
- *
- * @return Client
- */
- public function reset()
- {
- $this->resetParameters();
- $this->clearAuth();
- $this->clearCookies();
-
- return $this;
- }
-
- /**
- * Set a file to upload (using a POST request)
- *
- * Can be used in two ways:
- *
- * 1. $data is null (default): $filename is treated as the name if a local file which
- * will be read and sent. Will try to guess the content type using mime_content_type().
- * 2. $data is set - $filename is sent as the file name, but $data is sent as the file
- * contents and no file is read from the file system. In this case, you need to
- * manually set the Content-Type ($ctype) or it will default to
- * application/octet-stream.
- *
- * @param string $filename Name of file to upload, or name to save as
- * @param string $formname Name of form element to send as
- * @param string $data Data to send (if null, $filename is read and sent)
- * @param string $ctype Content type to use (if $data is set and $ctype is
- * null, will be application/octet-stream)
- * @return Client
- * @throws Exception\RuntimeException
- */
- public function setFileUpload($filename, $formname, $data = null, $ctype = null)
- {
- if ($data === null) {
- ErrorHandler::start();
- $data = file_get_contents($filename);
- $error = ErrorHandler::stop();
- if ($data === false) {
- throw new Exception\RuntimeException("Unable to read file '{$filename}' for upload", 0, $error);
- }
- if (!$ctype) {
- $ctype = $this->detectFileMimeType($filename);
- }
- }
-
- $this->getRequest()->getFiles()->set($filename, array(
- 'formname' => $formname,
- 'filename' => basename($filename),
- 'ctype' => $ctype,
- 'data' => $data
- ));
-
- return $this;
- }
-
- /**
- * Remove a file to upload
- *
- * @param string $filename
- * @return bool
- */
- public function removeFileUpload($filename)
- {
- $file = $this->getRequest()->getFiles()->get($filename);
- if (!empty($file)) {
- $this->getRequest()->getFiles()->set($filename, null);
- return true;
- }
- return false;
- }
-
- /**
- * Prepare Cookies
- *
- * @param string $domain
- * @param string $path
- * @param bool $secure
- * @return Header\Cookie|bool
- */
- protected function prepareCookies($domain, $path, $secure)
- {
- $validCookies = array();
-
- if (!empty($this->cookies)) {
- foreach ($this->cookies as $id => $cookie) {
- if ($cookie->isExpired()) {
- unset($this->cookies[$id]);
- continue;
- }
-
- if ($cookie->isValidForRequest($domain, $path, $secure)) {
- // OAM hack some domains try to set the cookie multiple times
- $validCookies[$cookie->getName()] = $cookie;
- }
- }
- }
-
- $cookies = Header\Cookie::fromSetCookieArray($validCookies);
- $cookies->setEncodeValue($this->config['encodecookies']);
-
- return $cookies;
- }
-
- /**
- * Prepare the request headers
- *
- * @param resource|string $body
- * @param Http $uri
- * @throws Exception\RuntimeException
- * @return array
- */
- protected function prepareHeaders($body, $uri)
- {
- $headers = array();
-
- // Set the host header
- if ($this->config['httpversion'] == Request::VERSION_11) {
- $host = $uri->getHost();
- // If the port is not default, add it
- if (!(($uri->getScheme() == 'http' && $uri->getPort() == 80) ||
- ($uri->getScheme() == 'https' && $uri->getPort() == 443))) {
- $host .= ':' . $uri->getPort();
- }
-
- $headers['Host'] = $host;
- }
-
- // Set the connection header
- if (!$this->getRequest()->getHeaders()->has('Connection')) {
- if (!$this->config['keepalive']) {
- $headers['Connection'] = 'close';
- }
- }
-
- // Set the Accept-encoding header if not set - depending on whether
- // zlib is available or not.
- if (!$this->getRequest()->getHeaders()->has('Accept-Encoding')) {
- if (function_exists('gzinflate')) {
- $headers['Accept-Encoding'] = 'gzip, deflate';
- } else {
- $headers['Accept-Encoding'] = 'identity';
- }
- }
-
-
- // Set the user agent header
- if (!$this->getRequest()->getHeaders()->has('User-Agent') && isset($this->config['useragent'])) {
- $headers['User-Agent'] = $this->config['useragent'];
- }
-
- // Set HTTP authentication if needed
- if (!empty($this->auth)) {
- switch ($this->auth['type']) {
- case self::AUTH_BASIC :
- $auth = $this->calcAuthDigest($this->auth['user'], $this->auth['password'], $this->auth['type']);
- if ($auth !== false) {
- $headers['Authorization'] = 'Basic ' . $auth;
- }
- break;
- case self::AUTH_DIGEST :
- throw new Exception\RuntimeException("The digest authentication is not implemented yet");
- }
- }
-
- // Content-type
- $encType = $this->getEncType();
- if (!empty($encType)) {
- $headers['Content-Type'] = $encType;
- }
-
- if (!empty($body)) {
- if (is_resource($body)) {
- $fstat = fstat($body);
- $headers['Content-Length'] = $fstat['size'];
- } else {
- $headers['Content-Length'] = strlen($body);
- }
- }
-
- // Merge the headers of the request (if any)
- // here we need right 'http field' and not lowercase letters
- $requestHeaders = $this->getRequest()->getHeaders();
- foreach ($requestHeaders as $requestHeaderElement) {
- $headers[$requestHeaderElement->getFieldName()] = $requestHeaderElement->getFieldValue();
- }
- return $headers;
- }
-
-
- /**
- * Prepare the request body (for PATCH, POST and PUT requests)
- *
- * @return string
- * @throws \Zend\Http\Client\Exception\RuntimeException
- */
- protected function prepareBody()
- {
- // According to RFC2616, a TRACE request should not have a body.
- if ($this->getRequest()->isTrace()) {
- return '';
- }
-
- $rawBody = $this->getRequest()->getContent();
- if (!empty($rawBody)) {
- return $rawBody;
- }
-
- $body = '';
- $totalFiles = 0;
-
- if (!$this->getRequest()->getHeaders()->has('Content-Type')) {
- $totalFiles = count($this->getRequest()->getFiles()->toArray());
- // If we have files to upload, force encType to multipart/form-data
- if ($totalFiles > 0) {
- $this->setEncType(self::ENC_FORMDATA);
- }
- } else {
- $this->setEncType($this->getHeader('Content-Type'));
- }
-
- // If we have POST parameters or files, encode and add them to the body
- if (count($this->getRequest()->getPost()->toArray()) > 0 || $totalFiles > 0) {
- if (stripos($this->getEncType(), self::ENC_FORMDATA) === 0) {
- $boundary = '---ZENDHTTPCLIENT-' . md5(microtime());
- $this->setEncType(self::ENC_FORMDATA, $boundary);
-
- // Get POST parameters and encode them
- $params = self::flattenParametersArray($this->getRequest()->getPost()->toArray());
- foreach ($params as $pp) {
- $body .= $this->encodeFormData($boundary, $pp[0], $pp[1]);
- }
-
- // Encode files
- foreach ($this->getRequest()->getFiles()->toArray() as $file) {
- $fhead = array('Content-Type' => $file['ctype']);
- $body .= $this->encodeFormData($boundary, $file['formname'], $file['data'], $file['filename'], $fhead);
- }
- $body .= "--{$boundary}--\r\n";
- } elseif (stripos($this->getEncType(), self::ENC_URLENCODED) === 0) {
- // Encode body as application/x-www-form-urlencoded
- $body = http_build_query($this->getRequest()->getPost()->toArray());
- } else {
- throw new Client\Exception\RuntimeException("Cannot handle content type '{$this->encType}' automatically");
- }
- }
-
- return $body;
- }
-
-
- /**
- * Attempt to detect the MIME type of a file using available extensions
- *
- * This method will try to detect the MIME type of a file. If the fileinfo
- * extension is available, it will be used. If not, the mime_magic
- * extension which is deprecated but is still available in many PHP setups
- * will be tried.
- *
- * If neither extension is available, the default application/octet-stream
- * MIME type will be returned
- *
- * @param string $file File path
- * @return string MIME type
- */
- protected function detectFileMimeType($file)
- {
- $type = null;
-
- // First try with fileinfo functions
- if (function_exists('finfo_open')) {
- if (static::$fileInfoDb === null) {
- ErrorHandler::start();
- static::$fileInfoDb = finfo_open(FILEINFO_MIME);
- ErrorHandler::stop();
- }
-
- if (static::$fileInfoDb) {
- $type = finfo_file(static::$fileInfoDb, $file);
- }
-
- } elseif (function_exists('mime_content_type')) {
- $type = mime_content_type($file);
- }
-
- // Fallback to the default application/octet-stream
- if (! $type) {
- $type = 'application/octet-stream';
- }
-
- return $type;
- }
-
- /**
- * Encode data to a multipart/form-data part suitable for a POST request.
- *
- * @param string $boundary
- * @param string $name
- * @param mixed $value
- * @param string $filename
- * @param array $headers Associative array of optional headers @example ("Content-Transfer-Encoding" => "binary")
- * @return string
- */
- public function encodeFormData($boundary, $name, $value, $filename = null, $headers = array())
- {
- $ret = "--{$boundary}\r\n" .
- 'Content-Disposition: form-data; name="' . $name . '"';
-
- if ($filename) {
- $ret .= '; filename="' . $filename . '"';
- }
- $ret .= "\r\n";
-
- foreach ($headers as $hname => $hvalue) {
- $ret .= "{$hname}: {$hvalue}\r\n";
- }
- $ret .= "\r\n";
- $ret .= "{$value}\r\n";
-
- return $ret;
- }
-
- /**
- * Convert an array of parameters into a flat array of (key, value) pairs
- *
- * Will flatten a potentially multi-dimentional array of parameters (such
- * as POST parameters) into a flat array of (key, value) paris. In case
- * of multi-dimentional arrays, square brackets ([]) will be added to the
- * key to indicate an array.
- *
- * @since 1.9
- *
- * @param array $parray
- * @param string $prefix
- * @return array
- */
- protected function flattenParametersArray($parray, $prefix = null)
- {
- if (!is_array($parray)) {
- return $parray;
- }
-
- $parameters = array();
-
- foreach ($parray as $name => $value) {
- // Calculate array key
- if ($prefix) {
- if (is_int($name)) {
- $key = $prefix . '[]';
- } else {
- $key = $prefix . "[$name]";
- }
- } else {
- $key = $name;
- }
-
- if (is_array($value)) {
- $parameters = array_merge($parameters, $this->flattenParametersArray($value, $key));
-
- } else {
- $parameters[] = array($key, $value);
- }
- }
-
- return $parameters;
- }
-
- /**
- * Separating this from send method allows subclasses to wrap
- * the interaction with the adapter
- *
- * @param Http $uri
- * @param string $method
- * @param bool $secure
- * @param array $headers
- * @param string $body
- * @return string the raw response
- * @throws Exception\RuntimeException
- */
- protected function doRequest(Http $uri, $method, $secure = false, $headers = array(), $body = '')
- {
- // Open the connection, send the request and read the response
- $this->adapter->connect($uri->getHost(), $uri->getPort(), $secure);
-
- if ($this->config['outputstream']) {
- if ($this->adapter instanceof Client\Adapter\StreamInterface) {
- $stream = $this->openTempStream();
- $this->adapter->setOutputStream($stream);
- } else {
- throw new Exception\RuntimeException('Adapter does not support streaming');
- }
- }
- // HTTP connection
- $this->lastRawRequest = $this->adapter->write($method,
- $uri, $this->config['httpversion'], $headers, $body);
-
- return $this->adapter->read();
- }
-
- /**
- * Create a HTTP authentication "Authorization:" header according to the
- * specified user, password and authentication method.
- *
- * @see http://www.faqs.org/rfcs/rfc2617.html
- * @param string $user
- * @param string $password
- * @param string $type
- * @return string
- * @throws Client\Exception\InvalidArgumentException
- */
- public static function encodeAuthHeader($user, $password, $type = self::AUTH_BASIC)
- {
- $authHeader = null;
-
- switch ($type) {
- case self::AUTH_BASIC:
- // In basic authentication, the user name cannot contain ":"
- if (strpos($user, ':') !== false) {
- throw new Client\Exception\InvalidArgumentException("The user name cannot contain ':' in 'Basic' HTTP authentication");
- }
-
- $authHeader = 'Basic ' . base64_encode($user . ':' . $password);
- break;
-
- //case self::AUTH_DIGEST:
- /**
- * @todo Implement digest authentication
- */
- // break;
-
- default:
- throw new Client\Exception\InvalidArgumentException("Not a supported HTTP authentication type: '$type'");
-
- }
- return $authHeader;
- }
- }