]> git.mxchange.org Git - friendica.git/commitdiff
Create Network class
authorAdam Magness <adam.magness@gmail.com>
Sat, 27 Jan 2018 03:51:07 +0000 (22:51 -0500)
committerAdam Magness <adam.magness@gmail.com>
Sat, 27 Jan 2018 03:51:07 +0000 (22:51 -0500)
Create Network class and copy methods

include/network.php
src/Util/Network.php [new file with mode: 0644]

index 73fa80f981bb804e8988498bb3c474e26b9f57b5..7798fd9cd8bac12f1a767ac5998a151a930a4e14 100644 (file)
@@ -9,1079 +9,111 @@ use Friendica\Core\System;
 use Friendica\Core\Config;
 use Friendica\Network\Probe;
 use Friendica\Object\Image;
+use Friendica\Util\Network;
 use Friendica\Util\XML;
 
-/**
- * @brief Curl wrapper
- *
- * If binary flag is true, return binary results.
- * Set the cookiejar argument to a string (e.g. "/tmp/friendica-cookies.txt")
- * to preserve cookies from one request to the next.
- *
- * @param string  $url            URL to fetch
- * @param boolean $binary         default false
- *                                TRUE if asked to return binary results (file download)
- * @param integer $redirects      The recursion counter for internal use - default 0
- * @param integer $timeout        Timeout in seconds, default system config value or 60 seconds
- * @param string  $accept_content supply Accept: header with 'accept_content' as the value
- * @param string  $cookiejar      Path to cookie jar file
- *
- * @return string The fetched content
- */
 function fetch_url($url, $binary = false, &$redirects = 0, $timeout = 0, $accept_content = null, $cookiejar = 0)
 {
-       $ret = z_fetch_url(
-               $url,
-               $binary,
-               $redirects,
-               ['timeout'=>$timeout,
-               'accept_content'=>$accept_content,
-               'cookiejar'=>$cookiejar
-               ]
-       );
-
-       return($ret['body']);
+       return Network::fetchURL($url, $binary, $redirects, $timeout, $accept_content, $cookiejar);
 }
 
-/**
- * @brief fetches an URL.
- *
- * @param string  $url       URL to fetch
- * @param boolean $binary    default false
- *                           TRUE if asked to return binary results (file download)
- * @param int     $redirects The recursion counter for internal use - default 0
- * @param array   $opts      (optional parameters) assoziative array with:
- *                           'accept_content' => supply Accept: header with 'accept_content' as the value
- *                           'timeout' => int Timeout in seconds, default system config value or 60 seconds
- *                           'http_auth' => username:password
- *                           'novalidate' => do not validate SSL certs, default is to validate using our CA list
- *                           'nobody' => only return the header
- *                           'cookiejar' => path to cookie jar file
- *
- * @return array an assoziative array with:
- *    int 'return_code' => HTTP return code or 0 if timeout or failure
- *    boolean 'success' => boolean true (if HTTP 2xx result) or false
- *    string 'redirect_url' => in case of redirect, content was finally retrieved from this URL
- *    string 'header' => HTTP headers
- *    string 'body' => fetched content
- */
 function z_fetch_url($url, $binary = false, &$redirects = 0, $opts = [])
 {
-       $ret = ['return_code' => 0, 'success' => false, 'header' => '', 'info' => '', 'body' => ''];
-
-       $stamp1 = microtime(true);
-
-       $a = get_app();
-
-       if (blocked_url($url)) {
-               logger('z_fetch_url: domain of ' . $url . ' is blocked', LOGGER_DATA);
-               return $ret;
-       }
-
-       $ch = @curl_init($url);
-
-       if (($redirects > 8) || (!$ch)) {
-               return $ret;
-       }
-
-       @curl_setopt($ch, CURLOPT_HEADER, true);
-
-       if (x($opts, "cookiejar")) {
-               curl_setopt($ch, CURLOPT_COOKIEJAR, $opts["cookiejar"]);
-               curl_setopt($ch, CURLOPT_COOKIEFILE, $opts["cookiejar"]);
-       }
-
-       // These settings aren't needed. We're following the location already.
-       //      @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
-       //      @curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
-
-       if (x($opts, 'accept_content')) {
-               curl_setopt(
-                       $ch,
-                       CURLOPT_HTTPHEADER,
-                       ['Accept: ' . $opts['accept_content']]
-               );
-       }
-
-       @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-       @curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
-
-       $range = intval(Config::get('system', 'curl_range_bytes', 0));
-
-       if ($range > 0) {
-               @curl_setopt($ch, CURLOPT_RANGE, '0-' . $range);
-       }
-
-       // Without this setting it seems as if some webservers send compressed content
-       // This seems to confuse curl so that it shows this uncompressed.
-       /// @todo  We could possibly set this value to "gzip" or something similar
-       curl_setopt($ch, CURLOPT_ENCODING, '');
-
-       if (x($opts, 'headers')) {
-               @curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['headers']);
-       }
-
-       if (x($opts, 'nobody')) {
-               @curl_setopt($ch, CURLOPT_NOBODY, $opts['nobody']);
-       }
-
-       if (x($opts, 'timeout')) {
-               @curl_setopt($ch, CURLOPT_TIMEOUT, $opts['timeout']);
-       } else {
-               $curl_time = Config::get('system', 'curl_timeout', 60);
-               @curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time));
-       }
-
-       // by default we will allow self-signed certs
-       // but you can override this
-
-       $check_cert = Config::get('system', 'verifyssl');
-       @curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
-
-       if ($check_cert) {
-               @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
-       }
-
-       $proxy = Config::get('system', 'proxy');
-
-       if (strlen($proxy)) {
-               @curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
-               @curl_setopt($ch, CURLOPT_PROXY, $proxy);
-               $proxyuser = @Config::get('system', 'proxyuser');
-
-               if (strlen($proxyuser)) {
-                       @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser);
-               }
-       }
-
-       if (Config::get('system', 'ipv4_resolve', false)) {
-               curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
-       }
-
-       if ($binary) {
-               @curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
-       }
-
-       $a->set_curl_code(0);
-
-       // don't let curl abort the entire application
-       // if it throws any errors.
-
-       $s = @curl_exec($ch);
-       $curl_info = @curl_getinfo($ch);
-
-       // Special treatment for HTTP Code 416
-       // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416
-       if (($curl_info['http_code'] == 416) && ($range > 0)) {
-               @curl_setopt($ch, CURLOPT_RANGE, '');
-               $s = @curl_exec($ch);
-               $curl_info = @curl_getinfo($ch);
-       }
-
-       if (curl_errno($ch) !== CURLE_OK) {
-               logger('fetch_url error fetching ' . $url . ': ' . curl_error($ch), LOGGER_NORMAL);
-       }
-
-       $ret['errno'] = curl_errno($ch);
-
-       $base = $s;
-       $ret['info'] = $curl_info;
-
-       $http_code = $curl_info['http_code'];
-
-       logger('fetch_url ' . $url . ': ' . $http_code . " " . $s, LOGGER_DATA);
-       $header = '';
-
-       // Pull out multiple headers, e.g. proxy and continuation headers
-       // allow for HTTP/2.x without fixing code
-
-       while (preg_match('/^HTTP\/[1-2].+? [1-5][0-9][0-9]/', $base)) {
-               $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4);
-               $header .= $chunk;
-               $base = substr($base, strlen($chunk));
-       }
-
-       $a->set_curl_code($http_code);
-       $a->set_curl_content_type($curl_info['content_type']);
-       $a->set_curl_headers($header);
-
-       if ($http_code == 301 || $http_code == 302 || $http_code == 303 || $http_code == 307) {
-               $new_location_info = @parse_url($curl_info['redirect_url']);
-               $old_location_info = @parse_url($curl_info['url']);
-
-               $newurl = $curl_info['redirect_url'];
-
-               if (($new_location_info['path'] == '') && ( $new_location_info['host'] != '')) {
-                       $newurl = $new_location_info['scheme'] . '://' . $new_location_info['host'] . $old_location_info['path'];
-               }
-
-               $matches = [];
-
-               if (preg_match('/(Location:|URI:)(.*?)\n/i', $header, $matches)) {
-                       $newurl = trim(array_pop($matches));
-               }
-               if (strpos($newurl, '/') === 0) {
-                       $newurl = $old_location_info["scheme"]."://".$old_location_info["host"].$newurl;
-               }
-
-               if (filter_var($newurl, FILTER_VALIDATE_URL)) {
-                       $redirects++;
-                       @curl_close($ch);
-                       return z_fetch_url($newurl, $binary, $redirects, $opts);
-               }
-       }
-
-       $a->set_curl_code($http_code);
-       $a->set_curl_content_type($curl_info['content_type']);
-
-       $rc = intval($http_code);
-       $ret['return_code'] = $rc;
-       $ret['success'] = (($rc >= 200 && $rc <= 299) ? true : false);
-       $ret['redirect_url'] = $url;
-
-       if (!$ret['success']) {
-               $ret['error'] = curl_error($ch);
-               $ret['debug'] = $curl_info;
-               logger('z_fetch_url: error: '.$url.': '.$ret['return_code'].' - '.$ret['error'], LOGGER_DEBUG);
-               logger('z_fetch_url: debug: '.print_r($curl_info, true), LOGGER_DATA);
-       }
-
-       $ret['body'] = substr($s, strlen($header));
-       $ret['header'] = $header;
-
-       if (x($opts, 'debug')) {
-               $ret['debug'] = $curl_info;
-       }
-
-       @curl_close($ch);
-
-       $a->save_timestamp($stamp1, 'network');
-
-       return($ret);
+       return Network::zFetchURL($url, $binary, $redirects, $opts);
 }
 
