Subversion-Projekte lars-tiefland.cakephp

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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