Completed
Pull Request — master (#375)
by
unknown
01:32
created

Requests_Transport_cURL::format_get()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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