Completed
Pull Request — 1.11.x (#1599)
by José
28:19
created

Requests_Transport_fsockopen::request()   F

Complexity

Conditions 49
Paths > 20000

Size

Total Lines 235
Code Lines 137

Duplication

Lines 7
Ratio 2.98 %

Importance

Changes 0
Metric Value
cc 49
eloc 137
nc 42430375
nop 4
dl 7
loc 235
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * fsockopen HTTP transport
4
 *
5
 * @package Requests
6
 * @subpackage Transport
7
 */
8
9
/**
10
 * fsockopen HTTP transport
11
 *
12
 * @package Requests
13
 * @subpackage Transport
14
 */
15
class Requests_Transport_fsockopen implements Requests_Transport {
16
	/**
17
	 * Second to microsecond conversion
18
	 *
19
	 * @var integer
20
	 */
21
	const SECOND_IN_MICROSECONDS = 1000000;
22
23
	/**
24
	 * Raw HTTP data
25
	 *
26
	 * @var string
27
	 */
28
	public $headers = '';
29
30
	/**
31
	 * Stream metadata
32
	 *
33
	 * @var array Associative array of properties, see {@see https://secure.php.net/stream_get_meta_data}
34
	 */
35
	public $info;
36
37
	/**
38
	 * What's the maximum number of bytes we should keep?
39
	 *
40
	 * @var int|bool Byte count, or false if no limit.
41
	 */
42
	protected $max_bytes = false;
43
44
	protected $connect_error = '';
45
46
	/**
47
	 * Perform a request
48
	 *
49
	 * @throws Requests_Exception On failure to connect to socket (`fsockopenerror`)
50
	 * @throws Requests_Exception On socket timeout (`timeout`)
51
	 *
52
	 * @param string $url URL to request
53
	 * @param array $headers Associative array of request headers
54
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
55
	 * @param array $options Request options, see {@see Requests::response()} for documentation
56
	 * @return string Raw HTTP result
57
	 */
58
	public function request($url, $headers = array(), $data = array(), $options = array()) {
59
		$options['hooks']->dispatch('fsockopen.before_request');
60
61
		$url_parts = parse_url($url);
62
		if (empty($url_parts)) {
63
			throw new Requests_Exception('Invalid URL.', 'invalidurl', $url);
64
		}
65
		$host = $url_parts['host'];
66
		$context = stream_context_create();
67
		$verifyname = false;
68
		$case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers);
69
70
		// HTTPS support
71
		if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
72
			$remote_socket = 'ssl://' . $host;
73
			if (!isset($url_parts['port'])) {
74
				$url_parts['port'] = 443;
75
			}
76
77
			$context_options = array(
78
				'verify_peer' => true,
79
				// 'CN_match' => $host,
80
				'capture_peer_cert' => true
81
			);
82
			$verifyname = true;
83
84
			// SNI, if enabled (OpenSSL >=0.9.8j)
85
			if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) {
86
				$context_options['SNI_enabled'] = true;
87 View Code Duplication
				if (isset($options['verifyname']) && $options['verifyname'] === false) {
88
					$context_options['SNI_enabled'] = false;
89
				}
90
			}
91
92
			if (isset($options['verify'])) {
93
				if ($options['verify'] === false) {
94
					$context_options['verify_peer'] = false;
95
				}
96
				elseif (is_string($options['verify'])) {
97
					$context_options['cafile'] = $options['verify'];
98
				}
99
			}
100
101 View Code Duplication
			if (isset($options['verifyname']) && $options['verifyname'] === false) {
102
				$context_options['verify_peer_name'] = false;
103
				$verifyname = false;
104
			}
105
106
			stream_context_set_option($context, array('ssl' => $context_options));
107
		}
108
		else {
109
			$remote_socket = 'tcp://' . $host;
110
		}
111
112
		$this->max_bytes = $options['max_bytes'];
113
114
		if (!isset($url_parts['port'])) {
115
			$url_parts['port'] = 80;
116
		}
117
		$remote_socket .= ':' . $url_parts['port'];
118
119
		set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE);
120
121
		$options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket));
122
123
		$socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
124
125
		restore_error_handler();
126
127
		if ($verifyname && !$this->verify_certificate_from_context($host, $context)) {
128
			throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
129
		}
130
131
		if (!$socket) {
132
			if ($errno === 0) {
133
				// Connection issue
134
				throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
135
			}
136
137
			throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno);
138
		}
139
140
		$data_format = $options['data_format'];
