Completed
Push — 1.11.x ( 518476...344d9e )
by José
55:02 queued 28:13
created

Requests_Transport_cURL   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 528
Duplicated Lines 1.7 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 9
loc 528
rs 5.5667
c 0
b 0
f 0
wmc 72
lcom 1
cbo 4

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 4
A __destruct() 0 5 2
C request() 3 56 10
C request_multiple() 3 79 11
A get_subrequest_handle() 3 17 3
F setup_handle() 0 85 20
B process_response() 0 27 4
A stream_headers() 0 15 3
B stream_body() 0 28 5
B format_get() 0 22 4
B 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
		do {
218
			$active = false;
219
220
			do {
221
				$status = curl_multi_exec($multihandle, $active);
222
			}
223
			while ($status === CURLM_CALL_MULTI_PERFORM);
224
225
			$to_process = array();
226
227
			// Read the information as needed
228
			while ($done = curl_multi_info_read($multihandle)) {
229
				$key = array_search($done['handle'], $subhandles, true);
230
				if (!isset($to_process[$key])) {
231
					$to_process[$key] = $done;
232
				}
233
			}
234
235
			// Parse the finished requests before we start getting the new ones
236
			foreach ($to_process as $key => $done) {
237
				$options = $requests[$key]['options'];
238
				if (CURLE_OK !== $done['result']) {
239
					//get error string for handle.
240
					$reason = curl_error($done['handle']);
241
					$exception = new Requests_Exception_Transport_cURL(
242
									$reason,
243
									Requests_Exception_Transport_cURL::EASY,
244
									$done['handle'],
245
									$done['result']
246
								);
247
					$responses[$key] = $exception;
248
					$options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key]));
249
				}
250
				else {
251
					$responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
252
253
					$options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key]));
254
				}
255
256
				curl_multi_remove_handle($multihandle, $done['handle']);
257
				curl_close($done['handle']);
258
259 View Code Duplication
				if (!is_string($responses[$key])) {
260
					$options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
261
				}
262
				$completed++;
263
			}
264
		}
265
		while ($active || $completed < count($subrequests));
266
267
		$request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
268
269
		curl_multi_close($multihandle);
270
271
		return $responses;
272
	}
273
274
	/**
275
	 * Get the cURL handle for use in a multi-request
276
	 *
277
	 * @param string $url URL to request
278
	 * @param array $headers Associative array of request headers
279
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
280
	 * @param array $options Request options, see {@see Requests::response()} for documentation
281
	 * @return resource Subrequest's cURL handle
282
	 */
283
	public function &get_subrequest_handle($url, $headers, $data, $options) {
284
		$this->setup_handle($url, $headers, $data, $options);
285
286 View Code Duplication
		if ($options['filename'] !== false) {
287
			$this->stream_handle = fopen($options['filename'], 'wb');
288
		}
289
290
		$this->response_data = '';
291
		$this->response_bytes = 0;
292
		$this->response_byte_limit = false;
293
		if ($options['max_bytes'] !== false) {
294
			$this->response_byte_limit = $options['max_bytes'];
295
		}
296
		$this->hooks = $options['hooks'];
297
298
		return $this->handle;
299
	}
300
301
	/**
302
	 * Setup the cURL handle for the given data
303
	 *
304
	 * @param string $url URL to request
305
	 * @param array $headers Associative array of request headers
306
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
307
	 * @param array $options Request options, see {@see Requests::response()} for documentation
308
	 */
309
	protected function setup_handle($url, $headers, $data, $options) {
310
		$options['hooks']->dispatch('curl.before_request', array(&$this->handle));
311
312
		// Force closing the connection for old versions of cURL (<7.22).
313
		if ( ! isset( $headers['Connection'] ) ) {
314
			$headers['Connection'] = 'close';
315
		}
316
317
		$headers = Requests::flatten($headers);
318
319
		if (!empty($data)) {
320
			$data_format = $options['data_format'];
321
322
			if ($data_format === 'query') {
323
				$url = self::format_get($url, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 309 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...
324
				$data = '';
325
			}
326
			elseif (!is_string($data)) {
327
				$data = http_build_query($data, null, '&');
328
			}
329
		}
330
331
		switch ($options['type']) {
332
			case Requests::POST:
333
				curl_setopt($this->handle, CURLOPT_POST, true);
334
				curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
335
				break;
336
			case Requests::HEAD:
337
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
338
				curl_setopt($this->handle, CURLOPT_NOBODY, true);
339
				break;
340
			case Requests::TRACE:
341
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
342
				break;
343
			case Requests::PATCH:
344
			case Requests::PUT:
345
			case Requests::DELETE:
346
			case Requests::OPTIONS:
347
			default:
348
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
349
				if (!empty($data)) {
350
					curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
351
				}
352
		}
353
354
		// cURL requires a minimum timeout of 1 second when using the system
355
		// DNS resolver, as it uses `alarm()`, which is second resolution only.
356
		// There's no way to detect which DNS resolver is being used from our
357
		// end, so we need to round up regardless of the supplied timeout.
358
		//
359
		// https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609
360
		$timeout = max($options['timeout'], 1);
361
362
		if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
363
			curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
364
		}
365
		else {
366
			curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
367
		}
368
369
		if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
370
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
371
		}
