1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage HTTP
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 use Joomla\Registry\Registry;
13
14 /**
15 * HTTP transport class for using cURL.
16 *
17 * @since 11.3
18 */
19 class JHttpTransportCurl implements JHttpTransport
20 {
21 /**
22 * @var Registry The client options.
23 * @since 11.3
24 */
25 protected $options;
26
27 /**
28 * Constructor. CURLOPT_FOLLOWLOCATION must be disabled when open_basedir or safe_mode are enabled.
29 *
30 * @param Registry $options Client options object.
31 *
32 * @link https://secure.php.net/manual/en/function.curl-setopt.php
33 * @since 11.3
34 * @throws RuntimeException
35 */
36 public function __construct(Registry $options)
37 {
38 if (!function_exists('curl_init') || !is_callable('curl_init'))
39 {
40 throw new RuntimeException('Cannot use a cURL transport when curl_init() is not available.');
41 }
42
43 $this->options = $options;
44 }
45
46 /**
47 * Send a request to the server and return a JHttpResponse object with the response.
48 *
49 * @param string $method The HTTP method for sending the request.
50 * @param JUri $uri The URI to the resource to request.
51 * @param mixed $data Either an associative array or a string to be sent with the request.
52 * @param array $headers An array of request headers to send with the request.
53 * @param integer $timeout Read timeout in seconds.
54 * @param string $userAgent The optional user agent string to send with the request.
55 *
56 * @return JHttpResponse
57 *
58 * @since 11.3
59 * @throws RuntimeException
60 */
61 public function request($method, JUri $uri, $data = null, array $headers = null, $timeout = null, $userAgent = null)
62 {
63 // Setup the cURL handle.
64 $ch = curl_init();
65
66 $options = array();
67
68 // Set the request method.
69 switch (strtoupper($method))
70 {
71 case 'GET':
72 $options[CURLOPT_HTTPGET] = true;
73 break;
74
75 case 'POST':
76 $options[CURLOPT_POST] = true;
77 break;
78
79 case 'PUT':
80 default:
81 $options[CURLOPT_CUSTOMREQUEST] = strtoupper($method);
82 break;
83 }
84
85 // Don't wait for body when $method is HEAD
86 $options[CURLOPT_NOBODY] = ($method === 'HEAD');
87
88 // Initialize the certificate store
89 $options[CURLOPT_CAINFO] = $this->options->get('curl.certpath', __DIR__ . '/cacert.pem');
90
91 // If data exists let's encode it and make sure our Content-type header is set.
92 if (isset($data))
93 {
94 // If the data is a scalar value simply add it to the cURL post fields.
95 if (is_scalar($data) || (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'multipart/form-data') === 0))
96 {
97 $options[CURLOPT_POSTFIELDS] = $data;
98 }
99
100 // Otherwise we need to encode the value first.
101 else
102 {
103 $options[CURLOPT_POSTFIELDS] = http_build_query($data);
104 }
105
106 if (!isset($headers['Content-Type']))
107 {
108 $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
109 }
110
111 // Add the relevant headers.
112 if (is_scalar($options[CURLOPT_POSTFIELDS]))
113 {
114 $headers['Content-Length'] = strlen($options[CURLOPT_POSTFIELDS]);
115 }
116 }
117
118 // Build the headers string for the request.
119 $headerArray = array();
120
121 if (isset($headers))
122 {
123 foreach ($headers as $key => $value)
124 {
125 $headerArray[] = $key . ': ' . $value;
126 }
127
128 // Add the headers string into the stream context options array.
129 $options[CURLOPT_HTTPHEADER] = $headerArray;
130 }
131
132 // Curl needs the accepted encoding header as option
133 if (isset($headers['Accept-Encoding']))
134 {
135 $options[CURLOPT_ENCODING] = $headers['Accept-Encoding'];
136 }
137
138 // If an explicit timeout is given user it.
139 if (isset($timeout))
140 {
141 $options[CURLOPT_TIMEOUT] = (int) $timeout;
142 $options[CURLOPT_CONNECTTIMEOUT] = (int) $timeout;
143 }
144
145 // If an explicit user agent is given use it.
146 if (isset($userAgent))
147 {
148 $options[CURLOPT_USERAGENT] = $userAgent;
149 }
150
151 // Set the request URL.
152 $options[CURLOPT_URL] = (string) $uri;
153
154 // We want our headers. :-)
155 $options[CURLOPT_HEADER] = true;
156
157 // Return it... echoing it would be tacky.
158 $options[CURLOPT_RETURNTRANSFER] = true;
159
160 // Override the Expect header to prevent cURL from confusing itself in its own stupidity.
161 // Link: http://the-stickman.com/web-development/php-and-curl-disabling-100-continue-header/
162 $options[CURLOPT_HTTPHEADER][] = 'Expect:';
163
164 // Follow redirects if server config allows
165 if ($this->redirectsAllowed())
166 {
167 $options[CURLOPT_FOLLOWLOCATION] = (bool) $this->options->get('follow_location', true);
168 }
169
170 // Proxy configuration
171 $config = JFactory::getConfig();
172
173 if ($config->get('proxy_enable'))
174 {
175 $options[CURLOPT_PROXY] = $config->get('proxy_host') . ':' . $config->get('proxy_port');
176
177 if ($user = $config->get('proxy_user'))
178 {
179 $options[CURLOPT_PROXYUSERPWD] = $user . ':' . $config->get('proxy_pass');
180 }
181 }
182
183 // Set any custom transport options
184 foreach ($this->options->get('transport.curl', array()) as $key => $value)
185 {
186 $options[$key] = $value;
187 }
188
189 // Authentification, if needed
190 if ($this->options->get('userauth') && $this->options->get('passwordauth'))
191 {
192 $options[CURLOPT_USERPWD] = $this->options->get('userauth') . ':' . $this->options->get('passwordauth');
193 $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
194 }
195
196 // Set the cURL options.
197 curl_setopt_array($ch, $options);
198
199 // Execute the request and close the connection.
200 $content = curl_exec($ch);
201
202 // Check if the content is a string. If it is not, it must be an error.
203 if (!is_string($content))
204 {
205 $message = curl_error($ch);
206
207 if (empty($message))
208 {
209 // Error but nothing from cURL? Create our own
210 $message = 'No HTTP response received';
211 }
212
213 throw new RuntimeException($message);
214 }
215
216 // Get the request information.
217 $info = curl_getinfo($ch);
218
219 // Close the connection.
220 curl_close($ch);
221
222 $response = $this->getResponse($content, $info);
223
224 // Manually follow redirects if server doesn't allow to follow location using curl
225 if ($response->code >= 301 && $response->code < 400 && isset($response->headers['Location']) && (bool) $this->options->get('follow_location', true))
226 {
227 $redirect_uri = new JUri($response->headers['Location']);
228 if (in_array($redirect_uri->getScheme(), array('file', 'scp')))
229 {
230 throw new RuntimeException('Curl redirect cannot be used in file or scp requests.');
231 }
232 $response = $this->request($method, $redirect_uri, $data, $headers, $timeout, $userAgent);
233 }
234
235 return $response;
236 }
237
238 /**
239 * Method to get a response object from a server response.
240 *
241 * @param string $content The complete server response, including headers
242 * as a string if the response has no errors.
243 * @param array $info The cURL request information.
244 *
245 * @return JHttpResponse
246 *
247 * @since 11.3
248 * @throws UnexpectedValueException
249 */
250 protected function getResponse($content, $info)
251 {
252 // Create the response object.
253 $return = new JHttpResponse;
254
255 // Try to get header size
256 if (isset($info['header_size']))
257 {
258 $headerString = trim(substr($content, 0, $info['header_size']));
259 $headerArray = explode("\r\n\r\n", $headerString);
260
261 // Get the last set of response headers as an array.
262 $headers = explode("\r\n", array_pop($headerArray));
263
264 // Set the body for the response.
265 $return->body = substr($content, $info['header_size']);
266 }
267 // Fallback and try to guess header count by redirect count
268 else
269 {
270 // Get the number of redirects that occurred.
271 $redirects = isset($info['redirect_count']) ? $info['redirect_count'] : 0;
272
273 /*
274 * Split the response into headers and body. If cURL encountered redirects, the headers for the redirected requests will
275 * also be included. So we split the response into header + body + the number of redirects and only use the last two
276 * sections which should be the last set of headers and the actual body.
277 */
278 $response = explode("\r\n\r\n", $content, 2 + $redirects);
279
280 // Set the body for the response.
281 $return->body = array_pop($response);
282
283 // Get the last set of response headers as an array.
284 $headers = explode("\r\n", array_pop($response));
285 }
286
287 // Get the response code from the first offset of the response headers.
288 preg_match('/[0-9]{3}/', array_shift($headers), $matches);
289
290 $code = count($matches) ? $matches[0] : null;
291
292 if (is_numeric($code))
293 {
294 $return->code = (int) $code;
295 }
296
297 // No valid response code was detected.
298 else
299 {
300 throw new UnexpectedValueException('No HTTP response code found.');
301 }
302
303 // Add the response headers to the response object.
304 foreach ($headers as $header)
305 {
306 $pos = strpos($header, ':');
307 $return->headers[trim(substr($header, 0, $pos))] = trim(substr($header, ($pos + 1)));
308 }
309
310 return $return;
311 }
312
313 /**
314 * Method to check if HTTP transport cURL is available for use
315 *
316 * @return boolean true if available, else false
317 *
318 * @since 12.1
319 */
320 public static function isSupported()
321 {
322 return function_exists('curl_version') && curl_version();
323 }
324
325 /**
326 * Check if redirects are allowed
327 *
328 * @return boolean
329 *
330 * @since 12.1
331 */
332 private function redirectsAllowed()
333 {
334 $curlVersion = curl_version();
335
336 // In PHP 5.6.0 or later there are no issues with curl redirects
337 if (version_compare(PHP_VERSION, '5.6', '>='))
338 {
339 // But if open_basedir is enabled we also need to check if libcurl version is 7.19.4 or higher
340 if (!ini_get('open_basedir') || version_compare($curlVersion['version'], '7.19.4', '>='))
341 {
342 return true;
343 }
344 }
345
346 // From PHP 5.4.0 to 5.5.30 curl redirects are only allowed if open_basedir is disabled
347 elseif (version_compare(PHP_VERSION, '5.4', '>='))
348 {
349 if (!ini_get('open_basedir'))
350 {
351 return true;
352 }
353 }
354
355 // From PHP 5.1.5 to 5.3.30 curl redirects are only allowed if safe_mode and open_basedir are disabled
356 else
357 {
358 if (!ini_get('safe_mode') && !ini_get('open_basedir'))
359 {
360 return true;
361 }
362 }
363
364 return false;
365 }
366 }
367