]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - extlib/HTTP/Request2/Adapter/Curl.php
Merge branch '1.0.x' into testing
[quix0rs-gnu-social.git] / extlib / HTTP / Request2 / Adapter / Curl.php
index 4d4de0dcc7c1740dc1c64981257496974989494f..fecfbd7abceb7d48f39de33d90eaa55923a2486d 100644 (file)
@@ -6,7 +6,7 @@
  *\r
  * LICENSE:\r
  *\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
  * All rights reserved.\r
  *\r
  * Redistribution and use in source and binary forms, with or without\r
@@ -37,7 +37,7 @@
  * @package    HTTP_Request2\r
  * @author     Alexey Borzov <avb@php.net>\r
  * @license    http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version    CVS: $Id: Curl.php 278226 2009-04-03 21:32:48Z avb $\r
+ * @version    SVN: $Id: Curl.php 310800 2011-05-06 07:29:56Z avb $\r
  * @link       http://pear.php.net/package/HTTP_Request2\r
  */\r
 \r
@@ -52,7 +52,7 @@ require_once 'HTTP/Request2/Adapter.php';
  * @category    HTTP\r
  * @package     HTTP_Request2\r
  * @author      Alexey Borzov <avb@php.net>\r
- * @version     Release: 0.4.1\r
+ * @version     Release: 2.0.0RC1\r
  */\r
 class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter\r
 {\r
@@ -79,6 +79,46 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
         'ssl_passphrase'  => CURLOPT_SSLCERTPASSWD\r
    );\r
 \r
