Completed
Pull Request — master (#379)
by
unknown
01:21
created

Requests_Transport_cURL::__destruct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 5
rs 10
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
		if ($options['filename'] !== false) {
138
			@$this->stream_handle = fopen($options['filename'], 'wb');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
139 View Code Duplication
			if($this->stream_handle===false) {
140
				$error = error_get_last();
141
				throw new Requests_Exception($error['message'], 'fopen');
142
			}
143
		}
144
145
		$this->response_data = '';
146
		$this->response_bytes = 0;
147
		$this->response_byte_limit = false;
148
		if ($options['max_bytes'] !== false) {
149
			$this->response_byte_limit = $options['max_bytes'];
150
		}
151
152
		if (isset($options['verify'])) {
153
			if ($options['verify'] === false) {
154
				curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
155
				curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0);
156
			}
157
			elseif (is_string($options['verify'])) {
158
				curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']);
159
			}
160
		}
161
162
		if (isset($options['verifyname']) && $options['verifyname'] === false) {
163
			curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
164
		}
165
166
		curl_exec($this->handle);
167
		$response = $this->response_data;
168
169
		$options['hooks']->dispatch('curl.after_send', array());
170
171
		if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) {
172
			// Reset encoding and try again
173
			curl_setopt($this->handle, CURLOPT_ENCODING, 'none');
174
175
			$this->response_data = '';
176
			$this->response_bytes = 0;
177
			curl_exec($this->handle);
178
			$response = $this->response_data;
179
		}
180
181
		$this->process_response($response, $options);
182
183
		// Need to remove the $this reference from the curl handle.
184
		// Otherwise Requests_Transport_cURL wont be garbage collected and the curl_close() will never be called.
185
		curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null);
186
		curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null);
187
188
		return $this->headers;
189
	}
190
191
	/**
192
	 * Send multiple requests simultaneously
193
	 *
194
	 * @param array $requests Request data
195
	 * @param array $options Global options
196
	 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
197
	 */
198
	public function request_multiple($requests, $options) {
199
		// If you're not requesting, we can't get any responses ¯\_(ツ)_/¯
200
		if (empty($requests)) {
201
			return array();
202
		}
203
204
		$multihandle = curl_multi_init();
205
		$subrequests = array();
206
		$subhandles = array();
207
208
		$class = get_class($this);
209
		foreach ($requests as $id => $request) {
210
			$subrequests[$id] = new $class();
211
			$subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
212
			$request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
213
			curl_multi_add_handle($multihandle, $subhandles[$id]);
214
		}
215
216
		$completed = 0;
217
		$responses = array();
218
219
		$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 209. 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...
220
221
		do {
222
			$active = false;
223
224
			do {
225
				$status = curl_multi_exec($multihandle, $active);
226
			}
227
			while ($status === CURLM_CALL_MULTI_PERFORM);
228
229
			$to_process = array();
230
231
			// Read the information as needed
232
			while ($done = curl_multi_info_read($multihandle)) {
233
				$key = array_search($done['handle'], $subhandles, true);
234
				if (!isset($to_process[$key])) {
235
					$to_process[$key] = $done;
236
				}
237
			}
238
239
			// Parse the finished requests before we start getting the new ones
240
			foreach ($to_process as $key => $done) {
241
				$options = $requests[$key]['options'];
242
				if (CURLE_OK !== $done['result']) {
243
					//get error string for handle.
244
					$reason = curl_error($done['handle']);
245
					$exception = new Requests_Exception_Transport_cURL(
246
									$reason,
247
									Requests_Exception_Transport_cURL::EASY,
248
									$done['handle'],
249
									$done['result']
250
								);
251
					$responses[$key] = $exception;
252
					$options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key]));
253
				}
254
				else {
255
					$responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
256
257
					$options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key]));
258
				}
259
260
				curl_multi_remove_handle($multihandle, $done['handle']);
261
				curl_close($done['handle']);
262
263 View Code Duplication
				if (!is_string($responses[$key])) {
264
					$options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
265
				}
