PageRenderTime 65ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/common/libraries/plugin/pear/HTTP/Request2/Adapter/Curl.php

https://bitbucket.org/gugli/chamilo-dev
PHP | 492 lines | 297 code | 35 blank | 160 comment | 55 complexity | a9100c7ada88c11d6f87e1f7875e12eb MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1, LGPL-3.0, GPL-3.0, MIT, GPL-2.0
  1. <?php
  2. /**
  3. * Adapter for HTTP_Request2 wrapping around cURL extension
  4. *
  5. * PHP version 5
  6. *
  7. * LICENSE:
  8. *
  9. * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or without
  13. * modification, are permitted provided that the following conditions
  14. * are met:
  15. *
  16. * * Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. * * Redistributions in binary form must reproduce the above copyright
  19. * notice, this list of conditions and the following disclaimer in the
  20. * documentation and/or other materials provided with the distribution.
  21. * * The names of the authors may not be used to endorse or promote products
  22. * derived from this software without specific prior written permission.
  23. *
  24. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  25. * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  26. * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  27. * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  28. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  29. * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  30. * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  31. * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
  32. * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  33. * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  34. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  35. *
  36. * @category HTTP
  37. * @package HTTP_Request2
  38. * @author Alexey Borzov <avb@php.net>
  39. * @license http://opensource.org/licenses/bsd-license.php New BSD License
  40. * @version SVN: $Id: Curl.php 291118 2009-11-21 17:58:23Z avb $
  41. * @link http://pear.php.net/package/HTTP_Request2
  42. */
  43. /**
  44. * Base class for HTTP_Request2 adapters
  45. */
  46. require_once 'HTTP/Request2/Adapter.php';
  47. /**
  48. * Adapter for HTTP_Request2 wrapping around cURL extension
  49. *
  50. * @category HTTP
  51. * @package HTTP_Request2
  52. * @author Alexey Borzov <avb@php.net>
  53. * @version Release: 0.5.2
  54. */
  55. class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
  56. {
  57. /**
  58. * Mapping of header names to cURL options
  59. * @var array
  60. */
  61. protected static $headerMap = array('accept-encoding' => CURLOPT_ENCODING, 'cookie' => CURLOPT_COOKIE,
  62. 'referer' => CURLOPT_REFERER, 'user-agent' => CURLOPT_USERAGENT);
  63. /**
  64. * Mapping of SSL context options to cURL options
  65. * @var array
  66. */
  67. protected static $sslContextMap = array('ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
  68. 'ssl_cafile' => CURLOPT_CAINFO, 'ssl_capath' => CURLOPT_CAPATH, 'ssl_local_cert' => CURLOPT_SSLCERT,
  69. 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD);
  70. /**
  71. * Response being received
  72. * @var HTTP_Request2_Response
  73. */
  74. protected $response;
  75. /**
  76. * Whether 'sentHeaders' event was sent to observers
  77. * @var boolean
  78. */
  79. protected $eventSentHeaders = false;
  80. /**
  81. * Whether 'receivedHeaders' event was sent to observers
  82. * @var boolean
  83. */
  84. protected $eventReceivedHeaders = false;
  85. /**
  86. * Position within request body
  87. * @var integer
  88. * @see callbackReadBody()
  89. */
  90. protected $position = 0;
  91. /**
  92. * Information about last transfer, as returned by curl_getinfo()
  93. * @var array
  94. */
  95. protected $lastInfo;
  96. /**
  97. * Sends request to the remote server and returns its response
  98. *
  99. * @param HTTP_Request2
  100. * @return HTTP_Request2_Response
  101. * @throws HTTP_Request2_Exception
  102. */
  103. public function sendRequest(HTTP_Request2 $request)
  104. {
  105. if (! extension_loaded('curl'))
  106. {
  107. throw new HTTP_Request2_Exception('cURL extension not available');
  108. }
  109. $this->request = $request;
  110. $this->response = null;
  111. $this->position = 0;
  112. $this->eventSentHeaders = false;
  113. $this->eventReceivedHeaders = false;
  114. try
  115. {
  116. if (false === curl_exec($ch = $this->createCurlHandle()))
  117. {
  118. $errorMessage = 'Error sending request: #' . curl_errno($ch) . ' ' . curl_error($ch);
  119. }
  120. }
  121. catch (Exception $e)
  122. {
  123. }
  124. $this->lastInfo = curl_getinfo($ch);
  125. curl_close($ch);
  126. $response = $this->response;
  127. unset($this->request, $this->requestBody, $this->response);
  128. if (! empty($e))
  129. {
  130. throw $e;
  131. }
  132. elseif (! empty($errorMessage))
  133. {
  134. throw new HTTP_Request2_Exception($errorMessage);
  135. }
  136. if (0 < $this->lastInfo['size_download'])
  137. {
  138. $request->setLastEvent('receivedBody', $response);
  139. }
  140. return $response;
  141. }
  142. /**
  143. * Returns information about last transfer
  144. *
  145. * @return array associative array as returned by curl_getinfo()
  146. */
  147. public function getInfo()
  148. {
  149. return $this->lastInfo;
  150. }
  151. /**
  152. * Creates a new cURL handle and populates it with data from the request
  153. *
  154. * @return resource a cURL handle, as created by curl_init()
  155. * @throws HTTP_Request2_Exception
  156. */
  157. protected function createCurlHandle()
  158. {
  159. $ch = curl_init();
  160. curl_setopt_array($ch, array(// setup write callbacks
  161. CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
  162. CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),
  163. // buffer size
  164. CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),
  165. // connection timeout
  166. CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
  167. // save full outgoing headers, in case someone is interested
  168. CURLINFO_HEADER_OUT => true,
  169. // request url
  170. CURLOPT_URL => $this->request->getUrl()->getUrl()));
  171. // set up redirects
  172. if (! $this->request->getConfig('follow_redirects'))
  173. {
  174. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
  175. }
  176. else
  177. {
  178. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  179. curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
  180. // limit redirects to http(s), works in 5.2.10+
  181. if (defined('CURLOPT_REDIR_PROTOCOLS'))
  182. {
  183. curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
  184. }
  185. // works sometime after 5.3.0, http://bugs.php.net/bug.php?id=49571
  186. if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR '))
  187. {
  188. curl_setopt($ch, CURLOPT_POSTREDIR, 3);
  189. }
  190. }
  191. // request timeout
  192. if ($timeout = $this->request->getConfig('timeout'))
  193. {
  194. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
  195. }
  196. // set HTTP version
  197. switch ($this->request->getConfig('protocol_version'))
  198. {
  199. case '1.0' :
  200. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
  201. break;
  202. case '1.1' :
  203. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  204. }
  205. // set request method
  206. switch ($this->request->getMethod())
  207. {
  208. case HTTP_Request2 :: METHOD_GET :
  209. curl_setopt($ch, CURLOPT_HTTPGET, true);
  210. break;
  211. case HTTP_Request2 :: METHOD_POST :
  212. curl_setopt($ch, CURLOPT_POST, true);
  213. break;
  214. case HTTP_Request2 :: METHOD_HEAD :
  215. curl_setopt($ch, CURLOPT_NOBODY, true);
  216. break;
  217. default :
  218. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
  219. }
  220. // set proxy, if needed
  221. if ($host = $this->request->getConfig('proxy_host'))
  222. {
  223. if (! ($port = $this->request->getConfig('proxy_port')))
  224. {
  225. throw new HTTP_Request2_Exception('Proxy port not provided');
  226. }
  227. curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
  228. if ($user = $this->request->getConfig('proxy_user'))
  229. {
  230. curl_setopt($ch, CURLOPT_PROXYUSERPWD, $user . ':' . $this->request->getConfig('proxy_password'));
  231. switch ($this->request->getConfig('proxy_auth_scheme'))
  232. {
  233. case HTTP_Request2 :: AUTH_BASIC :
  234. curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
  235. break;
  236. case HTTP_Request2 :: AUTH_DIGEST :
  237. curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
  238. }
  239. }
  240. }
  241. // set authentication data
  242. if ($auth = $this->request->getAuth())
  243. {
  244. curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
  245. switch ($auth['scheme'])
  246. {
  247. case HTTP_Request2 :: AUTH_BASIC :
  248. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
  249. break;
  250. case HTTP_Request2 :: AUTH_DIGEST :
  251. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
  252. }
  253. }
  254. // set SSL options
  255. if (0 == strcasecmp($this->request->getUrl()->getScheme(), 'https'))
  256. {
  257. foreach ($this->request->getConfig() as $name => $value)
  258. {
  259. if ('ssl_verify_host' == $name && null !== $value)
  260. {
  261. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value ? 2 : 0);
  262. }
  263. elseif (isset(self :: $sslContextMap[$name]) && null !== $value)
  264. {
  265. curl_setopt($ch, self :: $sslContextMap[$name], $value);
  266. }
  267. }
  268. }
  269. $headers = $this->request->getHeaders();
  270. // make cURL automagically send proper header
  271. if (! isset($headers['accept-encoding']))
  272. {
  273. $headers['accept-encoding'] = '';
  274. }
  275. // set headers having special cURL keys
  276. foreach (self :: $headerMap as $name => $option)
  277. {
  278. if (isset($headers[$name]))
  279. {
  280. curl_setopt($ch, $option, $headers[$name]);
  281. unset($headers[$name]);
  282. }
  283. }
  284. $this->calculateRequestLength($headers);
  285. if (isset($headers['content-length']))
  286. {
  287. $this->workaroundPhpBug47204($ch, $headers);
  288. }
  289. // set headers not having special keys
  290. $headersFmt = array();
  291. foreach ($headers as $name => $value)
  292. {
  293. $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
  294. $headersFmt[] = $canonicalName . ': ' . $value;
  295. }
  296. curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
  297. return $ch;
  298. }
  299. /**
  300. * Workaround for PHP bug #47204 that prevents rewinding request body
  301. *
  302. * The workaround consists of reading the entire request body into memory
  303. * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large
  304. * file uploads, use Socket adapter instead.
  305. *
  306. * @param resource cURL handle
  307. * @param array Request headers
  308. */
  309. protected function workaroundPhpBug47204($ch, &$headers)
  310. {
  311. // no redirects, no digest auth -> probably no rewind needed
  312. if (! $this->request->getConfig('follow_redirects') && (! ($auth = $this->request->getAuth()) || HTTP_Request2 :: AUTH_DIGEST != $auth['scheme']))
  313. {
  314. curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
  315. // rewind may be needed, read the whole body into memory
  316. }
  317. else
  318. {
  319. if ($this->requestBody instanceof HTTP_Request2_MultipartBody)
  320. {
  321. $this->requestBody = $this->requestBody->__toString();
  322. }
  323. elseif (is_resource($this->requestBody))
  324. {
  325. $fp = $this->requestBody;
  326. $this->requestBody = '';
  327. while (! feof($fp))
  328. {
  329. $this->requestBody .= fread($fp, 16384);
  330. }
  331. }
  332. // curl hangs up if content-length is present
  333. unset($headers['content-length']);
  334. curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
  335. }
  336. }
  337. /**
  338. * Callback function called by cURL for reading the request body
  339. *
  340. * @param resource cURL handle
  341. * @param resource file descriptor (not used)
  342. * @param integer maximum length of data to return
  343. * @return string part of the request body, up to $length bytes
  344. */
  345. protected function callbackReadBody($ch, $fd, $length)
  346. {
  347. if (! $this->eventSentHeaders)
  348. {
  349. $this->request->setLastEvent('sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT));
  350. $this->eventSentHeaders = true;
  351. }
  352. if (in_array($this->request->getMethod(), self :: $bodyDisallowed) || 0 == $this->contentLength || $this->position >= $this->contentLength)
  353. {
  354. return '';
  355. }
  356. if (is_string($this->requestBody))
  357. {
  358. $string = substr($this->requestBody, $this->position, $length);
  359. }
  360. elseif (is_resource($this->requestBody))
  361. {
  362. $string = fread($this->requestBody, $length);
  363. }
  364. else
  365. {
  366. $string = $this->requestBody->read($length);
  367. }
  368. $this->request->setLastEvent('sentBodyPart', strlen($string));
  369. $this->position += strlen($string);
  370. return $string;
  371. }
  372. /**
  373. * Callback function called by cURL for saving the response headers
  374. *
  375. * @param resource cURL handle
  376. * @param string response header (with trailing CRLF)
  377. * @return integer number of bytes saved
  378. * @see HTTP_Request2_Response::parseHeaderLine()
  379. */
  380. protected function callbackWriteHeader($ch, $string)
  381. {
  382. // we may receive a second set of headers if doing e.g. digest auth
  383. if ($this->eventReceivedHeaders || ! $this->eventSentHeaders)
  384. {
  385. // don't bother with 100-Continue responses (bug #15785)
  386. if (! $this->eventSentHeaders || $this->response->getStatus() >= 200)
  387. {
  388. $this->request->setLastEvent('sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT));
  389. }
  390. $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
  391. // if body wasn't read by a callback, send event with total body size
  392. if ($upload > $this->position)
  393. {
  394. $this->request->setLastEvent('sentBodyPart', $upload - $this->position);
  395. $this->position = $upload;
  396. }
  397. $this->eventSentHeaders = true;
  398. // we'll need a new response object
  399. if ($this->eventReceivedHeaders)
  400. {
  401. $this->eventReceivedHeaders = false;
  402. $this->response = null;
  403. }
  404. }
  405. if (empty($this->response))
  406. {
  407. $this->response = new HTTP_Request2_Response($string, false);
  408. }
  409. else
  410. {
  411. $this->response->parseHeaderLine($string);
  412. if ('' == trim($string))
  413. {
  414. // don't bother with 100-Continue responses (bug #15785)
  415. if (200 <= $this->response->getStatus())
  416. {
  417. $this->request->setLastEvent('receivedHeaders', $this->response);
  418. }
  419. // for versions lower than 5.2.10, check the redirection URL protocol
  420. if ($this->request->getConfig('follow_redirects') && ! defined('CURLOPT_REDIR_PROTOCOLS') && $this->response->isRedirect())
  421. {
  422. $redirectUrl = new Net_URL2($this->response->getHeader('location'));
  423. if ($redirectUrl->isAbsolute() && ! in_array($redirectUrl->getScheme(), array('http', 'https')))
  424. {
  425. return - 1;
  426. }
  427. }
  428. $this->eventReceivedHeaders = true;
  429. }
  430. }
  431. return strlen($string);
  432. }
  433. /**
  434. * Callback function called by cURL for saving the response body
  435. *
  436. * @param resource cURL handle (not used)
  437. * @param string part of the response body
  438. * @return integer number of bytes saved
  439. * @see HTTP_Request2_Response::appendBody()
  440. */
  441. protected function callbackWriteBody($ch, $string)
  442. {
  443. // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
  444. // response doesn't start with proper HTTP status line (see bug #15716)
  445. if (empty($this->response))
  446. {
  447. throw new HTTP_Request2_Exception("Malformed response: {$string}");
  448. }
  449. if ($this->request->getConfig('store_body'))
  450. {
  451. $this->response->appendBody($string);
  452. }
  453. $this->request->setLastEvent('receivedBodyPart', $string);
  454. return strlen($string);
  455. }
  456. }
  457. ?>