-/**
- * @brief Send POST request to $url
- *
- * @param string  $url       URL to post
- * @param mixed   $params    array of POST variables
- * @param string  $headers   HTTP headers
- * @param integer $redirects Recursion counter for internal use - default = 0
- * @param integer $timeout   The timeout in seconds, default system config value or 60 seconds
- *
- * @return string The content
- */
 function post_url($url, $params, $headers = null, &$redirects = 0, $timeout = 0)
 {
-       $stamp1 = microtime(true);
-
-       if (blocked_url($url)) {
-               logger('post_url: domain of ' . $url . ' is blocked', LOGGER_DATA);
-               return false;
-       }
-
-       $a = get_app();
-       $ch = curl_init($url);
-
-       if (($redirects > 8) || (!$ch)) {
-               return false;
-       }
-
-       logger('post_url: start ' . $url, LOGGER_DATA);
-
-       curl_setopt($ch, CURLOPT_HEADER, true);
-       curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-       curl_setopt($ch, CURLOPT_POST, 1);
-       curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
-       curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
-
-       if (Config::get('system', 'ipv4_resolve', false)) {
-               curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
-       }
-
-       if (intval($timeout)) {
-               curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
-       } else {
-               $curl_time = Config::get('system', 'curl_timeout', 60);
-               curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time));
-       }
-
-       if (defined('LIGHTTPD')) {
-               if (!is_array($headers)) {
-                       $headers = ['Expect:'];
-               } else {
-                       if (!in_array('Expect:', $headers)) {
-                               array_push($headers, 'Expect:');
-                       }
-               }
-       }
-
-       if ($headers) {
-               curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
-       }
-
-       $check_cert = Config::get('system', 'verifyssl');
-       curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
-
-       if ($check_cert) {
-               @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
-       }
-
-       $proxy = Config::get('system', 'proxy');
-
-       if (strlen($proxy)) {
-               curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
-               curl_setopt($ch, CURLOPT_PROXY, $proxy);
-               $proxyuser = Config::get('system', 'proxyuser');
-               if (strlen($proxyuser)) {
-                       curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser);
-               }
-       }
-
-       $a->set_curl_code(0);
-
-       // don't let curl abort the entire application
-       // if it throws any errors.
-
-       $s = @curl_exec($ch);
-
-       $base = $s;
-       $curl_info = curl_getinfo($ch);
-       $http_code = $curl_info['http_code'];
-
-       logger('post_url: result ' . $http_code . ' - ' . $url, LOGGER_DATA);
-
-       $header = '';
-
-       // Pull out multiple headers, e.g. proxy and continuation headers
-       // allow for HTTP/2.x without fixing code
-
-       while (preg_match('/^HTTP\/[1-2].+? [1-5][0-9][0-9]/', $base)) {
-               $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4);
-               $header .= $chunk;
-               $base = substr($base, strlen($chunk));
-       }
-
-       if ($http_code == 301 || $http_code == 302 || $http_code == 303 || $http_code == 307) {
-               $matches = [];
-               preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches);
-               $newurl = trim(array_pop($matches));
-
-               if (strpos($newurl, '/') === 0) {
-                       $newurl = $old_location_info["scheme"] . "://" . $old_location_info["host"] . $newurl;
-               }
-
-               if (filter_var($newurl, FILTER_VALIDATE_URL)) {
-                       $redirects++;
-                       logger('post_url: redirect ' . $url . ' to ' . $newurl);
-                       return post_url($newurl, $params, $headers, $redirects, $timeout);
-               }
-       }
-
-       $a->set_curl_code($http_code);
-
-       $body = substr($s, strlen($header));
-
-       $a->set_curl_headers($header);
-
-       curl_close($ch);
-
-       $a->save_timestamp($stamp1, 'network');
-
-       logger('post_url: end ' . $url, LOGGER_DATA);
-
-       return $body;
+       return Network::postURL($url, $params, $headers, $redirects, $timeout);
 }
 
-// Generic XML return
-// Outputs a basic dfrn XML status structure to STDOUT, with a <status> variable
-// of $st and an optional text <message> of $message and terminates the current process.
-
 function xml_status($st, $message = '')
 {
-       $result = ['status' => $st];
-
-       if ($message != '') {
-               $result['message'] = $message;
-       }
-
-       if ($st) {
-               logger('xml_status returning non_zero: ' . $st . " message=" . $message);
-       }
-
-       header("Content-type: text/xml");
-
-       $xmldata = ["result" => $result];
-
-       echo XML::fromArray($xmldata, $xml);
-
-       killme();
+       Network::xmlStatus($st, $message);
 }
 
-/**
- * @brief Send HTTP status header and exit.
- *
- * @param integer $val HTTP status result value
- * @param array $description optional message
- *    'title' => header title
- *    'description' => optional message
- */
-
-/**
- * @brief Send HTTP status header and exit.
- *
- * @param integer $val         HTTP status result value
- * @param array   $description optional message
- *                             'title' => header title
- *                             'description' => optional message
- */
 function http_status_exit($val, $description = [])
 {
-       $err = '';
-       if ($val >= 400) {
-               $err = 'Error';
-               if (!isset($description["title"])) {
-                       $description["title"] = $err." ".$val;
-               }
-       }
-       if ($val >= 200 && $val < 300)
-               $err = 'OK';
-
-       logger('http_status_exit ' . $val);
-       header($_SERVER["SERVER_PROTOCOL"] . ' ' . $val . ' ' . $err);
-
-       if (isset($description["title"])) {
-               $tpl = get_markup_template('http_status.tpl');
-               echo replace_macros(
-                       $tpl,
-                       [
-                               '$title' => $description["title"],
-                               '$description' => $description["description"]]
-               );
-       }
-
-       killme();
+       Network::httpStatusExit($val, $description);
 }
 
-/**
- * @brief Check URL to se if ts's real
- *
- * Take a URL from the wild, prepend http:// if necessary
- * and check DNS to see if it's real (or check if is a valid IP address)
- *
- * @param string $url The URL to be validated
- * @return string|boolean The actual working URL, false else
- */
 function validate_url($url)
 {
-       if (Config::get('system', 'disable_url_validation')) {
-               return $url;
-       }
-
-       // no naked subdomains (allow localhost for tests)
-       if (strpos($url, '.') === false && strpos($url, '/localhost/') === false) {
-               return false;
-       }
-
-       if (substr($url, 0, 4) != 'http') {
-               $url = 'http://' . $url;
-       }
-
-       /// @TODO Really suppress function outcomes? Why not find them + debug them?
-       $h = @parse_url($url);
-
-       if ((is_array($h)) && (@dns_get_record($h['host'], DNS_A + DNS_CNAME + DNS_PTR) || filter_var($h['host'], FILTER_VALIDATE_IP) )) {
-               return $url;
-       }
-
-       return false;
+       return Network::validateURL($url);
 }
 
-/**
- * @brief Checks that email is an actual resolvable internet address
- *
- * @param string $addr The email address
- * @return boolean True if it's a valid email address, false if it's not
- */
 function validate_email($addr)
 {
-       if (Config::get('system', 'disable_email_validation')) {
-               return true;
-       }
-
-       if (! strpos($addr, '@')) {
-               return false;
-       }
-
-       $h = substr($addr, strpos($addr, '@') + 1);
-
-       if (($h) && (dns_get_record($h, DNS_A + DNS_CNAME + DNS_PTR + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP) )) {
-               return true;
-       }
-       return false;
+       return Network::validateEmail($addr);
 }
 
-/**
- * @brief Check if URL is allowed
- *
- * Check $url against our list of allowed sites,
- * wildcards allowed. If allowed_sites is unset return true;
- *
- * @param string $url URL which get tested
- * @return boolean True if url is allowed otherwise return false
- */
 function allowed_url($url)
 {
-       $h = @parse_url($url);
-
-       if (! $h) {
-               return false;
-       }
-
-       $str_allowed = Config::get('system', 'allowed_sites');
-       if (! $str_allowed) {
-               return true;
-       }
-
-       $found = false;
-
-       $host = strtolower($h['host']);
-
-       // always allow our own site
-       if ($host == strtolower($_SERVER['SERVER_NAME'])) {
-               return true;
-       }
-
-       $fnmatch = function_exists('fnmatch');
-       $allowed = explode(',', $str_allowed);
-
-       if (count($allowed)) {
-               foreach ($allowed as $a) {
-                       $pat = strtolower(trim($a));
-                       if (($fnmatch && fnmatch($pat, $host)) || ($pat == $host)) {
-                               $found = true;
-                               break;
-                       }
-               }
-       }
-       return $found;
+       return Network::allowedURL($url);
 }
 
