Requests_Transport_cURL::stream_headers()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 2
dl 0
loc 15
rs 9.7666
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
	 * cURL version number
42
	 *
43
	 * @var int
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
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_protocolsFound
104
			curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
105
		}
106
		if (defined('CURLOPT_REDIR_PROTOCOLS')) {
107
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_redir_protocolsFound
108
			curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
109
		}
110
	}
111
112
	/**
113
	 * Destructor
114
	 */
115
	public function __destruct() {
116
		if (is_resource($this->handle)) {
117
			curl_close($this->handle);
118
		}
119
	}
120
121
	/**
122
	 * Perform a request
123
	 *
124
	 * @throws Requests_Exception On a cURL error (`curlerror`)
125
	 *
126
	 * @param string $url URL to request
127
	 * @param array $headers Associative array of request headers
128
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
129
	 * @param array $options Request options, see {@see Requests::response()} for documentation
130
	 * @return string Raw HTTP result
131
	 */
132
	public function request($url, $headers = array(), $data = array(), $options = array()) {
133
		$this->hooks = $options['hooks'];
134
135
		$this->setup_handle($url, $headers, $data, $options);
136
137
		$options['hooks']->dispatch('curl.before_send', array(&$this->handle));
138
139 View Code Duplication
		if ($options['filename'] !== false) {
140
			$this->stream_handle = fopen($options['filename'], 'wb');
141
		}
142
143
		$this->response_data       = '';
144
		$this->response_bytes      = 0;
145
		$this->response_byte_limit = false;
146
		if ($options['max_bytes'] !== false) {
147
			$this->response_byte_limit = $options['max_bytes'];
148
		}
149
150
		if (isset($options['verify'])) {
151
			if ($options['verify'] === false) {
152
				curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
153
				curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0);
154
			}
155
			elseif (is_string($options['verify'])) {
156
				curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']);
157
			}
158
		}
159
160
		if (isset($options['verifyname']) && $options['verifyname'] === false) {
161
			curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
162
		}
163
164
		curl_exec($this->handle);
165
		$response = $this->response_data;
166
167
		$options['hooks']->dispatch('curl.after_send', array());
168
169
		if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) {
170
			// Reset encoding and try again
171
			curl_setopt($this->handle, CURLOPT_ENCODING, 'none');
172
173
			$this->response_data  = '';
174
			$this->response_bytes = 0;
175
			curl_exec($this->handle);
176
			$response = $this->response_data;
177
		}
178
179
		$this->process_response($response, $options);
180
181
		// Need to remove the $this reference from the curl handle.
182
		// Otherwise Requests_Transport_cURL wont be garbage collected and the curl_close() will never be called.
183
		curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null);
184
		curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null);
185
186
		return $this->headers;
187
	}
188
189
	/**
190
	 * Send multiple requests simultaneously
191
	 *
192
	 * @param array $requests Request data
193
	 * @param array $options Global options
194
	 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
195
	 */
196
	public function request_multiple($requests, $options) {
197
		// If you're not requesting, we can't get any responses ¯\_(ツ)_/¯
198
		if (empty($requests)) {
199
			return array();
200
		}
201
202
		$multihandle = curl_multi_init();
203
		$subrequests = array();
204
		$subhandles  = array();
205
206
		$class = get_class($this);
207
		foreach ($requests as $id => $request) {
208
			$subrequests[$id] = new $class();
209
			$subhandles[$id]  = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
210
			$request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
211
			curl_multi_add_handle($multihandle, $subhandles[$id]);
212
		}
213
214
		$completed       = 0;
215
		$responses       = array();
216
		$subrequestcount = count($subrequests);
217
218
		$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 207. 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...
219
220
		do {
221
			$active = 0;
222
223
			do {
224
				$status = curl_multi_exec($multihandle, $active);
225
			}
226
			while ($status === CURLM_CALL_MULTI_PERFORM);
227
228
			$to_process = array();
229
230
			// Read the information as needed
231
			while ($done = curl_multi_info_read($multihandle)) {
232
				$key = array_search($done['handle'], $subhandles, true);
233
				if (!isset($to_process[$key])) {
234
					$to_process[$key] = $done;
235
				}
236
			}
237
238
			// Parse the finished requests before we start getting the new ones
239
			foreach ($to_process as $key => $done) {
240
				$options = $requests[$key]['options'];
241
				if ($done['result'] !== CURLE_OK) {
242
					//get error string for handle.
243
					$reason          = curl_error($done['handle']);
244
					$exception       = new Requests_Exception_Transport_cURL(
245
						$reason,
246
						Requests_Exception_Transport_cURL::EASY,
247
						$done['handle'],
248
						$done['result']
249
					);
250
					$responses[$key] = $exception;
251
					$options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key]));
252
				}
253
				else {
254
					$responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
255
256
					$options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key]));
257
				}
258
259
				curl_multi_remove_handle($multihandle, $done['handle']);
260
				curl_close($done['handle']);
261
262 View Code Duplication
				if (!is_string($responses[$key])) {
263
					$options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
264
				}
265
				$completed++;
266
			}
267
		}
268
		while ($active || $completed < $subrequestcount);
269
270
		$request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
