Completed
Pull Request — master (#284)
by
unknown
01:30
created

Requests_Transport_cURL   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 559
Duplicated Lines 1.61 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 9
loc 559
rs 2.4
c 0
b 0
f 0
wmc 75
lcom 1
cbo 4

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 4
A __destruct() 0 5 2
B request() 3 56 10
D request_multiple() 3 110 14
A get_subrequest_handle() 3 17 3
F setup_handle() 0 85 20
A process_response() 0 27 4
A stream_headers() 0 15 3
A stream_body() 0 28 5
A format_get() 0 22 4
A test() 0 15 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Requests_Transport_cURL often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Requests_Transport_cURL, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * cURL HTTP transport
4
 *
5
 * @package Requests
6
 * @subpackage Transport
7
 */
8
9
/**
10
 * cURL HTTP transport
11
 *
12
 * @package Requests
13
 * @subpackage Transport
14
 */
15
class Requests_Transport_cURL implements Requests_Transport {
16
	const CURL_7_10_5 = 0x070A05;
17
	const CURL_7_16_2 = 0x071002;
18
19
	/**
20
	 * Raw HTTP data
21
	 *
22
	 * @var string
23
	 */
24
	public $headers = '';
25
26
	/**
27
	 * Raw body data
28
	 *
29
	 * @var string
30
	 */
31
	public $response_data = '';
32
33
	/**
34
	 * Information on the current request
35
	 *
36
	 * @var array cURL information array, see {@see https://secure.php.net/curl_getinfo}
37
	 */
38
	public $info;
39
40
	/**
41
	 * Version string
42
	 *
43
	 * @var long
44
	 */
45
	public $version;
46
47
	/**
48
	 * cURL handle
49
	 *
50
	 * @var resource
51
	 */
52
	protected $handle;
53
54
	/**
55
	 * Hook dispatcher instance
56
	 *
57
	 * @var Requests_Hooks
58
	 */
59
	protected $hooks;
60
61
	/**
62
	 * Have we finished the headers yet?
63
	 *
64
	 * @var boolean
65
	 */
66
	protected $done_headers = false;
67
68
	/**
69
	 * If streaming to a file, keep the file pointer
70
	 *
71
	 * @var resource
72
	 */
73
	protected $stream_handle;
74
75
	/**
76
	 * How many bytes are in the response body?
77
	 *
78
	 * @var int
79
	 */
80
	protected $response_bytes;
81
82
	/**
83
	 * What's the maximum number of bytes we should keep?
84
	 *
85
	 * @var int|bool Byte count, or false if no limit.
86
	 */
87
	protected $response_byte_limit;
88
89
	/**
90
	 * Constructor
91
	 */
92
	public function __construct() {
93
		$curl = curl_version();
94
		$this->version = $curl['version_number'];
95
		$this->handle = curl_init();
96
97
		curl_setopt($this->handle, CURLOPT_HEADER, false);
98
		curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1);
99
		if ($this->version >= self::CURL_7_10_5) {
100
			curl_setopt($this->handle, CURLOPT_ENCODING, '');
101
		}
102
		if (defined('CURLOPT_PROTOCOLS')) {
103
			curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
104
		}
105
		if (defined('CURLOPT_REDIR_PROTOCOLS')) {
106
			curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
107
		}
108
	}
109
110
	/**
111
	 * Destructor
112
	 */
113
	public function __destruct() {
114
		if (is_resource($this->handle)) {
115
			curl_close($this->handle);
116
		}
117
	}
118
119
	/**
120
	 * Perform a request
121
	 *
122
	 * @throws Requests_Exception On a cURL error (`curlerror`)
123
	 *
124
	 * @param string $url URL to request
125
	 * @param array $headers Associative array of request headers
126
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
127
	 * @param array $options Request options, see {@see Requests::response()} for documentation
128
	 * @return string Raw HTTP result
129
	 */
130
	public function request($url, $headers = array(), $data = array(), $options = array()) {
131
		$this->hooks = $options['hooks'];
132
133
		$this->setup_handle($url, $headers, $data, $options);
134
135
		$options['hooks']->dispatch('curl.before_send', array(&$this->handle));
136
137 View Code Duplication
		if ($options['filename'] !== false) {
138
			$this->stream_handle = fopen($options['filename'], 'wb');
139
		}
140
141
		$this->response_data = '';
142
		$this->response_bytes = 0;
143
		$this->response_byte_limit = false;
144
		if ($options['max_bytes'] !== false) {
145
			$this->response_byte_limit = $options['max_bytes'];
146
		}
147
148
		if (isset($options['verify'])) {
149
			if ($options['verify'] === false) {
150
				curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
151
				curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0);
152
			}
153
			elseif (is_string($options['verify'])) {
154
				curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']);
155
			}
156
		}
157
158
		if (isset($options['verifyname']) && $options['verifyname'] === false) {
159
			curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
160
		}
161
162
		curl_exec($this->handle);
163
		$response = $this->response_data;
164
165
		$options['hooks']->dispatch('curl.after_send', array());
166
167
		if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) {
168
			// Reset encoding and try again
169
			curl_setopt($this->handle, CURLOPT_ENCODING, 'none');
170
171
			$this->response_data = '';
172
			$this->response_bytes = 0;
173
			curl_exec($this->handle);
174
			$response = $this->response_data;
175
		}
