HttpSocket::reset()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 13
rs 9.4285
1
<?php
2
/**
3
 * HTTP Socket connection class.
4
 *
5
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
6
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
13
 * @link          http://cakephp.org CakePHP(tm) Project
14
 * @package       Cake.Network.Http
15
 * @since         CakePHP(tm) v 1.2.0
16
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
17
 */
18
19
App::uses('CakeSocket', 'Network');
20
App::uses('Router', 'Routing');
21
App::uses('Hash', 'Utility');
22
23
/**
24
 * CakePHP network socket connection class.
25
 *
26
 * Core base class for HTTP network communication. HttpSocket can be used as an
27
 * Object Oriented replacement for cURL in many places.
28
 *
29
 * @package       Cake.Network.Http
30
 */
31
class HttpSocket extends CakeSocket {
32
33
/**
34
 * When one activates the $quirksMode by setting it to true, all checks meant to
35
 * enforce RFC 2616 (HTTP/1.1 specs).
36
 * will be disabled and additional measures to deal with non-standard responses will be enabled.
37
 *
38
 * @var boolean
39
 */
40
	public $quirksMode = false;
41
42
/**
43
 * Contain information about the last request (read only)
44
 *
45
 * @var array
46
 */
47
	public $request = array(
48
		'method' => 'GET',
49
		'uri' => array(
50
			'scheme' => 'http',
51
			'host' => null,
52
			'port' => 80,
53
			'user' => null,
54
			'pass' => null,
55
			'path' => null,
56
			'query' => null,
57
			'fragment' => null
58
		),
59
		'version' => '1.1',
60
		'body' => '',
61
		'line' => null,
62
		'header' => array(
63
			'Connection' => 'close',
64
			'User-Agent' => 'CakePHP'
65
		),
66
		'raw' => null,
67
		'redirect' => false,
68
		'cookies' => array(),
69
	);
70
71
/**
72
 * Contain information about the last response (read only)
73
 *
74
 * @var array
75
 */
76
	public $response = null;
77
78
/**
79
 * Response class name
80
 *
81
 * @var string
82
 */
83
	public $responseClass = 'HttpSocketResponse';
84
85
/**
86
 * Configuration settings for the HttpSocket and the requests
87
 *
88
 * @var array
89
 */
90
	public $config = array(
91
		'persistent' => false,
92
		'host' => 'localhost',
93
		'protocol' => 'tcp',
94
		'port' => 80,
95
		'timeout' => 30,
96
		'ssl_verify_peer' => true,
97
		'ssl_allow_self_signed' => false,
98
		'ssl_verify_depth' => 5,
99
		'ssl_verify_host' => true,
100
		'request' => array(
101
			'uri' => array(
102
				'scheme' => array('http', 'https'),
103
				'host' => 'localhost',
104
				'port' => array(80, 443)
105
			),
106
			'redirect' => false,
107
			'cookies' => array(),
108
		)
109
	);
110
111
/**
112
 * Authentication settings
113
 *
114
 * @var array
115
 */
116
	protected $_auth = array();
117
118
/**
119
 * Proxy settings
120
 *
121
 * @var array
122
 */
123
	protected $_proxy = array();
124
125
/**
126
 * Resource to receive the content of request
127
 *
128
 * @var mixed
129
 */
130
	protected $_contentResource = null;
131
132
/**
133
 * Build an HTTP Socket using the specified configuration.
134
 *
135
 * You can use a URL string to set the URL and use default configurations for
136
 * all other options:
137
 *
138
 * `$http = new HttpSocket('http://cakephp.org/');`
139
 *
140
 * Or use an array to configure multiple options:
141
 *
142
 * {{{
143
 * $http = new HttpSocket(array(
144
 *    'host' => 'cakephp.org',
145
 *    'timeout' => 20
146
 * ));
147
 * }}}
148
 *
149
 * See HttpSocket::$config for options that can be used.
150
 *
151
 * @param string|array $config Configuration information, either a string URL or an array of options.
152
 */
153
	public function __construct($config = array()) {
154
		if (is_string($config)) {
155
			$this->_configUri($config);
156
		} elseif (is_array($config)) {
157
			if (isset($config['request']['uri']) && is_string($config['request']['uri'])) {
158
				$this->_configUri($config['request']['uri']);
159
				unset($config['request']['uri']);
160
			}
161
			$this->config = Hash::merge($this->config, $config);
162
		}
163
		parent::__construct($this->config);
164
	}
165
166
/**
167
 * Set authentication settings.
168
 *
169
 * Accepts two forms of parameters. If all you need is a username + password, as with
170
 * Basic authentication you can do the following:
171
 *
172
 * {{{
173
 * $http->configAuth('Basic', 'mark', 'secret');
174
 * }}}
175
 *
176
 * If you are using an authentication strategy that requires more inputs, like Digest authentication
177
 * you can call `configAuth()` with an array of user information.
178
 *
179
 * {{{
180
 * $http->configAuth('Digest', array(
181
 *		'user' => 'mark',
182
 *		'pass' => 'secret',
183
 *		'realm' => 'my-realm',
184
 *		'nonce' => 1235
185
 * ));
186
 * }}}
187
 *
188
 * To remove any set authentication strategy, call `configAuth()` with no parameters:
189
 *
190
 * `$http->configAuth();`
191
 *
192
 * @param string $method Authentication method (ie. Basic, Digest). If empty, disable authentication
193
 * @param string|array $user Username for authentication. Can be an array with settings to authentication class
194
 * @param string $pass Password for authentication
195
 * @return void
196
 */
197
	public function configAuth($method, $user = null, $pass = null) {
198
		if (empty($method)) {
199
			$this->_auth = array();
200
			return;
201
		}
202
		if (is_array($user)) {
203
			$this->_auth = array($method => $user);
204
			return;
205
		}
206
		$this->_auth = array($method => compact('user', 'pass'));
207
	}
208
209
/**
210
 * Set proxy settings
211
 *
212
 * @param string|array $host Proxy host. Can be an array with settings to authentication class
213
 * @param integer $port Port. Default 3128.
214
 * @param string $method Proxy method (ie, Basic, Digest). If empty, disable proxy authentication
215
 * @param string $user Username if your proxy need authentication
216
 * @param string $pass Password to proxy authentication
217
 * @return void
218
 */
219
	public function configProxy($host, $port = 3128, $method = null, $user = null, $pass = null) {
220
		if (empty($host)) {
221
			$this->_proxy = array();
222
			return;
223
		}
224
		if (is_array($host)) {
225
			$this->_proxy = $host + array('host' => null);
226
			return;
227
		}
228
		$this->_proxy = compact('host', 'port', 'method', 'user', 'pass');
229
	}
230
231
/**
232
 * Set the resource to receive the request content. This resource must support fwrite.
233
 *
234
 * @param resource|boolean $resource Resource or false to disable the resource use
235
 * @return void
236
 * @throws SocketException
237
 */
238
	public function setContentResource($resource) {
239
		if ($resource === false) {
240
			$this->_contentResource = null;
241
			return;
242
		}
243
		if (!is_resource($resource)) {
244
			throw new SocketException(__d('cake_dev', 'Invalid resource.'));
245
		}
246
		$this->_contentResource = $resource;
247
	}
248
249
/**
250
 * Issue the specified request. HttpSocket::get() and HttpSocket::post() wrap this
251
 * method and provide a more granular interface.
252
 *
253
 * @param string|array $request Either an URI string, or an array defining host/uri
254
 * @return mixed false on error, HttpSocketResponse on success
255
 * @throws SocketException
256
 */
257
	public function request($request = array()) {
258
		$this->reset(false);
259
260
		if (is_string($request)) {
261
			$request = array('uri' => $request);
262
		} elseif (!is_array($request)) {
263
			return false;
264
		}
265
266
		if (!isset($request['uri'])) {
267
			$request['uri'] = null;
268
		}
269
		$uri = $this->_parseUri($request['uri']);
270
		if (!isset($uri['host'])) {
271
			$host = $this->config['host'];
272
		}
273
		if (isset($request['host'])) {
274
			$host = $request['host'];
275
			unset($request['host']);
276
		}
277
		$request['uri'] = $this->url($request['uri']);
278
		$request['uri'] = $this->_parseUri($request['uri'], true);
0 ignored issues
show
Security Bug introduced by
It seems like $request['uri'] can also be of type false; however, HttpSocket::_parseUri() does only seem to accept string|array|null, did you maybe forget to handle an error condition?
Loading history...
279
		$this->request = Hash::merge($this->request, array_diff_key($this->config['request'], array('cookies' => true)), $request);
280
281
		$this->_configUri($this->request['uri']);
282
283
		$Host = $this->request['uri']['host'];
284
		if (!empty($this->config['request']['cookies'][$Host])) {
285
			if (!isset($this->request['cookies'])) {
286
				$this->request['cookies'] = array();
287
			}
288
			if (!isset($request['cookies'])) {
289
				$request['cookies'] = array();
290
			}
291
			$this->request['cookies'] = array_merge($this->request['cookies'], $this->config['request']['cookies'][$Host], $request['cookies']);
292
		}
293
294
		if (isset($host)) {
295
			$this->config['host'] = $host;
296
		}
297
		$this->_setProxy();
298
		$this->request['proxy'] = $this->_proxy;
299
300
		$cookies = null;
301
302
		if (is_array($this->request['header'])) {
303
			if (!empty($this->request['cookies'])) {
304
				$cookies = $this->buildCookies($this->request['cookies']);
305
			}
306
			$scheme = '';
307
			$port = 0;
308 View Code Duplication
			if (isset($this->request['uri']['scheme'])) {
309
				$scheme = $this->request['uri']['scheme'];
310
			}
311 View Code Duplication
			if (isset($this->request['uri']['port'])) {
312
				$port = $this->request['uri']['port'];
313
			}
314
			if (
315
				($scheme === 'http' && $port != 80) ||
316
				($scheme === 'https' && $port != 443) ||
317
				($port != 80 && $port != 443)
318
			) {
319
				$Host .= ':' . $port;
320
			}
321
			$this->request['header'] = array_merge(compact('Host'), $this->request['header']);
322
		}
323
324
		if (isset($this->request['uri']['user'], $this->request['uri']['pass'])) {
325
			$this->configAuth('Basic', $this->request['uri']['user'], $this->request['uri']['pass']);
326
		} elseif (isset($this->request['auth'], $this->request['auth']['method'], $this->request['auth']['user'], $this->request['auth']['pass'])) {
327
			$this->configAuth($this->request['auth']['method'], $this->request['auth']['user'], $this->request['auth']['pass']);
328
		}
329
		$this->_setAuth();
330
		$this->request['auth'] = $this->_auth;
331
332
		if (is_array($this->request['body'])) {
333
			$this->request['body'] = http_build_query($this->request['body'], '', '&');
334
		}
335
336 View Code Duplication
		if (!empty($this->request['body']) && !isset($this->request['header']['Content-Type'])) {
337
			$this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded';
338
		}
339
340 View Code Duplication
		if (!empty($this->request['body']) && !isset($this->request['header']['Content-Length'])) {
341
			$this->request['header']['Content-Length'] = strlen($this->request['body']);
342
		}
343
344
		$connectionType = null;
345
		if (isset($this->request['header']['Connection'])) {
346
			$connectionType = $this->request['header']['Connection'];
347
		}
348
		$this->request['header'] = $this->_buildHeader($this->request['header']) . $cookies;
349
350
		if (empty($this->request['line'])) {
351
			$this->request['line'] = $this->_buildRequestLine($this->request);
352
		}
353
354
		if ($this->quirksMode === false && $this->request['line'] === false) {
355
			return false;
356
		}
357
358
		$this->_configContext($this->request['uri']['host']);
359
360
		$this->request['raw'] = '';
361
		if ($this->request['line'] !== false) {
362
			$this->request['raw'] = $this->request['line'];
363
		}
364
365
		if ($this->request['header'] !== false) {
366
			$this->request['raw'] .= $this->request['header'];
367
		}
368
369
		$this->request['raw'] .= "\r\n";
370
		$this->request['raw'] .= $this->request['body'];
371
		$this->write($this->request['raw']);
372
373
		$response = null;
374
		$inHeader = true;
375
		while ($data = $this->read()) {
376
			if ($this->_contentResource) {
377
				if ($inHeader) {
378
					$response .= $data;
379
					$pos = strpos($response, "\r\n\r\n");
380
					if ($pos !== false) {
381
						$pos += 4;
382
						$data = substr($response, $pos);
383
						fwrite($this->_contentResource, $data);
384
385
						$response = substr($response, 0, $pos);
386
						$inHeader = false;
387
					}
388
				} else {
389
					fwrite($this->_contentResource, $data);
390
					fflush($this->_contentResource);
391
				}
392
			} else {
393
				$response .= $data;
394
			}
395
		}
396
397
		if ($connectionType === 'close') {
398
			$this->disconnect();
399
		}
400
401
		list($plugin, $responseClass) = pluginSplit($this->responseClass, true);
402
		App::uses($responseClass, $plugin . 'Network/Http');
403
		if (!class_exists($responseClass)) {
404
			throw new SocketException(__d('cake_dev', 'Class %s not found.', $this->responseClass));
405
		}
406
		$this->response = new $responseClass($response);
0 ignored issues
show
Documentation Bug introduced by
It seems like new $responseClass($response) of type object is incompatible with the declared type array of property $response.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
407
408
		if (!empty($this->response->cookies)) {
409
			if (!isset($this->config['request']['cookies'][$Host])) {
410
				$this->config['request']['cookies'][$Host] = array();
411
			}
412
			$this->config['request']['cookies'][$Host] = array_merge($this->config['request']['cookies'][$Host], $this->response->cookies);
413
		}
414
415
		if ($this->request['redirect'] && $this->response->isRedirect()) {
416
			$request['uri'] = trim(urldecode($this->response->getHeader('Location')), '=');
417
			$request['redirect'] = is_int($this->request['redirect']) ? $this->request['redirect'] - 1 : $this->request['redirect'];
418
			$this->response = $this->request($request);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->request($request) of type * is incompatible with the declared type array of property $response.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
419
		}
420
421
		return $this->response;
422
	}
423
424
/**
425
 * Issues a GET request to the specified URI, query, and request.
426
 *
427
 * Using a string uri and an array of query string parameters:
428
 *
429
 * `$response = $http->get('http://google.com/search', array('q' => 'cakephp', 'client' => 'safari'));`
430
 *
431
 * Would do a GET request to `http://google.com/search?q=cakephp&client=safari`
432
 *
433
 * You could express the same thing using a uri array and query string parameters:
434
 *
435
 * {{{
436
 * $response = $http->get(
437
 *     array('host' => 'google.com', 'path' => '/search'),
438
 *     array('q' => 'cakephp', 'client' => 'safari')
439
 * );
440
 * }}}
441
 *
442
 * @param string|array $uri URI to request. Either a string uri, or a uri array, see HttpSocket::_parseUri()
443
 * @param array $query Querystring parameters to append to URI
444
 * @param array $request An indexed array with indexes such as 'method' or uri
445
 * @return mixed Result of request, either false on failure or the response to the request.
446
 */
447
	public function get($uri = null, $query = array(), $request = array()) {
448
		if (!empty($query)) {
449
			$uri = $this->_parseUri($uri, $this->config['request']['uri']);
450
			if (isset($uri['query'])) {
451
				$uri['query'] = array_merge($uri['query'], $query);
452
			} else {
453
				$uri['query'] = $query;
454
			}
455
			$uri = $this->_buildUri($uri);
0 ignored issues
show
Security Bug introduced by
It seems like $uri defined by $this->_buildUri($uri) on line 455 can also be of type false; however, HttpSocket::_buildUri() does only seem to accept string|array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
456
		}
457
458
		$request = Hash::merge(array('method' => 'GET', 'uri' => $uri), $request);
459
		return $this->request($request);
460
	}
461
462
/**
463
 * Issues a POST request to the specified URI, query, and request.
464
 *
465
 * `post()` can be used to post simple data arrays to a URL:
466
 *
467
 * {{{
468
 * $response = $http->post('http://example.com', array(
469
 *     'username' => 'batman',
470
 *     'password' => 'bruce_w4yne'
471
 * ));
472
 * }}}
473
 *
474
 * @param string|array $uri URI to request. See HttpSocket::_parseUri()
475
 * @param array $data Array of request body data keys and values.
476
 * @param array $request An indexed array with indexes such as 'method' or uri
477
 * @return mixed Result of request, either false on failure or the response to the request.
478
 */
479
	public function post($uri = null, $data = array(), $request = array()) {
480
		$request = Hash::merge(array('method' => 'POST', 'uri' => $uri, 'body' => $data), $request);
481
		return $this->request($request);
482
	}
483
484
/**
485
 * Issues a PUT request to the specified URI, query, and request.
486
 *
487
 * @param string|array $uri URI to request, See HttpSocket::_parseUri()
488
 * @param array $data Array of request body data keys and values.
489
 * @param array $request An indexed array with indexes such as 'method' or uri
490
 * @return mixed Result of request
491
 */
492
	public function put($uri = null, $data = array(), $request = array()) {
493
		$request = Hash::merge(array('method' => 'PUT', 'uri' => $uri, 'body' => $data), $request);
494
		return $this->request($request);
495
	}
496
497
/**
498
 * Issues a PATCH request to the specified URI, query, and request.
499
 *
500
 * @param string|array $uri URI to request, See HttpSocket::_parseUri()
501
 * @param array $data Array of request body data keys and values.
502
 * @param array $request An indexed array with indexes such as 'method' or uri
503
 * @return mixed Result of request
504
 */
505
	public function patch($uri = null, $data = array(), $request = array()) {
506
		$request = Hash::merge(array('method' => 'PATCH', 'uri' => $uri, 'body' => $data), $request);
507
		return $this->request($request);
508
	}
509
510
/**
511
 * Issues a DELETE request to the specified URI, query, and request.
512
 *
513
 * @param string|array $uri URI to request (see {@link _parseUri()})
514
 * @param array $data Array of request body data keys and values.
515
 * @param array $request An indexed array with indexes such as 'method' or uri
516
 * @return mixed Result of request
517
 */
518
	public function delete($uri = null, $data = array(), $request = array()) {
519
		$request = Hash::merge(array('method' => 'DELETE', 'uri' => $uri, 'body' => $data), $request);
520
		return $this->request($request);
521
	}
522
523
/**
524
 * Normalizes URLs into a $uriTemplate. If no template is provided
525
 * a default one will be used. Will generate the URL using the
526
 * current config information.
527
 *
528
 * ### Usage:
529
 *
530
 * After configuring part of the request parameters, you can use url() to generate
531
 * URLs.
532
 *
533
 * {{{
534
 * $http = new HttpSocket('http://www.cakephp.org');
535
 * $url = $http->url('/search?q=bar');
536
 * }}}
537
 *
538
 * Would return `http://www.cakephp.org/search?q=bar`
539
 *
540
 * url() can also be used with custom templates:
541
 *
542
 * `$url = $http->url('http://www.cakephp/search?q=socket', '/%path?%query');`
543
 *
544
 * Would return `/search?q=socket`.
545
 *
546
 * @param string|array Either a string or array of URL options to create a URL with.
547
 * @param string $uriTemplate A template string to use for URL formatting.
548
 * @return mixed Either false on failure or a string containing the composed URL.
549
 */
550
	public function url($url = null, $uriTemplate = null) {
551
		if ($url === null) {
552
			$url = '/';
553
		}
554
		if (is_string($url)) {
555
			$scheme = $this->config['request']['uri']['scheme'];
556
			if (is_array($scheme)) {
557
				$scheme = $scheme[0];
558
			}
559
			$port = $this->config['request']['uri']['port'];
560
			if (is_array($port)) {
561
				$port = $port[0];
562
			}
563
			if ($url{0} === '/') {
564
				$url = $this->config['request']['uri']['host'] . ':' . $port . $url;
565
			}
566
			if (!preg_match('/^.+:\/\/|\*|^\//', $url)) {
567
				$url = $scheme . '://' . $url;
568
			}
569
		} elseif (!is_array($url) && !empty($url)) {
570
			return false;
571
		}
572
573
		$base = array_merge($this->config['request']['uri'], array('scheme' => array('http', 'https'), 'port' => array(80, 443)));
574
		$url = $this->_parseUri($url, $base);
575
576
		if (empty($url)) {
577
			$url = $this->config['request']['uri'];
578
		}
579
580
		if (!empty($uriTemplate)) {
581
			return $this->_buildUri($url, $uriTemplate);
582
		}
583
		return $this->_buildUri($url);
584
	}
585
586
/**
587
 * Set authentication in request
588
 *
589
 * @return void
590
 * @throws SocketException
591
 */
592
	protected function _setAuth() {
593
		if (empty($this->_auth)) {
594
			return;
595
		}
596
		$method = key($this->_auth);
597
		list($plugin, $authClass) = pluginSplit($method, true);
598
		$authClass = Inflector::camelize($authClass) . 'Authentication';
599
		App::uses($authClass, $plugin . 'Network/Http');
600
601
		if (!class_exists($authClass)) {
602
			throw new SocketException(__d('cake_dev', 'Unknown authentication method.'));
603
		}
604
		if (!method_exists($authClass, 'authentication')) {
605
			throw new SocketException(__d('cake_dev', 'The %s does not support authentication.', $authClass));
606
		}
607
		call_user_func_array("$authClass::authentication", array($this, &$this->_auth[$method]));
608
	}
609
610
/**
611
 * Set the proxy configuration and authentication
612
 *
613
 * @return void
614
 * @throws SocketException
615
 */
616
	protected function _setProxy() {
617 View Code Duplication
		if (empty($this->_proxy) || !isset($this->_proxy['host'], $this->_proxy['port'])) {
618
			return;
619
		}
620
		$this->config['host'] = $this->_proxy['host'];
621
		$this->config['port'] = $this->_proxy['port'];
622
623 View Code Duplication
		if (empty($this->_proxy['method']) || !isset($this->_proxy['user'], $this->_proxy['pass'])) {
624
			return;
625
		}
626
		list($plugin, $authClass) = pluginSplit($this->_proxy['method'], true);
627
		$authClass = Inflector::camelize($authClass) . 'Authentication';
628
		App::uses($authClass, $plugin . 'Network/Http');
629
630
		if (!class_exists($authClass)) {
631
			throw new SocketException(__d('cake_dev', 'Unknown authentication method for proxy.'));
632
		}
633
		if (!method_exists($authClass, 'proxyAuthentication')) {
634
			throw new SocketException(__d('cake_dev', 'The %s does not support proxy authentication.', $authClass));
635
		}
636
		call_user_func_array("$authClass::proxyAuthentication", array($this, &$this->_proxy));
637
	}
638
639
/**
640
 * Parses and sets the specified URI into current request configuration.
641
 *
642
 * @param string|array $uri URI, See HttpSocket::_parseUri()
643
 * @return boolean If uri has merged in config
644
 */
645
	protected function _configUri($uri = null) {
646
		if (empty($uri)) {
647
			return false;
648
		}
649
650
		if (is_array($uri)) {
651
			$uri = $this->_parseUri($uri);
652
		} else {
653
			$uri = $this->_parseUri($uri, true);
654
		}
655
656
		if (!isset($uri['host'])) {
657
			return false;
658
		}
659
		$config = array(
660
			'request' => array(
661
				'uri' => array_intersect_key($uri, $this->config['request']['uri'])
662
			)
663
		);
664
		$this->config = Hash::merge($this->config, $config);
665
		$this->config = Hash::merge($this->config, array_intersect_key($this->config['request']['uri'], $this->config));
666
		return true;
667
	}
668
669
/**
670
 * Configure the socket's context. Adds in configuration
671
 * that can not be declared in the class definition.
672
 *
673
 * @param string $host The host you're connecting to.
674
 * @return void
675
 */
676
	protected function _configContext($host) {
677
		foreach ($this->config as $key => $value) {
678
			if (substr($key, 0, 4) !== 'ssl_') {
679
				continue;
680
			}
681
			$contextKey = substr($key, 4);
682
			if (empty($this->config['context']['ssl'][$contextKey])) {
683
				$this->config['context']['ssl'][$contextKey] = $value;
684
			}
685
			unset($this->config[$key]);
686
		}
687
		if (empty($this->config['context']['ssl']['cafile'])) {
688
			$this->config['context']['ssl']['cafile'] = CAKE . 'Config' . DS . 'cacert.pem';
689
		}
690
		if (!empty($this->config['context']['ssl']['verify_host'])) {
691
			$this->config['context']['ssl']['CN_match'] = $host;
692
		}
693
		unset($this->config['context']['ssl']['verify_host']);
694
	}
695
696
/**
697
 * Takes a $uri array and turns it into a fully qualified URL string
698
 *
699
 * @param string|array $uri Either A $uri array, or a request string. Will use $this->config if left empty.
700
 * @param string $uriTemplate The Uri template/format to use.
701
 * @return mixed A fully qualified URL formatted according to $uriTemplate, or false on failure
702
 */
703
	protected function _buildUri($uri = array(), $uriTemplate = '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') {
704
		if (is_string($uri)) {
705
			$uri = array('host' => $uri);
706
		}
707
		$uri = $this->_parseUri($uri, true);
708
709
		if (!is_array($uri) || empty($uri)) {
710
			return false;
711
		}
712
713
		$uri['path'] = preg_replace('/^\//', null, $uri['path']);
714
		$uri['query'] = http_build_query($uri['query'], '', '&');
715
		$uri['query'] = rtrim($uri['query'], '=');
716
		$stripIfEmpty = array(
717
			'query' => '?%query',
718
			'fragment' => '#%fragment',
719
			'user' => '%user:%pass@',
720
			'host' => '%host:%port/'
721
		);
722
723
		foreach ($stripIfEmpty as $key => $strip) {
724
			if (empty($uri[$key])) {
725
				$uriTemplate = str_replace($strip, null, $uriTemplate);
726
			}
727
		}
728
729
		$defaultPorts = array('http' => 80, 'https' => 443);
730
		if (array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) {
731
			$uriTemplate = str_replace(':%port', null, $uriTemplate);
732
		}
733
		foreach ($uri as $property => $value) {
734
			$uriTemplate = str_replace('%' . $property, $value, $uriTemplate);
735
		}
736
737
		if ($uriTemplate === '/*') {
738
			$uriTemplate = '*';
739
		}
740
		return $uriTemplate;
741
	}
742
743
/**
744
 * Parses the given URI and breaks it down into pieces as an indexed array with elements
745
 * such as 'scheme', 'port', 'query'.
746
 *
747
 * @param string|array $uri URI to parse
748
 * @param boolean|array $base If true use default URI config, otherwise indexed array to set 'scheme', 'host', 'port', etc.
749
 * @return array Parsed URI
750
 */
751
	protected function _parseUri($uri = null, $base = array()) {
752
		$uriBase = array(
753
			'scheme' => array('http', 'https'),
754
			'host' => null,
755
			'port' => array(80, 443),
756
			'user' => null,
757
			'pass' => null,
758
			'path' => '/',
759
			'query' => null,
760
			'fragment' => null
761
		);
762
763
		if (is_string($uri)) {
764
			$uri = parse_url($uri);
765
		}
766
		if (!is_array($uri) || empty($uri)) {
767
			return false;
768
		}
769
		if ($base === true) {
770
			$base = $uriBase;
771
		}
772
773
		if (isset($base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) {
774
			if (isset($uri['scheme']) && !isset($uri['port'])) {
775
				$base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])];
776
			} elseif (isset($uri['port']) && !isset($uri['scheme'])) {
777
				$base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])];
