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