/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, $data, strlen($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(80, 443)));
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($message, strlen($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($body, strlen($chunkSize));
811 $chunkLength = hexdec($hexLength);
812 $chunk = substr($body, 0, $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($uri, true);
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($uri, true);
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($strip, null, $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(80, 443),
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('=', $item, 2);
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($key, 0, strpos($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 = '&';
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($q, null, $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, $offsets, PREG_OFFSET_CAPTURE);
1174
1175 foreach ($offsets[0] as $offset) {
1176 $field = substr_replace($field, strtoupper($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, $offsets, PREG_OFFSET_CAPTURE);
1198 foreach ($offsets[0] as $offset) {
1199 $field = substr_replace($field, strtoupper($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