778
			}
779
		}
780
781
		if (is_array($base) && !empty($base)) {
782
			$uri = array_merge($base, $uri);
783
		}
784
785 View Code Duplication
		if (isset($uri['scheme']) && is_array($uri['scheme'])) {
786
			$uri['scheme'] = array_shift($uri['scheme']);
787
		}
788 View Code Duplication
		if (isset($uri['port']) && is_array($uri['port'])) {
789
			$uri['port'] = array_shift($uri['port']);
790
		}
791
792
		if (array_key_exists('query', $uri)) {
793
			$uri['query'] = $this->_parseQuery($uri['query']);
794
		}
795
796
		if (!array_intersect_key($uriBase, $uri)) {
797
			return false;
798
		}
799
		return $uri;
800
	}
801
802
/**
803
 * 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
804
 * supports nesting by using the php bracket syntax. So this means you can parse queries like:
805
 *
806
 * - ?key[subKey]=value
807
 * - ?key[]=value1&key[]=value2
808
 *
809
 * A leading '?' mark in $query is optional and does not effect the outcome of this function.
810
 * For the complete capabilities of this implementation take a look at HttpSocketTest::testparseQuery()
811
 *
812
 * @param string|array $query A query string to parse into an array or an array to return directly "as is"
813
 * @return array The $query parsed into a possibly multi-level array. If an empty $query is
814
 *     given, an empty array is returned.
815
 */
