X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FNetwork%2FProbe.php;h=3fe035286f3bed729a11b947d06ac3eb551d063b;hb=722aada460aa023212b7d97932fe338fe54861a8;hp=da6106a39a1708fe249f4769fde302a259040770;hpb=b1b4e98bd948c2a4414cfa062491abc913a7c557;p=friendica.git diff --git a/src/Network/Probe.php b/src/Network/Probe.php index da6106a39a..3fe035286f 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -23,7 +23,7 @@ namespace Friendica\Network; use DOMDocument; use DomXPath; -use Friendica\Core\Cache\Duration; +use Friendica\Core\Hook; use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; @@ -32,11 +32,13 @@ use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\GServer; use Friendica\Model\Profile; +use Friendica\Model\User; use Friendica\Protocol\ActivityNamespace; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Email; use Friendica\Protocol\Feed; use Friendica\Util\Crypto; +use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; use Friendica\Util\Strings; use Friendica\Util\XML; @@ -46,6 +48,8 @@ use Friendica\Util\XML; */ class Probe { + const WEBFINGER = '/.well-known/webfinger?resource={uri}'; + private static $baseurl; private static $istimeout; @@ -87,13 +91,19 @@ class Probe "community", "keywords", "location", "about", "hide", "batch", "notify", "poll", "request", "confirm", "subscribe", "poco", "following", "followers", "inbox", "outbox", "sharedinbox", - "priority", "network", "pubkey", "baseurl", "gsid"]; + "priority", "network", "pubkey", "manually-approve", "baseurl", "gsid"]; + + $numeric_fields = ["gsid", "hide", "account-type", "manually-approve"]; $newdata = []; foreach ($fields as $field) { if (isset($data[$field])) { - $newdata[$field] = $data[$field]; - } elseif ($field != "gsid") { + if (in_array($field, $numeric_fields)) { + $newdata[$field] = (int)$data[$field]; + } else { + $newdata[$field] = $data[$field]; + } + } elseif (!in_array($field, $numeric_fields)) { $newdata[$field] = ""; } else { $newdata[$field] = null; @@ -159,7 +169,7 @@ class Probe Logger::info('Probing', ['host' => $host, 'ssl_url' => $ssl_url, 'url' => $url, 'callstack' => System::callstack(20)]); $xrd = null; - $curlResult = Network::curl($ssl_url, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']); + $curlResult = DI::httpRequest()->get($ssl_url, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']); $ssl_connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0); if ($curlResult->isSuccess()) { $xml = $curlResult->getBody(); @@ -170,21 +180,21 @@ class Probe $host_url = $host; } } elseif ($curlResult->isTimeout()) { - Logger::info('Probing timeout', ['url' => $ssl_url], Logger::DEBUG); + Logger::info('Probing timeout', ['url' => $ssl_url]); self::$istimeout = true; - return false; + return []; } if (!is_object($xrd) && !empty($url)) { - $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']); + $curlResult = DI::httpRequest()->get($url, false, ['timeout' => $xrd_timeout, 'accept_content' => 'application/xrd+xml']); $connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0); if ($curlResult->isTimeout()) { - Logger::info('Probing timeout', ['url' => $url], Logger::DEBUG); + Logger::info('Probing timeout', ['url' => $url]); self::$istimeout = true; - return false; + return []; } elseif ($connection_error && $ssl_connection_error) { self::$istimeout = true; - return false; + return []; } $xml = $curlResult->getBody(); @@ -192,17 +202,17 @@ class Probe $host_url = 'http://'.$host; } if (!is_object($xrd)) { - Logger::log("No xrd object found for ".$host, Logger::DEBUG); + Logger::info('No xrd object found', ['host' => $host]); return []; } $links = XML::elementToArray($xrd); if (!isset($links["xrd"]["link"])) { - Logger::log("No xrd data found for ".$host, Logger::DEBUG); + Logger::info('No xrd data found', ['host' => $host]); return []; } - $lrdd = ['application/jrd+json' => $host_url . '/.well-known/webfinger?resource={uri}']; + $lrdd = []; foreach ($links["xrd"]["link"] as $value => $link) { if (!empty($link["@attributes"])) { @@ -222,7 +232,7 @@ class Probe self::$baseurl = $host_url; - Logger::log("Probing successful for ".$host, Logger::DEBUG); + Logger::info('Probing successful', ['host' => $host]); return $lrdd; } @@ -253,7 +263,7 @@ class Probe $profile_link = ''; $links = self::lrdd($webbie); - Logger::log('webfingerDfrn: '.$webbie.':'.print_r($links, true), Logger::DATA); + Logger::debug('Result', ['url' => $webbie, 'links' => $links]); if (!empty($links) && is_array($links)) { foreach ($links as $link) { if ($link['@attributes']['rel'] === ActivityNamespace::DFRN) { @@ -280,71 +290,17 @@ class Probe */ public static function lrdd(string $uri) { - $lrdd = self::hostMeta($uri); - $webfinger = null; - - if (is_bool($lrdd)) { + $data = self::getWebfingerArray($uri); + if (empty($data)) { return []; } + $webfinger = $data['webfinger']; - if (!$lrdd) { - $parts = @parse_url($uri); - if (!$parts || empty($parts["host"]) || empty($parts["path"])) { - return []; - } - - $host = $parts['scheme'] . '://' . $parts["host"]; - if (!empty($parts["port"])) { - $host .= ':'.$parts["port"]; - } - - $path_parts = explode("/", trim($parts["path"], "/")); - - $nick = array_pop($path_parts); - - do { - $lrdd = self::hostMeta($host); - $host .= "/".array_shift($path_parts); - } while (!$lrdd && (sizeof($path_parts) > 0)); - } - - if (!$lrdd) { - Logger::log("No lrdd data found for ".$uri, Logger::DEBUG); + if (empty($webfinger["links"])) { + Logger::info('No webfinger links found', ['uri' => $uri]); return []; } - foreach ($lrdd as $type => $template) { - if ($webfinger) { - continue; - } - - $path = str_replace('{uri}', urlencode($uri), $template); - $webfinger = self::webfinger($path, $type); - - if (!$webfinger && (strstr($uri, "@"))) { - $path = str_replace('{uri}', urlencode("acct:".$uri), $template); - $webfinger = self::webfinger($path, $type); - } - - // Special treatment for Mastodon - // Problem is that Mastodon uses an URL format like http://domain.tld/@nick - // But the webfinger for this format fails. - if (!$webfinger && !empty($nick)) { - // Mastodon uses a "@" as prefix for usernames in their url format - $nick = ltrim($nick, '@'); - - $addr = $nick."@".$host; - - $path = str_replace('{uri}', urlencode("acct:".$addr), $template); - $webfinger = self::webfinger($path, $type); - } - } - - if (!is_array($webfinger["links"])) { - Logger::log("No webfinger links found for ".$uri, Logger::DEBUG); - return false; - } - $data = []; foreach ($webfinger["links"] as $link) { @@ -374,12 +330,13 @@ class Probe * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function uri($uri, $network = '', $uid = -1, $cache = true) + public static function uri($uri, $network = '', $uid = -1) { - if ($cache) { - $result = DI::cache()->get('Probe::uri:' . $network . ':' . $uri); - if (!is_null($result)) { - return $result; + // Local profiles aren't probed via network + if (empty($network) && strpos($uri, DI::baseUrl()->getHostname())) { + $data = self::localProbe($uri); + if (!empty($data)) { + return $data; } } @@ -387,25 +344,27 @@ class Probe $uid = local_user(); } - self::$istimeout = false; - - $data = self::detect($uri, $network, $uid); - - // When the previous detection process had got a time out - // we could falsely detect a Friendica profile as AP profile. - if (!self::$istimeout) { + if (empty($network) || ($network == Protocol::ACTIVITYPUB)) { $ap_profile = ActivityPub::probeProfile($uri); + } else { + $ap_profile = []; + } + + self::$istimeout = false; + if ($network != Protocol::ACTIVITYPUB) { + $data = self::detect($uri, $network, $uid, $ap_profile); + if (!is_array($data)) { + $data = []; + } if (empty($data) || (!empty($ap_profile) && empty($network) && (($data['network'] ?? '') != Protocol::DFRN))) { - $subscribe = $data['subscribe']; $data = $ap_profile; - $data['subscribe'] = $subscribe; } elseif (!empty($ap_profile)) { $ap_profile['batch'] = ''; $data = array_merge($ap_profile, $data); } } else { - Logger::notice('Time out detected. AP will not be probed.', ['uri' => $uri]); + $data = $ap_profile; } if (!isset($data['url'])) { @@ -413,7 +372,7 @@ class Probe } if (empty($data['photo'])) { - $data['photo'] = DI::baseUrl() . '/images/person-300.jpg'; + $data['photo'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO; } if (empty($data['name'])) { @@ -434,10 +393,6 @@ class Probe } } - if (!empty(self::$baseurl)) { - $data['baseurl'] = self::$baseurl; - } - if (!empty($data['baseurl']) && empty($data['gsid'])) { $data['gsid'] = GServer::getID($data['baseurl']); } @@ -455,14 +410,7 @@ class Probe $data['hide'] = self::getHideStatus($data['url']); } - $data = self::rearrangeData($data); - - // Only store into the cache if the value seems to be valid - if (!in_array($data['network'], [Protocol::PHANTOM, Protocol::MAIL])) { - DI::cache()->set('Probe::uri:' . $network . ':' . $uri, $data, Duration::DAY); - } - - return $data; + return self::rearrangeData($data); } @@ -475,7 +423,7 @@ class Probe */ private static function getHideStatus($url) { - $curlResult = Network::curl($url); + $curlResult = DI::httpRequest()->get($url); if (!$curlResult->isSuccess()) { return false; } @@ -529,50 +477,6 @@ class Probe return false; } - /** - * Checks if a profile url should be OStatus but only provides partial information - * - * @param array $webfinger Webfinger data - * @param string $lrdd Path template for webfinger request - * @param string $type type - * - * @return array fixed webfinger data - * @throws HTTPException\InternalServerErrorException - */ - private static function fixOStatus($webfinger, $lrdd, $type) - { - if (empty($webfinger['links']) || empty($webfinger['subject'])) { - return $webfinger; - } - - $is_ostatus = false; - $has_key = false; - - foreach ($webfinger['links'] as $link) { - if ($link['rel'] == ActivityNamespace::OSTATUSSUB) { - $is_ostatus = true; - } - if ($link['rel'] == 'magic-public-key') { - $has_key = true; - } - } - - if (!$is_ostatus || $has_key) { - return $webfinger; - } - - $url = Network::switchScheme($webfinger['subject']); - $path = str_replace('{uri}', urlencode($url), $lrdd); - $webfinger2 = self::webfinger($path, $type); - - // Is the new webfinger detectable as OStatus? - if (self::ostatus($webfinger2, true)) { - $webfinger = $webfinger2; - } - - return $webfinger; - } - /** * Fetch the "subscribe" and add it to the result * @@ -587,7 +491,7 @@ class Probe } foreach ($webfinger['links'] as $link) { - if ($link['rel'] === ActivityNamespace::OSTATUSSUB) { + if (!empty($link['template']) && ($link['rel'] === ActivityNamespace::OSTATUSSUB)) { $result['subscribe'] = $link['template']; } } @@ -596,129 +500,244 @@ class Probe } /** - * Fetch information (protocol endpoints and user information) about a given uri - * - * This function is only called by the "uri" function that adds caching and rearranging of data. - * - * @param string $uri Address that should be probed - * @param string $network Test for this specific network - * @param integer $uid User ID for the probe (only used for mails) + * Get webfinger data from a given URI * - * @return array uri data - * @throws HTTPException\InternalServerErrorException + * @param string $uri + * @return array Webfinger array */ - private static function detect($uri, $network, $uid) + private static function getWebfingerArray(string $uri) { $parts = parse_url($uri); - if (!empty($parts["scheme"]) && !empty($parts["host"])) { - $host = $parts["host"]; - if (!empty($parts["port"])) { - $host .= ':'.$parts["port"]; + if (!empty($parts['scheme']) && !empty($parts['host'])) { + $host = $parts['host']; + if (!empty($parts['port'])) { + $host .= ':'.$parts['port']; } - if ($host == 'twitter.com') { - return self::twitter($uri); - } - $lrdd = self::hostMeta($host); + $baseurl = $parts['scheme'] . '://' . $host; - if (is_bool($lrdd)) { - return []; - } + $nick = ''; + $addr = ''; $path_parts = explode("/", trim($parts['path'] ?? '', "/")); + if (!empty($path_parts)) { + $nick = ltrim(end($path_parts), '@'); + // When the last part of the URI is numeric then it is most likely an ID and not a nick name + if (!is_numeric($nick)) { + $addr = $nick."@".$host; + } else { + $nick = ''; + } + } - while (!$lrdd && (sizeof($path_parts) > 1)) { - $host .= "/".array_shift($path_parts); + $webfinger = self::getWebfinger($parts['scheme'] . '://' . $host . self::WEBFINGER, 'application/jrd+json', $uri, $addr); + if (empty($webfinger)) { $lrdd = self::hostMeta($host); } - if (!$lrdd) { - Logger::log('No XRD data was found for '.$uri, Logger::DEBUG); - return self::feed($uri); - } - $nick = array_pop($path_parts); - // Mastodon uses a "@" as prefix for usernames in their url format - $nick = ltrim($nick, '@'); + if (empty($webfinger) && empty($lrdd)) { + while (empty($lrdd) && empty($webfinger) && (sizeof($path_parts) > 1)) { + $host .= "/".array_shift($path_parts); + $baseurl = $parts['scheme'] . '://' . $host; - $addr = $nick."@".$host; - } elseif (strstr($uri, '@')) { - // If the URI starts with "mailto:" then jump directly to the mail detection - if (strpos($uri, 'mailto:') !== false) { - $uri = str_replace('mailto:', '', $uri); - return self::mail($uri, $uid); - } + if (!empty($nick)) { + $addr = $nick."@".$host; + } - if ($network == Protocol::MAIL) { - return self::mail($uri, $uid); + $webfinger = self::getWebfinger($parts['scheme'] . '://' . $host . self::WEBFINGER, 'application/jrd+json', $uri, $addr); + if (empty($webfinger)) { + $lrdd = self::hostMeta($host); + } + } + + if (empty($lrdd) && empty($webfinger)) { + return []; + } } + } elseif (strstr($uri, '@')) { // Remove "acct:" from the URI $uri = str_replace('acct:', '', $uri); $host = substr($uri, strpos($uri, '@') + 1); $nick = substr($uri, 0, strpos($uri, '@')); + $addr = $uri; - if (strpos($uri, '@twitter.com')) { - return self::twitter($uri); + $webfinger = self::getWebfinger('https://' . $host . self::WEBFINGER, 'application/jrd+json', $uri, $addr); + if (self::$istimeout) { + return []; } - $lrdd = self::hostMeta($host); - if (is_bool($lrdd)) { - return []; + if (empty($webfinger)) { + $webfinger = self::getWebfinger('http://' . $host . self::WEBFINGER, 'application/jrd+json', $uri, $addr); + if (self::$istimeout) { + return []; + } + } else { + $baseurl = 'https://' . $host; } - if (!$lrdd) { - Logger::log('No XRD data was found for '.$uri, Logger::DEBUG); - return self::mail($uri, $uid); + if (empty($webfinger)) { + $lrdd = self::hostMeta($host); + if (self::$istimeout) { + return []; + } + $baseurl = self::$baseurl; + } else { + $baseurl = 'http://' . $host; } - $addr = $uri; } else { - Logger::log("Uri ".$uri." was not detectable", Logger::DEBUG); + Logger::info('URI was not detectable', ['uri' => $uri]); return []; } - $webfinger = false; + if (empty($webfinger)) { + foreach ($lrdd as $type => $template) { + if ($webfinger) { + continue; + } - /// @todo Do we need the prefix "acct:" or "acct://"? + $webfinger = self::getWebfinger($template, $type, $uri, $addr); + } + } - foreach ($lrdd as $type => $template) { - if ($webfinger) { - continue; + if (empty($webfinger)) { + return []; + } + + if ($webfinger['detected'] == $addr) { + $webfinger['nick'] = $nick; + $webfinger['addr'] = $addr; + } + + $webfinger['baseurl'] = $baseurl; + + return $webfinger; + } + + /** + * Perform network request for webfinger data + * + * @param string $template + * @param string $type + * @param string $uri + * @param string $addr + * @return array webfinger results + */ + private static function getWebfinger(string $template, string $type, string $uri, string $addr) + { + // First try the address because this is the primary purpose of webfinger + if (!empty($addr)) { + $detected = $addr; + $path = str_replace('{uri}', urlencode("acct:" . $addr), $template); + $webfinger = self::webfinger($path, $type); + if (self::$istimeout) { + return []; } + } - // At first try it with the given uri + // Then try the URI + if (empty($webfinger) && $uri != $addr) { + $detected = $uri; $path = str_replace('{uri}', urlencode($uri), $template); $webfinger = self::webfinger($path, $type); + if (self::$istimeout) { + return []; + } + } + + if (empty($webfinger)) { + return []; + } + + return ['webfinger' => $webfinger, 'detected' => $detected]; + } + + /** + * Fetch information (protocol endpoints and user information) about a given uri + * + * This function is only called by the "uri" function that adds caching and rearranging of data. + * + * @param string $uri Address that should be probed + * @param string $network Test for this specific network + * @param integer $uid User ID for the probe (only used for mails) + * @param array $ap_profile Previously probed AP profile + * + * @return array uri data + * @throws HTTPException\InternalServerErrorException + */ + private static function detect(string $uri, string $network, int $uid, array $ap_profile) + { + $hookData = [ + 'uri' => $uri, + 'network' => $network, + 'uid' => $uid, + 'result' => [], + ]; + + Hook::callAll('probe_detect', $hookData); + + if ($hookData['result']) { + if (!is_array($hookData['result'])) { + return []; + } else { + return $hookData['result']; + } + } - // Fix possible problems with GNU Social probing to wrong scheme - $webfinger = self::fixOStatus($webfinger, $template, $type); + $parts = parse_url($uri); - // We cannot be sure that the detected address was correct, so we don't use the values - if ($webfinger && ($uri != $addr)) { - $nick = ""; - $addr = ""; + if (!empty($parts['scheme']) && !empty($parts['host'])) { + if (in_array($parts['host'], ['twitter.com', 'mobile.twitter.com'])) { + return self::twitter($uri); + } + } elseif (strstr($uri, '@')) { + // If the URI starts with "mailto:" then jump directly to the mail detection + if (strpos($uri, 'mailto:') !== false) { + $uri = str_replace('mailto:', '', $uri); + return self::mail($uri, $uid); } - // Try webfinger with the address (user@domain.tld) - if (!$webfinger) { - $path = str_replace('{uri}', urlencode($addr), $template); - $webfinger = self::webfinger($path, $type); + if ($network == Protocol::MAIL) { + return self::mail($uri, $uid); } - // Mastodon needs to have it with "acct:" - if (!$webfinger) { - $path = str_replace('{uri}', urlencode("acct:".$addr), $template); - $webfinger = self::webfinger($path, $type); + if (Strings::endsWith($uri, '@twitter.com') + || Strings::endsWith($uri, '@mobile.twitter.com') + ) { + return self::twitter($uri); } + } else { + Logger::info('URI was not detectable', ['uri' => $uri]); + return []; } - if (!$webfinger) { - return self::feed($uri); + Logger::info('Probing start', ['uri' => $uri]); + + if (!empty($ap_profile['addr']) && ($ap_profile['addr'] != $uri)) { + $data = self::getWebfingerArray($ap_profile['addr']); } - $result = []; + if (empty($data)) { + $data = self::getWebfingerArray($uri); + } - Logger::info("Probing", ['uri' => $uri]); + if (empty($data)) { + if (!empty($parts['scheme'])) { + return self::feed($uri); + } elseif (!empty($uid)) { + return self::mail($uri, $uid); + } else { + return []; + } + } + + $webfinger = $data['webfinger']; + $nick = $data['nick'] ?? ''; + $addr = $data['addr'] ?? ''; + $baseurl = $data['baseurl'] ?? ''; + + $result = []; if (in_array($network, ["", Protocol::DFRN])) { $result = self::dfrn($webfinger); @@ -730,12 +749,12 @@ class Probe $result = self::ostatus($webfinger); } if (in_array($network, ['', Protocol::ZOT])) { - $result = self::zot($webfinger, $result); + $result = self::zot($webfinger, $result, $baseurl); } if ((!$result && ($network == "")) || ($network == Protocol::PUMPIO)) { $result = self::pumpio($webfinger, $addr); } - if ((!$result && ($network == "")) || ($network == Protocol::FEED)) { + if (empty($result['network']) && empty($ap_profile['network']) || ($network == Protocol::FEED)) { $result = self::feed($uri); } else { // We overwrite the detected nick with our try if the previois routines hadn't detected it. @@ -755,18 +774,15 @@ class Probe $result["network"] = Protocol::PHANTOM; } + if (empty($result['baseurl']) && !empty($baseurl)) { + $result['baseurl'] = $baseurl; + } + if (empty($result["url"])) { $result["url"] = $uri; } - Logger::log($uri." is ".$result["network"], Logger::DEBUG); - - if (empty($result["baseurl"]) && ($result["network"] != Protocol::PHANTOM)) { - $pos = strpos($result["url"], $host); - if ($pos) { - $result["baseurl"] = substr($result["url"], 0, $pos).$host; - } - } + Logger::info('Probing done', ['uri' => $uri, 'network' => $result["network"]]); return $result; } @@ -780,7 +796,7 @@ class Probe * @return array Zot data * @throws HTTPException\InternalServerErrorException */ - private static function zot($webfinger, $data) + private static function zot($webfinger, $data, $baseurl) { if (!empty($webfinger["aliases"]) && is_array($webfinger["aliases"])) { foreach ($webfinger["aliases"] as $alias) { @@ -801,12 +817,12 @@ class Probe } } - if (empty($zot_url) && !empty($data['addr']) && !empty(self::$baseurl)) { - $condition = ['nurl' => Strings::normaliseLink(self::$baseurl), 'platform' => ['hubzilla']]; + if (empty($zot_url) && !empty($data['addr']) && !empty($baseurl)) { + $condition = ['nurl' => Strings::normaliseLink($baseurl), 'platform' => ['hubzilla']]; if (!DBA::exists('gserver', $condition)) { return $data; } - $zot_url = self::$baseurl . '/.well-known/zot-info?address=' . $data['addr']; + $zot_url = $baseurl . '/.well-known/zot-info?address=' . $data['addr']; } if (empty($zot_url)) { @@ -828,7 +844,7 @@ class Probe public static function pollZot($url, $data) { - $curlResult = Network::curl($url); + $curlResult = DI::httpRequest()->get($url); if ($curlResult->isTimeout()) { return $data; } @@ -879,7 +895,7 @@ class Probe } if (!empty($json['public_forum'])) { $data['community'] = $json['public_forum']; - $data['account-type'] = Contact::PAGE_COMMUNITY; + $data['account-type'] = User::PAGE_FLAGS_COMMUNITY; } if (!empty($json['profile'])) { @@ -921,22 +937,22 @@ class Probe * @return array webfinger data * @throws HTTPException\InternalServerErrorException */ - private static function webfinger($url, $type) + public static function webfinger($url, $type) { $xrd_timeout = DI::config()->get('system', 'xrd_timeout', 20); - $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout, 'accept_content' => $type]); + $curlResult = DI::httpRequest()->get($url, false, ['timeout' => $xrd_timeout, 'accept_content' => $type]); if ($curlResult->isTimeout()) { self::$istimeout = true; - return false; + return []; } $data = $curlResult->getBody(); $webfinger = json_decode($data, true); - if (is_array($webfinger)) { + if (!empty($webfinger)) { if (!isset($webfinger["links"])) { - Logger::log("No json webfinger links for ".$url, Logger::DEBUG); - return false; + Logger::info('No json webfinger links', ['url' => $url]); + return []; } return $webfinger; } @@ -944,14 +960,14 @@ class Probe // If it is not JSON, maybe it is XML $xrd = XML::parseString($data, true); if (!is_object($xrd)) { - Logger::log("No webfinger data retrievable for ".$url, Logger::DEBUG); - return false; + Logger::info('No webfinger data retrievable', ['url' => $url]); + return []; } $xrd_arr = XML::elementToArray($xrd); if (!isset($xrd_arr["xrd"]["link"])) { - Logger::log("No XML webfinger links for ".$url, Logger::DEBUG); - return false; + Logger::info('No XML webfinger links', ['url' => $url]); + return []; } $webfinger = []; @@ -994,21 +1010,21 @@ class Probe */ private static function pollNoscrape($noscrape_url, $data) { - $curlResult = Network::curl($noscrape_url); + $curlResult = DI::httpRequest()->get($noscrape_url); if ($curlResult->isTimeout()) { self::$istimeout = true; - return false; + return []; } $content = $curlResult->getBody(); if (!$content) { - Logger::log("Empty body for ".$noscrape_url, Logger::DEBUG); - return false; + Logger::info('Empty body', ['url' => $noscrape_url]); + return []; } $json = json_decode($content, true); if (!is_array($json)) { - Logger::log("No json data for ".$noscrape_url, Logger::DEBUG); - return false; + Logger::info('No json data', ['url' => $noscrape_url]); + return []; } if (!empty($json["fn"])) { @@ -1121,7 +1137,7 @@ class Probe { $data = []; - Logger::log("Check profile ".$profile_link, Logger::DEBUG); + Logger::info('Check profile', ['link' => $profile_link]); // Fetch data via noscrape - this is faster $noscrape_url = str_replace(["/hcard/", "/profile/"], "/noscrape/", $profile_link); @@ -1155,7 +1171,7 @@ class Probe $prof_data["fn"] = $data['name'] ?? null; $prof_data["key"] = $data['pubkey'] ?? null; - Logger::log("Result for profile ".$profile_link.": ".print_r($prof_data, true), Logger::DEBUG); + Logger::debug('Result', ['link' => $profile_link, 'data' => $prof_data]); return $prof_data; } @@ -1218,7 +1234,7 @@ class Probe } if (!isset($data["network"]) || ($hcard_url == "")) { - return false; + return []; } // Fetch data via noscrape - this is faster @@ -1252,26 +1268,26 @@ class Probe */ private static function pollHcard($hcard_url, $data, $dfrn = false) { - $curlResult = Network::curl($hcard_url); + $curlResult = DI::httpRequest()->get($hcard_url); if ($curlResult->isTimeout()) { self::$istimeout = true; - return false; + return []; } $content = $curlResult->getBody(); if (!$content) { - return false; + return []; } $doc = new DOMDocument(); if (!@$doc->loadHTML($content)) { - return false; + return []; } $xpath = new DomXPath($doc); $vcards = $xpath->query("//div[contains(concat(' ', @class, ' '), ' vcard ')]"); if (!is_object($vcards)) { - return false; + return []; } if (!isset($data["baseurl"])) { @@ -1409,7 +1425,7 @@ class Probe } if (empty($data["url"]) || empty($hcard_url)) { - return false; + return []; } if (!empty($webfinger["aliases"]) && is_array($webfinger["aliases"])) { @@ -1430,7 +1446,7 @@ class Probe $data = self::pollHcard($hcard_url, $data); if (!$data) { - return false; + return []; } if (!empty($data["url"]) @@ -1440,6 +1456,7 @@ class Probe && !empty($hcard_url) ) { $data["network"] = Protocol::DIASPORA; + $data["manually-approve"] = false; // The Diaspora handle must always be lowercase if (!empty($data["addr"])) { @@ -1450,7 +1467,7 @@ class Probe $data["notify"] = $data["baseurl"] . "/receive/users/" . $data["guid"]; $data["batch"] = $data["baseurl"] . "/receive/public"; } else { - return false; + return []; } return $data; @@ -1483,7 +1500,7 @@ class Probe $data["addr"] = str_replace('acct:', '', $webfinger["subject"]); } - if (is_array($webfinger["links"])) { + if (!empty($webfinger["links"])) { // The array is reversed to take into account the order of preference for same-rel links // See: https://tools.ietf.org/html/rfc7033#section-4.4.4 foreach (array_reverse($webfinger["links"]) as $link) { @@ -1491,7 +1508,7 @@ class Probe && (($link["type"] ?? "") == "text/html") && ($link["href"] != "") ) { - $data["url"] = $link["href"]; + $data["url"] = $data["alias"] = $link["href"]; } elseif (($link["rel"] == "salmon") && !empty($link["href"])) { $data["notify"] = $link["href"]; } elseif (($link["rel"] == ActivityNamespace::FEED) && !empty($link["href"])) { @@ -1506,10 +1523,10 @@ class Probe $pubkey = substr($pubkey, 5); } } elseif (Strings::normaliseLink($pubkey) == 'http://') { - $curlResult = Network::curl($pubkey); + $curlResult = DI::httpRequest()->get($pubkey); if ($curlResult->isTimeout()) { self::$istimeout = true; - return false; + return $short ? false : []; } $pubkey = $curlResult->getBody(); } @@ -1530,8 +1547,9 @@ class Probe && isset($data["url"]) ) { $data["network"] = Protocol::OSTATUS; + $data["manually-approve"] = false; } else { - return false; + return $short ? false : []; } if ($short) { @@ -1539,15 +1557,15 @@ class Probe } // Fetch all additional data from the feed - $curlResult = Network::curl($data["poll"]); + $curlResult = DI::httpRequest()->get($data["poll"]); if ($curlResult->isTimeout()) { self::$istimeout = true; - return false; + return []; } $feed = $curlResult->getBody(); $feed_data = Feed::import($feed); if (!$feed_data) { - return false; + return []; } if (!empty($feed_data["header"]["author-name"])) { @@ -1574,8 +1592,7 @@ class Probe $data["url"] = $feed_data["header"]["author-link"]; } - if (($data['poll'] == $data['url']) && ($data["alias"] != '')) { - $data['url'] = $data["alias"]; + if ($data["url"] == $data["alias"]) { $data["alias"] = ''; } @@ -1592,14 +1609,14 @@ class Probe */ private static function pumpioProfileData($profile_link) { - $curlResult = Network::curl($profile_link); + $curlResult = DI::httpRequest()->get($profile_link); if (!$curlResult->isSuccess()) { - return false; + return []; } $doc = new DOMDocument(); if (!@$doc->loadHTML($curlResult->getBody())) { - return false; + return []; } $xpath = new DomXPath($doc); @@ -1680,13 +1697,13 @@ class Probe $data["network"] = Protocol::PUMPIO; } else { - return false; + return []; } $profile_data = self::pumpioProfileData($data["url"]); if (!$profile_data) { - return false; + return []; } $data = array_merge($data, $profile_data); @@ -1710,9 +1727,9 @@ class Probe */ private static function twitter($uri) { - if (preg_match('=(.*)@twitter.com=i', $uri, $matches)) { + if (preg_match('=([^@]+)@(?:mobile\.)?twitter\.com$=i', $uri, $matches)) { $nick = $matches[1]; - } elseif (preg_match('=https?://twitter.com/(.*)=i', $uri, $matches)) { + } elseif (preg_match('=^https?://(?:mobile\.)?twitter\.com/(.+)=i', $uri, $matches)) { $nick = $matches[1]; } else { return []; @@ -1725,39 +1742,6 @@ class Probe $data['network'] = Protocol::TWITTER; $data['baseurl'] = 'https://twitter.com'; - $curlResult = Network::curl($data['url'], false); - if (!$curlResult->isSuccess()) { - return []; - } - - $body = $curlResult->getBody(); - $doc = new DOMDocument(); - @$doc->loadHTML($body); - $xpath = new DOMXPath($doc); - - $list = $xpath->query('//img[@class]'); - foreach ($list as $node) { - $img_attr = []; - if ($node->attributes->length) { - foreach ($node->attributes as $attribute) { - $img_attr[$attribute->name] = $attribute->value; - } - } - - if (empty($img_attr['class'])) { - continue; - } - - if (strpos($img_attr['class'], 'ProfileAvatar-image') !== false) { - if (!empty($img_attr['src'])) { - $data['photo'] = $img_attr['src']; - } - if (!empty($img_attr['alt'])) { - $data['name'] = $img_attr['alt']; - } - } - } - return $data; } @@ -1805,6 +1789,9 @@ class Probe $base = $xpath->evaluate('string(/html/head/base/@href)') ?: $base; $baseParts = parse_url($base); + if (empty($baseParts['host'])) { + return $href; + } // Naked domain case (scheme://basehost) $path = $baseParts['path'] ?? '/'; @@ -1856,23 +1843,23 @@ class Probe */ private static function feed($url, $probe = true) { - $curlResult = Network::curl($url); + $curlResult = DI::httpRequest()->get($url); if ($curlResult->isTimeout()) { self::$istimeout = true; - return false; + return []; } $feed = $curlResult->getBody(); $feed_data = Feed::import($feed); if (!$feed_data) { if (!$probe) { - return false; + return []; } $feed_url = self::getFeedLink($url, $feed); if (!$feed_url) { - return false; + return []; } return self::feed($feed_url, false); @@ -1897,12 +1884,6 @@ class Probe $data["url"] = $url; $data["poll"] = $url; - if (!empty($feed_data["header"]["author-link"])) { - $data["baseurl"] = $feed_data["header"]["author-link"]; - } else { - $data["baseurl"] = $data["url"]; - } - $data["network"] = Protocol::FEED; return $data; @@ -1920,11 +1901,11 @@ class Probe private static function mail($uri, $uid) { if (!Network::isEmailDomainValid($uri)) { - return false; + return []; } if ($uid == 0) { - return false; + return []; } $user = DBA::selectFirst('user', ['prvkey'], ['uid' => $uid]); @@ -1934,7 +1915,7 @@ class Probe $mailacct = DBA::selectFirst('mailacct', $fields, $condition); if (!DBA::isResult($user) || !DBA::isResult($mailacct)) { - return false; + return []; } $mailbox = Email::constructMailboxName($mailacct); @@ -1942,14 +1923,14 @@ class Probe openssl_private_decrypt(hex2bin($mailacct['pass']), $password, $user['prvkey']); $mbox = Email::connect($mailbox, $mailacct['user'], $password); if (!$mbox) { - return false; + return []; } $msgs = Email::poll($mbox, $uri); - Logger::log('searching '.$uri.', '.count($msgs).' messages found.', Logger::DEBUG); + Logger::info('Messages found', ['uri' => $uri, 'count' => count($msgs)]); if (!count($msgs)) { - return false; + return []; } $phost = substr($uri, strpos($uri, '@') + 1); @@ -2029,8 +2010,220 @@ class Probe $fixed = $scheme.$host.$port.$path.$query.$fragment; - Logger::log('Base: '.$base.' - Avatar: '.$avatar.' - Fixed: '.$fixed, Logger::DATA); + Logger::debug('Avatar fixed', ['base' => $base, 'avatar' => $avatar, 'fixed' => $fixed]); return $fixed; } + + /** + * Fetch the last date that the contact had posted something (publically) + * + * @param string $data probing result + * @return string last activity + */ + public static function getLastUpdate(array $data) + { + $uid = User::getIdForURL($data['url']); + if (!empty($uid)) { + $contact = Contact::selectFirst(['url', 'last-item'], ['self' => true, 'uid' => $uid]); + if (!empty($contact['last-item'])) { + return $contact['last-item']; + } + } + + if ($lastUpdate = self::updateFromNoScrape($data)) { + return $lastUpdate; + } + + if (!empty($data['outbox'])) { + return self::updateFromOutbox($data['outbox'], $data); + } elseif (!empty($data['poll']) && ($data['network'] == Protocol::ACTIVITYPUB)) { + return self::updateFromOutbox($data['poll'], $data); + } elseif (!empty($data['poll'])) { + return self::updateFromFeed($data); + } + + return ''; + } + + /** + * Fetch the last activity date from the "noscrape" endpoint + * + * @param array $data Probing result + * @return string last activity + * + * @return bool 'true' if update was successful or the server was unreachable + */ + private static function updateFromNoScrape(array $data) + { + if (empty($data['baseurl'])) { + return ''; + } + + // Check the 'noscrape' endpoint when it is a Friendica server + $gserver = DBA::selectFirst('gserver', ['noscrape'], ["`nurl` = ? AND `noscrape` != ''", + Strings::normaliseLink($data['baseurl'])]); + if (!DBA::isResult($gserver)) { + return ''; + } + + $curlResult = DI::httpRequest()->get($gserver['noscrape'] . '/' . $data['nick']); + + if ($curlResult->isSuccess() && !empty($curlResult->getBody())) { + $noscrape = json_decode($curlResult->getBody(), true); + if (!empty($noscrape) && !empty($noscrape['updated'])) { + return DateTimeFormat::utc($noscrape['updated'], DateTimeFormat::MYSQL); + } + } + + return ''; + } + + /** + * Fetch the last activity date from an ActivityPub Outbox + * + * @param string $feed + * @param array $data Probing result + * @return string last activity + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + private static function updateFromOutbox(string $feed, array $data) + { + $outbox = ActivityPub::fetchContent($feed); + if (empty($outbox)) { + return ''; + } + + if (!empty($outbox['orderedItems'])) { + $items = $outbox['orderedItems']; + } elseif (!empty($outbox['first']['orderedItems'])) { + $items = $outbox['first']['orderedItems']; + } elseif (!empty($outbox['first']['href']) && ($outbox['first']['href'] != $feed)) { + return self::updateFromOutbox($outbox['first']['href'], $data); + } elseif (!empty($outbox['first'])) { + if (is_string($outbox['first']) && ($outbox['first'] != $feed)) { + return self::updateFromOutbox($outbox['first'], $data); + } else { + Logger::warning('Unexpected data', ['outbox' => $outbox]); + } + return ''; + } else { + $items = []; + } + + $last_updated = ''; + foreach ($items as $activity) { + if (!empty($activity['published'])) { + $published = DateTimeFormat::utc($activity['published']); + } elseif (!empty($activity['object']['published'])) { + $published = DateTimeFormat::utc($activity['object']['published']); + } else { + continue; + } + + if ($last_updated < $published) { + $last_updated = $published; + } + } + + if (!empty($last_updated)) { + return $last_updated; + } + + return ''; + } + + /** + * Fetch the last activity date from an XML feed + * + * @param array $data Probing result + * @return string last activity + */ + private static function updateFromFeed(array $data) + { + // Search for the newest entry in the feed + $curlResult = DI::httpRequest()->get($data['poll']); + if (!$curlResult->isSuccess()) { + return ''; + } + + $doc = new DOMDocument(); + @$doc->loadXML($curlResult->getBody()); + + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); + + $entries = $xpath->query('/atom:feed/atom:entry'); + + $last_updated = ''; + + foreach ($entries as $entry) { + $published_item = $xpath->query('atom:published/text()', $entry)->item(0); + $updated_item = $xpath->query('atom:updated/text()' , $entry)->item(0); + $published = !empty($published_item->nodeValue) ? DateTimeFormat::utc($published_item->nodeValue) : null; + $updated = !empty($updated_item->nodeValue) ? DateTimeFormat::utc($updated_item->nodeValue) : null; + + if (empty($published) || empty($updated)) { + Logger::notice('Invalid entry for XPath.', ['entry' => $entry, 'url' => $data['url']]); + continue; + } + + if ($last_updated < $published) { + $last_updated = $published; + } + + if ($last_updated < $updated) { + $last_updated = $updated; + } + } + + if (!empty($last_updated)) { + return $last_updated; + } + + return ''; + } + + /** + * Probe data from local profiles without network traffic + * + * @param string $url + * @return array probed data + */ + private static function localProbe(string $url) + { + $uid = User::getIdForURL($url); + if (empty($uid)) { + return []; + } + + $profile = User::getOwnerDataById($uid); + if (empty($profile)) { + return []; + } + + $approfile = ActivityPub\Transmitter::getProfile($uid); + if (empty($approfile)) { + return []; + } + + if (empty($profile['gsid'])) { + $profile['gsid'] = GServer::getID($approfile['generator']['url']); + } + + $data = ['name' => $profile['name'], 'nick' => $profile['nick'], 'guid' => $approfile['diaspora:guid'] ?? '', + 'url' => $profile['url'], 'addr' => $profile['addr'], 'alias' => $profile['alias'], + 'photo' => $profile['photo'], 'account-type' => $profile['contact-type'], + 'community' => ($profile['contact-type'] == User::ACCOUNT_TYPE_COMMUNITY), + 'keywords' => $profile['keywords'], 'location' => $profile['location'], 'about' => $profile['about'], + 'hide' => !$profile['net-publish'], 'batch' => '', 'notify' => $profile['notify'], + 'poll' => $profile['poll'], 'request' => $profile['request'], 'confirm' => $profile['confirm'], + 'subscribe' => $approfile['generator']['url'] . '/follow?url={uri}', 'poco' => $profile['poco'], + 'following' => $approfile['following'], 'followers' => $approfile['followers'], + 'inbox' => $approfile['inbox'], 'outbox' => $approfile['outbox'], + 'sharedinbox' => $approfile['endpoints']['sharedInbox'], 'network' => Protocol::DFRN, + 'pubkey' => $profile['upubkey'], 'baseurl' => $approfile['generator']['url'], 'gsid' => $profile['gsid'], + 'manually-approve' => in_array($profile['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP])]; + return self::rearrangeData($data); + } }