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