Changeset 6903 for temp/trunk
- Timestamp:
- 2006/10/26 18:34:21 (20 years ago)
- File:
-
- 1 edited
-
temp/trunk/data/module/Request.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
temp/trunk/data/module/Request.php
r6902 r6903 1 <?php2 // +-----------------------------------------------------------------------+3 // | Copyright (c) 2002-2003, Richard Heyes |4 // | All rights reserved. |5 // | |6 // | Redistribution and use in source and binary forms, with or without |7 // | modification, are permitted provided that the following conditions |8 // | are met: |9 // | |10 // | o Redistributions of source code must retain the above copyright |11 // | notice, this list of conditions and the following disclaimer. |12 // | o Redistributions in binary form must reproduce the above copyright |13 // | notice, this list of conditions and the following disclaimer in the |14 // | documentation and/or other materials provided with the distribution.|15 // | o The names of the authors may not be used to endorse or promote |16 // | products derived from this software without specific prior written |17 // | permission. |18 // | |19 // | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |20 // | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |21 // | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |22 // | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |23 // | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |24 // | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |25 // | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |26 // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |27 // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |28 // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |29 // | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |30 // | |31 // +-----------------------------------------------------------------------+32 // | Author: Richard Heyes <[email protected]> |33 // +-----------------------------------------------------------------------+34 //35 // $Id$36 //37 // HTTP_Request Class38 //39 // Simple example, (Fetches yahoo.com and displays it):40 //41 // $a = &new HTTP_Request('http://www.yahoo.com/');42 // $a->sendRequest();43 // echo $a->getResponseBody();44 //45 46 if(!defined('REQUEST_PHP_DIR')) {47 $REQUEST_PHP_DIR = realpath(dirname( __FILE__));48 define("REQUEST_PHP_DIR", $REQUEST_PHP_DIR);49 }50 51 //require_once REQUEST_PHP_DIR . '/PEAR.php';52 //require_once REQUEST_PHP_DIR . '/Net/Socket.php';53 //require_once REQUEST_PHP_DIR . '/Net/URL.php';54 55 require_once 'PEAR.php';56 require_once 'Net/Socket.php';57 require_once 'Net/URL.php';58 59 define('HTTP_REQUEST_METHOD_GET', 'GET', true);60 define('HTTP_REQUEST_METHOD_HEAD', 'HEAD', true);61 define('HTTP_REQUEST_METHOD_POST', 'POST', true);62 define('HTTP_REQUEST_METHOD_PUT', 'PUT', true);63 define('HTTP_REQUEST_METHOD_DELETE', 'DELETE', true);64 define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true);65 define('HTTP_REQUEST_METHOD_TRACE', 'TRACE', true);66 67 define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true);68 define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true);69 70 class HTTP_Request {71 72 /**73 * Instance of Net_URL74 * @var object Net_URL75 */76 var $_url;77 78 /**79 * Type of request80 * @var string81 */82 var $_method;83 84 /**85 * HTTP Version86 * @var string87 */88 var $_http;89 90 /**91 * Request headers92 * @var array93 */94 var $_requestHeaders;95 96 /**97 * Basic Auth Username98 * @var string99 */100 var $_user;101 102 /**103 * Basic Auth Password104 * @var string105 */106 var $_pass;107 108 /**109 * Socket object110 * @var object Net_Socket111 */112 var $_sock;113 114 /**115 * Proxy server116 * @var string117 */118 var $_proxy_host;119 120 /**121 * Proxy port122 * @var integer123 */124 var $_proxy_port;125 126 /**127 * Proxy username128 * @var string129 */130 var $_proxy_user;131 132 /**133 * Proxy password134 * @var string135 */136 var $_proxy_pass;137 138 /**139 * Post data140 * @var array141 */142 var $_postData;143 144 /**145 * Request body146 * @var string147 */148 var $_body;149 150 /**151 * A list of methods that MUST NOT have a request body, per RFC 2616152 * @var array153 */154 var $_bodyDisallowed = array('TRACE');155 156 /**157 * Files to post158 * @var array159 */160 var $_postFiles = array();161 162 /**163 * Connection timeout.164 * @var float165 */166 var $_timeout;167 168 /**169 * HTTP_Response object170 * @var object HTTP_Response171 */172 var $_response;173 174 /**175 * Whether to allow redirects176 * @var boolean177 */178 var $_allowRedirects;179 180 /**181 * Maximum redirects allowed182 * @var integer183 */184 var $_maxRedirects;185 186 /**187 * Current number of redirects188 * @var integer189 */190 var $_redirects;191 192 /**193 * Whether to append brackets [] to array variables194 * @var bool195 */196 var $_useBrackets = true;197 198 /**199 * Attached listeners200 * @var array201 */202 var $_listeners = array();203 204 /**205 * Whether to save response body in response object property206 * @var bool207 */208 var $_saveBody = true;209 210 /**211 * Timeout for reading from socket (array(seconds, microseconds))212 * @var array213 */214 var $_readTimeout = null;215 216 /**217 * Options to pass to Net_Socket::connect. See stream_context_create218 * @var array219 */220 var $_socketOptions = null;221 222 /**223 * Constructor224 *225 * Sets up the object226 * @param string The url to fetch/access227 * @param array Associative array of parameters which can have the following keys:228 * <ul>229 * <li>method - Method to use, GET, POST etc (string)</li>230 * <li>http - HTTP Version to use, 1.0 or 1.1 (string)</li>231 * <li>user - Basic Auth username (string)</li>232 * <li>pass - Basic Auth password (string)</li>233 * <li>proxy_host - Proxy server host (string)</li>234 * <li>proxy_port - Proxy server port (integer)</li>235 * <li>proxy_user - Proxy auth username (string)</li>236 * <li>proxy_pass - Proxy auth password (string)</li>237 * <li>timeout - Connection timeout in seconds (float)</li>238 * <li>allowRedirects - Whether to follow redirects or not (bool)</li>239 * <li>maxRedirects - Max number of redirects to follow (integer)</li>240 * <li>useBrackets - Whether to append [] to array variable names (bool)</li>241 * <li>saveBody - Whether to save response body in response object property (bool)</li>242 * <li>readTimeout - Timeout for reading / writing data over the socket (array (seconds, microseconds))</li>243 * <li>socketOptions - Options to pass to Net_Socket object (array)</li>244 * </ul>245 * @access public246 */247 function HTTP_Request($url = '', $params = array())248 {249 $this->_method = HTTP_REQUEST_METHOD_GET;250 $this->_http = HTTP_REQUEST_HTTP_VER_1_1;251 $this->_requestHeaders = array();252 $this->_postData = array();253 $this->_body = null;254 255 $this->_user = null;256 $this->_pass = null;257 258 $this->_proxy_host = null;259 $this->_proxy_port = null;260 $this->_proxy_user = null;261 $this->_proxy_pass = null;262 263 $this->_allowRedirects = false;264 $this->_maxRedirects = 3;265 $this->_redirects = 0;266 267 $this->_timeout = null;268 $this->_response = null;269 270 foreach ($params as $key => $value) {271 $this->{'_' . $key} = $value;272 }273 274 if (!empty($url)) {275 $this->setURL($url);276 }277 278 // Default useragent279 $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )');280 281 // We don't do keep-alives by default282 $this->addHeader('Connection', 'close');283 284 // Basic authentication285 if (!empty($this->_user)) {286 $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass));287 }288 289 // Proxy authentication (see bug #5913)290 if (!empty($this->_proxy_user)) {291 $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass));292 }293 294 // Use gzip encoding if possible295 // Avoid gzip encoding if using multibyte functions (see #1781)296 if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib') &&297 0 == (2 & ini_get('mbstring.func_overload'))) {298 299 $this->addHeader('Accept-Encoding', 'gzip');300 }301 }302 303 /**304 * Generates a Host header for HTTP/1.1 requests305 *306 * @access private307 * @return string308 */309 function _generateHostHeader()310 {311 if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) {312 $host = $this->_url->host . ':' . $this->_url->port;313 314 } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) {315 $host = $this->_url->host . ':' . $this->_url->port;316 317 } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) {318 $host = $this->_url->host . ':' . $this->_url->port;319 320 } else {321 $host = $this->_url->host;322 }323 324 return $host;325 }326 327 /**328 * Resets the object to its initial state (DEPRECATED).329 * Takes the same parameters as the constructor.330 *331 * @param string $url The url to be requested332 * @param array $params Associative array of parameters333 * (see constructor for details)334 * @access public335 * @deprecated deprecated since 1.2, call the constructor if this is necessary336 */337 function reset($url, $params = array())338 {339 $this->HTTP_Request($url, $params);340 }341 342 /**343 * Sets the URL to be requested344 *345 * @param string The url to be requested346 * @access public347 */348 function setURL($url)349 {350 $this->_url = &new Net_URL($url, $this->_useBrackets);351 352 if (!empty($this->_url->user) || !empty($this->_url->pass)) {353 $this->setBasicAuth($this->_url->user, $this->_url->pass);354 }355 356 if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) {357 $this->addHeader('Host', $this->_generateHostHeader());358 }359 360 // set '/' instead of empty path rather than check later (see bug #8662)361 if (empty($this->_url->path)) {362 $this->_url->path = '/';363 }364 }365 366 /**367 * Returns the current request URL368 *369 * @return string Current request URL370 * @access public371 */372 function getUrl($url)373 {374 return empty($this->_url)? '': $this->_url->getUrl();375 }376 377 /**378 * Sets a proxy to be used379 *380 * @param string Proxy host381 * @param int Proxy port382 * @param string Proxy username383 * @param string Proxy password384 * @access public385 */386 function setProxy($host, $port = 8080, $user = null, $pass = null)387 {388 $this->_proxy_host = $host;389 $this->_proxy_port = $port;390 $this->_proxy_user = $user;391 $this->_proxy_pass = $pass;392 393 if (!empty($user)) {394 $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass));395 }396 }397 398 /**399 * Sets basic authentication parameters400 *401 * @param string Username402 * @param string Password403 */404 function setBasicAuth($user, $pass)405 {406 $this->_user = $user;407 $this->_pass = $pass;408 409 $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass));410 }411 412 /**413 * Sets the method to be used, GET, POST etc.414 *415 * @param string Method to use. Use the defined constants for this416 * @access public417 */418 function setMethod($method)419 {420 $this->_method = $method;421 }422 423 /**424 * Sets the HTTP version to use, 1.0 or 1.1425 *426 * @param string Version to use. Use the defined constants for this427 * @access public428 */429 function setHttpVer($http)430 {431 $this->_http = $http;432 }433 434 /**435 * Adds a request header436 *437 * @param string Header name438 * @param string Header value439 * @access public440 */441 function addHeader($name, $value)442 {443 $this->_requestHeaders[strtolower($name)] = $value;444 }445 446 /**447 * Removes a request header448 *449 * @param string Header name to remove450 * @access public451 */452 function removeHeader($name)453 {454 if (isset($this->_requestHeaders[strtolower($name)])) {455 unset($this->_requestHeaders[strtolower($name)]);456 }457 }458 459 /**460 * Adds a querystring parameter461 *462 * @param string Querystring parameter name463 * @param string Querystring parameter value464 * @param bool Whether the value is already urlencoded or not, default = not465 * @access public466 */467 function addQueryString($name, $value, $preencoded = false)468 {469 $this->_url->addQueryString($name, $value, $preencoded);470 }471 472 /**473 * Sets the querystring to literally what you supply474 *475 * @param string The querystring data. Should be of the format foo=bar&x=y etc476 * @param bool Whether data is already urlencoded or not, default = already encoded477 * @access public478 */479 function addRawQueryString($querystring, $preencoded = true)480 {481 $this->_url->addRawQueryString($querystring, $preencoded);482 }483 484 /**485 * Adds postdata items486 *487 * @param string Post data name488 * @param string Post data value489 * @param bool Whether data is already urlencoded or not, default = not490 * @access public491 */492 function addPostData($name, $value, $preencoded = false)493 {494 if ($preencoded) {495 $this->_postData[$name] = $value;496 } else {497 $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value);498 }499 }500 501 /**502 * Recursively applies the callback function to the value503 *504 * @param mixed Callback function505 * @param mixed Value to process506 * @access private507 * @return mixed Processed value508 */509 function _arrayMapRecursive($callback, $value)510 {511 if (!is_array($value)) {512 return call_user_func($callback, $value);513 } else {514 $map = array();515 foreach ($value as $k => $v) {516 $map[$k] = $this->_arrayMapRecursive($callback, $v);517 }518 return $map;519 }520 }521 522 /**523 * Adds a file to upload524 *525 * This also changes content-type to 'multipart/form-data' for proper upload526 *527 * @access public528 * @param string name of file-upload field529 * @param mixed file name(s)530 * @param mixed content-type(s) of file(s) being uploaded531 * @return bool true on success532 * @throws PEAR_Error533 */534 function addFile($inputName, $fileName, $contentType = 'application/octet-stream')535 {536 if (!is_array($fileName) && !is_readable($fileName)) {537 return PEAR::raiseError("File '{$fileName}' is not readable");538 } elseif (is_array($fileName)) {539 foreach ($fileName as $name) {540 if (!is_readable($name)) {541 return PEAR::raiseError("File '{$name}' is not readable");542 }543 }544 }545 $this->addHeader('Content-Type', 'multipart/form-data');546 $this->_postFiles[$inputName] = array(547 'name' => $fileName,548 'type' => $contentType549 );550 return true;551 }552 553 /**554 * Adds raw postdata (DEPRECATED)555 *556 * @param string The data557 * @param bool Whether data is preencoded or not, default = already encoded558 * @access public559 * @deprecated deprecated since 1.3.0, method setBody() should be used instead560 */561 function addRawPostData($postdata, $preencoded = true)562 {563 $this->_body = $preencoded ? $postdata : urlencode($postdata);564 }565 566 /**567 * Sets the request body (for POST, PUT and similar requests)568 *569 * @param string Request body570 * @access public571 */572 function setBody($body)573 {574 $this->_body = $body;575 }576 577 /**578 * Clears any postdata that has been added (DEPRECATED).579 *580 * Useful for multiple request scenarios.581 *582 * @access public583 * @deprecated deprecated since 1.2584 */585 function clearPostData()586 {587 $this->_postData = null;588 }589 590 /**591 * Appends a cookie to "Cookie:" header592 *593 * @param string $name cookie name594 * @param string $value cookie value595 * @access public596 */597 function addCookie($name, $value)598 {599 $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : '';600 $this->addHeader('Cookie', $cookies . $name . '=' . $value);601 }602 603 /**604 * Clears any cookies that have been added (DEPRECATED).605 *606 * Useful for multiple request scenarios607 *608 * @access public609 * @deprecated deprecated since 1.2610 */611 function clearCookies()612 {613 $this->removeHeader('Cookie');614 }615 616 /**617 * Sends the request618 *619 * @access public620 * @param bool Whether to store response body in Response object property,621 * set this to false if downloading a LARGE file and using a Listener622 * @return mixed PEAR error on error, true otherwise623 */624 function sendRequest($saveBody = true)625 {626 if (!is_a($this->_url, 'Net_URL')) {627 return PEAR::raiseError('No URL given.');628 }629 630 $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host;631 $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port;632 633 // 4.3.0 supports SSL connections using OpenSSL. The function test determines634 // we running on at least 4.3.0635 if (strcasecmp($this->_url->protocol, 'https') == 0 AND function_exists('file_get_contents') AND extension_loaded('openssl')) {636 if (isset($this->_proxy_host)) {637 return PEAR::raiseError('HTTPS proxies are not supported.');638 }639 $host = 'ssl://' . $host;640 }641 642 // magic quotes may fuck up file uploads and chunked response processing643 $magicQuotes = ini_get('magic_quotes_runtime');644 ini_set('magic_quotes_runtime', false);645 646 // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive647 // connection token to a proxy server...648 if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) &&649 'Keep-Alive' == $this->_requestHeaders['connection'])650 {651 $this->removeHeader('connection');652 }653 654 $keepAlive = (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection'])) ||655 (!empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']);656 $sockets = &PEAR::getStaticProperty('HTTP_Request', 'sockets');657 $sockKey = $host . ':' . $port;658 unset($this->_sock);659 660 // There is a connected socket in the "static" property?661 if ($keepAlive && !empty($sockets[$sockKey]) &&662 !empty($sockets[$sockKey]->fp))663 {664 $this->_sock =& $sockets[$sockKey];665 $err = null;666 } else {667 $this->_notify('connect');668 $this->_sock =& new Net_Socket();669 $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions);670 }671 PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest());672 673 if (!PEAR::isError($err)) {674 if (!empty($this->_readTimeout)) {675 $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]);676 }677 678 $this->_notify('sentRequest');679 680 // Read the response681 $this->_response = &new HTTP_Response($this->_sock, $this->_listeners);682 $err = $this->_response->process(683 $this->_saveBody && $saveBody,684 HTTP_REQUEST_METHOD_HEAD != $this->_method685 );686 687 if ($keepAlive) {688 $keepAlive = (isset($this->_response->_headers['content-length'])689 || (isset($this->_response->_headers['transfer-encoding'])690 && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked'));691 if ($keepAlive) {692 if (isset($this->_response->_headers['connection'])) {693 $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive';694 } else {695 $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol;696 }697 }698 }699 }700 701 ini_set('magic_quotes_runtime', $magicQuotes);702 703 if (PEAR::isError($err)) {704 return $err;705 }706 707 if (!$keepAlive) {708 $this->disconnect();709 // Store the connected socket in "static" property710 } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) {711 $sockets[$sockKey] =& $this->_sock;712 }713 714 // Check for redirection715 if ( $this->_allowRedirects716 AND $this->_redirects <= $this->_maxRedirects717 AND $this->getResponseCode() > 300718 AND $this->getResponseCode() < 399719 AND !empty($this->_response->_headers['location'])) {720 721 722 $redirect = $this->_response->_headers['location'];723 724 // Absolute URL725 if (preg_match('/^https?:\/\//i', $redirect)) {726 $this->_url = &new Net_URL($redirect);727 $this->addHeader('Host', $this->_generateHostHeader());728 // Absolute path729 } elseif ($redirect{0} == '/') {730 $this->_url->path = $redirect;731 732 // Relative path733 } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') {734 if (substr($this->_url->path, -1) == '/') {735 $redirect = $this->_url->path . $redirect;736 } else {737 $redirect = dirname($this->_url->path) . '/' . $redirect;738 }739 $redirect = Net_URL::resolvePath($redirect);740 $this->_url->path = $redirect;741 742 // Filename, no path743 } else {744 if (substr($this->_url->path, -1) == '/') {745 $redirect = $this->_url->path . $redirect;746 } else {747 $redirect = dirname($this->_url->path) . '/' . $redirect;748 }749 $this->_url->path = $redirect;750 }751 752 $this->_redirects++;753 return $this->sendRequest($saveBody);754 755 // Too many redirects756 } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) {757 return PEAR::raiseError('Too many redirects');758 }759 760 return true;761 }762 763 /**764 * Disconnect the socket, if connected. Only useful if using Keep-Alive.765 *766 * @access public767 */768 function disconnect()769 {770 if (!empty($this->_sock) && !empty($this->_sock->fp)) {771 $this->_notify('disconnect');772 $this->_sock->disconnect();773 }774 }775 776 /**777 * Returns the response code778 *779 * @access public780 * @return mixed Response code, false if not set781 */782 function getResponseCode()783 {784 return isset($this->_response->_code) ? $this->_response->_code : false;785 }786 787 /**788 * Returns either the named header or all if no name given789 *790 * @access public791 * @param string The header name to return, do not set to get all headers792 * @return mixed either the value of $headername (false if header is not present)793 * or an array of all headers794 */795 function getResponseHeader($headername = null)796 {797 if (!isset($headername)) {798 return isset($this->_response->_headers)? $this->_response->_headers: array();799 } else {800 $headername = strtolower($headername);801 return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false;802 }803 }804 805 /**806 * Returns the body of the response807 *808 * @access public809 * @return mixed response body, false if not set810 */811 function getResponseBody()812 {813 return isset($this->_response->_body) ? $this->_response->_body : false;814 }815 816 /**817 * Returns cookies set in response818 *819 * @access public820 * @return mixed array of response cookies, false if none are present821 */822 function getResponseCookies()823 {824 return isset($this->_response->_cookies) ? $this->_response->_cookies : false;825 }826 827 /**828 * Builds the request string829 *830 * @access private831 * @return string The request string832 */833 function _buildRequest()834 {835 $separator = ini_get('arg_separator.output');836 ini_set('arg_separator.output', '&');837 $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : '';838 ini_set('arg_separator.output', $separator);839 840 $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : '';841 $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : '';842 $path = $this->_url->path . $querystring;843 $url = $host . $port . $path;844 845 $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n";846 847 if (in_array($this->_method, $this->_bodyDisallowed) ||848 (empty($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method ||849 (empty($this->_postData) && empty($this->_postFiles)))))850 {851 $this->removeHeader('Content-Type');852 } else {853 if (empty($this->_requestHeaders['content-type'])) {854 // Add default content-type855 $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');856 } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) {857 $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime());858 $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);859 }860 }861 862 // Request Headers863 if (!empty($this->_requestHeaders)) {864 foreach ($this->_requestHeaders as $name => $value) {865 $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));866 $request .= $canonicalName . ': ' . $value . "\r\n";867 }868 }869 870 // No post data or wrong method, so simply add a final CRLF871 if (in_array($this->_method, $this->_bodyDisallowed) ||872 (HTTP_REQUEST_METHOD_POST != $this->_method && empty($this->_body))) {873 874 $request .= "\r\n";875 876 // Post data if it's an array877 } elseif (HTTP_REQUEST_METHOD_POST == $this->_method &&878 (!empty($this->_postData) || !empty($this->_postFiles))) {879 880 // "normal" POST request881 if (!isset($boundary)) {882 $postdata = implode('&', array_map(883 create_function('$a', 'return $a[0] . \'=\' . $a[1];'),884 $this->_flattenArray('', $this->_postData)885 ));886 887 // multipart request, probably with file uploads888 } else {889 $postdata = '';890 if (!empty($this->_postData)) {891 $flatData = $this->_flattenArray('', $this->_postData);892 foreach ($flatData as $item) {893 $postdata .= '--' . $boundary . "\r\n";894 $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"';895 $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n";896 }897 }898 foreach ($this->_postFiles as $name => $value) {899 if (is_array($value['name'])) {900 $varname = $name . ($this->_useBrackets? '[]': '');901 } else {902 $varname = $name;903 $value['name'] = array($value['name']);904 }905 foreach ($value['name'] as $key => $filename) {906 $fp = fopen($filename, 'r');907 $data = fread($fp, filesize($filename));908 fclose($fp);909 $basename = basename($filename);910 $type = is_array($value['type'])? @$value['type'][$key]: $value['type'];911 912 $postdata .= '--' . $boundary . "\r\n";913 $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"';914 $postdata .= "\r\nContent-Type: " . $type;915 $postdata .= "\r\n\r\n" . $data . "\r\n";916 }917 }918 $postdata .= '--' . $boundary . "--\r\n";919 }920 $request .= 'Content-Length: ' . strlen($postdata) . "\r\n\r\n";921 $request .= $postdata;922 923 // Explicitly set request body924 } elseif (!empty($this->_body)) {925 926 $request .= 'Content-Length: ' . strlen($this->_body) . "\r\n\r\n";927 $request .= $this->_body;928 }929 930 return $request;931 }932 933 /**934 * Helper function to change the (probably multidimensional) associative array935 * into the simple one.936 *937 * @param string name for item938 * @param mixed item's values939 * @return array array with the following items: array('item name', 'item value');940 */941 function _flattenArray($name, $values)942 {943 if (!is_array($values)) {944 return array(array($name, $values));945 } else {946 $ret = array();947 foreach ($values as $k => $v) {948 if (empty($name)) {949 $newName = $k;950 } elseif ($this->_useBrackets) {951 $newName = $name . '[' . $k . ']';952 } else {953 $newName = $name;954 }955 $ret = array_merge($ret, $this->_flattenArray($newName, $v));956 }957 return $ret;958 }959 }960 961 962 /**963 * Adds a Listener to the list of listeners that are notified of964 * the object's events965 *966 * @param object HTTP_Request_Listener instance to attach967 * @return boolean whether the listener was successfully attached968 * @access public969 */970 function attach(&$listener)971 {972 if (!is_a($listener, 'HTTP_Request_Listener')) {973 return false;974 }975 $this->_listeners[$listener->getId()] =& $listener;976 return true;977 }978 979 980 /**981 * Removes a Listener from the list of listeners982 *983 * @param object HTTP_Request_Listener instance to detach984 * @return boolean whether the listener was successfully detached985 * @access public986 */987 function detach(&$listener)988 {989 if (!is_a($listener, 'HTTP_Request_Listener') ||990 !isset($this->_listeners[$listener->getId()])) {991 return false;992 }993 unset($this->_listeners[$listener->getId()]);994 return true;995 }996 997 998 /**999 * Notifies all registered listeners of an event.1000 *1001 * Events sent by HTTP_Request object1002 * - 'connect': on connection to server1003 * - 'sentRequest': after the request was sent1004 * - 'disconnect': on disconnection from server1005 *1006 * Events sent by HTTP_Response object1007 * - 'gotHeaders': after receiving response headers (headers are passed in $data)1008 * - 'tick': on receiving a part of response body (the part is passed in $data)1009 * - 'gzTick': on receiving a gzip-encoded part of response body (ditto)1010 * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped)1011 *1012 * @param string Event name1013 * @param mixed Additional data1014 * @access private1015 */1016 function _notify($event, $data = null)1017 {1018 foreach (array_keys($this->_listeners) as $id) {1019 $this->_listeners[$id]->update($this, $event, $data);1020 }1021 }1022 }1023 1024 1025 /**1026 * Response class to complement the Request class1027 */1028 class HTTP_Response1029 {1030 /**1031 * Socket object1032 * @var object1033 */1034 var $_sock;1035 1036 /**1037 * Protocol1038 * @var string1039 */1040 var $_protocol;1041 1042 /**1043 * Return code1044 * @var string1045 */1046 var $_code;1047 1048 /**1049 * Response headers1050 * @var array1051 */1052 var $_headers;1053 1054 /**1055 * Cookies set in response1056 * @var array1057 */1058 var $_cookies;1059 1060 /**1061 * Response body1062 * @var string1063 */1064 var $_body = '';1065 1066 /**1067 * Used by _readChunked(): remaining length of the current chunk1068 * @var string1069 */1070 var $_chunkLength = 0;1071 1072 /**1073 * Attached listeners1074 * @var array1075 */1076 var $_listeners = array();1077 1078 /**1079 * Bytes left to read from message-body1080 * @var null|int1081 */1082 var $_toRead;1083 1084 /**1085 * Constructor1086 *1087 * @param object Net_Socket socket to read the response from1088 * @param array listeners attached to request1089 * @return mixed PEAR Error on error, true otherwise1090 */1091 function HTTP_Response(&$sock, &$listeners)1092 {1093 $this->_sock =& $sock;1094 $this->_listeners =& $listeners;1095 }1096 1097 1098 /**1099 * Processes a HTTP response1100 *1101 * This extracts response code, headers, cookies and decodes body if it1102 * was encoded in some way1103 *1104 * @access public1105 * @param bool Whether to store response body in object property, set1106 * this to false if downloading a LARGE file and using a Listener.1107 * This is assumed to be true if body is gzip-encoded.1108 * @param bool Whether the response can actually have a message-body.1109 * Will be set to false for HEAD requests.1110 * @throws PEAR_Error1111 * @return mixed true on success, PEAR_Error in case of malformed response1112 */1113 function process($saveBody = true, $canHaveBody = true)1114 {1115 do {1116 $line = $this->_sock->readLine();1117 if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) {1118 return PEAR::raiseError('Malformed response.');1119 } else {1120 $this->_protocol = 'HTTP/' . $http_version;1121 $this->_code = intval($returncode);1122 }1123 while ('' !== ($header = $this->_sock->readLine())) {1124 $this->_processHeader($header);1125 }1126 } while (100 == $this->_code);1127 1128 $this->_notify('gotHeaders', $this->_headers);1129 1130 // RFC 2616, section 4.4:1131 // 1. Any response message which "MUST NOT" include a message-body ...1132 // is always terminated by the first empty line after the header fields1133 // 3. ... If a message is received with both a1134 // Transfer-Encoding header field and a Content-Length header field,1135 // the latter MUST be ignored.1136 $canHaveBody = $canHaveBody && $this->_code >= 200 &&1137 $this->_code != 204 && $this->_code != 304;1138 1139 // If response body is present, read it and decode1140 $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']);1141 $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']);1142 $hasBody = false;1143 if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) ||1144 0 != $this->_headers['content-length']))1145 {1146 if ($chunked || !isset($this->_headers['content-length'])) {1147 $this->_toRead = null;1148 } else {1149 $this->_toRead = $this->_headers['content-length'];1150 }1151 while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) {1152 if ($chunked) {1153 $data = $this->_readChunked();1154 } elseif (is_null($this->_toRead)) {1155 $data = $this->_sock->read(4096);1156 } else {1157 $data = $this->_sock->read(min(4096, $this->_toRead));1158 $this->_toRead -= strlen($data);1159 }1160 if ('' == $data) {1161 break;1162 } else {1163 $hasBody = true;1164 if ($saveBody || $gzipped) {1165 $this->_body .= $data;1166 }1167 $this->_notify($gzipped? 'gzTick': 'tick', $data);1168 }1169 }1170 }1171 1172 if ($hasBody) {1173 // Uncompress the body if needed1174 if ($gzipped) {1175 $body = $this->_decodeGzip($this->_body);1176 if (PEAR::isError($body)) {1177 return $body;1178 }1179 $this->_body = $body;1180 $this->_notify('gotBody', $this->_body);1181 } else {1182 $this->_notify('gotBody');1183 }1184 }1185 return true;1186 }1187 1188 1189 /**1190 * Processes the response header1191 *1192 * @access private1193 * @param string HTTP header1194 */1195 function _processHeader($header)1196 {1197 if (false === strpos($header, ':')) {1198 return;1199 }1200 list($headername, $headervalue) = explode(':', $header, 2);1201 $headername = strtolower($headername);1202 $headervalue = ltrim($headervalue);1203 1204 if ('set-cookie' != $headername) {1205 if (isset($this->_headers[$headername])) {1206 $this->_headers[$headername] .= ',' . $headervalue;1207 } else {1208 $this->_headers[$headername] = $headervalue;1209 }1210 } else {1211 $this->_parseCookie($headervalue);1212 }1213 }1214 1215 1216 /**1217 * Parse a Set-Cookie header to fill $_cookies array1218 *1219 * @access private1220 * @param string value of Set-Cookie header1221 */1222 function _parseCookie($headervalue)1223 {1224 $cookie = array(1225 'expires' => null,1226 'domain' => null,1227 'path' => null,1228 'secure' => false1229 );1230 1231 // Only a name=value pair1232 if (!strpos($headervalue, ';')) {1233 $pos = strpos($headervalue, '=');1234 $cookie['name'] = trim(substr($headervalue, 0, $pos));1235 $cookie['value'] = trim(substr($headervalue, $pos + 1));1236 1237 // Some optional parameters are supplied1238 } else {1239 $elements = explode(';', $headervalue);1240 $pos = strpos($elements[0], '=');1241 $cookie['name'] = trim(substr($elements[0], 0, $pos));1242 $cookie['value'] = trim(substr($elements[0], $pos + 1));1243 1244 for ($i = 1; $i < count($elements); $i++) {1245 if (false === strpos($elements[$i], '=')) {1246 $elName = trim($elements[$i]);1247 $elValue = null;1248 } else {1249 list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));1250 }1251 $elName = strtolower($elName);1252 if ('secure' == $elName) {1253 $cookie['secure'] = true;1254 } elseif ('expires' == $elName) {1255 $cookie['expires'] = str_replace('"', '', $elValue);1256 } elseif ('path' == $elName || 'domain' == $elName) {1257 $cookie[$elName] = urldecode($elValue);1258 } else {1259 $cookie[$elName] = $elValue;1260 }1261 }1262 }1263 $this->_cookies[] = $cookie;1264 }1265 1266 1267 /**1268 * Read a part of response body encoded with chunked Transfer-Encoding1269 *1270 * @access private1271 * @return string1272 */1273 function _readChunked()1274 {1275 // at start of the next chunk?1276 if (0 == $this->_chunkLength) {1277 $line = $this->_sock->readLine();1278 if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) {1279 $this->_chunkLength = hexdec($matches[1]);1280 // Chunk with zero length indicates the end1281 if (0 == $this->_chunkLength) {1282 $this->_sock->readLine(); // make this an eof()1283 return '';1284 }1285 } else {1286 return '';1287 }1288 }1289 $data = $this->_sock->read($this->_chunkLength);1290 $this->_chunkLength -= strlen($data);1291 if (0 == $this->_chunkLength) {1292 $this->_sock->readLine(); // Trailing CRLF1293 }1294 return $data;1295 }1296 1297 1298 /**1299 * Notifies all registered listeners of an event.1300 *1301 * @param string Event name1302 * @param mixed Additional data1303 * @access private1304 * @see HTTP_Request::_notify()1305 */1306 function _notify($event, $data = null)1307 {1308 foreach (array_keys($this->_listeners) as $id) {1309 $this->_listeners[$id]->update($this, $event, $data);1310 }1311 }1312 1313 1314 /**1315 * Decodes the message-body encoded by gzip1316 *1317 * The real decoding work is done by gzinflate() built-in function, this1318 * method only parses the header and checks data for compliance with1319 * RFC 19521320 *1321 * @access private1322 * @param string gzip-encoded data1323 * @return string decoded data1324 */1325 function _decodeGzip($data)1326 {1327 $length = strlen($data);1328 // If it doesn't look like gzip-encoded data, don't bother1329 if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {1330 return $data;1331 }1332 $method = ord(substr($data, 2, 1));1333 if (8 != $method) {1334 return PEAR::raiseError('_decodeGzip(): unknown compression method');1335 }1336 $flags = ord(substr($data, 3, 1));1337 if ($flags & 224) {1338 return PEAR::raiseError('_decodeGzip(): reserved bits are set');1339 }1340 1341 // header is 10 bytes minimum. may be longer, though.1342 $headerLength = 10;1343 // extra fields, need to skip 'em1344 if ($flags & 4) {1345 if ($length - $headerLength - 2 < 8) {1346 return PEAR::raiseError('_decodeGzip(): data too short');1347 }1348 $extraLength = unpack('v', substr($data, 10, 2));1349 if ($length - $headerLength - 2 - $extraLength[1] < 8) {1350 return PEAR::raiseError('_decodeGzip(): data too short');1351 }1352 $headerLength += $extraLength[1] + 2;1353 }1354 // file name, need to skip that1355 if ($flags & 8) {1356 if ($length - $headerLength - 1 < 8) {1357 return PEAR::raiseError('_decodeGzip(): data too short');1358 }1359 $filenameLength = strpos(substr($data, $headerLength), chr(0));1360 if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {1361 return PEAR::raiseError('_decodeGzip(): data too short');1362 }1363 $headerLength += $filenameLength + 1;1364 }1365 // comment, need to skip that also1366 if ($flags & 16) {1367 if ($length - $headerLength - 1 < 8) {1368 return PEAR::raiseError('_decodeGzip(): data too short');1369 }1370 $commentLength = strpos(substr($data, $headerLength), chr(0));1371 if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {1372 return PEAR::raiseError('_decodeGzip(): data too short');1373 }1374 $headerLength += $commentLength + 1;1375 }1376 // have a CRC for header. let's check1377 if ($flags & 1) {1378 if ($length - $headerLength - 2 < 8) {1379 return PEAR::raiseError('_decodeGzip(): data too short');1380 }1381 $crcReal = 0xffff & crc32(substr($data, 0, $headerLength));1382 $crcStored = unpack('v', substr($data, $headerLength, 2));1383 if ($crcReal != $crcStored[1]) {1384 return PEAR::raiseError('_decodeGzip(): header CRC check failed');1385 }1386 $headerLength += 2;1387 }1388 // unpacked data CRC and size at the end of encoded data1389 $tmp = unpack('V2', substr($data, -8));1390 $dataCrc = $tmp[1];1391 $dataSize = $tmp[2];1392 1393 // finally, call the gzinflate() function1394 $unpacked = @gzinflate(substr($data, $headerLength, -8), $dataSize);1395 if (false === $unpacked) {1396 return PEAR::raiseError('_decodeGzip(): gzinflate() call failed');1397 } elseif ($dataSize != strlen($unpacked)) {1398 return PEAR::raiseError('_decodeGzip(): data size check failed');1399 } elseif ($dataCrc != crc32($unpacked)) {1400 return PEAR::raiseError('_decodeGzip(): data CRC check failed');1401 }1402 return $unpacked;1403 }1404 } // End class HTTP_Response1405 ?>
Note: See TracChangeset
for help on using the changeset viewer.