-/**
- * Checks if the provided url domain is on the domain blocklist.
- * Returns true if it is or malformed URL, false if not.
- *
- * @param string $url The url to check the domain from
- *
- * @return boolean
- */
 function blocked_url($url)
 {
-       $h = @parse_url($url);
-
-       if (! $h) {
-               return true;
-       }
-
-       $domain_blocklist = Config::get('system', 'blocklist', []);
-       if (! $domain_blocklist) {
-               return false;
-       }
-
-       $host = strtolower($h['host']);
-
-       foreach ($domain_blocklist as $domain_block) {
-               if (strtolower($domain_block['domain']) == $host) {
-                       return true;
-               }
-       }
-
-       return false;
+       return Network::blockedURL($url);
 }
 
-/**
- * @brief Check if email address is allowed to register here.
- *
- * Compare against our list (wildcards allowed).
- *
- * @param  string $email email address
- * @return boolean False if not allowed, true if allowed
- *    or if allowed list is not configured
- */
-function allowed_email($email)
+function allowedEmail($email)
 {
-       $domain = strtolower(substr($email, strpos($email, '@') + 1));
-       if (!$domain) {
-               return false;
-       }
-
-       $str_allowed = Config::get('system', 'allowed_email', '');
-       if (!x($str_allowed)) {
-               return true;
-       }
-
-       $allowed = explode(',', $str_allowed);
-
-       return allowed_domain($domain, $allowed);
+       return Network::allowedEmail($email);
 }
 
-/**
- * Checks for the existence of a domain in a domain list
- *
- * @brief Checks for the existence of a domain in a domain list
- * @param string $domain
- * @param array  $domain_list
- * @return boolean
- */
 function allowed_domain($domain, array $domain_list)
 {
-       $found = false;
-
-       foreach ($domain_list as $item) {
-               $pat = strtolower(trim($item));
-               if (fnmatch($pat, $domain) || ($pat == $domain)) {
-                       $found = true;
-                       break;
-               }
-       }
-
-       return $found;
+       return Network::allowedDomain($domain, $domain_list);
 }
 
 function avatar_img($email)
 {
-       $avatar['size'] = 175;
-       $avatar['email'] = $email;
-       $avatar['url'] = '';
-       $avatar['success'] = false;
-
-       Addon::callHooks('avatar_lookup', $avatar);
-
-       if (! $avatar['success']) {
-               $avatar['url'] = System::baseUrl() . '/images/person-175.jpg';
-       }
-
-       logger('Avatar: ' . $avatar['email'] . ' ' . $avatar['url'], LOGGER_DEBUG);
-       return $avatar['url'];
+       return Network::avatarImg($email);
 }
 
 
 function parse_xml_string($s, $strict = true)
 {
-       // the "strict" parameter is deactivated
-
-       /// @todo Move this function to the xml class
-       libxml_use_internal_errors(true);
-
-       $x = @simplexml_load_string($s);
-       if (!$x) {
-               logger('libxml: parse: error: ' . $s, LOGGER_DATA);
-               foreach (libxml_get_errors() as $err) {
-                       logger('libxml: parse: ' . $err->code." at ".$err->line.":".$err->column." : ".$err->message, LOGGER_DATA);
-               }
-               libxml_clear_errors();
-       }
-       return $x;
+       return Network::parseXmlString($s, $strict);
 }
 
 function scale_external_images($srctext, $include_link = true, $scale_replace = false)
 {
-       // Suppress "view full size"
-       if (intval(Config::get('system', 'no_view_full_size'))) {
-               $include_link = false;
-       }
-
-       // Picture addresses can contain special characters
-       $s = htmlspecialchars_decode($srctext);
-
-       $matches = null;
-       $c = preg_match_all('/\[img.*?\](.*?)\[\/img\]/ism', $s, $matches, PREG_SET_ORDER);
-       if ($c) {
-               foreach ($matches as $mtch) {
-                       logger('scale_external_image: ' . $mtch[1]);
-
-                       $hostname = str_replace('www.', '', substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3));
-                       if (stristr($mtch[1], $hostname)) {
-                               continue;
-                       }
-
-                       // $scale_replace, if passed, is an array of two elements. The
-                       // first is the name of the full-size image. The second is the
-                       // name of a remote, scaled-down version of the full size image.
-                       // This allows Friendica to display the smaller remote image if
-                       // one exists, while still linking to the full-size image
-                       if ($scale_replace) {
-                               $scaled = str_replace($scale_replace[0], $scale_replace[1], $mtch[1]);
-                       } else {
-                               $scaled = $mtch[1];
-                       }
-                       $i = fetch_url($scaled);
-                       if (! $i) {
-                               return $srctext;
-                       }
-
-                       // guess mimetype from headers or filename
-                       $type = Image::guessType($mtch[1], true);
-
-                       if ($i) {
-                               $Image = new Image($i, $type);
-                               if ($Image->isValid()) {
-                                       $orig_width = $Image->getWidth();
-                                       $orig_height = $Image->getHeight();
-
-                                       if ($orig_width > 640 || $orig_height > 640) {
-                                               $Image->scaleDown(640);
-                                               $new_width = $Image->getWidth();
-                                               $new_height = $Image->getHeight();
-                                               logger('scale_external_images: ' . $orig_width . '->' . $new_width . 'w ' . $orig_height . '->' . $new_height . 'h' . ' match: ' . $mtch[0], LOGGER_DEBUG);
-                                               $s = str_replace(
-                                                       $mtch[0],
-                                                       '[img=' . $new_width . 'x' . $new_height. ']' . $scaled . '[/img]'
-                                                       . "\n" . (($include_link)
-                                                               ? '[url=' . $mtch[1] . ']' . L10n::t('view full size') . '[/url]' . "\n"
-                                                               : ''),
-                                                       $s
-                                               );
-                                               logger('scale_external_images: new string: ' . $s, LOGGER_DEBUG);
-                                       }
-                               }
-                       }
-               }
-       }
-
-       // replace the special char encoding
-       $s = htmlspecialchars($s, ENT_NOQUOTES, 'UTF-8');
-       return $s;
+       return Network::scaleExternalImages($srctext, $include_link, $scale_replace);
 }
 
-
 function fix_contact_ssl_policy(&$contact, $new_policy)
 {
-       $ssl_changed = false;
-       if ((intval($new_policy) == SSL_POLICY_SELFSIGN || $new_policy === 'self') && strstr($contact['url'], 'https:')) {
-               $ssl_changed = true;
-               $contact['url']     =   str_replace('https:', 'http:', $contact['url']);
-               $contact['request'] =   str_replace('https:', 'http:', $contact['request']);
-               $contact['notify']  =   str_replace('https:', 'http:', $contact['notify']);
-               $contact['poll']    =   str_replace('https:', 'http:', $contact['poll']);
-               $contact['confirm'] =   str_replace('https:', 'http:', $contact['confirm']);
-               $contact['poco']    =   str_replace('https:', 'http:', $contact['poco']);
-       }
-
-       if ((intval($new_policy) == SSL_POLICY_FULL || $new_policy === 'full') && strstr($contact['url'], 'http:')) {
-               $ssl_changed = true;
-               $contact['url']     =   str_replace('http:', 'https:', $contact['url']);
-               $contact['request'] =   str_replace('http:', 'https:', $contact['request']);
-               $contact['notify']  =   str_replace('http:', 'https:', $contact['notify']);
-               $contact['poll']    =   str_replace('http:', 'https:', $contact['poll']);
-               $contact['confirm'] =   str_replace('http:', 'https:', $contact['confirm']);
-               $contact['poco']    =   str_replace('http:', 'https:', $contact['poco']);
-       }
-
-       if ($ssl_changed) {
-               $fields = ['url' => $contact['url'], 'request' => $contact['request'],
-                               'notify' => $contact['notify'], 'poll' => $contact['poll'],
-                               'confirm' => $contact['confirm'], 'poco' => $contact['poco']];
-               dba::update('contact', $fields, ['id' => $contact['id']]);
-       }
+       Network::fixContactSslPolicy($contact, $new_policy);
 }
 
