Completed
Pull Request — master (#337)
by
unknown
01:22
created

Requests_Transport_cURL::test()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 4
nop 1
dl 0
loc 15
rs 9.2222
c 0
b 0
f 0
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
		$curl_errno = curl_errno($this->handle);
168
169 View Code Duplication
		if ($curl_errno === 23 && $this->response_byte_limit && $this->response_byte_limit === $this->response_bytes) {
170
			// CURLE_WRITE_ERROR - Not actually an error in this case. We've drained as much data from the request that we want.
171
			$curl_errno = false;
172
		}
173
174
		if ($curl_errno === 23 || $curl_errno === 61) {
175
			// Reset encoding and try again
176
			curl_setopt($this->handle, CURLOPT_ENCODING, 'none');
177
178
			$this->response_data = '';
179
			$this->response_bytes = 0;
180
			curl_exec($this->handle);
181
			$response = $this->response_data;
182
		}
183
184
		$this->process_response($response, $options);
185
186
		// Need to remove the $this reference from the curl handle.
187
		// Otherwise Requests_Transport_cURL wont be garbage collected and the curl_close() will never be called.
188
		curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null);
189
		curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null);
190
191
		return $this->headers;
192
	}
193
194
	/**
195
	 * Send multiple requests simultaneously
196
	 *
197
	 * @param array $requests Request data
198
	 * @param array $options Global options
199
	 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
200
	 */
201
	public function request_multiple($requests, $options) {
202
		// If you're not requesting, we can't get any responses ¯\_(ツ)_/¯
203
		if (empty($requests)) {
204
			return array();
205
		}
206
207
		$multihandle = curl_multi_init();
208
		$subrequests = array();
209
		$subhandles = array();
210
211
		$class = get_class($this);
212
		foreach ($requests as $id => $request) {
213
			$subrequests[$id] = new $class();
214
			$subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
215
			$request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
216
			curl_multi_add_handle($multihandle, $subhandles[$id]);
217
		}
218
219
		$completed = 0;
220
		$responses = array();
221
222
		$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 212. 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...
223
224
		do {
225
			$active = false;
226
227
			do {
228
				$status = curl_multi_exec($multihandle, $active);
229
			}
230
			while ($status === CURLM_CALL_MULTI_PERFORM);
231
232
			$to_process = array();
233
234
			// Read the information as needed
235
			while ($done = curl_multi_info_read($multihandle)) {
236
				$key = array_search($done['handle'], $subhandles, true);
237
				if (!isset($to_process[$key])) {
238
					$to_process[$key] = $done;
239
				}
240
			}
241
242
			// Parse the finished requests before we start getting the new ones
243
			foreach ($to_process as $key => $done) {
244
				$options = $requests[$key]['options'];
245
				if (CURLE_OK !== $done['result']) {
246
					//get error string for handle.
247
					$reason = curl_error($done['handle']);
248
					$exception = new Requests_Exception_Transport_cURL(
249
									$reason,
250
									Requests_Exception_Transport_cURL::EASY,
251
									$done['handle'],
252
									$done['result']
253
								);
254
					$responses[$key] = $exception;
255
					$options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key]));
256
				}
257
				else {
258
					$responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
259
260
					$options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key]));
261
				}
262
263
				curl_multi_remove_handle($multihandle, $done['handle']);
264
				curl_close($done['handle']);
265
266 View Code Duplication
				if (!is_string($responses[$key])) {
267
					$options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
268
				}
269
				$completed++;
270
			}
271
		}
272
		while ($active || $completed < count($subrequests));
273
274
		$request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
275
276
		curl_multi_close($multihandle);
277
278
		return $responses;
279
	}
280
281
	/**
282
	 * Get the cURL handle for use in a multi-request
283
	 *
284
	 * @param string $url URL to request
285
	 * @param array $headers Associative array of request headers
286
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
287
	 * @param array $options Request options, see {@see Requests::response()} for documentation
288
	 * @return resource Subrequest's cURL handle
289
	 */
