]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/HTTP/Request2/Adapter/Curl.php
Rebuilt HTTPClient class as an extension of PEAR HTTP_Request2 package, adding redire...
[quix0rs-gnu-social.git] / extlib / HTTP / Request2 / Adapter / Curl.php
1 <?php\r
2 /**\r
3  * Adapter for HTTP_Request2 wrapping around cURL extension\r
4  *\r
5  * PHP version 5\r
6  *\r
7  * LICENSE:\r
8  *\r
9  * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
10  * All rights reserved.\r
11  *\r
12  * Redistribution and use in source and binary forms, with or without\r
13  * modification, are permitted provided that the following conditions\r
14  * are met:\r
15  *\r
16  *    * Redistributions of source code must retain the above copyright\r
17  *      notice, this list of conditions and the following disclaimer.\r
18  *    * Redistributions in binary form must reproduce the above copyright\r
19  *      notice, this list of conditions and the following disclaimer in the\r
20  *      documentation and/or other materials provided with the distribution.\r
21  *    * The names of the authors may not be used to endorse or promote products\r
22  *      derived from this software without specific prior written permission.\r
23  *\r
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS\r
25  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\r
26  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
27  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\r
28  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\r
29  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
30  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\r
31  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY\r
32  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\r
33  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
34  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
35  *\r
36  * @category   HTTP\r
37  * @package    HTTP_Request2\r
38  * @author     Alexey Borzov <avb@php.net>\r
39  * @license    http://opensource.org/licenses/bsd-license.php New BSD License\r
40  * @version    CVS: $Id: Curl.php 278226 2009-04-03 21:32:48Z avb $\r
41  * @link       http://pear.php.net/package/HTTP_Request2\r
42  */\r
43 \r
44 /**\r
45  * Base class for HTTP_Request2 adapters\r
46  */\r
47 require_once 'HTTP/Request2/Adapter.php';\r
48 \r
49 /**\r
50  * Adapter for HTTP_Request2 wrapping around cURL extension\r
51  *\r
52  * @category    HTTP\r
53  * @package     HTTP_Request2\r
54  * @author      Alexey Borzov <avb@php.net>\r
55  * @version     Release: 0.4.1\r
56  */\r
57 class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter\r
58 {\r
59    /**\r
60     * Mapping of header names to cURL options\r
61     * @var  array\r
62     */\r
63     protected static $headerMap = array(\r
64         'accept-encoding' => CURLOPT_ENCODING,\r
65         'cookie'          => CURLOPT_COOKIE,\r
66         'referer'         => CURLOPT_REFERER,\r
67         'user-agent'      => CURLOPT_USERAGENT\r
68     );\r
69 \r
70    /**\r
71     * Mapping of SSL context options to cURL options\r
72     * @var  array\r
73     */\r
74     protected static $sslContextMap = array(\r
75         'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,\r
76         'ssl_cafile'      => CURLOPT_CAINFO,\r
77         'ssl_capath'      => CURLOPT_CAPATH,\r
78         'ssl_local_cert'  => CURLOPT_SSLCERT,\r
79         'ssl_passphrase'  => CURLOPT_SSLCERTPASSWD\r
80    );\r
81 \r
82    /**\r
83     * Response being received\r
84     * @var  HTTP_Request2_Response\r
85     */\r
86     protected $response;\r
87 \r
88    /**\r
89     * Whether 'sentHeaders' event was sent to observers\r
90     * @var  boolean\r
91     */\r
92     protected $eventSentHeaders = false;\r
93 \r
94    /**\r
95     * Whether 'receivedHeaders' event was sent to observers\r
96     * @var boolean\r
97     */\r
98     protected $eventReceivedHeaders = false;\r
99 \r
100    /**\r
101     * Position within request body\r
102     * @var  integer\r
103     * @see  callbackReadBody()\r
104     */\r
105     protected $position = 0;\r
106 \r
107    /**\r
108     * Information about last transfer, as returned by curl_getinfo()\r
109     * @var  array\r
110     */\r
111     protected $lastInfo;\r
112 \r
113    /**\r
114     * Sends request to the remote server and returns its response\r
115     *\r
116     * @param    HTTP_Request2\r
117     * @return   HTTP_Request2_Response\r
118     * @throws   HTTP_Request2_Exception\r
119     */\r
120     public function sendRequest(HTTP_Request2 $request)\r
121     {\r
122         if (!extension_loaded('curl')) {\r
123             throw new HTTP_Request2_Exception('cURL extension not available');\r
124         }\r
125 \r
126         $this->request              = $request;\r
127         $this->response             = null;\r
128         $this->position             = 0;\r
129         $this->eventSentHeaders     = false;\r
130         $this->eventReceivedHeaders = false;\r
131 \r
132         try {\r
133             if (false === curl_exec($ch = $this->createCurlHandle())) {\r
134                 $errorMessage = 'Error sending request: #' . curl_errno($ch) .\r
135                                                        ' ' . curl_error($ch);\r
136             }\r
137         } catch (Exception $e) {\r
138         }\r
139         $this->lastInfo = curl_getinfo($ch);\r
140         curl_close($ch);\r
141 \r
142         if (!empty($e)) {\r
143             throw $e;\r
144         } elseif (!empty($errorMessage)) {\r
145             throw new HTTP_Request2_Exception($errorMessage);\r
146         }\r
147 \r
148         if (0 < $this->lastInfo['size_download']) {\r
149             $this->request->setLastEvent('receivedBody', $this->response);\r
150         }\r
151         return $this->response;\r
152     }\r
153 \r
154    /**\r
155     * Returns information about last transfer\r
156     *\r
157     * @return   array   associative array as returned by curl_getinfo()\r
158     */\r
159     public function getInfo()\r
160     {\r
161         return $this->lastInfo;\r
162     }\r
163 \r
164    /**\r
165     * Creates a new cURL handle and populates it with data from the request\r
166     *\r
167     * @return   resource    a cURL handle, as created by curl_init()\r
168     * @throws   HTTP_Request2_Exception\r
169     */\r
170     protected function createCurlHandle()\r
171     {\r
172         $ch = curl_init();\r
173 \r
174         curl_setopt_array($ch, array(\r
175             // setup callbacks\r
176             CURLOPT_READFUNCTION   => array($this, 'callbackReadBody'),\r
177             CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),\r
178             CURLOPT_WRITEFUNCTION  => array($this, 'callbackWriteBody'),\r
179             // disallow redirects\r
180             CURLOPT_FOLLOWLOCATION => false,\r
181             // buffer size\r
182             CURLOPT_BUFFERSIZE     => $this->request->getConfig('buffer_size'),\r
183             // connection timeout\r
184             CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),\r
185             // save full outgoing headers, in case someone is interested\r
186             CURLINFO_HEADER_OUT    => true,\r
187             // request url\r
188             CURLOPT_URL            => $this->request->getUrl()->getUrl()\r
189         ));\r
190 \r
191         // request timeout\r
192         if ($timeout = $this->request->getConfig('timeout')) {\r
193             curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);\r
194         }\r
195 \r
196         // set HTTP version\r
197         switch ($this->request->getConfig('protocol_version')) {\r
198             case '1.0':\r
199                 curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);\r
200                 break;\r
201             case '1.1':\r
202                 curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);\r
203         }\r
204 \r
205         // set request method\r
206         switch ($this->request->getMethod()) {\r
207             case HTTP_Request2::METHOD_GET:\r
208                 curl_setopt($ch, CURLOPT_HTTPGET, true);\r
209                 break;\r
210             case HTTP_Request2::METHOD_POST:\r
211                 curl_setopt($ch, CURLOPT_POST, true);\r
212                 break;\r
213             default:\r
214                 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());\r
215         }\r
216 \r
217         // set proxy, if needed\r
218         if ($host = $this->request->getConfig('proxy_host')) {\r
219             if (!($port = $this->request->getConfig('proxy_port'))) {\r
220                 throw new HTTP_Request2_Exception('Proxy port not provided');\r
221             }\r
222             curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);\r
223             if ($user = $this->request->getConfig('proxy_user')) {\r
224                 curl_setopt($ch, CURLOPT_PROXYUSERPWD, $user . ':' .\r
225                             $this->request->getConfig('proxy_password'));\r
226                 switch ($this->request->getConfig('proxy_auth_scheme')) {\r
227                     case HTTP_Request2::AUTH_BASIC:\r
228                         curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);\r
229                         break;\r
230                     case HTTP_Request2::AUTH_DIGEST:\r
231                         curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);\r
232                 }\r
233             }\r
234         }\r
235 \r
236         // set authentication data\r
237         if ($auth = $this->request->getAuth()) {\r
238             curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);\r
239             switch ($auth['scheme']) {\r
240                 case HTTP_Request2::AUTH_BASIC:\r
241                     curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);\r
242                     break;\r
243                 case HTTP_Request2::AUTH_DIGEST:\r
244                     curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);\r
245             }\r
246         }\r
247 \r
248         // set SSL options\r
249         if (0 == strcasecmp($this->request->getUrl()->getScheme(), 'https')) {\r
250             foreach ($this->request->getConfig() as $name => $value) {\r
251                 if ('ssl_verify_host' == $name && null !== $value) {\r
252                     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);\r
253                 } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {\r
254                     curl_setopt($ch, self::$sslContextMap[$name], $value);\r
255                 }\r
256             }\r
257         }\r
258 \r
259         $headers = $this->request->getHeaders();\r
260         // make cURL automagically send proper header\r
261         if (!isset($headers['accept-encoding'])) {\r
262             $headers['accept-encoding'] = '';\r
263         }\r
264 \r
265         // set headers having special cURL keys\r
266         foreach (self::$headerMap as $name => $option) {\r
267             if (isset($headers[$name])) {\r
268                 curl_setopt($ch, $option, $headers[$name]);\r
269                 unset($headers[$name]);\r
270             }\r
271         }\r
272 \r
273         $this->calculateRequestLength($headers);\r
274 \r
275         // set headers not having special keys\r
276         $headersFmt = array();\r
277         foreach ($headers as $name => $value) {\r
278             $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));\r
279             $headersFmt[]  = $canonicalName . ': ' . $value;\r
280         }\r
281         curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);\r
282 \r
283         return $ch;\r
284     }\r
285 \r
286    /**\r
287     * Callback function called by cURL for reading the request body\r
288     *\r
289     * @param    resource    cURL handle\r
290     * @param    resource    file descriptor (not used)\r
291     * @param    integer     maximum length of data to return\r
292     * @return   string      part of the request body, up to $length bytes \r
293     */\r
294     protected function callbackReadBody($ch, $fd, $length)\r
295     {\r
296         if (!$this->eventSentHeaders) {\r
297             $this->request->setLastEvent(\r
298                 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)\r
299             );\r
300             $this->eventSentHeaders = true;\r
301         }\r
302         if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||\r
303             0 == $this->contentLength || $this->position >= $this->contentLength\r
304         ) {\r
305             return '';\r
306         }\r
307         if (is_string($this->requestBody)) {\r
308             $string = substr($this->requestBody, $this->position, $length);\r
309         } elseif (is_resource($this->requestBody)) {\r
310             $string = fread($this->requestBody, $length);\r
311         } else {\r
312             $string = $this->requestBody->read($length);\r
313         }\r
314         $this->request->setLastEvent('sentBodyPart', strlen($string));\r
315         $this->position += strlen($string);\r
316         return $string;\r
317     }\r
318 \r
319    /**\r
320     * Callback function called by cURL for saving the response headers\r
321     *\r
322     * @param    resource    cURL handle\r
323     * @param    string      response header (with trailing CRLF)\r
324     * @return   integer     number of bytes saved\r
325     * @see      HTTP_Request2_Response::parseHeaderLine()\r
326     */\r
327     protected function callbackWriteHeader($ch, $string)\r
328     {\r
329         // we may receive a second set of headers if doing e.g. digest auth\r
330         if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {\r
331             // don't bother with 100-Continue responses (bug #15785)\r
332             if (!$this->eventSentHeaders ||\r
333                 $this->response->getStatus() >= 200\r
334             ) {\r
335                 $this->request->setLastEvent(\r
336                     'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)\r
337                 );\r
338             }\r
339             $this->eventSentHeaders = true;\r
340             // we'll need a new response object\r
341             if ($this->eventReceivedHeaders) {\r
342                 $this->eventReceivedHeaders = false;\r
343                 $this->response             = null;\r
344             }\r
345         }\r
346         if (empty($this->response)) {\r
347             $this->response = new HTTP_Request2_Response($string, false);\r
348         } else {\r
349             $this->response->parseHeaderLine($string);\r
350             if ('' == trim($string)) {\r
351                 // don't bother with 100-Continue responses (bug #15785)\r
352                 if (200 <= $this->response->getStatus()) {\r
353                     $this->request->setLastEvent('receivedHeaders', $this->response);\r
354                 }\r
355                 $this->eventReceivedHeaders = true;\r
356             }\r
357         }\r
358         return strlen($string);\r
359     }\r
360 \r
361    /**\r
362     * Callback function called by cURL for saving the response body\r
363     *\r
364     * @param    resource    cURL handle (not used)\r
365     * @param    string      part of the response body\r
366     * @return   integer     number of bytes saved\r
367     * @see      HTTP_Request2_Response::appendBody()\r
368     */\r
369     protected function callbackWriteBody($ch, $string)\r
370     {\r
371         // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if \r
372         // response doesn't start with proper HTTP status line (see bug #15716)\r
373         if (empty($this->response)) {\r
374             throw new HTTP_Request2_Exception("Malformed response: {$string}");\r
375         }\r
376         if ($this->request->getConfig('store_body')) {\r
377             $this->response->appendBody($string);\r
378         }\r
379         $this->request->setLastEvent('receivedBodyPart', $string);\r
380         return strlen($string);\r
381     }\r
382 }\r
383 ?>\r