Automatically exported from code.google.com/p/planningalerts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1396 lines
45 KiB

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