/libs/PEAR.1.9/HTTP/Request2/Adapter/Curl.php

http://xe-core.googlecode.com/ · PHP · 461 lines · 265 code · 35 blank · 161 comment · 57 complexity · 6fd91528692d4493fa29046597fb4590 MD5 · raw file

  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(
  62. 'accept-encoding' => CURLOPT_ENCODING,
  63. 'cookie' => CURLOPT_COOKIE,
  64. 'referer' => CURLOPT_REFERER,
  65. 'user-agent' => CURLOPT_USERAGENT
  66. );
  67. /**
  68. * Mapping of SSL context options to cURL options
  69. * @var array
  70. */
  71. protected static $sslContextMap = array(
  72. 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
  73. 'ssl_cafile' => CURLOPT_CAINFO,
  74. 'ssl_capath' => CURLOPT_CAPATH,
  75. 'ssl_local_cert' => CURLOPT_SSLCERT,
  76. 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD
  77. );
  78. /**
  79. * Response being received
  80. * @var HTTP_Request2_Response
  81. */
  82. protected $response;
  83. /**
  84. * Whether 'sentHeaders' event was sent to observers
  85. * @var boolean
  86. */
  87. protected $eventSentHeaders = false;
  88. /**
  89. * Whether 'receivedHeaders' event was sent to observers
  90. * @var boolean
  91. */
  92. protected $eventReceivedHeaders = false;
  93. /**
  94. * Position within request body
  95. * @var integer
  96. * @see callbackReadBody()
  97. */
  98. protected $position = 0;
  99. /**
  100. * Information about last transfer, as returned by curl_getinfo()
  101. * @var array
  102. */
  103. protected $lastInfo;
  104. /**
  105. * Sends request to the remote server and returns its response
  106. *
  107. * @param HTTP_Request2
  108. * @return HTTP_Request2_Response
  109. * @throws HTTP_Request2_Exception
  110. */
  111. public function sendRequest(HTTP_Request2 $request)
  112. {
  113. if (!extension_loaded('curl')) {
  114. throw new HTTP_Request2_Exception('cURL extension not available');
  115. }
  116. $this->request = $request;
  117. $this->response = null;
  118. $this->position = 0;
  119. $this->eventSentHeaders = false;
  120. $this->eventReceivedHeaders = false;
  121. try {
  122. if (false === curl_exec($ch = $this->createCurlHandle())) {
  123. $errorMessage = 'Error sending request: #' . curl_errno($ch) .
  124. ' ' . curl_error($ch);
  125. }
  126. } catch (Exception $e) {
  127. }
  128. $this->lastInfo = curl_getinfo($ch);
  129. curl_close($ch);
  130. $response = $this->response;
  131. unset($this->request, $this->requestBody, $this->response);
  132. if (!empty($e)) {
  133. throw $e;
  134. } elseif (!empty($errorMessage)) {
  135. throw new HTTP_Request2_Exception($errorMessage);
  136. }
  137. if (0 < $this->lastInfo['size_download']) {
  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(
  161. // setup write callbacks
  162. CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
  163. CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),
  164. // buffer size
  165. CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),
  166. // connection timeout
  167. CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
  168. // save full outgoing headers, in case someone is interested
  169. CURLINFO_HEADER_OUT => true,
  170. // request url
  171. CURLOPT_URL => $this->request->getUrl()->getUrl()
  172. ));
  173. // set up redirects
  174. if (!$this->request->getConfig('follow_redirects')) {
  175. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
  176. } else {
  177. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  178. curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
  179. // limit redirects to http(s), works in 5.2.10+
  180. if (defined('CURLOPT_REDIR_PROTOCOLS')) {
  181. curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
  182. }
  183. // works sometime after 5.3.0, http://bugs.php.net/bug.php?id=49571
  184. if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR ')) {
  185. curl_setopt($ch, CURLOPT_POSTREDIR, 3);
  186. }
  187. }
  188. // request timeout
  189. if ($timeout = $this->request->getConfig('timeout')) {
  190. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
  191. }
  192. // set HTTP version
  193. switch ($this->request->getConfig('protocol_version')) {
  194. case '1.0':
  195. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
  196. break;
  197. case '1.1':
  198. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  199. }
  200. // set request method
  201. switch ($this->request->getMethod()) {
  202. case HTTP_Request2::METHOD_GET:
  203. curl_setopt($ch, CURLOPT_HTTPGET, true);
  204. break;
  205. case HTTP_Request2::METHOD_POST:
  206. curl_setopt($ch, CURLOPT_POST, true);
  207. break;
  208. case HTTP_Request2::METHOD_HEAD:
  209. curl_setopt($ch, CURLOPT_NOBODY, true);
  210. break;
  211. default:
  212. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
  213. }
  214. // set proxy, if needed
  215. if ($host = $this->request->getConfig('proxy_host')) {
  216. if (!($port = $this->request->getConfig('proxy_port'))) {
  217. throw new HTTP_Request2_Exception('Proxy port not provided');
  218. }
  219. curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
  220. if ($user = $this->request->getConfig('proxy_user')) {
  221. curl_setopt($ch, CURLOPT_PROXYUSERPWD, $user . ':' .
  222. $this->request->getConfig('proxy_password'));
  223. switch ($this->request->getConfig('proxy_auth_scheme')) {
  224. case HTTP_Request2::AUTH_BASIC:
  225. curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
  226. break;
  227. case HTTP_Request2::AUTH_DIGEST:
  228. curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
  229. }
  230. }
  231. }
  232. // set authentication data
  233. if ($auth = $this->request->getAuth()) {
  234. curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
  235. switch ($auth['scheme']) {
  236. case HTTP_Request2::AUTH_BASIC:
  237. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
  238. break;
  239. case HTTP_Request2::AUTH_DIGEST:
  240. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
  241. }
  242. }
  243. // set SSL options
  244. if (0 == strcasecmp($this->request->getUrl()->getScheme(), 'https')) {
  245. foreach ($this->request->getConfig() as $name => $value) {
  246. if ('ssl_verify_host' == $name && null !== $value) {
  247. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);
  248. } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {
  249. curl_setopt($ch, self::$sslContextMap[$name], $value);
  250. }
  251. }
  252. }
  253. $headers = $this->request->getHeaders();
  254. // make cURL automagically send proper header
  255. if (!isset($headers['accept-encoding'])) {
  256. $headers['accept-encoding'] = '';
  257. }
  258. // set headers having special cURL keys
  259. foreach (self::$headerMap as $name => $option) {
  260. if (isset($headers[$name])) {
  261. curl_setopt($ch, $option, $headers[$name]);
  262. unset($headers[$name]);
  263. }
  264. }
  265. $this->calculateRequestLength($headers);
  266. if (isset($headers['content-length'])) {
  267. $this->workaroundPhpBug47204($ch, $headers);
  268. }
  269. // set headers not having special keys
  270. $headersFmt = array();
  271. foreach ($headers as $name => $value) {
  272. $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
  273. $headersFmt[] = $canonicalName . ': ' . $value;
  274. }
  275. curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
  276. return $ch;
  277. }
  278. /**
  279. * Workaround for PHP bug #47204 that prevents rewinding request body
  280. *
  281. * The workaround consists of reading the entire request body into memory
  282. * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large
  283. * file uploads, use Socket adapter instead.
  284. *
  285. * @param resource cURL handle
  286. * @param array Request headers
  287. */
  288. protected function workaroundPhpBug47204($ch, &$headers)
  289. {
  290. // no redirects, no digest auth -> probably no rewind needed
  291. if (!$this->request->getConfig('follow_redirects')
  292. && (!($auth = $this->request->getAuth())
  293. || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])
  294. ) {
  295. curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
  296. // rewind may be needed, read the whole body into memory
  297. } else {
  298. if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {
  299. $this->requestBody = $this->requestBody->__toString();
  300. } elseif (is_resource($this->requestBody)) {
  301. $fp = $this->requestBody;
  302. $this->requestBody = '';
  303. while (!feof($fp)) {
  304. $this->requestBody .= fread($fp, 16384);
  305. }
  306. }
  307. // curl hangs up if content-length is present
  308. unset($headers['content-length']);
  309. curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
  310. }
  311. }
  312. /**
  313. * Callback function called by cURL for reading the request body
  314. *
  315. * @param resource cURL handle
  316. * @param resource file descriptor (not used)
  317. * @param integer maximum length of data to return
  318. * @return string part of the request body, up to $length bytes
  319. */
  320. protected function callbackReadBody($ch, $fd, $length)
  321. {
  322. if (!$this->eventSentHeaders) {
  323. $this->request->setLastEvent(
  324. 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
  325. );
  326. $this->eventSentHeaders = true;
  327. }
  328. if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||
  329. 0 == $this->contentLength || $this->position >= $this->contentLength
  330. ) {
  331. return '';
  332. }
  333. if (is_string($this->requestBody)) {
  334. $string = substr($this->requestBody, $this->position, $length);
  335. } elseif (is_resource($this->requestBody)) {
  336. $string = fread($this->requestBody, $length);
  337. } else {
  338. $string = $this->requestBody->read($length);
  339. }
  340. $this->request->setLastEvent('sentBodyPart', strlen($string));
  341. $this->position += strlen($string);
  342. return $string;
  343. }
  344. /**
  345. * Callback function called by cURL for saving the response headers
  346. *
  347. * @param resource cURL handle
  348. * @param string response header (with trailing CRLF)
  349. * @return integer number of bytes saved
  350. * @see HTTP_Request2_Response::parseHeaderLine()
  351. */
  352. protected function callbackWriteHeader($ch, $string)
  353. {
  354. // we may receive a second set of headers if doing e.g. digest auth
  355. if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {
  356. // don't bother with 100-Continue responses (bug #15785)
  357. if (!$this->eventSentHeaders ||
  358. $this->response->getStatus() >= 200
  359. ) {
  360. $this->request->setLastEvent(
  361. 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
  362. );
  363. }
  364. $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
  365. // if body wasn't read by a callback, send event with total body size
  366. if ($upload > $this->position) {
  367. $this->request->setLastEvent(
  368. 'sentBodyPart', $upload - $this->position
  369. );
  370. $this->position = $upload;
  371. }
  372. $this->eventSentHeaders = true;
  373. // we'll need a new response object
  374. if ($this->eventReceivedHeaders) {
  375. $this->eventReceivedHeaders = false;
  376. $this->response = null;
  377. }
  378. }
  379. if (empty($this->response)) {
  380. $this->response = new HTTP_Request2_Response($string, false);
  381. } else {
  382. $this->response->parseHeaderLine($string);
  383. if ('' == trim($string)) {
  384. // don't bother with 100-Continue responses (bug #15785)
  385. if (200 <= $this->response->getStatus()) {
  386. $this->request->setLastEvent('receivedHeaders', $this->response);
  387. }
  388. // for versions lower than 5.2.10, check the redirection URL protocol
  389. if ($this->request->getConfig('follow_redirects') && !defined('CURLOPT_REDIR_PROTOCOLS')
  390. && $this->response->isRedirect()
  391. ) {
  392. $redirectUrl = new Net_URL2($this->response->getHeader('location'));
  393. if ($redirectUrl->isAbsolute()
  394. && !in_array($redirectUrl->getScheme(), array('http', 'https'))
  395. ) {
  396. return -1;
  397. }
  398. }
  399. $this->eventReceivedHeaders = true;
  400. }
  401. }
  402. return strlen($string);
  403. }
  404. /**
  405. * Callback function called by cURL for saving the response body
  406. *
  407. * @param resource cURL handle (not used)
  408. * @param string part of the response body
  409. * @return integer number of bytes saved
  410. * @see HTTP_Request2_Response::appendBody()
  411. */
  412. protected function callbackWriteBody($ch, $string)
  413. {
  414. // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
  415. // response doesn't start with proper HTTP status line (see bug #15716)
  416. if (empty($this->response)) {
  417. throw new HTTP_Request2_Exception("Malformed response: {$string}");
  418. }
  419. if ($this->request->getConfig('store_body')) {
  420. $this->response->appendBody($string);
  421. }
  422. $this->request->setLastEvent('receivedBodyPart', $string);
  423. return strlen($string);
  424. }
  425. }
  426. ?>