cake/libs/http_socket.php

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