]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/Requests/Requests/Transport/cURL.php
Sync with current version of Requests
[Github/YOURLS.git] / includes / Requests / 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 http://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 $fp;
53
54         /**
55          * Have we finished the headers yet?
56          *
57          * @var boolean
58          */
59         protected $done_headers = false;
60
61         /**
62          * If streaming to a file, keep the file pointer
63          *
64          * @var resource
65          */
66         protected $stream_handle;
67
68         /**
69          * How many bytes are in the response body?
70          *
71          * @var int
72          */
73         protected $response_bytes;
74
75         /**
76          * What's the maximum number of bytes we should keep?
77          *
78          * @var int|bool Byte count, or false if no limit.
79          */
80         protected $response_byte_limit;
81
82         /**
83          * Constructor
84          */
85         public function __construct() {
86                 $curl = curl_version();
87                 $this->version = $curl['version_number'];
88                 $this->fp = curl_init();
89
90                 curl_setopt($this->fp, CURLOPT_HEADER, false);
91                 curl_setopt($this->fp, CURLOPT_RETURNTRANSFER, 1);
92                 if ($this->version >= self::CURL_7_10_5) {
93                         curl_setopt($this->fp, CURLOPT_ENCODING, '');
94                 }
95                 if (defined('CURLOPT_PROTOCOLS')) {
96                         curl_setopt($this->fp, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
97                 }
98                 if (defined('CURLOPT_REDIR_PROTOCOLS')) {
99                         curl_setopt($this->fp, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
100                 }
101         }
102
103         /**
104          * Perform a request
105          *
106          * @throws Requests_Exception On a cURL error (`curlerror`)
107          *
108          * @param string $url URL to request
109          * @param array $headers Associative array of request headers
110          * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
111          * @param array $options Request options, see {@see Requests::response()} for documentation
112          * @return string Raw HTTP result
113          */
114         public function request($url, $headers = array(), $data = array(), $options = array()) {
115                 $this->setup_handle($url, $headers, $data, $options);
116
117                 $options['hooks']->dispatch('curl.before_send', array(&$this->fp));
118
119                 if ($options['filename'] !== false) {
120                         $this->stream_handle = fopen($options['filename'], 'wb');
121                 }
122
123                 $this->response_data = '';
124                 $this->response_bytes = 0;
125                 $this->response_byte_limit = false;
126                 if ($options['max_bytes'] !== false) {
127                         $this->response_byte_limit = $options['max_bytes'];
128                 }
129
130                 if (isset($options['verify'])) {
131                         if ($options['verify'] === false) {
132                                 curl_setopt($this->fp, CURLOPT_SSL_VERIFYHOST, 0);
133                                 curl_setopt($this->fp, CURLOPT_SSL_VERIFYPEER, 0);
134
135                         } elseif (is_string($options['verify'])) {
136                                 curl_setopt($this->fp, CURLOPT_CAINFO, $options['verify']);
137                         }
138                 }
139
140                 if (isset($options['verifyname']) && $options['verifyname'] === false) {
141                         curl_setopt($this->fp, CURLOPT_SSL_VERIFYHOST, 0);
142                 }
143
144                 curl_exec($this->fp);
145                 $response = $this->response_data;
146
147                 $options['hooks']->dispatch('curl.after_send', array(&$fake_headers));
148
149                 if (curl_errno($this->fp) === 23 || curl_errno($this->fp) === 61) {
150                         // Reset encoding and try again
151                         curl_setopt($this->fp, CURLOPT_ENCODING, 'none');
152
153                         $this->response_data = '';
154                         $this->response_bytes = 0;
155                         curl_exec($this->fp);
156                         $response = $this->response_data;
157                 }
158
159                 $this->process_response($response, $options);
160                 curl_close($this->fp);
161                 return $this->headers;
162         }
163
164         /**
165          * Send multiple requests simultaneously
166          *
167          * @param array $requests Request data
168          * @param array $options Global options
169          * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
170          */
171         public function request_multiple($requests, $options) {
172                 $multihandle = curl_multi_init();
173                 $subrequests = array();
174                 $subhandles = array();
175
176                 $class = get_class($this);
177                 foreach ($requests as $id => $request) {
178                         $subrequests[$id] = new $class();
179                         $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
180                         $request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
181                         curl_multi_add_handle($multihandle, $subhandles[$id]);
182                 }
183
184                 $completed = 0;
185                 $responses = array();
186
187                 $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle));
188
189                 do {
190                         $active = false;
191
192                         do {
193                                 $status = curl_multi_exec($multihandle, $active);
194                         }
195                         while ($status === CURLM_CALL_MULTI_PERFORM);
196
197                         $to_process = array();
198
199                         // Read the information as needed
200                         while ($done = curl_multi_info_read($multihandle)) {
201                                 $key = array_search($done['handle'], $subhandles, true);
202                                 if (!isset($to_process[$key])) {
203                                         $to_process[$key] = $done;
204                                 }
205                         }
206
207                         // Parse the finished requests before we start getting the new ones
208                         foreach ($to_process as $key => $done) {
209                                 $options = $requests[$key]['options'];
210                                 $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
211
212                                 $options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key]));
213
214                                 curl_multi_remove_handle($multihandle, $done['handle']);
215                                 curl_close($done['handle']);
216
217                                 if (!is_string($responses[$key])) {
218                                         $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
219                                 }
220                                 $completed++;
221                         }
222                 }
223                 while ($active || $completed < count($subrequests));
224
225                 $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
226
227                 curl_multi_close($multihandle);
228
229                 return $responses;
230         }
231
232         /**
233          * Get the cURL handle for use in a multi-request
234          *
235          * @param string $url URL to request
236          * @param array $headers Associative array of request headers
237          * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
238          * @param array $options Request options, see {@see Requests::response()} for documentation
239          * @return resource Subrequest's cURL handle
240          */
241         public function &get_subrequest_handle($url, $headers, $data, $options) {
242                 $this->setup_handle($url, $headers, $data, $options);
243
244                 if ($options['filename'] !== false) {
245                         $this->stream_handle = fopen($options['filename'], 'wb');
246                         curl_setopt($this->fp, CURLOPT_FILE, $this->stream_handle);
247                 }
248
249                 $this->response_data = '';
250                 $this->response_bytes = 0;
251                 $this->response_byte_limit = false;
252                 if ($options['max_bytes'] !== false) {
253                         $this->response_byte_limit = $options['max_bytes'];
254                 }
255
256                 return $this->fp;
257         }
258
259         /**
260          * Setup the cURL handle for the given data
261          *
262          * @param string $url URL to request
263          * @param array $headers Associative array of request headers
264          * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
265          * @param array $options Request options, see {@see Requests::response()} for documentation
266          */
267         protected function setup_handle($url, $headers, $data, $options) {
268                 $options['hooks']->dispatch('curl.before_request', array(&$this->fp));
269
270                 $headers = Requests::flatten($headers);
271                 if (in_array($options['type'], array(Requests::HEAD, Requests::GET, Requests::DELETE)) & !empty($data)) {
272                         $url = self::format_get($url, $data);
273                 }
274                 elseif (!empty($data) && !is_string($data)) {
275                         $data = http_build_query($data, null, '&');
276                 }
277
278                 switch ($options['type']) {
279                         case Requests::POST:
280                                 curl_setopt($this->fp, CURLOPT_POST, true);
281                                 curl_setopt($this->fp, CURLOPT_POSTFIELDS, $data);
282                                 break;
283                         case Requests::PATCH:
284                         case Requests::PUT:
285                                 curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, $options['type']);
286                                 curl_setopt($this->fp, CURLOPT_POSTFIELDS, $data);
287                                 break;
288                         case Requests::DELETE:
289                                 curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, 'DELETE');
290                                 break;
291                         case Requests::HEAD:
292                                 curl_setopt($this->fp, CURLOPT_NOBODY, true);
293                                 break;
294                 }
295
296                 if( is_int($options['timeout']) or $this->version < self::CURL_7_16_2 ) {
297                         curl_setopt($this->fp, CURLOPT_TIMEOUT, ceil($options['timeout']));
298                 } else {
299                         curl_setopt($this->fp, CURLOPT_TIMEOUT_MS, round($options['timeout'] * 1000) );
300                 }
301                 if( is_int($options['connect_timeout'])  or $this->version < self::CURL_7_16_2 ) {
302                         curl_setopt($this->fp, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
303                 } else {
304                         curl_setopt($this->fp, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
305                 }
306                 curl_setopt($this->fp, CURLOPT_URL, $url);
307                 curl_setopt($this->fp, CURLOPT_REFERER, $url);
308                 curl_setopt($this->fp, CURLOPT_USERAGENT, $options['useragent']);
309                 curl_setopt($this->fp, CURLOPT_HTTPHEADER, $headers);
310
311                 if (true === $options['blocking']) {
312                         curl_setopt($this->fp, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
313                         curl_setopt($this->fp, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
314                         curl_setopt($this->fp, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
315                 }
316         }
317
318         public function process_response($response, $options) {
319                 if ($options['blocking'] === false) {
320                         $fake_headers = '';
321                         $options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
322                         return false;
323                 }
324                 if ($options['filename'] !== false) {
325                         fclose($this->stream_handle);
326                         $this->headers = trim($this->headers);
327                 }
328                 else {
329                         $this->headers .= $response;
330                 }
331
332                 if (curl_errno($this->fp)) {
333                         throw new Requests_Exception('cURL error ' . curl_errno($this->fp) . ': ' . curl_error($this->fp), 'curlerror', $this->fp);
334                 }
335                 $this->info = curl_getinfo($this->fp);
336
337                 $options['hooks']->dispatch('curl.after_request', array(&$this->headers));
338                 return $this->headers;
339         }
340
341         /**
342          * Collect the headers as they are received
343          *
344          * @param resource $handle cURL resource
345          * @param string $headers Header string
346          * @return integer Length of provided header
347          */
348         public function stream_headers($handle, $headers) {
349                 // Why do we do this? cURL will send both the final response and any
350                 // interim responses, such as a 100 Continue. We don't need that.
351                 // (We may want to keep this somewhere just in case)
352                 if ($this->done_headers) {
353                         $this->headers = '';
354                         $this->done_headers = false;
355                 }
356                 $this->headers .= $headers;
357
358                 if ($headers === "\r\n") {
359                         $this->done_headers = true;
360                 }
361                 return strlen($headers);
362         }
363
364         /**
365          * Collect data as it's received
366          *
367          * @since 1.6.1
368          *
369          * @param resource $handle cURL resource
370          * @param string $data Body data
371          * @return integer Length of provided data
372          */
373         protected function stream_body($handle, $data) {
374                 $data_length = strlen($data);
375
376                 // Are we limiting the response size?
377                 if ($this->response_byte_limit) {
378                         if ($this->response_bytes === $this->response_byte_limit) {
379                                 // Already at maximum, move on
380                                 return $data_length;
381                         }
382
383                         if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
384                                 // Limit the length
385                                 $limited_length = ($this->response_byte_limit - $this->response_bytes);
386                                 $data = substr($data, 0, $limited_length);
387                         }
388                 }
389
390                 if ($this->stream_handle) {
391                         fwrite($this->stream_handle, $data);
392                 }
393                 else {
394                         $this->response_data .= $data;
395                 }
396
397                 $this->response_bytes += strlen($data);
398                 return $data_length;
399         }
400
401         /**
402          * Format a URL given GET data
403          *
404          * @param string $url
405          * @param array|object $data Data to build query using, see {@see http://php.net/http_build_query}
406          * @return string URL with data
407          */
408         protected static function format_get($url, $data) {
409                 if (!empty($data)) {
410                         $url_parts = parse_url($url);
411                         if (empty($url_parts['query'])) {
412                                 $query = $url_parts['query'] = '';
413                         }
414                         else {
415                                 $query = $url_parts['query'];
416                         }
417
418                         $query .= '&' . http_build_query($data, null, '&');
419                         $query = trim($query, '&');
420
421                         if (empty($url_parts['query'])) {
422                                 $url .= '?' . $query;
423                         }
424                         else {
425                                 $url = str_replace($url_parts['query'], $query, $url);
426                         }
427                 }
428                 return $url;
429         }
430
431         /**
432          * Whether this transport is valid
433          *
434          * @codeCoverageIgnore
435          * @return boolean True if the transport is valid, false otherwise.
436          */
437         public static function test($capabilities = array()) {
438                 if (!function_exists('curl_init') && !function_exists('curl_exec'))
439                         return false;
440
441                 // If needed, check that our installed curl version supports SSL
442                 if (isset( $capabilities['ssl'] ) && $capabilities['ssl']) {
443                         $curl_version = curl_version();
444                         if (!(CURL_VERSION_SSL & $curl_version['features']))
445                                 return false;
446                 }
447
448                 return true;
449         }
450 }