WP_Http_Curl::test()   B
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 9
nc 7
nop 1
dl 0
loc 23
rs 8.5906
c 0
b 0
f 0
1
<?php
2
/**
3
 * HTTP API: WP_Http_Curl class
4
 *
5
 * @package WordPress
6
 * @subpackage HTTP
7
 * @since 4.4.0
8
 */
9
10
/**
11
 * Core class used to integrate Curl as an HTTP transport.
12
 *
13
 * HTTP request method uses Curl extension to retrieve the url.
14
 *
15
 * Requires the Curl extension to be installed.
16
 *
17
 * @since 2.7.0
18
 */
19
class WP_Http_Curl {
20
21
	/**
22
	 * Temporary header storage for during requests.
23
	 *
24
	 * @since 3.2.0
25
	 * @access private
26
	 * @var string
27
	 */
28
	private $headers = '';
29
30
	/**
31
	 * Temporary body storage for during requests.
32
	 *
33
	 * @since 3.6.0
34
	 * @access private
35
	 * @var string
36
	 */
37
	private $body = '';
38
39
	/**
40
	 * The maximum amount of data to receive from the remote server.
41
	 *
42
	 * @since 3.6.0
43
	 * @access private
44
	 * @var int
45
	 */
46
	private $max_body_length = false;
47
48
	/**
49
	 * The file resource used for streaming to file.
50
	 *
51
	 * @since 3.6.0
52
	 * @access private
53
	 * @var resource
54
	 */
55
	private $stream_handle = false;
56
57
	/**
58
	 * The total bytes written in the current request.
59
	 *
60
	 * @since 4.1.0
61
	 * @access private
62
	 * @var int
63
	 */
64
	private $bytes_written_total = 0;
65
66
	/**
67
	 * Send a HTTP request to a URI using cURL extension.
68
	 *
69
	 * @access public
70
	 * @since 2.7.0
71
	 *
72
	 * @param string $url The request URL.
73
	 * @param string|array $args Optional. Override the defaults.
74
	 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error
75
	 */
76
	public function request($url, $args = array()) {
77
		$defaults = array(
78
			'method' => 'GET', 'timeout' => 5,
79
			'redirection' => 5, 'httpversion' => '1.0',
80
			'blocking' => true,
81
			'headers' => array(), 'body' => null, 'cookies' => array()
82
		);
83
84
		$r = wp_parse_args( $args, $defaults );
85
86 View Code Duplication
		if ( isset( $r['headers']['User-Agent'] ) ) {
87
			$r['user-agent'] = $r['headers']['User-Agent'];
88
			unset( $r['headers']['User-Agent'] );
89
		} elseif ( isset( $r['headers']['user-agent'] ) ) {
90
			$r['user-agent'] = $r['headers']['user-agent'];
91
			unset( $r['headers']['user-agent'] );
92
		}
93
94
		// Construct Cookie: header if any cookies are set.
95
		WP_Http::buildCookieHeader( $r );
96
97
		$handle = curl_init();
98
99
		// cURL offers really easy proxy support.
100
		$proxy = new WP_HTTP_Proxy();
101
102
		if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
103
104
			curl_setopt( $handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
105
			curl_setopt( $handle, CURLOPT_PROXY, $proxy->host() );
106
			curl_setopt( $handle, CURLOPT_PROXYPORT, $proxy->port() );
107
108
			if ( $proxy->use_authentication() ) {
109
				curl_setopt( $handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
110
				curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
111
			}
112
		}
113
114
		$is_local = isset($r['local']) && $r['local'];
115
		$ssl_verify = isset($r['sslverify']) && $r['sslverify'];
116 View Code Duplication
		if ( $is_local ) {
117
			/** This filter is documented in wp-includes/class-wp-http-streams.php */
118
			$ssl_verify = apply_filters( 'https_local_ssl_verify', $ssl_verify );
119
		} elseif ( ! $is_local ) {
120
			/** This filter is documented in wp-includes/class-wp-http-streams.php */
121
			$ssl_verify = apply_filters( 'https_ssl_verify', $ssl_verify );
122
		}
123
124
		/*
125
		 * CURLOPT_TIMEOUT and CURLOPT_CONNECTTIMEOUT expect integers. Have to use ceil since.
126
		 * a value of 0 will allow an unlimited timeout.
127
		 */
128
		$timeout = (int) ceil( $r['timeout'] );
129
		curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, $timeout );
130
		curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout );
131
132
		curl_setopt( $handle, CURLOPT_URL, $url);
133
		curl_setopt( $handle, CURLOPT_RETURNTRANSFER, true );
134
		curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, ( $ssl_verify === true ) ? 2 : false );
135
		curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, $ssl_verify );
