[TASK] Update PEAR packages
[Packages/TYPO3.CMS.git] / typo3 / contrib / pear / HTTP / Request2 / Adapter / Curl.php
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-2012, 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 324746 2012-04-03 15:09:16Z avb $
41 * @link http://pear.php.net/package/HTTP_Request2
42 */
43
44 /**
45 * Base class for HTTP_Request2 adapters
46 */
47 require_once 'HTTP/Request2/Adapter.php';
48
49 /**
50 * Adapter for HTTP_Request2 wrapping around cURL extension
51 *
52 * @category HTTP
53 * @package HTTP_Request2
54 * @author Alexey Borzov <avb@php.net>
55 * @license http://opensource.org/licenses/bsd-license.php New BSD License
56 * @version Release: 2.1.1
57 * @link http://pear.php.net/package/HTTP_Request2
58 */
59 class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
60 {
61 /**
62 * Mapping of header names to cURL options
63 * @var array
64 */
65 protected static $headerMap = array(
66 'accept-encoding' => CURLOPT_ENCODING,
67 'cookie' => CURLOPT_COOKIE,
68 'referer' => CURLOPT_REFERER,
69 'user-agent' => CURLOPT_USERAGENT
70 );
71
72 /**
73 * Mapping of SSL context options to cURL options
74 * @var array
75 */
76 protected static $sslContextMap = array(
77 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
78 'ssl_cafile' => CURLOPT_CAINFO,
79 'ssl_capath' => CURLOPT_CAPATH,
80 'ssl_local_cert' => CURLOPT_SSLCERT,
81 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD
82 );
83
84 /**
85 * Mapping of CURLE_* constants to Exception subclasses and error codes
86 * @var array
87 */
88 protected static $errorMap = array(
89 CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException',
90 HTTP_Request2_Exception::NON_HTTP_REDIRECT),
91 CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),
92 CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'),
93 CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'),
94 // error returned from write callback
95 CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException',
96 HTTP_Request2_Exception::NON_HTTP_REDIRECT),
97 CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException',
98 HTTP_Request2_Exception::TIMEOUT),
99 CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'),
100 CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'),
101 CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException',
102 HTTP_Request2_Exception::MISCONFIGURATION),
103 CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException',
104 HTTP_Request2_Exception::MISCONFIGURATION),
105 CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException',
106 HTTP_Request2_Exception::NON_HTTP_REDIRECT),
107 CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException',
108 HTTP_Request2_Exception::TOO_MANY_REDIRECTS),
109 CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'),
110 CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'),
111 CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException',
112 HTTP_Request2_Exception::MISCONFIGURATION),
113 CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException',
114 HTTP_Request2_Exception::MISCONFIGURATION),
115 CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'),
116 CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'),
117 CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException',
118 HTTP_Request2_Exception::INVALID_ARGUMENT),
119 CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'),
120 CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'),
121 CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'),
122 );
123
124 /**
125 * Response being received
126 * @var HTTP_Request2_Response
127 */
128 protected $response;
129
130 /**
131 * Whether 'sentHeaders' event was sent to observers
132 * @var boolean
133 */
134 protected $eventSentHeaders = false;
135
136 /**
137 * Whether 'receivedHeaders' event was sent to observers
138 * @var boolean
139 */
140 protected $eventReceivedHeaders = false;
141
142 /**
143 * Position within request body
144 * @var integer
145 * @see callbackReadBody()
146 */
147 protected $position = 0;
148
149 /**
150 * Information about last transfer, as returned by curl_getinfo()
151 * @var array
152 */
153 protected $lastInfo;
154
155 /**
156 * Creates a subclass of HTTP_Request2_Exception from curl error data
157 *
158 * @param resource $ch curl handle
159 *
160 * @return HTTP_Request2_Exception
161 */
162 protected static function wrapCurlError($ch)
163 {
164 $nativeCode = curl_errno($ch);
165 $message = 'Curl error: ' . curl_error($ch);
166 if (!isset(self::$errorMap[$nativeCode])) {
167 return new HTTP_Request2_Exception($message, 0, $nativeCode);
168 } else {
169 $class = self::$errorMap[$nativeCode][0];
170 $code = empty(self::$errorMap[$nativeCode][1])
171 ? 0 : self::$errorMap[$nativeCode][1];
172 return new $class($message, $code, $nativeCode);
173 }
174 }
175
176 /**
177 * Sends request to the remote server and returns its response
178 *
179 * @param HTTP_Request2 $request HTTP request message
180 *
181 * @return HTTP_Request2_Response
182 * @throws HTTP_Request2_Exception
183 */
184 public function sendRequest(HTTP_Request2 $request)
185 {
186 if (!extension_loaded('curl')) {
187 throw new HTTP_Request2_LogicException(
188 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION
189 );
190 }
191
192 $this->request = $request;
193 $this->response = null;
194 $this->position = 0;
195 $this->eventSentHeaders = false;
196 $this->eventReceivedHeaders = false;
197
198 try {
199 if (false === curl_exec($ch = $this->createCurlHandle())) {
200 $e = self::wrapCurlError($ch);
201 }
202 } catch (Exception $e) {
203 }
204 if (isset($ch)) {
205 $this->lastInfo = curl_getinfo($ch);
206 curl_close($ch);
207 }
208
209 $response = $this->response;
210 unset($this->request, $this->requestBody, $this->response);
211
212 if (!empty($e)) {
213 throw $e;
214 }
215
216 if ($jar = $request->getCookieJar()) {
217 $jar->addCookiesFromResponse($response, $request->getUrl());
218 }
219
220 if (0 < $this->lastInfo['size_download']) {
221 $request->setLastEvent('receivedBody', $response);
222 }
223 return $response;
224 }
225
226 /**
227 * Returns information about last transfer
228 *
229 * @return array associative array as returned by curl_getinfo()
230 */
231 public function getInfo()
232 {
233 return $this->lastInfo;
234 }
235
236 /**
237 * Creates a new cURL handle and populates it with data from the request
238 *
239 * @return resource a cURL handle, as created by curl_init()
240 * @throws HTTP_Request2_LogicException
241 */
242 protected function createCurlHandle()
243 {
244 $ch = curl_init();
245
246 curl_setopt_array($ch, array(
247 // setup write callbacks
248 CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
249 CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),
250 // buffer size
251 CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),
252 // connection timeout
253 CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
254 // save full outgoing headers, in case someone is interested
255 CURLINFO_HEADER_OUT => true,
256 // request url
257 CURLOPT_URL => $this->request->getUrl()->getUrl()
258 ));
259
260 // set up redirects
261 if (!$this->request->getConfig('follow_redirects')) {
262 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
263 } else {
264 if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {
265 throw new HTTP_Request2_LogicException(
266 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',
267 HTTP_Request2_Exception::MISCONFIGURATION
268 );
269 }
270 curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
271 // limit redirects to http(s), works in 5.2.10+
272 if (defined('CURLOPT_REDIR_PROTOCOLS')) {
273 curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
274 }
275 // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571
276 if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {
277 curl_setopt($ch, CURLOPT_POSTREDIR, 3);
278 }
279 }
280
281 // request timeout
282 if ($timeout = $this->request->getConfig('timeout')) {
283 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
284 }
285
286 // set HTTP version
287 switch ($this->request->getConfig('protocol_version')) {
288 case '1.0':
289 curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
290 break;
291 case '1.1':
292 curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
293 }
294
295 // set request method
296 switch ($this->request->getMethod()) {
297 case HTTP_Request2::METHOD_GET:
298 curl_setopt($ch, CURLOPT_HTTPGET, true);
299 break;
300 case HTTP_Request2::METHOD_POST:
301 curl_setopt($ch, CURLOPT_POST, true);
302 break;
303 case HTTP_Request2::METHOD_HEAD:
304 curl_setopt($ch, CURLOPT_NOBODY, true);
305 break;
306 case HTTP_Request2::METHOD_PUT:
307 curl_setopt($ch, CURLOPT_UPLOAD, true);
308 break;
309 default:
310 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
311 }
312
313 // set proxy, if needed
314 if ($host = $this->request->getConfig('proxy_host')) {
315 if (!($port = $this->request->getConfig('proxy_port'))) {
316 throw new HTTP_Request2_LogicException(
317 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE
318 );
319 }
320 curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
321 if ($user = $this->request->getConfig('proxy_user')) {
322 curl_setopt(
323 $ch, CURLOPT_PROXYUSERPWD,
324 $user . ':' . $this->request->getConfig('proxy_password')
325 );
326 switch ($this->request->getConfig('proxy_auth_scheme')) {
327 case HTTP_Request2::AUTH_BASIC:
328 curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
329 break;
330 case HTTP_Request2::AUTH_DIGEST:
331 curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
332 }
333 }
334 if ($type = $this->request->getConfig('proxy_type')) {
335 switch ($type) {
336 case 'http':
337 curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
338 break;
339 case 'socks5':
340 curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
341 break;
342 default:
343 throw new HTTP_Request2_NotImplementedException(
344 "Proxy type '{$type}' is not supported"
345 );
346 }
347 }
348 }
349
350 // set authentication data
351 if ($auth = $this->request->getAuth()) {
352 curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
353 switch ($auth['scheme']) {
354 case HTTP_Request2::AUTH_BASIC:
355 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
356 break;
357 case HTTP_Request2::AUTH_DIGEST:
358 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
359 }
360 }
361
362 // set SSL options
363 foreach ($this->request->getConfig() as $name => $value) {
364 if ('ssl_verify_host' == $name && null !== $value) {
365 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);
366 } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {
367 curl_setopt($ch, self::$sslContextMap[$name], $value);
368 }
369 }
370
371 $headers = $this->request->getHeaders();
372 // make cURL automagically send proper header
373 if (!isset($headers['accept-encoding'])) {
374 $headers['accept-encoding'] = '';
375 }
376
377 if (($jar = $this->request->getCookieJar())
378 && ($cookies = $jar->getMatching($this->request->getUrl(), true))
379 ) {
380 $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
381 }
382
383 // set headers having special cURL keys
384 foreach (self::$headerMap as $name => $option) {
385 if (isset($headers[$name])) {
386 curl_setopt($ch, $option, $headers[$name]);
387 unset($headers[$name]);
388 }
389 }
390
391 $this->calculateRequestLength($headers);
392 if (isset($headers['content-length'])) {
393 $this->workaroundPhpBug47204($ch, $headers);
394 }
395
396 // set headers not having special keys
397 $headersFmt = array();
398 foreach ($headers as $name => $value) {
399 $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
400 $headersFmt[] = $canonicalName . ': ' . $value;
401 }
402 curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
403
404 return $ch;
405 }
406
407 /**
408 * Workaround for PHP bug #47204 that prevents rewinding request body
409 *
410 * The workaround consists of reading the entire request body into memory
411 * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large
412 * file uploads, use Socket adapter instead.
413 *
414 * @param resource $ch cURL handle
415 * @param array &$headers Request headers
416 */
417 protected function workaroundPhpBug47204($ch, &$headers)
418 {
419 // no redirects, no digest auth -> probably no rewind needed
420 if (!$this->request->getConfig('follow_redirects')
421 && (!($auth = $this->request->getAuth())
422 || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])
423 ) {
424 curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
425
426 } else {
427 // rewind may be needed, read the whole body into memory
428 if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {
429 $this->requestBody = $this->requestBody->__toString();
430
431 } elseif (is_resource($this->requestBody)) {
432 $fp = $this->requestBody;
433 $this->requestBody = '';
434 while (!feof($fp)) {
435 $this->requestBody .= fread($fp, 16384);
436 }
437 }
438 // curl hangs up if content-length is present
439 unset($headers['content-length']);
440 curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
441 }
442 }
443
444 /**
445 * Callback function called by cURL for reading the request body
446 *
447 * @param resource $ch cURL handle
448 * @param resource $fd file descriptor (not used)
449 * @param integer $length maximum length of data to return
450 *
451 * @return string part of the request body, up to $length bytes
452 */
453 protected function callbackReadBody($ch, $fd, $length)
454 {
455 if (!$this->eventSentHeaders) {
456 $this->request->setLastEvent(
457 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
458 );
459 $this->eventSentHeaders = true;
460 }
461 if (in_array($this->request->getMethod(), self::$bodyDisallowed)
462 || 0 == $this->contentLength || $this->position >= $this->contentLength
463 ) {
464 return '';
465 }
466 if (is_string($this->requestBody)) {
467 $string = substr($this->requestBody, $this->position, $length);
468 } elseif (is_resource($this->requestBody)) {
469 $string = fread($this->requestBody, $length);
470 } else {
471 $string = $this->requestBody->read($length);
472 }
473 $this->request->setLastEvent('sentBodyPart', strlen($string));
474 $this->position += strlen($string);
475 return $string;
476 }
477
478 /**
479 * Callback function called by cURL for saving the response headers
480 *
481 * @param resource $ch cURL handle
482 * @param string $string response header (with trailing CRLF)
483 *
484 * @return integer number of bytes saved
485 * @see HTTP_Request2_Response::parseHeaderLine()
486 */
487 protected function callbackWriteHeader($ch, $string)
488 {
489 // we may receive a second set of headers if doing e.g. digest auth
490 if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {
491 // don't bother with 100-Continue responses (bug #15785)
492 if (!$this->eventSentHeaders
493 || $this->response->getStatus() >= 200
494 ) {
495 $this->request->setLastEvent(
496 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
497 );
498 }
499 $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
500 // if body wasn't read by a callback, send event with total body size
501 if ($upload > $this->position) {
502 $this->request->setLastEvent(
503 'sentBodyPart', $upload - $this->position
504 );
505 $this->position = $upload;
506 }
507 if ($upload && (!$this->eventSentHeaders
508 || $this->response->getStatus() >= 200)
509 ) {
510 $this->request->setLastEvent('sentBody', $upload);
511 }
512 $this->eventSentHeaders = true;
513 // we'll need a new response object
514 if ($this->eventReceivedHeaders) {
515 $this->eventReceivedHeaders = false;
516 $this->response = null;
517 }
518 }
519 if (empty($this->response)) {
520 $this->response = new HTTP_Request2_Response(
521 $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)
522 );
523 } else {
524 $this->response->parseHeaderLine($string);
525 if ('' == trim($string)) {
526 // don't bother with 100-Continue responses (bug #15785)
527 if (200 <= $this->response->getStatus()) {
528 $this->request->setLastEvent('receivedHeaders', $this->response);
529 }
530
531 if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {
532 $redirectUrl = new Net_URL2($this->response->getHeader('location'));
533
534 // for versions lower than 5.2.10, check the redirection URL protocol
535 if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()
536 && !in_array($redirectUrl->getScheme(), array('http', 'https'))
537 ) {
538 return -1;
539 }
540
541 if ($jar = $this->request->getCookieJar()) {
542 $jar->addCookiesFromResponse($this->response, $this->request->getUrl());
543 if (!$redirectUrl->isAbsolute()) {
544 $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);
545 }
546 if ($cookies = $jar->getMatching($redirectUrl, true)) {
547 curl_setopt($ch, CURLOPT_COOKIE, $cookies);
548 }
549 }
550 }
551 $this->eventReceivedHeaders = true;
552 }
553 }
554 return strlen($string);
555 }
556
557 /**
558 * Callback function called by cURL for saving the response body
559 *
560 * @param resource $ch cURL handle (not used)
561 * @param string $string part of the response body
562 *
563 * @return integer number of bytes saved
564 * @throws HTTP_Request2_MessageException
565 * @see HTTP_Request2_Response::appendBody()
566 */
567 protected function callbackWriteBody($ch, $string)
568 {
569 // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
570 // response doesn't start with proper HTTP status line (see bug #15716)
571 if (empty($this->response)) {
572 throw new HTTP_Request2_MessageException(
573 "Malformed response: {$string}",
574 HTTP_Request2_Exception::MALFORMED_RESPONSE
575 );
576 }
577 if ($this->request->getConfig('store_body')) {
578 $this->response->appendBody($string);
579 }
580 $this->request->setLastEvent('receivedBodyPart', $string);
581 return strlen($string);
582 }
583 }
584 ?>