From 3998b7ea63418a33d50d1336e31a08cbacf1a15b Mon Sep 17 00:00:00 2001 From: Chuck Hagenbuch Date: Wed, 19 Aug 2009 13:08:10 -0400 Subject: [PATCH] first take at a new horde_http_client API that allows alternate adapters --- framework/Http_Client/lib/Horde/Http/Client.php | 187 +++++++-------------- .../lib/Horde/Http/Client/Adapter/Base.php | 74 ++++++++ .../lib/Horde/Http/Client/Adapter/Curl.php | 51 ++++++ .../lib/Horde/Http/Client/Adapter/Fopen.php | 78 +++++++++ .../lib/Horde/Http/Client/Adapter/Peclhttp.php | 62 +++++++ .../Http_Client/lib/Horde/Http/Client/Request.php | 109 ++++++++++++ .../Http_Client/lib/Horde/Http/Client/Response.php | 4 + framework/Http_Client/package.xml | 12 ++ 8 files changed, 450 insertions(+), 127 deletions(-) create mode 100644 framework/Http_Client/lib/Horde/Http/Client/Adapter/Base.php create mode 100644 framework/Http_Client/lib/Horde/Http/Client/Adapter/Curl.php create mode 100644 framework/Http_Client/lib/Horde/Http/Client/Adapter/Fopen.php create mode 100644 framework/Http_Client/lib/Horde/Http/Client/Adapter/Peclhttp.php create mode 100644 framework/Http_Client/lib/Horde/Http/Client/Request.php diff --git a/framework/Http_Client/lib/Horde/Http/Client.php b/framework/Http_Client/lib/Horde/Http/Client.php index faffc647a..0e64cdb41 100644 --- a/framework/Http_Client/lib/Horde/Http/Client.php +++ b/framework/Http_Client/lib/Horde/Http/Client.php @@ -6,8 +6,6 @@ * @license http://opensource.org/licenses/bsd-license.php BSD * @category Horde * @package Horde_Http_Client - * - * @TODO - add support for http://pecl.php.net/package/pecl_http ? */ /** @@ -19,51 +17,20 @@ class Horde_Http_Client { /** - * URI to make our next request to - * @var string - */ - protected $_uri = null; - - /** - * Request headers - * @var array - */ - protected $_headers = array(); - - /** - * Proxy server - * @var string - */ - protected $_proxyServer = null; - - /** - * Proxy username - * @var string - */ - protected $_proxyUser = null; - - /** - * Proxy password - * @var string + * HTTP Adapter to use for transport + * @var Horde_Http_Client_Adapter */ - protected $_proxyPass = null; + protected $_adapter; /** - * HTTP timeout - * @var fload + * The current HTTP request + * @var Horde_Http_Client_Request */ - protected $_timeout = 5; + protected $_request; /** - * The most recent HTTP request - * - * An array with these values: - * 'uri' - * 'method' - * 'headers' - * 'data' - * - * @var array + * The previous HTTP request + * @var Horde_Http_Client_Request */ protected $_lastRequest; @@ -78,52 +45,42 @@ class Horde_Http_Client * * @param array $args Any Http_Client settings to initialize in the * constructor. Available settings are: - * uri - * headers - * proxyServer - * proxyUser - * proxyPass + * adapter + * adapter.proxyServer + * adapter.proxyUser + * adapter.proxyPass + * adapter.timeout + * request + * request.uri + * request.headers + * request.method + * request.data */ public function __construct($args = array()) { - if (!ini_get('allow_url_fopen')) { - throw new Horde_Http_Client_Exception('allow_url_fopen must be enabled'); - } - - foreach ($args as $key => $val) { - $this->$key = $val; + // Set or create adapter object + if (isset($args['adapter'])) { + $this->_adapter = $args['adapter']; + unset($args['adapter']); + } else { + $this->_adapter = $this->_createBestAvailableAdapter(); } - } - /** - * Set one or more headers - * - * @param mixed $headers A hash of header + value pairs, or a single header name - * @param string $value A header value - */ - public function setHeaders($headers, $value = null) - { - if (!is_array($headers)) { - $headers = array($headers => $value); + // Set or create request object + if (isset($args['request'])) { + $this->_request = $args['request']; + unset($args['request']); + } else { + $this->_request = new Horde_Http_Client_Request(); } - foreach ($headers as $header => $value) { - $this->_headers[$header] = $value; + foreach ($args as $key => $val) { + list($object, $objectkey) = explode('.', $key, 2); + $this->$object->$objectkey = $val; } } /** - * Get the current value of $header - * - * @param string $header Header name to get - * @return string $header's current value - */ - public function getHeader($header) - { - return isset($this->_headers[$header]) ? $this->_headers[$header] : null; - } - - /** * Send a GET request * * @return Horde_Http_Client_Response @@ -191,56 +148,21 @@ class Horde_Http_Client */ public function request($method, $uri = null, $data = null, $headers = array()) { - if (is_null($uri)) { - $uri = $this->uri; + if ($method !== null) { + $this->request->method = $method; } - - if (is_array($data)) { - $data = http_build_query($data, '', '&'); + if ($uri !== null) { + $this->request->uri = $uri; } - - $headers = array_merge($this->_headers, $headers); - - // Store the last request for ease of debugging. - $this->_lastRequest = array( - 'uri' => $uri, - 'method' => $method, - 'headers' => $headers, - 'data' => $data, - ); - - $opts = array('http' => array()); - // Proxy settings - check first, so we can include the correct headers - if ($this->proxyServer) { - $opts['http']['proxy'] = 'tcp://' . $this->proxyServer; - $opts['http']['request_fulluri'] = true; - if ($this->proxyUser && $this->proxyPass) { - $headers['Proxy-Authorization'] = 'Basic ' . base64_encode($this->proxyUser . ':' . $this->proxyPass); - } + if ($data !== null) { + $this->request->data = $data; } - - // Concatenate the headers - $hdr = array(); - foreach ($headers as $header => $value) { - $hdr[] = $header . ': ' . $value; + if (count($headers)) { + $this->request->setHeaders($headers); } - // Stream context config. - $opts['http']['method'] = $method; - $opts['http']['header'] = implode("\n", $hdr); - $opts['http']['content'] = $data; - $opts['http']['timeout'] = $this->_timeout; - - $context = stream_context_create($opts); - $stream = @fopen($uri, 'rb', false, $context); - if (!$stream) { - throw new Horde_Http_Client_Exception('Problem with ' . $uri . ': ', error_get_last()); - } - - $meta = stream_get_meta_data($stream); - $headers = isset($meta['wrapper_data']) ? $meta['wrapper_data'] : array(); - - $this->_lastResponse = new Horde_Http_Client_Response($uri, $stream, $headers); + $this->_lastRequest = $this->_request; + $this->_lastResponse = $this->_adapter->send($this->_lastRequest); return $this->_lastResponse; } @@ -263,13 +185,24 @@ class Horde_Http_Client */ public function __set($name, $value) { - switch ($name) { - case 'headers': - $this->setHeaders($value); - break; - } - $this->{'_' . $name} = $value; } + /** + * Find the best available adapter + * + * @return Horde_Http_Client_Adapter + */ + public function _createBestAvailableAdapter() + { + /*if (class_exists('HttpRequest', false)) { + return new Horde_Http_Client_Adapter_Peclhttp(); + } else*/if (extension_loaded('curl')) { + return new Horde_Http_Client_Adapter_Curl(); + } elseif (ini_get('allow_url_fopen')) { + return new Horde_Http_Client_Adapter_Fopen(); + } else { + throw new Horde_Http_Client_Exception('No HTTP adapters are available. You must install pecl_http, curl, or enable allow_url_fopen.'); + } + } } diff --git a/framework/Http_Client/lib/Horde/Http/Client/Adapter/Base.php b/framework/Http_Client/lib/Horde/Http/Client/Adapter/Base.php new file mode 100644 index 000000000..95bf25956 --- /dev/null +++ b/framework/Http_Client/lib/Horde/Http/Client/Adapter/Base.php @@ -0,0 +1,74 @@ + + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ + +/** + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ +class Horde_Http_Client_Adapter_Base +{ + /** + * Proxy server + * @var string + */ + protected $_proxyServer = null; + + /** + * Proxy username + * @var string + */ + protected $_proxyUser = null; + + /** + * Proxy password + * @var string + */ + protected $_proxyPass = null; + + /** + * HTTP timeout + * @var float + */ + protected $_timeout = 5; + + /** + * Constructor + */ + public function __construct($args = array()) + { + foreach ($args as $key => $value) { + $this->$key = $value; + } + } + + /** + * Get an adapter parameter + * + * @param string $name The parameter to get. + * @return mixed Parameter value. + */ + public function __get($name) + { + return isset($this->{'_' . $name}) ? $this->{'_' . $name} : null; + } + + /** + * Set an adapter parameter + * + * @param string $name The parameter to set. + * @param mixed $value Parameter value. + */ + public function __set($name, $value) + { + $this->{'_' . $name} = $value; + } +} diff --git a/framework/Http_Client/lib/Horde/Http/Client/Adapter/Curl.php b/framework/Http_Client/lib/Horde/Http/Client/Adapter/Curl.php new file mode 100644 index 000000000..1fc720c43 --- /dev/null +++ b/framework/Http_Client/lib/Horde/Http/Client/Adapter/Curl.php @@ -0,0 +1,51 @@ + + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ + +/** + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ +class Horde_Http_Client_Adapter_Curl extends Horde_Http_Client_Adapter_Base +{ + public function __construct() + { + if (!extension_loaded('curl')) { + throw new Horde_Http_Client_Exception('The curl extension is not installed. See http://php.net/curl.installation'); + } + } + + /** + * Send an HTTP request + * + * @param Horde_Http_Client_Request HTTP Request object + * + * @return Horde_Http_Client_Response + */ + public function send($request) + { + $body = tmpfile(); + $headers = tmpfile(); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $request->uri); + curl_setopt($curl, CURLOPT_FILE, $body); + curl_setopt($curl, CURLOPT_POSTFIELDS, $request->data); + curl_setopt($curl, CURLOPT_WRITEHEADER, $headers); + + $result = curl_exec($curl); + + rewind($body); + rewind($headers); + + return new Horde_Http_Client_Response($request->uri, $body, stream_get_contents($headers)); + } +} diff --git a/framework/Http_Client/lib/Horde/Http/Client/Adapter/Fopen.php b/framework/Http_Client/lib/Horde/Http/Client/Adapter/Fopen.php new file mode 100644 index 000000000..4cdc70eb4 --- /dev/null +++ b/framework/Http_Client/lib/Horde/Http/Client/Adapter/Fopen.php @@ -0,0 +1,78 @@ + + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ + +/** + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ +class Horde_Http_Client_Adapter_Fopen extends Horde_Http_Client_Adapter_Base +{ + public function __construct() + { + if (!ini_get('allow_url_fopen')) { + throw new Horde_Http_Client_Exception('allow_url_fopen must be enabled'); + } + } + + /** + * Send an HTTP request + * + * @param Horde_Http_Client_Request HTTP Request object + * + * @return Horde_Http_Client_Response + */ + public function send($request) + { + $method = $request->method; + $uri = $request->uri; + $headers = $request->headers; + $data = $request->data; + if (is_array($data)) { + $data = http_build_query($data, '', '&'); + } + + $opts = array('http' => array()); + + // Proxy settings - check first, so we can include the correct headers + if ($this->proxyServer) { + $opts['http']['proxy'] = 'tcp://' . $this->proxyServer; + $opts['http']['request_fulluri'] = true; + if ($this->proxyUser && $this->proxyPass) { + $headers['Proxy-Authorization'] = 'Basic ' . base64_encode($this->proxyUser . ':' . $this->proxyPass); + } + } + + // Concatenate the headers + $hdr = array(); + foreach ($headers as $header => $value) { + $hdr[] = $header . ': ' . $value; + } + + // Stream context config. + $opts['http']['method'] = $method; + $opts['http']['header'] = implode("\n", $hdr); + $opts['http']['content'] = $data; + $opts['http']['timeout'] = $this->timeout; + + $context = stream_context_create($opts); + $stream = @fopen($uri, 'rb', false, $context); + if (!$stream) { + throw new Horde_Http_Client_Exception('Problem with ' . $uri . ': ', error_get_last()); + } + + $meta = stream_get_meta_data($stream); + $headers = isset($meta['wrapper_data']) ? $meta['wrapper_data'] : array(); + + return new Horde_Http_Client_Response($uri, $stream, $headers); + } + +} diff --git a/framework/Http_Client/lib/Horde/Http/Client/Adapter/Peclhttp.php b/framework/Http_Client/lib/Horde/Http/Client/Adapter/Peclhttp.php new file mode 100644 index 000000000..06ff3f33c --- /dev/null +++ b/framework/Http_Client/lib/Horde/Http/Client/Adapter/Peclhttp.php @@ -0,0 +1,62 @@ + + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ + +/** + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ +class Horde_Http_Client_Adapter_Peclhttp extends Horde_Http_Client_Adapter_Base +{ + public static $methods = array( + 'GET' => HTTP_METH_GET, + 'HEAD' => HTTP_METH_HEAD, + 'POST' => HTTP_METH_POST, + 'PUT' => HTTP_METH_PUT, + 'DELETE' => HTTP_METH_DELETE, + ); + + public function __construct() + { + if (!class_exists('HttpRequest', false)) { + throw new Horde_Http_Client_Exception('The pecl_http extension is not installed. See http://php.net/http.install'); + } + } + + /** + * Send an HTTP request + * + * @param Horde_Http_Client_Request HTTP Request object + * + * @return Horde_Http_Client_Response + */ + public function send($request) + { + $httpRequest = new HttpRequest($request->uri, self::$methods[$request->method]); + $httpRequest->setHeaders($request->headers); + + $data = $request->data; + if (is_array($data)) { + $httpRequest->setPostFields($data); + } else { + $httpRequest->setRawPostData($dat); + } + + try { + $httpResponse = $httpRequest->send(); + } catch (HttpException $e) { + throw new Horde_Http_Client_Exception($e->getMessage(), $e->getCode(), $e); + } + + /*@TODO build Horde_Http_Client_Response from $httpResponse */ + return $httpResponse; + } +} diff --git a/framework/Http_Client/lib/Horde/Http/Client/Request.php b/framework/Http_Client/lib/Horde/Http/Client/Request.php new file mode 100644 index 000000000..b5f699a34 --- /dev/null +++ b/framework/Http_Client/lib/Horde/Http/Client/Request.php @@ -0,0 +1,109 @@ + + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ + +/** + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php BSD + * @category Horde + * @package Horde_Http_Client + */ +class Horde_Http_Client_Request +{ + /** + * URI + * @var string + */ + protected $_uri; + + /** + * Request method + * @var string + */ + protected $_method = 'GET'; + + /** + * Request headers + * @var array + */ + protected $_headers = array(); + + /** + * Request data. Can be an array of form data that will be encoded + * automatically, or a raw string + * @var mixed + */ + protected $_data; + + /** + * Constructor + */ + public function __construct($args = array()) + { + foreach ($args as $key => $value) { + $this->$key = $value; + } + } + + /** + * Get a request parameter + * + * @param string $name The parameter to get. + * @return mixed Parameter value. + */ + public function __get($name) + { + return isset($this->{'_' . $name}) ? $this->{'_' . $name} : null; + } + + /** + * Set a request parameter + * + * @param string $name The parameter to set. + * @param mixed $value Parameter value. + */ + public function __set($name, $value) + { + switch ($name) { + case 'headers': + $this->setHeaders($value); + break; + } + + $this->{'_' . $name} = $value; + } + + /** + * Set one or more headers + * + * @param mixed $headers A hash of header + value pairs, or a single header name + * @param string $value A header value + */ + public function setHeaders($headers, $value = null) + { + if (!is_array($headers)) { + $headers = array($headers => $value); + } + + foreach ($headers as $header => $value) { + $this->_headers[$header] = $value; + } + } + + /** + * Get the current value of $header + * + * @param string $header Header name to get + * @return string $header's current value + */ + public function getHeader($header) + { + return isset($this->_headers[$header]) ? $this->_headers[$header] : null; + } +} diff --git a/framework/Http_Client/lib/Horde/Http/Client/Response.php b/framework/Http_Client/lib/Horde/Http/Client/Response.php index 12b39beb4..e158e422e 100644 --- a/framework/Http_Client/lib/Horde/Http/Client/Response.php +++ b/framework/Http_Client/lib/Horde/Http/Client/Response.php @@ -65,6 +65,10 @@ class Horde_Http_Client_Response */ protected function _parseHeaders($headers) { + if (!is_array($headers)) { + $headers = explode("\n", $headers); + } + $lastHeader = null; foreach ($headers as $headerLine) { // stream_get_meta returns all headers generated while processing a diff --git a/framework/Http_Client/package.xml b/framework/Http_Client/package.xml index ee48a78b8..fd1c3f24a 100644 --- a/framework/Http_Client/package.xml +++ b/framework/Http_Client/package.xml @@ -32,8 +32,15 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + @@ -54,8 +61,13 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + -- 2.11.0