* All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * @category HTTP * @package HTTP_Client * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License * @version CVS: $Id: Client.php,v 1.11 2008/10/11 14:29:27 avb Exp $ * @link http://pear.php.net/package/HTTP_Client */ /* * Do this define in your script if you wish HTTP_Client to follow browser * quirks rather than HTTP specification (RFC2616). This means: * - do a GET request after redirect with code 301, rather than use the * same method as before redirect. */ // define('HTTP_CLIENT_QUIRK_MODE', true); /** * Class for performing HTTP requests */ require_once 'HTTP/Request.php'; /** * Class used to store cookies and pass them between HTTP requests. */ require_once 'HTTP/Client/CookieManager.php'; /** * A simple HTTP client class. * * The class wraps around HTTP_Request providing a higher-level * API for performing multiple HTTP requests * * Note that the class implements all the methods defined by the Iterator * interface for going over the received responses. Being PHP4-compatible it is * not declared with 'implements Iterator', though. Therefore if you are * running PHP5 and want to use HTTP_Client in 'foreach' context you should do * the following: * * class PHP5_HTTP_Client extends HTTP_Client implements Iterator {} * * $client = new PHP5_HTTP_Client(); * * // ... perform requests ... * * foreach ($client as $url => $response) { * // do something * } * * * If you are running PHP4 you'll have to call the methods manually: * * for ($client->rewind(); $client->valid(); $client->next()) { * $url = $client->key(); * $response = $client->current(); * // do something * } * * * @category HTTP * @package HTTP_Client * @author Alexey Borzov * @version Release: 1.2.1 */ class HTTP_Client { /**#@+ * @access private */ /** * Cookie manager object * @var HTTP_Client_CookieManager */ var $_cookieManager; /** * Received HTTP responses * @var array */ var $_responses; /** * Default headers to send on every request * @var array */ var $_defaultHeaders = array(); /** * Default parameters for HTTP_Request's constructor * @var array */ var $_defaultRequestParams = array(); /** * How many redirects were done * @var integer */ var $_redirectCount = 0; /** * Maximum allowed redirects * @var integer */ var $_maxRedirects = 5; /** * Listeners attached to the client * @var array */ var $_listeners = array(); /** * Whether the listener should be propagated to Request objects * @var array */ var $_propagate = array(); /** * Whether to keep all the responses or just the most recent one * @var boolean */ var $_isHistoryEnabled = true; /** * Index for iteration over the responses * @var integer */ var $_idx = 0; /**#@-*/ /** * Constructor * * @access public * @param array Parameters to pass to HTTP_Request's constructor * @param array Default headers to send on every request * @param HTTP_Client_CookieManager Cookie manager object to use */ function HTTP_Client($defaultRequestParams = null, $defaultHeaders = null, $cookieManager = null) { if (!empty($cookieManager) && is_a($cookieManager, 'HTTP_Client_CookieManager')) { $this->_cookieManager = $cookieManager; } else { $this->_cookieManager =& new HTTP_Client_CookieManager(); } if (isset($defaultHeaders)) { $this->setDefaultHeader($defaultHeaders); } if (isset($defaultRequestParams)) { $this->setRequestParameter($defaultRequestParams); } } /** * Sets the maximum redirects that will be processed. * * Setting this to 0 disables redirect processing. If not 0 and the * number of redirects in a request is bigger than this number, then an * error will be raised. * * @access public * @param int Max number of redirects to process */ function setMaxRedirects($value) { $this->_maxRedirects = $value; } /** * Sets whether to keep all the responses or just the most recent one * * @access public * @param bool Whether to enable history */ function enableHistory($enable) { $this->_isHistoryEnabled = (bool)$enable; } /** * Creates a HTTP_Request objects, applying all the necessary defaults * * @param string URL * @param string Method, constants are defined in HTTP_Request * @param array Extra headers to send * @access private * @return HTTP_Request Request object with all defaults applied */ function &_createRequest($url, $method = HTTP_REQUEST_METHOD_GET, $headers = array()) { $req =& new HTTP_Request($url, $this->_defaultRequestParams); $req->setMethod($method); foreach ($this->_defaultHeaders as $name => $value) { $req->addHeader($name, $value); } foreach ($headers as $name => $value) { $req->addHeader($name, $value); } $this->_cookieManager->passCookies($req); foreach ($this->_propagate as $id => $propagate) { if ($propagate) { $req->attach($this->_listeners[$id]); } } return $req; } /** * Sends a 'HEAD' HTTP request * * @param string URL * @param array Extra headers to send * @access public * @return integer HTTP response code * @throws PEAR_Error */ function head($url, $headers = array()) { $request =& $this->_createRequest($url, HTTP_REQUEST_METHOD_HEAD, $headers); return $this->_performRequest($request); } /** * Sends a 'GET' HTTP request * * @param string URL * @param mixed additional data to send * @param boolean Whether the data is already urlencoded * @param array Extra headers to send * @access public * @return integer HTTP response code * @throws PEAR_Error */ function get($url, $data = null, $preEncoded = false, $headers = array()) { $request =& $this->_createRequest($url, HTTP_REQUEST_METHOD_GET, $headers); if (is_array($data)) { foreach ($data as $name => $value) { $request->addQueryString($name, $value, $preEncoded); } } elseif (isset($data)) { $request->addRawQueryString($data, $preEncoded); } return $this->_performRequest($request); } /** * Sends a 'POST' HTTP request * * @param string URL * @param mixed Data to send * @param boolean Whether the data is already urlencoded * @param array Files to upload. Elements of the array should have the form: * array(name, filename(s)[, content type]), see HTTP_Request::addFile() * @param array Extra headers to send * @access public * @return integer HTTP response code * @throws PEAR_Error */ function post($url, $data, $preEncoded = false, $files = array(), $headers = array()) { $request =& $this->_createRequest($url, HTTP_REQUEST_METHOD_POST, $headers); if (is_array($data)) { foreach ($data as $name => $value) { $request->addPostData($name, $value, $preEncoded); } } else { $request->addRawPostData($data, $preEncoded); } foreach ($files as $fileData) { $res = call_user_func_array(array(&$request, 'addFile'), $fileData); if (PEAR::isError($res)) { return $res; } } return $this->_performRequest($request); } /** * Sends a 'PUT' HTTP request * * @param string URL * @param string Request body * @param array Extra headers to send * @access public * @return integer HTTP response code * @throws PEAR_Error */ function put($url, $body = '', $headers = array()) { $request =& $this->_createRequest($url, HTTP_REQUEST_METHOD_PUT, $headers); $request->setBody($body); return $this->_performRequest($request); } /** * Sends a 'DELETE' HTTP request * * @param string URL * @param array Extra headers to send * @access public * @return integer HTTP response code * @throws PEAR_Error */ function delete($url, $headers = array()) { $request =& $this->_createRequest($url, HTTP_REQUEST_METHOD_DELETE, $headers); return $this->_performRequest($request); } /** * Sets default header(s) for HTTP requests * * @param mixed header name or array ('header name' => 'header value') * @param string header value if $name is not an array * @access public */ function setDefaultHeader($name, $value = null) { if (is_array($name)) { $this->_defaultHeaders = array_merge($this->_defaultHeaders, $name); } else { $this->_defaultHeaders[$name] = $value; } } /** * Sets parameter(s) for HTTP requests * * @param mixed parameter name or array ('parameter name' => 'parameter value') * @param string parameter value if $name is not an array * @access public */ function setRequestParameter($name, $value = null) { if (is_array($name)) { $this->_defaultRequestParams = array_merge($this->_defaultRequestParams, $name); } else { $this->_defaultRequestParams[$name] = $value; } } /** * Performs a request, processes redirects * * @param HTTP_Request Request object * @access private * @return integer HTTP response code * @throws PEAR_Error */ function _performRequest(&$request) { // If this is not a redirect, notify the listeners of new request if (0 == $this->_redirectCount && '' != $request->getUrl()) { $this->_notify('request', $request->getUrl()); } if (PEAR::isError($err = $request->sendRequest())) { $this->_redirectCount = 0; return $err; } $this->_pushResponse($request); $code = $request->getResponseCode(); if ($this->_maxRedirects > 0) { if (in_array($code, array(300, 301, 302, 303, 307))) { if ('' == ($location = $request->getResponseHeader('Location'))) { $this->_redirectCount = 0; return PEAR::raiseError("No 'Location' field on redirect"); } // Bug #5759: do not try to follow non-HTTP redirects if (null === ($redirectUrl = $this->_redirectUrl($request->_url, $location))) { $this->_redirectCount = 0; return $code; } // Redirect via tag, see request #5734 } elseif (200 <= $code && $code < 300) { $redirectUrl = $this->_getMetaRedirect($request); } } if (!empty($redirectUrl)) { if (++$this->_redirectCount > $this->_maxRedirects) { $this->_redirectCount = 0; return PEAR::raiseError('Too many redirects'); } // Notify of redirection $this->_notify('httpRedirect', $redirectUrl); // we access the private properties directly, as there are no accessors for them switch ($request->_method) { case HTTP_REQUEST_METHOD_POST: // Bug #13487: if doing a redirect via , use GET if (302 == $code || 303 == $code || $code < 300 || (301 == $code && defined('HTTP_CLIENT_QUIRK_MODE'))) { return $this->get($redirectUrl); } elseif (!empty($request->_postData) || !empty($request->_postFiles)) { $postFiles = array(); foreach ($request->_postFiles as $name => $data) { $postFiles[] = array($name, $data['name'], $data['type']); } return $this->post($redirectUrl, $request->_postData, true, $postFiles); } else { return $this->post($redirectUrl, $request->_body, true); } case HTTP_REQUEST_METHOD_HEAD: return (303 == $code? $this->get($redirectUrl): $this->head($redirectUrl)); case HTTP_REQUEST_METHOD_GET: default: return $this->get($redirectUrl); } // switch } else { $this->_redirectCount = 0; if (400 >= $code) { $this->_notify('httpSuccess'); $this->setDefaultHeader('Referer', $request->getUrl()); // some result processing should go here } else { $this->_notify('httpError'); } } return $code; } /** * Returns the most recent HTTP response * * To access previous responses use iteration methods * * @access public * @return array */ function ¤tResponse() { return $this->_responses[count($this->_responses) - 1]; } /** * Saves the server's response to responses list * * @param HTTP_Request Request object already containing the response * @access private */ function _pushResponse(&$request) { $this->_cookieManager->updateCookies($request); $idx = $this->_isHistoryEnabled? count($this->_responses): 0; $this->_responses[$idx] = array( 'url' => $request->getUrl(), 'code' => $request->getResponseCode(), 'headers' => $request->getResponseHeader(), 'body' => $request->getResponseBody() ); } /** * Clears object's internal properties * * @access public */ function reset() { $this->_cookieManager->reset(); $this->_responses = array(); $this->_defaultHeaders = array(); $this->_defaultRequestParams = array(); $this->_idx = 0; } /** * Adds a Listener to the list of listeners that are notified of * the object's events * * Events sent by HTTP_Client objects: * - 'request': sent on HTTP request that is not a redirect * - 'httpSuccess': sent when we receive a successfull 2xx response * - 'httpRedirect': sent when we receive a redirection response * - 'httpError': sent on 4xx, 5xx response * * @param HTTP_Request_Listener Listener to attach * @param boolean Whether the listener should be attached * to the created HTTP_Request objects * @return boolean whether the listener was successfully attached * @access public */ function attach(&$listener, $propagate = false) { if (!is_a($listener, 'HTTP_Request_Listener')) { return false; } $this->_listeners[$listener->getId()] =& $listener; $this->_propagate[$listener->getId()] = $propagate; return true; } /** * Removes a Listener from the list of listeners * * @param HTTP_Request_Listener Listener to detach * @return boolean Whether the listener was successfully detached * @access public */ function detach(&$listener) { if (!is_a($listener, 'HTTP_Request_Listener') || !isset($this->_listeners[$listener->getId()])) { return false; } unset($this->_listeners[$listener->getId()], $this->_propagate[$listener->getId()]); return true; } /** * Notifies all registered listeners of an event. * * @param string Event name * @param mixed Additional data * @access private */ function _notify($event, $data = null) { foreach (array_keys($this->_listeners) as $id) { $this->_listeners[$id]->update($this, $event, $data); } } /** * Calculates the absolute URL of a redirect * * @param Net_Url Object containing the request URL * @param string Value of the 'Location' response header * @return string|null Absolute URL we are being redirected to, null in case of non-HTTP URL * @access private */ function _redirectUrl($url, $location) { // If it begins with a scheme (as defined in RFC 2396) then it is absolute URI if (preg_match('/^([a-zA-Z][a-zA-Z0-9+.-]*):/', $location, $matches)) { // Bug #5759: we shouldn't try to follow non-HTTP redirects if ('http' == strtolower($matches[1]) || 'https' == strtolower($matches[1])) { return $location; } else { return null; } } else { if ('/' == $location[0]) { $url->path = Net_URL::resolvePath($location); } elseif('/' == substr($url->path, -1)) { $url->path = Net_URL::resolvePath($url->path . $location); } else { $dirname = (DIRECTORY_SEPARATOR == dirname($url->path)? '/': dirname($url->path)); $url->path = Net_URL::resolvePath($dirname . '/' . $location); } $url->querystring = array(); $url->anchor = ''; return $url->getUrl(); } } /** * Returns the cookie manager object (e.g. for storing it somewhere) * * @return HTTP_Client_CookieManager * @access public */ function getCookieManager() { return $this->_cookieManager; } /** * Tries to extract a redirect URL from <> tag (request #5734) * * @param HTTP_Request A request object already containing the response * @return string|null Absolute URI we are being redirected to, null if no redirect / invalid redirect * @access private */ function _getMetaRedirect(&$request) { // Non-HTML response or empty response body if ('text/html' != substr($request->getResponseHeader('content-type'), 0, 9) || '' == ($body = $request->getResponseBody())) { return null; } // No tag if (!preg_match('!]*http-equiv\\s*=\\s*("Refresh"|\'Refresh\'|Refresh)[^>]*)>!is', $body, $matches)) { return null; } // Just a refresh, no redirect if (!preg_match('!content\\s*=\\s*("[^"]+"|\'[^\']+\'|\\S+)!is', $matches[1], $urlMatches)) { return null; } $parts = explode(';', ('\'' == substr($urlMatches[1], 0, 1) || '"' == substr($urlMatches[1], 0, 1))? substr($urlMatches[1], 1, -1): $urlMatches[1]); if (empty($parts[1]) || !preg_match('/url\\s*=\\s*("[^"]+"|\'[^\']+\'|\\S+)/is', $parts[1], $urlMatches)) { return null; } $url = ('\'' == substr($urlMatches[1], 0, 1) || '"' == substr($urlMatches[1], 0, 1))? substr($urlMatches[1], 1, -1): $urlMatches[1]; // We do finally have an url... Now check that it's: // a) HTTP, b) not to the same page $previousUrl = $request->getUrl(); $redirectUrl = $this->_redirectUrl($request->_url, html_entity_decode($url)); return (null === $redirectUrl || $redirectUrl == $previousUrl)? null: $redirectUrl; } /** * Returns the current element (HTTP response) * * @access public * @return array */ function current() { $response = $this->_responses[$this->_idx]; unset($response['url']); return $response; } /** * Returns the key of the current element * * @access public * @return string URL producing the response */ function key() { return $this->_responses[$this->_idx]['url']; } /** * Moves forward to next element * * @access public */ function next() { $this->_idx++; } /** * Rewinds to the first element (response) * * @access public */ function rewind() { $this->_idx = 0; } /** * Checks if there is a current element after call to rewind() or next() * * @access public * @return bool */ function valid() { return $this->_idx < count($this->_responses); } } ?>