JHttpTransportCurl::request()   F
last analyzed

Complexity

Conditions 28
Paths > 20000

Size

Total Lines 175
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 68
c 0
b 0
f 0
dl 0
loc 175
rs 0
cc 28
nc 69120
nop 6

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	 * @see     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