136
137
		if ( $ssl_verify ) {
138
			curl_setopt( $handle, CURLOPT_CAINFO, $r['sslcertificates'] );
139
		}
140
141
		curl_setopt( $handle, CURLOPT_USERAGENT, $r['user-agent'] );
142
143
		/*
144
		 * The option doesn't work with safe mode or when open_basedir is set, and there's
145
		 * a bug #17490 with redirected POST requests, so handle redirections outside Curl.
146
		 */
147
		curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, false );
148
		if ( defined( 'CURLOPT_PROTOCOLS' ) ) // PHP 5.2.10 / cURL 7.19.4
149
			curl_setopt( $handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS );
150
151
		switch ( $r['method'] ) {
152
			case 'HEAD':
153
				curl_setopt( $handle, CURLOPT_NOBODY, true );
154
				break;
155 View Code Duplication
			case 'POST':
156
				curl_setopt( $handle, CURLOPT_POST, true );
157
				curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] );
158
				break;
159 View Code Duplication
			case 'PUT':
160
				curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, 'PUT' );
161
				curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] );
162
				break;
163
			default:
164
				curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, $r['method'] );
165
				if ( ! is_null( $r['body'] ) )
166
					curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] );
167
				break;
168
		}
169
170
		if ( true === $r['blocking'] ) {
171
			curl_setopt( $handle, CURLOPT_HEADERFUNCTION, array( $this, 'stream_headers' ) );
172
			curl_setopt( $handle, CURLOPT_WRITEFUNCTION, array( $this, 'stream_body' ) );
173
		}
174
175
		curl_setopt( $handle, CURLOPT_HEADER, false );
176
177
		if ( isset( $r['limit_response_size'] ) )
178
			$this->max_body_length = intval( $r['limit_response_size'] );
179
		else
180
			$this->max_body_length = false;
0 ignored issues
show
Documentation Bug introduced by
The property $max_body_length was declared of type integer, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
181
182
		// If streaming to a file open a file handle, and setup our curl streaming handler.
183
		if ( $r['stream'] ) {
184
			if ( ! WP_DEBUG )
185
				$this->stream_handle = @fopen( $r['filename'], 'w+' );
186
			else
187
				$this->stream_handle = fopen( $r['filename'], 'w+' );
188
			if ( ! $this->stream_handle )
189
				return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) );
190
		} else {
191
			$this->stream_handle = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type resource of property $stream_handle.

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...
192
		}
193
194
		if ( !empty( $r['headers'] ) ) {
195
			// cURL expects full header strings in each element.
196
			$headers = array();
197
			foreach ( $r['headers'] as $name => $value ) {
198
				$headers[] = "{$name}: $value";
199
			}
200
			curl_setopt( $handle, CURLOPT_HTTPHEADER, $headers );
201
		}
202
203
		if ( $r['httpversion'] == '1.0' )
204
			curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 );
205
		else
206
			curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
207
208
		/**
209
		 * Fires before the cURL request is executed.
210
		 *
211
		 * Cookies are not currently handled by the HTTP API. This action allows
212
		 * plugins to handle cookies themselves.
213
		 *
214
		 * @since 2.8.0
215
		 *
216
		 * @param resource &$handle The cURL handle returned by curl_init().
217
		 * @param array    $r       The HTTP request arguments.
218
		 * @param string   $url     The request URL.
219
		 */
220
		do_action_ref_array( 'http_api_curl', array( &$handle, $r, $url ) );
221
222
		// We don't need to return the body, so don't. Just execute request and return.
223
		if ( ! $r['blocking'] ) {
224
			curl_exec( $handle );
225
226
			if ( $curl_error = curl_error( $handle ) ) {
227
				curl_close( $handle );
228
				return new WP_Error( 'http_request_failed', $curl_error );
229
			}
230 View Code Duplication
			if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ) ) ) {
231
				curl_close( $handle );
232
				return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) );
233
			}
234
235
			curl_close( $handle );
236
			return array( 'headers' => array(), 'body' => '', 'response' => array('code' => false, 'message' => false), 'cookies' => array() );
237
		}
238
239
		curl_exec( $handle );
240
		$theHeaders = WP_Http::processHeaders( $this->headers, $url );
241
		$theBody = $this->body;
242
		$bytes_written_total = $this->bytes_written_total;
243
244
		$this->headers = '';
245
		$this->body = '';
246
		$this->bytes_written_total = 0;
247
248
		$curl_error = curl_errno( $handle );
249
250
		// If an error occurred, or, no response.
