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