-/**
- * @brief Remove Google Analytics and other tracking platforms params from URL
- *
- * @param string $url Any user-submitted URL that may contain tracking params
- * @return string The same URL stripped of tracking parameters
- */
 function strip_tracking_query_params($url)
 {
-       $urldata = parse_url($url);
-       if (is_string($urldata["query"])) {
-               $query = $urldata["query"];
-               parse_str($query, $querydata);
-
-               if (is_array($querydata)) {
-                       foreach ($querydata as $param => $value) {
-                               if (in_array(
-                                       $param,
-                                       [
-                                               "utm_source", "utm_medium", "utm_term", "utm_content", "utm_campaign",
-                                               "wt_mc", "pk_campaign", "pk_kwd", "mc_cid", "mc_eid",
-                                               "fb_action_ids", "fb_action_types", "fb_ref",
-                                               "awesm", "wtrid",
-                                               "woo_campaign", "woo_source", "woo_medium", "woo_content", "woo_term"]
-                                       )
-                               ) {
-                                       $pair = $param . "=" . urlencode($value);
-                                       $url = str_replace($pair, "", $url);
-
-                                       // Second try: if the url isn't encoded completely
-                                       $pair = $param . "=" . str_replace(" ", "+", $value);
-                                       $url = str_replace($pair, "", $url);
-
-                                       // Third try: Maybey the url isn't encoded at all
-                                       $pair = $param . "=" . $value;
-                                       $url = str_replace($pair, "", $url);
-
-                                       $url = str_replace(["?&", "&&"], ["?", ""], $url);
-                               }
-                       }
-               }
-
-               if (substr($url, -1, 1) == "?") {
-                       $url = substr($url, 0, -1);
-               }
-       }
-
-       return $url;
+       return Network::stripTrackingQueryParams($url);
 }
 
-/**
- * @brief Returns the original URL of the provided URL
- *
- * This function strips tracking query params and follows redirections, either
- * through HTTP code or meta refresh tags. Stops after 10 redirections.
- *
- * @todo Remove the $fetchbody parameter that generates an extraneous HEAD request
- *
- * @see ParseUrl::getSiteinfo
- *
- * @param string $url       A user-submitted URL
- * @param int    $depth     The current redirection recursion level (internal)
- * @param bool   $fetchbody Wether to fetch the body or not after the HEAD requests
- * @return string A canonical URL
- */
 function original_url($url, $depth = 1, $fetchbody = false)
 {
-       $a = get_app();
-
-       $url = strip_tracking_query_params($url);
-
-       if ($depth > 10) {
-               return($url);
-       }
-
-       $url = trim($url, "'");
-
-       $stamp1 = microtime(true);
-
-       $ch = curl_init();
-       curl_setopt($ch, CURLOPT_URL, $url);
-       curl_setopt($ch, CURLOPT_HEADER, 1);
-       curl_setopt($ch, CURLOPT_NOBODY, 1);
-       curl_setopt($ch, CURLOPT_TIMEOUT, 10);
-       curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-       curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
-
-       curl_exec($ch);
-       $curl_info = @curl_getinfo($ch);
-       $http_code = $curl_info['http_code'];
-       curl_close($ch);
-
-       $a->save_timestamp($stamp1, "network");
-
-       if ($http_code == 0)
-               return($url);
-
-       if ((($curl_info['http_code'] == "301") || ($curl_info['http_code'] == "302"))
-               && (($curl_info['redirect_url'] != "") || ($curl_info['location'] != ""))
-       ) {
-               if ($curl_info['redirect_url'] != "") {
-                       return(original_url($curl_info['redirect_url'], ++$depth, $fetchbody));
-               } else {
-                       return(original_url($curl_info['location'], ++$depth, $fetchbody));
-               }
-       }
-
-       // Check for redirects in the meta elements of the body if there are no redirects in the header.
-       if (!$fetchbody) {
-               return(original_url($url, ++$depth, true));
-       }
-
-       // if the file is too large then exit
-       if ($curl_info["download_content_length"] > 1000000) {
-               return($url);
-       }
-
-       // if it isn't a HTML file then exit
-       if (($curl_info["content_type"] != "") && !strstr(strtolower($curl_info["content_type"]), "html")) {
-               return($url);
-       }
-
-       $stamp1 = microtime(true);
-
-       $ch = curl_init();
-       curl_setopt($ch, CURLOPT_URL, $url);
-       curl_setopt($ch, CURLOPT_HEADER, 0);
-       curl_setopt($ch, CURLOPT_NOBODY, 0);
-       curl_setopt($ch, CURLOPT_TIMEOUT, 10);
-       curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-       curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
-
-       $body = curl_exec($ch);
-       curl_close($ch);
-
-       $a->save_timestamp($stamp1, "network");
-
-       if (trim($body) == "") {
-               return($url);
-       }
-
-       // Check for redirect in meta elements
-       $doc = new DOMDocument();
-       @$doc->loadHTML($body);
-
-       $xpath = new DomXPath($doc);
-
-       $list = $xpath->query("//meta[@content]");
-       foreach ($list as $node) {
-               $attr = [];
-               if ($node->attributes->length) {
-                       foreach ($node->attributes as $attribute) {
-                               $attr[$attribute->name] = $attribute->value;
-                       }
-               }
-
-               if (@$attr["http-equiv"] == 'refresh') {
-                       $path = $attr["content"];
-                       $pathinfo = explode(";", $path);
-                       foreach ($pathinfo as $value) {
-                               if (substr(strtolower($value), 0, 4) == "url=") {
-                                       return(original_url(substr($value, 4), ++$depth));
-                               }
-                       }
-               }
-       }
-
-       return $url;
+       return Network::originalURL($url, $depth, $fetchbody);
 }
 
 function short_link($url)
 {
-       require_once 'library/slinky.php';
-       $slinky = new Slinky($url);
-       $yourls_url = Config::get('yourls', 'url1');
-       if ($yourls_url) {
-               $yourls_username = Config::get('yourls', 'username1');
-               $yourls_password = Config::get('yourls', 'password1');
-               $yourls_ssl = Config::get('yourls', 'ssl1');
-               $yourls = new Slinky_YourLS();
-               $yourls->set('username', $yourls_username);
-               $yourls->set('password', $yourls_password);
-               $yourls->set('ssl', $yourls_ssl);
-               $yourls->set('yourls-url', $yourls_url);
-               $slinky->set_cascade([$yourls, new Slinky_Ur1ca(), new Slinky_TinyURL()]);
-       } else {
-               // setup a cascade of shortening services
-               // try to get a short link from these services
-               // in the order ur1.ca, tinyurl
-               $slinky->set_cascade([new Slinky_Ur1ca(), new Slinky_TinyURL()]);
-       }
-       return $slinky->short();
+       return Network::shortLink($url);
 }
 
-/**
- * @brief Encodes content to json
- *
- * This function encodes an array to json format
- * and adds an application/json HTTP header to the output.
- * After finishing the process is getting killed.
- *
- * @param array $x The input content
- */
 function json_return_and_die($x)
 {
-       header("content-type: application/json");
-       echo json_encode($x);
-       killme();
+       Network::jsonReturnAndDie($x);
 }
 
-/**
- * @brief Find the matching part between two url
- *
- * @param string $url1
- * @param string $url2
- * @return string The matching part
- */
 function matching_url($url1, $url2)
 {
-       if (($url1 == "") || ($url2 == "")) {
-               return "";
-       }
-
-       $url1 = normalise_link($url1);
-       $url2 = normalise_link($url2);
-
-       $parts1 = parse_url($url1);
-       $parts2 = parse_url($url2);
-
-       if (!isset($parts1["host"]) || !isset($parts2["host"])) {
-               return "";
-       }
-
-       if ($parts1["scheme"] != $parts2["scheme"]) {
-               return "";
-       }
-
-       if ($parts1["host"] != $parts2["host"]) {
-               return "";
-       }
-
-       if ($parts1["port"] != $parts2["port"]) {
-               return "";
-       }
-
-       $match = $parts1["scheme"]."://".$parts1["host"];
-
-       if ($parts1["port"]) {
-               $match .= ":".$parts1["port"];
-       }
-
-       $pathparts1 = explode("/", $parts1["path"]);
-       $pathparts2 = explode("/", $parts2["path"]);
-
-       $i = 0;
-       $path = "";
-       do {
-               $path1 = $pathparts1[$i];
-               $path2 = $pathparts2[$i];
-
-               if ($path1 == $path2) {
-                       $path .= $path1."/";
-               }
-       } while (($path1 == $path2) && ($i++ <= count($pathparts1)));
-
-       $match .= $path;
-
-       return normalise_link($match);
+       return Network::matchingURL($url1, $url2);
 }
 
-/**
- * @brief Glue url parts together
- *
- * @param array $parsed URL parts
- *
- * @return string The glued URL
- */
 function unParseUrl($parsed)
 {
-       $get = function ($key) use ($parsed) {
-               return isset($parsed[$key]) ? $parsed[$key] : null;
-       };
-
-       $pass      = $get('pass');
-       $user      = $get('user');
-       $userinfo  = $pass !== null ? "$user:$pass" : $user;
-       $port      = $get('port');
-       $scheme    = $get('scheme');
-       $query     = $get('query');
-       $fragment  = $get('fragment');
-       $authority = ($userinfo !== null ? $userinfo."@" : '') .
-                                       $get('host') .
-                                       ($port ? ":$port" : '');
-
-       return  (strlen($scheme) ? $scheme.":" : '') .
-               (strlen($authority) ? "//".$authority : '') .
-               $get('path') .
-               (strlen($query) ? "?".$query : '') .
-               (strlen($fragment) ? "#".$fragment : '');
+       return Network::unParseURL($parsed);
 }