251
		if ( $curl_error || ( 0 == strlen( $theBody ) && empty( $theHeaders['headers'] ) ) ) {
252
			if ( CURLE_WRITE_ERROR /* 23 */ == $curl_error ) {
253
				if ( ! $this->max_body_length || $this->max_body_length != $bytes_written_total ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->max_body_length of type integer|false is loosely compared to false; 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...
254
					if ( $r['stream'] ) {
255
						curl_close( $handle );
256
						fclose( $this->stream_handle );
257
						return new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) );
258
					} else {
259
						curl_close( $handle );
260
						return new WP_Error( 'http_request_failed', curl_error( $handle ) );
261
					}
262
				}
263
			} else {
264
				if ( $curl_error = curl_error( $handle ) ) {
265
					curl_close( $handle );
266
					return new WP_Error( 'http_request_failed', $curl_error );
267
				}
268
			}
269 View Code Duplication
			if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ) ) ) {
270
				curl_close( $handle );
271
				return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) );
272
			}
273
		}
274
275
		curl_close( $handle );
276
277
		if ( $r['stream'] )
278
			fclose( $this->stream_handle );
279
280
		$response = array(
281
			'headers' => $theHeaders['headers'],
282
			'body' => null,
283
			'response' => $theHeaders['response'],
284
			'cookies' => $theHeaders['cookies'],
285
			'filename' => $r['filename']
286
		);
287
288
		// Handle redirects.
289
		if ( false !== ( $redirect_response = WP_HTTP::handle_redirects( $url, $r, $response ) ) )
290
			return $redirect_response;
291
292 View Code Duplication
		if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($theHeaders['headers']) )
293
			$theBody = WP_Http_Encoding::decompress( $theBody );
294
295
		$response['body'] = $theBody;
296
297
		return $response;
298
	}
299
300
	/**
301
	 * Grabs the headers of the cURL request.
302
	 *
303
	 * Each header is sent individually to this callback, so we append to the `$header` property
304
	 * for temporary storage
305
	 *
306
	 * @since 3.2.0
307
	 * @access private
308
	 *
309
	 * @param resource $handle  cURL handle.
310
	 * @param string   $headers cURL request headers.
311
	 * @return int Length of the request headers.
312
	 */
313
	private function stream_headers( $handle, $headers ) {
314
		$this->headers .= $headers;
315
		return strlen( $headers );
316
	}
317
318
	/**
319
	 * Grabs the body of the cURL request.
320
	 *
321
	 * The contents of the document are passed in chunks, so we append to the `$body`
322
	 * property for temporary storage. Returning a length shorter than the length of
323
	 * `$data` passed in will cause cURL to abort the request with `CURLE_WRITE_ERROR`.
324
	 *
325
	 * @since 3.6.0
326
	 * @access private
327
	 *
328
	 * @param resource $handle  cURL handle.
329
	 * @param string   $data    cURL request body.
330
	 * @return int Total bytes of data written.
331
	 */
332
	private function stream_body( $handle, $data ) {
333
		$data_length = strlen( $data );
334
335
		if ( $this->max_body_length && ( $this->bytes_written_total + $data_length ) > $this->max_body_length ) {
336
			$data_length = ( $this->max_body_length - $this->bytes_written_total );
337
			$data = substr( $data, 0, $data_length );
338
		}
339
340
		if ( $this->stream_handle ) {
341
			$bytes_written = fwrite( $this->stream_handle, $data );
342
		} else {
343
			$this->body .= $data;
344
			$bytes_written = $data_length;
345
		}
346
347
		$this->bytes_written_total += $bytes_written;
348
349
		// Upon event of this function returning less than strlen( $data ) curl will error with CURLE_WRITE_ERROR.
350
		return $bytes_written;
351
	}
352
353
	/**
354
	 * Determines whether this class can be used for retrieving a URL.
355
	 *
356
	 * @static
357
	 * @since 2.7.0
358
	 *
359
	 * @param array $args Optional. Array of request arguments. Default empty array.
360
	 * @return bool False means this class can not be used, true means it can.
361
	 */
362
	public static function test( $args = array() ) {
363
		if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) )
364
			return false;
365
366
		$is_ssl = isset( $args['ssl'] ) && $args['ssl'];
367
368
		if ( $is_ssl ) {
369
			$curl_version = curl_version();
370
			// Check whether this cURL version support SSL requests.
371
			if ( ! (CURL_VERSION_SSL & $curl_version['features']) )
372
				return false;
373
		}
374
375
		/**
376
		 * Filters whether cURL can be used as a transport for retrieving a URL.
377
		 *
378
		 * @since 2.7.0
379
		 *
380
		 * @param bool  $use_class Whether the class can be used. Default true.
381
		 * @param array $args      An array of request arguments.
382
		 */
383
		return apply_filters( 'use_curl_transport', true, $args );
384
	}
385
}
386