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-2011, 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-2011, 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 | } |
---|