271
272
		curl_multi_close($multihandle);
273
274
		return $responses;
275
	}
276
277
	/**
278
	 * Get the cURL handle for use in a multi-request
279
	 *
280
	 * @param string $url URL to request
281
	 * @param array $headers Associative array of request headers
282
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
283
	 * @param array $options Request options, see {@see Requests::response()} for documentation
284
	 * @return resource Subrequest's cURL handle
285
	 */
286
	public function &get_subrequest_handle($url, $headers, $data, $options) {
287
		$this->setup_handle($url, $headers, $data, $options);
288
289 View Code Duplication
		if ($options['filename'] !== false) {
290
			$this->stream_handle = fopen($options['filename'], 'wb');
291
		}
292
293
		$this->response_data       = '';
294
		$this->response_bytes      = 0;
295
		$this->response_byte_limit = false;
296
		if ($options['max_bytes'] !== false) {
297
			$this->response_byte_limit = $options['max_bytes'];
298
		}
299
		$this->hooks = $options['hooks'];
300
301
		return $this->handle;
302
	}
303
304
	/**
305
	 * Setup the cURL handle for the given data
306
	 *
307
	 * @param string $url URL to request
308
	 * @param array $headers Associative array of request headers
309
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
310
	 * @param array $options Request options, see {@see Requests::response()} for documentation
311
	 */
312
	protected function setup_handle($url, $headers, $data, $options) {
313
		$options['hooks']->dispatch('curl.before_request', array(&$this->handle));
314
315
		// Force closing the connection for old versions of cURL (<7.22).
316
		if (!isset($headers['Connection'])) {
317
			$headers['Connection'] = 'close';
318
		}
319
320
		$headers = Requests::flatten($headers);
321
322
		if (!empty($data)) {
323
			$data_format = $options['data_format'];
324
325
			if ($data_format === 'query') {
326
				$url  = self::format_get($url, $data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 312 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...
327
				$data = '';
328
			}
329
			elseif (!is_string($data)) {
330
				$data = http_build_query($data, null, '&');
331
			}
332
		}
333
334
		switch ($options['type']) {
335
			case Requests::POST:
336
				curl_setopt($this->handle, CURLOPT_POST, true);
337
				curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
338
				break;
339
			case Requests::HEAD:
340
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
341
				curl_setopt($this->handle, CURLOPT_NOBODY, true);
342
				break;
343
			case Requests::TRACE:
344
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
345
				break;
346
			case Requests::PATCH:
347
			case Requests::PUT:
348
			case Requests::DELETE:
349
			case Requests::OPTIONS:
350
			default:
351
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
352
				if (!empty($data)) {
353
					curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
354
				}
355
		}
356
357
		// cURL requires a minimum timeout of 1 second when using the system
358
		// DNS resolver, as it uses `alarm()`, which is second resolution only.
359
		// There's no way to detect which DNS resolver is being used from our
360
		// end, so we need to round up regardless of the supplied timeout.
361
		//
362
		// https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609
363
		$timeout = max($options['timeout'], 1);
364
365
		if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
366
			curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
367
		}
368
		else {
369
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound
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
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound
378
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
379
		}
380
		curl_setopt($this->handle, CURLOPT_URL, $url);
381
		curl_setopt($this->handle, CURLOPT_REFERER, $url);
382
		curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
383
		if (!empty($headers)) {
384
			curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
385
		}
386
		if ($options['protocol_version'] === 1.1) {
387
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
388
		}
389
		else {
390
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
391
		}
392
393
		if ($options['blocking'] === true) {
394
			curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array($this, 'stream_headers'));
395
			curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array($this, 'stream_body'));
396
			curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
397
		}
398
	}
399
400
	/**
401
	 * Process a response
402
	 *
403
	 * @param string $response Response data from the body
404
	 * @param array $options Request options
405
	 * @return string|false HTTP response data including headers. False if non-blocking.
406
	 * @throws Requests_Exception
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
			$query     = '';
507
			$url_parts = parse_url($url);
508
			if (empty($url_parts['query'])) {
509
				$url_parts['query'] = '';
510
			}
511
			else {
512
				$query = $url_parts['query'];
513
			}
514
515
			$query .= '&' . http_build_query($data, null, '&');
516
			$query  = trim($query, '&');
517
518
			if (empty($url_parts['query'])) {
519
				$url .= '?' . $query;
520
			}
521
			else {
522
				$url = str_replace($url_parts['query'], $query, $url);
523
			}
524
		}
525
		return $url;
526
	}
527
528
	/**
529
	 * Whether this transport is valid
530
	 *
531
	 * @codeCoverageIgnore
532
	 * @return boolean True if the transport is valid, false otherwise.
533
	 */
534
	public static function test($capabilities = array()) {
535
		if (!function_exists('curl_init') || !function_exists('curl_exec')) {
536
			return false;
537
		}
538
539
		// If needed, check that our installed curl version supports SSL
540
		if (isset($capabilities['ssl']) && $capabilities['ssl']) {
541
			$curl_version = curl_version();
542
			if (!(CURL_VERSION_SSL & $curl_version['features'])) {
543
				return false;
544
			}
545
		}
546
547
		return true;
548
	}
549
}
550