|
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 * @var string |
|
26 */ |
|
27 private $headers = ''; |
|
28 |
|
29 /** |
|
30 * Temporary body storage for during requests. |
|
31 * |
|
32 * @since 3.6.0 |
|
33 * @var string |
|
34 */ |
|
35 private $body = ''; |
|
36 |
|
37 /** |
|
38 * The maximum amount of data to receive from the remote server. |
|
39 * |
|
40 * @since 3.6.0 |
|
41 * @var int |
|
42 */ |
|
43 private $max_body_length = false; |
|
44 |
|
45 /** |
|
46 * The file resource used for streaming to file. |
|
47 * |
|
48 * @since 3.6.0 |
|
49 * @var resource |
|
50 */ |
|
51 private $stream_handle = false; |
|
52 |
|
53 /** |
|
54 * The total bytes written in the current request. |
|
55 * |
|
56 * @since 4.1.0 |
|
57 * @var int |
|
58 */ |
|
59 private $bytes_written_total = 0; |
|
60 |
|
61 /** |
|
62 * Send a HTTP request to a URI using cURL extension. |
|
63 * |
|
64 * @since 2.7.0 |
|
65 * |
|
66 * @param string $url The request URL. |
|
67 * @param string|array $args Optional. Override the defaults. |
|
68 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error |
|
69 */ |
|
70 public function request($url, $args = array()) { |
|
71 $defaults = array( |
|
72 'method' => 'GET', 'timeout' => 5, |
|
73 'redirection' => 5, 'httpversion' => '1.0', |
|
74 'blocking' => true, |
|
75 'headers' => array(), 'body' => null, 'cookies' => array() |
|
76 ); |
|
77 |
|
78 $r = wp_parse_args( $args, $defaults ); |
|
79 |
|
80 if ( isset( $r['headers']['User-Agent'] ) ) { |
|
81 $r['user-agent'] = $r['headers']['User-Agent']; |
|
82 unset( $r['headers']['User-Agent'] ); |
|
83 } elseif ( isset( $r['headers']['user-agent'] ) ) { |
|
84 $r['user-agent'] = $r['headers']['user-agent']; |
|
85 unset( $r['headers']['user-agent'] ); |
|
86 } |
|
87 |
|
88 // Construct Cookie: header if any cookies are set. |
|
89 WP_Http::buildCookieHeader( $r ); |
|
90 |
|
91 $handle = curl_init(); |
|
92 |
|
93 // cURL offers really easy proxy support. |
|
94 $proxy = new WP_HTTP_Proxy(); |
|
95 |
|
96 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { |
|
97 |
|
98 curl_setopt( $handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP ); |
|
99 curl_setopt( $handle, CURLOPT_PROXY, $proxy->host() ); |
|
100 curl_setopt( $handle, CURLOPT_PROXYPORT, $proxy->port() ); |
|
101 |
|
102 if ( $proxy->use_authentication() ) { |
|
103 curl_setopt( $handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY ); |
|
104 curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $proxy->authentication() ); |
|
105 } |
|
106 } |
|
107 |
|
108 $is_local = isset($r['local']) && $r['local']; |
|
109 $ssl_verify = isset($r['sslverify']) && $r['sslverify']; |
|
110 if ( $is_local ) { |
|
111 /** This filter is documented in wp-includes/class-wp-http-streams.php */ |
|
112 $ssl_verify = apply_filters( 'https_local_ssl_verify', $ssl_verify ); |
|
113 } elseif ( ! $is_local ) { |
|
114 /** This filter is documented in wp-includes/class-wp-http-streams.php */ |
|
115 $ssl_verify = apply_filters( 'https_ssl_verify', $ssl_verify ); |
|
116 } |
|
117 |
|
118 /* |
|
119 * CURLOPT_TIMEOUT and CURLOPT_CONNECTTIMEOUT expect integers. Have to use ceil since. |
|
120 * a value of 0 will allow an unlimited timeout. |
|
121 */ |
|
122 $timeout = (int) ceil( $r['timeout'] ); |
|
123 curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, $timeout ); |
|
124 curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout ); |
|
125 |
|
126 curl_setopt( $handle, CURLOPT_URL, $url); |
|
127 curl_setopt( $handle, CURLOPT_RETURNTRANSFER, true ); |
|
128 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, ( $ssl_verify === true ) ? 2 : false ); |
|
129 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, $ssl_verify ); |
|
130 |
|
131 if ( $ssl_verify ) { |
|
132 curl_setopt( $handle, CURLOPT_CAINFO, $r['sslcertificates'] ); |
|
133 } |
|
134 |
|
135 curl_setopt( $handle, CURLOPT_USERAGENT, $r['user-agent'] ); |
|
136 |
|
137 /* |
|
138 * The option doesn't work with safe mode or when open_basedir is set, and there's |
|
139 * a bug #17490 with redirected POST requests, so handle redirections outside Curl. |
|
140 */ |
|
141 curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, false ); |
|
142 if ( defined( 'CURLOPT_PROTOCOLS' ) ) // PHP 5.2.10 / cURL 7.19.4 |
|
143 curl_setopt( $handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS ); |
|
144 |
|
145 switch ( $r['method'] ) { |
|
146 case 'HEAD': |
|
147 curl_setopt( $handle, CURLOPT_NOBODY, true ); |
|
148 break; |
|
149 case 'POST': |
|
150 curl_setopt( $handle, CURLOPT_POST, true ); |
|
151 curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] ); |
|
152 break; |
|
153 case 'PUT': |
|
154 curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, 'PUT' ); |
|
155 curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] ); |
|
156 break; |
|
157 default: |
|
158 curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, $r['method'] ); |
|
159 if ( ! is_null( $r['body'] ) ) |
|
160 curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] ); |
|
161 break; |
|
162 } |
|
163 |
|
164 if ( true === $r['blocking'] ) { |
|
165 curl_setopt( $handle, CURLOPT_HEADERFUNCTION, array( $this, 'stream_headers' ) ); |
|
166 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, array( $this, 'stream_body' ) ); |
|
167 } |
|
168 |
|
169 curl_setopt( $handle, CURLOPT_HEADER, false ); |
|
170 |
|
171 if ( isset( $r['limit_response_size'] ) ) |
|
172 $this->max_body_length = intval( $r['limit_response_size'] ); |
|
173 else |
|
174 $this->max_body_length = false; |
|
175 |
|
176 // If streaming to a file open a file handle, and setup our curl streaming handler. |
|
177 if ( $r['stream'] ) { |
|
178 if ( ! WP_DEBUG ) |
|
179 $this->stream_handle = @fopen( $r['filename'], 'w+' ); |
|
180 else |
|
181 $this->stream_handle = fopen( $r['filename'], 'w+' ); |
|
182 if ( ! $this->stream_handle ) { |
|
183 return new WP_Error( 'http_request_failed', sprintf( |
|
184 /* translators: 1: fopen() 2: file name */ |
|
185 __( 'Could not open handle for %1$s to %2$s.' ), |
|
186 'fopen()', |
|
187 $r['filename'] |
|
188 ) ); |
|
189 } |
|
190 } else { |
|
191 $this->stream_handle = false; |
|
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() (passed by reference). |
|
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 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 ) { |
|
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 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 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 * |
|
308 * @param resource $handle cURL handle. |
|
309 * @param string $headers cURL request headers. |
|
310 * @return int Length of the request headers. |
|
311 */ |
|
312 private function stream_headers( $handle, $headers ) { |
|
313 $this->headers .= $headers; |
|
314 return strlen( $headers ); |
|
315 } |
|
316 |
|
317 /** |
|
318 * Grabs the body of the cURL request. |
|
319 * |
|
320 * The contents of the document are passed in chunks, so we append to the `$body` |
|
321 * property for temporary storage. Returning a length shorter than the length of |
|
322 * `$data` passed in will cause cURL to abort the request with `CURLE_WRITE_ERROR`. |
|
323 * |
|
324 * @since 3.6.0 |
|
325 * |
|
326 * @param resource $handle cURL handle. |
|
327 * @param string $data cURL request body. |
|
328 * @return int Total bytes of data written. |
|
329 */ |
|
330 private function stream_body( $handle, $data ) { |
|
331 $data_length = strlen( $data ); |
|
332 |
|
333 if ( $this->max_body_length && ( $this->bytes_written_total + $data_length ) > $this->max_body_length ) { |
|
334 $data_length = ( $this->max_body_length - $this->bytes_written_total ); |
|
335 $data = substr( $data, 0, $data_length ); |
|
336 } |
|
337 |
|
338 if ( $this->stream_handle ) { |
|
339 $bytes_written = fwrite( $this->stream_handle, $data ); |
|
340 } else { |
|
341 $this->body .= $data; |
|
342 $bytes_written = $data_length; |
|
343 } |
|
344 |
|
345 $this->bytes_written_total += $bytes_written; |
|
346 |
|
347 // Upon event of this function returning less than strlen( $data ) curl will error with CURLE_WRITE_ERROR. |
|
348 return $bytes_written; |
|
349 } |
|
350 |
|
351 /** |
|
352 * Determines whether this class can be used for retrieving a URL. |
|
353 * |
|
354 * @static |
|
355 * @since 2.7.0 |
|
356 * |
|
357 * @param array $args Optional. Array of request arguments. Default empty array. |
|
358 * @return bool False means this class can not be used, true means it can. |
|
359 */ |
|
360 public static function test( $args = array() ) { |
|
361 if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) ) |
|
362 return false; |
|
363 |
|
364 $is_ssl = isset( $args['ssl'] ) && $args['ssl']; |
|
365 |
|
366 if ( $is_ssl ) { |
|
367 $curl_version = curl_version(); |
|
368 // Check whether this cURL version support SSL requests. |
|
369 if ( ! (CURL_VERSION_SSL & $curl_version['features']) ) |
|
370 return false; |
|
371 } |
|
372 |
|
373 /** |
|
374 * Filters whether cURL can be used as a transport for retrieving a URL. |
|
375 * |
|
376 * @since 2.7.0 |
|
377 * |
|
378 * @param bool $use_class Whether the class can be used. Default true. |
|
379 * @param array $args An array of request arguments. |
|
380 */ |
|
381 return apply_filters( 'use_curl_transport', true, $args ); |
|
382 } |
|
383 } |