1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Part of the Joomla Framework Http Package |
4
|
|
|
* |
5
|
|
|
* @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved. |
6
|
|
|
* @license GNU General Public License version 2 or later; see LICENSE |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace Joomla\Http\Transport; |
10
|
|
|
|
11
|
|
|
use Composer\CaBundle\CaBundle; |
12
|
|
|
use Joomla\Http\Exception\InvalidResponseCodeException; |
13
|
|
|
use Joomla\Http\TransportInterface; |
14
|
|
|
use Joomla\Http\Response; |
15
|
|
|
use Joomla\Uri\UriInterface; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* HTTP transport class for using cURL. |
19
|
|
|
* |
20
|
|
|
* @since 1.0 |
21
|
|
|
*/ |
22
|
|
|
class Curl implements TransportInterface |
23
|
|
|
{ |
24
|
|
|
/** |
25
|
|
|
* The client options. |
26
|
|
|
* |
27
|
|
|
* @var array|\ArrayAccess |
28
|
|
|
* @since 1.0 |
29
|
|
|
*/ |
30
|
|
|
protected $options; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Constructor. CURLOPT_FOLLOWLOCATION must be disabled when open_basedir or safe_mode are enabled. |
34
|
|
|
* |
35
|
|
|
* @param array|\ArrayAccess $options Client options array. |
36
|
|
|
* |
37
|
|
|
* @see http://www.php.net/manual/en/function.curl-setopt.php |
38
|
|
|
* @since 1.0 |
39
|
|
|
* @throws \InvalidArgumentException |
40
|
|
|
* @throws \RuntimeException |
41
|
|
|
*/ |
42
|
|
View Code Duplication |
public function __construct($options = array()) |
|
|
|
|
43
|
|
|
{ |
44
|
|
|
if (!function_exists('curl_init') || !is_callable('curl_init')) |
45
|
|
|
{ |
46
|
|
|
throw new \RuntimeException('Cannot use a cURL transport when curl_init() is not available.'); |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
if (!is_array($options) && !($options instanceof \ArrayAccess)) |
50
|
|
|
{ |
51
|
|
|
throw new \InvalidArgumentException( |
52
|
|
|
'The options param must be an array or implement the ArrayAccess interface.' |
53
|
|
|
); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
$this->options = $options; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Send a request to the server and return a Response object with the response. |
61
|
|
|
* |
62
|
|
|
* @param string $method The HTTP method for sending the request. |
63
|
|
|
* @param UriInterface $uri The URI to the resource to request. |
64
|
|
|
* @param mixed $data Either an associative array or a string to be sent with the request. |
65
|
|
|
* @param array $headers An array of request headers to send with the request. |
66
|
|
|
* @param integer $timeout Read timeout in seconds. |
67
|
|
|
* @param string $userAgent The optional user agent string to send with the request. |
68
|
|
|
* |
69
|
|
|
* @return Response |
70
|
|
|
* |
71
|
|
|
* @since 1.0 |
72
|
|
|
* @throws \RuntimeException |
73
|
|
|
*/ |
74
|
|
|
public function request($method, UriInterface $uri, $data = null, array $headers = null, $timeout = null, $userAgent = null) |
75
|
|
|
{ |
76
|
|
|
// Setup the cURL handle. |
77
|
|
|
$ch = curl_init(); |
78
|
|
|
|
79
|
|
|
$options = array(); |
80
|
|
|
|
81
|
|
|
// Set the request method. |
82
|
|
|
switch (strtoupper($method)) |
83
|
|
|
{ |
84
|
|
|
case 'GET': |
85
|
|
|
$options[CURLOPT_HTTPGET] = true; |
86
|
|
|
break; |
87
|
|
|
|
88
|
|
|
case 'POST': |
89
|
|
|
$options[CURLOPT_POST] = true; |
90
|
|
|
break; |
91
|
|
|
|
92
|
|
|
default: |
93
|
|
|
$options[CURLOPT_CUSTOMREQUEST] = strtoupper($method); |
94
|
|
|
break; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
// Don't wait for body when $method is HEAD |
98
|
|
|
$options[CURLOPT_NOBODY] = ($method === 'HEAD'); |
99
|
|
|
|
100
|
|
|
// Initialize the certificate store |
101
|
|
|
$options[CURLOPT_CAINFO] = isset($this->options['curl.certpath']) ? $this->options['curl.certpath'] : CaBundle::getSystemCaRootBundlePath(); |
102
|
|
|
|
103
|
|
|
// If data exists let's encode it and make sure our Content-type header is set. |
104
|
|
|
if (isset($data)) |
105
|
|
|
{ |
106
|
|
|
// If the data is a scalar value simply add it to the cURL post fields. |
107
|
|
|
if (is_scalar($data) || (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'multipart/form-data') === 0)) |
108
|
|
|
{ |
109
|
|
|
$options[CURLOPT_POSTFIELDS] = $data; |
110
|
|
|
} |
111
|
|
|
else |
112
|
|
|
// Otherwise we need to encode the value first. |
113
|
|
|
{ |
114
|
|
|
$options[CURLOPT_POSTFIELDS] = http_build_query($data); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
if (!isset($headers['Content-Type'])) |
118
|
|
|
{ |
119
|
|
|
$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
// Add the relevant headers. |
123
|
|
|
if (is_scalar($options[CURLOPT_POSTFIELDS])) |
124
|
|
|
{ |
125
|
|
|
$headers['Content-Length'] = strlen($options[CURLOPT_POSTFIELDS]); |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
// Build the headers string for the request. |
130
|
|
|
$headerArray = array(); |
131
|
|
|
|
132
|
|
|
if (isset($headers)) |
133
|
|
|
{ |
134
|
|
|
foreach ($headers as $key => $value) |
135
|
|
|
{ |
136
|
|
|
$headerArray[] = $key . ': ' . $value; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
// Add the headers string into the stream context options array. |
140
|
|
|
$options[CURLOPT_HTTPHEADER] = $headerArray; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// Curl needs the accepted encoding header as option |
144
|
|
|
if (isset($headers['Accept-Encoding'])) |
145
|
|
|
{ |
146
|
|
|
$options[CURLOPT_ENCODING] = $headers['Accept-Encoding']; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
// If an explicit timeout is given user it. |
150
|
|
|
if (isset($timeout)) |
151
|
|
|
{ |
152
|
|
|
$options[CURLOPT_TIMEOUT] = (int) $timeout; |
153
|
|
|
$options[CURLOPT_CONNECTTIMEOUT] = (int) $timeout; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
// If an explicit user agent is given use it. |
157
|
|
|
if (isset($userAgent)) |
158
|
|
|
{ |
159
|
|
|
$options[CURLOPT_USERAGENT] = $userAgent; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
// Set the request URL. |
163
|
|
|
$options[CURLOPT_URL] = (string) $uri; |
164
|
|
|
|
165
|
|
|
// We want our headers. :-) |
166
|
|
|
$options[CURLOPT_HEADER] = true; |
167
|
|
|
|
168
|
|
|
// Return it... echoing it would be tacky. |
169
|
|
|
$options[CURLOPT_RETURNTRANSFER] = true; |
170
|
|
|
|
171
|
|
|
// Override the Expect header to prevent cURL from confusing itself in its own stupidity. |
172
|
|
|
// Link: http://the-stickman.com/web-development/php-and-curl-disabling-100-continue-header/ |
173
|
|
|
$options[CURLOPT_HTTPHEADER][] = 'Expect:'; |
174
|
|
|
|
175
|
|
|
// Follow redirects if server config allows |
176
|
|
|
if ($this->redirectsAllowed()) |
177
|
|
|
{ |
178
|
|
|
$options[CURLOPT_FOLLOWLOCATION] = (bool) isset($this->options['follow_location']) ? $this->options['follow_location'] : true; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
// Authentication, if needed |
182
|
|
|
if (isset($this->options['userauth']) && isset($this->options['passwordauth'])) |
183
|
|
|
{ |
184
|
|
|
$options[CURLOPT_USERPWD] = $this->options['userauth'] . ':' . $this->options['passwordauth']; |
185
|
|
|
$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
// Set any custom transport options |
189
|
|
|
if (isset($this->options['transport.curl'])) |
190
|
|
|
{ |
191
|
|
|
foreach ($this->options['transport.curl'] as $key => $value) |
192
|
|
|
{ |
193
|
|
|
$options[$key] = $value; |
194
|
|
|
} |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
// Set the cURL options. |
198
|
|
|
curl_setopt_array($ch, $options); |
199
|
|
|
|
200
|
|
|
// Execute the request and close the connection. |
201
|
|
|
$content = curl_exec($ch); |
202
|
|
|
|
203
|
|
|
// Check if the content is a string. If it is not, it must be an error. |
204
|
|
|
if (!is_string($content)) |
205
|
|
|
{ |
206
|
|
|
$message = curl_error($ch); |
207
|
|
|
|
208
|
|
|
if (empty($message)) |
209
|
|
|
{ |
210
|
|
|
// Error but nothing from cURL? Create our own |
211
|
|
|
$message = 'No HTTP response received'; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
throw new \RuntimeException($message); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
// Get the request information. |
218
|
|
|
$info = curl_getinfo($ch); |
219
|
|
|
|
220
|
|
|
// Close the connection. |
221
|
|
|
curl_close($ch); |
222
|
|
|
|
223
|
|
|
return $this->getResponse($content, $info); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Method to get a response object from a server response. |
228
|
|
|
* |
229
|
|
|
* @param string $content The complete server response, including headers |
230
|
|
|
* as a string if the response has no errors. |
231
|
|
|
* @param array $info The cURL request information. |
232
|
|
|
* |
233
|
|
|
* @return Response |
234
|
|
|
* |
235
|
|
|
* @since 1.0 |
236
|
|
|
* @throws InvalidResponseCodeException |
237
|
|
|
*/ |
238
|
|
|
protected function getResponse($content, $info) |
239
|
|
|
{ |
240
|
|
|
// Create the response object. |
241
|
|
|
$return = new Response; |
242
|
|
|
|
243
|
|
|
// Get the number of redirects that occurred. |
244
|
|
|
$redirects = isset($info['redirect_count']) ? $info['redirect_count'] : 0; |
245
|
|
|
|
246
|
|
|
/* |
247
|
|
|
* Split the response into headers and body. If cURL encountered redirects, the headers for the redirected requests will |
248
|
|
|
* also be included. So we split the response into header + body + the number of redirects and only use the last two |
249
|
|
|
* sections which should be the last set of headers and the actual body. |
250
|
|
|
*/ |
251
|
|
|
$response = explode("\r\n\r\n", $content, 2 + $redirects); |
252
|
|
|
|
253
|
|
|
// Set the body for the response. |
254
|
|
|
$return->body = array_pop($response); |
255
|
|
|
|
256
|
|
|
// Get the last set of response headers as an array. |
257
|
|
|
$headers = explode("\r\n", array_pop($response)); |
258
|
|
|
|
259
|
|
|
// Get the response code from the first offset of the response headers. |
260
|
|
|
preg_match('/[0-9]{3}/', array_shift($headers), $matches); |
261
|
|
|
|
262
|
|
|
$code = count($matches) ? $matches[0] : null; |
263
|
|
|
|
264
|
|
|
if (is_numeric($code)) |
265
|
|
|
{ |
266
|
|
|
$return->code = (int) $code; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
// No valid response code was detected. |
270
|
|
|
else |
271
|
|
|
{ |
272
|
|
|
throw new InvalidResponseCodeException('No HTTP response code found.'); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
// Add the response headers to the response object. |
276
|
|
View Code Duplication |
foreach ($headers as $header) |
|
|
|
|
277
|
|
|
{ |
278
|
|
|
$pos = strpos($header, ':'); |
279
|
|
|
$return->headers[trim(substr($header, 0, $pos))] = trim(substr($header, ($pos + 1))); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
return $return; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Method to check if HTTP transport cURL is available for use |
287
|
|
|
* |
288
|
|
|
* @return boolean True if available, else false |
289
|
|
|
* |
290
|
|
|
* @since 1.0 |
291
|
|
|
*/ |
292
|
|
|
public static function isSupported() |
293
|
|
|
{ |
294
|
|
|
return function_exists('curl_version') && curl_version(); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Check if redirects are allowed |
299
|
|
|
* |
300
|
|
|
* @return boolean |
301
|
|
|
* |
302
|
|
|
* @since 1.2.1 |
303
|
|
|
*/ |
304
|
|
|
private function redirectsAllowed() |
305
|
|
|
{ |
306
|
|
|
// There are no issues on PHP 5.6 and later |
307
|
|
|
if (version_compare(PHP_VERSION, '5.6', '>=')) |
308
|
|
|
{ |
309
|
|
|
return true; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
// For PHP 5.3, redirects are not allowed if safe_mode and open_basedir are enabled |
313
|
|
|
if (PHP_MAJOR_VERSION === 5 && PHP_MINOR_VERSION === 3) |
314
|
|
|
{ |
315
|
|
|
if (!ini_get('safe_mode') && !ini_get('open_basedir')) |
316
|
|
|
{ |
317
|
|
|
return true; |
318
|
|
|
} |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
// For PHP 5.4 and 5.5, we only need to check if open_basedir is disabled |
322
|
|
|
return !ini_get('open_basedir'); |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.