816
	protected function _parseQuery($query) {
817
		if (is_array($query)) {
818
			return $query;
819
		}
820
821
		$parsedQuery = array();
822
823
		if (is_string($query) && !empty($query)) {
824
			$query = preg_replace('/^\?/', '', $query);
825
			$items = explode('&', $query);
826
827
			foreach ($items as $item) {
828 View Code Duplication
				if (strpos($item, '=') !== false) {
829
					list($key, $value) = explode('=', $item, 2);
830
				} else {
831
					$key = $item;
832
					$value = null;
833
				}
834
835
				$key = urldecode($key);
836
				$value = urldecode($value);
837
838
				if (preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) {
839
					$subKeys = $matches[1];
840
					$rootKey = substr($key, 0, strpos($key, '['));
841
					if (!empty($rootKey)) {
842
						array_unshift($subKeys, $rootKey);
843
					}
844
					$queryNode =& $parsedQuery;
845
846
					foreach ($subKeys as $subKey) {
847
						if (!is_array($queryNode)) {
848
							$queryNode = array();
849
						}
850
851
						if ($subKey === '') {
852
							$queryNode[] = array();
853
							end($queryNode);
854
							$subKey = key($queryNode);
855
						}
856
						$queryNode =& $queryNode[$subKey];
857
					}
858
					$queryNode = $value;
859
					continue;
860
				}
861
				if (!isset($parsedQuery[$key])) {
862
					$parsedQuery[$key] = $value;
863
				} else {
864
					$parsedQuery[$key] = (array)$parsedQuery[$key];
865
					$parsedQuery[$key][] = $value;
866
				}
867
			}
868
		}
869
		return $parsedQuery;
870
	}
