http_socket.php

Go to the documentation of this file.
00001 <?php
00002 /* SVN FILE: $Id: http__socket_8php-source.html 580 2008-07-01 14:45:49Z gwoo $ */
00003 /**
00004  * HTTP Socket connection class.
00005  *
00006  * PHP versions 4 and 5
00007  *
00008  * CakePHP(tm) :  Rapid Development Framework <http://www.cakephp.org/>
00009  * Copyright 2005-2008, Cake Software Foundation, Inc.
00010  *                              1785 E. Sahara Avenue, Suite 490-204
00011  *                              Las Vegas, Nevada 89104
00012  *
00013  * Licensed under The MIT License
00014  * Redistributions of files must retain the above copyright notice.
00015  *
00016  * @filesource
00017  * @copyright       Copyright 2005-2008, Cake Software Foundation, Inc.
00018  * @link                http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
00019  * @package         cake
00020  * @subpackage      cake.cake.libs
00021  * @since           CakePHP(tm) v 1.2.0
00022  * @version         $Revision: 580 $
00023  * @modifiedby      $LastChangedBy: gwoo $
00024  * @lastmodified    $Date: 2008-07-01 09:45:49 -0500 (Tue, 01 Jul 2008) $
00025  * @license         http://www.opensource.org/licenses/mit-license.php The MIT License
00026  */
00027 App::import('Core', array('Socket', 'Set', 'Router'));
00028 
00029 /**
00030  * Cake network socket connection class.
00031  *
00032  * Core base class for HTTP network communication.
00033  *
00034  * @package     cake
00035  * @subpackage  cake.cake.libs
00036  */
00037 class HttpSocket extends CakeSocket {
00038 /**
00039  * Object description
00040  *
00041  * @var string
00042  * @access public
00043  */
00044     var $description = 'HTTP-based DataSource Interface';
00045 
00046 /**
00047  * When one activates the $quirksMode by setting it to true, all checks meant to enforce RFC 2616 (HTTP/1.1 specs)
00048  * will be disabled and additional measures to deal with non-standard responses will be enabled.
00049  *
00050  * @var boolean
00051  * @access public
00052  */
00053     var $quirksMode = false;
00054 
00055 /**
00056  * The default values to use for a request
00057  *
00058  * @var array
00059  * @access public
00060  */
00061     var $request = array(
00062         'method' => 'GET',
00063         'uri' => array(
00064             'scheme' => 'http',
00065             'host' => null,
00066             'port' => 80,
00067             'user' => null,
00068             'pass' => null,
00069             'path' => null,
00070             'query' => null,
00071             'fragment' => null
00072         ),
00073         'auth' => array(
00074             'method' => 'basic',
00075             'user' => null,
00076             'pass' => null
00077         ),
00078         'version' => '1.1',
00079         'body' => '',
00080         'line' => null,
00081         'header' => array(
00082             'Connection' => 'close',
00083             'User-Agent' => 'CakePHP'
00084         ),
00085         'raw' => null,
00086         'cookies' => array()
00087     );
00088 /**
00089 * The default structure for storing the response
00090 *
00091 * @var array
00092 * @access public
00093 */
00094     var $response = array(
00095         'raw' => array(
00096             'status-line' => null,
00097             'header' => null,
00098             'body' => null,
00099             'response' => null
00100         ),
00101         'status' => array(
00102             'http-version' => null,
00103             'code' => null,
00104             'reason-phrase' => null
00105         ),
00106         'header' => array(),
00107         'body' => '',
00108         'cookies' => array(),
00109     );
00110 
00111 /**
00112  * Default configuration settings for the HttpSocket
00113  *
00114  * @var array
00115  * @access public
00116  */
00117     var $config = array(
00118         'persistent' => false,
00119         'host'       => 'localhost',
00120         'protocol'   => 'tcp',
00121         'port'       => 80,
00122         'timeout'    => 30,
00123         'request' => array(
00124             'uri' => array(
00125                 'scheme' => 'http',
00126                 'host' => 'localhost',
00127                 'port' => 80
00128             ),
00129             'auth' => array(
00130                 'method' => 'basic',
00131                 'user' => null,
00132                 'pass' => null
00133             ),
00134             'cookies' => array(),
00135         )
00136     );
00137 
00138 /**
00139  * String that represents a line break.
00140  *
00141  * @var string
00142  * @access public
00143  */
00144     var $lineBreak = "\r\n";
00145 
00146 /**
00147  * Build an HTTP Socket using the specified configuration.
00148  *
00149  * @param array $config Configuration
00150  */
00151     function __construct($config = array()) {
00152         if (is_string($config)) {
00153             $this->configUri($config);
00154         } elseif (is_array($config)) {
00155             if (isset($config['request']['uri']) && is_string($config['request']['uri'])) {
00156                 $this->configUri($config['request']['uri']);
00157                 unset($config['request']['uri']);
00158             }
00159             $this->config = Set::merge($this->config, $config);
00160         }
00161         parent::__construct($this->config);
00162     }
00163 
00164 /**
00165  * Issue the specified request.
00166  *
00167  * @param mixed $request Either an URI string, or an array defining host/uri
00168  * @return mixed false on error, request body on success
00169  * @access public
00170  */
00171     function request($request = array()) {
00172         $this->reset(false);
00173 
00174         if (is_string($request)) {
00175             $request = array('uri' => $request);
00176         } elseif (!is_array($request)) {
00177             return false;
00178         }
00179 
00180         if (!isset($request['uri'])) {
00181             $request['uri'] = null;
00182         }
00183         $uri = $this->parseUri($request['uri']);
00184 
00185         if (!isset($uri['host'])) {
00186             $host = $this->config['host'];
00187         }
00188         if (isset($request['host'])) {
00189             $host = $request['host'];
00190             unset($request['host']);
00191         }
00192 
00193         $request['uri'] = $this->url($request['uri']);
00194         $request['uri'] = $this->parseUri($request['uri'], true);
00195         $this->request = Set::merge($this->request, $this->config['request'], $request);
00196 
00197         $this->configUri($this->request['uri']);
00198 
00199         if (isset($host)) {
00200             $this->config['host'] = $host;
00201         }
00202         $cookies = null;
00203 
00204         if (is_array($this->request['header'])) {
00205             $this->request['header'] = $this->parseHeader($this->request['header']);
00206             if (!empty($this->request['cookies'])) {
00207                 $cookies = $this->buildCookies($this->request['cookies']);
00208             }
00209             $this->request['header'] = array_merge(array('Host' => $this->request['uri']['host']), $this->request['header']);
00210         }
00211 
00212         if (isset($this->request['auth']['user']) && isset($this->request['auth']['pass'])) {
00213             $this->request['header']['Authorization'] = $this->request['auth']['method'] ." ". base64_encode($this->request['auth']['user'] .":".$this->request['auth']['pass']);
00214         }
00215         if (isset($this->request['uri']['user']) && isset($this->request['uri']['pass'])) {
00216             $this->request['header']['Authorization'] = $this->request['auth']['method'] ." ". base64_encode($this->request['uri']['user'] .":".$this->request['uri']['pass']);
00217         }
00218 
00219         if (is_array($this->request['body'])) {
00220             $this->request['body'] = $this->httpSerialize($this->request['body']);
00221         }
00222 
00223         if (!empty($this->request['body']) && !isset($this->request['header']['Content-Type'])) {
00224             $this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded';
00225         }
00226 
00227         if (!empty($this->request['body']) && !isset($this->request['header']['Content-Length'])) {
00228             $this->request['header']['Content-Length'] = strlen($this->request['body']);
00229         }
00230 
00231         $connectionType = @$this->request['header']['Connection'];
00232         $this->request['header'] = $this->buildHeader($this->request['header']).$cookies;
00233 
00234         if (empty($this->request['line'])) {
00235             $this->request['line'] = $this->buildRequestLine($this->request);
00236         }
00237 
00238         if ($this->quirksMode === false && $this->request['line'] === false) {
00239             return $this->response = false;
00240         }
00241 
00242         if ($this->request['line'] !== false) {
00243             $this->request['raw'] = $this->request['line'];
00244         }
00245 
00246         if ($this->request['header'] !== false) {
00247             $this->request['raw'] .= $this->request['header'];
00248         }
00249 
00250         $this->request['raw'] .= "\r\n";
00251         $this->request['raw'] .= $this->request['body'];
00252         $this->write($this->request['raw']);
00253 
00254         $response = null;
00255         while ($data = $this->read()) {
00256             $response .= $data;
00257         }
00258 
00259         if ($connectionType == 'close') {
00260             $this->disconnect();
00261         }
00262 
00263         $this->response = $this->parseResponse($response);
00264         if (!empty($this->response['cookies'])) {
00265             $this->config['request']['cookies'] = array_merge($this->config['request']['cookies'], $this->response['cookies']);
00266         }
00267 
00268         return $this->response['body'];
00269     }
00270 
00271 /**
00272  * Issues a GET request to the specified URI, query, and request.
00273  *
00274  * @param mixed $uri URI to request (see {@link parseUri()})
00275  * @param array $query Query to append to URI
00276  * @param array $request An indexed array with indexes such as 'method' or uri
00277  * @return mixed Result of request
00278  * @access public
00279  */
00280     function get($uri = null, $query = array(), $request = array()) {
00281         if (!empty($query)) {
00282             $uri =$this->parseUri($uri);
00283             if (isset($uri['query'])) {
00284                 $uri['query'] = array_merge($uri['query'], $query);
00285             } else {
00286                 $uri['query'] = $query;
00287             }
00288             $uri = $this->buildUri($uri);
00289         }
00290 
00291         $request = Set::merge(array('method' => 'GET', 'uri' => $uri), $request);
00292         return $this->request($request);
00293     }
00294 
00295 /**
00296  * Issues a POST request to the specified URI, query, and request.
00297  *
00298  * @param mixed $uri URI to request (see {@link parseUri()})
00299  * @param array $query Query to append to URI
00300  * @param array $request An indexed array with indexes such as 'method' or uri
00301  * @return mixed Result of request
00302  * @access public
00303  */
00304     function post($uri = null, $data = array(), $request = array()) {
00305         $request = Set::merge(array('method' => 'POST', 'uri' => $uri, 'body' => $data), $request);
00306         return $this->request($request);
00307     }
00308 
00309 /**
00310  * Issues a PUT request to the specified URI, query, and request.
00311  *
00312  * @param mixed $uri URI to request (see {@link parseUri()})
00313  * @param array $query Query to append to URI
00314  * @param array $request An indexed array with indexes such as 'method' or uri
00315  * @return mixed Result of request
00316  * @access public
00317  */
00318     function put($uri = null, $data = array(), $request = array()) {
00319         $request = Set::merge(array('method' => 'PUT', 'uri' => $uri, 'body' => $data), $request);
00320         return $this->request($request);
00321     }
00322 
00323 /**
00324  * Issues a DELETE request to the specified URI, query, and request.
00325  *
00326  * @param mixed $uri URI to request (see {@link parseUri()})
00327  * @param array $query Query to append to URI
00328  * @param array $request An indexed array with indexes such as 'method' or uri
00329  * @return mixed Result of request
00330  * @access public
00331  */
00332     function delete($uri = null, $data = array(), $request = array()) {
00333         $request = Set::merge(array('method' => 'DELETE', 'uri' => $uri, 'body' => $data), $request);
00334         return $this->request($request);
00335     }
00336 /**
00337  * undocumented function
00338  *
00339  * @param unknown $url
00340  * @param unknown $uriTemplate
00341  * @return void
00342  * @access public
00343  */
00344     function url($url = null, $uriTemplate = null) {
00345         if (is_null($url)) {
00346             $url = '/';
00347         }
00348         if (is_string($url)) {
00349             if ($url{0} == '/') {
00350                 $url = $this->config['request']['uri']['host'].':'.$this->config['request']['uri']['port'] . $url;
00351             }
00352             if (!preg_match('/^.+:\/\/|\*|^\//', $url)) {
00353                 $url = $this->config['request']['uri']['scheme'].'://'.$url;
00354             }
00355         } elseif (!is_array($url) && !empty($url)) {
00356             return false;
00357         }
00358 
00359         $base = array_merge($this->config['request']['uri'], array('scheme' => array('http', 'https'), 'port' => array(80, 443)));
00360         $url = $this->parseUri($url, $base);
00361 
00362         if (empty($url)) {
00363             $url = $this->config['request']['uri'];
00364         }
00365 
00366         if (!empty($uriTemplate)) {
00367             return $this->buildUri($url, $uriTemplate);
00368         }
00369         return $this->buildUri($url);
00370     }
00371 
00372 /**
00373  * Parses the given message and breaks it down in parts.
00374  *
00375  * @param string $message Message to parse
00376  * @return array Parsed message (with indexed elements such as raw, status, header, body)
00377  * @access protected
00378  */
00379     function parseResponse($message) {
00380         if (is_array($message)) {
00381             return $message;
00382         } elseif (!is_string($message)) {
00383             return false;
00384         }
00385 
00386         static $responseTemplate;
00387 
00388         if (empty($responseTemplate)) {
00389             $classVars = get_class_vars(__CLASS__);
00390             $responseTemplate = $classVars['response'];
00391         }
00392 
00393         $response = $responseTemplate;
00394 
00395         if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
00396             return false;
00397         }
00398 
00399         list($null, $response['raw']['status-line'], $response['raw']['header']) = $match;
00400         $response['raw']['response'] = $message;
00401         $response['raw']['body'] = substr($message, strlen($match[0]));
00402 
00403         if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $response['raw']['status-line'], $match)) {
00404             $response['status']['http-version'] = $match[1];
00405             $response['status']['code'] = (int)$match[2];
00406             $response['status']['reason-phrase'] = $match[3];
00407         }
00408 
00409         $response['header'] = $this->parseHeader($response['raw']['header']);
00410         $decoded = $this->decodeBody($response['raw']['body'], @$response['header']['Transfer-Encoding']);
00411         $response['body'] = $decoded['body'];
00412 
00413         if (!empty($decoded['header'])) {
00414             $response['header'] = $this->parseHeader($this->buildHeader($response['header']).$this->buildHeader($decoded['header']));
00415         }
00416 
00417         if (!empty($response['header'])) {
00418             $response['cookies'] = $this->parseCookies($response['header']);
00419         }
00420 
00421         foreach ($response['raw'] as $field => $val) {
00422             if ($val === '') {
00423                 $response['raw'][$field] = null;
00424             }
00425         }
00426 
00427         return $response;
00428     }
00429 /**
00430  * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
00431  * 'body' and 'header' or false on failure.
00432  *
00433  * @param string $body A string continaing the body to decode
00434  * @param mixed $encoding Can be false in case no encoding is being used, or a string representing the encoding
00435  * @return mixed Array or false
00436  * @access protected
00437  */
00438     function decodeBody($body, $encoding = 'chunked') {
00439         if (!is_string($body)) {
00440             return false;
00441         }
00442         if (empty($encoding)) {
00443             return array('body' => $body, 'header' => false);
00444         }
00445         $decodeMethod = 'decode'.Inflector::camelize(str_replace('-', '_', $encoding)).'Body';
00446 
00447         if (!is_callable(array(&$this, $decodeMethod))) {
00448             if (!$this->quirksMode) {
00449                 trigger_error(sprintf(__('HttpSocket::decodeBody - Unknown encoding: %s. Activate quirks mode to surpress error.', true), h($encoding)), E_USER_WARNING);
00450             }
00451             return array('body' => $body, 'header' => false);
00452         }
00453         return $this->{$decodeMethod}($body);
00454     }
00455 /**
00456  * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
00457  * a result.
00458  *
00459  * @param string $body A string continaing the chunked body to decode
00460  * @return mixed Array or false
00461  * @access protected
00462  */
00463     function decodeChunkedBody($body) {
00464         if (!is_string($body)) {
00465             return false;
00466         }
00467 
00468         $decodedBody = null;
00469         $chunkLength = null;
00470 
00471         while ($chunkLength !== 0) {
00472             if (!preg_match("/^([0-9a-f]+) *(?:;(.+)=(.+))?\r\n/iU", $body, $match)) {
00473                 if (!$this->quirksMode) {
00474                     trigger_error(__('HttpSocket::decodeChunkedBody - Could not parse malformed chunk. Activate quirks mode to do this.', true), E_USER_WARNING);
00475                     return false;
00476                 }
00477                 break;
00478             }
00479 
00480             $chunkSize = 0;
00481             $hexLength = 0;
00482             $chunkExtensionName = '';
00483             $chunkExtensionValue = '';
00484             if (isset($match[0])) {
00485                 $chunkSize = $match[0];
00486             }
00487             if (isset($match[1])) {
00488                 $hexLength = $match[1];
00489             }
00490             if (isset($match[2])) {
00491                 $chunkExtensionName = $match[2];
00492             }
00493             if (isset($match[3])) {
00494                 $chunkExtensionValue = $match[3];
00495             }
00496 
00497             $body = substr($body, strlen($chunkSize));
00498             $chunkLength = hexdec($hexLength);
00499             $chunk = substr($body, 0, $chunkLength);
00500             if (!empty($chunkExtensionName)) {
00501                 /**
00502                  * @todo See if there are popular chunk extensions we should implement
00503                  */
00504             }
00505             $decodedBody .= $chunk;
00506             if ($chunkLength !== 0) {
00507                 $body = substr($body, $chunkLength+strlen("\r\n"));
00508             }
00509         }
00510 
00511         $entityHeader = false;
00512         if (!empty($body)) {
00513             $entityHeader = $this->parseHeader($body);
00514         }
00515         return array('body' => $decodedBody, 'header' => $entityHeader);
00516     }
00517 /**
00518  * Parses and sets the specified URI into current request configuration.
00519  *
00520  * @param mixed $uri URI (see {@link parseUri()})
00521  * @return array Current configuration settings
00522  * @access protected
00523  */
00524     function configUri($uri = null) {
00525         if (empty($uri)) {
00526             return false;
00527         }
00528 
00529         if (is_array($uri)) {
00530             $uri = $this->parseUri($uri);
00531         } else {
00532             $uri = $this->parseUri($uri, true);
00533         }
00534 
00535         if (!isset($uri['host'])) {
00536             return false;
00537         }
00538 
00539         $config = array(
00540             'request' => array(
00541                 'uri' => array_intersect_key($uri, $this->config['request']['uri']),
00542                 'auth' => array_intersect_key($uri, $this->config['request']['auth'])
00543             )
00544         );
00545         $this->config = Set::merge($this->config, $config);
00546         $this->config = Set::merge($this->config, array_intersect_key($this->config['request']['uri'], $this->config));
00547         return $this->config;
00548     }
00549 /**
00550  * Takes a $uri array and turns it into a fully qualified URL string
00551  *
00552  * @param array $uri A $uri array, or uses $this->config if left empty
00553  * @param string $uriTemplate The Uri template/format to use
00554  * @return string A fully qualified URL formated according to $uriTemplate
00555  * @access protected
00556  */
00557     function buildUri($uri = array(), $uriTemplate = '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') {
00558         if (is_string($uri)) {
00559             $uri = array('host' => $uri);
00560         }
00561         $uri = $this->parseUri($uri, true);
00562 
00563         if (!is_array($uri) || empty($uri)) {
00564             return false;
00565         }
00566 
00567         $uri['path'] = preg_replace('/^\//', null, $uri['path']);
00568         $uri['query'] = $this->httpSerialize($uri['query']);
00569         $stripIfEmpty = array(
00570             'query' => '?%query',
00571             'fragment' => '#%fragment',
00572             'user' => '%user:%pass@'
00573         );
00574 
00575         foreach ($stripIfEmpty as $key => $strip) {
00576             if (empty($uri[$key])) {
00577                 $uriTemplate = str_replace($strip, null, $uriTemplate);
00578             }
00579         }
00580 
00581         $defaultPorts = array('http' => 80, 'https' => 443);
00582         if (array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) {
00583             $uriTemplate = str_replace(':%port', null, $uriTemplate);
00584         }
00585 
00586         foreach ($uri as $property => $value) {
00587             $uriTemplate = str_replace('%'.$property, $value, $uriTemplate);
00588         }
00589 
00590         if ($uriTemplate === '/*') {
00591             $uriTemplate = '*';
00592         }
00593         return $uriTemplate;
00594     }
00595 /**
00596  * Parses the given URI and breaks it down into pieces as an indexed array with elements
00597  * such as 'scheme', 'port', 'query'.
00598  *
00599  * @param string $uri URI to parse
00600  * @param mixed $base If true use default URI config, otherwise indexed array to set 'scheme', 'host', 'port', etc.
00601  * @return array Parsed URI
00602  * @access protected
00603  */
00604     function parseUri($uri = null, $base = array()) {
00605         $uriBase = array(
00606             'scheme' => array('http', 'https'),
00607             'host' => null,
00608             'port' => array(80, 443),
00609             'user' => null,
00610             'pass' => null,
00611             'path' => '/',
00612             'query' => null,
00613             'fragment' => null
00614         );
00615 
00616         if (is_string($uri)) {
00617             $uri = parse_url($uri);
00618         }
00619         if (!is_array($uri) || empty($uri)) {
00620             return false;
00621         }
00622         if ($base === true) {
00623             $base = $uriBase;
00624         }
00625 
00626         if (isset($base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) {
00627             if (isset($uri['scheme']) && !isset($uri['port'])) {
00628                 $base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])];
00629             } elseif (isset($uri['port']) && !isset($uri['scheme'])) {
00630                 $base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])];
00631             }
00632         }
00633 
00634         if (is_array($base) && !empty($base)) {
00635             $uri = array_merge($base, $uri);
00636         }
00637 
00638         if (isset($uri['scheme']) && is_array($uri['scheme'])) {
00639             $uri['scheme'] = array_shift($uri['scheme']);
00640         }
00641         if (isset($uri['port']) && is_array($uri['port'])) {
00642             $uri['port'] = array_shift($uri['port']);
00643         }
00644 
00645         if (array_key_exists('query', $uri)) {
00646             $uri['query'] = $this->parseQuery($uri['query']);
00647         }
00648 
00649         if (!array_intersect_key($uriBase, $uri)) {
00650             return false;
00651         }
00652         return $uri;
00653     }
00654 /**
00655  * 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
00656  * supports nesting by using the php bracket syntax. So this menas you can parse queries like:
00657  *
00658  * - ?key[subKey]=value
00659  * - ?key[]=value1&key[]=value2
00660  *
00661  * A leading '?' mark in $query is optional and does not effect the outcome of this function. For the complete capabilities of this implementation
00662  * take a look at HttpSocketTest::testParseQuery()
00663  *
00664  * @param mixed $query A query string to parse into an array or an array to return directly "as is"
00665  * @return array The $query parsed into a possibly multi-level array. If an empty $query is given, an empty array is returned.
00666  * @access protected
00667  */
00668     function parseQuery($query) {
00669         if (is_array($query)) {
00670             return $query;
00671         }
00672         $parsedQuery = array();
00673 
00674         if (is_string($query) && !empty($query)) {
00675             $query = preg_replace('/^\?/', '', $query);
00676             $items = explode('&', $query);
00677 
00678             foreach ($items as $item) {
00679                 if (strpos($item, '=') !== false) {
00680                     list($key, $value) = explode('=', $item);
00681                 } else {
00682                     $key = $item;
00683                     $value = null;
00684                 }
00685 
00686                 $key = urldecode($key);
00687                 $value = urldecode($value);
00688 
00689                 if (preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) {
00690                     $subKeys = $matches[1];
00691                     $rootKey = substr($key, 0, strpos($key, '['));
00692                     if (!empty($rootKey)) {
00693                         array_unshift($subKeys, $rootKey);
00694                     }
00695                     $queryNode =& $parsedQuery;
00696 
00697                     foreach ($subKeys as $subKey) {
00698                         if (!is_array($queryNode)) {
00699                             $queryNode = array();
00700                         }
00701 
00702                         if ($subKey === '') {
00703                             $queryNode[] = array();
00704                             end($queryNode);
00705                             $subKey = key($queryNode);
00706                         }
00707                         $queryNode =& $queryNode[$subKey];
00708                     }
00709                     $queryNode = $value;
00710                 } else {
00711                     $parsedQuery[$key] = $value;
00712                 }
00713             }
00714         }
00715         return $parsedQuery;
00716     }
00717 /**
00718  * Builds a request line according to HTTP/1.1 specs. Activate quirks mode to work outside specs.
00719  *
00720  * @param array $request Needs to contain a 'uri' key. Should also contain a 'method' key, otherwise defaults to GET.
00721  * @param string $versionToken The version token to use, defaults to HTTP/1.1
00722  * @return string Request line
00723  * @access protected
00724  */
00725     function buildRequestLine($request = array(), $versionToken = 'HTTP/1.1') {
00726         $asteriskMethods = array('OPTIONS');
00727 
00728         if (is_string($request)) {
00729             $isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match);
00730             if (!$this->quirksMode && (!$isValid || ($match[2] == '*' && !in_array($match[3], $asteriskMethods)))) {
00731                 trigger_error(__('HttpSocket::buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.', true), E_USER_WARNING);
00732                 return false;
00733             }
00734             return $request;
00735         } elseif (!is_array($request)) {
00736             return false;
00737         } elseif (!array_key_exists('uri', $request)) {
00738             return false;
00739         }
00740 
00741         $request['uri'] = $this->parseUri($request['uri']);
00742         $request = array_merge(array('method' => 'GET'), $request);
00743         $request['uri'] = $this->buildUri($request['uri'], '/%path?%query');
00744 
00745         if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) {
00746             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);
00747             return false;
00748         }
00749         return $request['method'].' '.$request['uri'].' '.$versionToken.$this->lineBreak;
00750     }
00751 
00752 /**
00753  * Serializes an array for transport.
00754  *
00755  * @param array $data Data to serialize
00756  * @return string Serialized variable
00757  * @access protected
00758  */
00759     function httpSerialize($data = array()) {
00760         if (is_string($data)) {
00761             return $data;
00762         }
00763         if (empty($data) || !is_array($data)) {
00764             return false;
00765         }
00766         return substr(Router::queryString($data), 1);
00767     }
00768 /**
00769  * Builds the header.
00770  *
00771  * @param array $header Header to build
00772  * @return string Header built from array
00773  * @access protected
00774  */
00775     function buildHeader($header, $mode = 'standard') {
00776         if (is_string($header)) {
00777             return $header;
00778         } elseif (!is_array($header)) {
00779             return false;
00780         }
00781 
00782         $returnHeader = '';
00783         foreach ($header as $field => $contents) {
00784             if (is_array($contents) && $mode == 'standard') {
00785                 $contents = join(',', $contents);
00786             }
00787             foreach ((array)$contents as $content) {
00788                 $contents = preg_replace("/\r\n(?![\t ])/", "\r\n ", $content);
00789                 $field = $this->escapeToken($field);
00790 
00791                 $returnHeader .= $field.': '.$contents.$this->lineBreak;
00792             }
00793         }
00794         return $returnHeader;
00795     }
00796 
00797 /**
00798  * Parses an array based header.
00799  *
00800  * @param array $header Header as an indexed array (field => value)
00801  * @return array Parsed header
00802  * @access protected
00803  */
00804     function parseHeader($header) {
00805         if (is_array($header)) {
00806             foreach ($header as $field => $value) {
00807                 unset($header[$field]);
00808                 $field = strtolower($field);
00809                 preg_match_all('/(?:^|(?<=-))[a-z]/U', $field, $offsets, PREG_OFFSET_CAPTURE);
00810 
00811                 foreach ($offsets[0] as $offset) {
00812                     $field = substr_replace($field, strtoupper($offset[0]), $offset[1], 1);
00813                 }
00814                 $header[$field] = $value;
00815             }
00816             return $header;
00817         } elseif (!is_string($header)) {
00818             return false;
00819         }
00820 
00821         preg_match_all("/(.+):(.+)(?:(?<![\t ])".$this->lineBreak."|\$)/Uis", $header, $matches, PREG_SET_ORDER);
00822 
00823         $header = array();
00824         foreach ($matches as $match) {
00825             list(, $field, $value) = $match;
00826 
00827             $value = trim($value);
00828             $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
00829 
00830             $field = $this->unescapeToken($field);
00831 
00832             $field = strtolower($field);
00833             preg_match_all('/(?:^|(?<=-))[a-z]/U', $field, $offsets, PREG_OFFSET_CAPTURE);
00834             foreach ($offsets[0] as $offset) {
00835                 $field = substr_replace($field, strtoupper($offset[0]), $offset[1], 1);
00836             }
00837 
00838             if (!isset($header[$field])) {
00839                 $header[$field] = $value;
00840             } else {
00841                 $header[$field] = array_merge((array)$header[$field], (array)$value);
00842             }
00843         }
00844         return $header;
00845     }
00846 /**
00847  * undocumented function
00848  *
00849  * @param unknown $header
00850  * @return void
00851  * @access public
00852  * @todo Make this 100% RFC 2965 confirm
00853  */
00854     function parseCookies($header) {
00855         if (!isset($header['Set-Cookie'])) {
00856             return false;
00857         }
00858 
00859         $cookies = array();
00860         foreach ((array)$header['Set-Cookie'] as $cookie) {
00861             $parts = preg_split('/(?<![^;]");[ \t]*/', $cookie);
00862             list($name, $value) = explode('=', array_shift($parts));
00863             $cookies[$name] = compact('value');
00864             foreach ($parts as $part) {
00865                 if (strpos($part, '=') !== false) {
00866                     list($key, $value) = explode('=', $part);
00867                 } else {
00868                     $key = $part;
00869                     $value = true;
00870                 }
00871 
00872                 $key = strtolower($key);
00873                 if (!isset($cookies[$name][$key])) {
00874                     $cookies[$name][$key] = $value;
00875                 }
00876             }
00877         }
00878         return $cookies;
00879     }
00880 /**
00881  * undocumented function
00882  *
00883  * @param unknown $cookies
00884  * @return void
00885  * @access public
00886  * @todo Refactor token escape mechanism to be configurable
00887  */
00888     function buildCookies($cookies) {
00889         $header = array();
00890         foreach ($cookies as $name => $cookie) {
00891             $header[] = $name.'='.$this->escapeToken($cookie['value'], array(';'));
00892         }
00893         $header = $this->buildHeader(array('Cookie' => $header), 'pragmatic');
00894         return $header;
00895     }
00896 /**
00897  * undocumented function
00898  *
00899  * @return void
00900  * @access public
00901  */
00902     function saveCookies() {
00903 
00904     }
00905 /**
00906  * undocumented function
00907  *
00908  * @return void
00909  * @access public
00910  */
00911     function loadCookies() {
00912 
00913     }
00914 /**
00915  * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
00916  *
00917  * @param string $token Token to unescape
00918  * @return string Unescaped token
00919  * @access protected
00920  * @todo Test $chars parameter
00921  */
00922     function unescapeToken($token, $chars = null) {
00923         $regex = '/"(['.join('', $this->__tokenEscapeChars(true, $chars)).'])"/';
00924         $token = preg_replace($regex, '\\1', $token);
00925         return $token;
00926     }
00927 /**
00928  * Escapes a given $token according to RFC 2616 (HTTP 1.1 specs)
00929  *
00930  * @param string $token Token to escape
00931  * @return string Escaped token
00932  * @access protected
00933  * @todo Test $chars parameter
00934  */
00935     function escapeToken($token, $chars = null) {
00936         $regex = '/(['.join('', $this->__tokenEscapeChars(true, $chars)).'])/';
00937         $token = preg_replace($regex, '"\\1"', $token);
00938         return $token;
00939     }
00940 
00941 /**
00942  * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
00943  *
00944  * @param boolean $hex true to get them as HEX values, false otherwise
00945  * @return array Escape chars
00946  * @access private
00947  * @todo Test $chars parameter
00948  */
00949     function __tokenEscapeChars($hex = true, $chars = null) {
00950         if (!empty($chars)) {
00951             $escape = $chars;
00952         } else {
00953             $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
00954             for ($i = 0; $i <= 31; $i++) {
00955                 $escape[] = chr($i);
00956             }
00957             $escape[] = chr(127);
00958         }
00959 
00960         if ($hex == false) {
00961             return $escape;
00962         }
00963         $regexChars = '';
00964         foreach ($escape as $key => $char) {
00965             $escape[$key] = '\\x'.str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
00966         }
00967         return $escape;
00968     }
00969 /**
00970  * Resets the state of this HttpSocket instance to it's initial state (before Object::__construct got executed) or does
00971  * the same thing partially for the request and the response property only.
00972  *
00973  * @param boolean $full If set to false only HttpSocket::response and HttpSocket::request are reseted
00974  * @return boolean True on success
00975  * @access public
00976  */
00977     function reset($full = true) {
00978         static $initalState = array();
00979         if (empty($initalState)) {
00980             $initalState = get_class_vars(__CLASS__);
00981         }
00982 
00983         if ($full == false) {
00984             $this->request = $initalState['request'];
00985             $this->response = $initalState['response'];
00986             return true;
00987         }
00988         parent::reset($initalState);
00989         return true;
00990     }
00991 }
00992 
00993 ?>