+   /**\r
+    * Mapping of CURLE_* constants to Exception subclasses and error codes\r
+    * @var  array\r
+    */\r
+    protected static $errorMap = array(\r
+        CURLE_UNSUPPORTED_PROTOCOL  => array('HTTP_Request2_MessageException',\r
+                                             HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+        CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),\r
+        CURLE_COULDNT_RESOLVE_HOST  => array('HTTP_Request2_ConnectionException'),\r
+        CURLE_COULDNT_CONNECT       => array('HTTP_Request2_ConnectionException'),\r
+        // error returned from write callback\r
+        CURLE_WRITE_ERROR           => array('HTTP_Request2_MessageException',\r
+                                             HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+        CURLE_OPERATION_TIMEOUTED   => array('HTTP_Request2_MessageException',\r
+                                             HTTP_Request2_Exception::TIMEOUT),\r
+        CURLE_HTTP_RANGE_ERROR      => array('HTTP_Request2_MessageException'),\r
+        CURLE_SSL_CONNECT_ERROR     => array('HTTP_Request2_ConnectionException'),\r
+        CURLE_LIBRARY_NOT_FOUND     => array('HTTP_Request2_LogicException',\r
+                                             HTTP_Request2_Exception::MISCONFIGURATION),\r
+        CURLE_FUNCTION_NOT_FOUND    => array('HTTP_Request2_LogicException',\r
+                                             HTTP_Request2_Exception::MISCONFIGURATION),\r
+        CURLE_ABORTED_BY_CALLBACK   => array('HTTP_Request2_MessageException',\r
+                                             HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+        CURLE_TOO_MANY_REDIRECTS    => array('HTTP_Request2_MessageException',\r
+                                             HTTP_Request2_Exception::TOO_MANY_REDIRECTS),\r
+        CURLE_SSL_PEER_CERTIFICATE  => array('HTTP_Request2_ConnectionException'),\r
+        CURLE_GOT_NOTHING           => array('HTTP_Request2_MessageException'),\r
+        CURLE_SSL_ENGINE_NOTFOUND   => array('HTTP_Request2_LogicException',\r
+                                             HTTP_Request2_Exception::MISCONFIGURATION),\r
+        CURLE_SSL_ENGINE_SETFAILED  => array('HTTP_Request2_LogicException',\r
+                                             HTTP_Request2_Exception::MISCONFIGURATION),\r
+        CURLE_SEND_ERROR            => array('HTTP_Request2_MessageException'),\r
+        CURLE_RECV_ERROR            => array('HTTP_Request2_MessageException'),\r
+        CURLE_SSL_CERTPROBLEM       => array('HTTP_Request2_LogicException',\r
+                                             HTTP_Request2_Exception::INVALID_ARGUMENT),\r
+        CURLE_SSL_CIPHER            => array('HTTP_Request2_ConnectionException'),\r
+        CURLE_SSL_CACERT            => array('HTTP_Request2_ConnectionException'),\r
+        CURLE_BAD_CONTENT_ENCODING  => array('HTTP_Request2_MessageException'),\r
+    );\r
+\r
    /**\r
     * Response being received\r
     * @var  HTTP_Request2_Response\r
@@ -110,6 +150,26 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
     */\r
     protected $lastInfo;\r
 \r
+   /**\r
+    * Creates a subclass of HTTP_Request2_Exception from curl error data\r
+    *\r
+    * @param resource curl handle\r
+    * @return HTTP_Request2_Exception\r
+    */\r
+    protected static function wrapCurlError($ch)\r
+    {\r
+        $nativeCode = curl_errno($ch);\r
+        $message    = 'Curl error: ' . curl_error($ch);\r
+        if (!isset(self::$errorMap[$nativeCode])) {\r
+            return new HTTP_Request2_Exception($message, 0, $nativeCode);\r
+        } else {\r
+            $class = self::$errorMap[$nativeCode][0];\r
+            $code  = empty(self::$errorMap[$nativeCode][1])\r
+                     ? 0 : self::$errorMap[$nativeCode][1];\r
+            return new $class($message, $code, $nativeCode);\r
+        }\r
+    }\r
+\r
    /**\r
     * Sends request to the remote server and returns its response\r
     *\r
@@ -120,7 +180,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
     public function sendRequest(HTTP_Request2 $request)\r
     {\r
         if (!extension_loaded('curl')) {\r
-            throw new HTTP_Request2_Exception('cURL extension not available');\r
+            throw new HTTP_Request2_LogicException(\r
+                'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION\r
+            );\r
         }\r
 \r
         $this->request              = $request;\r
@@ -131,24 +193,30 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
 \r
         try {\r
             if (false === curl_exec($ch = $this->createCurlHandle())) {\r
-                $errorMessage = 'Error sending request: #' . curl_errno($ch) .\r
-                                                       ' ' . curl_error($ch);\r
+                $e = self::wrapCurlError($ch);\r
             }\r
         } catch (Exception $e) {\r
         }\r
-        $this->lastInfo = curl_getinfo($ch);\r
-        curl_close($ch);\r
+        if (isset($ch)) {\r
+            $this->lastInfo = curl_getinfo($ch);\r
+            curl_close($ch);\r
+        }\r
+\r
+        $response = $this->response;\r
+        unset($this->request, $this->requestBody, $this->response);\r
 \r
         if (!empty($e)) {\r
             throw $e;\r
-        } elseif (!empty($errorMessage)) {\r
-            throw new HTTP_Request2_Exception($errorMessage);\r
+        }\r
+\r
+        if ($jar = $request->getCookieJar()) {\r
+            $jar->addCookiesFromResponse($response, $request->getUrl());\r
         }\r
 \r
         if (0 < $this->lastInfo['size_download']) {\r
-            $this->request->setLastEvent('receivedBody', $this->response);\r
+            $request->setLastEvent('receivedBody', $response);\r
         }\r
-        return $this->response;\r
+        return $response;\r
     }\r
 \r
    /**\r
@@ -165,19 +233,16 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
     * Creates a new cURL handle and populates it with data from the request\r
     *\r
     * @return   resource    a cURL handle, as created by curl_init()\r
-    * @throws   HTTP_Request2_Exception\r
+    * @throws   HTTP_Request2_LogicException\r
     */\r
     protected function createCurlHandle()\r
     {\r
         $ch = curl_init();\r
 \r
         curl_setopt_array($ch, array(\r
-            // setup callbacks\r
-            CURLOPT_READFUNCTION   => array($this, 'callbackReadBody'),\r
+            // setup write callbacks\r
             CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),\r
             CURLOPT_WRITEFUNCTION  => array($this, 'callbackWriteBody'),\r
-            // disallow redirects\r
-            CURLOPT_FOLLOWLOCATION => false,\r
             // buffer size\r
             CURLOPT_BUFFERSIZE     => $this->request->getConfig('buffer_size'),\r
             // connection timeout\r
@@ -188,6 +253,27 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
             CURLOPT_URL            => $this->request->getUrl()->getUrl()\r
         ));\r
 \r
+        // set up redirects\r
+        if (!$this->request->getConfig('follow_redirects')) {\r
+            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);\r
+        } else {\r
+            if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {\r
+                throw new HTTP_Request2_LogicException(\r
+                    'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',\r
+                    HTTP_Request2_Exception::MISCONFIGURATION\r
+                );\r
+            }\r
+            curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));\r
+            // limit redirects to http(s), works in 5.2.10+\r
+            if (defined('CURLOPT_REDIR_PROTOCOLS')) {\r
+                curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);\r
+            }\r
+            // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571\r
+            if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {\r
+                curl_setopt($ch, CURLOPT_POSTREDIR, 3);\r
+            }\r
+        }\r
+\r
         // request timeout\r
         if ($timeout = $this->request->getConfig('timeout')) {\r
             curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);\r