871
872
/**
873
 * Builds a request line according to HTTP/1.1 specs. Activate quirks mode to work outside specs.
874
 *
875
 * @param array $request Needs to contain a 'uri' key. Should also contain a 'method' key, otherwise defaults to GET.
876
 * @param string $versionToken The version token to use, defaults to HTTP/1.1
877
 * @return string Request line
878
 * @throws SocketException
879
 */
880
	protected function _buildRequestLine($request = array(), $versionToken = 'HTTP/1.1') {
881
		$asteriskMethods = array('OPTIONS');
882
883
		if (is_string($request)) {
884
			$isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match);
885 View Code Duplication
			if (!$this->quirksMode && (!$isValid || ($match[2] === '*' && !in_array($match[3], $asteriskMethods)))) {
886
				throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.'));
887
			}
888
			return $request;
889
		} elseif (!is_array($request)) {
890
			return false;
891
		} elseif (!array_key_exists('uri', $request)) {
892
			return false;
893
		}
894
895
		$request['uri'] = $this->_parseUri($request['uri']);
896
		$request = array_merge(array('method' => 'GET'), $request);
897
		if (!empty($this->_proxy['host'])) {
898
			$request['uri'] = $this->_buildUri($request['uri'], '%scheme://%host:%port/%path?%query');
0 ignored issues
show
Security Bug introduced by
It seems like $request['uri'] can also be of type false; however, HttpSocket::_buildUri() does only seem to accept string|array, did you maybe forget to handle an error condition?
Loading history...
899
		} else {
900
			$request['uri'] = $this->_buildUri($request['uri'], '/%path?%query');
0 ignored issues
show
Security Bug introduced by
It seems like $request['uri'] can also be of type false; however, HttpSocket::_buildUri() does only seem to accept string|array, did you maybe forget to handle an error condition?
Loading history...
901
		}