176
177
		$this->process_response($response, $options);
178
179
		// Need to remove the $this reference from the curl handle.
180
		// Otherwise Requests_Transport_cURL wont be garbage collected and the curl_close() will never be called.
181
		curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null);
182
		curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null);
183
184
		return $this->headers;
185
	}
186
187
	/**
188
	 * Send multiple requests simultaneously
189
	 *
190
	 * @param array $requests Request data
191
	 * @param array $options Global options
192
	 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
193
	 */
194
	public function request_multiple($requests, $options) {
195
		// If you're not requesting, we can't get any responses ¯\_(ツ)_/¯
196
		if (empty($requests)) {
197
			return array();
198
		}
199
200
		$multihandle = curl_multi_init();
201
		$subrequests = array();
202
		$subhandles = array();
203
204
		$class = get_class($this);
205
		foreach ($requests as $id => $request) {
206
			$subrequests[$id] = new $class();
207
			$subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
208
			$request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
209
			curl_multi_add_handle($multihandle, $subhandles[$id]);
210
		}
211
212
		$completed = 0;
213
		$responses = array();
214
215
		$request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle));
0 ignored issues
show
Bug introduced by
The variable $request seems to be defined by a foreach iteration on line 205. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
216
217
		$made_progress = true;
218
		$all_requests_start = microtime(true);
219
		$microseconds_taken_by_last_curl_operation = 0;
220
		do {
221
			$active = false;
222
223
			if (!$made_progress) {
224
				// For a small fraction of slow requests, curl_multi_select will busy loop and return immediately with no indication of error (it returns 0 immediately with/without setting curl_multi_errno).
225
				// (https://github.com/rmccue/Requests/pull/284)
226
				// To mitigate this, add our own sleep if curl returned but no requests completed (failing or succeeding)
227
				// - The amount of time to sleep is the smaller of 2ms or 10% of total time spent waiting for curl requests
228
				//   (10% was arbitrarily chosen to slowing down requests that would normally take less than 1 millisecond)
229
				// - The amount of time that was already spent in curl_multi_exec+curl_multi_select in the previous request is subtracted from that.
230
				//   (E.g. if curl_multi_select already slept for 2 milliseconds, curl_multi_select might be operating normally)
231
				$elapsed_microseconds = (microtime(true) - $all_requests_start) * 1000000;
232
				$microseconds_to_sleep = min($elapsed_microseconds / 10, 2000) - $microseconds_taken_by_last_curl_operation;
233
				if ($microseconds_to_sleep >= 1) {
234
					usleep($microseconds_to_sleep);
235
				}
236
			}
237
			$operation_start = microtime(true);
238
			$made_progress = false;
239
			$is_first_multi_exec = true;
240
			do {
241
				if (!$is_first_multi_exec) {
242
					// Sleep for 1 millisecond to avoid a busy loop that would chew CPU
243
					// See ORA2PG-498
244
					usleep(1000);
245
				}
246
				$status = curl_multi_exec($multihandle, $active);
247
				$is_first_multi_exec = false;
248
			}
249
			while ($status === CURLM_CALL_MULTI_PERFORM);
250
251
			// curl_multi_select will sleep for at most 50 milliseconds before returning.
252
			curl_multi_select($multihandle, 0.05);
253
			$microseconds_taken_by_last_curl_operation = (microtime(true) - $operation_start) * 100000;
254
255
			$to_process = array();
256
257
			// Read the information as needed
258
			while ($done = curl_multi_info_read($multihandle)) {
259
				$key = array_search($done['handle'], $subhandles, true);
260
				if (!isset($to_process[$key])) {
261
					$to_process[$key] = $done;
262
				}
263
			}
264
265
			// Parse the finished requests before we start getting the new ones
266
			foreach ($to_process as $key => $done) {
267
				$made_progress = true;
268
				$options = $requests[$key]['options'];
269
				if (CURLE_OK !== $done['result']) {
270
					//get error string for handle.
271
					$reason = curl_error($done['handle']);
272
					$exception = new Requests_Exception_Transport_cURL(
273
									$reason,
274
									Requests_Exception_Transport_cURL::EASY,
275
									$done['handle'],
276
									$done['result']
277
								);
278
					$responses[$key] = $exception;
279
					$options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key]));
