1<?php
2
3/**
4 * Zend Framework
5 *
6 * LICENSE
7 *
8 * This source file is subject to the new BSD license that is bundled
9 * with this package in the file LICENSE.txt.
10 * It is also available through the world-wide-web at this URL:
11 * http://framework.zend.com/license/new-bsd
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@zend.com so we can send you a copy immediately.
15 *
16 * @category   Zend
17 * @package    Zend_Http
18 * @subpackage Client_Adapter
19 * @version    $Id$
20 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
21 * @license    http://framework.zend.com/license/new-bsd     New BSD License
22 */
23
24/**
25 * @see Zend_Uri_Http
26 */
27require_once 'Zend/Uri/Http.php';
28
29/**
30 * @see Zend_Http_Client_Adapter_Interface
31 */
32require_once 'Zend/Http/Client/Adapter/Interface.php';
33/**
34 * @see Zend_Http_Client_Adapter_Stream
35 */
36require_once 'Zend/Http/Client/Adapter/Stream.php';
37
38/**
39 * An adapter class for Zend_Http_Client based on the curl extension.
40 * Curl requires libcurl. See for full requirements the PHP manual: http://php.net/curl
41 *
42 * @category   Zend
43 * @package    Zend_Http
44 * @subpackage Client_Adapter
45 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
46 * @license    http://framework.zend.com/license/new-bsd     New BSD License
47 */
48class Zend_Http_Client_Adapter_Curl implements Zend_Http_Client_Adapter_Interface, Zend_Http_Client_Adapter_Stream
49{
50    /**
51     * Parameters array
52     *
53     * @var array
54     */
55    protected $_config = array();
56
57    /**
58     * What host/port are we connected to?
59     *
60     * @var array
61     */
62    protected $_connected_to = array(null, null);
63
64    /**
65     * The curl session handle
66     *
67     * @var resource|null
68     */
69    protected $_curl = null;
70
71    /**
72     * List of cURL options that should never be overwritten
73     *
74     * @var array
75     */
76    protected $_invalidOverwritableCurlOptions;
77
78    /**
79     * Response gotten from server
80     *
81     * @var string
82     */
83    protected $_response = null;
84
85    /**
86     * Stream for storing output
87     *
88     * @var resource
89     */
90    protected $out_stream;
91
92    /**
93     * Adapter constructor
94     *
95     * Config is set using setConfig()
96     *
97     * @return void
98     * @throws Zend_Http_Client_Adapter_Exception
99     */
100    public function __construct()
101    {
102        if (!extension_loaded('curl')) {
103            require_once 'Zend/Http/Client/Adapter/Exception.php';
104            throw new Zend_Http_Client_Adapter_Exception('cURL extension has to be loaded to use this Zend_Http_Client adapter.');
105        }
106        $this->_invalidOverwritableCurlOptions = array(
107            CURLOPT_HTTPGET,
108            CURLOPT_POST,
109            CURLOPT_PUT,
110            CURLOPT_CUSTOMREQUEST,
111            CURLOPT_HEADER,
112            CURLOPT_RETURNTRANSFER,
113            CURLOPT_HTTPHEADER,
114            CURLOPT_POSTFIELDS,
115            CURLOPT_INFILE,
116            CURLOPT_INFILESIZE,
117            CURLOPT_PORT,
118            CURLOPT_MAXREDIRS,
119            CURLOPT_CONNECTTIMEOUT,
120            CURL_HTTP_VERSION_1_1,
121            CURL_HTTP_VERSION_1_0,
122        );
123    }
124
125    /**
126     * Set the configuration array for the adapter
127     *
128     * @throws Zend_Http_Client_Adapter_Exception
129     * @param  Zend_Config | array $config
130     * @return Zend_Http_Client_Adapter_Curl
131     */
132    public function setConfig($config = array())
133    {
134        if ($config instanceof Zend_Config) {
135            $config = $config->toArray();
136
137        } elseif (! is_array($config)) {
138            require_once 'Zend/Http/Client/Adapter/Exception.php';
139            throw new Zend_Http_Client_Adapter_Exception(
140                'Array or Zend_Config object expected, got ' . gettype($config)
141            );
142        }
143
144        if(isset($config['proxy_user']) && isset($config['proxy_pass'])) {
145            $this->setCurlOption(CURLOPT_PROXYUSERPWD, $config['proxy_user'].":".$config['proxy_pass']);
146            unset($config['proxy_user'], $config['proxy_pass']);
147        }
148
149        foreach ($config as $k => $v) {
150            $option = strtolower($k);
151            switch($option) {
152                case 'proxy_host':
153                    $this->setCurlOption(CURLOPT_PROXY, $v);
154                    break;
155                case 'proxy_port':
156                    $this->setCurlOption(CURLOPT_PROXYPORT, $v);
157                    break;
158                default:
159                    $this->_config[$option] = $v;
160                    break;
161            }
162        }
163
164        return $this;
165    }
166
167    /**
168      * Retrieve the array of all configuration options
169      *
170      * @return array
171      */
172     public function getConfig()
173     {
174         return $this->_config;
175     }
176
177    /**
178     * Direct setter for cURL adapter related options.
179     *
180     * @param  string|int $option
181     * @param  mixed $value
182     * @return Zend_Http_Adapter_Curl
183     */
184    public function setCurlOption($option, $value)
185    {
186        if (!isset($this->_config['curloptions'])) {
187            $this->_config['curloptions'] = array();
188        }
189        $this->_config['curloptions'][$option] = $value;
190        return $this;
191    }
192
193    /**
194     * Initialize curl
195     *
196     * @param  string  $host
197     * @param  int     $port
198     * @param  boolean $secure
199     * @return void
200     * @throws Zend_Http_Client_Adapter_Exception if unable to connect
201     */
202    public function connect($host, $port = 80, $secure = false)
203    {
204        // If we're already connected, disconnect first
205        if ($this->_curl) {
206            $this->close();
207        }
208
209        // If we are connected to a different server or port, disconnect first
210        if ($this->_curl
211            && is_array($this->_connected_to)
212            && ($this->_connected_to[0] != $host
213            || $this->_connected_to[1] != $port)
214        ) {
215            $this->close();
216        }
217
218        // Do the actual connection
219        $this->_curl = curl_init();
220        if ($port != 80) {
221            curl_setopt($this->_curl, CURLOPT_PORT, intval($port));
222        }
223
224        // Set connection timeout
225        $connectTimeout  = $this->_config['timeout'];
226        $constant        = CURLOPT_CONNECTTIMEOUT;
227        if (defined('CURLOPT_CONNECTTIMEOUT_MS')) {
228            $connectTimeout *= 1000;
229            $constant = constant('CURLOPT_CONNECTTIMEOUT_MS');
230        }
231        curl_setopt($this->_curl, $constant, $connectTimeout);
232
233        // Set request timeout (once connection is established)
234        if (array_key_exists('request_timeout', $this->_config)) {
235            $requestTimeout  = $this->_config['request_timeout'];
236            $constant        = CURLOPT_TIMEOUT;
237            if (defined('CURLOPT_TIMEOUT_MS')) {
238                $requestTimeout *= 1000;
239                $constant = constant('CURLOPT_TIMEOUT_MS');
240            }
241            curl_setopt($this->_curl, $constant, $requestTimeout);
242        }
243
244        // Set Max redirects
245        curl_setopt($this->_curl, CURLOPT_MAXREDIRS, $this->_config['maxredirects']);
246
247        if (!$this->_curl) {
248            $this->close();
249
250            require_once 'Zend/Http/Client/Adapter/Exception.php';
251            throw new Zend_Http_Client_Adapter_Exception('Unable to Connect to ' .  $host . ':' . $port);
252        }
253
254        if ($secure !== false) {
255            // Behave the same like Zend_Http_Adapter_Socket on SSL options.
256            if (isset($this->_config['sslcert'])) {
257                curl_setopt($this->_curl, CURLOPT_SSLCERT, $this->_config['sslcert']);
258            }
259            if (isset($this->_config['sslpassphrase'])) {
260                curl_setopt($this->_curl, CURLOPT_SSLCERTPASSWD, $this->_config['sslpassphrase']);
261            }
262        }
263
264        // Update connected_to
265        $this->_connected_to = array($host, $port);
266    }
267
268    /**
269     * Send request to the remote server
270     *
271     * @param  string        $method
272     * @param  Zend_Uri_Http $uri
273     * @param  float         $http_ver
274     * @param  array         $headers
275     * @param  string        $body
276     * @return string        $request
277     * @throws Zend_Http_Client_Adapter_Exception If connection fails, connected to wrong host, no PUT file defined, unsupported method, or unsupported cURL option
278     */
279    public function write($method, $uri, $httpVersion = 1.1, $headers = array(), $body = '')
280    {
281        // Make sure we're properly connected
282        if (!$this->_curl) {
283            require_once 'Zend/Http/Client/Adapter/Exception.php';
284            throw new Zend_Http_Client_Adapter_Exception("Trying to write but we are not connected");
285        }
286
287        if ($this->_connected_to[0] != $uri->getHost() || $this->_connected_to[1] != $uri->getPort()) {
288            require_once 'Zend/Http/Client/Adapter/Exception.php';
289            throw new Zend_Http_Client_Adapter_Exception("Trying to write but we are connected to the wrong host");
290        }
291
292        // set URL
293        curl_setopt($this->_curl, CURLOPT_URL, $uri->__toString());
294
295        // ensure correct curl call
296        $curlValue = true;
297        switch ($method) {
298            case Zend_Http_Client::GET:
299                $curlMethod = CURLOPT_HTTPGET;
300                break;
301
302            case Zend_Http_Client::POST:
303                $curlMethod = CURLOPT_POST;
304                break;
305
306            case Zend_Http_Client::PUT:
307                // There are two different types of PUT request, either a Raw Data string has been set
308                // or CURLOPT_INFILE and CURLOPT_INFILESIZE are used.
309                if(is_resource($body)) {
310                    $this->_config['curloptions'][CURLOPT_INFILE] = $body;
311                }
312                if (isset($this->_config['curloptions'][CURLOPT_INFILE])) {
313                    // Now we will probably already have Content-Length set, so that we have to delete it
314                    // from $headers at this point:
315                    foreach ($headers AS $k => $header) {
316                        if (preg_match('/Content-Length:\s*(\d+)/i', $header, $m)) {
317                            if(is_resource($body)) {
318                                $this->_config['curloptions'][CURLOPT_INFILESIZE] = (int)$m[1];
319                            }
320                            unset($headers[$k]);
321                        }
322                    }
323
324                    if (!isset($this->_config['curloptions'][CURLOPT_INFILESIZE])) {
325                        require_once 'Zend/Http/Client/Adapter/Exception.php';
326                        throw new Zend_Http_Client_Adapter_Exception("Cannot set a file-handle for cURL option CURLOPT_INFILE without also setting its size in CURLOPT_INFILESIZE.");
327                    }
328
329                    if(is_resource($body)) {
330                        $body = '';
331                    }
332
333                    $curlMethod = CURLOPT_PUT;
334                } else {
335                    $curlMethod = CURLOPT_CUSTOMREQUEST;
336                    $curlValue = "PUT";
337                }
338                break;
339
340            case Zend_Http_Client::PATCH:
341                $curlMethod = CURLOPT_CUSTOMREQUEST;
342                $curlValue = "PATCH";
343                break;
344
345            case Zend_Http_Client::DELETE:
346                $curlMethod = CURLOPT_CUSTOMREQUEST;
347                $curlValue = "DELETE";
348                break;
349
350            case Zend_Http_Client::OPTIONS:
351                $curlMethod = CURLOPT_CUSTOMREQUEST;
352                $curlValue = "OPTIONS";
353                break;
354
355            case Zend_Http_Client::TRACE:
356                $curlMethod = CURLOPT_CUSTOMREQUEST;
357                $curlValue = "TRACE";
358                break;
359
360            case Zend_Http_Client::HEAD:
361                $curlMethod = CURLOPT_CUSTOMREQUEST;
362                $curlValue = "HEAD";
363                break;
364
365            default:
366                // For now, through an exception for unsupported request methods
367                require_once 'Zend/Http/Client/Adapter/Exception.php';
368                throw new Zend_Http_Client_Adapter_Exception("Method currently not supported");
369        }
370
371        if(is_resource($body) && $curlMethod != CURLOPT_PUT) {
372            require_once 'Zend/Http/Client/Adapter/Exception.php';
373            throw new Zend_Http_Client_Adapter_Exception("Streaming requests are allowed only with PUT");
374        }
375
376        // get http version to use
377        $curlHttp = ($httpVersion == 1.1) ? CURL_HTTP_VERSION_1_1 : CURL_HTTP_VERSION_1_0;
378
379        // mark as HTTP request and set HTTP method
380        curl_setopt($this->_curl, CURLOPT_HTTP_VERSION, $curlHttp);
381        curl_setopt($this->_curl, $curlMethod, $curlValue);
382
383        if($this->out_stream) {
384            // headers will be read into the response
385            curl_setopt($this->_curl, CURLOPT_HEADER, false);
386            curl_setopt($this->_curl, CURLOPT_HEADERFUNCTION, array($this, "readHeader"));
387            // and data will be written into the file
388            curl_setopt($this->_curl, CURLOPT_FILE, $this->out_stream);
389        } else {
390            // ensure headers are also returned
391            curl_setopt($this->_curl, CURLOPT_HEADER, true);
392            curl_setopt($this->_curl, CURLINFO_HEADER_OUT, true);
393
394            // ensure actual response is returned
395            curl_setopt($this->_curl, CURLOPT_RETURNTRANSFER, true);
396        }
397
398        // set additional headers
399        $headers['Accept'] = '';
400        curl_setopt($this->_curl, CURLOPT_HTTPHEADER, $headers);
401
402        /**
403         * Make sure POSTFIELDS is set after $curlMethod is set:
404         * @link http://de2.php.net/manual/en/function.curl-setopt.php#81161
405         */
406        if ($method == Zend_Http_Client::POST) {
407            curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body);
408        } elseif ($curlMethod == CURLOPT_PUT) {
409            // this covers a PUT by file-handle:
410            // Make the setting of this options explicit (rather than setting it through the loop following a bit lower)
411            // to group common functionality together.
412            curl_setopt($this->_curl, CURLOPT_INFILE, $this->_config['curloptions'][CURLOPT_INFILE]);
413            curl_setopt($this->_curl, CURLOPT_INFILESIZE, $this->_config['curloptions'][CURLOPT_INFILESIZE]);
414            unset($this->_config['curloptions'][CURLOPT_INFILE]);
415            unset($this->_config['curloptions'][CURLOPT_INFILESIZE]);
416        } elseif ($method == Zend_Http_Client::PUT) {
417            // This is a PUT by a setRawData string, not by file-handle
418            curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body);
419        } elseif ($method == Zend_Http_Client::PATCH) {
420            // This is a PATCH by a setRawData string
421            curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body);
422        } elseif ($method == Zend_Http_Client::DELETE) {
423            // This is a DELETE by a setRawData string
424            curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body);
425        } elseif ($method == Zend_Http_Client::OPTIONS) {
426            // This is an OPTIONS by a setRawData string
427            curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body);
428        }
429
430        // set additional curl options
431        if (isset($this->_config['curloptions'])) {
432            foreach ((array)$this->_config['curloptions'] as $k => $v) {
433                if (!in_array($k, $this->_invalidOverwritableCurlOptions)) {
434                    if (curl_setopt($this->_curl, $k, $v) == false) {
435                        require_once 'Zend/Http/Client/Exception.php';
436                        throw new Zend_Http_Client_Exception(sprintf("Unknown or erroreous cURL option '%s' set", $k));
437                    }
438                }
439            }
440        }
441
442        // send the request
443        $response = curl_exec($this->_curl);
444
445        // if we used streaming, headers are already there
446        if(!is_resource($this->out_stream)) {
447            $this->_response = $response;
448        }
449
450        $request  = curl_getinfo($this->_curl, CURLINFO_HEADER_OUT);
451        $request .= $body;
452
453        if (empty($this->_response)) {
454            require_once 'Zend/Http/Client/Exception.php';
455            throw new Zend_Http_Client_Exception("Error in cURL request: " . curl_error($this->_curl));
456        }
457
458        // cURL automatically decodes chunked-messages, this means we have to disallow the Zend_Http_Response to do it again
459        if (stripos($this->_response, "Transfer-Encoding: chunked\r\n")) {
460            $this->_response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $this->_response);
461        }
462
463        // Eliminate multiple HTTP responses.
464        do {
465            $parts  = preg_split('|(?:\r?\n){2}|m', $this->_response, 2);
466            $again  = false;
467
468            if (isset($parts[1]) && preg_match("|^HTTP/1\.[01](.*?)\r\n|mi", $parts[1])) {
469                $this->_response    = $parts[1];
470                $again              = true;
471            }
472        } while ($again);
473
474        // cURL automatically handles Proxy rewrites, remove the "HTTP/1.0 200 Connection established" string:
475        if (stripos($this->_response, "HTTP/1.0 200 Connection established\r\n\r\n") !== false) {
476            $this->_response = str_ireplace("HTTP/1.0 200 Connection established\r\n\r\n", '', $this->_response);
477        }
478
479        return $request;
480    }
481
482    /**
483     * Return read response from server
484     *
485     * @return string
486     */
487    public function read()
488    {
489        return $this->_response;
490    }
491
492    /**
493     * Close the connection to the server
494     *
495     */
496    public function close()
497    {
498        if(is_resource($this->_curl)) {
499            curl_close($this->_curl);
500        }
501        $this->_curl         = null;
502        $this->_connected_to = array(null, null);
503    }
504
505    /**
506     * Get cUrl Handle
507     *
508     * @return resource
509     */
510    public function getHandle()
511    {
512        return $this->_curl;
513    }
514
515    /**
516     * Set output stream for the response
517     *
518     * @param resource $stream
519     * @return Zend_Http_Client_Adapter_Socket
520     */
521    public function setOutputStream($stream)
522    {
523        $this->out_stream = $stream;
524        return $this;
525    }
526
527    /**
528     * Header reader function for CURL
529     *
530     * @param resource $curl
531     * @param string $header
532     * @return int
533     */
534    public function readHeader($curl, $header)
535    {
536        $this->_response .= $header;
537        return strlen($header);
538    }
539}
540