diff --git a/src/Util/Network.php b/src/Util/Network.php
new file mode 100644 (file)
index 0000000..f25d9c9
--- /dev/null
@@ -0,0 +1,1086 @@
+<?php
+/**
+ * @file src/Util/Network.php
+ */
+namespace Friendica\Util;
+
+use Friendica\App;
+use Friendica\Core\Addon;
+use Friendica\Core\L10n;
+use Friendica\Core\System;
+use Friendica\Core\Config;
+use Friendica\Network\Probe;
+use Friendica\Object\Image;
+use Friendica\Util\XML;
+
+require_once 'library/slinky.php';
+
+class Network
+{
+       /**
+        * @brief Curl wrapper
+        *
+        * If binary flag is true, return binary results.
+        * Set the cookiejar argument to a string (e.g. "/tmp/friendica-cookies.txt")
+        * to preserve cookies from one request to the next.
+        *
+        * @param string  $url            URL to fetch
+        * @param boolean $binary         default false
+        *                                TRUE if asked to return binary results (file download)
+        * @param integer $redirects      The recursion counter for internal use - default 0
+        * @param integer $timeout        Timeout in seconds, default system config value or 60 seconds
+        * @param string  $accept_content supply Accept: header with 'accept_content' as the value
+        * @param string  $cookiejar      Path to cookie jar file
+        *
+        * @return string The fetched content
+        */
+       public static function fetchURL($url, $binary = false, &$redirects = 0, $timeout = 0, $accept_content = null, $cookiejar = 0)
+       {
+               $ret = self::zFetchURL(
+                       $url,
+                       $binary,
+                       $redirects,
+                       ['timeout'=>$timeout,
+                       'accept_content'=>$accept_content,
+                       'cookiejar'=>$cookiejar
+                       ]
+               );
+
+               return($ret['body']);
+       }
+
+       /**
+        * @brief fetches an URL.
+        *
+        * @param string  $url       URL to fetch
+        * @param boolean $binary    default false
+        *                           TRUE if asked to return binary results (file download)
+        * @param int     $redirects The recursion counter for internal use - default 0
+        * @param array   $opts      (optional parameters) assoziative array with:
+        *                           'accept_content' => supply Accept: header with 'accept_content' as the value
+        *                           'timeout' => int Timeout in seconds, default system config value or 60 seconds
+        *                           'http_auth' => username:password
+        *                           'novalidate' => do not validate SSL certs, default is to validate using our CA list
+        *                           'nobody' => only return the header
+        *                           'cookiejar' => path to cookie jar file
+        *
+        * @return array an assoziative array with:
+        *    int 'return_code' => HTTP return code or 0 if timeout or failure
+        *    boolean 'success' => boolean true (if HTTP 2xx result) or false
+        *    string 'redirect_url' => in case of redirect, content was finally retrieved from this URL
+        *    string 'header' => HTTP headers
+        *    string 'body' => fetched content
+        */
+       public static function zFetchURL($url, $binary = false, &$redirects = 0, $opts = [])
+       {
+               $ret = ['return_code' => 0, 'success' => false, 'header' => '', 'info' => '', 'body' => ''];
+
+               $stamp1 = microtime(true);
+
+               $a = get_app();
+
+               if (blocked_url($url)) {
+                       logger('z_fetch_url: domain of ' . $url . ' is blocked', LOGGER_DATA);
+                       return $ret;
+               }
+
+               $ch = @curl_init($url);
+
+               if (($redirects > 8) || (!$ch)) {
+                       return $ret;
+               }
+
+               @curl_setopt($ch, CURLOPT_HEADER, true);
+
+               if (x($opts, "cookiejar")) {
+                       curl_setopt($ch, CURLOPT_COOKIEJAR, $opts["cookiejar"]);
+                       curl_setopt($ch, CURLOPT_COOKIEFILE, $opts["cookiejar"]);
+               }
+
+               // These settings aren't needed. We're following the location already.
+               //      @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+               //      @curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
+
+               if (x($opts, 'accept_content')) {
+                       curl_setopt(
+                               $ch,
+                               CURLOPT_HTTPHEADER,
+                               ['Accept: ' . $opts['accept_content']]
+                       );
+               }
+
+               @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+               @curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
+
+               $range = intval(Config::get('system', 'curl_range_bytes', 0));
+
+               if ($range > 0) {
+                       @curl_setopt($ch, CURLOPT_RANGE, '0-' . $range);
+               }
+
+               // Without this setting it seems as if some webservers send compressed content
+               // This seems to confuse curl so that it shows this uncompressed.
+               /// @todo  We could possibly set this value to "gzip" or something similar
+               curl_setopt($ch, CURLOPT_ENCODING, '');
+
+               if (x($opts, 'headers')) {
+                       @curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['headers']);
+               }
+
+               if (x($opts, 'nobody')) {
+                       @curl_setopt($ch, CURLOPT_NOBODY, $opts['nobody']);
+               }
+
+               if (x($opts, 'timeout')) {
+                       @curl_setopt($ch, CURLOPT_TIMEOUT, $opts['timeout']);
+               } else {
+                       $curl_time = Config::get('system', 'curl_timeout', 60);
+                       @curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time));
+               }
+
+               // by default we will allow self-signed certs
+               // but you can override this
+
+               $check_cert = Config::get('system', 'verifyssl');
+               @curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
+
+               if ($check_cert) {
+                       @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
+               }
+
+               $proxy = Config::get('system', 'proxy');
+
+               if (strlen($proxy)) {
+                       @curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
+                       @curl_setopt($ch, CURLOPT_PROXY, $proxy);
+                       $proxyuser = @Config::get('system', 'proxyuser');
+
+                       if (strlen($proxyuser)) {
+                               @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser);
+                       }
+               }
+
+               if (Config::get('system', 'ipv4_resolve', false)) {
+                       curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+               }
+
+               if ($binary) {
+                       @curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
+               }
+
+               $a->set_curl_code(0);
+
+               // don't let curl abort the entire application
+               // if it throws any errors.
+
+               $s = @curl_exec($ch);
+               $curl_info = @curl_getinfo($ch);
+
+               // Special treatment for HTTP Code 416
+               // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416
+               if (($curl_info['http_code'] == 416) && ($range > 0)) {
+                       @curl_setopt($ch, CURLOPT_RANGE, '');
+                       $s = @curl_exec($ch);
+                       $curl_info = @curl_getinfo($ch);
+               }
+
+               if (curl_errno($ch) !== CURLE_OK) {
+                       logger('fetch_url error fetching ' . $url . ': ' . curl_error($ch), LOGGER_NORMAL);
+               }
+
+               $ret['errno'] = curl_errno($ch);
+
+               $base = $s;
+               $ret['info'] = $curl_info;
+
+               $http_code = $curl_info['http_code'];
+
+               logger('fetch_url ' . $url . ': ' . $http_code . " " . $s, LOGGER_DATA);
+               $header = '';
+
+               // Pull out multiple headers, e.g. proxy and continuation headers
+               // allow for HTTP/2.x without fixing code
+
+               while (preg_match('/^HTTP\/[1-2].+? [1-5][0-9][0-9]/', $base)) {
+                       $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4);
+                       $header .= $chunk;
+                       $base = substr($base, strlen($chunk));
+               }
+
+               $a->set_curl_code($http_code);
+               $a->set_curl_content_type($curl_info['content_type']);
+               $a->set_curl_headers($header);
+
+               if ($http_code == 301 || $http_code == 302 || $http_code == 303 || $http_code == 307) {
+                       $new_location_info = @parse_url($curl_info['redirect_url']);
+                       $old_location_info = @parse_url($curl_info['url']);
+
+                       $newurl = $curl_info['redirect_url'];
+
+                       if (($new_location_info['path'] == '') && ( $new_location_info['host'] != '')) {
+                               $newurl = $new_location_info['scheme'] . '://' . $new_location_info['host'] . $old_location_info['path'];
+                       }
+
+                       $matches = [];
+
+                       if (preg_match('/(Location:|URI:)(.*?)\n/i', $header, $matches)) {
+                               $newurl = trim(array_pop($matches));
+                       }
+                       if (strpos($newurl, '/') === 0) {
+                               $newurl = $old_location_info["scheme"]."://".$old_location_info["host"].$newurl;
+                       }
+
+                       if (filter_var($newurl, FILTER_VALIDATE_URL)) {
+                               $redirects++;
+                               @curl_close($ch);
+                               return z_fetch_url($newurl, $binary, $redirects, $opts);
+                       }
+               }
+
+               $a->set_curl_code($http_code);
+               $a->set_curl_content_type($curl_info['content_type']);
+
+               $rc = intval($http_code);
+               $ret['return_code'] = $rc;
+               $ret['success'] = (($rc >= 200 && $rc <= 299) ? true : false);
+               $ret['redirect_url'] = $url;
+
+               if (!$ret['success']) {
+                       $ret['error'] = curl_error($ch);
+                       $ret['debug'] = $curl_info;
+                       logger('z_fetch_url: error: '.$url.': '.$ret['return_code'].' - '.$ret['error'], LOGGER_DEBUG);
+                       logger('z_fetch_url: debug: '.print_r($curl_info, true), LOGGER_DATA);
+               }
+
+               $ret['body'] = substr($s, strlen($header));
+               $ret['header'] = $header;
+
+               if (x($opts, 'debug')) {
+                       $ret['debug'] = $curl_info;
+               }
+
+               @curl_close($ch);
+
+               $a->save_timestamp($stamp1, 'network');
+
+               return($ret);
+       }
+
+       /**
+        * @brief Send POST request to $url
+        *
+        * @param string  $url       URL to post
+        * @param mixed   $params    array of POST variables
+        * @param string  $headers   HTTP headers
+        * @param integer $redirects Recursion counter for internal use - default = 0
+        * @param integer $timeout   The timeout in seconds, default system config value or 60 seconds
+        *
+        * @return string The content
+        */
+       public static function postURL($url, $params, $headers = null, &$redirects = 0, $timeout = 0)
+       {
+               $stamp1 = microtime(true);
+
+               if (blocked_url($url)) {
+                       logger('post_url: domain of ' . $url . ' is blocked', LOGGER_DATA);
+                       return false;
+               }
+
+               $a = get_app();
+               $ch = curl_init($url);
+
+               if (($redirects > 8) || (!$ch)) {
+                       return false;
+               }
+
+               logger('post_url: start ' . $url, LOGGER_DATA);
+
+               curl_setopt($ch, CURLOPT_HEADER, true);
+               curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+               curl_setopt($ch, CURLOPT_POST, 1);
+               curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
+               curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
+
+               if (Config::get('system', 'ipv4_resolve', false)) {
+                       curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+               }
+
+               if (intval($timeout)) {
+                       curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
+               } else {
+                       $curl_time = Config::get('system', 'curl_timeout', 60);
+                       curl_setopt($ch, CURLOPT_TIMEOUT, intval($curl_time));
+               }
+
+               if (defined('LIGHTTPD')) {
+                       if (!is_array($headers)) {
+                               $headers = ['Expect:'];
+                       } else {
+                               if (!in_array('Expect:', $headers)) {
+                                       array_push($headers, 'Expect:');
+                               }
+                       }
+               }
+
+               if ($headers) {
+                       curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+               }
+
+               $check_cert = Config::get('system', 'verifyssl');
+               curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false));
+
+               if ($check_cert) {
+                       @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
+               }
+
+               $proxy = Config::get('system', 'proxy');
+
+               if (strlen($proxy)) {
+                       curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, 1);
+                       curl_setopt($ch, CURLOPT_PROXY, $proxy);
+                       $proxyuser = Config::get('system', 'proxyuser');
+                       if (strlen($proxyuser)) {
+                               curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyuser);
+                       }
+               }
+
+               $a->set_curl_code(0);
+
+               // don't let curl abort the entire application
+               // if it throws any errors.
+
+               $s = @curl_exec($ch);
+
+               $base = $s;
+               $curl_info = curl_getinfo($ch);
+               $http_code = $curl_info['http_code'];
+
+               logger('post_url: result ' . $http_code . ' - ' . $url, LOGGER_DATA);
+
+               $header = '';
+
+               // Pull out multiple headers, e.g. proxy and continuation headers
+               // allow for HTTP/2.x without fixing code
+
+               while (preg_match('/^HTTP\/[1-2].+? [1-5][0-9][0-9]/', $base)) {
+                       $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4);
+                       $header .= $chunk;
+                       $base = substr($base, strlen($chunk));
+               }
+
+               if ($http_code == 301 || $http_code == 302 || $http_code == 303 || $http_code == 307) {
+                       $matches = [];
+                       preg_match('/(Location:|URI:)(.*?)\n/', $header, $matches);
+                       $newurl = trim(array_pop($matches));
+
+                       if (strpos($newurl, '/') === 0) {
+                               $newurl = $old_location_info["scheme"] . "://" . $old_location_info["host"] . $newurl;
+                       }
+
+                       if (filter_var($newurl, FILTER_VALIDATE_URL)) {
+                               $redirects++;
+                               logger('post_url: redirect ' . $url . ' to ' . $newurl);
+                               return post_url($newurl, $params, $headers, $redirects, $timeout);
+                       }
+               }
+
+               $a->set_curl_code($http_code);
+
+               $body = substr($s, strlen($header));
+
+               $a->set_curl_headers($header);
+
+               curl_close($ch);
+
+               $a->save_timestamp($stamp1, 'network');
+
+               logger('post_url: end ' . $url, LOGGER_DATA);
+
+               return $body;
+       }
+
+       /**
+        * Generic XML return
+        * Outputs a basic dfrn XML status structure to STDOUT, with a <status> variable
+        * of $st and an optional text <message> of $message and terminates the current process.
+        */
+       public static function xmlStatus($st, $message = '')
+       {
+               $result = ['status' => $st];
+
+               if ($message != '') {
+                       $result['message'] = $message;
+               }
+
+               if ($st) {
+                       logger('xml_status returning non_zero: ' . $st . " message=" . $message);
+               }
+
+               header("Content-type: text/xml");
+
+               $xmldata = ["result" => $result];
+
+               echo XML::fromArray($xmldata, $xml);
+
+               killme();
+       }
+
+       /**
+        * @brief Send HTTP status header and exit.
+        *
+        * @param integer $val         HTTP status result value
+        * @param array   $description optional message
+        *                             'title' => header title
+        *                             'description' => optional message
+        */
+       public static function httpStatusExit($val, $description = [])
+       {
+               $err = '';
+               if ($val >= 400) {
+                       $err = 'Error';
+                       if (!isset($description["title"])) {
+                               $description["title"] = $err." ".$val;
+                       }
+               }
+
+               if ($val >= 200 && $val < 300) {
+                       $err = 'OK';
+               }
+
+               logger('http_status_exit ' . $val);
+               header($_SERVER["SERVER_PROTOCOL"] . ' ' . $val . ' ' . $err);
+
+               if (isset($description["title"])) {
+                       $tpl = get_markup_template('http_status.tpl');
+                       echo replace_macros(
+                               $tpl,
+                               [
+                                       '$title' => $description["title"],
+                                       '$description' => $description["description"]]
+                       );
+               }
+
+               killme();
+       }
+
+       /**
+        * @brief Check URL to se if ts's real
+        *
+        * Take a URL from the wild, prepend http:// if necessary
+        * and check DNS to see if it's real (or check if is a valid IP address)
+        *
+        * @param string $url The URL to be validated
+        * @return string|boolean The actual working URL, false else
+        */
+       public static function validateURL($url)
+       {
+               if (Config::get('system', 'disable_url_validation')) {
+                       return $url;
+               }
+
+               // no naked subdomains (allow localhost for tests)
+               if (strpos($url, '.') === false && strpos($url, '/localhost/') === false) {
+                       return false;
+               }
+
+               if (substr($url, 0, 4) != 'http') {
+                       $url = 'http://' . $url;
+               }
+
+               /// @TODO Really suppress function outcomes? Why not find them + debug them?
+               $h = @parse_url($url);
+
+               if ((is_array($h)) && (@dns_get_record($h['host'], DNS_A + DNS_CNAME + DNS_PTR) || filter_var($h['host'], FILTER_VALIDATE_IP) )) {
+                       return $url;
+               }
+
+               return false;
+       }
+
+       /**
+        * @brief Checks that email is an actual resolvable internet address
+        *
+        * @param string $addr The email address
+        * @return boolean True if it's a valid email address, false if it's not
+        */
+       public static function validateEmail($addr)
+       {
+               if (Config::get('system', 'disable_email_validation')) {
+                       return true;
+               }
+
+               if (! strpos($addr, '@')) {
+                       return false;
+               }
+
+               $h = substr($addr, strpos($addr, '@') + 1);
+
+               if (($h) && (dns_get_record($h, DNS_A + DNS_CNAME + DNS_PTR + DNS_MX) || filter_var($h, FILTER_VALIDATE_IP) )) {
+                       return true;
+               }
+               return false;
+       }
+
+       /**
+        * @brief Check if URL is allowed
+        *
+        * Check $url against our list of allowed sites,
+        * wildcards allowed. If allowed_sites is unset return true;
+        *
+        * @param string $url URL which get tested
+        * @return boolean True if url is allowed otherwise return false
+        */
+       public static function allowedURL($url)
+       {
+               $h = @parse_url($url);
+
+               if (! $h) {
+                       return false;
+               }
+
+               $str_allowed = Config::get('system', 'allowed_sites');
+               if (! $str_allowed) {
+                       return true;
+               }
+
+               $found = false;
+
+               $host = strtolower($h['host']);
+
+               // always allow our own site
+               if ($host == strtolower($_SERVER['SERVER_NAME'])) {
+                       return true;
+               }
+
+               $fnmatch = function_exists('fnmatch');
+               $allowed = explode(',', $str_allowed);
+
+               if (count($allowed)) {
+                       foreach ($allowed as $a) {
+                               $pat = strtolower(trim($a));
+                               if (($fnmatch && fnmatch($pat, $host)) || ($pat == $host)) {
+                                       $found = true;
+                                       break;
+                               }
+                       }
+               }
+               return $found;
+       }
+
+       /**
+        * Checks if the provided url domain is on the domain blocklist.
+        * Returns true if it is or malformed URL, false if not.
+        *
+        * @param string $url The url to check the domain from
+        *
+        * @return boolean
+        */
+       public static function blockedURL($url)
+       {
+               $h = @parse_url($url);
+
+               if (! $h) {
+                       return true;
+               }
+
+               $domain_blocklist = Config::get('system', 'blocklist', []);
+               if (! $domain_blocklist) {
+                       return false;
+               }
+
+               $host = strtolower($h['host']);
+
+               foreach ($domain_blocklist as $domain_block) {
+                       if (strtolower($domain_block['domain']) == $host) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
+       /**
+        * @brief Check if email address is allowed to register here.
+        *
+        * Compare against our list (wildcards allowed).
+        *
+        * @param  string $email email address
+        * @return boolean False if not allowed, true if allowed
+        *    or if allowed list is not configured
+        */
+       public static function allowedEmail($email)
+       {
+               $domain = strtolower(substr($email, strpos($email, '@') + 1));
+               if (!$domain) {
+                       return false;
+               }
+
+               $str_allowed = Config::get('system', 'allowed_email', '');
+               if (!x($str_allowed)) {
+                       return true;
+               }
+
+               $allowed = explode(',', $str_allowed);
+
+               return allowed_domain($domain, $allowed);
+       }
+
+       /**
+        * Checks for the existence of a domain in a domain list
+        *
+        * @brief Checks for the existence of a domain in a domain list
+        * @param string $domain
+        * @param array  $domain_list
+        * @return boolean
+        */
+       public static function allowedDomain($domain, array $domain_list)
+       {
+               $found = false;
+
+               foreach ($domain_list as $item) {
+                       $pat = strtolower(trim($item));
+                       if (fnmatch($pat, $domain) || ($pat == $domain)) {
+                               $found = true;
+                               break;
+                       }
+               }
+
+               return $found;
+       }
+
+       public static function avatarImg($email)
+       {
+               $avatar['size'] = 175;
+               $avatar['email'] = $email;
+               $avatar['url'] = '';
+               $avatar['success'] = false;
+
+               Addon::callHooks('avatar_lookup', $avatar);
+
+               if (! $avatar['success']) {
+                       $avatar['url'] = System::baseUrl() . '/images/person-175.jpg';
+               }
+
+               logger('Avatar: ' . $avatar['email'] . ' ' . $avatar['url'], LOGGER_DEBUG);
+               return $avatar['url'];
+       }
+
+       public static function parseXmlString($s, $strict = true)
+       {
+               // the "strict" parameter is deactivated
+
+               /// @todo Move this function to the xml class
+               libxml_use_internal_errors(true);
+
+               $x = @simplexml_load_string($s);
+               if (!$x) {
+                       logger('libxml: parse: error: ' . $s, LOGGER_DATA);
+                       foreach (libxml_get_errors() as $err) {
+                               logger('libxml: parse: ' . $err->code." at ".$err->line.":".$err->column." : ".$err->message, LOGGER_DATA);
+                       }
+                       libxml_clear_errors();
+               }
+               return $x;
+       }
+
+       public static function scaleExternalImages($srctext, $include_link = true, $scale_replace = false)
+       {
+               // Suppress "view full size"
+               if (intval(Config::get('system', 'no_view_full_size'))) {
+                       $include_link = false;
+               }
+
+               // Picture addresses can contain special characters
+               $s = htmlspecialchars_decode($srctext);
+
+               $matches = null;
+               $c = preg_match_all('/\[img.*?\](.*?)\[\/img\]/ism', $s, $matches, PREG_SET_ORDER);
+               if ($c) {
+                       foreach ($matches as $mtch) {
+                               logger('scale_external_image: ' . $mtch[1]);
+
+                               $hostname = str_replace('www.', '', substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3));
+                               if (stristr($mtch[1], $hostname)) {
+                                       continue;
+                               }
+
+                               // $scale_replace, if passed, is an array of two elements. The
+                               // first is the name of the full-size image. The second is the
+                               // name of a remote, scaled-down version of the full size image.
+                               // This allows Friendica to display the smaller remote image if
+                               // one exists, while still linking to the full-size image
+                               if ($scale_replace) {
+                                       $scaled = str_replace($scale_replace[0], $scale_replace[1], $mtch[1]);
+                               } else {
+                                       $scaled = $mtch[1];
+                               }
+                               $i = fetch_url($scaled);
+                               if (! $i) {
+                                       return $srctext;
+                               }
+
+                               // guess mimetype from headers or filename
+                               $type = Image::guessType($mtch[1], true);
+
+                               if ($i) {
+                                       $Image = new Image($i, $type);
+                                       if ($Image->isValid()) {
+                                               $orig_width = $Image->getWidth();
+                                               $orig_height = $Image->getHeight();
+
+                                               if ($orig_width > 640 || $orig_height > 640) {
+                                                       $Image->scaleDown(640);
+                                                       $new_width = $Image->getWidth();
+                                                       $new_height = $Image->getHeight();
+                                                       logger('scale_external_images: ' . $orig_width . '->' . $new_width . 'w ' . $orig_height . '->' . $new_height . 'h' . ' match: ' . $mtch[0], LOGGER_DEBUG);
+                                                       $s = str_replace(
+                                                               $mtch[0],
+                                                               '[img=' . $new_width . 'x' . $new_height. ']' . $scaled . '[/img]'
+                                                               . "\n" . (($include_link)
+                                                                       ? '[url=' . $mtch[1] . ']' . L10n::t('view full size') . '[/url]' . "\n"
+                                                                       : ''),
+                                                               $s
+                                                       );
+                                                       logger('scale_external_images: new string: ' . $s, LOGGER_DEBUG);
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               // replace the special char encoding
+               $s = htmlspecialchars($s, ENT_NOQUOTES, 'UTF-8');
+               return $s;
+       }
+
+       public static function fixContactSslPolicy(&$contact, $new_policy)
+       {
+               $ssl_changed = false;
+               if ((intval($new_policy) == SSL_POLICY_SELFSIGN || $new_policy === 'self') && strstr($contact['url'], 'https:')) {
+                       $ssl_changed = true;
+                       $contact['url']     =   str_replace('https:', 'http:', $contact['url']);
+                       $contact['request'] =   str_replace('https:', 'http:', $contact['request']);
+                       $contact['notify']  =   str_replace('https:', 'http:', $contact['notify']);
+                       $contact['poll']    =   str_replace('https:', 'http:', $contact['poll']);
+                       $contact['confirm'] =   str_replace('https:', 'http:', $contact['confirm']);
+                       $contact['poco']    =   str_replace('https:', 'http:', $contact['poco']);
+               }
+
+               if ((intval($new_policy) == SSL_POLICY_FULL || $new_policy === 'full') && strstr($contact['url'], 'http:')) {
+                       $ssl_changed = true;
+                       $contact['url']     =   str_replace('http:', 'https:', $contact['url']);
+                       $contact['request'] =   str_replace('http:', 'https:', $contact['request']);
+                       $contact['notify']  =   str_replace('http:', 'https:', $contact['notify']);
+                       $contact['poll']    =   str_replace('http:', 'https:', $contact['poll']);
+                       $contact['confirm'] =   str_replace('http:', 'https:', $contact['confirm']);
+                       $contact['poco']    =   str_replace('http:', 'https:', $contact['poco']);
+               }
+
+               if ($ssl_changed) {
+                       $fields = ['url' => $contact['url'], 'request' => $contact['request'],
+                                       'notify' => $contact['notify'], 'poll' => $contact['poll'],
+                                       'confirm' => $contact['confirm'], 'poco' => $contact['poco']];
+                       dba::update('contact', $fields, ['id' => $contact['id']]);
+               }
+       }
+
+       /**
+        * @brief Remove Google Analytics and other tracking platforms params from URL
+        *
+        * @param string $url Any user-submitted URL that may contain tracking params
+        * @return string The same URL stripped of tracking parameters
+        */
+       public static function stripTrackingQueryParams($url)
+       {
+               $urldata = parse_url($url);
+               if (is_string($urldata["query"])) {
+                       $query = $urldata["query"];
+                       parse_str($query, $querydata);
+
+                       if (is_array($querydata)) {
+                               foreach ($querydata as $param => $value) {
+                                       if (in_array(
+                                               $param,
+                                               [
+                                                       "utm_source", "utm_medium", "utm_term", "utm_content", "utm_campaign",
+                                                       "wt_mc", "pk_campaign", "pk_kwd", "mc_cid", "mc_eid",
+                                                       "fb_action_ids", "fb_action_types", "fb_ref",
+                                                       "awesm", "wtrid",
+                                                       "woo_campaign", "woo_source", "woo_medium", "woo_content", "woo_term"]
+                                               )
+                                       ) {
+                                               $pair = $param . "=" . urlencode($value);
+                                               $url = str_replace($pair, "", $url);
+
+                                               // Second try: if the url isn't encoded completely
+                                               $pair = $param . "=" . str_replace(" ", "+", $value);
+                                               $url = str_replace($pair, "", $url);
+
+                                               // Third try: Maybey the url isn't encoded at all
+                                               $pair = $param . "=" . $value;
+                                               $url = str_replace($pair, "", $url);
+
+                                               $url = str_replace(["?&", "&&"], ["?", ""], $url);
+                                       }
+                               }
+                       }
+
+                       if (substr($url, -1, 1) == "?") {
+                               $url = substr($url, 0, -1);
+                       }
+               }
+
+               return $url;
+       }
+
+       /**
+        * @brief Returns the original URL of the provided URL
+        *
+        * This function strips tracking query params and follows redirections, either
+        * through HTTP code or meta refresh tags. Stops after 10 redirections.
+        *
+        * @todo Remove the $fetchbody parameter that generates an extraneous HEAD request
+        *
+        * @see ParseUrl::getSiteinfo
+        *
+        * @param string $url       A user-submitted URL
+        * @param int    $depth     The current redirection recursion level (internal)
+        * @param bool   $fetchbody Wether to fetch the body or not after the HEAD requests
+        * @return string A canonical URL
+        */
+       public static function originalURL($url, $depth = 1, $fetchbody = false)
+       {
+               $a = get_app();
+
+               $url = strip_tracking_query_params($url);
+
+               if ($depth > 10) {
+                       return($url);
+               }
+
+               $url = trim($url, "'");
+
+               $stamp1 = microtime(true);
+
+               $ch = curl_init();
+               curl_setopt($ch, CURLOPT_URL, $url);
+               curl_setopt($ch, CURLOPT_HEADER, 1);
+               curl_setopt($ch, CURLOPT_NOBODY, 1);
+               curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+               curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+               curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
+
+               curl_exec($ch);
+               $curl_info = @curl_getinfo($ch);
+               $http_code = $curl_info['http_code'];
+               curl_close($ch);
+
+               $a->save_timestamp($stamp1, "network");
+
+               if ($http_code == 0) {
+                       return($url);
+               }
+
+               if ((($curl_info['http_code'] == "301") || ($curl_info['http_code'] == "302"))
+                       && (($curl_info['redirect_url'] != "") || ($curl_info['location'] != ""))
+               ) {
+                       if ($curl_info['redirect_url'] != "") {
+                               return(original_url($curl_info['redirect_url'], ++$depth, $fetchbody));
+                       } else {
+                               return(original_url($curl_info['location'], ++$depth, $fetchbody));
+                       }
+               }
+
+               // Check for redirects in the meta elements of the body if there are no redirects in the header.
+               if (!$fetchbody) {
+                       return(original_url($url, ++$depth, true));
+               }
+
+               // if the file is too large then exit
+               if ($curl_info["download_content_length"] > 1000000) {
+                       return($url);
+               }
+
+               // if it isn't a HTML file then exit
+               if (($curl_info["content_type"] != "") && !strstr(strtolower($curl_info["content_type"]), "html")) {
+                       return($url);
+               }
+
+               $stamp1 = microtime(true);
+
+               $ch = curl_init();
+               curl_setopt($ch, CURLOPT_URL, $url);
+               curl_setopt($ch, CURLOPT_HEADER, 0);
+               curl_setopt($ch, CURLOPT_NOBODY, 0);
+               curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+               curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+               curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent());
+
+               $body = curl_exec($ch);
+               curl_close($ch);
+
+               $a->save_timestamp($stamp1, "network");
+
+               if (trim($body) == "") {
+                       return($url);
+               }
+
+               // Check for redirect in meta elements
+               $doc = new DOMDocument();
+               @$doc->loadHTML($body);
+
+               $xpath = new DomXPath($doc);
+
+               $list = $xpath->query("//meta[@content]");
+               foreach ($list as $node) {
+                       $attr = [];
+                       if ($node->attributes->length) {
+                               foreach ($node->attributes as $attribute) {
+                                       $attr[$attribute->name] = $attribute->value;
+                               }
+                       }
+
+                       if (@$attr["http-equiv"] == 'refresh') {
+                               $path = $attr["content"];
+                               $pathinfo = explode(";", $path);
+                               foreach ($pathinfo as $value) {
+                                       if (substr(strtolower($value), 0, 4) == "url=") {
+                                               return(original_url(substr($value, 4), ++$depth));
+                                       }
+                               }
+                       }
+               }
+
+               return $url;
+       }
+
+       public static function shortLink($url)
+       {
+               $slinky = new Slinky($url);
+               $yourls_url = Config::get('yourls', 'url1');
+               if ($yourls_url) {
+                       $yourls_username = Config::get('yourls', 'username1');
+                       $yourls_password = Config::get('yourls', 'password1');
+                       $yourls_ssl = Config::get('yourls', 'ssl1');
+                       $yourls = new Slinky_YourLS();
+                       $yourls->set('username', $yourls_username);
+                       $yourls->set('password', $yourls_password);
+                       $yourls->set('ssl', $yourls_ssl);
+                       $yourls->set('yourls-url', $yourls_url);
+                       $slinky->set_cascade([$yourls, new Slinky_Ur1ca(), new Slinky_TinyURL()]);
+               } else {
+                       // setup a cascade of shortening services
+                       // try to get a short link from these services
+                       // in the order ur1.ca, tinyurl
+                       $slinky->set_cascade([new Slinky_Ur1ca(), new Slinky_TinyURL()]);
+               }
+               return $slinky->short();
+       }
+
+       /**
+        * @brief Encodes content to json
+        *
+        * This function encodes an array to json format
+        * and adds an application/json HTTP header to the output.
+        * After finishing the process is getting killed.
+        *
+        * @param array $x The input content
+        */
+       public static function jsonReturnAndDie($x)
+       {
+               header("content-type: application/json");
+               echo json_encode($x);
+               killme();
+       }
+
+       /**
+        * @brief Find the matching part between two url
+        *
+        * @param string $url1
+        * @param string $url2
+        * @return string The matching part
+        */
+       public static function matchingURL($url1, $url2)
+       {
+               if (($url1 == "") || ($url2 == "")) {
+                       return "";
+               }
+
+               $url1 = normalise_link($url1);
+               $url2 = normalise_link($url2);
+
+               $parts1 = parse_url($url1);
+               $parts2 = parse_url($url2);
+
+               if (!isset($parts1["host"]) || !isset($parts2["host"])) {
+                       return "";
+               }
+
+               if ($parts1["scheme"] != $parts2["scheme"]) {
+                       return "";
+               }
+
+               if ($parts1["host"] != $parts2["host"]) {
+                       return "";
+               }
+
+               if ($parts1["port"] != $parts2["port"]) {
+                       return "";
+               }
+
+               $match = $parts1["scheme"]."://".$parts1["host"];
+
+               if ($parts1["port"]) {
+                       $match .= ":".$parts1["port"];
+               }
+
+               $pathparts1 = explode("/", $parts1["path"]);
+               $pathparts2 = explode("/", $parts2["path"]);
+
+               $i = 0;
+               $path = "";
+               do {
+                       $path1 = $pathparts1[$i];
+                       $path2 = $pathparts2[$i];
+
+                       if ($path1 == $path2) {
+                               $path .= $path1."/";
+                       }
+               } while (($path1 == $path2) && ($i++ <= count($pathparts1)));
+
+               $match .= $path;
+
+               return normalise_link($match);
+       }
+
+       /**
+        * @brief Glue url parts together
+        *
+        * @param array $parsed URL parts
+        *
+        * @return string The glued URL
+        */
+       public static function unParseURL($parsed)
+       {
+               $get = function ($key) use ($parsed) {
+                       return isset($parsed[$key]) ? $parsed[$key] : null;
+               };
+
+               $pass      = $get('pass');
+               $user      = $get('user');
+               $userinfo  = $pass !== null ? "$user:$pass" : $user;
+               $port      = $get('port');
+               $scheme    = $get('scheme');
+               $query     = $get('query');
+               $fragment  = $get('fragment');
+               $authority = ($userinfo !== null ? $userinfo."@" : '') .
+                                               $get('host') .
+                                               ($port ? ":$port" : '');
+
+               return  (strlen($scheme) ? $scheme.":" : '') .
+                       (strlen($authority) ? "//".$authority : '') .
+                       $get('path') .
+                       (strlen($query) ? "?".$query : '') .
+                       (strlen($fragment) ? "#".$fragment : '');
+       }
+}