280
				}
281
				else {
282
					$responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
283
284
					$options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key]));
285
				}
286
287
				curl_multi_remove_handle($multihandle, $done['handle']);
288
				curl_close($done['handle']);
289
290 View Code Duplication
				if (!is_string($responses[$key])) {
291
					$options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
292
				}
293
				$completed++;
294
			}
295
		}
296
		while ($active || $completed < count($subrequests));
297
298
		$request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
299
300
		curl_multi_close($multihandle);
301
302
		return $responses;
303
	}
304
305
	/**
306
	 * Get the cURL handle for use in a multi-request
307
	 *
308
	 * @param string $url URL to request
309
	 * @param array $headers Associative array of request headers
310
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
311
	 * @param array $options Request options, see {@see Requests::response()} for documentation
312
	 * @return resource Subrequest's cURL handle
313
	 */
314
	public function &get_subrequest_handle($url, $headers, $data, $options) {
315
		$this->setup_handle($url, $headers, $data, $options);
316
317 View Code Duplication
		if ($options['filename'] !== false) {
318
			$this->stream_handle = fopen($options['filename'], 'wb');
319
		}
320
321
		$this->response_data = '';
322
		$this->response_bytes = 0;
323
		$this->response_byte_limit = false;
324
		if ($options['max_bytes'] !== false) {
325
			$this->response_byte_limit = $options['max_bytes'];
326
		}
327
		$this->hooks = $options['hooks'];
328
329
		return $this->handle;
330
	}
331
332
	/**
333
	 * Setup the cURL handle for the given data
334
	 *
335
	 * @param string $url URL to request
336
	 * @param array $headers Associative array of request headers
337
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
338
	 * @param array $options Request options, see {@see Requests::response()} for documentation
339
	 */
340
	protected function setup_handle($url, $headers, $data, $options) {
341
		$options['hooks']->dispatch('curl.before_request', array(&$this->handle));
342
343
		// Force closing the connection for old versions of cURL (<7.22).
344
		if ( ! isset( $headers['Connection'] ) ) {
345
			$headers['Connection'] = 'close';
346
		}
347
348
		$headers = Requests::flatten($headers);
349
350
		if (!empty($data)) {
351
			$data_format = $options['data_format'];
352
353
			if ($data_format === 'query') {
354
				$url = self::format_get($url, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 340 can also be of type string; however, Requests_Transport_cURL::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...
355
				$data = '';
356
			}
357
			elseif (!is_string($data)) {
358
				$data = http_build_query($data, null, '&');
359
			}
360
		}
361
362
		switch ($options['type']) {
363
			case Requests::POST:
364
				curl_setopt($this->handle, CURLOPT_POST, true);
365
				curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
366
				break;
367
			case Requests::HEAD:
368
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
369
				curl_setopt($this->handle, CURLOPT_NOBODY, true);
370
				break;
371
			case Requests::TRACE:
372
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
373
				break;
374
			case Requests::PATCH:
375
			case Requests::PUT:
376
			case Requests::DELETE:
377
			case Requests::OPTIONS:
378
			default:
379
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
380
				if (!empty($data)) {
381
					curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
382
				}
383
		}
384
385
		// cURL requires a minimum timeout of 1 second when using the system
386
		// DNS resolver, as it uses `alarm()`, which is second resolution only.
387
		// There's no way to detect which DNS resolver is being used from our
388
		// end, so we need to round up regardless of the supplied timeout.
389
		//
390
		// https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609
391
		$timeout = max($options['timeout'], 1);
392
393
		if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
394
			curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
395
		}
396
		else {
397
			curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
398
		}
399
400
		if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
401
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
402
		}
403
		else {
404
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
405
		}
406
		curl_setopt($this->handle, CURLOPT_URL, $url);
407
		curl_setopt($this->handle, CURLOPT_REFERER, $url);
408
		curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
409
		if (!empty($headers)) {
410
			curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
411
		}
412
		if ($options['protocol_version'] === 1.1) {
413
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
414
		}
