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