WordPress 4.6.2
[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                 if (!empty($headers)) {
379                         curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
380                 }
381                 if ($options['protocol_version'] === 1.1) {
382                         curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
383                 }
384                 else {
385                         curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
386                 }
387
388                 if (true === $options['blocking']) {
389                         curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
390                         curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
391                         curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
392                 }
393         }
394
395         /**
396          * Process a response
397          *
398          * @param string $response Response data from the body
399          * @param array $options Request options
400          * @return string HTTP response data including headers
401          */
402         public function process_response($response, $options) {
403                 if ($options['blocking'] === false) {
404                         $fake_headers = '';
405                         $options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
406                         return false;
407                 }
408                 if ($options['filename'] !== false) {
409                         fclose($this->stream_handle);
410                         $this->headers = trim($this->headers);
411                 }
412                 else {
413                         $this->headers .= $response;
414                 }
415
416                 if (curl_errno($this->handle)) {
417                         $error = sprintf(
418                                 'cURL error %s: %s',
419                                 curl_errno($this->handle),
420                                 curl_error($this->handle)
421                         );
422                         throw new Requests_Exception($error, 'curlerror', $this->handle);
423                 }
424                 $this->info = curl_getinfo($this->handle);
425
426                 $options['hooks']->dispatch('curl.after_request', array(&$this->headers, &$this->info));
427                 return $this->headers;
428         }
429
430         /**
431          * Collect the headers as they are received
432          *
433          * @param resource $handle cURL resource
434          * @param string $headers Header string
435          * @return integer Length of provided header
436          */
437         public function stream_headers($handle, $headers) {
438                 // Why do we do this? cURL will send both the final response and any
439                 // interim responses, such as a 100 Continue. We don't need that.
440                 // (We may want to keep this somewhere just in case)
441                 if ($this->done_headers) {
442                         $this->headers = '';
443                         $this->done_headers = false;
444                 }
445                 $this->headers .= $headers;
446
447                 if ($headers === "\r\n") {
448                         $this->done_headers = true;
449                 }
450                 return strlen($headers);
451         }
452
453         /**
454          * Collect data as it's received
455          *
456          * @since 1.6.1
457          *
458          * @param resource $handle cURL resource
459          * @param string $data Body data
460          * @return integer Length of provided data
461          */
462         public function stream_body($handle, $data) {
463                 $this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit));
464                 $data_length = strlen($data);
465
466                 // Are we limiting the response size?
467                 if ($this->response_byte_limit) {
468                         if ($this->response_bytes === $this->response_byte_limit) {
469                                 // Already at maximum, move on
470                                 return $data_length;
471                         }
472
473                         if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
474                                 // Limit the length
475                                 $limited_length = ($this->response_byte_limit - $this->response_bytes);
476                                 $data = substr($data, 0, $limited_length);
477                         }
478                 }
479
480                 if ($this->stream_handle) {
481                         fwrite($this->stream_handle, $data);
482                 }
483                 else {
484                         $this->response_data .= $data;
485                 }
486
487                 $this->response_bytes += strlen($data);
488                 return $data_length;
489         }
490
491         /**
492          * Format a URL given GET data
493          *
494          * @param string $url
495          * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
496          * @return string URL with data
497          */
498         protected static function format_get($url, $data) {
499                 if (!empty($data)) {
500                         $url_parts = parse_url($url);
501                         if (empty($url_parts['query'])) {
502                                 $query = $url_parts['query'] = '';
503                         }
504                         else {
505                                 $query = $url_parts['query'];
506                         }
507
508                         $query .= '&' . http_build_query($data, null, '&');
509                         $query = trim($query, '&');
510
511                         if (empty($url_parts['query'])) {
512                                 $url .= '?' . $query;
513                         }
514                         else {
515                                 $url = str_replace($url_parts['query'], $query, $url);
516                         }
517                 }
518                 return $url;
519         }
520
521         /**
522          * Whether this transport is valid
523          *
524          * @codeCoverageIgnore
525          * @return boolean True if the transport is valid, false otherwise.
526          */
527         public static function test($capabilities = array()) {
528                 if (!function_exists('curl_init') || !function_exists('curl_exec')) {
529                         return false;
530                 }
531
532                 // If needed, check that our installed curl version supports SSL
533                 if (isset($capabilities['ssl']) && $capabilities['ssl']) {
534                         $curl_version = curl_version();
535                         if (!(CURL_VERSION_SSL & $curl_version['features'])) {
536                                 return false;
537                         }
538                 }
539
540                 return true;
541         }
542 }