372
		else {
373
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
374
		}
375
		curl_setopt($this->handle, CURLOPT_URL, $url);
376
		curl_setopt($this->handle, CURLOPT_REFERER, $url);
377
		curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
378
		if (!empty($headers)) {
379
			curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
380
		}
381
		if ($options['protocol_version'] === 1.1) {
382
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
383
		}
384
		else {
385
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
386
		}
387
388
		if (true === $options['blocking']) {
389
			curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
390
			curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
391
			curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
392
		}
393
	}
394
395
	/**
396
	 * Process a response
397
	 *
398
	 * @param string $response Response data from the body
399
	 * @param array $options Request options
400
	 * @return string HTTP response data including headers
401
	 */
402
	public function process_response($response, $options) {
403
		if ($options['blocking'] === false) {
404
			$fake_headers = '';
405
			$options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
406
			return false;
407
		}
408
		if ($options['filename'] !== false) {
409
			fclose($this->stream_handle);
410
			$this->headers = trim($this->headers);
411
		}
412
		else {
413
			$this->headers .= $response;
414
		}
415
416
		if (curl_errno($this->handle)) {
417
			$error = sprintf(
418
				'cURL error %s: %s',
419
				curl_errno($this->handle),
420
				curl_error($this->handle)
421
			);
422
			throw new Requests_Exception($error, 'curlerror', $this->handle);
423
		}
424
		$this->info = curl_getinfo($this->handle);
0 ignored issues
show
Documentation Bug introduced by
It seems like curl_getinfo($this->handle) of type * is incompatible with the declared type array of property $info.

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...
425
426
		$options['hooks']->dispatch('curl.after_request', array(&$this->headers, &$this->info));
427
		return $this->headers;
428
	}
429
430
	/**
431
	 * Collect the headers as they are received
432
	 *
433
	 * @param resource $handle cURL resource
434
	 * @param string $headers Header string
435
	 * @return integer Length of provided header
436
	 */
437
	public function stream_headers($handle, $headers) {
438
		// Why do we do this? cURL will send both the final response and any
439
		// interim responses, such as a 100 Continue. We don't need that.
440
		// (We may want to keep this somewhere just in case)
441
		if ($this->done_headers) {
442
			$this->headers = '';
443
			$this->done_headers = false;
444
		}
445
		$this->headers .= $headers;
446
447
		if ($headers === "\r\n") {
448
			$this->done_headers = true;
449
		}
450
		return strlen($headers);
451
	}
452
453
	/**
454
	 * Collect data as it's received
455
	 *
456
	 * @since 1.6.1
457
	 *
458
	 * @param resource $handle cURL resource
459
	 * @param string $data Body data
460
	 * @return integer Length of provided data
461
	 */
462
	public function stream_body($handle, $data) {
463
		$this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit));
464
		$data_length = strlen($data);
465
466
		// Are we limiting the response size?
467
		if ($this->response_byte_limit) {
468
			if ($this->response_bytes === $this->response_byte_limit) {
469
				// Already at maximum, move on
470
				return $data_length;
471
			}
472
473
			if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
474
				// Limit the length
475
				$limited_length = ($this->response_byte_limit - $this->response_bytes);
476
				$data = substr($data, 0, $limited_length);
477
			}
478
		}
479
480
		if ($this->stream_handle) {
481
			fwrite($this->stream_handle, $data);
482
		}
483
		else {
484
			$this->response_data .= $data;
485
		}
486
487
		$this->response_bytes += strlen($data);
488
		return $data_length;
489
	}
490
491
	/**
492
	 * Format a URL given GET data
493
	 *
494
	 * @param string $url
495
	 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
496
	 * @return string URL with data
497
	 */
498
	protected static function format_get($url, $data) {
499
		if (!empty($data)) {
500
			$url_parts = parse_url($url);
501
			if (empty($url_parts['query'])) {
502
				$query = $url_parts['query'] = '';
503
			}
504
			else {
505
				$query = $url_parts['query'];
506
			}
507
508
			$query .= '&' . http_build_query($data, null, '&');
509
			$query = trim($query, '&');
510
511
			if (empty($url_parts['query'])) {
512
				$url .= '?' . $query;
513
			}
514
			else {
515
				$url = str_replace($url_parts['query'], $query, $url);
516
			}
517
		}
518
		return $url;
519
	}
520
521
	/**
522
	 * Whether this transport is valid
523
	 *
524
	 * @codeCoverageIgnore
525
	 * @return boolean True if the transport is valid, false otherwise.
526
	 */
527
	public static function test($capabilities = array()) {
528
		if (!function_exists('curl_init') || !function_exists('curl_exec')) {
529
			return false;
530
		}
531
532
		// If needed, check that our installed curl version supports SSL
533
		if (isset($capabilities['ssl']) && $capabilities['ssl']) {
534
			$curl_version = curl_version();
535
			if (!(CURL_VERSION_SSL & $curl_version['features'])) {
536
				return false;
537
			}
538
		}
539
540
		return true;
541
	}
542
}
543