PageRenderTime 64ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/code/web/public_php/webtt/cake/libs/http_socket.php

https://bitbucket.org/ryzom/ryzomcore
PHP | 1081 lines | 648 code | 107 blank | 326 comment | 150 complexity | a77da40e3fa0461eeefeb1337ea5f4ab MD5 | raw file
Possible License(s): Apache-2.0, AGPL-3.0, GPL-3.0, LGPL-2.1
  1. <?php
  2. /**
  3. * HTTP Socket connection class.
  4. *
  5. * PHP versions 4 and 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package cake
  16. * @subpackage cake.cake.libs
  17. * @since CakePHP(tm) v 1.2.0
  18. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  19. */
  20. App::import('Core', array('CakeSocket', 'Set', 'Router'));
  21. /**
  22. * Cake network socket connection class.
  23. *
  24. * Core base class for HTTP network communication. HttpSocket can be used as an
  25. * Object Oriented replacement for cURL in many places.
  26. *
  27. * @package cake
  28. * @subpackage cake.cake.libs
  29. */
  30. class HttpSocket extends CakeSocket {
  31. /**
  32. * Object description
  33. *
  34. * @var string
  35. * @access public
  36. */
  37. var $description = 'HTTP-based DataSource Interface';
  38. /**
  39. * When one activates the $quirksMode by setting it to true, all checks meant to
  40. * enforce RFC 2616 (HTTP/1.1 specs).
  41. * will be disabled and additional measures to deal with non-standard responses will be enabled.
  42. *
  43. * @var boolean
  44. * @access public
  45. */
  46. var $quirksMode = false;
  47. /**
  48. * The default values to use for a request
  49. *
  50. * @var array
  51. * @access public
  52. */
  53. var $request = array(
  54. 'method' => 'GET',
  55. 'uri' => array(
  56. 'scheme' => 'http',
  57. 'host' => null,
  58. 'port' => 80,
  59. 'user' => null,
  60. 'pass' => null,
  61. 'path' => null,
  62. 'query' => null,
  63. 'fragment' => null
  64. ),
  65. 'auth' => array(
  66. 'method' => 'Basic',
  67. 'user' => null,
  68. 'pass' => null
  69. ),
  70. 'version' => '1.1',
  71. 'body' => '',
  72. 'line' => null,
  73. 'header' => array(
  74. 'Connection' => 'close',
  75. 'User-Agent' => 'CakePHP'
  76. ),
  77. 'raw' => null,
  78. 'cookies' => array()
  79. );
  80. /**
  81. * The default structure for storing the response
  82. *
  83. * @var array
  84. * @access public
  85. */
  86. var $response = array(
  87. 'raw' => array(
  88. 'status-line' => null,
  89. 'header' => null,
  90. 'body' => null,
  91. 'response' => null
  92. ),
  93. 'status' => array(
  94. 'http-version' => null,
  95. 'code' => null,
  96. 'reason-phrase' => null
  97. ),
  98. 'header' => array(),
  99. 'body' => '',
  100. 'cookies' => array()
  101. );
  102. /**
  103. * Default configuration settings for the HttpSocket
  104. *
  105. * @var array
  106. * @access public
  107. */
  108. var $config = array(
  109. 'persistent' => false,
  110. 'host' => 'localhost',
  111. 'protocol' => 'tcp',
  112. 'port' => 80,
  113. 'timeout' => 30,
  114. 'request' => array(
  115. 'uri' => array(
  116. 'scheme' => 'http',
  117. 'host' => 'localhost',
  118. 'port' => 80
  119. ),
  120. 'auth' => array(
  121. 'method' => 'Basic',
  122. 'user' => null,
  123. 'pass' => null
  124. ),
  125. 'cookies' => array()
  126. )
  127. );
  128. /**
  129. * String that represents a line break.
  130. *
  131. * @var string
  132. * @access public
  133. */
  134. var $lineBreak = "\r\n";
  135. /**
  136. * Build an HTTP Socket using the specified configuration.
  137. *
  138. * You can use a url string to set the url and use default configurations for
  139. * all other options:
  140. *
  141. * `$http =& new HttpSocket('http://cakephp.org/');`
  142. *
  143. * Or use an array to configure multiple options:
  144. *
  145. * {{{
  146. * $http =& new HttpSocket(array(
  147. * 'host' => 'cakephp.org',
  148. * 'timeout' => 20
  149. * ));
  150. * }}}
  151. *
  152. * See HttpSocket::$config for options that can be used.
  153. *
  154. * @param mixed $config Configuration information, either a string url or an array of options.
  155. * @access public
  156. */
  157. function __construct($config = array()) {
  158. if (is_string($config)) {
  159. $this->_configUri($config);
  160. } elseif (is_array($config)) {
  161. if (isset($config['request']['uri']) && is_string($config['request']['uri'])) {
  162. $this->_configUri($config['request']['uri']);
  163. unset($config['request']['uri']);
  164. }
  165. $this->config = Set::merge($this->config, $config);
  166. }
  167. parent::__construct($this->config);
  168. }
  169. /**
  170. * Issue the specified request. HttpSocket::get() and HttpSocket::post() wrap this
  171. * method and provide a more granular interface.
  172. *
  173. * @param mixed $request Either an URI string, or an array defining host/uri
  174. * @return mixed false on error, request body on success
  175. * @access public
  176. */
  177. function request($request = array()) {
  178. $this->reset(false);
  179. if (is_string($request)) {
  180. $request = array('uri' => $request);
  181. } elseif (!is_array($request)) {
  182. return false;
  183. }
  184. if (!isset($request['uri'])) {
  185. $request['uri'] = null;
  186. }
  187. $uri = $this->_parseUri($request['uri']);
  188. $hadAuth = false;
  189. if (is_array($uri) && array_key_exists('user', $uri)) {
  190. $hadAuth = true;
  191. }
  192. if (!isset($uri['host'])) {
  193. $host = $this->config['host'];
  194. }
  195. if (isset($request['host'])) {
  196. $host = $request['host'];
  197. unset($request['host']);
  198. }
  199. $request['uri'] = $this->url($request['uri']);
  200. $request['uri'] = $this->_parseUri($request['uri'], true);
  201. $this->request = Set::merge($this->request, $this->config['request'], $request);
  202. if (!$hadAuth && !empty($this->config['request']['auth']['user'])) {
  203. $this->request['uri']['user'] = $this->config['request']['auth']['user'];
  204. $this->request['uri']['pass'] = $this->config['request']['auth']['pass'];
  205. }
  206. $this->_configUri($this->request['uri']);
  207. if (isset($host)) {
  208. $this->config['host'] = $host;
  209. }
  210. $cookies = null;
  211. if (is_array($this->request['header'])) {
  212. $this->request['header'] = $this->_parseHeader($this->request['header']);
  213. if (!empty($this->request['cookies'])) {
  214. $cookies = $this->buildCookies($this->request['cookies']);
  215. }
  216. $Host = $this->request['uri']['host'];
  217. $schema = '';
  218. $port = 0;
  219. if (isset($this->request['uri']['schema'])) {
  220. $schema = $this->request['uri']['schema'];
  221. }
  222. if (isset($this->request['uri']['port'])) {
  223. $port = $this->request['uri']['port'];
  224. }
  225. if (
  226. ($schema === 'http' && $port != 80) ||
  227. ($schema === 'https' && $port != 443) ||
  228. ($port != 80 && $port != 443)
  229. ) {
  230. $Host .= ':' . $port;
  231. }
  232. $this->request['header'] = array_merge(compact('Host'), $this->request['header']);
  233. }
  234. if (isset($this->request['auth']['user']) && isset($this->request['auth']['pass'])) {
  235. $this->request['header']['Authorization'] = $this->request['auth']['method'] . " " . base64_encode($this->request['auth']['user'] . ":" . $this->request['auth']['pass']);
  236. }
  237. if (isset($this->request['uri']['user']) && isset($this->request['uri']['pass'])) {
  238. $this->request['header']['Authorization'] = $this->request['auth']['method'] . " " . base64_encode($this->request['uri']['user'] . ":" . $this->request['uri']['pass']);
  239. }
  240. if (is_array($this->request['body'])) {
  241. $this->request['body'] = $this->_httpSerialize($this->request['body']);
  242. }
  243. if (!empty($this->request['body']) && !isset($this->request['header']['Content-Type'])) {
  244. $this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded';
  245. }
  246. if (!empty($this->request['body']) && !isset($this->request['header']['Content-Length'])) {
  247. $this->request['header']['Content-Length'] = strlen($this->request['body']);
  248. }
  249. $connectionType = null;
  250. if (isset($this->request['header']['Connection'])) {
  251. $connectionType = $this->request['header']['Connection'];
  252. }
  253. $this->request['header'] = $this->_buildHeader($this->request['header']) . $cookies;
  254. if (empty($this->request['line'])) {
  255. $this->request['line'] = $this->_buildRequestLine($this->request);
  256. }
  257. if ($this->quirksMode === false && $this->request['line'] === false) {
  258. return $this->response = false;
  259. }
  260. if ($this->request['line'] !== false) {
  261. $this->request['raw'] = $this->request['line'];
  262. }
  263. if ($this->request['header'] !== false) {
  264. $this->request['raw'] .= $this->request['header'];
  265. }
  266. $this->request['raw'] .= "\r\n";
  267. $this->request['raw'] .= $this->request['body'];
  268. $this->write($this->request['raw']);
  269. $response = null;
  270. while ($data = $this->read()) {
  271. $response .= $data;
  272. }
  273. if ($connectionType == 'close') {
  274. $this->disconnect();
  275. }
  276. $this->response = $this->_parseResponse($response);
  277. if (!empty($this->response['cookies'])) {
  278. $this->config['request']['cookies'] = array_merge($this->config['request']['cookies'], $this->response['cookies']);
  279. }
  280. return $this->response['body'];
  281. }
  282. /**
  283. * Issues a GET request to the specified URI, query, and request.
  284. *
  285. * Using a string uri and an array of query string parameters:
  286. *
  287. * `$response = $http->get('http://google.com/search', array('q' => 'cakephp', 'client' => 'safari'));`
  288. *
  289. * Would do a GET request to `http://google.com/search?q=cakephp&client=safari`
  290. *
  291. * You could express the same thing using a uri array and query string parameters:
  292. *
  293. * {{{
  294. * $response = $http->get(
  295. * array('host' => 'google.com', 'path' => '/search'),
  296. * array('q' => 'cakephp', 'client' => 'safari')
  297. * );
  298. * }}}
  299. *
  300. * @param mixed $uri URI to request. Either a string uri, or a uri array, see HttpSocket::_parseUri()
  301. * @param array $query Querystring parameters to append to URI
  302. * @param array $request An indexed array with indexes such as 'method' or uri
  303. * @return mixed Result of request, either false on failure or the response to the request.
  304. * @access public
  305. */
  306. function get($uri = null, $query = array(), $request = array()) {
  307. if (!empty($query)) {
  308. $uri = $this->_parseUri($uri);
  309. if (isset($uri['query'])) {
  310. $uri['query'] = array_merge($uri['query'], $query);
  311. } else {
  312. $uri['query'] = $query;
  313. }
  314. $uri = $this->_buildUri($uri);
  315. }
  316. $request = Set::merge(array('method' => 'GET', 'uri' => $uri), $request);
  317. return $this->request($request);
  318. }
  319. /**
  320. * Issues a POST request to the specified URI, query, and request.
  321. *
  322. * `post()` can be used to post simple data arrays to a url:
  323. *
  324. * {{{
  325. * $response = $http->post('http://example.com', array(
  326. * 'username' => 'batman',
  327. * 'password' => 'bruce_w4yne'
  328. * ));
  329. * }}}
  330. *
  331. * @param mixed $uri URI to request. See HttpSocket::_parseUri()
  332. * @param array $data Array of POST data keys and values.
  333. * @param array $request An indexed array with indexes such as 'method' or uri
  334. * @return mixed Result of request, either false on failure or the response to the request.
  335. * @access public
  336. */
  337. function post($uri = null, $data = array(), $request = array()) {
  338. $request = Set::merge(array('method' => 'POST', 'uri' => $uri, 'body' => $data), $request);
  339. return $this->request($request);
  340. }
  341. /**
  342. * Issues a PUT request to the specified URI, query, and request.
  343. *
  344. * @param mixed $uri URI to request, See HttpSocket::_parseUri()
  345. * @param array $data Array of PUT data keys and values.
  346. * @param array $request An indexed array with indexes such as 'method' or uri
  347. * @return mixed Result of request
  348. * @access public
  349. */
  350. function put($uri = null, $data = array(), $request = array()) {
  351. $request = Set::merge(array('method' => 'PUT', 'uri' => $uri, 'body' => $data), $request);
  352. return $this->request($request);
  353. }
  354. /**
  355. * Issues a DELETE request to the specified URI, query, and request.
  356. *
  357. * @param mixed $uri URI to request (see {@link _parseUri()})
  358. * @param array $data Query to append to URI
  359. * @param array $request An indexed array with indexes such as 'method' or uri
  360. * @return mixed Result of request
  361. * @access public
  362. */
  363. function delete($uri = null, $data = array(), $request = array()) {
  364. $request = Set::merge(array('method' => 'DELETE', 'uri' => $uri, 'body' => $data), $request);
  365. return $this->request($request);
  366. }
  367. /**
  368. * Normalizes urls into a $uriTemplate. If no template is provided
  369. * a default one will be used. Will generate the url using the
  370. * current config information.
  371. *
  372. * ### Usage:
  373. *
  374. * After configuring part of the request parameters, you can use url() to generate
  375. * urls.
  376. *
  377. * {{{
  378. * $http->configUri('http://www.cakephp.org');
  379. * $url = $http->url('/search?q=bar');
  380. * }}}
  381. *
  382. * Would return `http://www.cakephp.org/search?q=bar`
  383. *
  384. * url() can also be used with custom templates:
  385. *
  386. * `$url = $http->url('http://www.cakephp/search?q=socket', '/%path?%query');`
  387. *
  388. * Would return `/search?q=socket`.
  389. *
  390. * @param mixed $url Either a string or array of url options to create a url with.
  391. * @param string $uriTemplate A template string to use for url formatting.
  392. * @return mixed Either false on failure or a string containing the composed url.
  393. * @access public
  394. */
  395. function url($url = null, $uriTemplate = null) {
  396. if (is_null($url)) {
  397. $url = '/';
  398. }
  399. if (is_string($url)) {
  400. if ($url{0} == '/') {
  401. $url = $this->config['request']['uri']['host'].':'.$this->config['request']['uri']['port'] . $url;
  402. }
  403. if (!preg_match('/^.+:\/\/|\*|^\//', $url)) {
  404. $url = $this->config['request']['uri']['scheme'].'://'.$url;
  405. }
  406. } elseif (!is_array($url) && !empty($url)) {
  407. return false;
  408. }
  409. $base = array_merge($this->config['request']['uri'], array('scheme' => array('http', 'https'), 'port' => array(80, 443)));
  410. $url = $this->_parseUri($url, $base);
  411. if (empty($url)) {
  412. $url = $this->config['request']['uri'];
  413. }
  414. if (!empty($uriTemplate)) {
  415. return $this->_buildUri($url, $uriTemplate);
  416. }
  417. return $this->_buildUri($url);
  418. }
  419. /**
  420. * Parses the given message and breaks it down in parts.
  421. *
  422. * @param string $message Message to parse
  423. * @return array Parsed message (with indexed elements such as raw, status, header, body)
  424. * @access protected
  425. */
  426. function _parseResponse($message) {
  427. if (is_array($message)) {
  428. return $message;
  429. } elseif (!is_string($message)) {
  430. return false;
  431. }
  432. static $responseTemplate;
  433. if (empty($responseTemplate)) {
  434. $classVars = get_class_vars(__CLASS__);
  435. $responseTemplate = $classVars['response'];
  436. }
  437. $response = $responseTemplate;
  438. if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
  439. return false;
  440. }
  441. list($null, $response['raw']['status-line'], $response['raw']['header']) = $match;
  442. $response['raw']['response'] = $message;
  443. $response['raw']['body'] = substr($message, strlen($match[0]));
  444. if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $response['raw']['status-line'], $match)) {
  445. $response['status']['http-version'] = $match[1];
  446. $response['status']['code'] = (int)$match[2];
  447. $response['status']['reason-phrase'] = $match[3];
  448. }
  449. $response['header'] = $this->_parseHeader($response['raw']['header']);
  450. $transferEncoding = null;
  451. if (isset($response['header']['Transfer-Encoding'])) {
  452. $transferEncoding = $response['header']['Transfer-Encoding'];
  453. }
  454. $decoded = $this->_decodeBody($response['raw']['body'], $transferEncoding);
  455. $response['body'] = $decoded['body'];
  456. if (!empty($decoded['header'])) {
  457. $response['header'] = $this->_parseHeader($this->_buildHeader($response['header']).$this->_buildHeader($decoded['header']));
  458. }
  459. if (!empty($response['header'])) {
  460. $response['cookies'] = $this->parseCookies($response['header']);
  461. }
  462. foreach ($response['raw'] as $field => $val) {
  463. if ($val === '') {
  464. $response['raw'][$field] = null;
  465. }
  466. }
  467. return $response;
  468. }
  469. /**
  470. * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
  471. * 'body' and 'header' or false on failure.
  472. *
  473. * @param string $body A string continaing the body to decode.
  474. * @param mixed $encoding Can be false in case no encoding is being used, or a string representing the encoding.
  475. * @return mixed Array of response headers and body or false.
  476. * @access protected
  477. */
  478. function _decodeBody($body, $encoding = 'chunked') {
  479. if (!is_string($body)) {
  480. return false;
  481. }
  482. if (empty($encoding)) {
  483. return array('body' => $body, 'header' => false);
  484. }
  485. $decodeMethod = '_decode'.Inflector::camelize(str_replace('-', '_', $encoding)).'Body';
  486. if (!is_callable(array(&$this, $decodeMethod))) {
  487. if (!$this->quirksMode) {
  488. trigger_error(sprintf(__('HttpSocket::_decodeBody - Unknown encoding: %s. Activate quirks mode to surpress error.', true), h($encoding)), E_USER_WARNING);
  489. }
  490. return array('body' => $body, 'header' => false);
  491. }
  492. return $this->{$decodeMethod}($body);
  493. }
  494. /**
  495. * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
  496. * a result.
  497. *
  498. * @param string $body A string continaing the chunked body to decode.
  499. * @return mixed Array of response headers and body or false.
  500. * @access protected
  501. */
  502. function _decodeChunkedBody($body) {
  503. if (!is_string($body)) {
  504. return false;
  505. }
  506. $decodedBody = null;
  507. $chunkLength = null;
  508. while ($chunkLength !== 0) {
  509. if (!preg_match("/^([0-9a-f]+) *(?:;(.+)=(.+))?\r\n/iU", $body, $match)) {
  510. if (!$this->quirksMode) {
  511. trigger_error(__('HttpSocket::_decodeChunkedBody - Could not parse malformed chunk. Activate quirks mode to do this.', true), E_USER_WARNING);
  512. return false;
  513. }
  514. break;
  515. }
  516. $chunkSize = 0;
  517. $hexLength = 0;
  518. $chunkExtensionName = '';
  519. $chunkExtensionValue = '';
  520. if (isset($match[0])) {
  521. $chunkSize = $match[0];
  522. }
  523. if (isset($match[1])) {
  524. $hexLength = $match[1];
  525. }
  526. if (isset($match[2])) {
  527. $chunkExtensionName = $match[2];
  528. }
  529. if (isset($match[3])) {
  530. $chunkExtensionValue = $match[3];
  531. }
  532. $body = substr($body, strlen($chunkSize));
  533. $chunkLength = hexdec($hexLength);
  534. $chunk = substr($body, 0, $chunkLength);
  535. if (!empty($chunkExtensionName)) {
  536. /**
  537. * @todo See if there are popular chunk extensions we should implement
  538. */
  539. }
  540. $decodedBody .= $chunk;
  541. if ($chunkLength !== 0) {
  542. $body = substr($body, $chunkLength+strlen("\r\n"));
  543. }
  544. }
  545. $entityHeader = false;
  546. if (!empty($body)) {
  547. $entityHeader = $this->_parseHeader($body);
  548. }
  549. return array('body' => $decodedBody, 'header' => $entityHeader);
  550. }
  551. /**
  552. * Parses and sets the specified URI into current request configuration.
  553. *
  554. * @param mixed $uri URI, See HttpSocket::_parseUri()
  555. * @return array Current configuration settings
  556. * @access protected
  557. */
  558. function _configUri($uri = null) {
  559. if (empty($uri)) {
  560. return false;
  561. }
  562. if (is_array($uri)) {
  563. $uri = $this->_parseUri($uri);
  564. } else {
  565. $uri = $this->_parseUri($uri, true);
  566. }
  567. if (!isset($uri['host'])) {
  568. return false;
  569. }
  570. $config = array(
  571. 'request' => array(
  572. 'uri' => array_intersect_key($uri, $this->config['request']['uri']),
  573. 'auth' => array_intersect_key($uri, $this->config['request']['auth'])
  574. )
  575. );
  576. $this->config = Set::merge($this->config, $config);
  577. $this->config = Set::merge($this->config, array_intersect_key($this->config['request']['uri'], $this->config));
  578. return $this->config;
  579. }
  580. /**
  581. * Takes a $uri array and turns it into a fully qualified URL string
  582. *
  583. * @param mixed $uri Either A $uri array, or a request string. Will use $this->config if left empty.
  584. * @param string $uriTemplate The Uri template/format to use.
  585. * @return mixed A fully qualified URL formated according to $uriTemplate, or false on failure
  586. * @access protected
  587. */
  588. function _buildUri($uri = array(), $uriTemplate = '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') {
  589. if (is_string($uri)) {
  590. $uri = array('host' => $uri);
  591. }
  592. $uri = $this->_parseUri($uri, true);
  593. if (!is_array($uri) || empty($uri)) {
  594. return false;
  595. }
  596. $uri['path'] = preg_replace('/^\//', null, $uri['path']);
  597. $uri['query'] = $this->_httpSerialize($uri['query']);
  598. $stripIfEmpty = array(
  599. 'query' => '?%query',
  600. 'fragment' => '#%fragment',
  601. 'user' => '%user:%pass@',
  602. 'host' => '%host:%port/'
  603. );
  604. foreach ($stripIfEmpty as $key => $strip) {
  605. if (empty($uri[$key])) {
  606. $uriTemplate = str_replace($strip, null, $uriTemplate);
  607. }
  608. }
  609. $defaultPorts = array('http' => 80, 'https' => 443);
  610. if (array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) {
  611. $uriTemplate = str_replace(':%port', null, $uriTemplate);
  612. }
  613. foreach ($uri as $property => $value) {
  614. $uriTemplate = str_replace('%'.$property, $value, $uriTemplate);
  615. }
  616. if ($uriTemplate === '/*') {
  617. $uriTemplate = '*';
  618. }
  619. return $uriTemplate;
  620. }
  621. /**
  622. * Parses the given URI and breaks it down into pieces as an indexed array with elements
  623. * such as 'scheme', 'port', 'query'.
  624. *
  625. * @param string $uri URI to parse
  626. * @param mixed $base If true use default URI config, otherwise indexed array to set 'scheme', 'host', 'port', etc.
  627. * @return array Parsed URI
  628. * @access protected
  629. */
  630. function _parseUri($uri = null, $base = array()) {
  631. $uriBase = array(
  632. 'scheme' => array('http', 'https'),
  633. 'host' => null,
  634. 'port' => array(80, 443),
  635. 'user' => null,
  636. 'pass' => null,
  637. 'path' => '/',
  638. 'query' => null,
  639. 'fragment' => null
  640. );
  641. if (is_string($uri)) {
  642. $uri = parse_url($uri);
  643. }
  644. if (!is_array($uri) || empty($uri)) {
  645. return false;
  646. }
  647. if ($base === true) {
  648. $base = $uriBase;
  649. }
  650. if (isset($base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) {
  651. if (isset($uri['scheme']) && !isset($uri['port'])) {
  652. $base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])];
  653. } elseif (isset($uri['port']) && !isset($uri['scheme'])) {
  654. $base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])];
  655. }
  656. }
  657. if (is_array($base) && !empty($base)) {
  658. $uri = array_merge($base, $uri);
  659. }
  660. if (isset($uri['scheme']) && is_array($uri['scheme'])) {
  661. $uri['scheme'] = array_shift($uri['scheme']);
  662. }
  663. if (isset($uri['port']) && is_array($uri['port'])) {
  664. $uri['port'] = array_shift($uri['port']);
  665. }
  666. if (array_key_exists('query', $uri)) {
  667. $uri['query'] = $this->_parseQuery($uri['query']);
  668. }
  669. if (!array_intersect_key($uriBase, $uri)) {
  670. return false;
  671. }
  672. return $uri;
  673. }
  674. /**
  675. * This function can be thought of as a reverse to PHP5's http_build_query(). It takes a given query string and turns it into an array and
  676. * supports nesting by using the php bracket syntax. So this menas you can parse queries like:
  677. *
  678. * - ?key[subKey]=value
  679. * - ?key[]=value1&key[]=value2
  680. *
  681. * A leading '?' mark in $query is optional and does not effect the outcome of this function.
  682. * For the complete capabilities of this implementation take a look at HttpSocketTest::testparseQuery()
  683. *
  684. * @param mixed $query A query string to parse into an array or an array to return directly "as is"
  685. * @return array The $query parsed into a possibly multi-level array. If an empty $query is
  686. * given, an empty array is returned.
  687. * @access protected
  688. */
  689. function _parseQuery($query) {
  690. if (is_array($query)) {
  691. return $query;
  692. }
  693. $parsedQuery = array();
  694. if (is_string($query) && !empty($query)) {
  695. $query = preg_replace('/^\?/', '', $query);
  696. $items = explode('&', $query);
  697. foreach ($items as $item) {
  698. if (strpos($item, '=') !== false) {
  699. list($key, $value) = explode('=', $item, 2);
  700. } else {
  701. $key = $item;
  702. $value = null;
  703. }
  704. $key = urldecode($key);
  705. $value = urldecode($value);
  706. if (preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) {
  707. $subKeys = $matches[1];
  708. $rootKey = substr($key, 0, strpos($key, '['));
  709. if (!empty($rootKey)) {
  710. array_unshift($subKeys, $rootKey);
  711. }
  712. $queryNode =& $parsedQuery;
  713. foreach ($subKeys as $subKey) {
  714. if (!is_array($queryNode)) {
  715. $queryNode = array();
  716. }
  717. if ($subKey === '') {
  718. $queryNode[] = array();
  719. end($queryNode);
  720. $subKey = key($queryNode);
  721. }
  722. $queryNode =& $queryNode[$subKey];
  723. }
  724. $queryNode = $value;
  725. } else {
  726. $parsedQuery[$key] = $value;
  727. }
  728. }
  729. }
  730. return $parsedQuery;
  731. }
  732. /**
  733. * Builds a request line according to HTTP/1.1 specs. Activate quirks mode to work outside specs.
  734. *
  735. * @param array $request Needs to contain a 'uri' key. Should also contain a 'method' key, otherwise defaults to GET.
  736. * @param string $versionToken The version token to use, defaults to HTTP/1.1
  737. * @return string Request line
  738. * @access protected
  739. */
  740. function _buildRequestLine($request = array(), $versionToken = 'HTTP/1.1') {
  741. $asteriskMethods = array('OPTIONS');
  742. if (is_string($request)) {
  743. $isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match);
  744. if (!$this->quirksMode && (!$isValid || ($match[2] == '*' && !in_array($match[3], $asteriskMethods)))) {
  745. trigger_error(__('HttpSocket::_buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.', true), E_USER_WARNING);
  746. return false;
  747. }
  748. return $request;
  749. } elseif (!is_array($request)) {
  750. return false;
  751. } elseif (!array_key_exists('uri', $request)) {
  752. return false;
  753. }
  754. $request['uri'] = $this->_parseUri($request['uri']);
  755. $request = array_merge(array('method' => 'GET'), $request);
  756. $request['uri'] = $this->_buildUri($request['uri'], '/%path?%query');
  757. if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) {
  758. trigger_error(sprintf(__('HttpSocket::_buildRequestLine - The "*" asterisk character is only allowed for the following methods: %s. Activate quirks mode to work outside of HTTP/1.1 specs.', true), join(',', $asteriskMethods)), E_USER_WARNING);
  759. return false;
  760. }
  761. return $request['method'].' '.$request['uri'].' '.$versionToken.$this->lineBreak;
  762. }
  763. /**
  764. * Serializes an array for transport.
  765. *
  766. * @param array $data Data to serialize
  767. * @return string Serialized variable
  768. * @access protected
  769. */
  770. function _httpSerialize($data = array()) {
  771. if (is_string($data)) {
  772. return $data;
  773. }
  774. if (empty($data) || !is_array($data)) {
  775. return false;
  776. }
  777. return substr(Router::queryString($data), 1);
  778. }
  779. /**
  780. * Builds the header.
  781. *
  782. * @param array $header Header to build
  783. * @return string Header built from array
  784. * @access protected
  785. */
  786. function _buildHeader($header, $mode = 'standard') {
  787. if (is_string($header)) {
  788. return $header;
  789. } elseif (!is_array($header)) {
  790. return false;
  791. }
  792. $returnHeader = '';
  793. foreach ($header as $field => $contents) {
  794. if (is_array($contents) && $mode == 'standard') {
  795. $contents = implode(',', $contents);
  796. }
  797. foreach ((array)$contents as $content) {
  798. $contents = preg_replace("/\r\n(?![\t ])/", "\r\n ", $content);
  799. $field = $this->_escapeToken($field);
  800. $returnHeader .= $field.': '.$contents.$this->lineBreak;
  801. }
  802. }
  803. return $returnHeader;
  804. }
  805. /**
  806. * Parses an array based header.
  807. *
  808. * @param array $header Header as an indexed array (field => value)
  809. * @return array Parsed header
  810. * @access protected
  811. */
  812. function _parseHeader($header) {
  813. if (is_array($header)) {
  814. foreach ($header as $field => $value) {
  815. unset($header[$field]);
  816. $field = strtolower($field);
  817. preg_match_all('/(?:^|(?<=-))[a-z]/U', $field, $offsets, PREG_OFFSET_CAPTURE);
  818. foreach ($offsets[0] as $offset) {
  819. $field = substr_replace($field, strtoupper($offset[0]), $offset[1], 1);
  820. }
  821. $header[$field] = $value;
  822. }
  823. return $header;
  824. } elseif (!is_string($header)) {
  825. return false;
  826. }
  827. preg_match_all("/(.+):(.+)(?:(?<![\t ])" . $this->lineBreak . "|\$)/Uis", $header, $matches, PREG_SET_ORDER);
  828. $header = array();
  829. foreach ($matches as $match) {
  830. list(, $field, $value) = $match;
  831. $value = trim($value);
  832. $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
  833. $field = $this->_unescapeToken($field);
  834. $field = strtolower($field);
  835. preg_match_all('/(?:^|(?<=-))[a-z]/U', $field, $offsets, PREG_OFFSET_CAPTURE);
  836. foreach ($offsets[0] as $offset) {
  837. $field = substr_replace($field, strtoupper($offset[0]), $offset[1], 1);
  838. }
  839. if (!isset($header[$field])) {
  840. $header[$field] = $value;
  841. } else {
  842. $header[$field] = array_merge((array)$header[$field], (array)$value);
  843. }
  844. }
  845. return $header;
  846. }
  847. /**
  848. * Parses cookies in response headers.
  849. *
  850. * @param array $header Header array containing one ore more 'Set-Cookie' headers.
  851. * @return mixed Either false on no cookies, or an array of cookies received.
  852. * @access public
  853. * @todo Make this 100% RFC 2965 confirm
  854. */
  855. function parseCookies($header) {
  856. if (!isset($header['Set-Cookie'])) {
  857. return false;
  858. }
  859. $cookies = array();
  860. foreach ((array)$header['Set-Cookie'] as $cookie) {
  861. if (strpos($cookie, '";"') !== false) {
  862. $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
  863. $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
  864. } else {
  865. $parts = preg_split('/\;[ \t]*/', $cookie);
  866. }
  867. list($name, $value) = explode('=', array_shift($parts), 2);
  868. $cookies[$name] = compact('value');
  869. foreach ($parts as $part) {
  870. if (strpos($part, '=') !== false) {
  871. list($key, $value) = explode('=', $part);
  872. } else {
  873. $key = $part;
  874. $value = true;
  875. }
  876. $key = strtolower($key);
  877. if (!isset($cookies[$name][$key])) {
  878. $cookies[$name][$key] = $value;
  879. }
  880. }
  881. }
  882. return $cookies;
  883. }
  884. /**
  885. * Builds cookie headers for a request.
  886. *
  887. * @param array $cookies Array of cookies to send with the request.
  888. * @return string Cookie header string to be sent with the request.
  889. * @access public
  890. * @todo Refactor token escape mechanism to be configurable
  891. */
  892. function buildCookies($cookies) {
  893. $header = array();
  894. foreach ($cookies as $name => $cookie) {
  895. $header[] = $name.'='.$this->_escapeToken($cookie['value'], array(';'));
  896. }
  897. $header = $this->_buildHeader(array('Cookie' => implode('; ', $header)), 'pragmatic');
  898. return $header;
  899. }
  900. /**
  901. * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
  902. *
  903. * @param string $token Token to unescape
  904. * @return string Unescaped token
  905. * @access protected
  906. * @todo Test $chars parameter
  907. */
  908. function _unescapeToken($token, $chars = null) {
  909. $regex = '/"(['.join('', $this->_tokenEscapeChars(true, $chars)).'])"/';
  910. $token = preg_replace($regex, '\\1', $token);
  911. return $token;
  912. }
  913. /**
  914. * Escapes a given $token according to RFC 2616 (HTTP 1.1 specs)
  915. *
  916. * @param string $token Token to escape
  917. * @return string Escaped token
  918. * @access protected
  919. * @todo Test $chars parameter
  920. */
  921. function _escapeToken($token, $chars = null) {
  922. $regex = '/(['.join('', $this->_tokenEscapeChars(true, $chars)).'])/';
  923. $token = preg_replace($regex, '"\\1"', $token);
  924. return $token;
  925. }
  926. /**
  927. * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
  928. *
  929. * @param boolean $hex true to get them as HEX values, false otherwise
  930. * @return array Escape chars
  931. * @access protected
  932. * @todo Test $chars parameter
  933. */
  934. function _tokenEscapeChars($hex = true, $chars = null) {
  935. if (!empty($chars)) {
  936. $escape = $chars;
  937. } else {
  938. $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
  939. for ($i = 0; $i <= 31; $i++) {
  940. $escape[] = chr($i);
  941. }
  942. $escape[] = chr(127);
  943. }
  944. if ($hex == false) {
  945. return $escape;
  946. }
  947. $regexChars = '';
  948. foreach ($escape as $key => $char) {
  949. $escape[$key] = '\\x'.str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
  950. }
  951. return $escape;
  952. }
  953. /**
  954. * Resets the state of this HttpSocket instance to it's initial state (before Object::__construct got executed) or does
  955. * the same thing partially for the request and the response property only.
  956. *
  957. * @param boolean $full If set to false only HttpSocket::response and HttpSocket::request are reseted
  958. * @return boolean True on success
  959. * @access public
  960. */
  961. function reset($full = true) {
  962. static $initalState = array();
  963. if (empty($initalState)) {
  964. $initalState = get_class_vars(__CLASS__);
  965. }
  966. if ($full == false) {
  967. $this->request = $initalState['request'];
  968. $this->response = $initalState['response'];
  969. return true;
  970. }
  971. parent::reset($initalState);
  972. return true;
  973. }
  974. }