141
142
		if ($data_format === 'query') {
143
			$path = self::format_get($url_parts, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 58 can also be of type string; however, Requests_Transport_fsockopen::format_get() does only seem to accept array|object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
144
			$data = '';
145
		}
146
		else {
147
			$path = self::format_get($url_parts, array());
148
		}
149
150
		$options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
151
152
		$request_body = '';
153
		$out = sprintf("%s %s HTTP/%.1f\r\n", $options['type'], $path, $options['protocol_version']);
154
155
		if ($options['type'] !== Requests::TRACE) {
156
			if (is_array($data)) {
157
				$request_body = http_build_query($data, null, '&');
158
			}
159
			else {
160
				$request_body = $data;
161
			}
162
163
			if (!empty($data)) {
164
				if (!isset($case_insensitive_headers['Content-Length'])) {
165
					$headers['Content-Length'] = strlen($request_body);
166
				}
167
168
				if (!isset($case_insensitive_headers['Content-Type'])) {
169
					$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
170
				}
171
			}
172
		}
173
174
		if (!isset($case_insensitive_headers['Host'])) {
175
			$out .= sprintf('Host: %s', $url_parts['host']);
176
177
			if (( 'http' === strtolower($url_parts['scheme']) && $url_parts['port'] !== 80 ) || ( 'https' === strtolower($url_parts['scheme']) && $url_parts['port'] !== 443 )) {
178
				$out .= ':' . $url_parts['port'];
179
			}
180
			$out .= "\r\n";
181
		}
182
183
		if (!isset($case_insensitive_headers['User-Agent'])) {
184
			$out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
185
		}
186
187
		$accept_encoding = $this->accept_encoding();
188
		if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) {
189
			$out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding);
190
		}
191
192
		$headers = Requests::flatten($headers);
193
194
		if (!empty($headers)) {
195
			$out .= implode($headers, "\r\n") . "\r\n";
196
		}
197
198
		$options['hooks']->dispatch('fsockopen.after_headers', array(&$out));
199
200
		if (substr($out, -2) !== "\r\n") {
201
			$out .= "\r\n";
202
		}
203
204
		if (!isset($case_insensitive_headers['Connection'])) {
205
			$out .= "Connection: Close\r\n";
206
		}
207
208
		$out .= "\r\n" . $request_body;
209
210
		$options['hooks']->dispatch('fsockopen.before_send', array(&$out));
211
212
		fwrite($socket, $out);
213
		$options['hooks']->dispatch('fsockopen.after_send', array($out));
214
215
		if (!$options['blocking']) {
216
			fclose($socket);
217
			$fake_headers = '';
218
			$options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers));
219
			return '';
220
		}
221
222
		$timeout_sec = (int) floor($options['timeout']);
223
		if ($timeout_sec == $options['timeout']) {
224
			$timeout_msec = 0;
225
		}
226
		else {
227
			$timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
228
		}
229
		stream_set_timeout($socket, $timeout_sec, $timeout_msec);
230
231
		$response = $body = $headers = '';
232
		$this->info = stream_get_meta_data($socket);
233
		$size = 0;
234
		$doingbody = false;
235
		$download = false;
236
		if ($options['filename']) {
237
			$download = fopen($options['filename'], 'wb');
238
		}
239
240
		while (!feof($socket)) {
241
			$this->info = stream_get_meta_data($socket);
242
			if ($this->info['timed_out']) {
243
				throw new Requests_Exception('fsocket timed out', 'timeout');
244
			}
245
246
			$block = fread($socket, Requests::BUFFER_SIZE);
247
			if (!$doingbody) {
248
				$response .= $block;
249
				if (strpos($response, "\r\n\r\n")) {
250
					list($headers, $block) = explode("\r\n\r\n", $response, 2);
251
					$doingbody = true;
252
				}
253
			}
254
255
			// Are we in body mode now?
256
			if ($doingbody) {
257
				$options['hooks']->dispatch('request.progress', array($block, $size, $this->max_bytes));
258
				$data_length = strlen($block);
259
				if ($this->max_bytes) {
260
					// Have we already hit a limit?
261
					if ($size === $this->max_bytes) {
262
						continue;
263
					}
264
					if (($size + $data_length) > $this->max_bytes) {
265
						// Limit the length
266
						$limited_length = ($this->max_bytes - $size);
267
						$block = substr($block, 0, $limited_length);
268
					}
269
				}
270
271
				$size += strlen($block);
272
				if ($download) {
273
					fwrite($download, $block);
274
				}
275
				else {
276
					$body .= $block;
277
				}
278
			}
279
		}
