diff options
| author | Horus3 | 2014-02-24 16:42:14 +0100 |
|---|---|---|
| committer | Horus3 | 2014-02-24 16:42:14 +0100 |
| commit | 06f945f27840b53e57795dadbc38e76f7e11ab1c (patch) | |
| tree | 689d5c7f4ffa15460c7e90f47c6a7dd59ce4e8bd /zend/library/Zend/Http | |
| download | random-06f945f27840b53e57795dadbc38e76f7e11ab1c.tar.gz | |
init
Diffstat (limited to 'zend/library/Zend/Http')
| -rw-r--r-- | zend/library/Zend/Http/Client.php | 1564 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Client/Adapter/Curl.php | 510 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Client/Adapter/Exception.php | 38 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Client/Adapter/Interface.php | 78 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Client/Adapter/Proxy.php | 343 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Client/Adapter/Socket.php | 543 | ||||
| -rwxr-xr-x | zend/library/Zend/Http/Client/Adapter/Stream.php | 46 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Client/Adapter/Test.php | 248 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Client/Exception.php | 36 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Exception.php | 36 | ||||
| -rw-r--r-- | zend/library/Zend/Http/Response.php | 667 | ||||
| -rwxr-xr-x | zend/library/Zend/Http/Response/Stream.php | 235 |
12 files changed, 4344 insertions, 0 deletions
diff --git a/zend/library/Zend/Http/Client.php b/zend/library/Zend/Http/Client.php new file mode 100644 index 0000000..8f3c13f --- /dev/null +++ b/zend/library/Zend/Http/Client.php @@ -0,0 +1,1564 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client + * @version $Id: Client.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * @see Zend_Loader + */ +require_once 'Zend/Loader.php'; + + +/** + * @see Zend_Uri + */ +require_once 'Zend/Uri.php'; + + +/** + * @see Zend_Http_Client_Adapter_Interface + */ +require_once 'Zend/Http/Client/Adapter/Interface.php'; + + +/** + * @see Zend_Http_Response + */ +require_once 'Zend/Http/Response.php'; + +/** + * @see Zend_Http_Response_Stream + */ +require_once 'Zend/Http/Response/Stream.php'; + +/** + * Zend_Http_Client is an implementation of an HTTP client in PHP. The client + * supports basic features like sending different HTTP requests and handling + * redirections, as well as more advanced features like proxy settings, HTTP + * authentication and cookie persistence (using a Zend_Http_CookieJar object) + * + * @todo Implement proxy settings + * @category Zend + * @package Zend_Http + * @subpackage Client + * @throws Zend_Http_Client_Exception + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Client +{ + /** + * HTTP request methods + */ + const GET = 'GET'; + const POST = 'POST'; + const PUT = 'PUT'; + const HEAD = 'HEAD'; + const DELETE = 'DELETE'; + const TRACE = 'TRACE'; + const OPTIONS = 'OPTIONS'; + const CONNECT = 'CONNECT'; + const MERGE = 'MERGE'; + + /** + * Supported HTTP Authentication methods + */ + const AUTH_BASIC = 'basic'; + //const AUTH_DIGEST = 'digest'; <-- not implemented yet + + /** + * HTTP protocol versions + */ + const HTTP_1 = '1.1'; + const HTTP_0 = '1.0'; + + /** + * Content attributes + */ + const CONTENT_TYPE = 'Content-Type'; + const CONTENT_LENGTH = 'Content-Length'; + + /** + * POST data encoding methods + */ + const ENC_URLENCODED = 'application/x-www-form-urlencoded'; + const ENC_FORMDATA = 'multipart/form-data'; + + /** + * Value types for Body key/value pairs + */ + const VTYPE_SCALAR = 'SCALAR'; + const VTYPE_FILE = 'FILE'; + + /** + * Configuration array, set using the constructor or using ::setConfig() + * + * @var array + */ + protected $config = array( + 'maxredirects' => 5, + 'strictredirects' => false, + 'useragent' => 'Zend_Http_Client', + 'timeout' => 10, + 'adapter' => 'Zend_Http_Client_Adapter_Socket', + 'httpversion' => self::HTTP_1, + 'keepalive' => false, + 'storeresponse' => true, + 'strict' => true, + 'output_stream' => false, + 'encodecookies' => true, + 'rfc3986_strict' => false + ); + + /** + * The adapter used to perform the actual connection to the server + * + * @var Zend_Http_Client_Adapter_Interface + */ + protected $adapter = null; + + /** + * Request URI + * + * @var Zend_Uri_Http + */ + protected $uri = null; + + /** + * Associative array of request headers + * + * @var array + */ + protected $headers = array(); + + /** + * HTTP request method + * + * @var string + */ + protected $method = self::GET; + + /** + * Associative array of GET parameters + * + * @var array + */ + protected $paramsGet = array(); + + /** + * Associative array of POST parameters + * + * @var array + */ + protected $paramsPost = array(); + + /** + * Request body content type (for POST requests) + * + * @var string + */ + protected $enctype = null; + + /** + * The raw post data to send. Could be set by setRawData($data, $enctype). + * + * @var string + */ + protected $raw_post_data = null; + + /** + * HTTP Authentication settings + * + * Expected to be an associative array with this structure: + * $this->auth = array('user' => 'username', 'password' => 'password', 'type' => 'basic') + * Where 'type' should be one of the supported authentication types (see the AUTH_* + * constants), for example 'basic' or 'digest'. + * + * If null, no authentication will be used. + * + * @var array|null + */ + protected $auth; + + /** + * File upload arrays (used in POST requests) + * + * An associative array, where each element is of the format: + * 'name' => array('filename.txt', 'text/plain', 'This is the actual file contents') + * + * @var array + */ + protected $files = array(); + + /** + * Ordered list of keys from key/value pair data to include in body + * + * An associative array, where each element is of the format: + * '<field name>' => VTYPE_SCALAR | VTYPE_FILE + * + * @var array + */ + protected $body_field_order = array(); + + /** + * The client's cookie jar + * + * @var Zend_Http_CookieJar + */ + protected $cookiejar = null; + + /** + * The last HTTP request sent by the client, as string + * + * @var string + */ + protected $last_request = null; + + /** + * The last HTTP response received by the client + * + * @var Zend_Http_Response + */ + protected $last_response = null; + + /** + * Redirection counter + * + * @var int + */ + protected $redirectCounter = 0; + + /** + * Status for unmasking GET array params + * + * @var boolean + */ + protected $_unmaskStatus = false; + + /** + * Status if the http_build_query function escapes brackets + * + * @var boolean + */ + protected $_queryBracketsEscaped = true; + + /** + * 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 + */ + static protected $_fileInfoDb = null; + + /** + * Constructor method. Will create a new HTTP client. Accepts the target + * URL and optionally configuration array. + * + * @param Zend_Uri_Http|string $uri + * @param array $config Configuration key-value pairs. + */ + public function __construct($uri = null, $config = null) + { + if ($uri !== null) { + $this->setUri($uri); + } + if ($config !== null) { + $this->setConfig($config); + } + + $this->_queryBracketsEscaped = version_compare(phpversion(), '5.1.3', '>='); + } + + /** + * Set the URI for the next request + * + * @param Zend_Uri_Http|string $uri + * @return Zend_Http_Client + * @throws Zend_Http_Client_Exception + */ + public function setUri($uri) + { + if ($uri instanceof Zend_Uri_Http) { + // clone the URI in order to keep the passed parameter constant + $uri = clone $uri; + } elseif (is_string($uri)) { + $uri = Zend_Uri::factory($uri); + } + + if (!$uri instanceof Zend_Uri_Http) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception('Passed parameter is not a valid HTTP URI.'); + } + + // Set auth if username and password has been specified in the uri + if ($uri->getUsername() && $uri->getPassword()) { + $this->setAuth($uri->getUsername(), $uri->getPassword()); + } + + // We have no ports, set the defaults + if (! $uri->getPort()) { + $uri->setPort(($uri->getScheme() == 'https' ? 443 : 80)); + } + + $this->uri = $uri; + + return $this; + } + + /** + * Get the URI for the next request + * + * @param boolean $as_string If true, will return the URI as a string + * @return Zend_Uri_Http|string + */ + public function getUri($as_string = false) + { + if ($as_string && $this->uri instanceof Zend_Uri_Http) { + return $this->uri->__toString(); + } else { + return $this->uri; + } + } + + /** + * Set configuration parameters for this HTTP client + * + * @param Zend_Config | array $config + * @return Zend_Http_Client + * @throws Zend_Http_Client_Exception + */ + public function setConfig($config = array()) + { + if ($config instanceof Zend_Config) { + $config = $config->toArray(); + + } elseif (! is_array($config)) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception('Array or Zend_Config object expected, got ' . gettype($config)); + } + + foreach ($config as $k => $v) { + $this->config[strtolower($k)] = $v; + } + + // Pass configuration options to the adapter if it exists + if ($this->adapter instanceof Zend_Http_Client_Adapter_Interface) { + $this->adapter->setConfig($config); + } + + return $this; + } + + /** + * Set the next request's method + * + * Validated the passed method and sets it. If we have files set for + * POST requests, and the new method is not POST, the files are silently + * dropped. + * + * @param string $method + * @return Zend_Http_Client + * @throws Zend_Http_Client_Exception + */ + public function setMethod($method = self::GET) + { + if (! preg_match('/^[^\x00-\x1f\x7f-\xff\(\)<>@,;:\\\\"\/\[\]\?={}\s]+$/', $method)) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("'{$method}' is not a valid HTTP request method."); + } + + if (($method == self::POST || $method == self::PUT || $method == self::DELETE) && $this->enctype === null) { + $this->setEncType(self::ENC_URLENCODED); + } + + $this->method = $method; + + return $this; + } + + /** + * Set one or more request headers + * + * This function can be used in several ways to set the client's request + * headers: + * 1. By providing two parameters: $name as the header to set (e.g. 'Host') + * and $value as it's value (e.g. 'www.example.com'). + * 2. By providing a single header string as the only parameter + * e.g. 'Host: www.example.com' + * 3. By providing an array of headers as the first parameter + * e.g. array('host' => 'www.example.com', 'x-foo: bar'). In This case + * the function will call itself recursively for each array item. + * + * @param string|array $name Header name, full header string ('Header: value') + * or an array of headers + * @param mixed $value Header value or null + * @return Zend_Http_Client + * @throws Zend_Http_Client_Exception + */ + public function setHeaders($name, $value = null) + { + // If we got an array, go recursive! + if (is_array($name)) { + foreach ($name as $k => $v) { + if (is_string($k)) { + $this->setHeaders($k, $v); + } else { + $this->setHeaders($v, null); + } + } + } else { + // Check if $name needs to be split + if ($value === null && (strpos($name, ':') > 0)) { + list($name, $value) = explode(':', $name, 2); + } + + // Make sure the name is valid if we are in strict mode + if ($this->config['strict'] && (! preg_match('/^[a-zA-Z0-9-]+$/', $name))) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("{$name} is not a valid HTTP header name"); + } + + $normalized_name = strtolower($name); + + // If $value is null or false, unset the header + if ($value === null || $value === false) { + unset($this->headers[$normalized_name]); + + // Else, set the header + } else { + // Header names are stored lowercase internally. + if (is_string($value)) { + $value = trim($value); + } + $this->headers[$normalized_name] = array($name, $value); + } + } + + return $this; + } + + /** + * Get the value of a specific header + * + * Note that if the header has more than one value, an array + * will be returned. + * + * @param string $key + * @return string|array|null The header value or null if it is not set + */ + public function getHeader($key) + { + $key = strtolower($key); + if (isset($this->headers[$key])) { + return $this->headers[$key][1]; + } else { + return null; + } + } + + /** + * Set a GET parameter for the request. Wrapper around _setParameter + * + * @param string|array $name + * @param string $value + * @return Zend_Http_Client + */ + public function setParameterGet($name, $value = null) + { + if (is_array($name)) { + foreach ($name as $k => $v) + $this->_setParameter('GET', $k, $v); + } else { + $this->_setParameter('GET', $name, $value); + } + + return $this; + } + + /** + * Set a POST parameter for the request. Wrapper around _setParameter + * + * @param string|array $name + * @param string $value + * @return Zend_Http_Client + */ + public function setParameterPost($name, $value = null) + { + if (is_array($name)) { + foreach ($name as $k => $v) + $this->_setParameter('POST', $k, $v); + } else { + $this->_setParameter('POST', $name, $value); + } + + return $this; + } + + /** + * Set a GET or POST parameter - used by SetParameterGet and SetParameterPost + * + * @param string $type GET or POST + * @param string $name + * @param string $value + * @return null + */ + protected function _setParameter($type, $name, $value) + { + $parray = array(); + $type = strtolower($type); + switch ($type) { + case 'get': + $parray = &$this->paramsGet; + break; + case 'post': + $parray = &$this->paramsPost; + if ( $value === null ) { + if (isset($this->body_field_order[$name])) + unset($this->body_field_order[$name]); + } else { + $this->body_field_order[$name] = self::VTYPE_SCALAR; + } + break; + } + + if ($value === null) { + if (isset($parray[$name])) unset($parray[$name]); + } else { + $parray[$name] = $value; + } + } + + /** + * Get the number of redirections done on the last request + * + * @return int + */ + public function getRedirectionsCount() + { + return $this->redirectCounter; + } + + /** + * Set HTTP authentication parameters + * + * $type should be one of the supported types - see the self::AUTH_* + * constants. + * + * To enable authentication: + * <code> + * $this->setAuth('shahar', 'secret', Zend_Http_Client::AUTH_BASIC); + * </code> + * + * To disable authentication: + * <code> + * $this->setAuth(false); + * </code> + * + * @see http://www.faqs.org/rfcs/rfc2617.html + * @param string|false $user User name or false disable authentication + * @param string $password Password + * @param string $type Authentication type + * @return Zend_Http_Client + * @throws Zend_Http_Client_Exception + */ + public function setAuth($user, $password = '', $type = self::AUTH_BASIC) + { + // If we got false or null, disable authentication + if ($user === false || $user === null) { + $this->auth = null; + + // Clear the auth information in the uri instance as well + if ($this->uri instanceof Zend_Uri_Http) { + $this->getUri()->setUsername(''); + $this->getUri()->setPassword(''); + } + // Else, set up authentication + } else { + // Check we got a proper authentication type + if (! defined('self::AUTH_' . strtoupper($type))) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("Invalid or not supported authentication type: '$type'"); + } + + $this->auth = array( + 'user' => (string) $user, + 'password' => (string) $password, + 'type' => $type + ); + } + + return $this; + } + + /** + * Set the HTTP client's cookie jar. + * + * A cookie jar is an object that holds and maintains cookies across HTTP requests + * and responses. + * + * @param Zend_Http_CookieJar|boolean $cookiejar Existing cookiejar object, true to create a new one, false to disable + * @return Zend_Http_Client + * @throws Zend_Http_Client_Exception + */ + public function setCookieJar($cookiejar = true) + { + Zend_Loader::loadClass('Zend_Http_CookieJar'); + + if ($cookiejar instanceof Zend_Http_CookieJar) { + $this->cookiejar = $cookiejar; + } elseif ($cookiejar === true) { + $this->cookiejar = new Zend_Http_CookieJar(); + } elseif (! $cookiejar) { + $this->cookiejar = null; + } else { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception('Invalid parameter type passed as CookieJar'); + } + + return $this; + } + + /** + * Return the current cookie jar or null if none. + * + * @return Zend_Http_CookieJar|null + */ + public function getCookieJar() + { + return $this->cookiejar; + } + + /** + * Add a cookie to the request. If the client has no Cookie Jar, the cookies + * will be added directly to the headers array as "Cookie" headers. + * + * @param Zend_Http_Cookie|string $cookie + * @param string|null $value If "cookie" is a string, this is the cookie value. + * @return Zend_Http_Client + * @throws Zend_Http_Client_Exception + */ + public function setCookie($cookie, $value = null) + { + Zend_Loader::loadClass('Zend_Http_Cookie'); + + if (is_array($cookie)) { + foreach ($cookie as $c => $v) { + if (is_string($c)) { + $this->setCookie($c, $v); + } else { + $this->setCookie($v); + } + } + + return $this; + } + + if ($value !== null && $this->config['encodecookies']) { + $value = urlencode($value); + } + + if (isset($this->cookiejar)) { + if ($cookie instanceof Zend_Http_Cookie) { + $this->cookiejar->addCookie($cookie); + } elseif (is_string($cookie) && $value !== null) { + $cookie = Zend_Http_Cookie::fromString("{$cookie}={$value}", + $this->uri, + $this->config['encodecookies']); + $this->cookiejar->addCookie($cookie); + } + } else { + if ($cookie instanceof Zend_Http_Cookie) { + $name = $cookie->getName(); + $value = $cookie->getValue(); + $cookie = $name; + } + + if (preg_match("/[=,; \t\r\n\013\014]/", $cookie)) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("Cookie name cannot contain these characters: =,; \t\r\n\013\014 ({$cookie})"); + } + + $value = addslashes($value); + + if (! isset($this->headers['cookie'])) { + $this->headers['cookie'] = array('Cookie', ''); + } + $this->headers['cookie'][1] .= $cookie . '=' . $value . '; '; + } + + 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 Zend_Http_Client + * @throws Zend_Http_Client_Exception + */ + public function setFileUpload($filename, $formname, $data = null, $ctype = null) + { + if ($data === null) { + if (($data = @file_get_contents($filename)) === false) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("Unable to read file '{$filename}' for upload"); + } + + if (! $ctype) { + $ctype = $this->_detectFileMimeType($filename); + } + } + + // Force enctype to multipart/form-data + $this->setEncType(self::ENC_FORMDATA); + + $this->files[] = array( + 'formname' => $formname, + 'filename' => basename($filename), + 'ctype' => $ctype, + 'data' => $data + ); + + $this->body_field_order[$formname] = self::VTYPE_FILE; + + return $this; + } + + /** + * Set the encoding type for POST data + * + * @param string $enctype + * @return Zend_Http_Client + */ + public function setEncType($enctype = self::ENC_URLENCODED) + { + $this->enctype = $enctype; + + return $this; + } + + /** + * Set the raw (already encoded) POST data. + * + * This function is here for two reasons: + * 1. For advanced user who would like to set their own data, already encoded + * 2. For backwards compatibilty: If someone uses the old post($data) method. + * this method will be used to set the encoded data. + * + * $data can also be stream (such as file) from which the data will be read. + * + * @param string|resource $data + * @param string $enctype + * @return Zend_Http_Client + */ + public function setRawData($data, $enctype = null) + { + $this->raw_post_data = $data; + $this->setEncType($enctype); + if (is_resource($data)) { + // We've got stream data + $stat = @fstat($data); + if($stat) { + $this->setHeaders(self::CONTENT_LENGTH, $stat['size']); + } + } + return $this; + } + + /** + * Set the unmask feature for GET parameters as array + * + * Example: + * foo%5B0%5D=a&foo%5B1%5D=b + * becomes + * foo=a&foo=b + * + * This is usefull for some services + * + * @param boolean $status + * @return Zend_Http_Client + */ + public function setUnmaskStatus($status = true) + { + $this->_unmaskStatus = (BOOL)$status; + return $this; + } + + /** + * Returns the currently configured unmask status + * + * @return boolean + */ + public function getUnmaskStatus() + { + return $this->_unmaskStatus; + } + + /** + * Clear all GET and POST parameters + * + * Should be used to reset the request parameters if the client is + * used for several concurrent requests. + * + * clearAll parameter controls if we clean just parameters or also + * headers and last_* + * + * @param bool $clearAll Should all data be cleared? + * @return Zend_Http_Client + */ + public function resetParameters($clearAll = false) + { + // Reset parameter data + $this->paramsGet = array(); + $this->paramsPost = array(); + $this->files = array(); + $this->raw_post_data = null; + $this->enctype = null; + + if($clearAll) { + $this->headers = array(); + $this->last_request = null; + $this->last_response = null; + } else { + // Clear outdated headers + if (isset($this->headers[strtolower(self::CONTENT_TYPE)])) { + unset($this->headers[strtolower(self::CONTENT_TYPE)]); + } + if (isset($this->headers[strtolower(self::CONTENT_LENGTH)])) { + unset($this->headers[strtolower(self::CONTENT_LENGTH)]); + } + } + + return $this; + } + + /** + * Get the last HTTP request as string + * + * @return string + */ + public function getLastRequest() + { + return $this->last_request; + } + + /** + * Get the last HTTP response received by this client + * + * If $config['storeresponse'] is set to false, or no response was + * stored yet, will return null + * + * @return Zend_Http_Response or null if none + */ + public function getLastResponse() + { + return $this->last_response; + } + + /** + * Load the connection adapter + * + * While this method is not called more than one for a client, it is + * seperated from ->request() to preserve logic and readability + * + * @param Zend_Http_Client_Adapter_Interface|string $adapter + * @return null + * @throws Zend_Http_Client_Exception + */ + public function setAdapter($adapter) + { + if (is_string($adapter)) { + try { + Zend_Loader::loadClass($adapter); + } catch (Zend_Exception $e) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("Unable to load adapter '$adapter': {$e->getMessage()}", 0, $e); + } + + $adapter = new $adapter; + } + + if (! $adapter instanceof Zend_Http_Client_Adapter_Interface) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception('Passed adapter is not a HTTP connection adapter'); + } + + $this->adapter = $adapter; + $config = $this->config; + unset($config['adapter']); + $this->adapter->setConfig($config); + } + + /** + * Load the connection adapter + * + * @return Zend_Http_Client_Adapter_Interface $adapter + */ + public function getAdapter() + { + if (null === $this->adapter) { + $this->setAdapter($this->config['adapter']); + } + + return $this->adapter; + } + + /** + * Set streaming for received data + * + * @param string|boolean $streamfile Stream file, true for temp file, false/null for no streaming + * @return Zend_Http_Client + */ + public function setStream($streamfile = true) + { + $this->setConfig(array("output_stream" => $streamfile)); + return $this; + } + + /** + * Get status of streaming for received data + * @return boolean|string + */ + public function getStream() + { + return $this->config["output_stream"]; + } + + /** + * Create temporary stream + * + * @return resource + */ + protected function _openTempStream() + { + $this->_stream_name = $this->config['output_stream']; + if(!is_string($this->_stream_name)) { + // If name is not given, create temp name + $this->_stream_name = tempnam(isset($this->config['stream_tmp_dir'])?$this->config['stream_tmp_dir']:sys_get_temp_dir(), + 'Zend_Http_Client'); + } + + if (false === ($fp = @fopen($this->_stream_name, "w+b"))) { + if ($this->adapter instanceof Zend_Http_Client_Adapter_Interface) { + $this->adapter->close(); + } + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("Could not open temp file {$this->_stream_name}"); + } + + return $fp; + } + + /** + * Send the HTTP request and return an HTTP response object + * + * @param string $method + * @return Zend_Http_Response + * @throws Zend_Http_Client_Exception + */ + public function request($method = null) + { + if (! $this->uri instanceof Zend_Uri_Http) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception('No valid URI has been passed to the client'); + } + + if ($method) { + $this->setMethod($method); + } + $this->redirectCounter = 0; + $response = null; + + // Make sure the adapter is loaded + if ($this->adapter == null) { + $this->setAdapter($this->config['adapter']); + } + + // Send the first request. If redirected, continue. + do { + // Clone the URI and add the additional GET parameters to it + $uri = clone $this->uri; + if (! empty($this->paramsGet)) { + $query = $uri->getQuery(); + if (! empty($query)) { + $query .= '&'; + } + $query .= http_build_query($this->paramsGet, null, '&'); + if ($this->config['rfc3986_strict']) { + $query = str_replace('+', '%20', $query); + } + + // @see ZF-11671 to unmask for some services to foo=val1&foo=val2 + if ($this->getUnmaskStatus()) { + if ($this->_queryBracketsEscaped) { + $query = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', $query); + } else { + $query = preg_replace('/\\[(?:[0-9]|[1-9][0-9]+)\\]=/', '=', $query); + } + } + + $uri->setQuery($query); + } + + $body = $this->_prepareBody(); + $headers = $this->_prepareHeaders(); + + // check that adapter supports streaming before using it + if(is_resource($body) && !($this->adapter instanceof Zend_Http_Client_Adapter_Stream)) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception('Adapter does not support streaming'); + } + + // Open the connection, send the request and read the response + $this->adapter->connect($uri->getHost(), $uri->getPort(), + ($uri->getScheme() == 'https' ? true : false)); + + if($this->config['output_stream']) { + if($this->adapter instanceof Zend_Http_Client_Adapter_Stream) { + $stream = $this->_openTempStream(); + $this->adapter->setOutputStream($stream); + } else { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception('Adapter does not support streaming'); + } + } + + $this->last_request = $this->adapter->write($this->method, + $uri, $this->config['httpversion'], $headers, $body); + + $response = $this->adapter->read(); + if (! $response) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception('Unable to read response, or response is empty'); + } + + if($this->config['output_stream']) { + $streamMetaData = stream_get_meta_data($stream); + if ($streamMetaData['seekable']) { + rewind($stream); + } + // cleanup the adapter + $this->adapter->setOutputStream(null); + $response = Zend_Http_Response_Stream::fromStream($response, $stream); + $response->setStreamName($this->_stream_name); + if(!is_string($this->config['output_stream'])) { + // we used temp name, will need to clean up + $response->setCleanup(true); + } + } else { + $response = Zend_Http_Response::fromString($response); + } + + if ($this->config['storeresponse']) { + $this->last_response = $response; + } + + // Load cookies into cookie jar + if (isset($this->cookiejar)) { + $this->cookiejar->addCookiesFromResponse($response, $uri, $this->config['encodecookies']); + } + + // If we got redirected, look for the Location header + if ($response->isRedirect() && ($location = $response->getHeader('location'))) { + + // Avoid problems with buggy servers that add whitespace at the + // end of some headers (See ZF-11283) + $location = trim($location); + + // Check whether we send the exact same request again, or drop the parameters + // and send a GET request + if ($response->getStatus() == 303 || + ((! $this->config['strictredirects']) && ($response->getStatus() == 302 || + $response->getStatus() == 301))) { + + $this->resetParameters(); + $this->setMethod(self::GET); + } + + // If we got a well formed absolute URI + if (($scheme = substr($location, 0, 6)) && ($scheme == 'http:/' || $scheme == 'https:')) { + $this->setHeaders('host', null); + $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->uri->setQuery($query); + + // Else, if we got just an absolute path, set it + if(strpos($location, '/') === 0) { + $this->uri->setPath($location); + + // Else, assume we have a relative path + } else { + // Get the current path directory, removing any trailing slashes + $path = $this->uri->getPath(); + $path = rtrim(substr($path, 0, strrpos($path, '/')), "/"); + $this->uri->setPath($path . '/' . $location); + } + } + ++$this->redirectCounter; + + } else { + // If we didn't get any location, stop redirecting + break; + } + + } while ($this->redirectCounter < $this->config['maxredirects']); + + return $response; + } + + /** + * Prepare the request headers + * + * @return array + */ + protected function _prepareHeaders() + { + $headers = array(); + + // Set the host header + if (! isset($this->headers['host'])) { + $host = $this->uri->getHost(); + + // If the port is not default, add it + if (! (($this->uri->getScheme() == 'http' && $this->uri->getPort() == 80) || + ($this->uri->getScheme() == 'https' && $this->uri->getPort() == 443))) { + $host .= ':' . $this->uri->getPort(); + } + + $headers[] = "Host: {$host}"; + } + + // Set the connection header + if (! isset($this->headers['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 (! isset($this->headers['accept-encoding'])) { + if (function_exists('gzinflate')) { + $headers[] = 'Accept-encoding: gzip, deflate'; + } else { + $headers[] = 'Accept-encoding: identity'; + } + } + + // Set the Content-Type header + if (($this->method == self::POST || $this->method == self::PUT) && + (! isset($this->headers[strtolower(self::CONTENT_TYPE)]) && isset($this->enctype))) { + + $headers[] = self::CONTENT_TYPE . ': ' . $this->enctype; + } + + // Set the user agent header + if (! isset($this->headers['user-agent']) && isset($this->config['useragent'])) { + $headers[] = "User-Agent: {$this->config['useragent']}"; + } + + // Set HTTP authentication if needed + if (is_array($this->auth)) { + $auth = self::encodeAuthHeader($this->auth['user'], $this->auth['password'], $this->auth['type']); + $headers[] = "Authorization: {$auth}"; + } + + // Load cookies from cookie jar + if (isset($this->cookiejar)) { + $cookstr = $this->cookiejar->getMatchingCookies($this->uri, + true, Zend_Http_CookieJar::COOKIE_STRING_CONCAT); + + if ($cookstr) { + $headers[] = "Cookie: {$cookstr}"; + } + } + + // Add all other user defined headers + foreach ($this->headers as $header) { + list($name, $value) = $header; + if (is_array($value)) { + $value = implode(', ', $value); + } + + $headers[] = "$name: $value"; + } + + return $headers; + } + + /** + * Prepare the request body (for POST and PUT requests) + * + * @return string + * @throws Zend_Http_Client_Exception + */ + protected function _prepareBody() + { + // According to RFC2616, a TRACE request should not have a body. + if ($this->method == self::TRACE) { + return ''; + } + + if (isset($this->raw_post_data) && is_resource($this->raw_post_data)) { + return $this->raw_post_data; + } + // If mbstring overloads substr and strlen functions, we have to + // override it's internal encoding + if (function_exists('mb_internal_encoding') && + ((int) ini_get('mbstring.func_overload')) & 2) { + + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } + + // If we have raw_post_data set, just use it as the body. + if (isset($this->raw_post_data)) { + $this->setHeaders(self::CONTENT_LENGTH, strlen($this->raw_post_data)); + if (isset($mbIntEnc)) { + mb_internal_encoding($mbIntEnc); + } + + return $this->raw_post_data; + } + + $body = ''; + + // If we have files to upload, force enctype to multipart/form-data + if (count ($this->files) > 0) { + $this->setEncType(self::ENC_FORMDATA); + } + + // If we have POST parameters or files, encode and add them to the body + if (count($this->paramsPost) > 0 || count($this->files) > 0) { + switch($this->enctype) { + case self::ENC_FORMDATA: + // Encode body as multipart/form-data + $boundary = '---ZENDHTTPCLIENT-' . md5(microtime()); + $this->setHeaders(self::CONTENT_TYPE, self::ENC_FORMDATA . "; boundary={$boundary}"); + + // Encode all files and POST vars in the order they were given + foreach ($this->body_field_order as $fieldName=>$fieldType) { + switch ($fieldType) { + case self::VTYPE_FILE: + foreach ($this->files as $file) { + if ($file['formname']===$fieldName) { + $fhead = array(self::CONTENT_TYPE => $file['ctype']); + $body .= self::encodeFormData($boundary, $file['formname'], $file['data'], $file['filename'], $fhead); + } + } + break; + case self::VTYPE_SCALAR: + if (isset($this->paramsPost[$fieldName])) { + if (is_array($this->paramsPost[$fieldName])) { + $flattened = self::_flattenParametersArray($this->paramsPost[$fieldName], $fieldName); + foreach ($flattened as $pp) { + $body .= self::encodeFormData($boundary, $pp[0], $pp[1]); + } + } else { + $body .= self::encodeFormData($boundary, $fieldName, $this->paramsPost[$fieldName]); + } + } + break; + } + } + + $body .= "--{$boundary}--\r\n"; + break; + + case self::ENC_URLENCODED: + // Encode body as application/x-www-form-urlencoded + $this->setHeaders(self::CONTENT_TYPE, self::ENC_URLENCODED); + $body = http_build_query($this->paramsPost, '', '&'); + break; + + default: + if (isset($mbIntEnc)) { + mb_internal_encoding($mbIntEnc); + } + + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("Cannot handle content type '{$this->enctype}' automatically." . + " Please use Zend_Http_Client::setRawData to send this kind of content."); + break; + } + } + + // Set the Content-Length if we have a body or if request is POST/PUT + if ($body || $this->method == self::POST || $this->method == self::PUT) { + $this->setHeaders(self::CONTENT_LENGTH, strlen($body)); + } + + if (isset($mbIntEnc)) { + mb_internal_encoding($mbIntEnc); + } + + return $body; + } + + /** + * Helper method that gets a possibly multi-level parameters array (get or + * post) and flattens it. + * + * The method returns an array of (key, value) pairs (because keys are not + * necessarily unique. If one of the parameters in as array, it will also + * add a [] suffix to the key. + * + * This method is deprecated since Zend Framework 1.9 in favour of + * self::_flattenParametersArray() and will be dropped in 2.0 + * + * @deprecated since 1.9 + * + * @param array $parray The parameters array + * @param bool $urlencode Whether to urlencode the name and value + * @return array + */ + protected function _getParametersRecursive($parray, $urlencode = false) + { + // Issue a deprecated notice + trigger_error("The " . __METHOD__ . " method is deprecated and will be dropped in 2.0.", + E_USER_NOTICE); + + if (! is_array($parray)) { + return $parray; + } + $parameters = array(); + + foreach ($parray as $name => $value) { + if ($urlencode) { + $name = urlencode($name); + } + + // If $value is an array, iterate over it + if (is_array($value)) { + $name .= ($urlencode ? '%5B%5D' : '[]'); + foreach ($value as $subval) { + if ($urlencode) { + $subval = urlencode($subval); + } + $parameters[] = array($name, $subval); + } + } else { + if ($urlencode) { + $value = urlencode($value); + } + $parameters[] = array($name, $value); + } + } + + return $parameters; + } + + /** + * 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 deprected 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 (self::$_fileInfoDb === null) { + self::$_fileInfoDb = @finfo_open(FILEINFO_MIME); + } + + if (self::$_fileInfoDb) { + $type = finfo_file(self::$_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 static 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; + } + + /** + * 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 Zend_Http_Client_Exception + */ + 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) { + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("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: + /** @see Zend_Http_Client_Exception */ + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("Not a supported HTTP authentication type: '$type'"); + } + + return $authHeader; + } + + /** + * 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 + */ + static 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, self::_flattenParametersArray($value, $key)); + + } else { + $parameters[] = array($key, $value); + } + } + + return $parameters; + } + +} diff --git a/zend/library/Zend/Http/Client/Adapter/Curl.php b/zend/library/Zend/Http/Client/Adapter/Curl.php new file mode 100644 index 0000000..a66b386 --- /dev/null +++ b/zend/library/Zend/Http/Client/Adapter/Curl.php @@ -0,0 +1,510 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @version $Id: Curl.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * @see Zend_Uri_Http + */ +require_once 'Zend/Uri/Http.php'; + +/** + * @see Zend_Http_Client_Adapter_Interface + */ +require_once 'Zend/Http/Client/Adapter/Interface.php'; +/** + * @see Zend_Http_Client_Adapter_Stream + */ +require_once 'Zend/Http/Client/Adapter/Stream.php'; + +/** + * An adapter class for Zend_Http_Client based on the curl extension. + * Curl requires libcurl. See for full requirements the PHP manual: http://php.net/curl + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Client_Adapter_Curl implements Zend_Http_Client_Adapter_Interface, Zend_Http_Client_Adapter_Stream +{ + /** + * Parameters array + * + * @var array + */ + protected $_config = array(); + + /** + * What host/port are we connected to? + * + * @var array + */ + protected $_connected_to = array(null, null); + + /** + * The curl session handle + * + * @var resource|null + */ + protected $_curl = null; + + /** + * List of cURL options that should never be overwritten + * + * @var array + */ + protected $_invalidOverwritableCurlOptions; + + /** + * Response gotten from server + * + * @var string + */ + protected $_response = null; + + /** + * Stream for storing output + * + * @var resource + */ + protected $out_stream; + + /** + * Adapter constructor + * + * Config is set using setConfig() + * + * @return void + * @throws Zend_Http_Client_Adapter_Exception + */ + public function __construct() + { + if (!extension_loaded('curl')) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('cURL extension has to be loaded to use this Zend_Http_Client adapter.'); + } + $this->_invalidOverwritableCurlOptions = array( + CURLOPT_HTTPGET, + CURLOPT_POST, + CURLOPT_PUT, + CURLOPT_CUSTOMREQUEST, + CURLOPT_HEADER, + CURLOPT_RETURNTRANSFER, + CURLOPT_HTTPHEADER, + CURLOPT_POSTFIELDS, + CURLOPT_INFILE, + CURLOPT_INFILESIZE, + CURLOPT_PORT, + CURLOPT_MAXREDIRS, + CURLOPT_CONNECTTIMEOUT, + CURL_HTTP_VERSION_1_1, + CURL_HTTP_VERSION_1_0, + ); + } + + /** + * Set the configuration array for the adapter + * + * @throws Zend_Http_Client_Adapter_Exception + * @param Zend_Config | array $config + * @return Zend_Http_Client_Adapter_Curl + */ + public function setConfig($config = array()) + { + if ($config instanceof Zend_Config) { + $config = $config->toArray(); + + } elseif (! is_array($config)) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Array or Zend_Config object expected, got ' . gettype($config) + ); + } + + if(isset($config['proxy_user']) && isset($config['proxy_pass'])) { + $this->setCurlOption(CURLOPT_PROXYUSERPWD, $config['proxy_user'].":".$config['proxy_pass']); + unset($config['proxy_user'], $config['proxy_pass']); + } + + foreach ($config as $k => $v) { + $option = strtolower($k); + switch($option) { + case 'proxy_host': + $this->setCurlOption(CURLOPT_PROXY, $v); + break; + case 'proxy_port': + $this->setCurlOption(CURLOPT_PROXYPORT, $v); + break; + default: + $this->_config[$option] = $v; + break; + } + } + + return $this; + } + + /** + * Retrieve the array of all configuration options + * + * @return array + */ + public function getConfig() + { + return $this->_config; + } + + /** + * Direct setter for cURL adapter related options. + * + * @param string|int $option + * @param mixed $value + * @return Zend_Http_Adapter_Curl + */ + public function setCurlOption($option, $value) + { + if (!isset($this->_config['curloptions'])) { + $this->_config['curloptions'] = array(); + } + $this->_config['curloptions'][$option] = $value; + return $this; + } + + /** + * Initialize curl + * + * @param string $host + * @param int $port + * @param boolean $secure + * @return void + * @throws Zend_Http_Client_Adapter_Exception if unable to connect + */ + public function connect($host, $port = 80, $secure = false) + { + // If we're already connected, disconnect first + if ($this->_curl) { + $this->close(); + } + + // If we are connected to a different server or port, disconnect first + if ($this->_curl + && is_array($this->_connected_to) + && ($this->_connected_to[0] != $host + || $this->_connected_to[1] != $port) + ) { + $this->close(); + } + + // Do the actual connection + $this->_curl = curl_init(); + if ($port != 80) { + curl_setopt($this->_curl, CURLOPT_PORT, intval($port)); + } + + // Set timeout + curl_setopt($this->_curl, CURLOPT_CONNECTTIMEOUT, $this->_config['timeout']); + + // Set Max redirects + curl_setopt($this->_curl, CURLOPT_MAXREDIRS, $this->_config['maxredirects']); + + if (!$this->_curl) { + $this->close(); + + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Unable to Connect to ' . $host . ':' . $port); + } + + if ($secure !== false) { + // Behave the same like Zend_Http_Adapter_Socket on SSL options. + if (isset($this->_config['sslcert'])) { + curl_setopt($this->_curl, CURLOPT_SSLCERT, $this->_config['sslcert']); + } + if (isset($this->_config['sslpassphrase'])) { + curl_setopt($this->_curl, CURLOPT_SSLCERTPASSWD, $this->_config['sslpassphrase']); + } + } + + // Update connected_to + $this->_connected_to = array($host, $port); + } + + /** + * Send request to the remote server + * + * @param string $method + * @param Zend_Uri_Http $uri + * @param float $http_ver + * @param array $headers + * @param string $body + * @return string $request + * @throws Zend_Http_Client_Adapter_Exception If connection fails, connected to wrong host, no PUT file defined, unsupported method, or unsupported cURL option + */ + public function write($method, $uri, $httpVersion = 1.1, $headers = array(), $body = '') + { + // Make sure we're properly connected + if (!$this->_curl) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception("Trying to write but we are not connected"); + } + + if ($this->_connected_to[0] != $uri->getHost() || $this->_connected_to[1] != $uri->getPort()) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception("Trying to write but we are connected to the wrong host"); + } + + // set URL + curl_setopt($this->_curl, CURLOPT_URL, $uri->__toString()); + + // ensure correct curl call + $curlValue = true; + switch ($method) { + case Zend_Http_Client::GET: + $curlMethod = CURLOPT_HTTPGET; + break; + + case Zend_Http_Client::POST: + $curlMethod = CURLOPT_POST; + break; + + case Zend_Http_Client::PUT: + // There are two different types of PUT request, either a Raw Data string has been set + // or CURLOPT_INFILE and CURLOPT_INFILESIZE are used. + if(is_resource($body)) { + $this->_config['curloptions'][CURLOPT_INFILE] = $body; + } + if (isset($this->_config['curloptions'][CURLOPT_INFILE])) { + // Now we will probably already have Content-Length set, so that we have to delete it + // from $headers at this point: + foreach ($headers AS $k => $header) { + if (preg_match('/Content-Length:\s*(\d+)/i', $header, $m)) { + if(is_resource($body)) { + $this->_config['curloptions'][CURLOPT_INFILESIZE] = (int)$m[1]; + } + unset($headers[$k]); + } + } + + if (!isset($this->_config['curloptions'][CURLOPT_INFILESIZE])) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception("Cannot set a file-handle for cURL option CURLOPT_INFILE without also setting its size in CURLOPT_INFILESIZE."); + } + + if(is_resource($body)) { + $body = ''; + } + + $curlMethod = CURLOPT_PUT; + } else { + $curlMethod = CURLOPT_CUSTOMREQUEST; + $curlValue = "PUT"; + } + break; + + case Zend_Http_Client::DELETE: + $curlMethod = CURLOPT_CUSTOMREQUEST; + $curlValue = "DELETE"; + break; + + case Zend_Http_Client::OPTIONS: + $curlMethod = CURLOPT_CUSTOMREQUEST; + $curlValue = "OPTIONS"; + break; + + case Zend_Http_Client::TRACE: + $curlMethod = CURLOPT_CUSTOMREQUEST; + $curlValue = "TRACE"; + break; + + case Zend_Http_Client::HEAD: + $curlMethod = CURLOPT_CUSTOMREQUEST; + $curlValue = "HEAD"; + break; + + default: + // For now, through an exception for unsupported request methods + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception("Method currently not supported"); + } + + if(is_resource($body) && $curlMethod != CURLOPT_PUT) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception("Streaming requests are allowed only with PUT"); + } + + // get http version to use + $curlHttp = ($httpVersion == 1.1) ? CURL_HTTP_VERSION_1_1 : CURL_HTTP_VERSION_1_0; + + // mark as HTTP request and set HTTP method + curl_setopt($this->_curl, $curlHttp, true); + curl_setopt($this->_curl, $curlMethod, $curlValue); + + if($this->out_stream) { + // headers will be read into the response + curl_setopt($this->_curl, CURLOPT_HEADER, false); + curl_setopt($this->_curl, CURLOPT_HEADERFUNCTION, array($this, "readHeader")); + // and data will be written into the file + curl_setopt($this->_curl, CURLOPT_FILE, $this->out_stream); + } else { + // ensure headers are also returned + curl_setopt($this->_curl, CURLOPT_HEADER, true); + + // ensure actual response is returned + curl_setopt($this->_curl, CURLOPT_RETURNTRANSFER, true); + } + + // set additional headers + $headers['Accept'] = ''; + curl_setopt($this->_curl, CURLOPT_HTTPHEADER, $headers); + + /** + * Make sure POSTFIELDS is set after $curlMethod is set: + * @link http://de2.php.net/manual/en/function.curl-setopt.php#81161 + */ + if ($method == Zend_Http_Client::POST) { + curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body); + } elseif ($curlMethod == CURLOPT_PUT) { + // this covers a PUT by file-handle: + // Make the setting of this options explicit (rather than setting it through the loop following a bit lower) + // to group common functionality together. + curl_setopt($this->_curl, CURLOPT_INFILE, $this->_config['curloptions'][CURLOPT_INFILE]); + curl_setopt($this->_curl, CURLOPT_INFILESIZE, $this->_config['curloptions'][CURLOPT_INFILESIZE]); + unset($this->_config['curloptions'][CURLOPT_INFILE]); + unset($this->_config['curloptions'][CURLOPT_INFILESIZE]); + } elseif ($method == Zend_Http_Client::PUT) { + // This is a PUT by a setRawData string, not by file-handle + curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body); + } elseif ($method == Zend_Http_Client::DELETE) { + // This is a DELETE by a setRawData string + curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body); + } + + // set additional curl options + if (isset($this->_config['curloptions'])) { + foreach ((array)$this->_config['curloptions'] as $k => $v) { + if (!in_array($k, $this->_invalidOverwritableCurlOptions)) { + if (curl_setopt($this->_curl, $k, $v) == false) { + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception(sprintf("Unknown or erroreous cURL option '%s' set", $k)); + } + } + } + } + + // send the request + $response = curl_exec($this->_curl); + + // if we used streaming, headers are already there + if(!is_resource($this->out_stream)) { + $this->_response = $response; + } + + $request = curl_getinfo($this->_curl, CURLINFO_HEADER_OUT); + $request .= $body; + + if (empty($this->_response)) { + require_once 'Zend/Http/Client/Exception.php'; + throw new Zend_Http_Client_Exception("Error in cURL request: " . curl_error($this->_curl)); + } + + // cURL automatically decodes chunked-messages, this means we have to disallow the Zend_Http_Response to do it again + if (stripos($this->_response, "Transfer-Encoding: chunked\r\n")) { + $this->_response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $this->_response); + } + + // Eliminate multiple HTTP responses. + do { + $parts = preg_split('|(?:\r?\n){2}|m', $this->_response, 2); + $again = false; + + if (isset($parts[1]) && preg_match("|^HTTP/1\.[01](.*?)\r\n|mi", $parts[1])) { + $this->_response = $parts[1]; + $again = true; + } + } while ($again); + + // cURL automatically handles Proxy rewrites, remove the "HTTP/1.0 200 Connection established" string: + if (stripos($this->_response, "HTTP/1.0 200 Connection established\r\n\r\n") !== false) { + $this->_response = str_ireplace("HTTP/1.0 200 Connection established\r\n\r\n", '', $this->_response); + } + + return $request; + } + + /** + * Return read response from server + * + * @return string + */ + public function read() + { + return $this->_response; + } + + /** + * Close the connection to the server + * + */ + public function close() + { + if(is_resource($this->_curl)) { + curl_close($this->_curl); + } + $this->_curl = null; + $this->_connected_to = array(null, null); + } + + /** + * Get cUrl Handle + * + * @return resource + */ + public function getHandle() + { + return $this->_curl; + } + + /** + * Set output stream for the response + * + * @param resource $stream + * @return Zend_Http_Client_Adapter_Socket + */ + public function setOutputStream($stream) + { + $this->out_stream = $stream; + return $this; + } + + /** + * Header reader function for CURL + * + * @param resource $curl + * @param string $header + * @return int + */ + public function readHeader($curl, $header) + { + $this->_response .= $header; + return strlen($header); + } +} diff --git a/zend/library/Zend/Http/Client/Adapter/Exception.php b/zend/library/Zend/Http/Client/Adapter/Exception.php new file mode 100644 index 0000000..6416175 --- /dev/null +++ b/zend/library/Zend/Http/Client/Adapter/Exception.php @@ -0,0 +1,38 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter_Exception + * @version $Id: Exception.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * @see Zend_Http_Client_Exception + */ +require_once 'Zend/Http/Client/Exception.php'; + +/** + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Client_Adapter_Exception extends Zend_Http_Client_Exception +{ + const READ_TIMEOUT = 1000; +} diff --git a/zend/library/Zend/Http/Client/Adapter/Interface.php b/zend/library/Zend/Http/Client/Adapter/Interface.php new file mode 100644 index 0000000..1a0eddd --- /dev/null +++ b/zend/library/Zend/Http/Client/Adapter/Interface.php @@ -0,0 +1,78 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @version $Id: Interface.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * An interface description for Zend_Http_Client_Adapter classes. + * + * These classes are used as connectors for Zend_Http_Client, performing the + * tasks of connecting, writing, reading and closing connection to the server. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +interface Zend_Http_Client_Adapter_Interface +{ + /** + * Set the configuration array for the adapter + * + * @param array $config + */ + public function setConfig($config = array()); + + /** + * Connect to the remote server + * + * @param string $host + * @param int $port + * @param boolean $secure + */ + public function connect($host, $port = 80, $secure = false); + + /** + * Send request to the remote server + * + * @param string $method + * @param Zend_Uri_Http $url + * @param string $http_ver + * @param array $headers + * @param string $body + * @return string Request as text + */ + public function write($method, $url, $http_ver = '1.1', $headers = array(), $body = ''); + + /** + * Read response from server + * + * @return string + */ + public function read(); + + /** + * Close the connection to the server + * + */ + public function close(); +} diff --git a/zend/library/Zend/Http/Client/Adapter/Proxy.php b/zend/library/Zend/Http/Client/Adapter/Proxy.php new file mode 100644 index 0000000..2ca3012 --- /dev/null +++ b/zend/library/Zend/Http/Client/Adapter/Proxy.php @@ -0,0 +1,343 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @version $Id: Proxy.php 25273 2013-03-06 08:02:21Z frosch $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * @see Zend_Uri_Http + */ +require_once 'Zend/Uri/Http.php'; +/** + * @see Zend_Http_Client + */ +require_once 'Zend/Http/Client.php'; +/** + * @see Zend_Http_Client_Adapter_Socket + */ +require_once 'Zend/Http/Client/Adapter/Socket.php'; + +/** + * HTTP Proxy-supporting Zend_Http_Client adapter class, based on the default + * socket based adapter. + * + * Should be used if proxy HTTP access is required. If no proxy is set, will + * fall back to Zend_Http_Client_Adapter_Socket behavior. Just like the + * default Socket adapter, this adapter does not require any special extensions + * installed. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Client_Adapter_Proxy extends Zend_Http_Client_Adapter_Socket +{ + /** + * Parameters array + * + * @var array + */ + protected $config = array( + 'ssltransport' => 'ssl', + 'sslcert' => null, + 'sslpassphrase' => null, + 'sslusecontext' => false, + 'proxy_host' => '', + 'proxy_port' => 8080, + 'proxy_user' => '', + 'proxy_pass' => '', + 'proxy_auth' => Zend_Http_Client::AUTH_BASIC, + 'persistent' => false, + ); + + /** + * Whether HTTPS CONNECT was already negotiated with the proxy or not + * + * @var boolean + */ + protected $negotiated = false; + + /** + * Stores the last CONNECT handshake request + * + * @var string + */ + protected $connectHandshakeRequest; + + /** + * Connect to the remote server + * + * Will try to connect to the proxy server. If no proxy was set, will + * fall back to the target server (behave like regular Socket adapter) + * + * @param string $host + * @param int $port + * @param boolean $secure + */ + public function connect($host, $port = 80, $secure = false) + { + // If no proxy is set, fall back to Socket adapter + if (!$this->config['proxy_host']) { + return parent::connect($host, $port, $secure); + } + + /* Url might require stream context even if proxy connection doesn't */ + if ($secure) { + $this->config['sslusecontext'] = true; + } + + // Connect (a non-secure connection) to the proxy server + return parent::connect( + $this->config['proxy_host'], + $this->config['proxy_port'], + false + ); + } + + /** + * Send request to the proxy server + * + * @param string $method + * @param Zend_Uri_Http $uri + * @param string $http_ver + * @param array $headers + * @param string $body + * @return string Request as string + * @throws Zend_Http_Client_Adapter_Exception + */ + public function write( + $method, $uri, $http_ver = '1.1', $headers = array(), $body = '' + ) + { + // If no proxy is set, fall back to default Socket adapter + if (!$this->config['proxy_host']) { + return parent::write($method, $uri, $http_ver, $headers, $body); + } + + // Make sure we're properly connected + if (!$this->socket) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Trying to write but we are not connected' + ); + } + + $host = $this->config['proxy_host']; + $port = $this->config['proxy_port']; + + if ($this->connected_to[0] != "tcp://$host" + || $this->connected_to[1] != $port + ) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Trying to write but we are connected to the wrong proxy server' + ); + } + + // Add Proxy-Authorization header + if ($this->config['proxy_user']) { + // Check to see if one already exists + $hasProxyAuthHeader = false; + foreach ($headers as $k => $v) { + if ((string) $k == 'proxy-authorization' + || preg_match("/^proxy-authorization:/i", $v) + ) { + $hasProxyAuthHeader = true; + break; + } + } + if (!$hasProxyAuthHeader) { + $headers[] = 'Proxy-authorization: ' + . Zend_Http_Client::encodeAuthHeader( + $this->config['proxy_user'], + $this->config['proxy_pass'], $this->config['proxy_auth'] + ); + } + } + + // if we are proxying HTTPS, preform CONNECT handshake with the proxy + if ($uri->getScheme() == 'https' && (!$this->negotiated)) { + $this->connectHandshake( + $uri->getHost(), $uri->getPort(), $http_ver, $headers + ); + $this->negotiated = true; + } + + // Save request method for later + $this->method = $method; + + // Build request headers + if ($this->negotiated) { + $path = $uri->getPath(); + if ($uri->getQuery()) { + $path .= '?' . $uri->getQuery(); + } + $request = "$method $path HTTP/$http_ver\r\n"; + } else { + $request = "$method $uri HTTP/$http_ver\r\n"; + } + + // Add all headers to the request string + foreach ($headers as $k => $v) { + if (is_string($k)) $v = "$k: $v"; + $request .= "$v\r\n"; + } + + if(is_resource($body)) { + $request .= "\r\n"; + } else { + // Add the request body + $request .= "\r\n" . $body; + } + + // Send the request + if (!@fwrite($this->socket, $request)) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Error writing request to proxy server' + ); + } + + if(is_resource($body)) { + if(stream_copy_to_stream($body, $this->socket) == 0) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Error writing request to server' + ); + } + } + + return $request; + } + + /** + * Preform handshaking with HTTPS proxy using CONNECT method + * + * @param string $host + * @param integer $port + * @param string $http_ver + * @param array $headers + * @return void + * @throws Zend_Http_Client_Adapter_Exception + */ + protected function connectHandshake( + $host, $port = 443, $http_ver = '1.1', array &$headers = array() + ) + { + $request = "CONNECT $host:$port HTTP/$http_ver\r\n" . + "Host: " . $this->config['proxy_host'] . "\r\n"; + + // Process provided headers, including important ones to CONNECT request + foreach ($headers as $k => $v) { + switch (strtolower(substr($v,0,strpos($v,':')))) { + case 'proxy-authorization': + // break intentionally omitted + + case 'user-agent': + $request .= $v . "\r\n"; + break; + + default: + break; + } + } + $request .= "\r\n"; + + // @see ZF-3189 + $this->connectHandshakeRequest = $request; + + // Send the request + if (!@fwrite($this->socket, $request)) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Error writing request to proxy server' + ); + } + + // Read response headers only + $response = ''; + $gotStatus = false; + while ($line = @fgets($this->socket)) { + $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false); + if ($gotStatus) { + $response .= $line; + if (!chop($line)) { + break; + } + } + } + + // Check that the response from the proxy is 200 + if (Zend_Http_Response::extractCode($response) != 200) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Unable to connect to HTTPS proxy. Server response: ' . $response + ); + } + + // If all is good, switch socket to secure mode. We have to fall back + // through the different modes + $modes = array( + STREAM_CRYPTO_METHOD_TLS_CLIENT, + STREAM_CRYPTO_METHOD_SSLv3_CLIENT, + STREAM_CRYPTO_METHOD_SSLv23_CLIENT, + STREAM_CRYPTO_METHOD_SSLv2_CLIENT + ); + + $success = false; + foreach($modes as $mode) { + $success = stream_socket_enable_crypto($this->socket, true, $mode); + if ($success) { + break; + } + } + + if (!$success) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Unable to connect to HTTPS server through proxy: could not ' + . 'negotiate secure connection.' + ); + } + } + + /** + * Close the connection to the server + * + */ + public function close() + { + parent::close(); + $this->negotiated = false; + } + + /** + * Destructor: make sure the socket is disconnected + * + */ + public function __destruct() + { + if ($this->socket) { + $this->close(); + } + } +} diff --git a/zend/library/Zend/Http/Client/Adapter/Socket.php b/zend/library/Zend/Http/Client/Adapter/Socket.php new file mode 100644 index 0000000..b2a0e9b --- /dev/null +++ b/zend/library/Zend/Http/Client/Adapter/Socket.php @@ -0,0 +1,543 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @version $Id: Socket.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * @see Zend_Uri_Http + */ +require_once 'Zend/Uri/Http.php'; +/** + * @see Zend_Http_Client_Adapter_Interface + */ +require_once 'Zend/Http/Client/Adapter/Interface.php'; +/** + * @see Zend_Http_Client_Adapter_Stream + */ +require_once 'Zend/Http/Client/Adapter/Stream.php'; + +/** + * A sockets based (stream_socket_client) adapter class for Zend_Http_Client. Can be used + * on almost every PHP environment, and does not require any special extensions. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interface, Zend_Http_Client_Adapter_Stream +{ + /** + * The socket for server connection + * + * @var resource|null + */ + protected $socket = null; + + /** + * What host/port are we connected to? + * + * @var array + */ + protected $connected_to = array(null, null); + + /** + * Stream for storing output + * + * @var resource + */ + protected $out_stream = null; + + /** + * Parameters array + * + * @var array + */ + protected $config = array( + 'persistent' => false, + 'ssltransport' => 'ssl', + 'sslcert' => null, + 'sslpassphrase' => null, + 'sslusecontext' => false + ); + + /** + * Request method - will be set by write() and might be used by read() + * + * @var string + */ + protected $method = null; + + /** + * Stream context + * + * @var resource + */ + protected $_context = null; + + /** + * Adapter constructor, currently empty. Config is set using setConfig() + * + */ + public function __construct() + { + } + + /** + * Set the configuration array for the adapter + * + * @param Zend_Config | array $config + */ + public function setConfig($config = array()) + { + if ($config instanceof Zend_Config) { + $config = $config->toArray(); + + } elseif (! is_array($config)) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Array or Zend_Config object expected, got ' . gettype($config) + ); + } + + foreach ($config as $k => $v) { + $this->config[strtolower($k)] = $v; + } + } + + /** + * Retrieve the array of all configuration options + * + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * Set the stream context for the TCP connection to the server + * + * Can accept either a pre-existing stream context resource, or an array + * of stream options, similar to the options array passed to the + * stream_context_create() PHP function. In such case a new stream context + * will be created using the passed options. + * + * @since Zend Framework 1.9 + * + * @param mixed $context Stream context or array of context options + * @return Zend_Http_Client_Adapter_Socket + */ + public function setStreamContext($context) + { + if (is_resource($context) && get_resource_type($context) == 'stream-context') { + $this->_context = $context; + + } elseif (is_array($context)) { + $this->_context = stream_context_create($context); + + } else { + // Invalid parameter + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + "Expecting either a stream context resource or array, got " . gettype($context) + ); + } + + return $this; + } + + /** + * Get the stream context for the TCP connection to the server. + * + * If no stream context is set, will create a default one. + * + * @return resource + */ + public function getStreamContext() + { + if (! $this->_context) { + $this->_context = stream_context_create(); + } + + return $this->_context; + } + + /** + * Connect to the remote server + * + * @param string $host + * @param int $port + * @param boolean $secure + */ + public function connect($host, $port = 80, $secure = false) + { + // If the URI should be accessed via SSL, prepend the Hostname with ssl:// + $host = ($secure ? $this->config['ssltransport'] : 'tcp') . '://' . $host; + + // If we are connected to the wrong host, disconnect first + if (($this->connected_to[0] != $host || $this->connected_to[1] != $port)) { + if (is_resource($this->socket)) $this->close(); + } + + // Now, if we are not connected, connect + if (! is_resource($this->socket) || ! $this->config['keepalive']) { + $context = $this->getStreamContext(); + if ($secure || $this->config['sslusecontext']) { + if ($this->config['sslcert'] !== null) { + if (! stream_context_set_option($context, 'ssl', 'local_cert', + $this->config['sslcert'])) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Unable to set sslcert option'); + } + } + if ($this->config['sslpassphrase'] !== null) { + if (! stream_context_set_option($context, 'ssl', 'passphrase', + $this->config['sslpassphrase'])) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Unable to set sslpassphrase option'); + } + } + } + + $flags = STREAM_CLIENT_CONNECT; + if ($this->config['persistent']) $flags |= STREAM_CLIENT_PERSISTENT; + + $this->socket = @stream_socket_client($host . ':' . $port, + $errno, + $errstr, + (int) $this->config['timeout'], + $flags, + $context); + + if (! $this->socket) { + $this->close(); + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Unable to Connect to ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr); + } + + // Set the stream timeout + if (! stream_set_timeout($this->socket, (int) $this->config['timeout'])) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Unable to set the connection timeout'); + } + + // Update connected_to + $this->connected_to = array($host, $port); + } + } + + /** + * Send request to the remote server + * + * @param string $method + * @param Zend_Uri_Http $uri + * @param string $http_ver + * @param array $headers + * @param string $body + * @return string Request as string + */ + public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '') + { + // Make sure we're properly connected + if (! $this->socket) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are not connected'); + } + + $host = $uri->getHost(); + $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host; + if ($this->connected_to[0] != $host || $this->connected_to[1] != $uri->getPort()) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are connected to the wrong host'); + } + + // Save request method for later + $this->method = $method; + + // Build request headers + $path = $uri->getPath(); + if ($uri->getQuery()) $path .= '?' . $uri->getQuery(); + $request = "{$method} {$path} HTTP/{$http_ver}\r\n"; + foreach ($headers as $k => $v) { + if (is_string($k)) $v = ucfirst($k) . ": $v"; + $request .= "$v\r\n"; + } + + if(is_resource($body)) { + $request .= "\r\n"; + } else { + // Add the request body + $request .= "\r\n" . $body; + } + + // Send the request + if (! @fwrite($this->socket, $request)) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Error writing request to server'); + } + + if(is_resource($body)) { + if(stream_copy_to_stream($body, $this->socket) == 0) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Error writing request to server'); + } + } + + return $request; + } + + /** + * Read response from server + * + * @return string + */ + public function read() + { + // First, read headers only + $response = ''; + $gotStatus = false; + + while (($line = @fgets($this->socket)) !== false) { + $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false); + if ($gotStatus) { + $response .= $line; + if (rtrim($line) === '') break; + } + } + + $this->_checkSocketReadTimeout(); + + $statusCode = Zend_Http_Response::extractCode($response); + + // Handle 100 and 101 responses internally by restarting the read again + if ($statusCode == 100 || $statusCode == 101) return $this->read(); + + // Check headers to see what kind of connection / transfer encoding we have + $headers = Zend_Http_Response::extractHeaders($response); + + /** + * Responses to HEAD requests and 204 or 304 responses are not expected + * to have a body - stop reading here + */ + if ($statusCode == 304 || $statusCode == 204 || + $this->method == Zend_Http_Client::HEAD) { + + // Close the connection if requested to do so by the server + if (isset($headers['connection']) && $headers['connection'] == 'close') { + $this->close(); + } + return $response; + } + + // If we got a 'transfer-encoding: chunked' header + if (isset($headers['transfer-encoding'])) { + + if (strtolower($headers['transfer-encoding']) == 'chunked') { + + do { + $line = @fgets($this->socket); + $this->_checkSocketReadTimeout(); + + $chunk = $line; + + // Figure out the next chunk size + $chunksize = trim($line); + if (! ctype_xdigit($chunksize)) { + $this->close(); + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Invalid chunk size "' . + $chunksize . '" unable to read chunked body'); + } + + // Convert the hexadecimal value to plain integer + $chunksize = hexdec($chunksize); + + // Read next chunk + $read_to = ftell($this->socket) + $chunksize; + + do { + $current_pos = ftell($this->socket); + if ($current_pos >= $read_to) break; + + if($this->out_stream) { + if(stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) { + $this->_checkSocketReadTimeout(); + break; + } + } else { + $line = @fread($this->socket, $read_to - $current_pos); + if ($line === false || strlen($line) === 0) { + $this->_checkSocketReadTimeout(); + break; + } + $chunk .= $line; + } + } while (! feof($this->socket)); + + $chunk .= @fgets($this->socket); + $this->_checkSocketReadTimeout(); + + if(!$this->out_stream) { + $response .= $chunk; + } + } while ($chunksize > 0); + } else { + $this->close(); + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Cannot handle "' . + $headers['transfer-encoding'] . '" transfer encoding'); + } + + // We automatically decode chunked-messages when writing to a stream + // this means we have to disallow the Zend_Http_Response to do it again + if ($this->out_stream) { + $response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $response); + } + // Else, if we got the content-length header, read this number of bytes + } elseif (isset($headers['content-length'])) { + + // If we got more than one Content-Length header (see ZF-9404) use + // the last value sent + if (is_array($headers['content-length'])) { + $contentLength = $headers['content-length'][count($headers['content-length']) - 1]; + } else { + $contentLength = $headers['content-length']; + } + + $current_pos = ftell($this->socket); + $chunk = ''; + + for ($read_to = $current_pos + $contentLength; + $read_to > $current_pos; + $current_pos = ftell($this->socket)) { + + if($this->out_stream) { + if(@stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) { + $this->_checkSocketReadTimeout(); + break; + } + } else { + $chunk = @fread($this->socket, $read_to - $current_pos); + if ($chunk === false || strlen($chunk) === 0) { + $this->_checkSocketReadTimeout(); + break; + } + + $response .= $chunk; + } + + // Break if the connection ended prematurely + if (feof($this->socket)) break; + } + + // Fallback: just read the response until EOF + } else { + + do { + if($this->out_stream) { + if(@stream_copy_to_stream($this->socket, $this->out_stream) == 0) { + $this->_checkSocketReadTimeout(); + break; + } + } else { + $buff = @fread($this->socket, 8192); + if ($buff === false || strlen($buff) === 0) { + $this->_checkSocketReadTimeout(); + break; + } else { + $response .= $buff; + } + } + + } while (feof($this->socket) === false); + + $this->close(); + } + + // Close the connection if requested to do so by the server + if (isset($headers['connection']) && $headers['connection'] == 'close') { + $this->close(); + } + + return $response; + } + + /** + * Close the connection to the server + * + */ + public function close() + { + if (is_resource($this->socket)) @fclose($this->socket); + $this->socket = null; + $this->connected_to = array(null, null); + } + + /** + * Check if the socket has timed out - if so close connection and throw + * an exception + * + * @throws Zend_Http_Client_Adapter_Exception with READ_TIMEOUT code + */ + protected function _checkSocketReadTimeout() + { + if ($this->socket) { + $info = stream_get_meta_data($this->socket); + $timedout = $info['timed_out']; + if ($timedout) { + $this->close(); + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + "Read timed out after {$this->config['timeout']} seconds", + Zend_Http_Client_Adapter_Exception::READ_TIMEOUT + ); + } + } + } + + /** + * Set output stream for the response + * + * @param resource $stream + * @return Zend_Http_Client_Adapter_Socket + */ + public function setOutputStream($stream) + { + $this->out_stream = $stream; + return $this; + } + + /** + * Destructor: make sure the socket is disconnected + * + * If we are in persistent TCP mode, will not close the connection + * + */ + public function __destruct() + { + if (! $this->config['persistent']) { + if ($this->socket) $this->close(); + } + } +} diff --git a/zend/library/Zend/Http/Client/Adapter/Stream.php b/zend/library/Zend/Http/Client/Adapter/Stream.php new file mode 100755 index 0000000..98f4410 --- /dev/null +++ b/zend/library/Zend/Http/Client/Adapter/Stream.php @@ -0,0 +1,46 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @version $Id: Stream.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * An interface description for Zend_Http_Client_Adapter_Stream classes. + * + * This interface decribes Zend_Http_Client_Adapter which supports streaming. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +interface Zend_Http_Client_Adapter_Stream +{ + /** + * Set output stream + * + * This function sets output stream where the result will be stored. + * + * @param resource $stream Stream to write the output to + * + */ + public function setOutputStream($stream); +} diff --git a/zend/library/Zend/Http/Client/Adapter/Test.php b/zend/library/Zend/Http/Client/Adapter/Test.php new file mode 100644 index 0000000..aa041af --- /dev/null +++ b/zend/library/Zend/Http/Client/Adapter/Test.php @@ -0,0 +1,248 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @version $Id: Test.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * @see Zend_Uri_Http + */ +require_once 'Zend/Uri/Http.php'; +/** + * @see Zend_Http_Response + */ +require_once 'Zend/Http/Response.php'; +/** + * @see Zend_Http_Client_Adapter_Interface + */ +require_once 'Zend/Http/Client/Adapter/Interface.php'; + +/** + * A testing-purposes adapter. + * + * Should be used to test all components that rely on Zend_Http_Client, + * without actually performing an HTTP request. You should instantiate this + * object manually, and then set it as the client's adapter. Then, you can + * set the expected response using the setResponse() method. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Adapter + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Client_Adapter_Test implements Zend_Http_Client_Adapter_Interface +{ + /** + * Parameters array + * + * @var array + */ + protected $config = array(); + + /** + * Buffer of responses to be returned by the read() method. Can be + * set using setResponse() and addResponse(). + * + * @var array + */ + protected $responses = array("HTTP/1.1 400 Bad Request\r\n\r\n"); + + /** + * Current position in the response buffer + * + * @var integer + */ + protected $responseIndex = 0; + + /** + * Wether or not the next request will fail with an exception + * + * @var boolean + */ + protected $_nextRequestWillFail = false; + + /** + * Adapter constructor, currently empty. Config is set using setConfig() + * + */ + public function __construct() + { } + + /** + * Set the nextRequestWillFail flag + * + * @param boolean $flag + * @return Zend_Http_Client_Adapter_Test + */ + public function setNextRequestWillFail($flag) + { + $this->_nextRequestWillFail = (bool) $flag; + + return $this; + } + + /** + * Set the configuration array for the adapter + * + * @param Zend_Config | array $config + */ + public function setConfig($config = array()) + { + if ($config instanceof Zend_Config) { + $config = $config->toArray(); + + } elseif (! is_array($config)) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Array or Zend_Config object expected, got ' . gettype($config) + ); + } + + foreach ($config as $k => $v) { + $this->config[strtolower($k)] = $v; + } + } + + + /** + * Connect to the remote server + * + * @param string $host + * @param int $port + * @param boolean $secure + * @param int $timeout + * @throws Zend_Http_Client_Adapter_Exception + */ + public function connect($host, $port = 80, $secure = false) + { + if ($this->_nextRequestWillFail) { + $this->_nextRequestWillFail = false; + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception('Request failed'); + } + } + + /** + * Send request to the remote server + * + * @param string $method + * @param Zend_Uri_Http $uri + * @param string $http_ver + * @param array $headers + * @param string $body + * @return string Request as string + */ + public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '') + { + $host = $uri->getHost(); + $host = (strtolower($uri->getScheme()) == 'https' ? 'sslv2://' . $host : $host); + + // Build request headers + $path = $uri->getPath(); + if ($uri->getQuery()) $path .= '?' . $uri->getQuery(); + $request = "{$method} {$path} HTTP/{$http_ver}\r\n"; + foreach ($headers as $k => $v) { + if (is_string($k)) $v = ucfirst($k) . ": $v"; + $request .= "$v\r\n"; + } + + // Add the request body + $request .= "\r\n" . $body; + + // Do nothing - just return the request as string + + return $request; + } + + /** + * Return the response set in $this->setResponse() + * + * @return string + */ + public function read() + { + if ($this->responseIndex >= count($this->responses)) { + $this->responseIndex = 0; + } + return $this->responses[$this->responseIndex++]; + } + + /** + * Close the connection (dummy) + * + */ + public function close() + { } + + /** + * Set the HTTP response(s) to be returned by this adapter + * + * @param Zend_Http_Response|array|string $response + */ + public function setResponse($response) + { + if ($response instanceof Zend_Http_Response) { + $response = $response->asString("\r\n"); + } + + $this->responses = (array)$response; + $this->responseIndex = 0; + } + + /** + * Add another response to the response buffer. + * + * @param string Zend_Http_Response|$response + */ + public function addResponse($response) + { + if ($response instanceof Zend_Http_Response) { + $response = $response->asString("\r\n"); + } + + $this->responses[] = $response; + } + + /** + * Sets the position of the response buffer. Selects which + * response will be returned on the next call to read(). + * + * @param integer $index + */ + public function setResponseIndex($index) + { + if ($index < 0 || $index >= count($this->responses)) { + require_once 'Zend/Http/Client/Adapter/Exception.php'; + throw new Zend_Http_Client_Adapter_Exception( + 'Index out of range of response buffer size'); + } + $this->responseIndex = $index; + } + + /** + * Retrieve the array of all configuration options + * + * @return array + */ + public function getConfig() + { + return $this->config; + } +} diff --git a/zend/library/Zend/Http/Client/Exception.php b/zend/library/Zend/Http/Client/Exception.php new file mode 100644 index 0000000..278b90b --- /dev/null +++ b/zend/library/Zend/Http/Client/Exception.php @@ -0,0 +1,36 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Client_Exception + * @version $Id: Exception.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * @see Zend_Http_Exception + */ +require_once 'Zend/Http/Exception.php'; + +/** + * @category Zend + * @package Zend_Http + * @subpackage Client + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Client_Exception extends Zend_Http_Exception +{} diff --git a/zend/library/Zend/Http/Exception.php b/zend/library/Zend/Http/Exception.php new file mode 100644 index 0000000..b780074 --- /dev/null +++ b/zend/library/Zend/Http/Exception.php @@ -0,0 +1,36 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Exception + * @version $Id: Exception.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * @see Zend_Exception + */ +require_once 'Zend/Exception.php'; + +/** + * @category Zend + * @package Zend_Http + * @subpackage Client + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Exception extends Zend_Exception +{} diff --git a/zend/library/Zend/Http/Response.php b/zend/library/Zend/Http/Response.php new file mode 100644 index 0000000..acc6955 --- /dev/null +++ b/zend/library/Zend/Http/Response.php @@ -0,0 +1,667 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Response + * @version $Id: Response.php 25081 2012-11-06 20:59:47Z rob $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It + * includes easy access to all the response's different elemts, as well as some + * convenience methods for parsing and validating HTTP responses. + * + * @package Zend_Http + * @subpackage Response + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Response +{ + /** + * List of all known HTTP response codes - used by responseCodeAsText() to + * translate numeric codes to messages. + * + * @var array + */ + protected static $messages = array( + // Informational 1xx + 100 => 'Continue', + 101 => 'Switching Protocols', + + // Success 2xx + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + + // Redirection 3xx + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', // 1.1 + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + // 306 is deprecated but reserved + 307 => 'Temporary Redirect', + + // Client Error 4xx + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + + // Server Error 5xx + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 509 => 'Bandwidth Limit Exceeded' + ); + + /** + * The HTTP version (1.0, 1.1) + * + * @var string + */ + protected $version; + + /** + * The HTTP response code + * + * @var int + */ + protected $code; + + /** + * The HTTP response code as string + * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500) + * + * @var string + */ + protected $message; + + /** + * The HTTP response headers array + * + * @var array + */ + protected $headers = array(); + + /** + * The HTTP response body + * + * @var string + */ + protected $body; + + /** + * HTTP response constructor + * + * In most cases, you would use Zend_Http_Response::fromString to parse an HTTP + * response string and create a new Zend_Http_Response object. + * + * NOTE: The constructor no longer accepts nulls or empty values for the code and + * headers and will throw an exception if the passed values do not form a valid HTTP + * responses. + * + * If no message is passed, the message will be guessed according to the response code. + * + * @param int $code Response code (200, 404, ...) + * @param array $headers Headers array + * @param string $body Response body + * @param string $version HTTP version + * @param string $message Response code as text + * @throws Zend_Http_Exception + */ + public function __construct($code, array $headers, $body = null, $version = '1.1', $message = null) + { + // Make sure the response code is valid and set it + if (self::responseCodeAsText($code) === null) { + require_once 'Zend/Http/Exception.php'; + throw new Zend_Http_Exception("{$code} is not a valid HTTP response code"); + } + + $this->code = $code; + + foreach ($headers as $name => $value) { + if (is_int($name)) { + $header = explode(":", $value, 2); + if (count($header) != 2) { + require_once 'Zend/Http/Exception.php'; + throw new Zend_Http_Exception("'{$value}' is not a valid HTTP header"); + } + + $name = trim($header[0]); + $value = trim($header[1]); + } + + $this->headers[ucwords(strtolower($name))] = $value; + } + + // Set the body + $this->body = $body; + + // Set the HTTP version + if (! preg_match('|^\d\.\d$|', $version)) { + require_once 'Zend/Http/Exception.php'; + throw new Zend_Http_Exception("Invalid HTTP response version: $version"); + } + + $this->version = $version; + + // If we got the response message, set it. Else, set it according to + // the response code + if (is_string($message)) { + $this->message = $message; + } else { + $this->message = self::responseCodeAsText($code); + } + } + + /** + * Check whether the response is an error + * + * @return boolean + */ + public function isError() + { + $restype = floor($this->code / 100); + if ($restype == 4 || $restype == 5) { + return true; + } + + return false; + } + + /** + * Check whether the response in successful + * + * @return boolean + */ + public function isSuccessful() + { + $restype = floor($this->code / 100); + if ($restype == 2 || $restype == 1) { // Shouldn't 3xx count as success as well ??? + return true; + } + + return false; + } + + /** + * Check whether the response is a redirection + * + * @return boolean + */ + public function isRedirect() + { + $restype = floor($this->code / 100); + if ($restype == 3) { + return true; + } + + return false; + } + + /** + * Get the response body as string + * + * This method returns the body of the HTTP response (the content), as it + * should be in it's readable version - that is, after decoding it (if it + * was decoded), deflating it (if it was gzip compressed), etc. + * + * If you want to get the raw body (as transfered on wire) use + * $this->getRawBody() instead. + * + * @return string + */ + public function getBody() + { + $body = ''; + + // Decode the body if it was transfer-encoded + switch (strtolower($this->getHeader('transfer-encoding'))) { + + // Handle chunked body + case 'chunked': + $body = self::decodeChunkedBody($this->body); + break; + + // No transfer encoding, or unknown encoding extension: + // return body as is + default: + $body = $this->body; + break; + } + + // Decode any content-encoding (gzip or deflate) if needed + switch (strtolower($this->getHeader('content-encoding'))) { + + // Handle gzip encoding + case 'gzip': + $body = self::decodeGzip($body); + break; + + // Handle deflate encoding + case 'deflate': + $body = self::decodeDeflate($body); + break; + + default: + break; + } + + return $body; + } + + /** + * Get the raw response body (as transfered "on wire") as string + * + * If the body is encoded (with Transfer-Encoding, not content-encoding - + * IE "chunked" body), gzip compressed, etc. it will not be decoded. + * + * @return string + */ + public function getRawBody() + { + return $this->body; + } + + /** + * Get the HTTP version of the response + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Get the HTTP response status code + * + * @return int + */ + public function getStatus() + { + return $this->code; + } + + /** + * Return a message describing the HTTP response code + * (Eg. "OK", "Not Found", "Moved Permanently") + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Get the response headers + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Get a specific header as string, or null if it is not set + * + * @param string$header + * @return string|array|null + */ + public function getHeader($header) + { + $header = ucwords(strtolower($header)); + if (! is_string($header) || ! isset($this->headers[$header])) return null; + + return $this->headers[$header]; + } + + /** + * Get all headers as string + * + * @param boolean $status_line Whether to return the first status line (IE "HTTP 200 OK") + * @param string $br Line breaks (eg. "\n", "\r\n", "<br />") + * @return string + */ + public function getHeadersAsString($status_line = true, $br = "\n") + { + $str = ''; + + if ($status_line) { + $str = "HTTP/{$this->version} {$this->code} {$this->message}{$br}"; + } + + // Iterate over the headers and stringify them + foreach ($this->headers as $name => $value) + { + if (is_string($value)) + $str .= "{$name}: {$value}{$br}"; + + elseif (is_array($value)) { + foreach ($value as $subval) { + $str .= "{$name}: {$subval}{$br}"; + } + } + } + + return $str; + } + + /** + * Get the entire response as string + * + * @param string $br Line breaks (eg. "\n", "\r\n", "<br />") + * @return string + */ + public function asString($br = "\n") + { + return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody(); + } + + /** + * Implements magic __toString() + * + * @return string + */ + public function __toString() + { + return $this->asString(); + } + + /** + * A convenience function that returns a text representation of + * HTTP response codes. Returns 'Unknown' for unknown codes. + * Returns array of all codes, if $code is not specified. + * + * Conforms to HTTP/1.1 as defined in RFC 2616 (except for 'Unknown') + * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 for reference + * + * @param int $code HTTP response code + * @param boolean $http11 Use HTTP version 1.1 + * @return string + */ + public static function responseCodeAsText($code = null, $http11 = true) + { + $messages = self::$messages; + if (! $http11) $messages[302] = 'Moved Temporarily'; + + if ($code === null) { + return $messages; + } elseif (isset($messages[$code])) { + return $messages[$code]; + } else { + return 'Unknown'; + } + } + + /** + * Extract the response code from a response string + * + * @param string $response_str + * @return int + */ + public static function extractCode($response_str) + { + preg_match("|^HTTP/[\d\.x]+ (\d+)|", $response_str, $m); + + if (isset($m[1])) { + return (int) $m[1]; + } else { + return false; + } + } + + /** + * Extract the HTTP message from a response + * + * @param string $response_str + * @return string + */ + public static function extractMessage($response_str) + { + preg_match("|^HTTP/[\d\.x]+ \d+ ([^\r\n]+)|", $response_str, $m); + + if (isset($m[1])) { + return $m[1]; + } else { + return false; + } + } + + /** + * Extract the HTTP version from a response + * + * @param string $response_str + * @return string + */ + public static function extractVersion($response_str) + { + preg_match("|^HTTP/([\d\.x]+) \d+|", $response_str, $m); + + if (isset($m[1])) { + return $m[1]; + } else { + return false; + } + } + + /** + * Extract the headers from a response string + * + * @param string $response_str + * @return array + */ + public static function extractHeaders($response_str) + { + $headers = array(); + + // First, split body and headers + $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2); + if (! $parts[0]) return $headers; + + // Split headers part to lines + $lines = explode("\n", $parts[0]); + unset($parts); + $last_header = null; + + foreach($lines as $line) { + $line = trim($line, "\r\n"); + if ($line == "") break; + + // Locate headers like 'Location: ...' and 'Location:...' (note the missing space) + if (preg_match("|^([\w-]+):\s*(.+)|", $line, $m)) { + unset($last_header); + $h_name = strtolower($m[1]); + $h_value = $m[2]; + + if (isset($headers[$h_name])) { + if (! is_array($headers[$h_name])) { + $headers[$h_name] = array($headers[$h_name]); + } + + $headers[$h_name][] = $h_value; + } else { + $headers[$h_name] = $h_value; + } + $last_header = $h_name; + } elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) { + if (is_array($headers[$last_header])) { + end($headers[$last_header]); + $last_header_key = key($headers[$last_header]); + $headers[$last_header][$last_header_key] .= $m[1]; + } else { + $headers[$last_header] .= $m[1]; + } + } + } + + return $headers; + } + + /** + * Extract the body from a response string + * + * @param string $response_str + * @return string + */ + public static function extractBody($response_str) + { + $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2); + if (isset($parts[1])) { + return $parts[1]; + } + return ''; + } + + /** + * Decode a "chunked" transfer-encoded body and return the decoded text + * + * @param string $body + * @return string + */ + public static function decodeChunkedBody($body) + { + $decBody = ''; + + // If mbstring overloads substr and strlen functions, we have to + // override it's internal encoding + if (function_exists('mb_internal_encoding') && + ((int) ini_get('mbstring.func_overload')) & 2) { + + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } + + while (trim($body)) { + if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) { + require_once 'Zend/Http/Exception.php'; + throw new Zend_Http_Exception("Error parsing body - doesn't seem to be a chunked message"); + } + + $length = hexdec(trim($m[1])); + $cut = strlen($m[0]); + $decBody .= substr($body, $cut, $length); + $body = substr($body, $cut + $length + 2); + } + + if (isset($mbIntEnc)) { + mb_internal_encoding($mbIntEnc); + } + + return $decBody; + } + + /** + * Decode a gzip encoded message (when Content-encoding = gzip) + * + * Currently requires PHP with zlib support + * + * @param string $body + * @return string + */ + public static function decodeGzip($body) + { + if (! function_exists('gzinflate')) { + require_once 'Zend/Http/Exception.php'; + throw new Zend_Http_Exception( + 'zlib extension is required in order to decode "gzip" encoding' + ); + } + + return gzinflate(substr($body, 10)); + } + + /** + * Decode a zlib deflated message (when Content-encoding = deflate) + * + * Currently requires PHP with zlib support + * + * @param string $body + * @return string + */ + public static function decodeDeflate($body) + { + if (! function_exists('gzuncompress')) { + require_once 'Zend/Http/Exception.php'; + throw new Zend_Http_Exception( + 'zlib extension is required in order to decode "deflate" encoding' + ); + } + + /** + * Some servers (IIS ?) send a broken deflate response, without the + * RFC-required zlib header. + * + * We try to detect the zlib header, and if it does not exsit we + * teat the body is plain DEFLATE content. + * + * This method was adapted from PEAR HTTP_Request2 by (c) Alexey Borzov + * + * @link http://framework.zend.com/issues/browse/ZF-6040 + */ + $zlibHeader = unpack('n', substr($body, 0, 2)); + if ($zlibHeader[1] % 31 == 0 && ord($body[0]) == 0x78 && in_array(ord($body[1]), array(0x01, 0x5e, 0x9c, 0xda))) { + return gzuncompress($body); + } else { + return gzinflate($body); + } + } + + /** + * Create a new Zend_Http_Response object from a string + * + * @param string $response_str + * @return Zend_Http_Response + */ + public static function fromString($response_str) + { + $code = self::extractCode($response_str); + $headers = self::extractHeaders($response_str); + $body = self::extractBody($response_str); + $version = self::extractVersion($response_str); + $message = self::extractMessage($response_str); + + return new Zend_Http_Response($code, $headers, $body, $version, $message); + } +} diff --git a/zend/library/Zend/Http/Response/Stream.php b/zend/library/Zend/Http/Response/Stream.php new file mode 100755 index 0000000..d5e17d5 --- /dev/null +++ b/zend/library/Zend/Http/Response/Stream.php @@ -0,0 +1,235 @@ +<?php + +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_Http + * @subpackage Response + * @version $Id: Stream.php 24593 2012-01-05 20:35:02Z matthew $ + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ + +/** + * Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It + * includes easy access to all the response's different elemts, as well as some + * convenience methods for parsing and validating HTTP responses. + * + * @package Zend_Http + * @subpackage Response + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_Http_Response_Stream extends Zend_Http_Response +{ + /** + * Response as stream + * + * @var resource + */ + protected $stream; + + /** + * The name of the file containing the stream + * + * Will be empty if stream is not file-based. + * + * @var string + */ + protected $stream_name; + + /** + * Should we clean up the stream file when this response is closed? + * + * @var boolean + */ + protected $_cleanup; + + /** + * Get the response as stream + * + * @return resourse + */ + public function getStream() + { + return $this->stream; + } + + /** + * Set the response stream + * + * @param resourse $stream + * @return Zend_Http_Response_Stream + */ + public function setStream($stream) + { + $this->stream = $stream; + return $this; + } + + /** + * Get the cleanup trigger + * + * @return boolean + */ + public function getCleanup() { + return $this->_cleanup; + } + + /** + * Set the cleanup trigger + * + * @param bool $cleanup Set cleanup trigger + */ + public function setCleanup($cleanup = true) { + $this->_cleanup = $cleanup; + } + + /** + * Get file name associated with the stream + * + * @return string + */ + public function getStreamName() { + return $this->stream_name; + } + + /** + * Set file name associated with the stream + * + * @param string $stream_name Name to set + * @return Zend_Http_Response_Stream + */ + public function setStreamName($stream_name) { + $this->stream_name = $stream_name; + return $this; + } + + + /** + * HTTP response constructor + * + * In most cases, you would use Zend_Http_Response::fromString to parse an HTTP + * response string and create a new Zend_Http_Response object. + * + * NOTE: The constructor no longer accepts nulls or empty values for the code and + * headers and will throw an exception if the passed values do not form a valid HTTP + * responses. + * + * If no message is passed, the message will be guessed according to the response code. + * + * @param int $code Response code (200, 404, ...) + * @param array $headers Headers array + * @param string $body Response body + * @param string $version HTTP version + * @param string $message Response code as text + * @throws Zend_Http_Exception + */ + public function __construct($code, $headers, $body = null, $version = '1.1', $message = null) + { + + if(is_resource($body)) { + $this->setStream($body); + $body = ''; + } + parent::__construct($code, $headers, $body, $version, $message); + } + + /** + * Create a new Zend_Http_Response_Stream object from a string + * + * @param string $response_str + * @param resource $stream + * @return Zend_Http_Response_Stream + */ + public static function fromStream($response_str, $stream) + { + $code = self::extractCode($response_str); + $headers = self::extractHeaders($response_str); + $version = self::extractVersion($response_str); + $message = self::extractMessage($response_str); + + return new self($code, $headers, $stream, $version, $message); + } + + /** + * Get the response body as string + * + * This method returns the body of the HTTP response (the content), as it + * should be in it's readable version - that is, after decoding it (if it + * was decoded), deflating it (if it was gzip compressed), etc. + * + * If you want to get the raw body (as transfered on wire) use + * $this->getRawBody() instead. + * + * @return string + */ + public function getBody() + { + if($this->stream != null) { + $this->readStream(); + } + return parent::getBody(); + } + + /** + * Get the raw response body (as transfered "on wire") as string + * + * If the body is encoded (with Transfer-Encoding, not content-encoding - + * IE "chunked" body), gzip compressed, etc. it will not be decoded. + * + * @return string + */ + public function getRawBody() + { + if($this->stream) { + $this->readStream(); + } + return $this->body; + } + + /** + * Read stream content and return it as string + * + * Function reads the remainder of the body from the stream and closes the stream. + * + * @return string + */ + protected function readStream() + { + if(!is_resource($this->stream)) { + return ''; + } + + if(isset($headers['content-length'])) { + $this->body = stream_get_contents($this->stream, $headers['content-length']); + } else { + $this->body = stream_get_contents($this->stream); + } + fclose($this->stream); + $this->stream = null; + } + + public function __destruct() + { + if(is_resource($this->stream)) { + fclose($this->stream); + $this->stream = null; + } + if($this->_cleanup) { + @unlink($this->stream_name); + } + } + +} |
