Open-Source PHP Framework - Designed for rapid development of performance-oriented scalable applications

/mvc/components/httpSocket

[return to app]
1 <?php
2
/**
3  * HTTP Socket connection class.
4  *
5  * PHP versions 5
6  *
7  * This is CakePHP's http_socket and cake_socket classes updated to PHP5 syntax
8  * and merged for Vork
9  *
10  * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
11  * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
12  *
13  * Licensed under The MIT License
14  * Redistributions of files must retain the above copyright notice.
15  *
16  * @copyright     Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
17  * @link          http://cakephp.org CakePHP(tm) Project
18  * @package       cake
19  * @subpackage    cake.cake.libs
20  * @since         CakePHP(tm) v 1.2.0
21  * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
22  */
23
24 /**
25  * Cake network socket connection class.
26  *
27  * Core base class for HTTP network communication. HttpSocket can be used as an
28  * Object Oriented replacement for cURL in many places.
29  *
30  * @package       cake
31  * @subpackage    cake.cake.libs
32  */
33
class httpSocketComponent {
34     
/**
35      * Object description
36      *
37      * @var string
38      * @access public
39      */
40     
public $description 'HTTP-based DataSource Interface';
41
42     
/**
43      * When one activates the $quirksMode by setting it to true, all checks meant to
44      * enforce RFC 2616 (HTTP/1.1 specs).
45      * will be disabled and additional measures to deal with non-standard responses will be enabled.
46      *
47      * @var boolean
48      * @access public
49      */
50     
public $quirksMode false;
51
52     
/**
53      * The default values to use for a request
54      *
55      * @var array
56      * @access public
57      */
58     
public $request = array(
59         
'method'    => 'GET',
60         
'uri'       => array(
61             
'scheme'    => 'http',
62             
'host'      => null,
63             
'port'      => 80,
64             
'user'      => null,
65             
'pass'      => null,
66             
'path'      => null,
67             
'query'     => null,
68             
'fragment'  => null
69         
),
70         
'auth'      => array(
71             
'method'    => 'Basic',
72             
'user'      => null,
73             
'pass'      => null
74         
),
75         
'version'   => '1.1',
76         
'body'      => '',
77         
'line'      => null,
78         
'header'    => array(
79             
'Connection'=> 'close',
80             
'User-Agent'=> 'VorkPHP'
81         
),
82         
'raw'       => null,
83         
'cookies'   => array()
84     );
85
86     
/**
87     * The default structure for storing the response
88     *
89     * @var array
90     * @access public
91     */
92     
public $response = array(
93         
'raw'       => array(
94             
'status-line'   => null,
95             
'header'        => null,
96             
'body'          => null,
97             
'response'      => null
98         
),
99         
'status'    => array(
100             
'http-version'  => null,
101             
'code'          => null,
102             
'reason-phrase' => null
103         
),
104         
'header'    => array(),
105         
'body'      => '',
106         
'cookies'   => array()
107     );
108
109     
/**
110      * Default configuration settings for the HttpSocket
111      *
112      * @var array
113      * @access public
114      */
115     
public $config = array(
116         
'persistent'    => false,
117         
'host'          => 'localhost',
118         
'protocol'      => 'tcp',
119         
'port'          => 80,
120         
'timeout'       => 30,
121         
'request'       => array(
122             
'uri'           => array(
123                 
'scheme'        => 'http',
124                 
'host'          => 'localhost',
125                 
'port'          => 80
126             
),
127             
'auth'          => array(
128                 
'method'        => 'Basic',
129                 
'user'          => null,
130                 
'pass'          => null
131             
),
132             
'cookies'       => array()
133         )
134     );
135
136     
/**
137      * String that represents a line break.
138      *
139      * @var string
140      * @access public
141      */
142     
public $lineBreak "\r\n";
143
144     
/**
145      * Reference to socket connection resource
146      *
147      * @var resource
148      * @access public
149      */
150     
public $connection null;
151
152     
/**
153      * This boolean contains the current state of the CakeSocket class
154      *
155      * @var boolean
156      * @access public
157      */
158     
public $connected false;
159
160     
/**
161      * This variable contains an array with the last error number (num) and string (str)
162      *
163      * @var array
164      * @access public
165      */
166     
public $lastError = array();
167
168     
/**
169      * Build an HTTP Socket using the specified configuration.
170      *
171      * You can use a url string to set the url and use default configurations for
172      * all other options:
173      *
174      * `$http =& new HttpSockect('http://cakephp.org/');`
175      *
176      * Or use an array to configure multiple options:
177      *
178      * {{{
179      * $http =& new HttpSocket(array(
180      *    'host' => 'cakephp.org',
181      *    'timeout' => 20
182      * ));
183      * }}}
184      *
185      * See HttpSocket::$config for options that can be used.
186      *
187      * @param mixed $config Configuration information, either a string url or an array of options.
188      * @access public
189      */
190     
public function __construct($config = array()) {
191         if (
is_string($config)) {
192             
$this->_configUri($config);
193         } elseif (
is_array($config)) {
194             if (isset(
$config['request']['uri']) && is_string($config['request']['uri'])) {
195                 
$this->_configUri($config['request']['uri']);
196                 unset(
$config['request']['uri']);
197             }
198             
$this->config get::component('set')->merge($this->config$config);
199         }
200         if (!
is_numeric($this->config['protocol'])) {
201             
$this->config['protocol'] = getprotobyname($this->config['protocol']);
202         }
203     }
204     
205     
/**
206      * Destructor, used to disconnect from current connection.
207      *
208      * @access private
209      */
210     
public function __destruct() {
211         
$this->disconnect();
212     }
213
214     
/**
215      * Issue the specified request. HttpSocket::get() and HttpSocket::post() wrap this
216      * method and provide a more granular interface.
217      *
218      * @param mixed $request Either an URI string, or an array defining host/uri
219      * @return mixed false on error, request body on success
220      * @access public
221      */
222     
public function request($request = array()) {
223         
$this->reset(false);
224
225         if (
is_string($request)) {
226             
$request = array('uri' => $request);
227         } elseif (!
is_array($request)) {
228             return 
false;
229         }
230
231         if (!isset(
$request['uri'])) {
232             
$request['uri'] = null;
233         }
234         
$uri $this->_parseUri($request['uri']);
235         
$hadAuth false;
236         if (
is_array($uri) && array_key_exists('user'$uri)) {
237             
$hadAuth true;
238         }
239         if (!isset(
$uri['host'])) {
240             
$host $this->config['host'];
241         }
242         if (isset(
$request['host'])) {
243             
$host $request['host'];
244             unset(
$request['host']);
245         }
246         
$request['uri'] = $this->url($request['uri']);
247         
$request['uri'] = $this->_parseUri($request['uri'], true);
248         
$this->request get::component('set')->merge($this->request$this->config['request'], $request);
249
250         if (!
$hadAuth && !empty($this->config['request']['auth']['user'])) {
251             
$this->request['uri']['user'] = $this->config['request']['auth']['user'];
252             
$this->request['uri']['pass'] = $this->config['request']['auth']['pass'];
253         }
254         
$this->_configUri($this->request['uri']);
255
256         if (isset(
$host)) {
257             
$this->config['host'] = $host;
258         }
259         
$cookies null;
260
261         if (
is_array($this->request['header'])) {
262             
$this->request['header'] = $this->_parseHeader($this->request['header']);
263             if (!empty(
$this->request['cookies'])) {
264                 
$cookies $this->buildCookies($this->request['cookies']);
265             }
266             
$Host $this->request['uri']['host'];
267             
$schema '';
268             
$port 0;
269             if (isset(
$this->request['uri']['schema'])) {
270                 
$schema $this->request['uri']['schema'];
271             }
272             if (isset(
$this->request['uri']['port'])) {
273                 
$port $this->request['uri']['port'];
274             }
275             if (
276                 (
$schema === 'http' && $port != 80) ||
277                 (
$schema === 'https' && $port != 443) ||
278                 (
$port != 80 && $port != 443)
279             ) {
280                 
$Host .= ':' $port;
281             }
282             
$this->request['header'] = array_merge(compact('Host'), $this->request['header']);
283         }
284
285         if (isset(
$this->request['auth']['user']) && isset($this->request['auth']['pass'])) {
286             
$this->request['header']['Authorization'] = $this->request['auth']['method']
287                                                       . 
" " base64_encode($this->request['auth']['user']
288                                                       . 
":" $this->request['auth']['pass']);
289         }
290         if (isset(
$this->request['uri']['user']) && isset($this->request['uri']['pass'])) {
291             
$this->request['header']['Authorization'] = $this->request['auth']['method']
292                                                       . 
" " base64_encode($this->request['uri']['user']
293                                                       . 
":" $this->request['uri']['pass']);
294         }
295
296         if (
is_array($this->request['body'])) {
297             
$this->request['body'] = $this->_httpSerialize($this->request['body']);
298         }
299
300         if (!empty(
$this->request['body']) && !isset($this->request['header']['Content-Type'])) {
301             
$this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded';
302         }
303
304         if (!empty(
$this->request['body']) && !isset($this->request['header']['Content-Length'])) {
305             
$this->request['header']['Content-Length'] = strlen($this->request['body']);
306         }
307
308         
$connectionType null;
309         if (isset(
$this->request['header']['Connection'])) {
310             
$connectionType $this->request['header']['Connection'];
311         }
312         
$this->request['header'] = $this->_buildHeader($this->request['header']) . $cookies;
313
314         if (empty(
$this->request['line'])) {
315             
$this->request['line'] = $this->_buildRequestLine($this->request);
316         }
317
318         if (
$this->quirksMode === false && $this->request['line'] === false) {
319             return 
$this->response false;
320         }
321
322         if (
$this->request['line'] !== false) {
323             
$this->request['raw'] = $this->request['line'];
324         }
325
326         if (
$this->request['header'] !== false) {
327             
$this->request['raw'] .= $this->request['header'];
328         }
329
330         
$this->request['raw'] .= "\r\n";
331         
$this->request['raw'] .= $this->request['body'];
332         
$this->write($this->request['raw']);
333
334         
$response null;
335         while (
$data $this->read()) {
336             
$response .= $data;
337         }
338
339         if (
$connectionType == 'close') {
340             
$this->disconnect();
341         }
342
343         
$this->response $this->_parseResponse($response);
344         if (!empty(
$this->response['cookies'])) {
345             
$this->config['request']['cookies'] = array_merge($this->config['request']['cookies'],
346                                                               
$this->response['cookies']);
347         }
348
349         return 
$this->response['body'];
350     }
351
352     
/**
353      * Issues a GET request to the specified URI, query, and request.
354      *
355      * Using a string uri and an array of query string parameters:
356      *
357      * `$response = $http->get('http://google.com/search', array('q' => 'cakephp', 'client' => 'safari'));`
358      *
359      * Would do a GET request to `http://google.com/search?q=cakephp&client=safari`
360      *
361      * You could express the same thing using a uri array and query string parameters:
362      *
363      * {{{
364      * $response = $http->get(
365      *     array('host' => 'google.com', 'path' => '/search'),
366      *     array('q' => 'cakephp', 'client' => 'safari')
367      * );
368      * }}}
369      *
370      * @param mixed $uri URI to request. Either a string uri, or a uri array, see HttpSocket::_parseUri()
371      * @param array $query Querystring parameters to append to URI
372      * @param array $request An indexed array with indexes such as 'method' or uri
373      * @return mixed Result of request, either false on failure or the response to the request.
374      * @access public
375      */
376     
public function get($uri null$query = array(), $request = array()) {
377         if (!empty(
$query)) {
378             
$uri $this->_parseUri($uri);
379             if (isset(
$uri['query'])) {
380                 
$uri['query'] = array_merge($uri['query'], $query);
381             } else {
382                 
$uri['query'] = $query;
383             }
384             
$uri $this->_buildUri($uri);
385         }
386
387         
$request get::component('set')->merge(array('method' => 'GET''uri' => $uri), $request);
388         return 
$this->request($request);
389     }
390
391     
/**
392      * Issues a POST request to the specified URI, query, and request.
393      *
394      * `post()` can be used to post simple data arrays to a url:
395      *
396      * {{{
397      * $response = $http->post('http://example.com', array(
398      *     'username' => 'batman',
399      *     'password' => 'bruce_w4yne'
400      * ));
401      * }}}
402      *
403      * @param mixed $uri URI to request. See HttpSocket::_parseUri()
404      * @param array $data Array of POST data keys and values.
405      * @param array $request An indexed array with indexes such as 'method' or uri
406      * @return mixed Result of request, either false on failure or the response to the request.
407      * @access public
408      */
409     
public function post($uri null$data = array(), $request = array()) {
410         
$request get::component('set')->merge(array('method' => 'POST''uri' => $uri'body' => $data),
 
$request);
411         return 
$this->request($request);
412     }
413
414     
/**
415      * Issues a PUT request to the specified URI, query, and request.
416      *
417      * @param mixed $uri URI to request, See HttpSocket::_parseUri()
418      * @param array $data Array of PUT data keys and values.
419      * @param array $request An indexed array with indexes such as 'method' or uri
420      * @return mixed Result of request
421      * @access public
422      */
423     
public function put($uri null$data = array(), $request = array()) {
424         
$request get::component('set')->merge(array('method' => 'PUT''uri' => $uri'body' => $data),
 
$request);
425         return 
$this->request($request);
426     }
427
428     
/**
429      * Issues a DELETE request to the specified URI, query, and request.
430      *
431      * @param mixed $uri URI to request (see {@link _parseUri()})
432      * @param array $data Query to append to URI
433      * @param array $request An indexed array with indexes such as 'method' or uri
434      * @return mixed Result of request
435      * @access public
436      */
437     
public function delete($uri null$data = array(), $request = array()) {
438         
$request get::component('set')->merge(array('method' => 'DELETE''uri' => $uri'body' => $data),
 
$request);
439         return 
$this->request($request);
440     }
441     
442     
/**
443      * Connect the socket to the given host and port.
444      *
445      * @return boolean Success
446      * @access public
447      */
448     
public function connect() {
449         if (isset(
$this->connection) && $this->connection != null) {
450             
$this->disconnect();
451         }
452
453         
$scheme null;
454         if (isset(
$this->config['request']) && $this->config['request']['uri']['scheme'] == 'https') {
455             
$scheme 'ssl://';
456         }
457
458         if (
$this->config['persistent'] == true) {
459             
$tmp null;
460             
$this->connection = @pfsockopen($scheme.$this->config['host'], $this->config['port'],
461                                             
$errNum$errStr$this->config['timeout']);
462         } else {
463             
$this->connection = @fsockopen($scheme.$this->config['host'], $this->config['port'],
464                                            
$errNum$errStr$this->config['timeout']);
465         }
466
467         if (!empty(
$errNum) || !empty($errStr)) {
468             
$this->setLastError($errStr$errNum);
469         }
470
471         
$this->connected is_resource($this->connection);
472         if (
$this->connected) {
473             
stream_set_timeout($this->connection$this->config['timeout']);
474         }
475         return 
$this->connected;
476     }
477     
478     
/**
479      * Get the host name of the current connection.
480      *
481      * @return string Host name
482      * @access public
483      */
484     
public function host() {
485         if (
get::component('validation')->ip($this->config['host'])) {
486             return 
gethostbyaddr($this->config['host']);
487         } else {
488             return 
gethostbyaddr($this->address());
489         }
490     }
491     
492     
/**
493      * Get the IP address of the current connection.
494      *
495      * @return string IP address
496      * @access public
497      */
498     
public function address() {
499         if (
get::component('validation')->ip($this->config['host'])) {
500             return 
$this->config['host'];
501         } else {
502             return 
gethostbyname($this->config['host']);
503         }
504     }
505     
506     
/**
507      * Get all IP addresses associated with the current connection.
508      *
509      * @return array IP addresses
510      * @access public
511      */
512     
public function addresses() {
513         if (
get::component('validation')->ip($this->config['host'])) {
514             return array(
$this->config['host']);
515         } else {
516             return 
gethostbynamel($this->config['host']);
517         }
518     }
519     
520     
/**
521      * Get the last error as a string.
522      *
523      * @return string Last error
524      * @access public
525      */
526     
public function lastError() {
527         if (!empty(
$this->lastError)) {
528             return 
$this->lastError['num'] . ': ' $this->lastError['str'];
529         } else {
530             return 
null;
531         }
532     }
533
534     
/**
535      * Set the last error.
536      *
537      * @param integer $errNum Error code
538      * @param string $errStr Error string
539      * @access public
540      */
541     
public function setLastError($errNum$errStr) {
542         
$this->lastError = array('num' => $errNum'str' => $errStr);
543     }
544     
545     
/**
546      * Write data to the socket.
547      *
548      * @param string $data The data to write to the socket
549      * @return boolean Success
550      * @access public
551      */
552     
public function write($data) {
553         if (!
$this->connected) {
554             if (!
$this->connect()) {
555                 return 
false;
556             }
557         }
558
559         return 
fwrite($this->connection$datastrlen($data));
560     }
561
562     
/**
563      * Read data from the socket. Returns false if no data is available or no connection could be
564      * established.
565      *
566      * @param integer $length Optional buffer length to read; defaults to 1024
567      * @return mixed Socket data
568      * @access public
569      */
570     
public function read($length 1024) {
571         if (!
$this->connected) {
572             if (!
$this->connect()) {
573                 return 
false;
574             }
575         }
576
577         if (!
feof($this->connection)) {
578             
$buffer fread($this->connection$length);
579             
$info stream_get_meta_data($this->connection);
580             if (
$info['timed_out']) {
581                 
$this->setLastError(E_WARNING'Connection timed out');
582                 return 
false;
583             }
584             return 
$buffer;
585         } else {
586             return 
false;
587         }
588     }
589
590     
/**
591      * Abort socket operation.
592      *
593      * @return boolean Success
594      * @access public
595      */
596     
public function abort() {
597     }
598
599     
/**
600      * Disconnect the socket from the current connection.
601      *
602      * @return boolean Success
603      * @access public
604      */
605     
public function disconnect() {
606         if (!
is_resource($this->connection)) {
607             
$this->connected false;
608             return 
true;
609         }
610         
$this->connected = !fclose($this->connection);
611
612         if (!
$this->connected) {
613             
$this->connection null;
614         }
615         return !
$this->connected;
616     }
617
618     
/**
619      * Normalizes urls into a $uriTemplate.  If no template is provided
620      * a default one will be used. Will generate the url using the
621      * current config information.
622      *
623      * ### Usage:
624      *
625      * After configuring part of the request parameters, you can use url() to generate
626      * urls.
627      *
628      * {{{
629      * $http->configUri('http://www.cakephp.org');
630      * $url = $http->url('/search?q=bar');
631      * }}}
632      *
633      * Would return `http://www.cakephp.org/search?q=bar`
634      *
635      * url() can also be used with custom templates:
636      *
637      * `$url = $http->url('http://www.cakephp/search?q=socket', '/%path?%query');`
638      *
639      * Would return `/search?q=socket`.
640      *
641      * @param mixed $url Either a string or array of url options to create a url with.
642      * @param string $uriTemplate A template string to use for url formatting.
643      * @return mixed Either false on failure or a string containing the composed url.
644      * @access public
645      */
646     
public function url($url null$uriTemplate null) {
647         if (
is_null($url)) {
648             
$url '/';
649         }
650         if (
is_string($url)) {
651             if (
$url{0} == '/') {
652                 
$url $this->config['request']['uri']['host'].':'.$this->config['request']['uri']['port'] . $url;
653             }
654             if (!
preg_match('/^.+:\/\/|\*|^\//'$url)) {
655                 
$url $this->config['request']['uri']['scheme'].'://'.$url;
656             }
657         } elseif (!
is_array($url) && !empty($url)) {
658             return 
false;
659         }
660
661         
$base array_merge($this->config['request']['uri'],
662                             array(
'scheme' => array('http''https'), 'port' => array(80443)));
663         
$url $this->_parseUri($url$base);
664
665         if (empty(
$url)) {
666             
$url $this->config['request']['uri'];
667         }
668
669         if (!empty(
$uriTemplate)) {
670             return 
$this->_buildUri($url$uriTemplate);
671         }
672         return 
$this->_buildUri($url);
673     }
674
675     
/**
676      * Parses the given message and breaks it down in parts.
677      *
678      * @param string $message Message to parse
679      * @return array Parsed message (with indexed elements such as raw, status, header, body)
680      * @access protected
681      */
682     
protected function _parseResponse($message) {
683         if (
is_array($message)) {
684             return 
$message;
685         } elseif (!
is_string($message)) {
686             return 
false;
687         }
688
689         static 
$responseTemplate;
690
691         if (empty(
$responseTemplate)) {
692             
$classVars get_class_vars(__CLASS__);
693             
$responseTemplate $classVars['response'];
694         }
695
696         
$response $responseTemplate;
697
698         if (!
preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us"$message$match)) {
699             return 
false;
700         }
701
702         list(
$null$response['raw']['status-line'], $response['raw']['header']) = $match;
703         
$response['raw']['response'] = $message;
704         
$response['raw']['body'] = substr($messagestrlen($match[0]));
705
706         if (
preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU"$response['raw']['status-line'], $match)) {
707             
$response['status']['http-version'] = $match[1];
708             
$response['status']['code'] = (int)$match[2];
709             
$response['status']['reason-phrase'] = $match[3];
710         }
711
712         
$response['header'] = $this->_parseHeader($response['raw']['header']);
713         
$transferEncoding null;
714         if (isset(
$response['header']['Transfer-Encoding'])) {
715             
$transferEncoding $response['header']['Transfer-Encoding'];
716         }
717         
$decoded $this->_decodeBody($response['raw']['body'], $transferEncoding);
718         
$response['body'] = $decoded['body'];
719
720         if (!empty(
$decoded['header'])) {
721             
$response['header'] = $this->_parseHeader($this->_buildHeader($response['header']) .
722                                                       
$this->_buildHeader($decoded['header']));
723         }
724
725         if (!empty(
$response['header'])) {
726             
$response['cookies'] = $this->parseCookies($response['header']);
727         }
728
729         foreach (
$response['raw'] as $field => $val) {
730             if (
$val === '') {
731                 
$response['raw'][$field] = null;
732             }
733         }
734
735         return 
$response;
736     }
737
738     
/**
739      * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
740      * 'body' and 'header' or false on failure.
741      *
742      * @param string $body A string continaing the body to decode.
743      * @param mixed $encoding Can be false in case no encoding is being used, or a string representing the
 
encoding.
744      * @return 
mixed Array of response headers and body or false.
745      * @
access protected
746      */
747     protected function 
_decodeBody($body$encoding 'chunked') {
748         if (!
is_string($body)) {
749             return 
false;
750         }
751         if (empty(
$encoding)) {
752             return array(
'body' => $body'header' => false);
753         }
754         
$decodeMethod '_decode' ucfirst($encoding) . 'Body';
755
756         if (!
is_callable(array(&$this$decodeMethod))) {
757             if (!
$this->quirksMode) {
758                 
trigger_error(sprintf('httpSocketComponent::_decodeBody - Unknown encoding: %s. ' .
759                                       
'Activate quirks mode to surpress error.'h($encoding)),
760                               
E_USER_WARNING);
761             }
762             return array(
'body' => $body'header' => false);
763         }
764         return 
$this->{$decodeMethod}($body);
765     }
766
767     
/**
768      * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
769      * a result.
770      *
771      * @param string $body A string continaing the chunked body to decode.
772      * @return mixed Array of response headers and body or false.
773      * @access protected
774      */
775     
protected function _decodeChunkedBody($body) {
776         if (!
is_string($body)) {
777             return 
false;
778         }
779
780         
$decodedBody null;
781         
$chunkLength null;
782
783         while (
$chunkLength !== 0) {
784             if (!
preg_match("/^([0-9a-f]+) *(?:;(.+)=(.+))?\r\n/iU"$body$match)) {
785                 if (!
$this->quirksMode) {
786                     
trigger_error('httpSocketComponent::_decodeChunkedBody - Could not parse malformed chunk. ' .
787                                   
'Activate quirks mode to do this.'E_USER_WARNING);
788                     return 
false;
789                 }
790                 break;
791             }
792
793             
$chunkSize 0;
794             
$hexLength 0;
795             
$chunkExtensionName '';
796             
$chunkExtensionValue '';
797             if (isset(
$match[0])) {
798                 
$chunkSize $match[0];
799             }
800             if (isset(
$match[1])) {
801                 
$hexLength $match[1];
802             }
803             if (isset(
$match[2])) {
804                 
$chunkExtensionName $match[2];
805             }
806             if (isset(
$match[3])) {
807                 
$chunkExtensionValue $match[3];
808             }
809
810             
$body substr($bodystrlen($chunkSize));
811             
$chunkLength hexdec($hexLength);
812             
$chunk substr($body0$chunkLength);
813             if (!empty(
$chunkExtensionName)) {
814                 
/**
815                  * @todo See if there are popular chunk extensions we should implement
816                  */
817             
}
818             
$decodedBody .= $chunk;
819             if (
$chunkLength !== 0) {
820                 
$body substr($body$chunkLength+strlen("\r\n"));
821             }
822         }
823
824         
$entityHeader false;
825         if (!empty(
$body)) {
826             
$entityHeader $this->_parseHeader($body);
827         }
828         return array(
'body' => $decodedBody'header' => $entityHeader);
829     }
830
831     
/**
832      * Parses and sets the specified URI into current request configuration.
833      *
834      * @param mixed $uri URI, See HttpSocket::_parseUri()
835      * @return array Current configuration settings
836      * @access protected
837      */
838     
protected function _configUri($uri null) {
839         if (empty(
$uri)) {
840             return 
false;
841         }
842
843         if (
is_array($uri)) {
844             
$uri $this->_parseUri($uri);
845         } else {
846             
$uri $this->_parseUri($uritrue);
847         }
848
849         if (!isset(
$uri['host'])) {
850             return 
false;
851         }
852         
$config = array(
853             
'request'   => array(
854                 
'uri'       => array_intersect_key($uri$this->config['request']['uri']),
855                 
'auth'      => array_intersect_key($uri$this->config['request']['auth'])
856             )
857         );
858         
$this->config get::component('set')->merge($this->config$config);
859         
$this->config get::component('set')->merge(
860             
$this->config,
861             
array_intersect_key($this->config['request']['uri'], $this->config)
862         );
863
864         return 
$this->config;
865     }
866
867     
/**
868      * Takes a $uri array and turns it into a fully qualified URL string
869      *
870      * @param mixed $uri Either A $uri array, or a request string.  Will use $this->config if left empty.
871      * @param string $uriTemplate The Uri template/format to use.
872      * @return mixed A fully qualified URL formated according to $uriTemplate, or false on failure
873      * @access protected
874      */
875     
protected function _buildUri($uri = array(),
876                                  
$uriTemplate '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') {
877         if (
is_string($uri)) {
878             
$uri = array('host' => $uri);
879         }
880         
$uri $this->_parseUri($uritrue);
881
882         if (!
is_array($uri) || empty($uri)) {
883             return 
false;
884         }
885
886         
$uri['path'] = preg_replace('/^\//'null$uri['path']);
887         
$uri['query'] = $this->_httpSerialize($uri['query']);
888         
$stripIfEmpty = array(
889             
'query'     => '?%query',
890             
'fragment'  => '#%fragment',
891             
'user'      => '%user:%pass@',
892             
'host'      => '%host:%port/'
893         
);
894
895         foreach (
$stripIfEmpty as $key => $strip) {
896             if (empty(
$uri[$key])) {
897                 
$uriTemplate str_replace($stripnull$uriTemplate);
898             }
899         }
900
901         
$defaultPorts = array('http' => 80'https' => 443);
902         if (
array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) {
903             
$uriTemplate str_replace(':%port'null$uriTemplate);
904         }
905         foreach (
$uri as $property => $value) {
906             
$uriTemplate str_replace('%'.$property$value$uriTemplate);
907         }
908
909         if (
$uriTemplate === '/*') {
910             
$uriTemplate '*';
911         }
912         return 
$uriTemplate;
913     }
914
915     
/**
916      * Parses the given URI and breaks it down into pieces as an indexed array with elements
917      * such as 'scheme', 'port', 'query'.
918      *
919      * @param string $uri URI to parse
920      * @param mixed $base If true use default URI config, otherwise indexed array to set 'scheme', 'host', 'port',
 
etc.
921      * @return array 
Parsed URI
922      
* @access protected
923      */
924     protected function 
_parseUri($uri null$base = array()) {
925         
$uriBase = array(
926             
'scheme'    => array('http''https'),
927             
'host'      => null,
928             
'port'      => array(80443),
929             
'user'      => null,
930             
'pass'      => null,
931             
'path'      => '/',
932             
'query'     => null,
933             
'fragment'  => null
934         
);
935
936         if (
is_string($uri)) {
937             
$uri parse_url($uri);
938         }
939         if (!
is_array($uri) || empty($uri)) {
940             return 
false;
941         }
942         if (
$base === true) {
943             
$base $uriBase;
944         }
945
946         if (isset(
$base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) {
947             if (isset(
$uri['scheme']) && !isset($uri['port'])) {
948                 
$base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])];
949             } elseif (isset(
$uri['port']) && !isset($uri['scheme'])) {
950                 
$base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])];
951             }
952         }
953
954         if (
is_array($base) && !empty($base)) {
955             
$uri array_merge($base$uri);
956         }
957
958         if (isset(
$uri['scheme']) && is_array($uri['scheme'])) {
959             
$uri['scheme'] = array_shift($uri['scheme']);
960         }
961         if (isset(
$uri['port']) && is_array($uri['port'])) {
962             
$uri['port'] = array_shift($uri['port']);
963         }
964
965         if (
array_key_exists('query'$uri)) {
966             
$uri['query'] = $this->_parseQuery($uri['query']);
967         }
968
969         if (!
array_intersect_key($uriBase$uri)) {
970             return 
false;
971         }
972         return 
$uri;
973     }
974
975     
/**
976      * This function can be thought of as a reverse to PHP5's http_build_query(). It takes a given
977      * query string and turns it into an array and supports nesting by using the php bracket syntax.
978      * So this menas you can parse queries like:
979      *
980      * - ?key[subKey]=value
981      * - ?key[]=value1&key[]=value2
982      *
983      * A leading '?' mark in $query is optional and does not effect the outcome of this function. 
984      * For the complete capabilities of this implementation take a look at HttpSocketTest::testparseQuery()
985      *
986      * @param mixed $query A query string to parse into an array or an array to return directly "as is"
987      * @return array The $query parsed into a possibly multi-level array. If an empty $query is
988      *     given, an empty array is returned.
989      * @access protected
990      */
991     
protected function _parseQuery($query) {
992         if (
is_array($query)) {
993             return 
$query;
994         }
995         
$parsedQuery = array();
996
997         if (
is_string($query) && !empty($query)) {
998             
$query preg_replace('/^\?/'''$query);
999             
$items explode('&'$query);
1000
1001             foreach (
$items as $item) {
1002                 if (
strpos($item'=') !== false) {
1003                     list(
$key$value) = explode('='$item2);
1004                 } else {
1005                     
$key $item;
1006                     
$value null;
1007                 }
1008
1009                 
$key urldecode($key);
1010                 
$value urldecode($value);
1011
1012                 if (
preg_match_all('/\[([^\[\]]*)\]/iUs'$key$matches)) {
1013                     
$subKeys $matches[1];
1014                     
$rootKey substr($key0strpos($key'['));
1015                     if (!empty(
$rootKey)) {
1016                         
array_unshift($subKeys$rootKey);
1017                     }
1018                     
$queryNode =& $parsedQuery;
1019
1020                     foreach (
$subKeys as $subKey) {
1021                         if (!
is_array($queryNode)) {
1022                             
$queryNode = array();
1023                         }
1024
1025                         if (
$subKey === '') {
1026                             
$queryNode[] = array();
1027                             
end($queryNode);
1028                             
$subKey key($queryNode);
1029                         }
1030                         
$queryNode =& $queryNode[$subKey];
1031                     }
1032                     
$queryNode $value;
1033                 } else {
1034                     
$parsedQuery[$key] = $value;
1035                 }
1036             }
1037         }
1038         return 
$parsedQuery;
1039     }
1040
1041     
/**
1042      * Builds a request line according to HTTP/1.1 specs. Activate quirks mode to work outside specs.
1043      *
1044      * @param array $request Needs to contain a 'uri' key. Should also contain a 'method' key,
1045      *        otherwise defaults to GET.
1046      * @param string $versionToken The version token to use, defaults to HTTP/1.1
1047      * @return string Request line
1048      * @access protected
1049      */
1050     
protected function _buildRequestLine($request = array(), $versionToken 'HTTP/1.1') {
1051         
$asteriskMethods = array('OPTIONS');
1052
1053         if (
is_string($request)) {
1054             
$isValid preg_match("/(.+) (.+) (.+)\r\n/U"$request$match);
1055             if (!
$this->quirksMode && (!$isValid || ($match[2] == '*' && !in_array($match[3], $asteriskMethods))))
 
{
1056                 
trigger_error('httpSocketComponent::_buildRequestLine - Passed an invalid request line string. ' .
1057                               
'Activate quirks mode to do this.'E_USER_WARNING);
1058                 return 
false;
1059             }
1060             return 
$request;
1061         } elseif (!
is_array($request)) {
1062             return 
false;
1063         } elseif (!
array_key_exists('uri'$request)) {
1064             return 
false;
1065         }
1066
1067         
$request['uri'] = $this->_parseUri($request['uri']);
1068         
$request array_merge(array('method' => 'GET'), $request);
1069         
$request['uri'] = $this->_buildUri($request['uri'], '/%path?%query');
1070
1071         if (!
$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) {
1072             
trigger_error(sprintf('httpSocketComponent::_buildRequestLine - The "*" asterisk character is only ' .
1073                                   
'allowed for the following methods: %s. Activate quirks mode to work outside ' .
1074                                   
'of HTTP/1.1 specs.',
1075                                   
join(','$asteriskMethods)),
1076                           
E_USER_WARNING);
1077             return 
false;
1078         }
1079         return 
$request['method'].' '.$request['uri'].' '.$versionToken.$this->lineBreak;
1080     }
1081
1082     
/**
1083      * Serializes an array for transport.
1084      *
1085      * @param array $data Data to serialize
1086      * @return string Serialized variable
1087      * @access protected
1088      */
1089     
protected function _httpSerialize($data = array()) {
1090         if (
is_string($data)) {
1091             return 
$data;
1092         }
1093         if (empty(
$data) || !is_array($data)) {
1094             return 
false;
1095         }
1096         return 
substr(self::_routerQueryString($data), 1);
1097     }
1098     
1099     
/**
1100      * Generates a well-formed querystring from $q
1101      *
1102      * @param mixed $q Query string
1103      * @param array $extra Extra querystring parameters.
1104      * @param bool $escape Whether or not to use escaped &
1105      * @return array
1106      * @access public
1107      * @static
1108      */
1109     
protected function _routerQueryString($q$extra = array(), $escape false) {
1110         if (empty(
$q) && empty($extra)) {
1111             return 
null;
1112         }
1113         
$join '&';
1114         if (
$escape === true) {
1115             
$join '&amp;';
1116         }
1117         
$out '';
1118
1119         if (
is_array($q)) {
1120             
$q array_merge($extra$q);
1121         } else {
1122             
$out $q;
1123             
$q $extra;
1124         }
1125         
$out .= http_build_query($qnull$join);
1126         if (isset(
$out[0]) && $out[0] != '?') {
1127             
$out '?' $out;
1128         }
1129         return 
$out;
1130     }
1131
1132     
/**
1133      * Builds the header.
1134      *
1135      * @param array $header Header to build
1136      * @return string Header built from array
1137      * @access protected
1138      */
1139     
protected function _buildHeader($header$mode 'standard') {
1140         if (
is_string($header)) {
1141             return 
$header;
1142         } elseif (!
is_array($header)) {
1143             return 
false;
1144         }
1145
1146         
$returnHeader '';
1147         foreach (
$header as $field => $contents) {
1148             if (
is_array($contents) && $mode == 'standard') {
1149                 
$contents implode(','$contents);
1150             }
1151             foreach ((array)
$contents as $content) {
1152                 
$contents preg_replace("/\r\n(?![\t ])/""\r\n "$content);
1153                 
$field $this->_escapeToken($field);
1154
1155                 
$returnHeader .= $field.': '.$contents.$this->lineBreak;
1156             }
1157         }
1158         return 
$returnHeader;
1159     }
1160
1161     
/**
1162      * Parses an array based header.
1163      *
1164      * @param array $header Header as an indexed array (field => value)
1165      * @return array Parsed header
1166      * @access protected
1167      */
1168     
protected function _parseHeader($header) {
1169         if (
is_array($header)) {
1170             foreach (
$header as $field => $value) {
1171                 unset(
$header[$field]);
1172                 
$field strtolower($field);
1173                 
preg_match_all('/(?:^|(?<=-))[a-z]/U'$field$offsetsPREG_OFFSET_CAPTURE);
1174
1175                 foreach (
$offsets[0] as $offset) {
1176                     
$field substr_replace($fieldstrtoupper($offset[0]), $offset[1], 1);
1177                 }
1178                 
$header[$field] = $value;
1179             }
1180             return 
$header;
1181         } elseif (!
is_string($header)) {
1182             return 
false;
1183         }
1184
1185         
preg_match_all("/(.+):(.+)(?:(?<![\t ])" $this->lineBreak "|\$)/Uis"$header$matches,
 
PREG_SET_ORDER);
1186
1187         
$header = array();
1188         foreach (
$matches as $match) {
1189             list(, 
$field$value) = $match;
1190
1191             
$value trim($value);
1192             
$value preg_replace("/[\t ]\r\n/""\r\n"$value);
1193
1194             
$field $this->_unescapeToken($field);
1195
1196             
$field strtolower($field);
1197             
preg_match_all('/(?:^|(?<=-))[a-z]/U'$field$offsetsPREG_OFFSET_CAPTURE);
1198             foreach (
$offsets[0] as $offset) {
1199                 
$field substr_replace($fieldstrtoupper($offset[0]), $offset[1], 1);
1200             }
1201
1202             if (!isset(
$header[$field])) {
1203                 
$header[$field] = $value;
1204             } else {
1205                 
$header[$field] = array_merge((array)$header[$field], (array)$value);
1206             }
1207         }
1208         return 
$header;
1209     }
1210
1211     
/**
1212      * Parses cookies in response headers.
1213      *
1214      * @param array $header Header array containing one ore more 'Set-Cookie' headers.
1215      * @return mixed Either false on no cookies, or an array of cookies recieved.
1216      * @access public
1217      * @todo Make this 100% RFC 2965 confirm
1218      */
1219     
protected function parseCookies($header) {
1220         if (!isset(
$header['Set-Cookie'])) {
1221             return 
false;
1222         }
1223
1224         
$cookies = array();
1225         foreach ((array)
$header['Set-Cookie'] as $cookie) {
1226             if (
strpos($cookie'";"') !== false) {
1227                 
$cookie str_replace('";"'"{__cookie_replace__}"$cookie);
1228                 
$parts  str_replace("{__cookie_replace__}"'";"'explode(';'$cookie));
1229             } else {
1230                 
$parts preg_split('/\;[ \t]*/'$cookie);
1231             }
1232
1233             list(
$name$value) = explode('='array_shift($parts), 2);
1234             
$cookies[$name] = compact('value');
1235
1236             foreach (
$parts as $part) {
1237                 if (
strpos($part'=') !== false) {
1238                     list(
$key$value) = explode('='$part);
1239                 } else {
1240                     
$key $part;
1241                     
$value true;
1242                 }
1243
1244                 
$key strtolower($key);
1245                 if (!isset(
$cookies[$name][$key])) {
1246                     
$cookies[$name][$key] = $value;
1247                 }
1248             }
1249         }
1250         return 
$cookies;
1251     }
1252
1253     
/**
1254      * Builds cookie headers for a request.
1255      *
1256      * @param array $cookies Array of cookies to send with the request.
1257      * @return string Cookie header string to be sent with the request.
1258      * @access public
1259      * @todo Refactor token escape mechanism to be configurable
1260      */
1261     
protected function buildCookies($cookies) {
1262         
$header = array();
1263         foreach (
$cookies as $name => $cookie) {
1264             
$header[] = $name.'='.$this->_escapeToken($cookie['value'], array(';'));
1265         }
1266         
$header $this->_buildHeader(array('Cookie' => implode('; '$header)), 'pragmatic');
1267         return 
$header;
1268     }
1269
1270     
/**
1271      * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
1272      *
1273      * @param string $token Token to unescape
1274      * @return string Unescaped token
1275      * @access protected
1276      * @todo Test $chars parameter
1277      */
1278     
protected function _unescapeToken($token$chars null) {
1279         
$regex '/"(['.join(''$this->_tokenEscapeChars(true$chars)).'])"/';
1280         
$token preg_replace($regex'\\1'$token);
1281         return 
$token;
1282     }
1283
1284     
/**
1285      * Escapes a given $token according to RFC 2616 (HTTP 1.1 specs)
1286      *
1287      * @param string $token Token to escape
1288      * @return string Escaped token
1289      * @access protected
1290      * @todo Test $chars parameter
1291      */
1292     
protected function _escapeToken($token$chars null) {
1293         
$regex '/(['.join(''$this->_tokenEscapeChars(true$chars)).'])/';
1294         
$token preg_replace($regex'"\\1"'$token);
1295         return 
$token;
1296     }
1297
1298     
/**
1299      * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
1300      *
1301      * @param boolean $hex true to get them as HEX values, false otherwise
1302      * @return array Escape chars
1303      * @access protected
1304      * @todo Test $chars parameter
1305      */
1306     
protected function _tokenEscapeChars($hex true$chars null) {
1307         if (!empty(
$chars)) {
1308             
$escape $chars;
1309         } else {
1310             
$escape = array('"'"("")""<"">""@"","";"":""\\""/""[""]""?""=""{""}",
 
" ");
1311             for (
$i 0$i <= 31$i++) {
1312                 
$escape[] = chr($i);
1313             }
1314             
$escape[] = chr(127);
1315         }
1316
1317         if (
$hex == false) {
1318             return 
$escape;
1319         }
1320         
$regexChars '';
1321         foreach (
$escape as $key => $char) {
1322             
$escape[$key] = '\\x'.str_pad(dechex(ord($char)), 2'0'STR_PAD_LEFT);
1323         }
1324         return 
$escape;
1325     }
1326
1327     
/**
1328      * Resets the state of this HttpSocket instance to it's initial state (before Object::__construct got
1329      * executed) or does the same thing partially for the request and the response property only.
1330      *
1331      * @param boolean $full If set to false only HttpSocket::response and HttpSocket::request are reseted
1332      * @return boolean True on success
1333      * @access public
1334      */
1335     
public function reset($full true) {
1336         static 
$initalState = array();
1337         if (empty(
$initalState)) {
1338             
$initalState get_class_vars(__CLASS__);
1339         }
1340         if (
$full == false) {
1341             
$this->request $initalState['request'];
1342             
$this->response $initalState['response'];
1343             return 
true;
1344         }
1345
1346         foreach (
$initialState as $property => $value) {
1347             
$this->{$property} = $value;
1348         }
1349         return 
true;
1350     }
1351 }
1352