280
		$this->headers = $headers;
281
282
		if ($download) {
283
			fclose($download);
284
		}
285
		else {
286
			$this->headers .= "\r\n\r\n" . $body;
287
		}
288
		fclose($socket);
289
290
		$options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers, &$this->info));
291
		return $this->headers;
292
	}
293
294
	/**
295
	 * Send multiple requests simultaneously
296
	 *
297
	 * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request}
298
	 * @param array $options Global options, see {@see Requests::response()} for documentation
299
	 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
300
	 */
301
	public function request_multiple($requests, $options) {
302
		$responses = array();
303
		$class = get_class($this);
304
		foreach ($requests as $id => $request) {
305
			try {
306
				$handler = new $class();
307
				$responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);
308
309
				$request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request));
310
			}
311
			catch (Requests_Exception $e) {
312
				$responses[$id] = $e;
313
			}
314
315 View Code Duplication
			if (!is_string($responses[$id])) {
316
				$request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id));
317
			}
318
		}
319
320
		return $responses;
321
	}
322
323
	/**
324
	 * Retrieve the encodings we can accept
325
	 *
326
	 * @return string Accept-Encoding header value
327
	 */
328
	protected static function accept_encoding() {
329
		$type = array();
330
		if (function_exists('gzinflate')) {
331
			$type[] = 'deflate;q=1.0';
332
		}
333
334
		if (function_exists('gzuncompress')) {
335
			$type[] = 'compress;q=0.5';
336
		}
337
338
		$type[] = 'gzip;q=0.5';
339
340
		return implode(', ', $type);
341
	}
342
343
	/**
344
	 * Format a URL given GET data
345
	 *
346
	 * @param array $url_parts
347
	 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
348
	 * @return string URL with data
349
	 */
350
	protected static function format_get($url_parts, $data) {
351
		if (!empty($data)) {
352
			if (empty($url_parts['query'])) {
353
				$url_parts['query'] = '';
354
			}
355
356
			$url_parts['query'] .= '&' . http_build_query($data, null, '&');
357
			$url_parts['query'] = trim($url_parts['query'], '&');
358
		}
359
		if (isset($url_parts['path'])) {
360
			if (isset($url_parts['query'])) {
361
				$get = $url_parts['path'] . '?' . $url_parts['query'];
362
			}
363
			else {
364
				$get = $url_parts['path'];
365
			}
366
		}
367
		else {
368
			$get = '/';
369
		}
370
		return $get;
371
	}
372
373
	/**
374
	 * Error handler for stream_socket_client()
375
	 *
376
	 * @param int $errno Error number (e.g. E_WARNING)
377
	 * @param string $errstr Error message
378
	 */
379
	public function connect_error_handler($errno, $errstr) {
380
		// Double-check we can handle it
381
		if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
382
			// Return false to indicate the default error handler should engage
383
			return false;
384
		}
385
386
		$this->connect_error .= $errstr . "\n";
387
		return true;
388
	}
389
390
	/**
391
	 * Verify the certificate against common name and subject alternative names
392
	 *
393
	 * Unfortunately, PHP doesn't check the certificate against the alternative
394
	 * names, leading things like 'https://www.github.com/' to be invalid.
395
	 * Instead
396
	 *
397
	 * @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
398
	 *
399
	 * @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`)
400
	 * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`)
401
	 * @param string $host Host name to verify against
402
	 * @param resource $context Stream context
403
	 * @return bool
404
	 */
405
	public function verify_certificate_from_context($host, $context) {
406
		$meta = stream_context_get_options($context);
407
408
		// If we don't have SSL options, then we couldn't make the connection at
409
		// all
410
		if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
411
			throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error');
412
		}
413
414
		$cert = openssl_x509_parse($meta['ssl']['peer_certificate']);
415
416
		return Requests_SSL::verify_certificate($host, $cert);
417
	}
418
419
	/**
420
	 * Whether this transport is valid
421
	 *
422
	 * @codeCoverageIgnore
423
	 * @return boolean True if the transport is valid, false otherwise.
424
	 */
425
	public static function test($capabilities = array()) {
426
		if (!function_exists('fsockopen')) {
427
			return false;
428
		}
429
430
		// If needed, check that streams support SSL
431
		if (isset($capabilities['ssl']) && $capabilities['ssl']) {
432
			if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) {
433
				return false;
434
			}
435
436
			// Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156
437
			if (defined('HHVM_VERSION')) {
438
				return false;
439
			}
440
		}
441
442
		return true;
443
	}
444
}
445