]> git.mxchange.org Git - friendica.git/blob - src/Util/Network.php
9cd3cb71977b719ffb1fb4b75a974e9d6a3abbbc
[friendica.git] / src / Util / Network.php
1 <?php
2 /**
3  * @file src/Util/Network.php
4  */
5 namespace Friendica\Util;
6
7 use Friendica\App;
8 use Friendica\Core\Addon;
9 use Friendica\Core\L10n;
10 use Friendica\Core\System;
11 use Friendica\Core\Config;
12 use Friendica\Network\Probe;
13 use Friendica\Object\Image;
14 use Friendica\Util\Network;
15 use Friendica\Util\XML;
16
17 require_once 'library/slinky.php';
18
19 class Network
20 {
21         /**
22          * @brief Curl wrapper
23          *
24          * If binary flag is true, return binary results.
25          * Set the cookiejar argument to a string (e.g. "/tmp/friendica-cookies.txt")
26          * to preserve cookies from one request to the next.
27          *
28          * @param string  $url            URL to fetch
29          * @param boolean $binary         default false
30          *                                TRUE if asked to return binary results (file download)
31          * @param integer $redirects      The recursion counter for internal use - default 0
32          * @param integer $timeout        Timeout in seconds, default system config value or 60 seconds
33          * @param string  $accept_content supply Accept: header with 'accept_content' as the value
34          * @param string  $cookiejar      Path to cookie jar file
35          *
36          * @return string The fetched content
37          */
38         public static function fetchURL($url, $binary = false, &$redirects = 0, $timeout = 0, $accept_content = null, $cookiejar = 0)
39         {
40                 $ret = self::zFetchURL(
41                         $url,
42                         $binary,
43                         $redirects,
44                         ['timeout'=>$timeout,
45                         'accept_content'=>$accept_content,
46                         'cookiejar'=>$cookiejar
47                         ]
48                 );
49
50                 return($ret['body']);
51         }
52
53         /**
54          * @brief fetches an URL.
55          *
56          * @param string  $url       URL to fetch
57          * @param boolean $binary    default false
58          *                           TRUE if asked to return binary results (file download)
59          * @param int     $redirects The recursion counter for internal use - default 0
60          * @param array   $opts      (optional parameters) assoziative array with:
61          *                           'accept_content' => supply Accept: header with 'accept_content' as the value
62          *                           'timeout' => int Timeout in seconds, default system config value or 60 seconds
63          *                           'http_auth' => username:password
64          *                           'novalidate' => do not validate SSL certs, default is to validate using our CA list
65          *                           'nobody' => only return the header
66          *                           'cookiejar' => path to cookie jar file
67          *
68          * @return array an assoziative array with:
69          *    int 'return_code' => HTTP return code or 0 if timeout or failure
70          *    boolean 'success' => boolean true (if HTTP 2xx result) or false
71          *    string 'redirect_url' => in case of redirect, content was finally retrieved from this URL
72          *    string 'header' => HTTP headers
73          *    string 'body' => fetched content
74          */
75         public static function zFetchURL($url, $binary = false, &$redirects = 0, $opts = [])
76         {
77                 $ret = ['return_code' => 0, 'success' => false, 'header' => '', 'info' => '', 'body' => ''];
78
79                 $stamp1 = microtime(true);
80
81                 $a = get_app();
82
83                 if (self::blockedURL($url)) {
84                         logger('z_fetch_url: domain of ' . $url . ' is blocked', LOGGER_DATA);
85                         return $ret;
86                 }
87
88                 $ch = @curl_init($url);
89
90                 if (($redirects > 8) || (!$ch)) {
91                         return $ret;
92                 }
93
94                 @curl_setopt($ch, CURLOPT_HEADER, true);
95
96                 if (x($opts, "cookiejar")) {
97                         curl_setopt($ch, CURLOPT_COOKIEJAR, $opts["cookiejar"]);
98                         curl_setopt($ch, CURLOPT_COOKIEFILE, $opts["cookiejar"]);
99                 }
100
101                 // These settings aren't needed. We're following the location already.
102                 //      @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
103                 //      @curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
104
105                 if (x($opts, 'accept_content')) {
106                         curl_setopt(
107                                 $ch,
108                                 CURLOPT_HTTPHEADER,
109                                 ['Accept: ' . $opts['accept_content']]
110                         );
111                 }
112
113                 @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
114                 @curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
115
116                 $range = intval(Config::get('system', 'curl_range_bytes', 0));
117
118                 if ($range > 0) {
119                         @curl_setopt($ch, CURLOPT_RANGE, '0-' . $range);
120                 }
121
122                 // Without this setting it seems as if some webservers send compressed content
123                 // This seems to confuse curl so that it shows this uncompressed.
124                 /// @todo  We could possibly set this value to "gzip" or something similar
125                 curl_setopt($ch, CURLOPT_ENCODING, '');
126
127                 if (x($opts, 'headers')) {
128                         @curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['headers']);
129                 }
130
131                 if (x($opts, 'nobody')) {
132                         @curl_setopt($ch, CURLOPT_NOBODY, $opts['nobody']);
133                 }
134
135                 if (x($opts, 'timeout')) {
136                         @curl_setopt($ch, CURLOPT_TIMEOUT, $opts['timeout']);
137                 } else {
138                         $curl_time = Config::get('system', 'curl_timeout', 60);
139                         @curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time));
140                 }
141
142                 // by default we will allow self-signed certs
143                 // but you can override this
144
145                 $check_cert = Config::get('system', 'verifyssl');
146                 @curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
147
148                 if ($check_cert) {
149                         @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
150                 }
151
152                 $proxy = Config::get('system', 'proxy');
153
154                 if (strlen($proxy)) {
155                         @curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
156                         @curl_setopt($ch, CURLOPT_PROXY, $proxy);
157                         $proxyuser = @Config::get('system', 'proxyuser');
158
159                         if (strlen($proxyuser)) {
160                                 @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser);
161                         }
162                 }
163
164                 if (Config::get('system', 'ipv4_resolve', false)) {
165                         curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
166                 }
167
168                 if ($binary) {
169                         @curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
170                 }
171
172                 $a->set_curl_code(0);
173
174                 // don't let curl abort the entire application
175                 // if it throws any errors.
176
177                 $s = @curl_exec($ch);
178                 $curl_info = @curl_getinfo($ch);
179
180                 // Special treatment for HTTP Code 416
181                 // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416
182                 if (($curl_info['http_code'] == 416) && ($range > 0)) {
183                         @curl_setopt($ch, CURLOPT_RANGE, '');
184                         $s = @curl_exec($ch);
185                         $curl_info = @curl_getinfo($ch);
186                 }
187
188                 if (curl_errno($ch) !== CURLE_OK) {
189                         logger('fetch_url error fetching ' . $url . ': ' . curl_error($ch), LOGGER_NORMAL);
190                 }
191
192                 $ret['errno'] = curl_errno($ch);
193
194                 $base = $s;
195                 $ret['info'] = $curl_info;
196
197                 $http_code = $curl_info['http_code'];
198
199                 logger('fetch_url ' . $url . ': ' . $http_code . " " . $s, LOGGER_DATA);
200                 $header = '';
201
202                 // Pull out multiple headers, e.g. proxy and continuation headers
203                 // allow for HTTP/2.x without fixing code
204
205                 while (preg_match('/^HTTP\/[1-2].+? [1-5][0-9][0-9]/', $base)) {
206                         $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4);
207                         $header .= $chunk;
208                         $base = substr($base, strlen($chunk));
209                 }
210
211                 $a->set_curl_code($http_code);
212                 $a->set_curl_content_type($curl_info['content_type']);
213                 $a->set_curl_headers($header);
214
215                 if ($http_code == 301 || $http_code == 302 || $http_code == 303 || $http_code == 307) {
216                         $new_location_info = @parse_url($curl_info['redirect_url']);
217                         $old_location_info = @parse_url($curl_info['url']);
218
219                         $newurl = $curl_info['redirect_url'];
220
221                         if (($new_location_info['path'] == '') && ( $new_location_info['host'] != '')) {
222                                 $newurl = $new_location_info['scheme'] . '://' . $new_location_info['host'] . $old_location_info['path'];
223                         }
224
225                         $matches = [];
226
227                         if (preg_match('/(Location:|URI:)(.*?)\n/i', $header, $matches)) {
228                                 $newurl = trim(array_pop($matches));
229                         }
230                         if (strpos($newurl, '/') === 0) {
231                                 $newurl = $old_location_info["scheme"]."://".$old_location_info["host"].$newurl;
232                         }
233
234                         if (filter_var($newurl, FILTER_VALIDATE_URL)) {
235                                 $redirects++;
236                                 @curl_close($ch);
237                                 return self::zFetchURL($newurl, $binary, $redirects, $opts);
238                         }
239                 }
240
241                 $a->set_curl_code($http_code);
242                 $a->set_curl_content_type($curl_info['content_type']);
243
244                 $rc = intval($http_code);
245                 $ret['return_code'] = $rc;
246                 $ret['success'] = (($rc >= 200 && $rc <= 299) ? true : false);
247                 $ret['redirect_url'] = $url;
248
249                 if (!$ret['success']) {
250                         $ret['error'] = curl_error($ch);
251                         $ret['debug'] = $curl_info;
252                         logger('z_fetch_url: error: '.$url.': '.$ret['return_code'].' - '.$ret['error'], LOGGER_DEBUG);
253                         logger('z_fetch_url: debug: '.print_r($curl_info, true), LOGGER_DATA);
254                 }
255
256                 $ret['body'] = substr($s, strlen($header));
257                 $ret['header'] = $header;
258
259                 if (x($opts, 'debug')) {
260                         $ret['debug'] = $curl_info;
261                 }
262
263                 @curl_close($ch);
264
265                 $a->save_timestamp($stamp1, 'network');
266
267                 return($ret);
268         }
269
270         /**
271          * @brief Send POST request to $url
272          *
273          * @param string  $url       URL to post
274          * @param mixed   $params    array of POST variables
275          * @param string  $headers   HTTP headers
276          * @param integer $redirects Recursion counter for internal use - default = 0
277          * @param integer $timeout   The timeout in seconds, default system config value or 60 seconds
278          *
279          * @return string The content
280          */
281         public static function postURL($url, $params, $headers = null, &$redirects = 0, $timeout = 0)
282         {
283                 $stamp1 = microtime(true);
284
285                 if (self::blockedURL($url)) {
286                         logger('post_url: domain of ' . $url . ' is blocked', LOGGER_DATA);
287                         return false;
288                 }
289
290                 $a = get_app();
291                 $ch = curl_init($url);
292
293                 if (($redirects > 8) || (!$ch)) {
294                         return false;
295                 }
296
297                 logger('post_url: start ' . $url, LOGGER_DATA);
298
299                 curl_setopt($ch, CURLOPT_HEADER, true);
300                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
301                 curl_setopt($ch, CURLOPT_POST, 1);
302                 curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
303                 curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
304
305                 if (Config::get('system', 'ipv4_resolve', false)) {
306                         curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
307                 }
308
309                 if (intval($timeout)) {
310                         curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
311                 } else {
312                         $curl_time = Config::get('system', 'curl_timeout', 60);
313                         curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time));
314                 }
315
316                 if (defined('LIGHTTPD')) {
317                         if (!is_array($headers)) {
318                                 $headers = ['Expect:'];
319                         } else {
320                                 if (!in_array('Expect:', $headers)) {
321                                         array_push($headers, 'Expect:');
322                                 }
323                         }
324                 }
325
326                 if ($headers) {
327                         curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
328                 }
329
330                 $check_cert = Config::get('system', 'verifyssl');
331                 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
332
333                 if ($check_cert) {
334                         @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
335                 }
336
337                 $proxy = Config::get('system', 'proxy');
338
339                 if (strlen($proxy)) {
340                         curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
341                         curl_setopt($ch, CURLOPT_PROXY, $proxy);
342                         $proxyuser = Config::get('system', 'proxyuser');
343                         if (strlen($proxyuser)) {
344                                 curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser);
345                         }
346                 }
347
348                 $a->set_curl_code(0);
349
350                 // don't let curl abort the entire application
351                 // if it throws any errors.
352
353                 $s = @curl_exec($ch);
354
355                 $base = $s;
356                 $curl_info = curl_getinfo($ch);
357                 $http_code = $curl_info['http_code'];
358
359                 logger('post_url: result ' . $http_code . ' - ' . $url, LOGGER_DATA);
360
361                 $header = '';
362
363                 // Pull out multiple headers, e.g. proxy and continuation headers
364                 // allow for HTTP/2.x without fixing code
365
366                 while (preg_match('/^HTTP\/[1-2].+? [1-5][0-9][0-9]/', $base)) {
367                         $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4);
368                         $header .= $chunk;
369                         $base = substr($base, strlen($chunk));
370                 }
371
372                 if ($http_code == 301 || $http_code == 302 || $http_code == 303 || $http_code == 307) {
373                         $matches = [];
374                         preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches);
375                         $newurl = trim(array_pop($matches));
376
377                         if (strpos($newurl, '/') === 0) {
378                                 $newurl = $old_location_info["scheme"] . "://" . $old_location_info["host"] . $newurl;
379                         }
380
381                         if (filter_var($newurl, FILTER_VALIDATE_URL)) {
382                                 $redirects++;
383                                 logger('post_url: redirect ' . $url . ' to ' . $newurl);
384                                 return self::postURL($newurl, $params, $headers, $redirects, $timeout);
385                         }
386                 }
387
388                 $a->set_curl_code($http_code);
389
390                 $body = substr($s, strlen($header));
391
392                 $a->set_curl_headers($header);
393
394                 curl_close($ch);
395
396                 $a->save_timestamp($stamp1, 'network');
397
398                 logger('post_url: end ' . $url, LOGGER_DATA);
399
400                 return $body;
401         }
402
403         /**
404          * Generic XML return
405          * Outputs a basic dfrn XML status structure to STDOUT, with a <status> variable
406          * of $st and an optional text <message> of $message and terminates the current process.
407          */
408         public static function xmlStatus($st, $message = '')
409         {
410                 $result = ['status' => $st];
411
412                 if ($message != '') {
413                         $result['message'] = $message;
414                 }
415
416                 if ($st) {
417                         logger('xml_status returning non_zero: ' . $st . " message=" . $message);
418                 }
419
420                 header("Content-type: text/xml");
421
422                 $xmldata = ["result" => $result];
423
424                 echo XML::fromArray($xmldata, $xml);
425
426                 killme();
427         }
428
429         /**
430          * @brief Send HTTP status header and exit.
431          *
432          * @param integer $val         HTTP status result value
433          * @param array   $description optional message
434          *                             'title' => header title
435          *                             'description' => optional message
436          */
437         public static function httpStatusExit($val, $description = [])
438         {
439                 $err = '';
440                 if ($val >= 400) {
441                         $err = 'Error';
442                         if (!isset($description["title"])) {
443                                 $description["title"] = $err." ".$val;
444                         }
445                 }
446
447                 if ($val >= 200 && $val < 300) {
448                         $err = 'OK';
449                 }
450
451                 logger('http_status_exit ' . $val);
452                 header($_SERVER["SERVER_PROTOCOL"] . ' ' . $val . ' ' . $err);
453
454                 if (isset($description["title"])) {
455                         $tpl = get_markup_template('http_status.tpl');
456                         echo replace_macros(
457                                 $tpl,
458                                 [
459                                         '$title' => $description["title"],
460                                         '$description' => $description["description"]]
461                         );
462                 }
463
464                 killme();
465         }
466
467         /**
468          * @brief Check URL to se if ts's real
469          *
470          * Take a URL from the wild, prepend http:// if necessary
471          * and check DNS to see if it's real (or check if is a valid IP address)
472          *
473          * @param string $url The URL to be validated
474          * @return string|boolean The actual working URL, false else
475          */
476         public static function validateURL($url)
477         {
478                 if (Config::get('system', 'disable_url_validation')) {
479                         return $url;
480                 }
481
482                 // no naked subdomains (allow localhost for tests)
483                 if (strpos($url, '.') === false && strpos($url, '/localhost/') === false) {
484                         return false;
485                 }
486
487                 if (substr($url, 0, 4) != 'http') {
488                         $url = 'http://' . $url;
489                 }
490
491                 /// @TODO Really suppress function outcomes? Why not find them + debug them?
492                 $h = @parse_url($url);
493
494                 if ((is_array($h)) && (@dns_get_record($h['host'], DNS_A + DNS_CNAME + DNS_PTR) || filter_var($h['host'], FILTER_VALIDATE_IP) )) {
495                         return $url;
496                 }
497
498                 return false;
499         }
500
501         /**
502          * @brief Checks that email is an actual resolvable internet address
503          *
504          * @param string $addr The email address
505          * @return boolean True if it's a valid email address, false if it's not
506          */
507         public static function validateEmail($addr)
508         {
509                 if (Config::get('system', 'disable_email_validation')) {
510                         return true;
511                 }
512
513                 if (! strpos($addr, '@')) {
514                         return false;
515                 }
516
517                 $h = substr($addr, strpos($addr, '@') + 1);
518
519                 if (($h) && (dns_get_record($h, DNS_A + DNS_CNAME + DNS_PTR + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP) )) {
520                         return true;
521                 }
522                 return false;
523         }
524
525         /**
526          * @brief Check if URL is allowed
527          *
528          * Check $url against our list of allowed sites,
529          * wildcards allowed. If allowed_sites is unset return true;
530          *
531          * @param string $url URL which get tested
532          * @return boolean True if url is allowed otherwise return false
533          */
534         public static function allowedURL($url)
535         {
536                 $h = @parse_url($url);
537
538                 if (! $h) {
539                         return false;
540                 }
541
542                 $str_allowed = Config::get('system', 'allowed_sites');
543                 if (! $str_allowed) {
544                         return true;
545                 }
546
547                 $found = false;
548
549                 $host = strtolower($h['host']);
550
551                 // always allow our own site
552                 if ($host == strtolower($_SERVER['SERVER_NAME'])) {
553                         return true;
554                 }
555
556                 $fnmatch = function_exists('fnmatch');
557                 $allowed = explode(',', $str_allowed);
558
559                 if (count($allowed)) {
560                         foreach ($allowed as $a) {
561                                 $pat = strtolower(trim($a));
562                                 if (($fnmatch && fnmatch($pat, $host)) || ($pat == $host)) {
563                                         $found = true;
564                                         break;
565                                 }
566                         }
567                 }
568                 return $found;
569         }
570
571         /**
572          * Checks if the provided url domain is on the domain blocklist.
573          * Returns true if it is or malformed URL, false if not.
574          *
575          * @param string $url The url to check the domain from
576          *
577          * @return boolean
578          */
579         public static function blockedURL($url)
580         {
581                 $h = @parse_url($url);
582
583                 if (! $h) {
584                         return true;
585                 }
586
587                 $domain_blocklist = Config::get('system', 'blocklist', []);
588                 if (! $domain_blocklist) {
589                         return false;
590                 }
591
592                 $host = strtolower($h['host']);
593
594                 foreach ($domain_blocklist as $domain_block) {
595                         if (strtolower($domain_block['domain']) == $host) {
596                                 return true;
597                         }
598                 }
599
600                 return false;
601         }
602
603         /**
604          * @brief Check if email address is allowed to register here.
605          *
606          * Compare against our list (wildcards allowed).
607          *
608          * @param  string $email email address
609          * @return boolean False if not allowed, true if allowed
610          *    or if allowed list is not configured
611          */
612         public static function allowedEmail($email)
613         {
614                 $domain = strtolower(substr($email, strpos($email, '@') + 1));
615                 if (!$domain) {
616                         return false;
617                 }
618
619                 $str_allowed = Config::get('system', 'allowed_email', '');
620                 if (!x($str_allowed)) {
621                         return true;
622                 }
623
624                 $allowed = explode(',', $str_allowed);
625
626                 return self::allowedDomain($domain, $allowed);
627         }
628
629         /**
630          * Checks for the existence of a domain in a domain list
631          *
632          * @brief Checks for the existence of a domain in a domain list
633          * @param string $domain
634          * @param array  $domain_list
635          * @return boolean
636          */
637         public static function allowedDomain($domain, array $domain_list)
638         {
639                 $found = false;
640
641                 foreach ($domain_list as $item) {
642                         $pat = strtolower(trim($item));
643                         if (fnmatch($pat, $domain) || ($pat == $domain)) {
644                                 $found = true;
645                                 break;
646                         }
647                 }
648
649                 return $found;
650         }
651
652         public static function avatarImg($email)
653         {
654                 $avatar['size'] = 175;
655                 $avatar['email'] = $email;
656                 $avatar['url'] = '';
657                 $avatar['success'] = false;
658
659                 Addon::callHooks('avatar_lookup', $avatar);
660
661                 if (! $avatar['success']) {
662                         $avatar['url'] = System::baseUrl() . '/images/person-175.jpg';
663                 }
664
665                 logger('Avatar: ' . $avatar['email'] . ' ' . $avatar['url'], LOGGER_DEBUG);
666                 return $avatar['url'];
667         }
668
669         public static function parseXmlString($s, $strict = true)
670         {
671                 // the "strict" parameter is deactivated
672
673                 /// @todo Move this function to the xml class
674                 libxml_use_internal_errors(true);
675
676                 $x = @simplexml_load_string($s);
677                 if (!$x) {
678                         logger('libxml: parse: error: ' . $s, LOGGER_DATA);
679                         foreach (libxml_get_errors() as $err) {
680                                 logger('libxml: parse: ' . $err->code." at ".$err->line.":".$err->column." : ".$err->message, LOGGER_DATA);
681                         }
682                         libxml_clear_errors();
683                 }
684                 return $x;
685         }
686
687         public static function scaleExternalImages($srctext, $include_link = true, $scale_replace = false)
688         {
689                 // Suppress "view full size"
690                 if (intval(Config::get('system', 'no_view_full_size'))) {
691                         $include_link = false;
692                 }
693
694                 // Picture addresses can contain special characters
695                 $s = htmlspecialchars_decode($srctext);
696
697                 $matches = null;
698                 $c = preg_match_all('/\[img.*?\](.*?)\[\/img\]/ism', $s, $matches, PREG_SET_ORDER);
699                 if ($c) {
700                         foreach ($matches as $mtch) {
701                                 logger('scale_external_image: ' . $mtch[1]);
702
703                                 $hostname = str_replace('www.', '', substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3));
704                                 if (stristr($mtch[1], $hostname)) {
705                                         continue;
706                                 }
707
708                                 // $scale_replace, if passed, is an array of two elements. The
709                                 // first is the name of the full-size image. The second is the
710                                 // name of a remote, scaled-down version of the full size image.
711                                 // This allows Friendica to display the smaller remote image if
712                                 // one exists, while still linking to the full-size image
713                                 if ($scale_replace) {
714                                         $scaled = str_replace($scale_replace[0], $scale_replace[1], $mtch[1]);
715                                 } else {
716                                         $scaled = $mtch[1];
717                                 }
718                                 $i = self::fetchURL($scaled);
719                                 if (! $i) {
720                                         return $srctext;
721                                 }
722
723                                 // guess mimetype from headers or filename
724                                 $type = Image::guessType($mtch[1], true);
725
726                                 if ($i) {
727                                         $Image = new Image($i, $type);
728                                         if ($Image->isValid()) {
729                                                 $orig_width = $Image->getWidth();
730                                                 $orig_height = $Image->getHeight();
731
732                                                 if ($orig_width > 640 || $orig_height > 640) {
733                                                         $Image->scaleDown(640);
734                                                         $new_width = $Image->getWidth();
735                                                         $new_height = $Image->getHeight();
736                                                         logger('scale_external_images: ' . $orig_width . '->' . $new_width . 'w ' . $orig_height . '->' . $new_height . 'h' . ' match: ' . $mtch[0], LOGGER_DEBUG);
737                                                         $s = str_replace(
738                                                                 $mtch[0],
739                                                                 '[img=' . $new_width . 'x' . $new_height. ']' . $scaled . '[/img]'
740                                                                 . "\n" . (($include_link)
741                                                                         ? '[url=' . $mtch[1] . ']' . L10n::t('view full size') . '[/url]' . "\n"
742                                                                         : ''),
743                                                                 $s
744                                                         );
745                                                         logger('scale_external_images: new string: ' . $s, LOGGER_DEBUG);
746                                                 }
747                                         }
748                                 }
749                         }
750                 }
751
752                 // replace the special char encoding
753                 $s = htmlspecialchars($s, ENT_NOQUOTES, 'UTF-8');
754                 return $s;
755         }
756
757         public static function fixContactSslPolicy(&$contact, $new_policy)
758         {
759                 $ssl_changed = false;
760                 if ((intval($new_policy) == SSL_POLICY_SELFSIGN || $new_policy === 'self') && strstr($contact['url'], 'https:')) {
761                         $ssl_changed = true;
762                         $contact['url']     =   str_replace('https:', 'http:', $contact['url']);
763                         $contact['request'] =   str_replace('https:', 'http:', $contact['request']);
764                         $contact['notify']  =   str_replace('https:', 'http:', $contact['notify']);
765                         $contact['poll']    =   str_replace('https:', 'http:', $contact['poll']);
766                         $contact['confirm'] =   str_replace('https:', 'http:', $contact['confirm']);
767                         $contact['poco']    =   str_replace('https:', 'http:', $contact['poco']);
768                 }
769
770                 if ((intval($new_policy) == SSL_POLICY_FULL || $new_policy === 'full') && strstr($contact['url'], 'http:')) {
771                         $ssl_changed = true;
772                         $contact['url']     =   str_replace('http:', 'https:', $contact['url']);
773                         $contact['request'] =   str_replace('http:', 'https:', $contact['request']);
774                         $contact['notify']  =   str_replace('http:', 'https:', $contact['notify']);
775                         $contact['poll']    =   str_replace('http:', 'https:', $contact['poll']);
776                         $contact['confirm'] =   str_replace('http:', 'https:', $contact['confirm']);
777                         $contact['poco']    =   str_replace('http:', 'https:', $contact['poco']);
778                 }
779
780                 if ($ssl_changed) {
781                         $fields = ['url' => $contact['url'], 'request' => $contact['request'],
782                                         'notify' => $contact['notify'], 'poll' => $contact['poll'],
783                                         'confirm' => $contact['confirm'], 'poco' => $contact['poco']];
784                         dba::update('contact', $fields, ['id' => $contact['id']]);
785                 }
786         }
787
788         /**
789          * @brief Remove Google Analytics and other tracking platforms params from URL
790          *
791          * @param string $url Any user-submitted URL that may contain tracking params
792          * @return string The same URL stripped of tracking parameters
793          */
794         public static function stripTrackingQueryParams($url)
795         {
796                 $urldata = parse_url($url);
797                 if (is_string($urldata["query"])) {
798                         $query = $urldata["query"];
799                         parse_str($query, $querydata);
800
801                         if (is_array($querydata)) {
802                                 foreach ($querydata as $param => $value) {
803                                         if (in_array(
804                                                 $param,
805                                                 [
806                                                         "utm_source", "utm_medium", "utm_term", "utm_content", "utm_campaign",
807                                                         "wt_mc", "pk_campaign", "pk_kwd", "mc_cid", "mc_eid",
808                                                         "fb_action_ids", "fb_action_types", "fb_ref",
809                                                         "awesm", "wtrid",
810                                                         "woo_campaign", "woo_source", "woo_medium", "woo_content", "woo_term"]
811                                                 )
812                                         ) {
813                                                 $pair = $param . "=" . urlencode($value);
814                                                 $url = str_replace($pair, "", $url);
815
816                                                 // Second try: if the url isn't encoded completely
817                                                 $pair = $param . "=" . str_replace(" ", "+", $value);
818                                                 $url = str_replace($pair, "", $url);
819
820                                                 // Third try: Maybey the url isn't encoded at all
821                                                 $pair = $param . "=" . $value;
822                                                 $url = str_replace($pair, "", $url);
823
824                                                 $url = str_replace(["?&", "&&"], ["?", ""], $url);
825                                         }
826                                 }
827                         }
828
829                         if (substr($url, -1, 1) == "?") {
830                                 $url = substr($url, 0, -1);
831                         }
832                 }
833
834                 return $url;
835         }
836
837         /**
838          * @brief Returns the original URL of the provided URL
839          *
840          * This function strips tracking query params and follows redirections, either
841          * through HTTP code or meta refresh tags. Stops after 10 redirections.
842          *
843          * @todo Remove the $fetchbody parameter that generates an extraneous HEAD request
844          *
845          * @see ParseUrl::getSiteinfo
846          *
847          * @param string $url       A user-submitted URL
848          * @param int    $depth     The current redirection recursion level (internal)
849          * @param bool   $fetchbody Wether to fetch the body or not after the HEAD requests
850          * @return string A canonical URL
851          */
852         public static function originalURL($url, $depth = 1, $fetchbody = false)
853         {
854                 $a = get_app();
855
856                 $url = self::stripTrackingQueryParams($url);
857
858                 if ($depth > 10) {
859                         return($url);
860                 }
861
862                 $url = trim($url, "'");
863
864                 $stamp1 = microtime(true);
865
866                 $ch = curl_init();
867                 curl_setopt($ch, CURLOPT_URL, $url);
868                 curl_setopt($ch, CURLOPT_HEADER, 1);
869                 curl_setopt($ch, CURLOPT_NOBODY, 1);
870                 curl_setopt($ch, CURLOPT_TIMEOUT, 10);
871                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
872                 curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
873
874                 curl_exec($ch);
875                 $curl_info = @curl_getinfo($ch);
876                 $http_code = $curl_info['http_code'];
877                 curl_close($ch);
878
879                 $a->save_timestamp($stamp1, "network");
880
881                 if ($http_code == 0) {
882                         return($url);
883                 }
884
885                 if ((($curl_info['http_code'] == "301") || ($curl_info['http_code'] == "302"))
886                         && (($curl_info['redirect_url'] != "") || ($curl_info['location'] != ""))
887                 ) {
888                         if ($curl_info['redirect_url'] != "") {
889                                 return(Network::originalURL($curl_info['redirect_url'], ++$depth, $fetchbody));
890                         } else {
891                                 return(Network::originalURL($curl_info['location'], ++$depth, $fetchbody));
892                         }
893                 }
894
895                 // Check for redirects in the meta elements of the body if there are no redirects in the header.
896                 if (!$fetchbody) {
897                         return(Network::originalURL($url, ++$depth, true));
898                 }
899
900                 // if the file is too large then exit
901                 if ($curl_info["download_content_length"] > 1000000) {
902                         return($url);
903                 }
904
905                 // if it isn't a HTML file then exit
906                 if (($curl_info["content_type"] != "") && !strstr(strtolower($curl_info["content_type"]), "html")) {
907                         return($url);
908                 }
909
910                 $stamp1 = microtime(true);
911
912                 $ch = curl_init();
913                 curl_setopt($ch, CURLOPT_URL, $url);
914                 curl_setopt($ch, CURLOPT_HEADER, 0);
915                 curl_setopt($ch, CURLOPT_NOBODY, 0);
916                 curl_setopt($ch, CURLOPT_TIMEOUT, 10);
917                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
918                 curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
919
920                 $body = curl_exec($ch);
921                 curl_close($ch);
922
923                 $a->save_timestamp($stamp1, "network");
924
925                 if (trim($body) == "") {
926                         return($url);
927                 }
928
929                 // Check for redirect in meta elements
930                 $doc = new DOMDocument();
931                 @$doc->loadHTML($body);
932
933                 $xpath = new DomXPath($doc);
934
935                 $list = $xpath->query("//meta[@content]");
936                 foreach ($list as $node) {
937                         $attr = [];
938                         if ($node->attributes->length) {
939                                 foreach ($node->attributes as $attribute) {
940                                         $attr[$attribute->name] = $attribute->value;
941                                 }
942                         }
943
944                         if (@$attr["http-equiv"] == 'refresh') {
945                                 $path = $attr["content"];
946                                 $pathinfo = explode(";", $path);
947                                 foreach ($pathinfo as $value) {
948                                         if (substr(strtolower($value), 0, 4) == "url=") {
949                                                 return(Network::originalURL(substr($value, 4), ++$depth));
950                                         }
951                                 }
952                         }
953                 }
954
955                 return $url;
956         }
957
958         public static function shortLink($url)
959         {
960                 $slinky = new Slinky($url);
961                 $yourls_url = Config::get('yourls', 'url1');
962                 if ($yourls_url) {
963                         $yourls_username = Config::get('yourls', 'username1');
964                         $yourls_password = Config::get('yourls', 'password1');
965                         $yourls_ssl = Config::get('yourls', 'ssl1');
966                         $yourls = new Slinky_YourLS();
967                         $yourls->set('username', $yourls_username);
968                         $yourls->set('password', $yourls_password);
969                         $yourls->set('ssl', $yourls_ssl);
970                         $yourls->set('yourls-url', $yourls_url);
971                         $slinky->set_cascade([$yourls, new Slinky_Ur1ca(), new Slinky_TinyURL()]);
972                 } else {
973                         // setup a cascade of shortening services
974                         // try to get a short link from these services
975                         // in the order ur1.ca, tinyurl
976                         $slinky->set_cascade([new Slinky_Ur1ca(), new Slinky_TinyURL()]);
977                 }
978                 return $slinky->short();
979         }
980
981         /**
982          * @brief Encodes content to json
983          *
984          * This function encodes an array to json format
985          * and adds an application/json HTTP header to the output.
986          * After finishing the process is getting killed.
987          *
988          * @param array $x The input content
989          */
990         public static function jsonReturnAndDie($x)
991         {
992                 header("content-type: application/json");
993                 echo json_encode($x);
994                 killme();
995         }
996
997         /**
998          * @brief Find the matching part between two url
999          *
1000          * @param string $url1
1001          * @param string $url2
1002          * @return string The matching part
1003          */
1004         public static function matchingURL($url1, $url2)
1005         {
1006                 if (($url1 == "") || ($url2 == "")) {
1007                         return "";
1008                 }
1009
1010                 $url1 = normalise_link($url1);
1011                 $url2 = normalise_link($url2);
1012
1013                 $parts1 = parse_url($url1);
1014                 $parts2 = parse_url($url2);
1015
1016                 if (!isset($parts1["host"]) || !isset($parts2["host"])) {
1017                         return "";
1018                 }
1019
1020                 if ($parts1["scheme"] != $parts2["scheme"]) {
1021                         return "";
1022                 }
1023
1024                 if ($parts1["host"] != $parts2["host"]) {
1025                         return "";
1026                 }
1027
1028                 if ($parts1["port"] != $parts2["port"]) {
1029                         return "";
1030                 }
1031
1032                 $match = $parts1["scheme"]."://".$parts1["host"];
1033
1034                 if ($parts1["port"]) {
1035                         $match .= ":".$parts1["port"];
1036                 }
1037
1038                 $pathparts1 = explode("/", $parts1["path"]);
1039                 $pathparts2 = explode("/", $parts2["path"]);
1040
1041                 $i = 0;
1042                 $path = "";
1043                 do {
1044                         $path1 = $pathparts1[$i];
1045                         $path2 = $pathparts2[$i];
1046
1047                         if ($path1 == $path2) {
1048                                 $path .= $path1."/";
1049                         }
1050                 } while (($path1 == $path2) && ($i++ <= count($pathparts1)));
1051
1052                 $match .= $path;
1053
1054                 return normalise_link($match);
1055         }
1056
1057         /**
1058          * @brief Glue url parts together
1059          *
1060          * @param array $parsed URL parts
1061          *
1062          * @return string The glued URL
1063          */
1064         public static function unParseURL($parsed)
1065         {
1066                 $get = function ($key) use ($parsed) {
1067                         return isset($parsed[$key]) ? $parsed[$key] : null;
1068                 };
1069
1070                 $pass      = $get('pass');
1071                 $user      = $get('user');
1072                 $userinfo  = $pass !== null ? "$user:$pass" : $user;
1073                 $port      = $get('port');
1074                 $scheme    = $get('scheme');
1075                 $query     = $get('query');
1076                 $fragment  = $get('fragment');
1077                 $authority = ($userinfo !== null ? $userinfo."@" : '') .
1078                                                 $get('host') .
1079                                                 ($port ? ":$port" : '');
1080
1081                 return  (strlen($scheme) ? $scheme.":" : '') .
1082                         (strlen($authority) ? "//".$authority : '') .
1083                         $get('path') .
1084                         (strlen($query) ? "?".$query : '') .
1085                         (strlen($fragment) ? "#".$fragment : '');
1086         }
1087 }