@@ -210,6 +296,12 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
             case HTTP_Request2::METHOD_POST:\r
                 curl_setopt($ch, CURLOPT_POST, true);\r
                 break;\r
+            case HTTP_Request2::METHOD_HEAD:\r
+                curl_setopt($ch, CURLOPT_NOBODY, true);\r
+                break;\r
+            case HTTP_Request2::METHOD_PUT:\r
+                curl_setopt($ch, CURLOPT_UPLOAD, true);\r
+                break;\r
             default:\r
                 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());\r
         }\r
@@ -217,7 +309,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
         // set proxy, if needed\r
         if ($host = $this->request->getConfig('proxy_host')) {\r
             if (!($port = $this->request->getConfig('proxy_port'))) {\r
-                throw new HTTP_Request2_Exception('Proxy port not provided');\r
+                throw new HTTP_Request2_LogicException(\r
+                    'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE\r
+                );\r
             }\r
             curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);\r
             if ($user = $this->request->getConfig('proxy_user')) {\r
@@ -246,13 +340,11 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
         }\r
 \r
         // set SSL options\r
-        if (0 == strcasecmp($this->request->getUrl()->getScheme(), 'https')) {\r
-            foreach ($this->request->getConfig() as $name => $value) {\r
-                if ('ssl_verify_host' == $name && null !== $value) {\r
-                    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);\r
-                } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {\r
-                    curl_setopt($ch, self::$sslContextMap[$name], $value);\r
-                }\r
+        foreach ($this->request->getConfig() as $name => $value) {\r
+            if ('ssl_verify_host' == $name && null !== $value) {\r
+                curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);\r
+            } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {\r
+                curl_setopt($ch, self::$sslContextMap[$name], $value);\r
             }\r
         }\r
 \r
@@ -262,6 +354,12 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
             $headers['accept-encoding'] = '';\r
         }\r
 \r
+        if (($jar = $this->request->getCookieJar())\r
+            && ($cookies = $jar->getMatching($this->request->getUrl(), true))\r
+        ) {\r
+            $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;\r
+        }\r
+\r
         // set headers having special cURL keys\r
         foreach (self::$headerMap as $name => $option) {\r
             if (isset($headers[$name])) {\r
@@ -271,6 +369,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
         }\r
 \r
         $this->calculateRequestLength($headers);\r
+        if (isset($headers['content-length'])) {\r
+            $this->workaroundPhpBug47204($ch, $headers);\r
+        }\r
 \r
         // set headers not having special keys\r
         $headersFmt = array();\r
@@ -283,13 +384,50 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
         return $ch;\r
     }\r
 \r
