]> git.mxchange.org Git - friendica.git/blob - src/Util/Network.php
Move L10n::t() calls to DI::l10n()->t() calls
[friendica.git] / src / Util / Network.php
1 <?php
2 /**
3  * @file src/Util/Network.php
4  */
5 namespace Friendica\Util;
6
7 use DOMDocument;
8 use DomXPath;
9 use Friendica\Core\Config;
10 use Friendica\Core\Hook;
11 use Friendica\Core\Logger;
12 use Friendica\Core\System;
13 use Friendica\DI;
14 use Friendica\Network\CurlResult;
15
16 class Network
17 {
18         /**
19          * Curl wrapper
20          *
21          * If binary flag is true, return binary results.
22          * Set the cookiejar argument to a string (e.g. "/tmp/friendica-cookies.txt")
23          * to preserve cookies from one request to the next.
24          *
25          * @param string  $url            URL to fetch
26          * @param bool    $binary         default false
27          *                                TRUE if asked to return binary results (file download)
28          * @param int     $timeout        Timeout in seconds, default system config value or 60 seconds
29          * @param string  $accept_content supply Accept: header with 'accept_content' as the value
30          * @param string  $cookiejar      Path to cookie jar file
31          * @param int     $redirects      The recursion counter for internal use - default 0
32          *
33          * @return string The fetched content
34          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
35          */
36         public static function fetchUrl(string $url, bool $binary = false, int $timeout = 0, string $accept_content = '', string $cookiejar = '', int &$redirects = 0)
37         {
38                 $ret = self::fetchUrlFull($url, $binary, $timeout, $accept_content, $cookiejar, $redirects);
39
40                 return $ret->getBody();
41         }
42
43         /**
44          * Curl wrapper with array of return values.
45          *
46          * Inner workings and parameters are the same as @ref fetchUrl but returns an array with
47          * all the information collected during the fetch.
48          *
49          * @param string  $url            URL to fetch
50          * @param bool    $binary         default false
51          *                                TRUE if asked to return binary results (file download)
52          * @param int     $timeout        Timeout in seconds, default system config value or 60 seconds
53          * @param string  $accept_content supply Accept: header with 'accept_content' as the value
54          * @param string  $cookiejar      Path to cookie jar file
55          * @param int     $redirects      The recursion counter for internal use - default 0
56          *
57          * @return CurlResult With all relevant information, 'body' contains the actual fetched content.
58          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
59          */
60         public static function fetchUrlFull(string $url, bool $binary = false, int $timeout = 0, string $accept_content = '', string $cookiejar = '', int &$redirects = 0)
61         {
62                 return self::curl(
63                         $url,
64                         $binary,
65                         [
66                                 'timeout'        => $timeout,
67                                 'accept_content' => $accept_content,
68                                 'cookiejar'      => $cookiejar
69                         ],
70                         $redirects
71                 );
72         }
73
74         /**
75          * fetches an URL.
76          *
77          * @param string  $url       URL to fetch
78          * @param bool    $binary    default false
79          *                           TRUE if asked to return binary results (file download)
80          * @param array   $opts      (optional parameters) assoziative array with:
81          *                           'accept_content' => supply Accept: header with 'accept_content' as the value
82          *                           'timeout' => int Timeout in seconds, default system config value or 60 seconds
83          *                           'http_auth' => username:password
84          *                           'novalidate' => do not validate SSL certs, default is to validate using our CA list
85          *                           'nobody' => only return the header
86          *                           'cookiejar' => path to cookie jar file
87          *                           'header' => header array
88          * @param int     $redirects The recursion counter for internal use - default 0
89          *
90          * @return CurlResult
91          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
92          */
93         public static function curl(string $url, bool $binary = false, array $opts = [], int &$redirects = 0)
94         {
95                 $stamp1 = microtime(true);
96
97                 $a = DI::app();
98
99                 if (strlen($url) > 1000) {
100                         Logger::log('URL is longer than 1000 characters. Callstack: ' . System::callstack(20), Logger::DEBUG);
101                         return CurlResult::createErrorCurl(substr($url, 0, 200));
102                 }
103
104                 $parts2 = [];
105                 $parts = parse_url($url);
106                 $path_parts = explode('/', $parts['path'] ?? '');
107                 foreach ($path_parts as $part) {
108                         if (strlen($part) <> mb_strlen($part)) {
109                                 $parts2[] = rawurlencode($part);
110                         } else {
111                                 $parts2[] = $part;
112                         }
113                 }
114                 $parts['path'] = implode('/', $parts2);
115                 $url = self::unparseURL($parts);
116
117                 if (self::isUrlBlocked($url)) {
118                         Logger::log('domain of ' . $url . ' is blocked', Logger::DATA);
119                         return CurlResult::createErrorCurl($url);
120                 }
121
122                 $ch = @curl_init($url);
123
124                 if (($redirects > 8) || (!$ch)) {
125                         return CurlResult::createErrorCurl($url);
126                 }
127
128                 @curl_setopt($ch, CURLOPT_HEADER, true);
129
130                 if (!empty($opts['cookiejar'])) {
131                         curl_setopt($ch, CURLOPT_COOKIEJAR, $opts["cookiejar"]);
132                         curl_setopt($ch, CURLOPT_COOKIEFILE, $opts["cookiejar"]);
133                 }
134
135                 // These settings aren't needed. We're following the location already.
136                 //      @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
137                 //      @curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
138
139                 if (!empty($opts['accept_content'])) {
140                         curl_setopt(
141                                 $ch,
142                                 CURLOPT_HTTPHEADER,
143                                 ['Accept: ' . $opts['accept_content']]
144                         );
145                 }
146
147                 if (!empty($opts['header'])) {
148                         curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['header']);
149                 }
150
151                 @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
152                 @curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());
153
154                 $range = intval(Config::get('system', 'curl_range_bytes', 0));
155
156                 if ($range > 0) {
157                         @curl_setopt($ch, CURLOPT_RANGE, '0-' . $range);
158                 }
159
160                 // Without this setting it seems as if some webservers send compressed content
161                 // This seems to confuse curl so that it shows this uncompressed.
162                 /// @todo  We could possibly set this value to "gzip" or something similar
163                 curl_setopt($ch, CURLOPT_ENCODING, '');
164
165                 if (!empty($opts['headers'])) {
166                         @curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['headers']);
167                 }
168
169                 if (!empty($opts['nobody'])) {
170                         @curl_setopt($ch, CURLOPT_NOBODY, $opts['nobody']);
171                 }
172
173                 if (!empty($opts['timeout'])) {
174                         @curl_setopt($ch, CURLOPT_TIMEOUT, $opts['timeout']);
175                 } else {
176                         $curl_time = Config::get('system', 'curl_timeout', 60);
177                         @curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time));
178                 }
179
180                 // by default we will allow self-signed certs
181                 // but you can override this
182
183                 $check_cert = Config::get('system', 'verifyssl');
184                 @curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
185
186                 if ($check_cert) {
187                         @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
188                 }
189
190                 $proxy = Config::get('system', 'proxy');
191
192                 if (strlen($proxy)) {
193                         @curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
194                         @curl_setopt($ch, CURLOPT_PROXY, $proxy);
195                         $proxyuser = @Config::get('system', 'proxyuser');
196
197                         if (strlen($proxyuser)) {
198                                 @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser);
199                         }
200                 }
201
202                 if (Config::get('system', 'ipv4_resolve', false)) {
203                         curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
204                 }
205
206                 if ($binary) {
207                         @curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
208                 }
209
210                 // don't let curl abort the entire application
211                 // if it throws any errors.
212
213                 $s = @curl_exec($ch);
214                 $curl_info = @curl_getinfo($ch);
215
216                 // Special treatment for HTTP Code 416
217                 // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416
218                 if (($curl_info['http_code'] == 416) && ($range > 0)) {
219                         @curl_setopt($ch, CURLOPT_RANGE, '');
220                         $s = @curl_exec($ch);
221                         $curl_info = @curl_getinfo($ch);
222                 }
223
224                 $curlResponse = new CurlResult($url, $s, $curl_info, curl_errno($ch), curl_error($ch));
225
226                 if ($curlResponse->isRedirectUrl()) {
227                         $redirects++;
228                         Logger::log('curl: redirect ' . $url . ' to ' . $curlResponse->getRedirectUrl());
229                         @curl_close($ch);
230                         return self::curl($curlResponse->getRedirectUrl(), $binary, $opts, $redirects);
231                 }
232
233                 @curl_close($ch);
234
235                 DI::profiler()->saveTimestamp($stamp1, 'network', System::callstack());
236
237                 return $curlResponse;
238         }
239
240         /**
241          * Send POST request to $url
242          *
243          * @param string  $url       URL to post
244          * @param mixed   $params    array of POST variables
245          * @param array   $headers   HTTP headers
246          * @param int     $redirects Recursion counter for internal use - default = 0
247          * @param int     $timeout   The timeout in seconds, default system config value or 60 seconds
248          *
249          * @return CurlResult The content
250          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
251          */
252         public static function post(string $url, $params, array $headers = [], int $timeout = 0, int &$redirects = 0)
253         {
254                 $stamp1 = microtime(true);
255
256                 if (self::isUrlBlocked($url)) {
257                         Logger::log('post_url: domain of ' . $url . ' is blocked', Logger::DATA);
258                         return CurlResult::createErrorCurl($url);
259                 }
260
261                 $a = DI::app();
262                 $ch = curl_init($url);
263
264                 if (($redirects > 8) || (!$ch)) {
265                         return CurlResult::createErrorCurl($url);
266                 }
267
268                 Logger::log('post_url: start ' . $url, Logger::DATA);
269
270                 curl_setopt($ch, CURLOPT_HEADER, true);
271                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
272                 curl_setopt($ch, CURLOPT_POST, 1);
273                 curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
274                 curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());
275
276                 if (Config::get('system', 'ipv4_resolve', false)) {
277                         curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
278                 }
279
280                 if (intval($timeout)) {
281                         curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
282                 } else {
283                         $curl_time = Config::get('system', 'curl_timeout', 60);
284                         curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time));
285                 }
286
287                 if (!empty($headers)) {
288                         curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
289                 }
290
291                 $check_cert = Config::get('system', 'verifyssl');
292                 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
293
294                 if ($check_cert) {
295                         @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
296                 }
297
298                 $proxy = Config::get('system', 'proxy');
299
300                 if (strlen($proxy)) {
301                         curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
302                         curl_setopt($ch, CURLOPT_PROXY, $proxy);
303                         $proxyuser = Config::get('system', 'proxyuser');
304                         if (strlen($proxyuser)) {
305                                 curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser);
306                         }
307                 }
308
309                 // don't let curl abort the entire application
310                 // if it throws any errors.
311
312                 $s = @curl_exec($ch);
313
314                 $curl_info = curl_getinfo($ch);
315
316                 $curlResponse = new CurlResult($url, $s, $curl_info, curl_errno($ch), curl_error($ch));
317
318                 if ($curlResponse->isRedirectUrl()) {
319                         $redirects++;
320                         Logger::log('post_url: redirect ' . $url . ' to ' . $curlResponse->getRedirectUrl());
321                         curl_close($ch);
322                         return self::post($curlResponse->getRedirectUrl(), $params, $headers, $redirects, $timeout);
323                 }
324
325                 curl_close($ch);
326
327                 DI::profiler()->saveTimestamp($stamp1, 'network', System::callstack());
328
329                 // Very old versions of Lighttpd don't like the "Expect" header, so we remove it when needed
330                 if ($curlResponse->getReturnCode() == 417) {
331                         $redirects++;
332
333                         if (empty($headers)) {
334                                 $headers = ['Expect:'];
335                         } else {
336                                 if (!in_array('Expect:', $headers)) {
337                                         array_push($headers, 'Expect:');
338                                 }
339                         }
340                         Logger::info('Server responds with 417, applying workaround', ['url' => $url]);
341                         return self::post($url, $params, $headers, $redirects, $timeout);
342                 }
343
344                 Logger::log('post_url: end ' . $url, Logger::DATA);
345
346                 return $curlResponse;
347         }
348
349         /**
350          * Return raw post data from a post request
351          *
352          * @return string post data
353          */
354         public static function postdata()
355         {
356                 return file_get_contents('php://input');
357         }
358
359         /**
360          * Check URL to see if it's real
361          *
362          * Take a URL from the wild, prepend http:// if necessary
363          * and check DNS to see if it's real (or check if is a valid IP address)
364          *
365          * @param string $url The URL to be validated
366          * @return string|boolean The actual working URL, false else
367          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
368          */
369         public static function isUrlValid(string $url)
370         {
371                 if (Config::get('system', 'disable_url_validation')) {
372                         return $url;
373                 }
374
375                 // no naked subdomains (allow localhost for tests)
376                 if (strpos($url, '.') === false && strpos($url, '/localhost/') === false) {
377                         return false;
378                 }
379
380                 if (substr($url, 0, 4) != 'http') {
381                         $url = 'http://' . $url;
382                 }
383
384                 /// @TODO Really suppress function outcomes? Why not find them + debug them?
385                 $h = @parse_url($url);
386
387                 if (!empty($h['host']) && (@dns_get_record($h['host'], DNS_A + DNS_CNAME) || filter_var($h['host'], FILTER_VALIDATE_IP))) {
388                         return $url;
389                 }
390
391                 return false;
392         }
393
394         /**
395          * Checks that email is an actual resolvable internet address
396          *
397          * @param string $addr The email address
398          * @return boolean True if it's a valid email address, false if it's not
399          */
400         public static function isEmailDomainValid(string $addr)
401         {
402                 if (Config::get('system', 'disable_email_validation')) {
403                         return true;
404                 }
405
406                 if (! strpos($addr, '@')) {
407                         return false;
408                 }
409
410                 $h = substr($addr, strpos($addr, '@') + 1);
411
412                 // Concerning the @ see here: https://stackoverflow.com/questions/36280957/dns-get-record-a-temporary-server-error-occurred
413                 if ($h && (@dns_get_record($h, DNS_A + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP))) {
414                         return true;
415                 }
416                 if ($h && @dns_get_record($h, DNS_CNAME + DNS_MX)) {
417                         return true;
418                 }
419                 return false;
420         }
421
422         /**
423          * Check if URL is allowed
424          *
425          * Check $url against our list of allowed sites,
426          * wildcards allowed. If allowed_sites is unset return true;
427          *
428          * @param string $url URL which get tested
429          * @return boolean True if url is allowed otherwise return false
430          */
431         public static function isUrlAllowed(string $url)
432         {
433                 $h = @parse_url($url);
434
435                 if (! $h) {
436                         return false;
437                 }
438
439                 $str_allowed = Config::get('system', 'allowed_sites');
440                 if (! $str_allowed) {
441                         return true;
442                 }
443
444                 $found = false;
445
446                 $host = strtolower($h['host']);
447
448                 // always allow our own site
449                 if ($host == strtolower($_SERVER['SERVER_NAME'])) {
450                         return true;
451                 }
452
453                 $fnmatch = function_exists('fnmatch');
454                 $allowed = explode(',', $str_allowed);
455
456                 if (count($allowed)) {
457                         foreach ($allowed as $a) {
458                                 $pat = strtolower(trim($a));
459                                 if (($fnmatch && fnmatch($pat, $host)) || ($pat == $host)) {
460                                         $found = true;
461                                         break;
462                                 }
463                         }
464                 }
465                 return $found;
466         }
467
468         /**
469          * Checks if the provided url domain is on the domain blocklist.
470          * Returns true if it is or malformed URL, false if not.
471          *
472          * @param string $url The url to check the domain from
473          *
474          * @return boolean
475          */
476         public static function isUrlBlocked(string $url)
477         {
478                 $host = @parse_url($url, PHP_URL_HOST);
479                 if (!$host) {
480                         return false;
481                 }
482
483                 $domain_blocklist = Config::get('system', 'blocklist', []);
484                 if (!$domain_blocklist) {
485                         return false;
486                 }
487
488                 foreach ($domain_blocklist as $domain_block) {
489                         if (fnmatch(strtolower($domain_block['domain']), strtolower($host))) {
490                                 return true;
491                         }
492                 }
493
494                 return false;
495         }
496
497         /**
498          * Check if email address is allowed to register here.
499          *
500          * Compare against our list (wildcards allowed).
501          *
502          * @param  string $email email address
503          * @return boolean False if not allowed, true if allowed
504          *                       or if allowed list is not configured
505          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
506          */
507         public static function isEmailDomainAllowed(string $email)
508         {
509                 $domain = strtolower(substr($email, strpos($email, '@') + 1));
510                 if (!$domain) {
511                         return false;
512                 }
513
514                 $str_allowed = Config::get('system', 'allowed_email', '');
515                 if (empty($str_allowed)) {
516                         return true;
517                 }
518
519                 $allowed = explode(',', $str_allowed);
520
521                 return self::isDomainAllowed($domain, $allowed);
522         }
523
524         /**
525          * Checks for the existence of a domain in a domain list
526          *
527          * @param string $domain
528          * @param array  $domain_list
529          * @return boolean
530          */
531         public static function isDomainAllowed(string $domain, array $domain_list)
532         {
533                 $found = false;
534
535                 foreach ($domain_list as $item) {
536                         $pat = strtolower(trim($item));
537                         if (fnmatch($pat, $domain) || ($pat == $domain)) {
538                                 $found = true;
539                                 break;
540                         }
541                 }
542
543                 return $found;
544         }
545
546         public static function lookupAvatarByEmail(string $email)
547         {
548                 $avatar['size'] = 300;
549                 $avatar['email'] = $email;
550                 $avatar['url'] = '';
551                 $avatar['success'] = false;
552
553                 Hook::callAll('avatar_lookup', $avatar);
554
555                 if (! $avatar['success']) {
556                         $avatar['url'] = DI::baseUrl() . '/images/person-300.jpg';
557                 }
558
559                 Logger::log('Avatar: ' . $avatar['email'] . ' ' . $avatar['url'], Logger::DEBUG);
560                 return $avatar['url'];
561         }
562
563         /**
564          * Remove Google Analytics and other tracking platforms params from URL
565          *
566          * @param string $url Any user-submitted URL that may contain tracking params
567          * @return string The same URL stripped of tracking parameters
568          */
569         public static function stripTrackingQueryParams(string $url)
570         {
571                 $urldata = parse_url($url);
572                 if (!empty($urldata["query"])) {
573                         $query = $urldata["query"];
574                         parse_str($query, $querydata);
575
576                         if (is_array($querydata)) {
577                                 foreach ($querydata as $param => $value) {
578                                         if (in_array(
579                                                 $param,
580                                                 [
581                                                         "utm_source", "utm_medium", "utm_term", "utm_content", "utm_campaign",
582                                                         "wt_mc", "pk_campaign", "pk_kwd", "mc_cid", "mc_eid",
583                                                         "fb_action_ids", "fb_action_types", "fb_ref",
584                                                         "awesm", "wtrid",
585                                                         "woo_campaign", "woo_source", "woo_medium", "woo_content", "woo_term"]
586                                                 )
587                                         ) {
588                                                 $pair = $param . "=" . urlencode($value);
589                                                 $url = str_replace($pair, "", $url);
590
591                                                 // Second try: if the url isn't encoded completely
592                                                 $pair = $param . "=" . str_replace(" ", "+", $value);
593                                                 $url = str_replace($pair, "", $url);
594
595                                                 // Third try: Maybey the url isn't encoded at all
596                                                 $pair = $param . "=" . $value;
597                                                 $url = str_replace($pair, "", $url);
598
599                                                 $url = str_replace(["?&", "&&"], ["?", ""], $url);
600                                         }
601                                 }
602                         }
603
604                         if (substr($url, -1, 1) == "?") {
605                                 $url = substr($url, 0, -1);
606                         }
607                 }
608
609                 return $url;
610         }
611
612         /**
613          * Returns the original URL of the provided URL
614          *
615          * This function strips tracking query params and follows redirections, either
616          * through HTTP code or meta refresh tags. Stops after 10 redirections.
617          *
618          * @todo  Remove the $fetchbody parameter that generates an extraneous HEAD request
619          *
620          * @see   ParseUrl::getSiteinfo
621          *
622          * @param string $url       A user-submitted URL
623          * @param int    $depth     The current redirection recursion level (internal)
624          * @param bool   $fetchbody Wether to fetch the body or not after the HEAD requests
625          * @return string A canonical URL
626          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
627          */
628         public static function finalUrl(string $url, int $depth = 1, bool $fetchbody = false)
629         {
630                 $a = DI::app();
631
632                 $url = self::stripTrackingQueryParams($url);
633
634                 if ($depth > 10) {
635                         return $url;
636                 }
637
638                 $url = trim($url, "'");
639
640                 $stamp1 = microtime(true);
641
642                 $ch = curl_init();
643                 curl_setopt($ch, CURLOPT_URL, $url);
644                 curl_setopt($ch, CURLOPT_HEADER, 1);
645                 curl_setopt($ch, CURLOPT_NOBODY, 1);
646                 curl_setopt($ch, CURLOPT_TIMEOUT, 10);
647                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
648                 curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());
649
650                 curl_exec($ch);
651                 $curl_info = @curl_getinfo($ch);
652                 $http_code = $curl_info['http_code'];
653                 curl_close($ch);
654
655                 DI::profiler()->saveTimestamp($stamp1, "network", System::callstack());
656
657                 if ($http_code == 0) {
658                         return $url;
659                 }
660
661                 if (in_array($http_code, ['301', '302'])) {
662                         if (!empty($curl_info['redirect_url'])) {
663                                 return self::finalUrl($curl_info['redirect_url'], ++$depth, $fetchbody);
664                         } elseif (!empty($curl_info['location'])) {
665                                 return self::finalUrl($curl_info['location'], ++$depth, $fetchbody);
666                         }
667                 }
668
669                 // Check for redirects in the meta elements of the body if there are no redirects in the header.
670                 if (!$fetchbody) {
671                         return(self::finalUrl($url, ++$depth, true));
672                 }
673
674                 // if the file is too large then exit
675                 if ($curl_info["download_content_length"] > 1000000) {
676                         return $url;
677                 }
678
679                 // if it isn't a HTML file then exit
680                 if (!empty($curl_info["content_type"]) && !strstr(strtolower($curl_info["content_type"]), "html")) {
681                         return $url;
682                 }
683
684                 $stamp1 = microtime(true);
685
686                 $ch = curl_init();
687                 curl_setopt($ch, CURLOPT_URL, $url);
688                 curl_setopt($ch, CURLOPT_HEADER, 0);
689                 curl_setopt($ch, CURLOPT_NOBODY, 0);
690                 curl_setopt($ch, CURLOPT_TIMEOUT, 10);
691                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
692                 curl_setopt($ch, CURLOPT_USERAGENT, $a->getUserAgent());
693
694                 $body = curl_exec($ch);
695                 curl_close($ch);
696
697                 DI::profiler()->saveTimestamp($stamp1, "network", System::callstack());
698
699                 if (trim($body) == "") {
700                         return $url;
701                 }
702
703                 // Check for redirect in meta elements
704                 $doc = new DOMDocument();
705                 @$doc->loadHTML($body);
706
707                 $xpath = new DomXPath($doc);
708
709                 $list = $xpath->query("//meta[@content]");
710                 foreach ($list as $node) {
711                         $attr = [];
712                         if ($node->attributes->length) {
713                                 foreach ($node->attributes as $attribute) {
714                                         $attr[$attribute->name] = $attribute->value;
715                                 }
716                         }
717
718                         if (@$attr["http-equiv"] == 'refresh') {
719                                 $path = $attr["content"];
720                                 $pathinfo = explode(";", $path);
721                                 foreach ($pathinfo as $value) {
722                                         if (substr(strtolower($value), 0, 4) == "url=") {
723                                                 return self::finalUrl(substr($value, 4), ++$depth);
724                                         }
725                                 }
726                         }
727                 }
728
729                 return $url;
730         }
731
732         /**
733          * Find the matching part between two url
734          *
735          * @param string $url1
736          * @param string $url2
737          * @return string The matching part
738          */
739         public static function getUrlMatch(string $url1, string $url2)
740         {
741                 if (($url1 == "") || ($url2 == "")) {
742                         return "";
743                 }
744
745                 $url1 = Strings::normaliseLink($url1);
746                 $url2 = Strings::normaliseLink($url2);
747
748                 $parts1 = parse_url($url1);
749                 $parts2 = parse_url($url2);
750
751                 if (!isset($parts1["host"]) || !isset($parts2["host"])) {
752                         return "";
753                 }
754
755                 if (empty($parts1["scheme"])) {
756                         $parts1["scheme"] = '';
757                 }
758                 if (empty($parts2["scheme"])) {
759                         $parts2["scheme"] = '';
760                 }
761
762                 if ($parts1["scheme"] != $parts2["scheme"]) {
763                         return "";
764                 }
765
766                 if (empty($parts1["host"])) {
767                         $parts1["host"] = '';
768                 }
769                 if (empty($parts2["host"])) {
770                         $parts2["host"] = '';
771                 }
772
773                 if ($parts1["host"] != $parts2["host"]) {
774                         return "";
775                 }
776
777                 if (empty($parts1["port"])) {
778                         $parts1["port"] = '';
779                 }
780                 if (empty($parts2["port"])) {
781                         $parts2["port"] = '';
782                 }
783
784                 if ($parts1["port"] != $parts2["port"]) {
785                         return "";
786                 }
787
788                 $match = $parts1["scheme"]."://".$parts1["host"];
789
790                 if ($parts1["port"]) {
791                         $match .= ":".$parts1["port"];
792                 }
793
794                 if (empty($parts1["path"])) {
795                         $parts1["path"] = '';
796                 }
797                 if (empty($parts2["path"])) {
798                         $parts2["path"] = '';
799                 }
800
801                 $pathparts1 = explode("/", $parts1["path"]);
802                 $pathparts2 = explode("/", $parts2["path"]);
803
804                 $i = 0;
805                 $path = "";
806                 do {
807                         $path1 = $pathparts1[$i] ?? '';
808                         $path2 = $pathparts2[$i] ?? '';
809
810                         if ($path1 == $path2) {
811                                 $path .= $path1."/";
812                         }
813                 } while (($path1 == $path2) && ($i++ <= count($pathparts1)));
814
815                 $match .= $path;
816
817                 return Strings::normaliseLink($match);
818         }
819
820         /**
821          * Glue url parts together
822          *
823          * @param array $parsed URL parts
824          *
825          * @return string The glued URL
826          */
827         public static function unparseURL(array $parsed)
828         {
829                 $get = function ($key) use ($parsed) {
830                         return isset($parsed[$key]) ? $parsed[$key] : null;
831                 };
832
833                 $pass      = $get('pass');
834                 $user      = $get('user');
835                 $userinfo  = $pass !== null ? "$user:$pass" : $user;
836                 $port      = $get('port');
837                 $scheme    = $get('scheme');
838                 $query     = $get('query');
839                 $fragment  = $get('fragment');
840                 $authority = ($userinfo !== null ? $userinfo."@" : '') .
841                                                 $get('host') .
842                                                 ($port ? ":$port" : '');
843
844                 return  (strlen($scheme) ? $scheme.":" : '') .
845                         (strlen($authority) ? "//".$authority : '') .
846                         $get('path') .
847                         (strlen($query) ? "?".$query : '') .
848                         (strlen($fragment) ? "#".$fragment : '');
849         }
850
851
852         /**
853          * Switch the scheme of an url between http and https
854          *
855          * @param string $url URL
856          *
857          * @return string switched URL
858          */
859         public static function switchScheme(string $url)
860         {
861                 $scheme = parse_url($url, PHP_URL_SCHEME);
862                 if (empty($scheme)) {
863                         return $url;
864                 }
865
866                 if ($scheme === 'http') {
867                         $url = str_replace('http://', 'https://', $url);
868                 } elseif ($scheme === 'https') {
869                         $url = str_replace('https://', 'http://', $url);
870                 }
871
872                 return $url;
873         }
874 }