source: branches/version-2_13-dev/data/module/HTTP/Request2/Adapter/Curl.php @ 23125

Revision 23125, 23.3 KB checked in by kimoto, 11 years ago (diff)

#2275 PEAR更新
不要なrequire_onceの削除
レガシーなPEARモジュールは使わない
SearchReplace?.phpのパスが間違っているので修正

Line 
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 */
47require_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 */
59class 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?>
Note: See TracBrowser for help on using the repository browser.