290
	public function &get_subrequest_handle($url, $headers, $data, $options) {
291
		$this->setup_handle($url, $headers, $data, $options);
292
293 View Code Duplication
		if ($options['filename'] !== false) {
294
			$this->stream_handle = fopen($options['filename'], 'wb');
295
		}
296
297
		$this->response_data = '';
298
		$this->response_bytes = 0;
299
		$this->response_byte_limit = false;
300
		if ($options['max_bytes'] !== false) {
301
			$this->response_byte_limit = $options['max_bytes'];
302
		}
303
		$this->hooks = $options['hooks'];
304
305
		return $this->handle;
306
	}
307
308
	/**
309
	 * Setup the cURL handle for the given data
310
	 *
311
	 * @param string $url URL to request
312
	 * @param array $headers Associative array of request headers
313
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
314
	 * @param array $options Request options, see {@see Requests::response()} for documentation
315
	 */
316
	protected function setup_handle($url, $headers, $data, $options) {
317
		$options['hooks']->dispatch('curl.before_request', array(&$this->handle));
318
319
		// Force closing the connection for old versions of cURL (<7.22).
320
		if ( ! isset( $headers['Connection'] ) ) {
321
			$headers['Connection'] = 'close';
322
		}
323
324
		$headers = Requests::flatten($headers);
325
326
		if (!empty($data)) {
327
			$data_format = $options['data_format'];
328
329
			if ($data_format === 'query') {
330
				$url = self::format_get($url, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 316 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...
331
				$data = '';
332
			}
333
			elseif (!is_string($data)) {
334
				$data = http_build_query($data, null, '&');
335
			}
336
		}
337
338
		switch ($options['type']) {
339
			case Requests::POST:
340
				curl_setopt($this->handle, CURLOPT_POST, true);
341
				curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
342
				break;
343
			case Requests::HEAD:
344
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
345
				curl_setopt($this->handle, CURLOPT_NOBODY, true);
346
				break;
347
			case Requests::TRACE:
348
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
349
				break;
350
			case Requests::PATCH:
351
			case Requests::PUT:
352
			case Requests::DELETE:
353
			case Requests::OPTIONS:
354
			default:
355
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
356
				if (!empty($data)) {
357
					curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
358
				}
359
		}
360
361
		// cURL requires a minimum timeout of 1 second when using the system
362
		// DNS resolver, as it uses `alarm()`, which is second resolution only.
363
		// There's no way to detect which DNS resolver is being used from our
364
		// end, so we need to round up regardless of the supplied timeout.
365
		//
366
		// https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609
367
		$timeout = max($options['timeout'], 1);
368
369
		if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
370
			curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
371
		}
372
		else {
373
			curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
374
		}
375
376
		if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
377
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
378
		}
379
		else {
380
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
381
		}
382
		curl_setopt($this->handle, CURLOPT_URL, $url);
383
		curl_setopt($this->handle, CURLOPT_REFERER, $url);
384
		curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
385
		if (!empty($headers)) {
386
			curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
387
		}
388
		if ($options['protocol_version'] === 1.1) {
389
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
390
		}
391
		else {
392
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
393
		}
394
395
		if (true === $options['blocking']) {
396
			curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
397
			curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
398
			curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
399
		}
400
	}
401
402
	/**
403
	 * Process a response
404
	 *
405
	 * @param string $response Response data from the body
406
	 * @param array $options Request options
407
	 * @return string HTTP response data including headers
408
	 */
409
	public function process_response($response, $options) {
410
		if ($options['blocking'] === false) {
411
			$fake_headers = '';
412
			$options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
413
			return false;
414
		}
415
		if ($options['filename'] !== false) {
416
			fclose($this->stream_handle);
417
			$this->headers = trim($this->headers);
418
		}
419
		else {
420
			$this->headers .= $response;
421
		}
422
423
		$curl_errno = curl_errno($this->handle);
424 View Code Duplication
		if ($curl_errno === 23 && $this->response_byte_limit && $this->response_byte_limit === $this->response_bytes) {
425
			// CURLE_WRITE_ERROR - Not actually an error in this case. We've drained as much data from the request that we want.
426
			$curl_errno = false;
427
		}
428
429
		if ($curl_errno) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $curl_errno of type false|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

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