266
				$completed++;
267
			}
268
		}
269
		while ($active || $completed < count($subrequests));
270
271
		$request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
272
273
		curl_multi_close($multihandle);
274
275
		return $responses;
276
	}
277
278
	/**
279
	 * Get the cURL handle for use in a multi-request
280
	 *
281
	 * @param string $url URL to request
282
	 * @param array $headers Associative array of request headers
283
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
284
	 * @param array $options Request options, see {@see Requests::response()} for documentation
285
	 * @return resource Subrequest's cURL handle
286
	 */
287
	public function &get_subrequest_handle($url, $headers, $data, $options) {
288
		$this->setup_handle($url, $headers, $data, $options);
289
290
		if ($options['filename'] !== false) {
291
			$this->stream_handle = fopen($options['filename'], 'wb');
292
		}
293
294
		$this->response_data = '';
295
		$this->response_bytes = 0;
296
		$this->response_byte_limit = false;
297
		if ($options['max_bytes'] !== false) {
298
			$this->response_byte_limit = $options['max_bytes'];
299
		}
300
		$this->hooks = $options['hooks'];
301
302
		return $this->handle;
303
	}
304
305
	/**
306
	 * Setup the cURL handle for the given data
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
	 */
313
	protected function setup_handle($url, $headers, $data, $options) {
314
		$options['hooks']->dispatch('curl.before_request', array(&$this->handle));
315
316
		// Force closing the connection for old versions of cURL (<7.22).
317
		if ( ! isset( $headers['Connection'] ) ) {
318
			$headers['Connection'] = 'close';
319
		}
320
321
		$headers = Requests::flatten($headers);
322
323
		if (!empty($data)) {
324
			$data_format = $options['data_format'];
325
326
			if ($data_format === 'query') {
327
				$url = self::format_get($url, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 313 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...
328
				$data = '';
329
			}
330
			elseif (!is_string($data)) {
331
				$data = http_build_query($data, null, '&');
332
			}
333
		}
334
335
		switch ($options['type']) {
336
			case Requests::POST:
337
				curl_setopt($this->handle, CURLOPT_POST, true);
338
				curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
339
				break;
340
			case Requests::HEAD:
341
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
342
				curl_setopt($this->handle, CURLOPT_NOBODY, true);
343
				break;
344
			case Requests::TRACE:
345
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
346
				break;
347
			case Requests::PATCH:
348
			case Requests::PUT:
349
			case Requests::DELETE:
350
			case Requests::OPTIONS:
351
			default:
352
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
353
				if (!empty($data)) {
354
					curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
355
				}
356
		}
357
358
		// cURL requires a minimum timeout of 1 second when using the system
359
		// DNS resolver, as it uses `alarm()`, which is second resolution only.
360
		// There's no way to detect which DNS resolver is being used from our
361
		// end, so we need to round up regardless of the supplied timeout.
362
		//
363
		// https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609
364
		$timeout = max($options['timeout'], 1);
365
366
		if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
367
			curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
368
		}
369
		else {
370
			curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
371
		}
372
373
		if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
374
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
375
		}
376
		else {
377
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
378
		}
379
		curl_setopt($this->handle, CURLOPT_URL, $url);
380
		curl_setopt($this->handle, CURLOPT_REFERER, $url);
381
		curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
382
		if (!empty($headers)) {
383
			curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
384
		}
385
		if ($options['protocol_version'] === 1.1) {
386
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
387
		}
388
		else {
389
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
390
		}
391
392
		if (true === $options['blocking']) {
393
			curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
394
			curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
395
			curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
396
		}
397
	}
398
399
	/**
400
	 * Process a response
401
	 *
402
	 * @param string $response Response data from the body
403
	 * @param array $options Request options
404
	 * @return string HTTP response data including headers
405
	 */
