XpressEngine Core  1.11.2
 All Classes Namespaces Files Functions Variables Pages
Curl.php
Go to the documentation of this file.
1 <?php
24 require_once 'HTTP/Request2/Adapter.php';
25 
37 {
42  protected static $headerMap = array(
43  'accept-encoding' => CURLOPT_ENCODING,
44  'cookie' => CURLOPT_COOKIE,
45  'referer' => CURLOPT_REFERER,
46  'user-agent' => CURLOPT_USERAGENT
47  );
48 
53  protected static $sslContextMap = array(
54  'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
55  'ssl_cafile' => CURLOPT_CAINFO,
56  'ssl_capath' => CURLOPT_CAPATH,
57  'ssl_local_cert' => CURLOPT_SSLCERT,
58  'ssl_passphrase' => CURLOPT_SSLCERTPASSWD
59  );
60 
65  protected static $errorMap = array(
66  CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException',
68  CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),
69  CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'),
70  CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'),
71  // error returned from write callback
72  CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException',
74  CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException',
76  CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'),
77  CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'),
78  CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException',
80  CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException',
82  CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException',
84  CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException',
86  CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'),
87  CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'),
88  CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException',
90  CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException',
92  CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'),
93  CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'),
94  CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException',
96  CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'),
97  CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'),
98  CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'),
99  );
100 
105  protected $response;
106 
111  protected $eventSentHeaders = false;
112 
117  protected $eventReceivedHeaders = false;
118 
124  protected $position = 0;
125 
130  protected $lastInfo;
131 
139  protected static function wrapCurlError($ch)
140  {
141  $nativeCode = curl_errno($ch);
142  $message = 'Curl error: ' . curl_error($ch);
143  if (!isset(self::$errorMap[$nativeCode])) {
144  return new HTTP_Request2_Exception($message, 0, $nativeCode);
145  } else {
146  $class = self::$errorMap[$nativeCode][0];
147  $code = empty(self::$errorMap[$nativeCode][1])
148  ? 0 : self::$errorMap[$nativeCode][1];
149  return new $class($message, $code, $nativeCode);
150  }
151  }
152 
162  {
163  if (!extension_loaded('curl')) {
165  'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION
166  );
167  }
168 
169  $this->request = $request;
170  $this->response = null;
171  $this->position = 0;
172  $this->eventSentHeaders = false;
173  $this->eventReceivedHeaders = false;
174 
175  try {
176  if (false === curl_exec($ch = $this->createCurlHandle())) {
177  $e = self::wrapCurlError($ch);
178  }
179  } catch (Exception $e) {
180  }
181  if (isset($ch)) {
182  $this->lastInfo = curl_getinfo($ch);
183  curl_close($ch);
184  }
185 
187  unset($this->request, $this->requestBody, $this->response);
188 
189  if (!empty($e)) {
190  throw $e;
191  }
192 
193  if ($jar = $request->getCookieJar()) {
194  $jar->addCookiesFromResponse($response, $request->getUrl());
195  }
196 
197  if (0 < $this->lastInfo['size_download']) {
198  $request->setLastEvent('receivedBody', $response);
199  }
200  return $response;
201  }
202 
208  public function getInfo()
209  {
210  return $this->lastInfo;
211  }
212 
220  protected function createCurlHandle()
221  {
222  $ch = curl_init();
223 
224  curl_setopt_array($ch, array(
225  // setup write callbacks
226  CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
227  CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),
228  // buffer size
229  CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),
230  // connection timeout
231  CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
232  // save full outgoing headers, in case someone is interested
233  CURLINFO_HEADER_OUT => true,
234  // request url
235  CURLOPT_URL => $this->request->getUrl()->getUrl()
236  ));
237 
238  // set up redirects
239  if (!$this->request->getConfig('follow_redirects')) {
240  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
241  } else {
242  if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {
244  'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',
246  );
247  }
248  curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
249  // limit redirects to http(s), works in 5.2.10+
250  if (defined('CURLOPT_REDIR_PROTOCOLS')) {
251  curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
252  }
253  // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571
254  if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {
255  curl_setopt($ch, CURLOPT_POSTREDIR, 3);
256  }
257  }
258 
259  // set local IP via CURLOPT_INTERFACE (request #19515)
260  if ($ip = $this->request->getConfig('local_ip')) {
261  curl_setopt($ch, CURLOPT_INTERFACE, $ip);
262  }
263 
264  // request timeout
265  if ($timeout = $this->request->getConfig('timeout')) {
266  curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
267  }
268 
269  // set HTTP version
270  switch ($this->request->getConfig('protocol_version')) {
271  case '1.0':
272  curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
273  break;
274  case '1.1':
275  curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
276  }
277 
278  // set request method
279  switch ($this->request->getMethod()) {
281  curl_setopt($ch, CURLOPT_HTTPGET, true);
282  break;
284  curl_setopt($ch, CURLOPT_POST, true);
285  break;
287  curl_setopt($ch, CURLOPT_NOBODY, true);
288  break;
290  curl_setopt($ch, CURLOPT_UPLOAD, true);
291  break;
292  default:
293  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
294  }
295 
296  // set proxy, if needed
297  if ($host = $this->request->getConfig('proxy_host')) {
298  if (!($port = $this->request->getConfig('proxy_port'))) {
300  'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE
301  );
302  }
303  curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
304  if ($user = $this->request->getConfig('proxy_user')) {
305  curl_setopt(
306  $ch, CURLOPT_PROXYUSERPWD,
307  $user . ':' . $this->request->getConfig('proxy_password')
308  );
309  switch ($this->request->getConfig('proxy_auth_scheme')) {
311  curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
312  break;
314  curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
315  }
316  }
317  if ($type = $this->request->getConfig('proxy_type')) {
318  switch ($type) {
319  case 'http':
320  curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
321  break;
322  case 'socks5':
323  curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
324  break;
325  default:
327  "Proxy type '{$type}' is not supported"
328  );
329  }
330  }
331  }
332 
333  // set authentication data
334  if ($auth = $this->request->getAuth()) {
335  curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
336  switch ($auth['scheme']) {
338  curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
339  break;
341  curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
342  }
343  }
344 
345  // set SSL options
346  foreach ($this->request->getConfig() as $name => $value) {
347  if ('ssl_verify_host' == $name && null !== $value) {
348  curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);
349  } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {
350  curl_setopt($ch, self::$sslContextMap[$name], $value);
351  }
352  }
353 
354  $headers = $this->request->getHeaders();
355  // make cURL automagically send proper header
356  if (!isset($headers['accept-encoding'])) {
357  $headers['accept-encoding'] = '';
358  }
359 
360  if (($jar = $this->request->getCookieJar())
361  && ($cookies = $jar->getMatching($this->request->getUrl(), true))
362  ) {
363  $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
364  }
365 
366  // set headers having special cURL keys
367  foreach (self::$headerMap as $name => $option) {
368  if (isset($headers[$name])) {
369  curl_setopt($ch, $option, $headers[$name]);
370  unset($headers[$name]);
371  }
372  }
373 
374  $this->calculateRequestLength($headers);
375  if (isset($headers['content-length']) || isset($headers['transfer-encoding'])) {
376  $this->workaroundPhpBug47204($ch, $headers);
377  }
378 
379  // set headers not having special keys
380  $headersFmt = array();
381  foreach ($headers as $name => $value) {
382  $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
383  $headersFmt[] = $canonicalName . ': ' . $value;
384  }
385  curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
386 
387  return $ch;
388  }
389 
400  protected function workaroundPhpBug47204($ch, &$headers)
401  {
402  // no redirects, no digest auth -> probably no rewind needed
403  if (!$this->request->getConfig('follow_redirects')
404  && (!($auth = $this->request->getAuth())
405  || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])
406  ) {
407  curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
408 
409  } else {
410  // rewind may be needed, read the whole body into memory
411  if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {
412  $this->requestBody = $this->requestBody->__toString();
413 
414  } elseif (is_resource($this->requestBody)) {
415  $fp = $this->requestBody;
416  $this->requestBody = '';
417  while (!feof($fp)) {
418  $this->requestBody .= fread($fp, 16384);
419  }
420  }
421  // curl hangs up if content-length is present
422  unset($headers['content-length']);
423  curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
424  }
425  }
426 
436  protected function callbackReadBody($ch, $fd, $length)
437  {
438  if (!$this->eventSentHeaders) {
439  $this->request->setLastEvent(
440  'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
441  );
442  $this->eventSentHeaders = true;
443  }
444  if (in_array($this->request->getMethod(), self::$bodyDisallowed)
445  || 0 == $this->contentLength || $this->position >= $this->contentLength
446  ) {
447  return '';
448  }
449  if (is_string($this->requestBody)) {
450  $string = substr($this->requestBody, $this->position, $length);
451  } elseif (is_resource($this->requestBody)) {
452  $string = fread($this->requestBody, $length);
453  } else {
454  $string = $this->requestBody->read($length);
455  }
456  $this->request->setLastEvent('sentBodyPart', strlen($string));
457  $this->position += strlen($string);
458  return $string;
459  }
460 
470  protected function callbackWriteHeader($ch, $string)
471  {
472  // we may receive a second set of headers if doing e.g. digest auth
473  if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {
474  // don't bother with 100-Continue responses (bug #15785)
475  if (!$this->eventSentHeaders
476  || $this->response->getStatus() >= 200
477  ) {
478  $this->request->setLastEvent(
479  'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
480  );
481  }
482  $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
483  // if body wasn't read by a callback, send event with total body size
484  if ($upload > $this->position) {
485  $this->request->setLastEvent(
486  'sentBodyPart', $upload - $this->position
487  );
488  $this->position = $upload;
489  }
490  if ($upload && (!$this->eventSentHeaders
491  || $this->response->getStatus() >= 200)
492  ) {
493  $this->request->setLastEvent('sentBody', $upload);
494  }
495  $this->eventSentHeaders = true;
496  // we'll need a new response object
497  if ($this->eventReceivedHeaders) {
498  $this->eventReceivedHeaders = false;
499  $this->response = null;
500  }
501  }
502  if (empty($this->response)) {
503  $this->response = new HTTP_Request2_Response(
504  $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)
505  );
506  } else {
507  $this->response->parseHeaderLine($string);
508  if ('' == trim($string)) {
509  // don't bother with 100-Continue responses (bug #15785)
510  if (200 <= $this->response->getStatus()) {
511  $this->request->setLastEvent('receivedHeaders', $this->response);
512  }
513 
514  if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {
515  $redirectUrl = new Net_URL2($this->response->getHeader('location'));
516 
517  // for versions lower than 5.2.10, check the redirection URL protocol
518  if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()
519  && !in_array($redirectUrl->getScheme(), array('http', 'https'))
520  ) {
521  return -1;
522  }
523 
524  if ($jar = $this->request->getCookieJar()) {
525  $jar->addCookiesFromResponse($this->response, $this->request->getUrl());
526  if (!$redirectUrl->isAbsolute()) {
527  $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);
528  }
529  if ($cookies = $jar->getMatching($redirectUrl, true)) {
530  curl_setopt($ch, CURLOPT_COOKIE, $cookies);
531  }
532  }
533  }
534  $this->eventReceivedHeaders = true;
535  }
536  }
537  return strlen($string);
538  }
539 
550  protected function callbackWriteBody($ch, $string)
551  {
552  // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
553  // response doesn't start with proper HTTP status line (see bug #15716)
554  if (empty($this->response)) {
556  "Malformed response: {$string}",
558  );
559  }
560  if ($this->request->getConfig('store_body')) {
561  $this->response->appendBody($string);
562  }
563  $this->request->setLastEvent('receivedBodyPart', $string);
564  return strlen($string);
565  }
566 }
567 ?>
resolve($reference)
Definition: URL2.php:882
const AUTH_BASIC
Definition: Request2.php:64
const METHOD_GET
Definition: Request2.php:50
workaroundPhpBug47204($ch, &$headers)
Definition: Curl.php:400
const METHOD_HEAD
Definition: Request2.php:51
const METHOD_PUT
Definition: Request2.php:53
callbackWriteHeader($ch, $string)
Definition: Curl.php:470
const METHOD_POST
Definition: Request2.php:52
const AUTH_DIGEST
Definition: Request2.php:65
sendRequest(HTTP_Request2 $request)
Definition: Curl.php:161
static wrapCurlError($ch)
Definition: Curl.php:139
calculateRequestLength(&$headers)
Definition: Adapter.php:96
setLastEvent($name, $data=null)
Definition: Request2.php:765
callbackWriteBody($ch, $string)
Definition: Curl.php:550
callbackReadBody($ch, $fd, $length)
Definition: Curl.php:436