add Http_Client
authorChuck Hagenbuch <chuck@horde.org>
Wed, 19 Nov 2008 03:49:32 +0000 (22:49 -0500)
committerChuck Hagenbuch <chuck@horde.org>
Wed, 19 Nov 2008 03:50:40 +0000 (22:50 -0500)
framework/Http_Client/examples/Horde/Http/Client/get-example-dot-com.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client/Exception.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client/Mock.php [new file with mode: 0644]
framework/Http_Client/lib/Horde/Http/Client/Response.php [new file with mode: 0644]
framework/Http_Client/package.xml [new file with mode: 0644]

diff --git a/framework/Http_Client/examples/Horde/Http/Client/get-example-dot-com.php b/framework/Http_Client/examples/Horde/Http/Client/get-example-dot-com.php
new file mode 100644 (file)
index 0000000..a73cd14
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Basic example for fetching a page with Horde_Http_Client
+ *
+ * Copyright 2007-2008 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
+ */
+
+require 'Horde/Http/Client.php';
+require 'Horde/Http/Client/Exception.php';
+require 'Horde/Http/Client/Response.php';
+
+$client = new Horde_Http_Client();
+$response = $client->GET('http://www.example.com/');
+var_dump($response);
+echo $response->getBody();
diff --git a/framework/Http_Client/lib/Horde/Http/Client.php b/framework/Http_Client/lib/Horde/Http/Client.php
new file mode 100644 (file)
index 0000000..3160fd0
--- /dev/null
@@ -0,0 +1,251 @@
+<?php
+/**
+ * Copyright 2007-2008 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
+ *
+ * @TODO - add support for http://pecl.php.net/package/pecl_http ?
+ */
+
+/**
+ * @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
+{
+    /**
+     * 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
+     */
+    protected $_proxyPass = null;
+
+    /**
+     * The most recent HTTP request
+     *
+     * An array with these values:
+     *   'uri'
+     *   'method'
+     *   'headers'
+     *   'data'
+     *
+     * @var array
+     */
+    protected $_lastRequest;
+
+    /**
+     * The most recent HTTP response
+     * @var Horde_Http_Client_Response
+     */
+    protected $_lastResponse;
+
+    /**
+     * Horde_Http_Client constructor.
+     *
+     * @param array $args Any Http_Client settings to initialize in the
+     * constructor. Available settings are:
+     *     uri
+     *     headers
+     *     proxyServer
+     *     proxyUser
+     *     proxyPass
+     */
+    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 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;
+    }
+
+    /**
+     * Send a GET request
+     *
+     * @return Horde_Http_Client_Response
+     */
+    public function get($uri = null, $headers = array())
+    {
+        return $this->request('GET', $uri, null, $headers);
+    }
+
+    /**
+     * Send a POST request
+     *
+     * @return Horde_Http_Client_Response
+     */
+    public function post($uri = null, $data = null, $headers = array())
+    {
+        return $this->request('POST', $uri, $data, $headers);
+    }
+
+    /**
+     * Send a PUT request
+     *
+     * @return Horde_Http_Client_Response
+     */
+    public function put($uri = null, $data = null, $headers = array())
+    {
+        // FIXME: suport method override (X-Method-Override: PUT).
+        return $this->request('PUT', $uri, $data, $headers);
+    }
+
+    /**
+     * Send a DELETE request
+     *
+     * @return Horde_Http_Client_Response
+     */
+    public function delete($uri = null, $headers = array())
+    {
+        // FIXME: suport method override (X-Method-Override: DELETE).
+        return $this->request('DELETE', $uri, null, $headers);
+    }
+
+    /**
+     * Send an HTTP request
+     *
+     * @param string $method HTTP request method (GET, PUT, etc.)
+     * @param string $uri URI to request, if different from $this->uri
+     * @param mixed $data Request data. Can be an array of form data that will be
+     *                    encoded automatically, or a raw string.
+     * @param array $headers Any headers specific to this request. They will
+     *                       be combined with $this->_headers, and override
+     *                       headers of the same name for this request only.
+     *
+     * @return Horde_Http_Client_Response
+     */
+    public function request($method, $uri = null, $data = null, $headers = array())
+    {
+        if (is_null($uri)) {
+            $uri = $this->uri;
+        }
+
+        if (is_array($data)) {
+            $data = http_build_query($data, '', '&');
+        }
+
+        $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,
+        );
+
+        // Stream context config.
+        $opts = array('http' => array(
+            'method' => $method,
+            'header' => implode("\n", $headers),
+            'content' => $data));
+
+        // Proxy settings
+        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);
+            }
+        }
+
+        $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);
+        return $this->_lastResponse;
+    }
+
+    /**
+     * Get a client 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 client 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;
+    }
+
+}
diff --git a/framework/Http_Client/lib/Horde/Http/Client/Exception.php b/framework/Http_Client/lib/Horde/Http/Client/Exception.php
new file mode 100644 (file)
index 0000000..9941e47
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Copyright 2007-2008 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_Exception extends Exception
+{
+    /**
+     * Constructor
+     */
+    public function __construct($message = null, $code_or_lasterror = 0)
+    {
+        if (is_array($code_or_lasterror)) {
+            if ($message) {
+                $message .= $code_or_lasterror['message'];
+            } else {
+                $message = $code_or_lasterror['message'];
+            }
+
+            $this->file = $code_or_lasterror['file'];
+            $this->line = $code_or_lasterror['line'];
+            $code = $code_or_lasterror['type'];
+        } else {
+            $code = $code_or_lasterror;
+        }
+
+        parent::__construct($message, $code);
+    }
+
+}
diff --git a/framework/Http_Client/lib/Horde/Http/Client/Mock.php b/framework/Http_Client/lib/Horde/Http/Client/Mock.php
new file mode 100644 (file)
index 0000000..e88bc6f
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Copyright 2007-2008 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_Mock extends Horde_Http_Client
+{
+    /**
+     * Array of mock responses
+     * @var array
+     */
+    protected $_responses = array();
+
+    /**
+     * Current mock response
+     * @var integer
+     */
+    protected $_responseIndex = 0;
+
+    /**
+     * Send an HTTP request
+     *
+     * @param string $method HTTP request method (GET, PUT, etc.)
+     * @param string $uri URI to request, if different from $this->uri
+     * @param mixed $data Request data. Can be an array of form data that will be
+     *                    encoded automatically, or a raw string.
+     * @param array $headers Any headers specific to this request. They will
+     *                       be combined with $this->_headers, and override
+     *                       headers of the same name for this request only.
+     *
+     * @return Horde_Http_Client_Response
+     *
+     * @TODO make lastRequest work somehow.
+     */
+    public function request($method, $uri = null, $data = null, $headers = array())
+    {
+        if ($this->_responseIndex >= count($this->_responses)) {
+            $this->_responseIndex = 0;
+        }
+        return $this->_responses[$this->_responseIndex++];
+    }
+
+    /**
+     * Set the HTTP response(s) to be returned by this adapter
+     *
+     * @param Horde_Http_Client_Response $response
+     */
+    public function setResponse($response)
+    {
+        $this->_responses = array($response);
+        $this->_responseIndex = 0;
+    }
+
+    /**
+     * Add another response to the response buffer.
+     *
+     * @param string $response
+     */
+    public function addResponse($response)
+    {
+        $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)) {
+            throw new OutOfBoundsException;
+        }
+        $this->_responseIndex = $index;
+    }
+
+}
diff --git a/framework/Http_Client/lib/Horde/Http/Client/Response.php b/framework/Http_Client/lib/Horde/Http/Client/Response.php
new file mode 100644 (file)
index 0000000..808d7b6
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Copyright 2007-2008 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_Response
+{
+    /**
+     * Fetched URI
+     * @var string
+     */
+    public $uri;
+
+    /**
+     * HTTP protocol version that was used
+     * @var float
+     */
+    public $httpVersion;
+
+    /**
+     * HTTP response code
+     * @var integer
+     */
+    public $code;
+
+    /**
+     * Response headers
+     * @var array
+     */
+    public $headers;
+
+    /**
+     * Response body
+     * @var stream
+     */
+    protected $_stream;
+
+    /**
+     * Constructor
+     */
+    public function __construct($uri, $stream, $headers = array())
+    {
+        $this->uri = $uri;
+        $this->_stream = $stream;
+        $this->_parseHeaders($headers);
+    }
+
+    /**
+     * Parse an array of response headers, mindful of line
+     * continuations, etc.
+     *
+     * @param array $headers
+     * @return array
+     */
+    protected function _parseHeaders($headers)
+    {
+        $lastHeader = null;
+        foreach ($headers as $headerLine) {
+            // stream_get_meta returns all headers generated while processing a
+            // request, including ones for redirects before an eventually successful
+            // request. We just want the last one, so whenever we hit a new HTTP
+            // header, throw out anything parsed previously and start over.
+            if (preg_match('/^HTTP\/(\d.\d) (\d{3})/', $headerLine, $httpMatches)) {
+                $this->httpVersion = $httpMatches[1];
+                $this->code = (int)$httpMatches[2];
+                $this->headers = array();
+                $lastHeader = null;
+            }
+
+            $headerLine = trim($headerLine, "\r\n");
+            if ($headerLine == '') {
+                break;
+            }
+            if (preg_match('|^([\w-]+):\s+(.+)|', $headerLine, $m)) {
+                unset($lastHeader);
+                $headerName = strtolower($m[1]);
+                $headerValue = $m[2];
+
+                if (isset($this->headers[$headerName])) {
+                    if (!is_array($this->headers[$headerName])) {
+                        $this->headers[$headerName] = array($this->headers[$headerName]);
+                    }
+
+                    $this->headers[$headerName][] = $headerValue;
+                } else {
+                    $this->headers[$headerName] = $headerValue;
+                }
+                $lastHeader = $headerName;
+            } elseif (preg_match("|^\s+(.+)$|", $headerLine, $m) && !is_null($lastHeader)) {
+                if (is_array($this->headers[$lastHeader])) {
+                    end($this->headers[$lastHeader]);
+                    $this->headers[$lastHeader][key($this->headers[$lastHeader])] .= $m[1];
+                } else {
+                    $this->headers[$lastHeader] .= $m[1];
+                }
+            }
+        }
+    }
+
+    /**
+     * Return the body of the HTTP response.
+     *
+     * @return string HTTP response body.
+     */
+    public function getBody()
+    {
+        $content = @stream_get_contents($this->_stream);
+        if ($content === false) {
+            throw new Horde_Http_Client_Exception('Problem reading data from ' . $this->uri . ': ' . $php_errormsg);
+        }
+        return $content;
+    }
+
+    /**
+     * Return a stream pointing to the response body that can be used
+     * with all standard PHP stream functions.
+     */
+    public function getStream()
+    {
+        return $this->_stream;
+    }
+
+    /**
+     * Get the value of a single response header.
+     *
+     * @param string $header Header name to get ('Content-Type', 'Content-Length', etc.). This is case sensitive.
+     *
+     * @return string HTTP header value.
+     */
+    public function getHeader($header)
+    {
+        return isset($this->headers[$header]) ? $this->headers[$header] : null;
+    }
+
+}
diff --git a/framework/Http_Client/package.xml b/framework/Http_Client/package.xml
new file mode 100644 (file)
index 0000000..ee48a78
--- /dev/null
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Http_Client</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde HTTP client libraries</summary>
+ <description>This package provides a light wrapper around PHP's stream support for performing HTTP requests.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-09-06</date>
+ <version>
+  <release>0.3.0</release>
+  <api>0.3.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/bsd-license.php">BSD</license>
+ <notes>* Refactor the constructor and setter/getter methods to be more generic.
+* Add support for proxy servers.</notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Http">
+      <dir name="Client">
+       <file name="Exception.php" role="php" />
+       <file name="Mock.php" role="php" />
+       <file name="Response.php" role="php" />
+      </dir> <!-- /lib/Horde/Http/Client -->
+      <file name="Client.php" role="php" />
+     </dir> <!-- /lib/Horde/Http -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib/ -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <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/Response.php" as="Horde/Http/Client/Response.php" />
+   <install name="lib/Horde/Http/Client.php" as="Horde/Http/Client.php" />
+  </filelist>
+ </phprelease>
+</package>