406
	public function process_response($response, $options) {
407
		if ($options['blocking'] === false) {
408
			$fake_headers = '';
409
			$options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
410
			return false;
411
		}
412
		if ($options['filename'] !== false) {
413
			fclose($this->stream_handle);
414
			$this->headers = trim($this->headers);
415
		}
416
		else {
417
			$this->headers .= $response;
418
		}
419
420
		if (curl_errno($this->handle)) {
421
			$error = sprintf(
422
				'cURL error %s: %s',
423
				curl_errno($this->handle),
424
				curl_error($this->handle)
425
			);
426
			throw new Requests_Exception($error, 'curlerror', $this->handle);
427
		}
428
		$this->info = curl_getinfo($this->handle);
429
430
		$options['hooks']->dispatch('curl.after_request', array(&$this->headers, &$this->info));
431
		return $this->headers;
432
	}
433
434
	/**
435
	 * Collect the headers as they are received
436
	 *
437
	 * @param resource $handle cURL resource
438
	 * @param string $headers Header string
439
	 * @return integer Length of provided header
440
	 */
441
	public function stream_headers($handle, $headers) {
442
		// Why do we do this? cURL will send both the final response and any
443
		// interim responses, such as a 100 Continue. We don't need that.
444
		// (We may want to keep this somewhere just in case)
445
		if ($this->done_headers) {
446
			$this->headers = '';
447
			$this->done_headers = false;
448
		}
449
		$this->headers .= $headers;
450
451
		if ($headers === "\r\n") {
452
			$this->done_headers = true;
453
		}
454
		return strlen($headers);
455
	}
456
457
	/**
458
	 * Collect data as it's received
459
	 *
460
	 * @since 1.6.1
461
	 *
462
	 * @param resource $handle cURL resource
463
	 * @param string $data Body data
464
	 * @return integer Length of provided data
465
	 */
466
	public function stream_body($handle, $data) {
467
		$this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit));
468
		$data_length = strlen($data);
469
470
		// Are we limiting the response size?
471
		if ($this->response_byte_limit) {
472
			if ($this->response_bytes === $this->response_byte_limit) {
473
				// Already at maximum, move on
474
				return $data_length;
475
			}
476
477
			if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
478
				// Limit the length
479
				$limited_length = ($this->response_byte_limit - $this->response_bytes);
480
				$data = substr($data, 0, $limited_length);
481
			}
482
		}
483
484
		if ($this->stream_handle) {
485
			fwrite($this->stream_handle, $data);
486
		}
487
		else {
488
			$this->response_data .= $data;
489
		}
490
491
		$this->response_bytes += strlen($data);
492
		return $data_length;
493
	}
494
495
	/**
496
	 * Format a URL given GET data
497
	 *
498
	 * @param string $url
499
	 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
500
	 * @return string URL with data
501
	 */
502
	protected static function format_get($url, $data) {
503
		if (!empty($data)) {
504
			$url_parts = parse_url($url);
505
			if (empty($url_parts['query'])) {
506
				$query = $url_parts['query'] = '';
507
			}
508
			else {
509
				$query = $url_parts['query'];
510
			}
511
512
			$query .= '&' . http_build_query($data, null, '&');
513
			$query = trim($query, '&');
514
515
			if (empty($url_parts['query'])) {
516
				$url .= '?' . $query;
517
			}
518
			else {
519
				$url = str_replace($url_parts['query'], $query, $url);
520
			}
521
		}
522
		return $url;
523
	}
524
525
	/**
526
	 * Whether this transport is valid
527
	 *
528
	 * @codeCoverageIgnore
529
	 * @return boolean True if the transport is valid, false otherwise.
530
	 */
531
	public static function test($capabilities = array()) {
532
		if (!function_exists('curl_init') || !function_exists('curl_exec')) {
533
			return false;
534
		}
535
536
		// If needed, check that our installed curl version supports SSL
537
		if (isset($capabilities['ssl']) && $capabilities['ssl']) {
538
			$curl_version = curl_version();
539
			if (!(CURL_VERSION_SSL & $curl_version['features'])) {
540
				return false;
541
			}
542
		}
543
544
		return true;
545
	}
546
}
547