+   /**\r
+    * Workaround for PHP bug #47204 that prevents rewinding request body\r
+    *\r
+    * The workaround consists of reading the entire request body into memory\r
+    * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large\r
+    * file uploads, use Socket adapter instead.\r
+    *\r
+    * @param    resource    cURL handle\r
+    * @param    array       Request headers\r
+    */\r
+    protected function workaroundPhpBug47204($ch, &$headers)\r
+    {\r
+        // no redirects, no digest auth -> probably no rewind needed\r
+        if (!$this->request->getConfig('follow_redirects')\r
+            && (!($auth = $this->request->getAuth())\r
+                || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])\r
+        ) {\r
+            curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));\r
+\r
+        // rewind may be needed, read the whole body into memory\r
+        } else {\r
+            if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {\r
+                $this->requestBody = $this->requestBody->__toString();\r
+\r
+            } elseif (is_resource($this->requestBody)) {\r
+                $fp = $this->requestBody;\r
+                $this->requestBody = '';\r
+                while (!feof($fp)) {\r
+                    $this->requestBody .= fread($fp, 16384);\r
+                }\r
+            }\r
+            // curl hangs up if content-length is present\r
+            unset($headers['content-length']);\r
+            curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);\r
+        }\r
+    }\r
+\r
    /**\r
     * Callback function called by cURL for reading the request body\r
     *\r
     * @param    resource    cURL handle\r
     * @param    resource    file descriptor (not used)\r
     * @param    integer     maximum length of data to return\r
-    * @return   string      part of the request body, up to $length bytes \r
+    * @return   string      part of the request body, up to $length bytes\r
     */\r
     protected function callbackReadBody($ch, $fd, $length)\r
     {\r
@@ -336,6 +474,19 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
                     'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)\r
                 );\r
             }\r
+            $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);\r
+            // if body wasn't read by a callback, send event with total body size\r
+            if ($upload > $this->position) {\r
+                $this->request->setLastEvent(\r
+                    'sentBodyPart', $upload - $this->position\r
+                );\r
+                $this->position = $upload;\r
+            }\r
+            if ($upload && (!$this->eventSentHeaders\r
+                            || $this->response->getStatus() >= 200)\r
+            ) {\r
+                $this->request->setLastEvent('sentBody', $upload);\r
+            }\r
             $this->eventSentHeaders = true;\r
             // we'll need a new response object\r
             if ($this->eventReceivedHeaders) {\r
@@ -344,7 +495,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
             }\r
         }\r
         if (empty($this->response)) {\r
-            $this->response = new HTTP_Request2_Response($string, false);\r
+            $this->response = new HTTP_Request2_Response(\r
+                $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)\r
+            );\r
         } else {\r
             $this->response->parseHeaderLine($string);\r
             if ('' == trim($string)) {\r
@@ -352,6 +505,27 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
                 if (200 <= $this->response->getStatus()) {\r
                     $this->request->setLastEvent('receivedHeaders', $this->response);\r
                 }\r
+\r
+                if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {\r
+                    $redirectUrl = new Net_URL2($this->response->getHeader('location'));\r
+\r
+                    // for versions lower than 5.2.10, check the redirection URL protocol\r
+                    if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()\r
+                        && !in_array($redirectUrl->getScheme(), array('http', 'https'))\r
+                    ) {\r
+                        return -1;\r
+                    }\r
+\r
+                    if ($jar = $this->request->getCookieJar()) {\r
+                        $jar->addCookiesFromResponse($this->response, $this->request->getUrl());\r
+                        if (!$redirectUrl->isAbsolute()) {\r
+                            $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);\r
+                        }\r
+                        if ($cookies = $jar->getMatching($redirectUrl, true)) {\r
+                            curl_setopt($ch, CURLOPT_COOKIE, $cookies);\r
+                        }\r
+                    }\r
+                }\r
                 $this->eventReceivedHeaders = true;\r
             }\r
         }\r
@@ -368,10 +542,13 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
     */\r
     protected function callbackWriteBody($ch, $string)\r
     {\r
-        // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if \r
+        // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if\r
         // response doesn't start with proper HTTP status line (see bug #15716)\r
         if (empty($this->response)) {\r
-            throw new HTTP_Request2_Exception("Malformed response: {$string}");\r
+            throw new HTTP_Request2_MessageException(\r
+                "Malformed response: {$string}",\r
+                HTTP_Request2_Exception::MALFORMED_RESPONSE\r
+            );\r
         }\r
         if ($this->request->getConfig('store_body')) {\r
             $this->response->appendBody($string);\r