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