902
903 View Code Duplication
		if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) {
904
			throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - The "*" asterisk character is only allowed for the following methods: %s. Activate quirks mode to work outside of HTTP/1.1 specs.', implode(',', $asteriskMethods)));
905
		}
906
		return $request['method'] . ' ' . $request['uri'] . ' ' . $versionToken . "\r\n";
907
	}
908
909
/**
910
 * Builds the header.
911
 *
912
 * @param array $header Header to build
913
 * @param string $mode
914
 * @return string Header built from array
915
 */
916
	protected function _buildHeader($header, $mode = 'standard') {
917
		if (is_string($header)) {
918
			return $header;
919
		} elseif (!is_array($header)) {
920
			return false;
921
		}
922
923
		$fieldsInHeader = array();
924
		foreach ($header as $key => $value) {
925
			$lowKey = strtolower($key);
926
			if (array_key_exists($lowKey, $fieldsInHeader)) {
927
				$header[$fieldsInHeader[$lowKey]] = $value;
928
				unset($header[$key]);
929
			} else {
930
				$fieldsInHeader[$lowKey] = $key;
931
			}
932
		}
933
934
		$returnHeader = '';
935
		foreach ($header as $field => $contents) {
936
			if (is_array($contents) && $mode === 'standard') {
937
				$contents = implode(',', $contents);
938
			}
939
			foreach ((array)$contents as $content) {
940
				$contents = preg_replace("/\r\n(?![\t ])/", "\r\n ", $content);
941
				$field = $this->_escapeToken($field);
942
943
				$returnHeader .= $field . ': ' . $contents . "\r\n";
944
			}
945
		}
946
		return $returnHeader;
947
	}
