Changeset 6912 for temp/trunk/data
- Timestamp:
- 2006/10/26 18:39:36 (20 years ago)
- Location:
- temp/trunk/data/module
- Files:
-
- 2 edited
-
Net/Socket.php (modified) (1 diff)
-
Request.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
temp/trunk/data/module/Net/Socket.php
r6910 r6912 20 20 // $Id$ 21 21 22 if(!defined('SOCKET_PHP_DIR')) { 23 $SOCKET_PHP_DIR = realpath(dirname( __FILE__)); 24 define("SOCKET_PHP_DIR", $SOCKET_PHP_DIR); 25 } 26 require_once SOCKET_PHP_DIR. '/../PEAR.php'; 22 require_once '../PEAR.php'; 27 23 28 24 define('NET_SOCKET_READ', 1); -
temp/trunk/data/module/Request.php
r6911 r6912 53 53 //require_once REQUEST_PHP_DIR . '/Net/URL.php'; 54 54 55 require_once 'PEAR.php'; 55 56 require_once 'Net/Socket.php'; 56 57 require_once 'Net/URL.php'; 57 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_URL 74 * @var object Net_URL 75 */ 76 var $_url; 77 78 /** 79 * Type of request 80 * @var string 81 */ 82 var $_method; 83 84 /** 85 * HTTP Version 86 * @var string 87 */ 88 var $_http; 89 90 /** 91 * Request headers 92 * @var array 93 */ 94 var $_requestHeaders; 95 96 /** 97 * Basic Auth Username 98 * @var string 99 */ 100 var $_user; 101 102 /** 103 * Basic Auth Password 104 * @var string 105 */ 106 var $_pass; 107 108 /** 109 * Socket object 110 * @var object Net_Socket 111 */ 112 var $_sock; 113 114 /** 115 * Proxy server 116 * @var string 117 */ 118 var $_proxy_host; 119 120 /** 121 * Proxy port 122 * @var integer 123 */ 124 var $_proxy_port; 125 126 /** 127 * Proxy username 128 * @var string 129 */ 130 var $_proxy_user; 131 132 /** 133 * Proxy password 134 * @var string 135 */ 136 var $_proxy_pass; 137 138 /** 139 * Post data 140 * @var array 141 */ 142 var $_postData; 143 144 /** 145 * Request body 146 * @var string 147 */ 148 var $_body; 149 150 /** 151 * A list of methods that MUST NOT have a request body, per RFC 2616 152 * @var array 153 */ 154 var $_bodyDisallowed = array('TRACE'); 155 156 /** 157 * Files to post 158 * @var array 159 */ 160 var $_postFiles = array(); 161 162 /** 163 * Connection timeout. 164 * @var float 165 */ 166 var $_timeout; 167 168 /** 169 * HTTP_Response object 170 * @var object HTTP_Response 171 */ 172 var $_response; 173 174 /** 175 * Whether to allow redirects 176 * @var boolean 177 */ 178 var $_allowRedirects; 179 180 /** 181 * Maximum redirects allowed 182 * @var integer 183 */ 184 var $_maxRedirects; 185 186 /** 187 * Current number of redirects 188 * @var integer 189 */ 190 var $_redirects; 191 192 /** 193 * Whether to append brackets [] to array variables 194 * @var bool 195 */ 196 var $_useBrackets = true; 197 198 /** 199 * Attached listeners 200 * @var array 201 */ 202 var $_listeners = array(); 203 204 /** 205 * Whether to save response body in response object property 206 * @var bool 207 */ 208 var $_saveBody = true; 209 210 /** 211 * Timeout for reading from socket (array(seconds, microseconds)) 212 * @var array 213 */ 214 var $_readTimeout = null; 215 216 /** 217 * Options to pass to Net_Socket::connect. See stream_context_create 218 * @var array 219 */ 220 var $_socketOptions = null; 221 222 /** 223 * Constructor 224 * 225 * Sets up the object 226 * @param string The url to fetch/access 227 * @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 public 246 */ 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 useragent 279 $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )'); 280 281 // We don't do keep-alives by default 282 $this->addHeader('Connection', 'close'); 283 284 // Basic authentication 285 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 possible 295 // 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 requests 305 * 306 * @access private 307 * @return string 308 */ 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 requested 332 * @param array $params Associative array of parameters 333 * (see constructor for details) 334 * @access public 335 * @deprecated deprecated since 1.2, call the constructor if this is necessary 336 */ 337 function reset($url, $params = array()) 338 { 339 $this->HTTP_Request($url, $params); 340 } 341 342 /** 343 * Sets the URL to be requested 344 * 345 * @param string The url to be requested 346 * @access public 347 */ 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 URL 368 * 369 * @return string Current request URL 370 * @access public 371 */ 372 function getUrl($url) 373 { 374 return empty($this->_url)? '': $this->_url->getUrl(); 375 } 376 377 /** 378 * Sets a proxy to be used 379 * 380 * @param string Proxy host 381 * @param int Proxy port 382 * @param string Proxy username 383 * @param string Proxy password 384 * @access public 385 */ 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 parameters 400 * 401 * @param string Username 402 * @param string Password 403 */ 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 this 416 * @access public 417 */ 418 function setMethod($method) 419 { 420 $this->_method = $method; 421 } 422 423 /** 424 * Sets the HTTP version to use, 1.0 or 1.1 425 * 426 * @param string Version to use. Use the defined constants for this 427 * @access public 428 */ 429 function setHttpVer($http) 430 { 431 $this->_http = $http; 432 } 433 434 /** 435 * Adds a request header 436 * 437 * @param string Header name 438 * @param string Header value 439 * @access public 440 */ 441 function addHeader($name, $value) 442 { 443 $this->_requestHeaders[strtolower($name)] = $value; 444 } 445 446 /** 447 * Removes a request header 448 * 449 * @param string Header name to remove 450 * @access public 451 */ 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 parameter 461 * 462 * @param string Querystring parameter name 463 * @param string Querystring parameter value 464 * @param bool Whether the value is already urlencoded or not, default = not 465 * @access public 466 */ 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 supply 474 * 475 * @param string The querystring data. Should be of the format foo=bar&x=y etc 476 * @param bool Whether data is already urlencoded or not, default = already encoded 477 * @access public 478 */ 479 function addRawQueryString($querystring, $preencoded = true) 480 { 481 $this->_url->addRawQueryString($querystring, $preencoded); 482 } 483 484 /** 485 * Adds postdata items 486 * 487 * @param string Post data name 488 * @param string Post data value 489 * @param bool Whether data is already urlencoded or not, default = not 490 * @access public 491 */ 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 value 503 * 504 * @param mixed Callback function 505 * @param mixed Value to process 506 * @access private 507 * @return mixed Processed value 508 */ 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 upload 524 * 525 * This also changes content-type to 'multipart/form-data' for proper upload 526 * 527 * @access public 528 * @param string name of file-upload field 529 * @param mixed file name(s) 530 * @param mixed content-type(s) of file(s) being uploaded 531 * @return bool true on success 532 * @throws PEAR_Error 533 */ 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' => $contentType 549 ); 550 return true; 551 } 552 553 /** 554 * Adds raw postdata (DEPRECATED) 555 * 556 * @param string The data 557 * @param bool Whether data is preencoded or not, default = already encoded 558 * @access public 559 * @deprecated deprecated since 1.3.0, method setBody() should be used instead 560 */ 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 body 570 * @access public 571 */ 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 public 583 * @deprecated deprecated since 1.2 584 */ 585 function clearPostData() 586 { 587 $this->_postData = null; 588 } 589 590 /** 591 * Appends a cookie to "Cookie:" header 592 * 593 * @param string $name cookie name 594 * @param string $value cookie value 595 * @access public 596 */ 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 scenarios 607 * 608 * @access public 609 * @deprecated deprecated since 1.2 610 */ 611 function clearCookies() 612 { 613 $this->removeHeader('Cookie'); 614 } 615 616 /** 617 * Sends the request 618 * 619 * @access public 620 * @param bool Whether to store response body in Response object property, 621 * set this to false if downloading a LARGE file and using a Listener 622 * @return mixed PEAR error on error, true otherwise 623 */ 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 determines 634 // we running on at least 4.3.0 635 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 processing 643 $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-Alive 647 // 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 response 681 $this->_response = &new HTTP_Response($this->_sock, $this->_listeners); 682 $err = $this->_response->process( 683 $this->_saveBody && $saveBody, 684 HTTP_REQUEST_METHOD_HEAD != $this->_method 685 ); 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" property 710 } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) { 711 $sockets[$sockKey] =& $this->_sock; 712 } 713 714 // Check for redirection 715 if ( $this->_allowRedirects 716 AND $this->_redirects <= $this->_maxRedirects 717 AND $this->getResponseCode() > 300 718 AND $this->getResponseCode() < 399 719 AND !empty($this->_response->_headers['location'])) { 720 721 722 $redirect = $this->_response->_headers['location']; 723 724 // Absolute URL 725 if (preg_match('/^https?:\/\//i', $redirect)) { 726 $this->_url = &new Net_URL($redirect); 727 $this->addHeader('Host', $this->_generateHostHeader()); 728 // Absolute path 729 } elseif ($redirect{0} == '/') { 730 $this->_url->path = $redirect; 731 732 // Relative path 733 } 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 path 743 } 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 redirects 756 } 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 public 767 */ 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 code 778 * 779 * @access public 780 * @return mixed Response code, false if not set 781 */ 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 given 789 * 790 * @access public 791 * @param string The header name to return, do not set to get all headers 792 * @return mixed either the value of $headername (false if header is not present) 793 * or an array of all headers 794 */ 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 response 807 * 808 * @access public 809 * @return mixed response body, false if not set 810 */ 811 function getResponseBody() 812 { 813 return isset($this->_response->_body) ? $this->_response->_body : false; 814 } 815 816 /** 817 * Returns cookies set in response 818 * 819 * @access public 820 * @return mixed array of response cookies, false if none are present 821 */ 822 function getResponseCookies() 823 { 824 return isset($this->_response->_cookies) ? $this->_response->_cookies : false; 825 } 826 827 /** 828 * Builds the request string 829 * 830 * @access private 831 * @return string The request string 832 */ 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-type 855 $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 Headers 863 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 CRLF 871 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 array 877 } elseif (HTTP_REQUEST_METHOD_POST == $this->_method && 878 (!empty($this->_postData) || !empty($this->_postFiles))) { 879 880 // "normal" POST request 881 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 uploads 888 } 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 body 924 } 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 array 935 * into the simple one. 936 * 937 * @param string name for item 938 * @param mixed item's values 939 * @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 of 964 * the object's events 965 * 966 * @param object HTTP_Request_Listener instance to attach 967 * @return boolean whether the listener was successfully attached 968 * @access public 969 */ 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 listeners 982 * 983 * @param object HTTP_Request_Listener instance to detach 984 * @return boolean whether the listener was successfully detached 985 * @access public 986 */ 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 object 1002 * - 'connect': on connection to server 1003 * - 'sentRequest': after the request was sent 1004 * - 'disconnect': on disconnection from server 1005 * 1006 * Events sent by HTTP_Response object 1007 * - '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 name 1013 * @param mixed Additional data 1014 * @access private 1015 */ 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 class 1027 */ 1028 class HTTP_Response 1029 { 1030 /** 1031 * Socket object 1032 * @var object 1033 */ 1034 var $_sock; 1035 1036 /** 1037 * Protocol 1038 * @var string 1039 */ 1040 var $_protocol; 1041 1042 /** 1043 * Return code 1044 * @var string 1045 */ 1046 var $_code; 1047 1048 /** 1049 * Response headers 1050 * @var array 1051 */ 1052 var $_headers; 1053 1054 /** 1055 * Cookies set in response 1056 * @var array 1057 */ 1058 var $_cookies; 1059 1060 /** 1061 * Response body 1062 * @var string 1063 */ 1064 var $_body = ''; 1065 1066 /** 1067 * Used by _readChunked(): remaining length of the current chunk 1068 * @var string 1069 */ 1070 var $_chunkLength = 0; 1071 1072 /** 1073 * Attached listeners 1074 * @var array 1075 */ 1076 var $_listeners = array(); 1077 1078 /** 1079 * Bytes left to read from message-body 1080 * @var null|int 1081 */ 1082 var $_toRead; 1083 1084 /** 1085 * Constructor 1086 * 1087 * @param object Net_Socket socket to read the response from 1088 * @param array listeners attached to request 1089 * @return mixed PEAR Error on error, true otherwise 1090 */ 1091 function HTTP_Response(&$sock, &$listeners) 1092 { 1093 $this->_sock =& $sock; 1094 $this->_listeners =& $listeners; 1095 } 1096 1097 1098 /** 1099 * Processes a HTTP response 1100 * 1101 * This extracts response code, headers, cookies and decodes body if it 1102 * was encoded in some way 1103 * 1104 * @access public 1105 * @param bool Whether to store response body in object property, set 1106 * 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_Error 1111 * @return mixed true on success, PEAR_Error in case of malformed response 1112 */ 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 fields 1133 // 3. ... If a message is received with both a 1134 // 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 decode 1140 $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 needed 1174 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 header 1191 * 1192 * @access private 1193 * @param string HTTP header 1194 */ 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 array 1218 * 1219 * @access private 1220 * @param string value of Set-Cookie header 1221 */ 1222 function _parseCookie($headervalue) 1223 { 1224 $cookie = array( 1225 'expires' => null, 1226 'domain' => null, 1227 'path' => null, 1228 'secure' => false 1229 ); 1230 1231 // Only a name=value pair 1232 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 supplied 1238 } 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-Encoding 1269 * 1270 * @access private 1271 * @return string 1272 */ 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 end 1281 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 CRLF 1293 } 1294 return $data; 1295 } 1296 1297 1298 /** 1299 * Notifies all registered listeners of an event. 1300 * 1301 * @param string Event name 1302 * @param mixed Additional data 1303 * @access private 1304 * @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 gzip 1316 * 1317 * The real decoding work is done by gzinflate() built-in function, this 1318 * method only parses the header and checks data for compliance with 1319 * RFC 1952 1320 * 1321 * @access private 1322 * @param string gzip-encoded data 1323 * @return string decoded data 1324 */ 1325 function _decodeGzip($data) 1326 { 1327 $length = strlen($data); 1328 // If it doesn't look like gzip-encoded data, don't bother 1329 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 'em 1344 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 that 1355 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 also 1366 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 check 1377 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 data 1389 $tmp = unpack('V2', substr($data, -8)); 1390 $dataCrc = $tmp[1]; 1391 $dataSize = $tmp[2]; 1392 1393 // finally, call the gzinflate() function 1394 $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_Response 58 1405 ?>
Note: See TracChangeset
for help on using the changeset viewer.
