|
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: Curl.php 22216 2010-05-20 21:12:05Z dragonbe $ |
|
20 * @copyright Copyright (c) 2005-2010 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 */ |
|
27 require_once 'Zend/Uri/Http.php'; |
|
28 |
|
29 /** |
|
30 * @see Zend_Http_Client_Adapter_Interface |
|
31 */ |
|
32 require_once 'Zend/Http/Client/Adapter/Interface.php'; |
|
33 /** |
|
34 * @see Zend_Http_Client_Adapter_Stream |
|
35 */ |
|
36 require_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-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
46 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
47 */ |
|
48 class 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 timeout |
|
225 curl_setopt($this->_curl, CURLOPT_CONNECTTIMEOUT, $this->_config['timeout']); |
|
226 |
|
227 // Set Max redirects |
|
228 curl_setopt($this->_curl, CURLOPT_MAXREDIRS, $this->_config['maxredirects']); |
|
229 |
|
230 if (!$this->_curl) { |
|
231 $this->close(); |
|
232 |
|
233 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
234 throw new Zend_Http_Client_Adapter_Exception('Unable to Connect to ' . $host . ':' . $port); |
|
235 } |
|
236 |
|
237 if ($secure !== false) { |
|
238 // Behave the same like Zend_Http_Adapter_Socket on SSL options. |
|
239 if (isset($this->_config['sslcert'])) { |
|
240 curl_setopt($this->_curl, CURLOPT_SSLCERT, $this->_config['sslcert']); |
|
241 } |
|
242 if (isset($this->_config['sslpassphrase'])) { |
|
243 curl_setopt($this->_curl, CURLOPT_SSLCERTPASSWD, $this->_config['sslpassphrase']); |
|
244 } |
|
245 } |
|
246 |
|
247 // Update connected_to |
|
248 $this->_connected_to = array($host, $port); |
|
249 } |
|
250 |
|
251 /** |
|
252 * Send request to the remote server |
|
253 * |
|
254 * @param string $method |
|
255 * @param Zend_Uri_Http $uri |
|
256 * @param float $http_ver |
|
257 * @param array $headers |
|
258 * @param string $body |
|
259 * @return string $request |
|
260 * @throws Zend_Http_Client_Adapter_Exception If connection fails, connected to wrong host, no PUT file defined, unsupported method, or unsupported cURL option |
|
261 */ |
|
262 public function write($method, $uri, $httpVersion = 1.1, $headers = array(), $body = '') |
|
263 { |
|
264 // Make sure we're properly connected |
|
265 if (!$this->_curl) { |
|
266 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
267 throw new Zend_Http_Client_Adapter_Exception("Trying to write but we are not connected"); |
|
268 } |
|
269 |
|
270 if ($this->_connected_to[0] != $uri->getHost() || $this->_connected_to[1] != $uri->getPort()) { |
|
271 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
272 throw new Zend_Http_Client_Adapter_Exception("Trying to write but we are connected to the wrong host"); |
|
273 } |
|
274 |
|
275 // set URL |
|
276 curl_setopt($this->_curl, CURLOPT_URL, $uri->__toString()); |
|
277 |
|
278 // ensure correct curl call |
|
279 $curlValue = true; |
|
280 switch ($method) { |
|
281 case Zend_Http_Client::GET: |
|
282 $curlMethod = CURLOPT_HTTPGET; |
|
283 break; |
|
284 |
|
285 case Zend_Http_Client::POST: |
|
286 $curlMethod = CURLOPT_POST; |
|
287 break; |
|
288 |
|
289 case Zend_Http_Client::PUT: |
|
290 // There are two different types of PUT request, either a Raw Data string has been set |
|
291 // or CURLOPT_INFILE and CURLOPT_INFILESIZE are used. |
|
292 if(is_resource($body)) { |
|
293 $this->_config['curloptions'][CURLOPT_INFILE] = $body; |
|
294 } |
|
295 if (isset($this->_config['curloptions'][CURLOPT_INFILE])) { |
|
296 // Now we will probably already have Content-Length set, so that we have to delete it |
|
297 // from $headers at this point: |
|
298 foreach ($headers AS $k => $header) { |
|
299 if (preg_match('/Content-Length:\s*(\d+)/i', $header, $m)) { |
|
300 if(is_resource($body)) { |
|
301 $this->_config['curloptions'][CURLOPT_INFILESIZE] = (int)$m[1]; |
|
302 } |
|
303 unset($headers[$k]); |
|
304 } |
|
305 } |
|
306 |
|
307 if (!isset($this->_config['curloptions'][CURLOPT_INFILESIZE])) { |
|
308 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
309 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."); |
|
310 } |
|
311 |
|
312 if(is_resource($body)) { |
|
313 $body = ''; |
|
314 } |
|
315 |
|
316 $curlMethod = CURLOPT_PUT; |
|
317 } else { |
|
318 $curlMethod = CURLOPT_CUSTOMREQUEST; |
|
319 $curlValue = "PUT"; |
|
320 } |
|
321 break; |
|
322 |
|
323 case Zend_Http_Client::DELETE: |
|
324 $curlMethod = CURLOPT_CUSTOMREQUEST; |
|
325 $curlValue = "DELETE"; |
|
326 break; |
|
327 |
|
328 case Zend_Http_Client::OPTIONS: |
|
329 $curlMethod = CURLOPT_CUSTOMREQUEST; |
|
330 $curlValue = "OPTIONS"; |
|
331 break; |
|
332 |
|
333 case Zend_Http_Client::TRACE: |
|
334 $curlMethod = CURLOPT_CUSTOMREQUEST; |
|
335 $curlValue = "TRACE"; |
|
336 break; |
|
337 |
|
338 case Zend_Http_Client::HEAD: |
|
339 $curlMethod = CURLOPT_CUSTOMREQUEST; |
|
340 $curlValue = "HEAD"; |
|
341 break; |
|
342 |
|
343 default: |
|
344 // For now, through an exception for unsupported request methods |
|
345 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
346 throw new Zend_Http_Client_Adapter_Exception("Method currently not supported"); |
|
347 } |
|
348 |
|
349 if(is_resource($body) && $curlMethod != CURLOPT_PUT) { |
|
350 require_once 'Zend/Http/Client/Adapter/Exception.php'; |
|
351 throw new Zend_Http_Client_Adapter_Exception("Streaming requests are allowed only with PUT"); |
|
352 } |
|
353 |
|
354 // get http version to use |
|
355 $curlHttp = ($httpVersion == 1.1) ? CURL_HTTP_VERSION_1_1 : CURL_HTTP_VERSION_1_0; |
|
356 |
|
357 // mark as HTTP request and set HTTP method |
|
358 curl_setopt($this->_curl, $curlHttp, true); |
|
359 curl_setopt($this->_curl, $curlMethod, $curlValue); |
|
360 |
|
361 if($this->out_stream) { |
|
362 // headers will be read into the response |
|
363 curl_setopt($this->_curl, CURLOPT_HEADER, false); |
|
364 curl_setopt($this->_curl, CURLOPT_HEADERFUNCTION, array($this, "readHeader")); |
|
365 // and data will be written into the file |
|
366 curl_setopt($this->_curl, CURLOPT_FILE, $this->out_stream); |
|
367 } else { |
|
368 // ensure headers are also returned |
|
369 curl_setopt($this->_curl, CURLOPT_HEADER, true); |
|
370 |
|
371 // ensure actual response is returned |
|
372 curl_setopt($this->_curl, CURLOPT_RETURNTRANSFER, true); |
|
373 } |
|
374 |
|
375 // set additional headers |
|
376 $headers['Accept'] = ''; |
|
377 curl_setopt($this->_curl, CURLOPT_HTTPHEADER, $headers); |
|
378 |
|
379 /** |
|
380 * Make sure POSTFIELDS is set after $curlMethod is set: |
|
381 * @link http://de2.php.net/manual/en/function.curl-setopt.php#81161 |
|
382 */ |
|
383 if ($method == Zend_Http_Client::POST) { |
|
384 curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body); |
|
385 } elseif ($curlMethod == CURLOPT_PUT) { |
|
386 // this covers a PUT by file-handle: |
|
387 // Make the setting of this options explicit (rather than setting it through the loop following a bit lower) |
|
388 // to group common functionality together. |
|
389 curl_setopt($this->_curl, CURLOPT_INFILE, $this->_config['curloptions'][CURLOPT_INFILE]); |
|
390 curl_setopt($this->_curl, CURLOPT_INFILESIZE, $this->_config['curloptions'][CURLOPT_INFILESIZE]); |
|
391 unset($this->_config['curloptions'][CURLOPT_INFILE]); |
|
392 unset($this->_config['curloptions'][CURLOPT_INFILESIZE]); |
|
393 } elseif ($method == Zend_Http_Client::PUT) { |
|
394 // This is a PUT by a setRawData string, not by file-handle |
|
395 curl_setopt($this->_curl, CURLOPT_POSTFIELDS, $body); |
|
396 } |
|
397 |
|
398 // set additional curl options |
|
399 if (isset($this->_config['curloptions'])) { |
|
400 foreach ((array)$this->_config['curloptions'] as $k => $v) { |
|
401 if (!in_array($k, $this->_invalidOverwritableCurlOptions)) { |
|
402 if (curl_setopt($this->_curl, $k, $v) == false) { |
|
403 require_once 'Zend/Http/Client/Exception.php'; |
|
404 throw new Zend_Http_Client_Exception(sprintf("Unknown or erroreous cURL option '%s' set", $k)); |
|
405 } |
|
406 } |
|
407 } |
|
408 } |
|
409 |
|
410 // send the request |
|
411 $response = curl_exec($this->_curl); |
|
412 |
|
413 // if we used streaming, headers are already there |
|
414 if(!is_resource($this->out_stream)) { |
|
415 $this->_response = $response; |
|
416 } |
|
417 |
|
418 $request = curl_getinfo($this->_curl, CURLINFO_HEADER_OUT); |
|
419 $request .= $body; |
|
420 |
|
421 if (empty($this->_response)) { |
|
422 require_once 'Zend/Http/Client/Exception.php'; |
|
423 throw new Zend_Http_Client_Exception("Error in cURL request: " . curl_error($this->_curl)); |
|
424 } |
|
425 |
|
426 // cURL automatically decodes chunked-messages, this means we have to disallow the Zend_Http_Response to do it again |
|
427 if (stripos($this->_response, "Transfer-Encoding: chunked\r\n")) { |
|
428 $this->_response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $this->_response); |
|
429 } |
|
430 |
|
431 // Eliminate multiple HTTP responses. |
|
432 do { |
|
433 $parts = preg_split('|(?:\r?\n){2}|m', $this->_response, 2); |
|
434 $again = false; |
|
435 |
|
436 if (isset($parts[1]) && preg_match("|^HTTP/1\.[01](.*?)\r\n|mi", $parts[1])) { |
|
437 $this->_response = $parts[1]; |
|
438 $again = true; |
|
439 } |
|
440 } while ($again); |
|
441 |
|
442 // cURL automatically handles Proxy rewrites, remove the "HTTP/1.0 200 Connection established" string: |
|
443 if (stripos($this->_response, "HTTP/1.0 200 Connection established\r\n\r\n") !== false) { |
|
444 $this->_response = str_ireplace("HTTP/1.0 200 Connection established\r\n\r\n", '', $this->_response); |
|
445 } |
|
446 |
|
447 return $request; |
|
448 } |
|
449 |
|
450 /** |
|
451 * Return read response from server |
|
452 * |
|
453 * @return string |
|
454 */ |
|
455 public function read() |
|
456 { |
|
457 return $this->_response; |
|
458 } |
|
459 |
|
460 /** |
|
461 * Close the connection to the server |
|
462 * |
|
463 */ |
|
464 public function close() |
|
465 { |
|
466 if(is_resource($this->_curl)) { |
|
467 curl_close($this->_curl); |
|
468 } |
|
469 $this->_curl = null; |
|
470 $this->_connected_to = array(null, null); |
|
471 } |
|
472 |
|
473 /** |
|
474 * Get cUrl Handle |
|
475 * |
|
476 * @return resource |
|
477 */ |
|
478 public function getHandle() |
|
479 { |
|
480 return $this->_curl; |
|
481 } |
|
482 |
|
483 /** |
|
484 * Set output stream for the response |
|
485 * |
|
486 * @param resource $stream |
|
487 * @return Zend_Http_Client_Adapter_Socket |
|
488 */ |
|
489 public function setOutputStream($stream) |
|
490 { |
|
491 $this->out_stream = $stream; |
|
492 return $this; |
|
493 } |
|
494 |
|
495 /** |
|
496 * Header reader function for CURL |
|
497 * |
|
498 * @param resource $curl |
|
499 * @param string $header |
|
500 * @return int |
|
501 */ |
|
502 public function readHeader($curl, $header) |
|
503 { |
|
504 $this->_response .= $header; |
|
505 return strlen($header); |
|
506 } |
|
507 } |