948
949
/**
950
 * Builds cookie headers for a request.
951
 *
952
 * Cookies can either be in the format returned in responses, or
953
 * a simple key => value pair.
954
 *
955
 * @param array $cookies Array of cookies to send with the request.
956
 * @return string Cookie header string to be sent with the request.
957
 */
958
	public function buildCookies($cookies) {
959
		$header = array();
960
		foreach ($cookies as $name => $cookie) {
961
			if (is_array($cookie)) {
962
				$value = $this->_escapeToken($cookie['value'], array(';'));
963
			} else {
964
				$value = $this->_escapeToken($cookie, array(';'));
965
			}
966
			$header[] = $name . '=' . $value;
967
		}
968
		return $this->_buildHeader(array('Cookie' => implode('; ', $header)), 'pragmatic');
969
	}
970
971
/**
972
 * Escapes a given $token according to RFC 2616 (HTTP 1.1 specs)
973
 *
974
 * @param string $token Token to escape
975
 * @param array $chars
976
 * @return string Escaped token
977
 */
978
	protected function _escapeToken($token, $chars = null) {
979
		$regex = '/([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])/';
980
		$token = preg_replace($regex, '"\\1"', $token);
981
		return $token;
982
	}
983
984
/**
985
 * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
986
 *
987
 * @param boolean $hex true to get them as HEX values, false otherwise
988
 * @param array $chars
989
 * @return array Escape chars
990
 */