415
		else {
416
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
417
		}
418
419
		if (true === $options['blocking']) {
420
			curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
421
			curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
422
			curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
423
		}
424
	}
425
426
	/**
427
	 * Process a response
428
	 *
429
	 * @param string $response Response data from the body
430
	 * @param array $options Request options
431
	 * @return string HTTP response data including headers
432
	 */
433
	public function process_response($response, $options) {
434
		if ($options['blocking'] === false) {
435
			$fake_headers = '';
436
			$options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
437
			return false;
438
		}
439
		if ($options['filename'] !== false) {
440
			fclose($this->stream_handle);
441
			$this->headers = trim($this->headers);
442
		}
443
		else {
444
			$this->headers .= $response;
445
		}
446
447
		if (curl_errno($this->handle)) {
448
			$error = sprintf(
449
				'cURL error %s: %s',
450
				curl_errno($this->handle),
451
				curl_error($this->handle)
452
			);
453
			throw new Requests_Exception($error, 'curlerror', $this->handle);
454
		}
455
		$this->info = curl_getinfo($this->handle);
456
457
		$options['hooks']->dispatch('curl.after_request', array(&$this->headers, &$this->info));
458
		return $this->headers;
459
	}
460
461
	/**
462
	 * Collect the headers as they are received
463
	 *
464
	 * @param resource $handle cURL resource
465
	 * @param string $headers Header string
466
	 * @return integer Length of provided header
467
	 */
468
	public function stream_headers($handle, $headers) {
469
		// Why do we do this? cURL will send both the final response and any
470
		// interim responses, such as a 100 Continue. We don't need that.
471
		// (We may want to keep this somewhere just in case)
472
		if ($this->done_headers) {
473
			$this->headers = '';
474
			$this->done_headers = false;
475
		}
476
		$this->headers .= $headers;
477
478
		if ($headers === "\r\n") {
479
			$this->done_headers = true;
480
		}
481
		return strlen($headers);
482
	}
483
484
	/**
485
	 * Collect data as it's received
486
	 *
487
	 * @since 1.6.1
488
	 *
489
	 * @param resource $handle cURL resource
490
	 * @param string $data Body data
491
	 * @return integer Length of provided data
492
	 */
493
	public function stream_body($handle, $data) {
494
		$this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit));
495
		$data_length = strlen($data);
496
497
		// Are we limiting the response size?
498
		if ($this->response_byte_limit) {
499
			if ($this->response_bytes === $this->response_byte_limit) {
500
				// Already at maximum, move on
501
				return $data_length;
502
			}
503
504
			if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
505
				// Limit the length
506
				$limited_length = ($this->response_byte_limit - $this->response_bytes);
507
				$data = substr($data, 0, $limited_length);
508
			}
509
		}
510
511
		if ($this->stream_handle) {
512
			fwrite($this->stream_handle, $data);
513
		}
514
		else {
515
			$this->response_data .= $data;
516
		}
517
518
		$this->response_bytes += strlen($data);
519
		return $data_length;
520
	}
521
522
	/**
523
	 * Format a URL given GET data
524
	 *
525
	 * @param string $url
526
	 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
527
	 * @return string URL with data
528
	 */
529
	protected static function format_get($url, $data) {
530
		if (!empty($data)) {
531
			$url_parts = parse_url($url);
532
			if (empty($url_parts['query'])) {
533
				$query = $url_parts['query'] = '';
534
			}
535
			else {
536
				$query = $url_parts['query'];
537
			}
538
539
			$query .= '&' . http_build_query($data, null, '&');
540
			$query = trim($query, '&');
541
542
			if (empty($url_parts['query'])) {
543
				$url .= '?' . $query;
544
			}
545
			else {
546
				$url = str_replace($url_parts['query'], $query, $url);
547
			}
548
		}
549
		return $url;
550
	}
551
552
	/**
553
	 * Whether this transport is valid
554
	 *
555
	 * @codeCoverageIgnore
556
	 * @return boolean True if the transport is valid, false otherwise.
557
	 */
558
	public static function test($capabilities = array()) {
559
		if (!function_exists('curl_init') || !function_exists('curl_exec')) {
560
			return false;
561
		}
562
563
		// If needed, check that our installed curl version supports SSL
564
		if (isset($capabilities['ssl']) && $capabilities['ssl']) {
565
			$curl_version = curl_version();
566
			if (!(CURL_VERSION_SSL & $curl_version['features'])) {
567
				return false;
568
			}
569
		}
570
571
		return true;
572
	}
573
}
574