]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - application/HttpUtils.php
Merge pull request #976 from ArthurHoaro/hotfix/url-parentheses
[github/shaarli/Shaarli.git] / application / HttpUtils.php
CommitLineData
451314eb
V
1<?php
2/**
3 * GET an HTTP URL to retrieve its content
634783f9 4 * Uses the cURL library or a fallback method
451314eb
V
5 *
6 * @param string $url URL to get (http://...)
7 * @param int $timeout network timeout (in seconds)
8 * @param int $maxBytes maximum downloaded bytes (default: 4 MiB)
9 *
10 * @return array HTTP response headers, downloaded content
11 *
12 * Output format:
13 * [0] = associative array containing HTTP response headers
14 * [1] = URL content (downloaded data)
15 *
16 * Example:
1557cefb 17 * list($headers, $data) = get_http_response('http://sebauvage.net/');
451314eb
V
18 * if (strpos($headers[0], '200 OK') !== false) {
19 * echo 'Data type: '.htmlspecialchars($headers['Content-Type']);
20 * } else {
21 * echo 'There was an error: '.htmlspecialchars($headers[0]);
22 * }
23 *
634783f9 24 * @see https://secure.php.net/manual/en/ref.curl.php
25 * @see https://secure.php.net/manual/en/functions.anonymous.php
26 * @see https://secure.php.net/manual/en/function.preg-split.php
27 * @see https://secure.php.net/manual/en/function.explode.php
28 * @see http://stackoverflow.com/q/17641073
29 * @see http://stackoverflow.com/q/9183178
30 * @see http://stackoverflow.com/q/1462720
451314eb 31 */
1557cefb 32function get_http_response($url, $timeout = 30, $maxBytes = 4194304)
451314eb 33{
1557cefb 34 $urlObj = new Url($url);
caa69b58 35 $cleanUrl = $urlObj->idnToAscii();
ce7b0b64 36
634783f9 37 if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) {
1557cefb
A
38 return array(array(0 => 'Invalid HTTP Url'), false);
39 }
40
634783f9 41 $userAgent =
42 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)'
43 . ' Gecko/20100101 Firefox/45.0';
44 $acceptLanguage =
45 substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3';
46 $maxRedirs = 3;
47
48 if (!function_exists('curl_init')) {
49 return get_http_response_fallback(
50 $cleanUrl,
51 $timeout,
52 $maxBytes,
53 $userAgent,
54 $acceptLanguage,
55 $maxRedirs
56 );
57 }
58
59 $ch = curl_init($cleanUrl);
60 if ($ch === false) {
61 return array(array(0 => 'curl_init() error'), false);
62 }
63
64 // General cURL settings
65 curl_setopt($ch, CURLOPT_AUTOREFERER, true);
66 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
67 curl_setopt($ch, CURLOPT_HEADER, true);
68 curl_setopt(
69 $ch,
70 CURLOPT_HTTPHEADER,
71 array('Accept-Language: ' . $acceptLanguage)
72 );
73 curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs);
74 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
75 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
76 curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
77
78 // Max download size management
79 curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024);
80 curl_setopt($ch, CURLOPT_NOPROGRESS, false);
81 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
82 function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes)
83 {
84 if (version_compare(phpversion(), '5.5', '<')) {
85 // PHP version lower than 5.5
86 // Callback has 4 arguments
87 $downloaded = $arg1;
88 } else {
89 // Callback has 5 arguments
90 $downloaded = $arg2;
91 }
92 // Non-zero return stops downloading
93 return ($downloaded > $maxBytes) ? 1 : 0;
94 }
95 );
96
97 $response = curl_exec($ch);
98 $errorNo = curl_errno($ch);
99 $errorStr = curl_error($ch);
100 $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
101 curl_close($ch);
102
103 if ($response === false) {
104 if ($errorNo == CURLE_COULDNT_RESOLVE_HOST) {
105 /*
106 * Workaround to match fallback method behaviour
107 * Removing this would require updating
108 * GetHttpUrlTest::testGetInvalidRemoteUrl()
109 */
110 return array(false, false);
111 }
112 return array(array(0 => 'curl_exec() error: ' . $errorStr), false);
113 }
114
115 // Formatting output like the fallback method
116 $rawHeaders = substr($response, 0, $headSize);
117
118 // Keep only headers from latest redirection
119 $rawHeadersArrayRedirs = explode("\r\n\r\n", trim($rawHeaders));
120 $rawHeadersLastRedir = end($rawHeadersArrayRedirs);
121
122 $content = substr($response, $headSize);
123 $headers = array();
124 foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) {
ee6f4b64 125 if (empty($line) || ctype_space($line)) {
634783f9 126 continue;
127 }
128 $splitLine = explode(': ', $line, 2);
129 if (count($splitLine) > 1) {
130 $key = $splitLine[0];
131 $value = $splitLine[1];
132 if (array_key_exists($key, $headers)) {
133 if (!is_array($headers[$key])) {
134 $headers[$key] = array(0 => $headers[$key]);
135 }
136 $headers[$key][] = $value;
137 } else {
138 $headers[$key] = $value;
139 }
140 } else {
141 $headers[] = $splitLine[0];
142 }
143 }
144
145 return array($headers, $content);
146}
147
148/**
149 * GET an HTTP URL to retrieve its content (fallback method)
150 *
151 * @param string $cleanUrl URL to get (http://... valid and in ASCII form)
152 * @param int $timeout network timeout (in seconds)
153 * @param int $maxBytes maximum downloaded bytes
154 * @param string $userAgent "User-Agent" header
155 * @param string $acceptLanguage "Accept-Language" header
156 * @param int $maxRedr maximum amount of redirections followed
157 *
158 * @return array HTTP response headers, downloaded content
159 *
160 * Output format:
161 * [0] = associative array containing HTTP response headers
162 * [1] = URL content (downloaded data)
163 *
164 * @see http://php.net/manual/en/function.file-get-contents.php
165 * @see http://php.net/manual/en/function.stream-context-create.php
166 * @see http://php.net/manual/en/function.get-headers.php
167 */
168function get_http_response_fallback(
169 $cleanUrl,
170 $timeout,
171 $maxBytes,
172 $userAgent,
173 $acceptLanguage,
174 $maxRedr
175) {
451314eb
V
176 $options = array(
177 'http' => array(
178 'method' => 'GET',
179 'timeout' => $timeout,
634783f9 180 'user_agent' => $userAgent,
181 'header' => "Accept: */*\r\n"
182 . 'Accept-Language: ' . $acceptLanguage
451314eb
V
183 )
184 );
185
1557cefb 186 stream_context_set_default($options);
634783f9 187 list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
ce7b0b64
A
188 if (! $headers || strpos($headers[0], '200 OK') === false) {
189 $options['http']['request_fulluri'] = true;
190 stream_context_set_default($options);
634783f9 191 list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
ce7b0b64 192 }
1557cefb 193
634783f9 194 if (! $headers) {
1557cefb
A
195 return array($headers, false);
196 }
451314eb
V
197
198 try {
199 // TODO: catch Exception in calling code (thumbnailer)
ce7b0b64 200 $context = stream_context_create($options);
1557cefb 201 $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes);
451314eb
V
202 } catch (Exception $exc) {
203 return array(array(0 => 'HTTP Error'), $exc->getMessage());
204 }
205
1557cefb
A
206 return array($headers, $content);
207}
208
209/**
ce7b0b64 210 * Retrieve HTTP headers, following n redirections (temporary and permanent ones).
1557cefb 211 *
ce7b0b64 212 * @param string $url initial URL to reach.
caa69b58 213 * @param int $redirectionLimit max redirection follow.
1557cefb 214 *
ce7b0b64 215 * @return array HTTP headers, or false if it failed.
1557cefb
A
216 */
217function get_redirected_headers($url, $redirectionLimit = 3)
218{
219 $headers = get_headers($url, 1);
ce7b0b64
A
220 if (!empty($headers['location']) && empty($headers['Location'])) {
221 $headers['Location'] = $headers['location'];
222 }
1557cefb
A
223
224 // Headers found, redirection found, and limit not reached.
225 if ($redirectionLimit-- > 0
226 && !empty($headers)
227 && (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false)
228 && !empty($headers['Location'])) {
229
230 $redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location'];
231 if ($redirection != $url) {
ce7b0b64 232 $redirection = getAbsoluteUrl($url, $redirection);
1557cefb
A
233 return get_redirected_headers($redirection, $redirectionLimit);
234 }
451314eb
V
235 }
236
1557cefb 237 return array($headers, $url);
451314eb 238}
482d67bd 239
ce7b0b64
A
240/**
241 * Get an absolute URL from a complete one, and another absolute/relative URL.
242 *
243 * @param string $originalUrl The original complete URL.
244 * @param string $newUrl The new one, absolute or relative.
245 *
246 * @return string Final URL:
247 * - $newUrl if it was already an absolute URL.
248 * - if it was relative, absolute URL from $originalUrl path.
249 */
250function getAbsoluteUrl($originalUrl, $newUrl)
251{
252 $newScheme = parse_url($newUrl, PHP_URL_SCHEME);
253 // Already an absolute URL.
254 if (!empty($newScheme)) {
255 return $newUrl;
256 }
257
258 $parts = parse_url($originalUrl);
259 $final = $parts['scheme'] .'://'. $parts['host'];
260 $final .= (!empty($parts['port'])) ? $parts['port'] : '';
261 $final .= '/';
262 if ($newUrl[0] != '/') {
263 $final .= substr(ltrim($parts['path'], '/'), 0, strrpos($parts['path'], '/'));
264 }
265 $final .= ltrim($newUrl, '/');
266 return $final;
267}
268
482d67bd
V
269/**
270 * Returns the server's base URL: scheme://domain.tld[:port]
271 *
272 * @param array $server the $_SERVER array
273 *
274 * @return string the server's base URL
275 *
276 * @see http://www.ietf.org/rfc/rfc7239.txt
277 * @see http://www.ietf.org/rfc/rfc6648.txt
278 * @see http://stackoverflow.com/a/3561399
279 * @see http://stackoverflow.com/q/452375
280 */
281function server_url($server)
282{
283 $scheme = 'http';
284 $port = '';
285
286 // Shaarli is served behind a proxy
287 if (isset($server['HTTP_X_FORWARDED_PROTO'])) {
288 // Keep forwarded scheme
85244fa0
A
289 if (strpos($server['HTTP_X_FORWARDED_PROTO'], ',') !== false) {
290 $schemes = explode(',', $server['HTTP_X_FORWARDED_PROTO']);
291 $scheme = trim($schemes[0]);
292 } else {
293 $scheme = $server['HTTP_X_FORWARDED_PROTO'];
294 }
482d67bd
V
295
296 if (isset($server['HTTP_X_FORWARDED_PORT'])) {
297 // Keep forwarded port
85244fa0
A
298 if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) {
299 $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']);
8e4be773 300 $port = trim($ports[0]);
85244fa0 301 } else {
8e4be773
A
302 $port = $server['HTTP_X_FORWARDED_PORT'];
303 }
304
305 if (($scheme == 'http' && $port != '80')
306 || ($scheme == 'https' && $port != '443')
307 ) {
308 $port = ':' . $port;
309 } else {
310 $port = '';
85244fa0 311 }
482d67bd
V
312 }
313
b80315e2
SM
314 if (isset($server['HTTP_X_FORWARDED_HOST'])) {
315 // Keep forwarded host
316 if (strpos($server['HTTP_X_FORWARDED_HOST'], ',') !== false) {
317 $hosts = explode(',', $server['HTTP_X_FORWARDED_HOST']);
318 $host = trim($hosts[0]);
319 } else {
320 $host = $server['HTTP_X_FORWARDED_HOST'];
321 }
322 } else {
323 $host = $server['SERVER_NAME'];
324 }
325
326 return $scheme.'://'.$host.$port;
482d67bd
V
327 }
328
329 // SSL detection
330 if ((! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on')
331 || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')) {
332 $scheme = 'https';
333 }
334
335 // Do not append standard port values
336 if (($scheme == 'http' && $server['SERVER_PORT'] != '80')
337 || ($scheme == 'https' && $server['SERVER_PORT'] != '443')) {
338 $port = ':'.$server['SERVER_PORT'];
339 }
340
341 return $scheme.'://'.$server['SERVER_NAME'].$port;
342}
343
344/**
345 * Returns the absolute URL of the current script, without the query
346 *
347 * If the resource is "index.php", then it is removed (for better-looking URLs)
348 *
349 * @param array $server the $_SERVER array
350 *
351 * @return string the absolute URL of the current script, without the query
352 */
353function index_url($server)
354{
355 $scriptname = $server['SCRIPT_NAME'];
5046bcb6 356 if (endsWith($scriptname, 'index.php')) {
482d67bd
V
357 $scriptname = substr($scriptname, 0, -9);
358 }
359 return server_url($server) . $scriptname;
360}
361
362/**
363 * Returns the absolute URL of the current script, with the query
364 *
365 * If the resource is "index.php", then it is removed (for better-looking URLs)
366 *
367 * @param array $server the $_SERVER array
368 *
369 * @return string the absolute URL of the current script, with the query
370 */
371function page_url($server)
372{
373 if (! empty($server['QUERY_STRING'])) {
374 return index_url($server).'?'.$server['QUERY_STRING'];
375 }
376 return index_url($server);
377}
50d17918
A
378
379/**
380 * Retrieve the initial IP forwarded by the reverse proxy.
381 *
382 * Inspired from: https://github.com/zendframework/zend-http/blob/master/src/PhpEnvironment/RemoteAddress.php
383 *
384 * @param array $server $_SERVER array which contains HTTP headers.
385 * @param array $trustedIps List of trusted IP from the configuration.
386 *
387 * @return string|bool The forwarded IP, or false if none could be extracted.
388 */
389function getIpAddressFromProxy($server, $trustedIps)
390{
391 $forwardedIpHeader = 'HTTP_X_FORWARDED_FOR';
392 if (empty($server[$forwardedIpHeader])) {
393 return false;
394 }
395
396 $ips = preg_split('/\s*,\s*/', $server[$forwardedIpHeader]);
397 $ips = array_diff($ips, $trustedIps);
398 if (empty($ips)) {
399 return false;
400 }
401
402 return array_pop($ips);
403}
a3130d2c
A
404
405/**
406 * Returns true if Shaarli's currently browsed in HTTPS.
407 * Supports reverse proxies (if the headers are correctly set).
408 *
409 * @param array $server $_SERVER.
410 *
411 * @return bool true if HTTPS, false otherwise.
412 */
413function is_https($server)
414{
415
416 if (isset($server['HTTP_X_FORWARDED_PORT'])) {
417 // Keep forwarded port
418 if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) {
419 $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']);
420 $port = trim($ports[0]);
421 } else {
422 $port = $server['HTTP_X_FORWARDED_PORT'];
423 }
424
425 if ($port == '443') {
426 return true;
427 }
428 }
429
430 return ! empty($server['HTTPS']);
431}