first take at a new horde_http_client API that allows alternate adapters
authorChuck Hagenbuch <chuck@horde.org>
Wed, 19 Aug 2009 17:08:10 +0000 (13:08 -0400)
committerChuck Hagenbuch <chuck@horde.org>
Wed, 19 Aug 2009 17:08:10 +0000 (13:08 -0400)
framework/Http_Client/lib/Horde/Http/Client.php
framework/Http_Client/lib/Horde/Http/Client/Adapter/Base.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client/Adapter/Curl.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client/Adapter/Fopen.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client/Adapter/Peclhttp.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client/Request.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client/Response.php
framework/Http_Client/package.xml

index faffc64..0e64cdb 100644 (file)
@@ -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 ?
  */
 
 /**
 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 (file)
index 0000000..95bf259
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Http_Client
+ */
+
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @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 (file)
index 0000000..1fc720c
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Http_Client
+ */
+
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @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 (file)
index 0000000..4cdc70e
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Http_Client
+ */
+
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @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 (file)
index 0000000..06ff3f3
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Http_Client
+ */
+
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @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 (file)
index 0000000..b5f699a
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Http_Client
+ */
+
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @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;
+    }
+}
index 12b39be..e158e42 100644 (file)
@@ -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
index ee48a78..fd1c3f2 100644 (file)
@@ -32,8 +32,15 @@ http://pear.php.net/dtd/package-2.0.xsd">
     <dir name="Horde">
      <dir name="Http">
       <dir name="Client">
+       <dir name="Adapter">
+        <file name="Base.php" role="php" />
+        <file name="Curl.php" role="php" />
+        <file name="Fopen.php" role="php" />
+        <file name="Peclhttp.php" role="php" />
+       </dir>
        <file name="Exception.php" role="php" />
        <file name="Mock.php" role="php" />
+       <file name="Request.php" role="php" />
        <file name="Response.php" role="php" />
       </dir> <!-- /lib/Horde/Http/Client -->
       <file name="Client.php" role="php" />
@@ -54,8 +61,13 @@ http://pear.php.net/dtd/package-2.0.xsd">
  </dependencies>
  <phprelease>
   <filelist>
+   <install name="lib/Horde/Http/Client/Adapter/Base.php" as="Horde/Http/Client/Adapter/Base.php" />
+   <install name="lib/Horde/Http/Client/Adapter/Curl.php" as="Horde/Http/Client/Adapter/Curl.php" />
+   <install name="lib/Horde/Http/Client/Adapter/Fopen.php" as="Horde/Http/Client/Adapter/Fopen.php" />
+   <install name="lib/Horde/Http/Client/Adapter/Peclhttp.php" as="Horde/Http/Client/Adapter/Peclhttp.php" />
    <install name="lib/Horde/Http/Client/Exception.php" as="Horde/Http/Client/Exception.php" />
    <install name="lib/Horde/Http/Client/Mock.php" as="Horde/Http/Client/Mock.php" />
+   <install name="lib/Horde/Http/Client/Request.php" as="Horde/Http/Client/Request.php" />
    <install name="lib/Horde/Http/Client/Response.php" as="Horde/Http/Client/Response.php" />
    <install name="lib/Horde/Http/Client.php" as="Horde/Http/Client.php" />
   </filelist>