]>
Commit | Line | Data |
---|---|---|
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 | 32 | function 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 | */ | |
168 | function 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 | */ |
217 | function 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 | */ | |
250 | function 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 | */ | |
281 | function 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 | */ | |
353 | function 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 | */ | |
371 | function 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 | */ | |
389 | function 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 | */ | |
413 | function 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 | } |