Changeset 6908 for temp/trunk/data/module/Request.php
- Timestamp:
- 2006/10/26 18:36:12 (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
r6907 r6908 53 53 //require_once REQUEST_PHP_DIR . '/Net/URL.php'; 54 54 55 require_once 'PEAR.php'; 56 55 require_once 'Net/Socket.php'; 56 require_once 'Net/URL.php'; 57 58 define('HTTP_REQUEST_METHOD_GET', 'GET', true); 59 define('HTTP_REQUEST_METHOD_HEAD', 'HEAD', true); 60 define('HTTP_REQUEST_METHOD_POST', 'POST', true); 61 define('HTTP_REQUEST_METHOD_PUT', 'PUT', true); 62 define('HTTP_REQUEST_METHOD_DELETE', 'DELETE', true); 63 define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true); 64 define('HTTP_REQUEST_METHOD_TRACE', 'TRACE', true); 65 66 define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true); 67 define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true); 68 69 class HTTP_Request { 70 71 /** 72 * Instance of Net_URL 73 * @var object Net_URL 74 */ 75 var $_url; 76 77 /** 78 * Type of request 79 * @var string 80 */ 81 var $_method; 82 83 /** 84 * HTTP Version 85 * @var string 86 */ 87 var $_http; 88 89 /** 90 * Request headers 91 * @var array 92 */ 93 var $_requestHeaders; 94 95 /** 96 * Basic Auth Username 97 * @var string 98 */ 99 var $_user; 100 101 /** 102 * Basic Auth Password 103 * @var string 104 */ 105 var $_pass; 106 107 /** 108 * Socket object 109 * @var object Net_Socket 110 */ 111 var $_sock; 112 113 /** 114 * Proxy server 115 * @var string 116 */ 117 var $_proxy_host; 118 119 /** 120 * Proxy port 121 * @var integer 122 */ 123 var $_proxy_port; 124 125 /** 126 * Proxy username 127 * @var string 128 */ 129 var $_proxy_user; 130 131 /** 132 * Proxy password 133 * @var string 134 */ 135 var $_proxy_pass; 136 137 /** 138 * Post data 139 * @var array 140 */ 141 var $_postData; 142 143 /** 144 * Request body 145 * @var string 146 */ 147 var $_body; 148 149 /** 150 * A list of methods that MUST NOT have a request body, per RFC 2616 151 * @var array 152 */ 153 var $_bodyDisallowed = array('TRACE'); 154 155 /** 156 * Files to post 157 * @var array 158 */ 159 var $_postFiles = array(); 160 161 /** 162 * Connection timeout. 163 * @var float 164 */ 165 var $_timeout; 166 167 /** 168 * HTTP_Response object 169 * @var object HTTP_Response 170 */ 171 var $_response; 172 173 /** 174 * Whether to allow redirects 175 * @var boolean 176 */ 177 var $_allowRedirects; 178 179 /** 180 * Maximum redirects allowed 181 * @var integer 182 */ 183 var $_maxRedirects; 184 185 /** 186 * Current number of redirects 187 * @var integer 188 */ 189 var $_redirects; 190 191 /** 192 * Whether to append brackets [] to array variables 193 * @var bool 194 */ 195 var $_useBrackets = true; 196 197 /** 198 * Attached listeners 199 * @var array 200 */ 201 var $_listeners = array(); 202 203 /** 204 * Whether to save response body in response object property 205 * @var bool 206 */ 207 var $_saveBody = true; 208 209 /** 210 * Timeout for reading from socket (array(seconds, microseconds)) 211 * @var array 212 */ 213 var $_readTimeout = null; 214 215 /** 216 * Options to pass to Net_Socket::connect. See stream_context_create 217 * @var array 218 */ 219 var $_socketOptions = null; 220 221 /** 222 * Constructor 223 * 224 * Sets up the object 225 * @param string The url to fetch/access 226 * @param array Associative array of parameters which can have the following keys: 227 * <ul> 228 * <li>method - Method to use, GET, POST etc (string)</li> 229 * <li>http - HTTP Version to use, 1.0 or 1.1 (string)</li> 230 * <li>user - Basic Auth username (string)</li> 231 * <li>pass - Basic Auth password (string)</li> 232 * <li>proxy_host - Proxy server host (string)</li> 233 * <li>proxy_port - Proxy server port (integer)</li> 234 * <li>proxy_user - Proxy auth username (string)</li> 235 * <li>proxy_pass - Proxy auth password (string)</li> 236 * <li>timeout - Connection timeout in seconds (float)</li> 237 * <li>allowRedirects - Whether to follow redirects or not (bool)</li> 238 * <li>maxRedirects - Max number of redirects to follow (integer)</li> 239 * <li>useBrackets - Whether to append [] to array variable names (bool)</li> 240 * <li>saveBody - Whether to save response body in response object property (bool)</li> 241 * <li>readTimeout - Timeout for reading / writing data over the socket (array (seconds, microseconds))</li> 242 * <li>socketOptions - Options to pass to Net_Socket object (array)</li> 243 * </ul> 244 * @access public 245 */ 246 function HTTP_Request($url = '', $params = array()) 247 { 248 $this->_method = HTTP_REQUEST_METHOD_GET; 249 $this->_http = HTTP_REQUEST_HTTP_VER_1_1; 250 $this->_requestHeaders = array(); 251 $this->_postData = array(); 252 $this->_body = null; 253 254 $this->_user = null; 255 $this->_pass = null; 256 257 $this->_proxy_host = null; 258 $this->_proxy_port = null; 259 $this->_proxy_user = null; 260 $this->_proxy_pass = null; 261 262 $this->_allowRedirects = false; 263 $this->_maxRedirects = 3; 264 $this->_redirects = 0; 265 266 $this->_timeout = null; 267 $this->_response = null; 268 269 foreach ($params as $key => $value) { 270 $this->{'_' . $key} = $value; 271 } 272 273 if (!empty($url)) { 274 $this->setURL($url); 275 } 276 277 // Default useragent 278 $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )'); 279 280 // We don't do keep-alives by default 281 $this->addHeader('Connection', 'close'); 282 283 // Basic authentication 284 if (!empty($this->_user)) { 285 $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass)); 286 } 287 288 // Proxy authentication (see bug #5913) 289 if (!empty($this->_proxy_user)) { 290 $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass)); 291 } 292 293 // Use gzip encoding if possible 294 // Avoid gzip encoding if using multibyte functions (see #1781) 295 if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib') && 296 0 == (2 & ini_get('mbstring.func_overload'))) { 297 298 $this->addHeader('Accept-Encoding', 'gzip'); 299 } 300 } 301 302 /** 303 * Generates a Host header for HTTP/1.1 requests 304 * 305 * @access private 306 * @return string 307 */ 308 function _generateHostHeader() 309 { 310 if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) { 311 $host = $this->_url->host . ':' . $this->_url->port; 312 313 } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) { 314 $host = $this->_url->host . ':' . $this->_url->port; 315 316 } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) { 317 $host = $this->_url->host . ':' . $this->_url->port; 318 319 } else { 320 $host = $this->_url->host; 321 } 322 323 return $host; 324 } 325 326 /** 327 * Resets the object to its initial state (DEPRECATED). 328 * Takes the same parameters as the constructor. 329 * 330 * @param string $url The url to be requested 331 * @param array $params Associative array of parameters 332 * (see constructor for details) 333 * @access public 334 * @deprecated deprecated since 1.2, call the constructor if this is necessary 335 */ 336 function reset($url, $params = array()) 337 { 338 $this->HTTP_Request($url, $params); 339 } 340 341 /** 342 * Sets the URL to be requested 343 * 344 * @param string The url to be requested 345 * @access public 346 */ 347 function setURL($url) 348 { 349 $this->_url = &new Net_URL($url, $this->_useBrackets); 350 351 if (!empty($this->_url->user) || !empty($this->_url->pass)) { 352 $this->setBasicAuth($this->_url->user, $this->_url->pass); 353 } 354 355 if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) { 356 $this->addHeader('Host', $this->_generateHostHeader()); 357 } 358 359 // set '/' instead of empty path rather than check later (see bug #8662) 360 if (empty($this->_url->path)) { 361 $this->_url->path = '/'; 362 } 363 } 364 365 /** 366 * Returns the current request URL 367 * 368 * @return string Current request URL 369 * @access public 370 */ 371 function getUrl($url) 372 { 373 return empty($this->_url)? '': $this->_url->getUrl(); 374 } 375 376 /** 377 * Sets a proxy to be used 378 * 379 * @param string Proxy host 380 * @param int Proxy port 381 * @param string Proxy username 382 * @param string Proxy password 383 * @access public 384 */ 385 function setProxy($host, $port = 8080, $user = null, $pass = null) 386 { 387 $this->_proxy_host = $host; 388 $this->_proxy_port = $port; 389 $this->_proxy_user = $user; 390 $this->_proxy_pass = $pass; 391 392 if (!empty($user)) { 393 $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass)); 394 } 395 } 396 397 /** 398 * Sets basic authentication parameters 399 * 400 * @param string Username 401 * @param string Password 402 */ 403 function setBasicAuth($user, $pass) 404 { 405 $this->_user = $user; 406 $this->_pass = $pass; 407 408 $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass)); 409 } 410 411 /** 412 * Sets the method to be used, GET, POST etc. 413 * 414 * @param string Method to use. Use the defined constants for this 415 * @access public 416 */ 417 function setMethod($method) 418 { 419 $this->_method = $method; 420 } 421 422 /** 423 * Sets the HTTP version to use, 1.0 or 1.1 424 * 425 * @param string Version to use. Use the defined constants for this 426 * @access public 427 */ 428 function setHttpVer($http) 429 { 430 $this->_http = $http; 431 } 432 433 /** 434 * Adds a request header 435 * 436 * @param string Header name 437 * @param string Header value 438 * @access public 439 */ 440 function addHeader($name, $value) 441 { 442 $this->_requestHeaders[strtolower($name)] = $value; 443 } 444 445 /** 446 * Removes a request header 447 * 448 * @param string Header name to remove 449 * @access public 450 */ 451 function removeHeader($name) 452 { 453 if (isset($this->_requestHeaders[strtolower($name)])) { 454 unset($this->_requestHeaders[strtolower($name)]); 455 } 456 } 457 458 /** 459 * Adds a querystring parameter 460 * 461 * @param string Querystring parameter name 462 * @param string Querystring parameter value 463 * @param bool Whether the value is already urlencoded or not, default = not 464 * @access public 465 */ 466 function addQueryString($name, $value, $preencoded = false) 467 { 468 $this->_url->addQueryString($name, $value, $preencoded); 469 } 470 471 /** 472 * Sets the querystring to literally what you supply 473 * 474 * @param string The querystring data. Should be of the format foo=bar&x=y etc 475 * @param bool Whether data is already urlencoded or not, default = already encoded 476 * @access public 477 */ 478 function addRawQueryString($querystring, $preencoded = true) 479 { 480 $this->_url->addRawQueryString($querystring, $preencoded); 481 } 482 483 /** 484 * Adds postdata items 485 * 486 * @param string Post data name 487 * @param string Post data value 488 * @param bool Whether data is already urlencoded or not, default = not 489 * @access public 490 */ 491 function addPostData($name, $value, $preencoded = false) 492 { 493 if ($preencoded) { 494 $this->_postData[$name] = $value; 495 } else { 496 $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value); 497 } 498 } 499 500 /** 501 * Recursively applies the callback function to the value 502 * 503 * @param mixed Callback function 504 * @param mixed Value to process 505 * @access private 506 * @return mixed Processed value 507 */ 508 function _arrayMapRecursive($callback, $value) 509 { 510 if (!is_array($value)) { 511 return call_user_func($callback, $value); 512 } else { 513 $map = array(); 514 foreach ($value as $k => $v) { 515 $map[$k] = $this->_arrayMapRecursive($callback, $v); 516 } 517 return $map; 518 } 519 } 520 521 /** 522 * Adds a file to upload 523 * 524 * This also changes content-type to 'multipart/form-data' for proper upload 525 * 526 * @access public 527 * @param string name of file-upload field 528 * @param mixed file name(s) 529 * @param mixed content-type(s) of file(s) being uploaded 530 * @return bool true on success 531 * @throws PEAR_Error 532 */ 533 function addFile($inputName, $fileName, $contentType = 'application/octet-stream') 534 { 535 if (!is_array($fileName) && !is_readable($fileName)) { 536 return PEAR::raiseError("File '{$fileName}' is not readable"); 537 } elseif (is_array($fileName)) { 538 foreach ($fileName as $name) { 539 if (!is_readable($name)) { 540 return PEAR::raiseError("File '{$name}' is not readable"); 541 } 542 } 543 } 544 $this->addHeader('Content-Type', 'multipart/form-data'); 545 $this->_postFiles[$inputName] = array( 546 'name' => $fileName, 547 'type' => $contentType 548 ); 549 return true; 550 } 551 552 /** 553 * Adds raw postdata (DEPRECATED) 554 * 555 * @param string The data 556 * @param bool Whether data is preencoded or not, default = already encoded 557 * @access public 558 * @deprecated deprecated since 1.3.0, method setBody() should be used instead 559 */ 560 function addRawPostData($postdata, $preencoded = true) 561 { 562 $this->_body = $preencoded ? $postdata : urlencode($postdata); 563 } 564 565 /** 566 * Sets the request body (for POST, PUT and similar requests) 567 * 568 * @param string Request body 569 * @access public 570 */ 571 function setBody($body) 572 { 573 $this->_body = $body; 574 } 575 576 /** 577 * Clears any postdata that has been added (DEPRECATED). 578 * 579 * Useful for multiple request scenarios. 580 * 581 * @access public 582 * @deprecated deprecated since 1.2 583 */ 584 function clearPostData() 585 { 586 $this->_postData = null; 587 } 588 589 /** 590 * Appends a cookie to "Cookie:" header 591 * 592 * @param string $name cookie name 593 * @param string $value cookie value 594 * @access public 595 */ 596 function addCookie($name, $value) 597 { 598 $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : ''; 599 $this->addHeader('Cookie', $cookies . $name . '=' . $value); 600 } 601 602 /** 603 * Clears any cookies that have been added (DEPRECATED). 604 * 605 * Useful for multiple request scenarios 606 * 607 * @access public 608 * @deprecated deprecated since 1.2 609 */ 610 function clearCookies() 611 { 612 $this->removeHeader('Cookie'); 613 } 614 615 /** 616 * Sends the request 617 * 618 * @access public 619 * @param bool Whether to store response body in Response object property, 620 * set this to false if downloading a LARGE file and using a Listener 621 * @return mixed PEAR error on error, true otherwise 622 */ 623 function sendRequest($saveBody = true) 624 { 625 if (!is_a($this->_url, 'Net_URL')) { 626 return PEAR::raiseError('No URL given.'); 627 } 628 629 $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host; 630 $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port; 631 632 // 4.3.0 supports SSL connections using OpenSSL. The function test determines 633 // we running on at least 4.3.0 634 if (strcasecmp($this->_url->protocol, 'https') == 0 AND function_exists('file_get_contents') AND extension_loaded('openssl')) { 635 if (isset($this->_proxy_host)) { 636 return PEAR::raiseError('HTTPS proxies are not supported.'); 637 } 638 $host = 'ssl://' . $host; 639 } 640 641 // magic quotes may fuck up file uploads and chunked response processing 642 $magicQuotes = ini_get('magic_quotes_runtime'); 643 ini_set('magic_quotes_runtime', false); 644 645 // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive 646 // connection token to a proxy server... 647 if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) && 648 'Keep-Alive' == $this->_requestHeaders['connection']) 649 { 650 $this->removeHeader('connection'); 651 } 652 653 $keepAlive = (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection'])) || 654 (!empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']); 655 $sockets = &PEAR::getStaticProperty('HTTP_Request', 'sockets'); 656 $sockKey = $host . ':' . $port; 657 unset($this->_sock); 658 659 // There is a connected socket in the "static" property? 660 if ($keepAlive && !empty($sockets[$sockKey]) && 661 !empty($sockets[$sockKey]->fp)) 662 { 663 $this->_sock =& $sockets[$sockKey]; 664 $err = null; 665 } else { 666 $this->_notify('connect'); 667 $this->_sock =& new Net_Socket(); 668 $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions); 669 } 670 PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest()); 671 672 if (!PEAR::isError($err)) { 673 if (!empty($this->_readTimeout)) { 674 $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]); 675 } 676 677 $this->_notify('sentRequest'); 678 679 // Read the response 680 $this->_response = &new HTTP_Response($this->_sock, $this->_listeners); 681 $err = $this->_response->process( 682 $this->_saveBody && $saveBody, 683 HTTP_REQUEST_METHOD_HEAD != $this->_method 684 ); 685 686 if ($keepAlive) { 687 $keepAlive = (isset($this->_response->_headers['content-length']) 688 || (isset($this->_response->_headers['transfer-encoding']) 689 && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked')); 690 if ($keepAlive) { 691 if (isset($this->_response->_headers['connection'])) { 692 $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive'; 693 } else { 694 $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol; 695 } 696 } 697 } 698 } 699 700 ini_set('magic_quotes_runtime', $magicQuotes); 701 702 if (PEAR::isError($err)) { 703 return $err; 704 } 705 706 if (!$keepAlive) { 707 $this->disconnect(); 708 // Store the connected socket in "static" property 709 } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) { 710 $sockets[$sockKey] =& $this->_sock; 711 } 712 713 // Check for redirection 714 if ( $this->_allowRedirects 715 AND $this->_redirects <= $this->_maxRedirects 716 AND $this->getResponseCode() > 300 717 AND $this->getResponseCode() < 399 718 AND !empty($this->_response->_headers['location'])) { 719 720 721 $redirect = $this->_response->_headers['location']; 722 723 // Absolute URL 724 if (preg_match('/^https?:\/\//i', $redirect)) { 725 $this->_url = &new Net_URL($redirect); 726 $this->addHeader('Host', $this->_generateHostHeader()); 727 // Absolute path 728 } elseif ($redirect{0} == '/') { 729 $this->_url->path = $redirect; 730 731 // Relative path 732 } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') { 733 if (substr($this->_url->path, -1) == '/') { 734 $redirect = $this->_url->path . $redirect; 735 } else { 736 $redirect = dirname($this->_url->path) . '/' . $redirect; 737 } 738 $redirect = Net_URL::resolvePath($redirect); 739 $this->_url->path = $redirect; 740 741 // Filename, no path 742 } else { 743 if (substr($this->_url->path, -1) == '/') { 744 $redirect = $this->_url->path . $redirect; 745 } else { 746 $redirect = dirname($this->_url->path) . '/' . $redirect; 747 } 748 $this->_url->path = $redirect; 749 } 750 751 $this->_redirects++; 752 return $this->sendRequest($saveBody); 753 754 // Too many redirects 755 } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) { 756 return PEAR::raiseError('Too many redirects'); 757 } 758 759 return true; 760 } 761 762 /** 763 * Disconnect the socket, if connected. Only useful if using Keep-Alive. 764 * 765 * @access public 766 */ 767 function disconnect() 768 { 769 if (!empty($this->_sock) && !empty($this->_sock->fp)) { 770 $this->_notify('disconnect'); 771 $this->_sock->disconnect(); 772 } 773 } 774 775 /** 776 * Returns the response code 777 * 778 * @access public 779 * @return mixed Response code, false if not set 780 */ 781 function getResponseCode() 782 { 783 return isset($this->_response->_code) ? $this->_response->_code : false; 784 } 785 786 /** 787 * Returns either the named header or all if no name given 788 * 789 * @access public 790 * @param string The header name to return, do not set to get all headers 791 * @return mixed either the value of $headername (false if header is not present) 792 * or an array of all headers 793 */ 794 function getResponseHeader($headername = null) 795 { 796 if (!isset($headername)) { 797 return isset($this->_response->_headers)? $this->_response->_headers: array(); 798 } else { 799 $headername = strtolower($headername); 800 return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false; 801 } 802 } 803 804 /** 805 * Returns the body of the response 806 * 807 * @access public 808 * @return mixed response body, false if not set 809 */ 810 function getResponseBody() 811 { 812 return isset($this->_response->_body) ? $this->_response->_body : false; 813 } 814 815 /** 816 * Returns cookies set in response 817 * 818 * @access public 819 * @return mixed array of response cookies, false if none are present 820 */ 821 function getResponseCookies() 822 { 823 return isset($this->_response->_cookies) ? $this->_response->_cookies : false; 824 } 825 826 /** 827 * Builds the request string 828 * 829 * @access private 830 * @return string The request string 831 */ 832 function _buildRequest() 833 { 834 $separator = ini_get('arg_separator.output'); 835 ini_set('arg_separator.output', '&'); 836 $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : ''; 837 ini_set('arg_separator.output', $separator); 838 839 $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : ''; 840 $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : ''; 841 $path = $this->_url->path . $querystring; 842 $url = $host . $port . $path; 843 844 $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n"; 845 846 if (in_array($this->_method, $this->_bodyDisallowed) || 847 (empty($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method || 848 (empty($this->_postData) && empty($this->_postFiles))))) 849 { 850 $this->removeHeader('Content-Type'); 851 } else { 852 if (empty($this->_requestHeaders['content-type'])) { 853 // Add default content-type 854 $this->addHeader('Content-Type', 'application/x-www-form-urlencoded'); 855 } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) { 856 $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime()); 857 $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary); 858 } 859 } 860 861 // Request Headers 862 if (!empty($this->_requestHeaders)) { 863 foreach ($this->_requestHeaders as $name => $value) { 864 $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); 865 $request .= $canonicalName . ': ' . $value . "\r\n"; 866 } 867 } 868 869 // No post data or wrong method, so simply add a final CRLF 870 if (in_array($this->_method, $this->_bodyDisallowed) || 871 (HTTP_REQUEST_METHOD_POST != $this->_method && empty($this->_body))) { 872 873 $request .= "\r\n"; 874 875 // Post data if it's an array 876 } elseif (HTTP_REQUEST_METHOD_POST == $this->_method && 877 (!empty($this->_postData) || !empty($this->_postFiles))) { 878 879 // "normal" POST request 880 if (!isset($boundary)) { 881 $postdata = implode('&', array_map( 882 create_function('$a', 'return $a[0] . \'=\' . $a[1];'), 883 $this->_flattenArray('', $this->_postData) 884 )); 885 886 // multipart request, probably with file uploads 887 } else { 888 $postdata = ''; 889 if (!empty($this->_postData)) { 890 $flatData = $this->_flattenArray('', $this->_postData); 891 foreach ($flatData as $item) { 892 $postdata .= '--' . $boundary . "\r\n"; 893 $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"'; 894 $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n"; 895 } 896 } 897 foreach ($this->_postFiles as $name => $value) { 898 if (is_array($value['name'])) { 899 $varname = $name . ($this->_useBrackets? '[]': ''); 900 } else { 901 $varname = $name; 902 $value['name'] = array($value['name']); 903 } 904 foreach ($value['name'] as $key => $filename) { 905 $fp = fopen($filename, 'r'); 906 $data = fread($fp, filesize($filename)); 907 fclose($fp); 908 $basename = basename($filename); 909 $type = is_array($value['type'])? @$value['type'][$key]: $value['type']; 910 911 $postdata .= '--' . $boundary . "\r\n"; 912 $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"'; 913 $postdata .= "\r\nContent-Type: " . $type; 914 $postdata .= "\r\n\r\n" . $data . "\r\n"; 915 } 916 } 917 $postdata .= '--' . $boundary . "--\r\n"; 918 } 919 $request .= 'Content-Length: ' . strlen($postdata) . "\r\n\r\n"; 920 $request .= $postdata; 921 922 // Explicitly set request body 923 } elseif (!empty($this->_body)) { 924 925 $request .= 'Content-Length: ' . strlen($this->_body) . "\r\n\r\n"; 926 $request .= $this->_body; 927 } 928 929 return $request; 930 } 931 932 /** 933 * Helper function to change the (probably multidimensional) associative array 934 * into the simple one. 935 * 936 * @param string name for item 937 * @param mixed item's values 938 * @return array array with the following items: array('item name', 'item value'); 939 */ 940 function _flattenArray($name, $values) 941 { 942 if (!is_array($values)) { 943 return array(array($name, $values)); 944 } else { 945 $ret = array(); 946 foreach ($values as $k => $v) { 947 if (empty($name)) { 948 $newName = $k; 949 } elseif ($this->_useBrackets) { 950 $newName = $name . '[' . $k . ']'; 951 } else { 952 $newName = $name; 953 } 954 $ret = array_merge($ret, $this->_flattenArray($newName, $v)); 955 } 956 return $ret; 957 } 958 } 959 960 961 /** 962 * Adds a Listener to the list of listeners that are notified of 963 * the object's events 964 * 965 * @param object HTTP_Request_Listener instance to attach 966 * @return boolean whether the listener was successfully attached 967 * @access public 968 */ 969 function attach(&$listener) 970 { 971 if (!is_a($listener, 'HTTP_Request_Listener')) { 972 return false; 973 } 974 $this->_listeners[$listener->getId()] =& $listener; 975 return true; 976 } 977 978 979 /** 980 * Removes a Listener from the list of listeners 981 * 982 * @param object HTTP_Request_Listener instance to detach 983 * @return boolean whether the listener was successfully detached 984 * @access public 985 */ 986 function detach(&$listener) 987 { 988 if (!is_a($listener, 'HTTP_Request_Listener') || 989 !isset($this->_listeners[$listener->getId()])) { 990 return false; 991 } 992 unset($this->_listeners[$listener->getId()]); 993 return true; 994 } 995 996 997 /** 998 * Notifies all registered listeners of an event. 999 * 1000 * Events sent by HTTP_Request object 1001 * - 'connect': on connection to server 1002 * - 'sentRequest': after the request was sent 1003 * - 'disconnect': on disconnection from server 1004 * 1005 * Events sent by HTTP_Response object 1006 * - 'gotHeaders': after receiving response headers (headers are passed in $data) 1007 * - 'tick': on receiving a part of response body (the part is passed in $data) 1008 * - 'gzTick': on receiving a gzip-encoded part of response body (ditto) 1009 * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped) 1010 * 1011 * @param string Event name 1012 * @param mixed Additional data 1013 * @access private 1014 */ 1015 function _notify($event, $data = null) 1016 { 1017 foreach (array_keys($this->_listeners) as $id) { 1018 $this->_listeners[$id]->update($this, $event, $data); 1019 } 1020 } 1021 } 1022 1023 1024 /** 1025 * Response class to complement the Request class 1026 */ 1027 class HTTP_Response 1028 { 1029 /** 1030 * Socket object 1031 * @var object 1032 */ 1033 var $_sock; 1034 1035 /** 1036 * Protocol 1037 * @var string 1038 */ 1039 var $_protocol; 1040 1041 /** 1042 * Return code 1043 * @var string 1044 */ 1045 var $_code; 1046 1047 /** 1048 * Response headers 1049 * @var array 1050 */ 1051 var $_headers; 1052 1053 /** 1054 * Cookies set in response 1055 * @var array 1056 */ 1057 var $_cookies; 1058 1059 /** 1060 * Response body 1061 * @var string 1062 */ 1063 var $_body = ''; 1064 1065 /** 1066 * Used by _readChunked(): remaining length of the current chunk 1067 * @var string 1068 */ 1069 var $_chunkLength = 0; 1070 1071 /** 1072 * Attached listeners 1073 * @var array 1074 */ 1075 var $_listeners = array(); 1076 1077 /** 1078 * Bytes left to read from message-body 1079 * @var null|int 1080 */ 1081 var $_toRead; 1082 1083 /** 1084 * Constructor 1085 * 1086 * @param object Net_Socket socket to read the response from 1087 * @param array listeners attached to request 1088 * @return mixed PEAR Error on error, true otherwise 1089 */ 1090 function HTTP_Response(&$sock, &$listeners) 1091 { 1092 $this->_sock =& $sock; 1093 $this->_listeners =& $listeners; 1094 } 1095 1096 1097 /** 1098 * Processes a HTTP response 1099 * 1100 * This extracts response code, headers, cookies and decodes body if it 1101 * was encoded in some way 1102 * 1103 * @access public 1104 * @param bool Whether to store response body in object property, set 1105 * this to false if downloading a LARGE file and using a Listener. 1106 * This is assumed to be true if body is gzip-encoded. 1107 * @param bool Whether the response can actually have a message-body. 1108 * Will be set to false for HEAD requests. 1109 * @throws PEAR_Error 1110 * @return mixed true on success, PEAR_Error in case of malformed response 1111 */ 1112 function process($saveBody = true, $canHaveBody = true) 1113 { 1114 do { 1115 $line = $this->_sock->readLine(); 1116 if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) { 1117 return PEAR::raiseError('Malformed response.'); 1118 } else { 1119 $this->_protocol = 'HTTP/' . $http_version; 1120 $this->_code = intval($returncode); 1121 } 1122 while ('' !== ($header = $this->_sock->readLine())) { 1123 $this->_processHeader($header); 1124 } 1125 } while (100 == $this->_code); 1126 1127 $this->_notify('gotHeaders', $this->_headers); 1128 1129 // RFC 2616, section 4.4: 1130 // 1. Any response message which "MUST NOT" include a message-body ... 1131 // is always terminated by the first empty line after the header fields 1132 // 3. ... If a message is received with both a 1133 // Transfer-Encoding header field and a Content-Length header field, 1134 // the latter MUST be ignored. 1135 $canHaveBody = $canHaveBody && $this->_code >= 200 && 1136 $this->_code != 204 && $this->_code != 304; 1137 1138 // If response body is present, read it and decode 1139 $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']); 1140 $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']); 1141 $hasBody = false; 1142 if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) || 1143 0 != $this->_headers['content-length'])) 1144 { 1145 if ($chunked || !isset($this->_headers['content-length'])) { 1146 $this->_toRead = null; 1147 } else { 1148 $this->_toRead = $this->_headers['content-length']; 1149 } 1150 while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) { 1151 if ($chunked) { 1152 $data = $this->_readChunked(); 1153 } elseif (is_null($this->_toRead)) { 1154 $data = $this->_sock->read(4096); 1155 } else { 1156 $data = $this->_sock->read(min(4096, $this->_toRead)); 1157 $this->_toRead -= strlen($data); 1158 } 1159 if ('' == $data) { 1160 break; 1161 } else { 1162 $hasBody = true; 1163 if ($saveBody || $gzipped) { 1164 $this->_body .= $data; 1165 } 1166 $this->_notify($gzipped? 'gzTick': 'tick', $data); 1167 } 1168 } 1169 } 1170 1171 if ($hasBody) { 1172 // Uncompress the body if needed 1173 if ($gzipped) { 1174 $body = $this->_decodeGzip($this->_body); 1175 if (PEAR::isError($body)) { 1176 return $body; 1177 } 1178 $this->_body = $body; 1179 $this->_notify('gotBody', $this->_body); 1180 } else { 1181 $this->_notify('gotBody'); 1182 } 1183 } 1184 return true; 1185 } 1186 1187 1188 /** 1189 * Processes the response header 1190 * 1191 * @access private 1192 * @param string HTTP header 1193 */ 1194 function _processHeader($header) 1195 { 1196 if (false === strpos($header, ':')) { 1197 return; 1198 } 1199 list($headername, $headervalue) = explode(':', $header, 2); 1200 $headername = strtolower($headername); 1201 $headervalue = ltrim($headervalue); 1202 1203 if ('set-cookie' != $headername) { 1204 if (isset($this->_headers[$headername])) { 1205 $this->_headers[$headername] .= ',' . $headervalue; 1206 } else { 1207 $this->_headers[$headername] = $headervalue; 1208 } 1209 } else { 1210 $this->_parseCookie($headervalue); 1211 } 1212 } 1213 1214 1215 /** 1216 * Parse a Set-Cookie header to fill $_cookies array 1217 * 1218 * @access private 1219 * @param string value of Set-Cookie header 1220 */ 1221 function _parseCookie($headervalue) 1222 { 1223 $cookie = array( 1224 'expires' => null, 1225 'domain' => null, 1226 'path' => null, 1227 'secure' => false 1228 ); 1229 1230 // Only a name=value pair 1231 if (!strpos($headervalue, ';')) { 1232 $pos = strpos($headervalue, '='); 1233 $cookie['name'] = trim(substr($headervalue, 0, $pos)); 1234 $cookie['value'] = trim(substr($headervalue, $pos + 1)); 1235 1236 // Some optional parameters are supplied 1237 } else { 1238 $elements = explode(';', $headervalue); 1239 $pos = strpos($elements[0], '='); 1240 $cookie['name'] = trim(substr($elements[0], 0, $pos)); 1241 $cookie['value'] = trim(substr($elements[0], $pos + 1)); 1242 1243 for ($i = 1; $i < count($elements); $i++) { 1244 if (false === strpos($elements[$i], '=')) { 1245 $elName = trim($elements[$i]); 1246 $elValue = null; 1247 } else { 1248 list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i])); 1249 } 1250 $elName = strtolower($elName); 1251 if ('secure' == $elName) { 1252 $cookie['secure'] = true; 1253 } elseif ('expires' == $elName) { 1254 $cookie['expires'] = str_replace('"', '', $elValue); 1255 } elseif ('path' == $elName || 'domain' == $elName) { 1256 $cookie[$elName] = urldecode($elValue); 1257 } else { 1258 $cookie[$elName] = $elValue; 1259 } 1260 } 1261 } 1262 $this->_cookies[] = $cookie; 1263 } 1264 1265 1266 /** 1267 * Read a part of response body encoded with chunked Transfer-Encoding 1268 * 1269 * @access private 1270 * @return string 1271 */ 1272 function _readChunked() 1273 { 1274 // at start of the next chunk? 1275 if (0 == $this->_chunkLength) { 1276 $line = $this->_sock->readLine(); 1277 if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) { 1278 $this->_chunkLength = hexdec($matches[1]); 1279 // Chunk with zero length indicates the end 1280 if (0 == $this->_chunkLength) { 1281 $this->_sock->readLine(); // make this an eof() 1282 return ''; 1283 } 1284 } else { 1285 return ''; 1286 } 1287 } 1288 $data = $this->_sock->read($this->_chunkLength); 1289 $this->_chunkLength -= strlen($data); 1290 if (0 == $this->_chunkLength) { 1291 $this->_sock->readLine(); // Trailing CRLF 1292 } 1293 return $data; 1294 } 1295 1296 1297 /** 1298 * Notifies all registered listeners of an event. 1299 * 1300 * @param string Event name 1301 * @param mixed Additional data 1302 * @access private 1303 * @see HTTP_Request::_notify() 1304 */ 1305 function _notify($event, $data = null) 1306 { 1307 foreach (array_keys($this->_listeners) as $id) { 1308 $this->_listeners[$id]->update($this, $event, $data); 1309 } 1310 } 1311 1312 1313 /** 1314 * Decodes the message-body encoded by gzip 1315 * 1316 * The real decoding work is done by gzinflate() built-in function, this 1317 * method only parses the header and checks data for compliance with 1318 * RFC 1952 1319 * 1320 * @access private 1321 * @param string gzip-encoded data 1322 * @return string decoded data 1323 */ 1324 function _decodeGzip($data) 1325 { 1326 $length = strlen($data); 1327 // If it doesn't look like gzip-encoded data, don't bother 1328 if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) { 1329 return $data; 1330 } 1331 $method = ord(substr($data, 2, 1)); 1332 if (8 != $method) { 1333 return PEAR::raiseError('_decodeGzip(): unknown compression method'); 1334 } 1335 $flags = ord(substr($data, 3, 1)); 1336 if ($flags & 224) { 1337 return PEAR::raiseError('_decodeGzip(): reserved bits are set'); 1338 } 1339 1340 // header is 10 bytes minimum. may be longer, though. 1341 $headerLength = 10; 1342 // extra fields, need to skip 'em 1343 if ($flags & 4) { 1344 if ($length - $headerLength - 2 < 8) { 1345 return PEAR::raiseError('_decodeGzip(): data too short'); 1346 } 1347 $extraLength = unpack('v', substr($data, 10, 2)); 1348 if ($length - $headerLength - 2 - $extraLength[1] < 8) { 1349 return PEAR::raiseError('_decodeGzip(): data too short'); 1350 } 1351 $headerLength += $extraLength[1] + 2; 1352 } 1353 // file name, need to skip that 1354 if ($flags & 8) { 1355 if ($length - $headerLength - 1 < 8) { 1356 return PEAR::raiseError('_decodeGzip(): data too short'); 1357 } 1358 $filenameLength = strpos(substr($data, $headerLength), chr(0)); 1359 if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) { 1360 return PEAR::raiseError('_decodeGzip(): data too short'); 1361 } 1362 $headerLength += $filenameLength + 1; 1363 } 1364 // comment, need to skip that also 1365 if ($flags & 16) { 1366 if ($length - $headerLength - 1 < 8) { 1367 return PEAR::raiseError('_decodeGzip(): data too short'); 1368 } 1369 $commentLength = strpos(substr($data, $headerLength), chr(0)); 1370 if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) { 1371 return PEAR::raiseError('_decodeGzip(): data too short'); 1372 } 1373 $headerLength += $commentLength + 1; 1374 } 1375 // have a CRC for header. let's check 1376 if ($flags & 1) { 1377 if ($length - $headerLength - 2 < 8) { 1378 return PEAR::raiseError('_decodeGzip(): data too short'); 1379 } 1380 $crcReal = 0xffff & crc32(substr($data, 0, $headerLength)); 1381 $crcStored = unpack('v', substr($data, $headerLength, 2)); 1382 if ($crcReal != $crcStored[1]) { 1383 return PEAR::raiseError('_decodeGzip(): header CRC check failed'); 1384 } 1385 $headerLength += 2; 1386 } 1387 // unpacked data CRC and size at the end of encoded data 1388 $tmp = unpack('V2', substr($data, -8)); 1389 $dataCrc = $tmp[1]; 1390 $dataSize = $tmp[2]; 1391 1392 // finally, call the gzinflate() function 1393 $unpacked = @gzinflate(substr($data, $headerLength, -8), $dataSize); 1394 if (false === $unpacked) { 1395 return PEAR::raiseError('_decodeGzip(): gzinflate() call failed'); 1396 } elseif ($dataSize != strlen($unpacked)) { 1397 return PEAR::raiseError('_decodeGzip(): data size check failed'); 1398 } elseif ($dataCrc != crc32($unpacked)) { 1399 return PEAR::raiseError('_decodeGzip(): data CRC check failed'); 1400 } 1401 return $unpacked; 1402 } 1403 } // End class HTTP_Response 57 1404 ?>
Note: See TracChangeset
for help on using the changeset viewer.
