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