991 View Code Duplication
	protected function _tokenEscapeChars($hex = true, $chars = null) {
992
		if (!empty($chars)) {
993
			$escape = $chars;
994
		} else {
995
			$escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
996
			for ($i = 0; $i <= 31; $i++) {
997
				$escape[] = chr($i);
998
			}
999
			$escape[] = chr(127);
1000
		}
1001
1002
		if (!$hex) {
1003
			return $escape;
1004
		}
1005
		foreach ($escape as $key => $char) {
1006
			$escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
1007
		}
1008
		return $escape;
1009
	}
1010
1011
/**
1012
 * Resets the state of this HttpSocket instance to it's initial state (before Object::__construct got executed) or does
1013
 * the same thing partially for the request and the response property only.
1014
 *
1015
 * @param boolean $full If set to false only HttpSocket::response and HttpSocket::request are reset
1016
 * @return boolean True on success
1017
 */
1018
	public function reset($full = true) {
1019
		static $initalState = array();
1020
		if (empty($initalState)) {
1021
			$initalState = get_class_vars(__CLASS__);
1022
		}
1023
		if (!$full) {
1024
			$this->request = $initalState['request'];
1025
			$this->response = $initalState['response'];
1026
			return true;
1027
		}
1028
		parent::reset($initalState);
1029
		return true;
1030
	}
1031
1032
}
1033