]> git.mxchange.org Git - friendica.git/blob - src/Util/Network.php
Merge pull request #13070 from xundeenergie/fix-share-via
[friendica.git] / src / Util / Network.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Util;
23
24 use Friendica\Core\Hook;
25 use Friendica\Core\Logger;
26 use Friendica\DI;
27 use Friendica\Model\Contact;
28 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
29 use Friendica\Network\HTTPClient\Client\HttpClientOptions;
30 use Friendica\Network\HTTPException\NotModifiedException;
31 use GuzzleHttp\Psr7\Uri;
32 use Psr\Http\Message\UriInterface;
33
34 class Network
35 {
36
37         /**
38          * Return raw post data from a post request
39          *
40          * @return string post data
41          */
42         public static function postdata()
43         {
44                 return file_get_contents('php://input');
45         }
46
47         /**
48          * Check URL to see if it's real
49          *
50          * Take a URL from the wild, prepend http:// if necessary
51          * and check DNS to see if it's real (or check if is a valid IP address)
52          *
53          * @param string $url The URL to be validated
54          *
55          * @return string|boolean The actual working URL, false else
56          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
57          */
58         public static function isUrlValid(string $url)
59         {
60                 if (DI::config()->get('system', 'disable_url_validation')) {
61                         return $url;
62                 }
63
64                 // no naked subdomains (allow localhost for tests)
65                 if (strpos($url, '.') === false && strpos($url, '/localhost/') === false) {
66                         return false;
67                 }
68
69                 if (substr($url, 0, 4) != 'http') {
70                         $url = 'http://' . $url;
71                 }
72
73                 $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
74                 $host = parse_url($url, PHP_URL_HOST);
75
76                 if (empty($host) || !(filter_var($host, FILTER_VALIDATE_IP) || @dns_get_record($host . '.', DNS_A + DNS_AAAA))) {
77                         return false;
78                 }
79
80                 if (in_array(parse_url($url, PHP_URL_SCHEME), ['https', 'http'])) {
81                         $options = [HttpClientOptions::VERIFY => true, HttpClientOptions::TIMEOUT => $xrd_timeout];
82                         try {
83                                 $curlResult = DI::httpClient()->head($url, $options);
84                         } catch (\Exception $e) {
85                                 return false;
86                         }
87
88                         // Workaround for systems that can't handle a HEAD request. Don't retry on timeouts.
89                         if (!$curlResult->isSuccess() && ($curlResult->getReturnCode() >= 400) && !in_array($curlResult->getReturnCode(), [408, 504])) {
90                                 try {
91                                         $curlResult = DI::httpClient()->get($url, HttpClientAccept::DEFAULT, $options);
92                                 } catch (\Exception $e) {
93                                         return false;
94                                 }
95                         }
96
97                         if (!$curlResult->isSuccess()) {
98                                 Logger::notice('Url not reachable', ['host' => $host, 'url' => $url]);
99                                 return false;
100                         } elseif ($curlResult->isRedirectUrl()) {
101                                 $url = $curlResult->getRedirectUrl();
102                         }
103                 }
104
105                 return $url;
106         }
107
108         /**
109          * Checks that email is an actual resolvable internet address
110          *
111          * @param string $addr The email address
112          * @return boolean True if it's a valid email address, false if it's not
113          */
114         public static function isEmailDomainValid(string $addr): bool
115         {
116                 if (DI::config()->get('system', 'disable_email_validation')) {
117                         return true;
118                 }
119
120                 if (! strpos($addr, '@')) {
121                         return false;
122                 }
123
124                 $h = substr($addr, strpos($addr, '@') + 1);
125
126                 // Concerning the @ see here: https://stackoverflow.com/questions/36280957/dns-get-record-a-temporary-server-error-occurred
127                 if ($h && (@dns_get_record($h, DNS_A + DNS_AAAA + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP))) {
128                         return true;
129                 }
130                 if ($h && @dns_get_record($h, DNS_CNAME + DNS_MX)) {
131                         return true;
132                 }
133                 return false;
134         }
135
136         /**
137          * Check if URL is allowed
138          *
139          * Check $url against our list of allowed sites,
140          * wildcards allowed. If allowed_sites is unset return true;
141          *
142          * @param string $url URL which get tested
143          * @return boolean True if url is allowed otherwise return false
144          */
145         public static function isUrlAllowed(string $url): bool
146         {
147                 $h = @parse_url($url);
148
149                 if (! $h) {
150                         return false;
151                 }
152
153                 $str_allowed = DI::config()->get('system', 'allowed_sites');
154                 if (! $str_allowed) {
155                         return true;
156                 }
157
158                 $found = false;
159
160                 $host = strtolower($h['host']);
161
162                 // always allow our own site
163                 if ($host == strtolower($_SERVER['SERVER_NAME'])) {
164                         return true;
165                 }
166
167                 $fnmatch = function_exists('fnmatch');
168                 $allowed = explode(',', $str_allowed);
169
170                 if (count($allowed)) {
171                         foreach ($allowed as $a) {
172                                 $pat = strtolower(trim($a));
173                                 if (($fnmatch && fnmatch($pat, $host)) || ($pat == $host)) {
174                                         $found = true;
175                                         break;
176                                 }
177                         }
178                 }
179                 return $found;
180         }
181
182         /**
183          * Checks if the provided url domain is on the domain blocklist.
184          * Returns true if it is or malformed URL, false if not.
185          *
186          * @param string $url The url to check the domain from
187          *
188          * @return boolean
189          *
190          * @deprecated since 2023.03 Use isUriBlocked instead
191          */
192         public static function isUrlBlocked(string $url): bool
193         {
194                 try {
195                         return self::isUriBlocked(new Uri($url));
196                 } catch (\Throwable $e) {
197                         Logger::warning('Invalid URL', ['url' => $url]);
198                         return false;
199                 }
200         }
201
202         /**
203          * Checks if the provided URI domain is on the domain blocklist.
204          *
205          * @param UriInterface $uri
206          * @return boolean
207          */
208         public static function isUriBlocked(UriInterface $uri): bool
209         {
210                 if (!$uri->getHost()) {
211                         return false;
212                 }
213
214                 $domain_blocklist = DI::config()->get('system', 'blocklist', []);
215                 if (!$domain_blocklist) {
216                         return false;
217                 }
218
219                 foreach ($domain_blocklist as $domain_block) {
220                         if (fnmatch(strtolower($domain_block['domain']), strtolower($uri->getHost()))) {
221                                 return true;
222                         }
223                 }
224
225                 return false;
226         }
227
228         /**
229          * Checks if the provided url is on the list of domains where redirects are blocked.
230          * Returns true if it is or malformed URL, false if not.
231          *
232          * @param string $url The url to check the domain from
233          *
234          * @return boolean
235          */
236         public static function isRedirectBlocked(string $url): bool
237         {
238                 $host = @parse_url($url, PHP_URL_HOST);
239                 if (!$host) {
240                         return false;
241                 }
242
243                 $no_redirect_list = DI::config()->get('system', 'no_redirect_list', []);
244                 if (!$no_redirect_list) {
245                         return false;
246                 }
247
248                 foreach ($no_redirect_list as $no_redirect) {
249                         if (fnmatch(strtolower($no_redirect), strtolower($host))) {
250                                 return true;
251                         }
252                 }
253
254                 return false;
255         }
256
257         /**
258          * Check if email address is allowed to register here.
259          *
260          * Compare against our list (wildcards allowed).
261          *
262          * @param  string $email email address
263          * @return boolean False if not allowed, true if allowed
264          *                       or if allowed list is not configured
265          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
266          */
267         public static function isEmailDomainAllowed(string $email): bool
268         {
269                 $domain = strtolower(substr($email, strpos($email, '@') + 1));
270                 if (!$domain) {
271                         return false;
272                 }
273
274                 $str_allowed = DI::config()->get('system', 'allowed_email', '');
275                 if (empty($str_allowed)) {
276                         return true;
277                 }
278
279                 $allowed = explode(',', $str_allowed);
280
281                 return self::isDomainAllowed($domain, $allowed);
282         }
283
284         /**
285          * Checks for the existence of a domain in a domain list
286          *
287          * @param string $domain
288          * @param array  $domain_list
289          *
290          * @return boolean
291          */
292         public static function isDomainAllowed(string $domain, array $domain_list): bool
293         {
294                 $found = false;
295
296                 foreach ($domain_list as $item) {
297                         $pat = strtolower(trim($item));
298                         if (fnmatch($pat, $domain) || ($pat == $domain)) {
299                                 $found = true;
300                                 break;
301                         }
302                 }
303
304                 return $found;
305         }
306
307         public static function lookupAvatarByEmail(string $email): string
308         {
309                 $avatar['size'] = 300;
310                 $avatar['email'] = $email;
311                 $avatar['url'] = '';
312                 $avatar['success'] = false;
313
314                 Hook::callAll('avatar_lookup', $avatar);
315
316                 if (! $avatar['success']) {
317                         $avatar['url'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO;
318                 }
319
320                 Logger::info('Avatar: ' . $avatar['email'] . ' ' . $avatar['url']);
321                 return $avatar['url'];
322         }
323
324         /**
325          * Remove Google Analytics and other tracking platforms params from URL
326          *
327          * @param string $url Any user-submitted URL that may contain tracking params
328          *
329          * @return string The same URL stripped of tracking parameters
330          */
331         public static function stripTrackingQueryParams(string $url): string
332         {
333                 $urldata = parse_url($url);
334
335                 if (!empty($urldata['query'])) {
336                         $query = $urldata['query'];
337                         parse_str($query, $querydata);
338
339                         if (is_array($querydata)) {
340                                 foreach ($querydata as $param => $value) {
341                                         if (in_array(
342                                                 $param,
343                                                 [
344                                                         'utm_source', 'utm_medium', 'utm_term', 'utm_content', 'utm_campaign',
345                                                         // As seen from Purism
346                                                         'mtm_source', 'mtm_medium', 'mtm_term', 'mtm_content', 'mtm_campaign',
347                                                         'wt_mc', 'pk_campaign', 'pk_kwd', 'mc_cid', 'mc_eid',
348                                                         'fb_action_ids', 'fb_action_types', 'fb_ref',
349                                                         'awesm', 'wtrid',
350                                                         'woo_campaign', 'woo_source', 'woo_medium', 'woo_content', 'woo_term']
351                                                 )
352                                         ) {
353                                                 $pair = $param . '=' . urlencode($value);
354                                                 $url = str_replace($pair, '', $url);
355
356                                                 // Second try: if the url isn't encoded completely
357                                                 $pair = $param . '=' . str_replace(' ', '+', $value);
358                                                 $url = str_replace($pair, '', $url);
359
360                                                 // Third try: Maybe the url isn't encoded at all
361                                                 $pair = $param . '=' . $value;
362                                                 $url = str_replace($pair, '', $url);
363
364                                                 $url = str_replace(['?&', '&&'], ['?', ''], $url);
365                                         }
366                                 }
367                         }
368
369                         if (substr($url, -1, 1) == '?') {
370                                 $url = substr($url, 0, -1);
371                         }
372                 }
373
374                 return $url;
375         }
376
377         /**
378          * Add a missing base path (scheme and host) to a given url
379          *
380          * @param string $url
381          * @param string $basepath
382          *
383          * @return string url
384          */
385         public static function addBasePath(string $url, string $basepath): string
386         {
387                 $url = trim($url);
388                 if (!empty(parse_url($url, PHP_URL_SCHEME)) || empty(parse_url($basepath, PHP_URL_SCHEME)) || empty($url) || empty(parse_url($url))) {
389                         return $url;
390                 }
391
392                 $base = [
393                         'scheme' => parse_url($basepath, PHP_URL_SCHEME),
394                         'host' => parse_url($basepath, PHP_URL_HOST),
395                 ];
396
397                 $parts = array_merge($base, parse_url('/' . ltrim($url, '/')));
398                 return self::unparseURL($parts);
399         }
400
401         /**
402          * Find the matching part between two url
403          *
404          * @param string $url1
405          * @param string $url2
406          *
407          * @return string The matching part or empty string on error
408          */
409         public static function getUrlMatch(string $url1, string $url2): string
410         {
411                 if (($url1 == '') || ($url2 == '')) {
412                         return '';
413                 }
414
415                 $url1 = Strings::normaliseLink($url1);
416                 $url2 = Strings::normaliseLink($url2);
417
418                 $parts1 = parse_url($url1);
419                 $parts2 = parse_url($url2);
420
421                 if (!isset($parts1['host']) || !isset($parts2['host'])) {
422                         return '';
423                 }
424
425                 if (empty($parts1['scheme'])) {
426                         $parts1['scheme'] = '';
427                 }
428                 if (empty($parts2['scheme'])) {
429                         $parts2['scheme'] = '';
430                 }
431
432                 if ($parts1['scheme'] != $parts2['scheme']) {
433                         return '';
434                 }
435
436                 if (empty($parts1['host'])) {
437                         $parts1['host'] = '';
438                 }
439                 if (empty($parts2['host'])) {
440                         $parts2['host'] = '';
441                 }
442
443                 if ($parts1['host'] != $parts2['host']) {
444                         return '';
445                 }
446
447                 if (empty($parts1['port'])) {
448                         $parts1['port'] = '';
449                 }
450                 if (empty($parts2['port'])) {
451                         $parts2['port'] = '';
452                 }
453
454                 if ($parts1['port'] != $parts2['port']) {
455                         return '';
456                 }
457
458                 $match = $parts1['scheme'] . '://' . $parts1['host'];
459
460                 if ($parts1['port']) {
461                         $match .= ':' . $parts1['port'];
462                 }
463
464                 if (empty($parts1['path'])) {
465                         $parts1['path'] = '';
466                 }
467                 if (empty($parts2['path'])) {
468                         $parts2['path'] = '';
469                 }
470
471                 $pathparts1 = explode('/', $parts1['path']);
472                 $pathparts2 = explode('/', $parts2['path']);
473
474                 $i = 0;
475                 $path = '';
476                 do {
477                         $path1 = $pathparts1[$i] ?? '';
478                         $path2 = $pathparts2[$i] ?? '';
479
480                         if ($path1 == $path2) {
481                                 $path .= $path1 . '/';
482                         }
483                 } while (($path1 == $path2) && ($i++ <= count($pathparts1)));
484
485                 $match .= $path;
486
487                 return Strings::normaliseLink($match);
488         }
489
490         /**
491          * Glue url parts together
492          *
493          * @param array $parsed URL parts
494          *
495          * @return string|null The glued URL or null on error
496          * @deprecated since version 2021.12, use GuzzleHttp\Psr7\Uri::fromParts($parts) instead
497          */
498         public static function unparseURL(array $parsed): string
499         {
500                 $get = function ($key) use ($parsed) {
501                         return isset($parsed[$key]) ? $parsed[$key] : null;
502                 };
503
504                 $pass      = $get('pass');
505                 $user      = $get('user');
506                 $userinfo  = $pass !== null ? "$user:$pass" : $user;
507                 $port      = $get('port');
508                 $scheme    = $get('scheme');
509                 $query     = $get('query');
510                 $fragment  = $get('fragment');
511                 $authority = ($userinfo !== null ? $userinfo . '@' : '') .
512                                                 $get('host') .
513                                                 ($port ? ":$port" : '');
514
515                 return  (!empty($scheme) ? $scheme . ':' : '') .
516                         (!empty($authority) ? '//' . $authority : '') .
517                         $get('path') .
518                         (!empty($query) ? '?' . $query : '') .
519                         (!empty($fragment) ? '#' . $fragment : '');
520         }
521
522         /**
523          * Convert an URI to an IDN compatible URI
524          *
525          * @param string $uri
526          *
527          * @return string
528          */
529         public static function convertToIdn(string $uri): string
530         {
531                 $parts = parse_url($uri);
532                 if (!empty($parts['scheme']) && !empty($parts['host'])) {
533                         $parts['host'] = idn_to_ascii($parts['host']);
534                         $uri = (string)Uri::fromParts($parts);
535                 } else {
536                         $parts = explode('@', $uri);
537                         if (count($parts) == 2) {
538                                 $uri = $parts[0] . '@' . idn_to_ascii($parts[1]);
539                         } else {
540                                 $uri = idn_to_ascii($uri);
541                         }
542                 }
543
544                 return $uri;
545         }
546
547         /**
548          * Switch the scheme of an url between http and https
549          *
550          * @param string $url
551          *
552          * @return string Switched URL
553          */
554         public static function switchScheme(string $url): string
555         {
556                 $scheme = parse_url($url, PHP_URL_SCHEME);
557                 if (empty($scheme)) {
558                         return $url;
559                 }
560
561                 if ($scheme === 'http') {
562                         $url = str_replace('http://', 'https://', $url);
563                 } elseif ($scheme === 'https') {
564                         $url = str_replace('https://', 'http://', $url);
565                 }
566
567                 return $url;
568         }
569
570         /**
571          * Adds query string parameters to the provided URI. Replace the value of existing keys.
572          *
573          * @param string $path
574          * @param array  $additionalParams Associative array of parameters
575          *
576          * @return string
577          */
578         public static function appendQueryParam(string $path, array $additionalParams): string
579         {
580                 $parsed = parse_url($path);
581
582                 $params = [];
583                 if (!empty($parsed['query'])) {
584                         parse_str($parsed['query'], $params);
585                 }
586
587                 $params = array_merge($params, $additionalParams);
588
589                 $parsed['query'] = http_build_query($params);
590
591                 return self::unparseURL($parsed);
592         }
593
594         /**
595          * Generates ETag and Last-Modified response headers and checks them against
596          * If-None-Match and If-Modified-Since request headers if present.
597          *
598          * Blocking function, sends 304 headers and exits if check passes.
599          *
600          * @param string $etag          The page etag
601          * @param string $last_modified The page last modification UTC date
602          *
603          * @return void
604          * @throws \Exception
605          */
606         public static function checkEtagModified(string $etag, string $last_modified)
607         {
608                 $last_modified = DateTimeFormat::utc($last_modified, 'D, d M Y H:i:s') . ' GMT';
609
610                 /**
611                  * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
612                  */
613                 $if_none_match     = filter_input(INPUT_SERVER, 'HTTP_IF_NONE_MATCH');
614                 $if_modified_since = filter_input(INPUT_SERVER, 'HTTP_IF_MODIFIED_SINCE');
615                 $flag_not_modified = null;
616                 if ($if_none_match) {
617                         $result = [];
618                         preg_match('/^(?:W\/")?([^"]+)"?$/i', $etag, $result);
619                         $etagTrimmed = $result[1];
620                         // Lazy exact ETag match, could check weak/strong ETags
621                         $flag_not_modified = $if_none_match == '*' || strpos($if_none_match, $etagTrimmed) !== false;
622                 }
623
624                 if ($if_modified_since && (!$if_none_match || $flag_not_modified)) {
625                         // Lazy exact Last-Modified match, could check If-Modified-Since validity
626                         $flag_not_modified = $if_modified_since == $last_modified;
627                 }
628
629                 header('Etag: ' . $etag);
630                 header('Last-Modified: ' . $last_modified);
631
632                 if ($flag_not_modified) {
633                         throw new NotModifiedException();
634                 }
635         }
636
637         /**
638          * Check if the given URL is a local link
639          *
640          * @param string $url
641          *
642          * @return bool
643          */
644         public static function isLocalLink(string $url): bool
645         {
646                 return (strpos(Strings::normaliseLink($url), Strings::normaliseLink(DI::baseUrl())) !== false);
647         }
648
649         /**
650          * Check if the given URL is a valid HTTP/HTTPS URL
651          *
652          * @param string $url
653          * @return bool
654          */
655         public static function isValidHttpUrl(string $url): bool
656         {
657                 $scheme = parse_url($url, PHP_URL_SCHEME);
658                 return !empty($scheme) && in_array($scheme, ['http', 'https']) && parse_url($url, PHP_URL_HOST);
659         }
660 }