X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;ds=sidebyside;f=src%2FModel%2FGServer.php;h=d759a24dff88f44adf9a6404cfccce754490866b;hb=5f9edf32d2bb2e7b8ad082d3df6b57fc04514cbb;hp=e665df35c08a03f12dd38c877cbd150a05eaf29c;hpb=200908032710835c60bbbfb207d511a912c8354a;p=friendica.git diff --git a/src/Model/GServer.php b/src/Model/GServer.php index e665df35c0..d759a24dff 100644 --- a/src/Model/GServer.php +++ b/src/Model/GServer.php @@ -2,7 +2,7 @@ /** * @file src/Model/GServer.php - * @brief This file includes the GServer class to handle with servers + * This file includes the GServer class to handle with servers */ namespace Friendica\Model; @@ -17,21 +17,100 @@ use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; use Friendica\Util\XML; use Friendica\Core\Logger; +use Friendica\Protocol\PortableContact; +use Friendica\Protocol\Diaspora; +use Friendica\Network\Probe; /** - * @brief This class handles GServer related functions + * This class handles GServer related functions */ class GServer { + /** + * Checks if the given server is reachable + * + * @param string $profile URL of the given profile + * @param string $server URL of the given server (If empty, taken from profile) + * @param string $network Network value that is used, when detection failed + * @param boolean $force Force an update. + * + * @return boolean 'true' if server seems vital + */ + public static function reachable(string $profile, string $server = '', string $network = '', bool $force = false) + { + if ($server == '') { + $server = Contact::getBasepath($profile); + } + + if ($server == '') { + return true; + } + + return self::check($server, $network, $force); + } + + /** + * Checks the state of the given server. + * + * @param string $server_url URL of the given server + * @param string $network Network value that is used, when detection failed + * @param boolean $force Force an update. + * + * @return boolean 'true' if server seems vital + */ + public static function check(string $server_url, string $network = '', bool $force = false) + { + // Unify the server address + $server_url = trim($server_url, '/'); + $server_url = str_replace('/index.php', '', $server_url); + + if ($server_url == '') { + return false; + } + + $gserver = DBA::selectFirst('gserver', [], ['nurl' => Strings::normaliseLink($server_url)]); + if (DBA::isResult($gserver)) { + if ($gserver['created'] <= DBA::NULL_DATETIME) { + $fields = ['created' => DateTimeFormat::utcNow()]; + $condition = ['nurl' => Strings::normaliseLink($server_url)]; + DBA::update('gserver', $fields, $condition); + } + + $last_contact = $gserver['last_contact']; + $last_failure = $gserver['last_failure']; + + // See discussion under https://forum.friendi.ca/display/0b6b25a8135aabc37a5a0f5684081633 + // It can happen that a zero date is in the database, but storing it again is forbidden. + if ($last_contact < DBA::NULL_DATETIME) { + $last_contact = DBA::NULL_DATETIME; + } + + if ($last_failure < DBA::NULL_DATETIME) { + $last_failure = DBA::NULL_DATETIME; + } + + if (!$force && !PortableContact::updateNeeded($gserver['created'], '', $last_failure, $last_contact)) { + Logger::info('No update needed', ['server' => $server_url]); + return ($last_contact >= $last_failure); + } + Logger::info('Server is outdated. Start discovery.', ['Server' => $server_url, 'Force' => $force, 'Created' => $gserver['created'], 'Failure' => $last_failure, 'Contact' => $last_contact]); + } else { + Logger::info('Server is unknown. Start discovery.', ['Server' => $server_url]); + } + + return self::detect($server_url, $network); + } + /** * Detect server data (type, protocol, version number, ...) * The detected data is then updated or inserted in the gserver table. * - * @param string $url Server url + * @param string $url URL of the given server + * @param string $network Network value that is used, when detection failed * * @return boolean 'true' if server could be detected */ - public static function detect($url) + public static function detect(string $url, string $network = '') { $serverdata = []; @@ -51,27 +130,38 @@ class GServer } // If that didn't work out well, we use some protocol specific endpoints - if (empty($nodeinfo) || empty($nodeinfo['network']) || ($nodeinfo['network'] == Protocol::DFRN)) { + // For Friendica and Zot based networks we have to dive deeper to reveal more details + if (empty($nodeinfo['network']) || in_array($nodeinfo['network'], [Protocol::DFRN, Protocol::ZOT])) { // Fetch the landing page, possibly it reveals some data - $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout]); - if ($curlResult->isSuccess()) { - $serverdata = self::analyseRootHeader($curlResult, $serverdata); - $serverdata = self::analyseRootBody($curlResult, $serverdata); + if (empty($nodeinfo['network'])) { + $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout]); + if ($curlResult->isSuccess()) { + $serverdata = self::analyseRootHeader($curlResult, $serverdata); + $serverdata = self::analyseRootBody($curlResult, $serverdata, $url); + } + + if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { + DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]); + return false; + } + } + + if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ACTIVITYPUB)) { + $serverdata = self::detectMastodonAlikes($url, $serverdata); } - if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { + // All following checks are done for systems that always have got a "host-meta" endpoint. + // With this check we don't have to waste time and ressources for dead systems. + // Also this hopefully prevents us from receiving abuse messages. + if (empty($serverdata['network']) && !self::validHostMeta($url)) { DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]); return false; } - if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::DFRN)) { + if (empty($serverdata['network']) || in_array($serverdata['network'], [Protocol::DFRN, Protocol::ACTIVITYPUB])) { $serverdata = self::detectFriendica($url, $serverdata); } - if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ACTIVITYPUB)) { - $serverdata = self::detectMastodonAlikes($url, $serverdata); - } - // the 'siteinfo.json' is some specific endpoint of Hubzilla and Red if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ZOT)) { $serverdata = self::fetchSiteinfo($url, $serverdata); @@ -100,12 +190,9 @@ class GServer $serverdata['network'] = Protocol::PHANTOM; } - // Check host-meta for phantom networks. - // Although this is not needed, it is a good indicator for a living system, - // since most systems had implemented it. - if (($serverdata['network'] == Protocol::PHANTOM) && !self::validHostMeta($url)) { - DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]); - return false; + // When we hadn't been able to detect the network type, we use the hint from the parameter + if (($serverdata['network'] == Protocol::PHANTOM) && !empty($network)) { + $serverdata['network'] = $network; } $serverdata['url'] = $url; @@ -119,24 +206,119 @@ class GServer $registeredUsers = 1; } - $gcontacts = DBA::count('gcontact', ['server_url' => [$url, $serverdata['nurl']]]); - $apcontacts = DBA::count('apcontact', ['baseurl' => [$url, $serverdata['nurl']]]); - $contacts = DBA::count('contact', ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]); - $serverdata['registered-users'] = max($gcontacts, $apcontacts, $contacts, $registeredUsers); + if ($serverdata['network'] != Protocol::PHANTOM) { + $gcontacts = DBA::count('gcontact', ['server_url' => [$url, $serverdata['nurl']]]); + $apcontacts = DBA::count('apcontact', ['baseurl' => [$url, $serverdata['nurl']]]); + $contacts = DBA::count('contact', ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]); + $serverdata['registered-users'] = max($gcontacts, $apcontacts, $contacts, $registeredUsers); + } else { + $serverdata['registered-users'] = $registeredUsers; + $serverdata = self::detectNetworkViaContacts($url, $serverdata); + } $serverdata['last_contact'] = DateTimeFormat::utcNow(); - if (!DBA::exists('gserver', ['nurl' => Strings::normaliseLink($url)])) { + $gserver = DBA::selectFirst('gserver', ['network'], ['nurl' => Strings::normaliseLink($url)]); + if (!DBA::isResult($gserver)) { $serverdata['created'] = DateTimeFormat::utcNow(); $ret = DBA::insert('gserver', $serverdata); } else { + // Don't override the network with 'unknown' when there had been a valid entry before + if (($serverdata['network'] == Protocol::PHANTOM) && !empty($gserver['network'])) { + unset($serverdata['network']); + } + $ret = DBA::update('gserver', $serverdata, ['nurl' => $serverdata['nurl']]); } + if (!empty($serverdata['network']) && in_array($serverdata['network'], [Protocol::DFRN, Protocol::DIASPORA])) { + self::discoverRelay($url); + } + return $ret; } - private static function fetchStatistics($url) + /** + * Fetch relay data from a given server url + * + * @param string $server_url address of the server + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + private static function discoverRelay(string $server_url) + { + Logger::info('Discover relay data', ['server' => $server_url]); + + $curlResult = Network::curl($server_url . '/.well-known/x-social-relay'); + if (!$curlResult->isSuccess()) { + return; + } + + $data = json_decode($curlResult->getBody(), true); + if (!is_array($data)) { + return; + } + + $gserver = DBA::selectFirst('gserver', ['id', 'relay-subscribe', 'relay-scope'], ['nurl' => Strings::normaliseLink($server_url)]); + if (!DBA::isResult($gserver)) { + return; + } + + if (($gserver['relay-subscribe'] != $data['subscribe']) || ($gserver['relay-scope'] != $data['scope'])) { + $fields = ['relay-subscribe' => $data['subscribe'], 'relay-scope' => $data['scope']]; + DBA::update('gserver', $fields, ['id' => $gserver['id']]); + } + + DBA::delete('gserver-tag', ['gserver-id' => $gserver['id']]); + + if ($data['scope'] == 'tags') { + // Avoid duplicates + $tags = []; + foreach ($data['tags'] as $tag) { + $tag = mb_strtolower($tag); + if (strlen($tag) < 100) { + $tags[$tag] = $tag; + } + } + + foreach ($tags as $tag) { + DBA::insert('gserver-tag', ['gserver-id' => $gserver['id'], 'tag' => $tag], true); + } + } + + // Create or update the relay contact + $fields = []; + if (isset($data['protocols'])) { + if (isset($data['protocols']['diaspora'])) { + $fields['network'] = Protocol::DIASPORA; + + if (isset($data['protocols']['diaspora']['receive'])) { + $fields['batch'] = $data['protocols']['diaspora']['receive']; + } elseif (is_string($data['protocols']['diaspora'])) { + $fields['batch'] = $data['protocols']['diaspora']; + } + } + + if (isset($data['protocols']['dfrn'])) { + $fields['network'] = Protocol::DFRN; + + if (isset($data['protocols']['dfrn']['receive'])) { + $fields['batch'] = $data['protocols']['dfrn']['receive']; + } elseif (is_string($data['protocols']['dfrn'])) { + $fields['batch'] = $data['protocols']['dfrn']; + } + } + } + Diaspora::setRelayContact($server_url, $fields); + } + + /** + * Fetch server data from '/statistics.json' on the given server + * + * @param string $url URL of the given server + * + * @return array server data + */ + private static function fetchStatistics(string $url) { $curlResult = Network::curl($url . '/statistics.json'); if (!$curlResult->isSuccess()) { @@ -186,13 +368,13 @@ class GServer } /** - * @brief Detect server type by using the nodeinfo data + * Detect server type by using the nodeinfo data * * @param string $url address of the server * @return array Server data * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function fetchNodeinfo($url, $curlResult) + private static function fetchNodeinfo(string $url, $curlResult) { $nodeinfo = json_decode($curlResult->getBody(), true); @@ -235,24 +417,24 @@ class GServer } /** - * @brief Parses Nodeinfo 1 + * Parses Nodeinfo 1 * * @param string $nodeinfo_url address of the nodeinfo path * @return array Server data * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function parseNodeinfo1($nodeinfo_url) + private static function parseNodeinfo1(string $nodeinfo_url) { $curlResult = Network::curl($nodeinfo_url); if (!$curlResult->isSuccess()) { - return false; + return []; } $nodeinfo = json_decode($curlResult->getBody(), true); if (!is_array($nodeinfo)) { - return false; + return []; } $server = []; @@ -305,31 +487,31 @@ class GServer } } - if (!$server) { - return false; + if (empty($server)) { + return []; } return $server; } /** - * @brief Parses Nodeinfo 2 + * Parses Nodeinfo 2 * * @param string $nodeinfo_url address of the nodeinfo path * @return array Server data * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function parseNodeinfo2($nodeinfo_url) + private static function parseNodeinfo2(string $nodeinfo_url) { $curlResult = Network::curl($nodeinfo_url); if (!$curlResult->isSuccess()) { - return false; + return []; } $nodeinfo = json_decode($curlResult->getBody(), true); if (!is_array($nodeinfo)) { - return false; + return []; } $server = []; @@ -383,13 +565,21 @@ class GServer } if (empty($server)) { - return false; + return []; } return $server; } - private static function fetchSiteinfo($url, $serverdata) + /** + * Fetch server information from a 'siteinfo.json' file on the given server + * + * @param string $url URL of the given server + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function fetchSiteinfo(string $url, array $serverdata) { $curlResult = Network::curl($url . '/siteinfo.json'); if (!$curlResult->isSuccess()) { @@ -446,7 +636,14 @@ class GServer return $serverdata; } - private static function validHostMeta($url) + /** + * Checks if the server contains a valid host meta file + * + * @param string $url URL of the given server + * + * @return boolean 'true' if the server seems to be vital + */ + private static function validHostMeta(string $url) { $xrd_timeout = Config::get('system', 'xrd_timeout'); $curlResult = Network::curl($url . '/.well-known/host-meta', false, ['timeout' => $xrd_timeout]); @@ -466,11 +663,16 @@ class GServer $valid = false; foreach ($elements['xrd']['link'] as $link) { - if (empty($link['rel']) || empty($link['type']) || empty($link['template'])) { + // When there is more than a single "link" element, the array looks slightly different + if (!empty($link['@attributes'])) { + $link = $link['@attributes']; + } + + if (empty($link['rel']) || empty($link['template'])) { continue; } - if ($link['type'] == 'application/xrd+xml') { + if ($link['rel'] == 'lrdd') { // When the webfinger host is the same like the system host, it should be ok. $valid = (parse_url($url, PHP_URL_HOST) == parse_url($link['template'], PHP_URL_HOST)); } @@ -479,7 +681,64 @@ class GServer return $valid; } - private static function checkPoCo($url, $serverdata) + /** + * Detect the network of the given server via their known contacts + * + * @param string $url URL of the given server + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function detectNetworkViaContacts(string $url, array $serverdata) + { + $contacts = []; + + $gcontacts = DBA::select('gcontact', ['url', 'nurl'], ['server_url' => [$url, $serverdata['nurl']]]); + while ($gcontact = DBA::fetch($gcontacts)) { + $contacts[$gcontact['nurl']] = $gcontact['url']; + } + DBA::close($gcontacts); + + $apcontacts = DBA::select('apcontact', ['url'], ['baseurl' => [$url, $serverdata['nurl']]]); + while ($gcontact = DBA::fetch($gcontacts)) { + $contacts[Strings::normaliseLink($apcontact['url'])] = $apcontact['url']; + } + DBA::close($apcontacts); + + $pcontacts = DBA::select('contact', ['url', 'nurl'], ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]); + while ($gcontact = DBA::fetch($gcontacts)) { + $contacts[$pcontact['nurl']] = $pcontact['url']; + } + DBA::close($pcontacts); + + if (empty($contacts)) { + return $serverdata; + } + + foreach ($contacts as $contact) { + $probed = Probe::uri($contact); + if (in_array($probed['network'], Protocol::FEDERATED)) { + $serverdata['network'] = $probed['network']; + break; + } + } + + $serverdata['registered-users'] = max($serverdata['registered-users'], count($contacts)); + + return $serverdata; + } + + /** + * Checks if the given server does have a '/poco' endpoint. + * This is used for the 'PortableContact' functionality, + * which is used by both Friendica and Hubzilla. + * + * @param string $url URL of the given server + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function checkPoCo(string $url, array $serverdata) { $curlResult = Network::curl($url. '/poco'); if (!$curlResult->isSuccess()) { @@ -502,7 +761,15 @@ class GServer return $serverdata; } - private static function detectNextcloud($url, $serverdata) + /** + * Detects the version number of a given server when it was a NextCloud installation + * + * @param string $url URL of the given server + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function detectNextcloud(string $url, array $serverdata) { $curlResult = Network::curl($url . '/status.php'); @@ -524,7 +791,15 @@ class GServer return $serverdata; } - private static function detectMastodonAlikes($url, $serverdata) + /** + * Detects data from a given server url if it was a mastodon alike system + * + * @param string $url URL of the given server + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function detectMastodonAlikes(string $url, array $serverdata) { $curlResult = Network::curl($url . '/api/v1/instance'); @@ -539,7 +814,7 @@ class GServer if (!empty($data['version'])) { $serverdata['platform'] = 'mastodon'; - $serverdata['version'] = defaults($data, 'version', ''); + $serverdata['version'] = $data['version'] ?? ''; $serverdata['network'] = Protocol::ACTIVITYPUB; } @@ -568,7 +843,15 @@ class GServer return $serverdata; } - private static function detectHubzilla($url, $serverdata) + /** + * Detects data from typical Hubzilla endpoints + * + * @param string $url URL of the given server + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function detectHubzilla(string $url, array $serverdata) { $curlResult = Network::curl($url . '/api/statusnet/config.json'); if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) { @@ -634,6 +917,13 @@ class GServer return $serverdata; } + /** + * Converts input value to a boolean value + * + * @param string|integer $val + * + * @return boolean + */ private static function toBoolean($val) { if (($val == 'true') || ($val == 1)) { @@ -645,27 +935,35 @@ class GServer return $val; } - private static function detectGNUSocial($url, $serverdata) + /** + * Detect if the URL belongs to a GNU Social server + * + * @param string $url URL of the given server + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function detectGNUSocial(string $url, array $serverdata) { - $curlResult = Network::curl($url . '/api/statusnet/version.json'); - + // Test for GNU Social + $curlResult = Network::curl($url . '/api/gnusocial/version.json'); if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') && ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) { - $serverdata['platform'] = 'StatusNet'; + $serverdata['platform'] = 'gnusocial'; // Remove junk that some GNU Social servers return - $serverdata['version'] = str_replace(chr(239).chr(187).chr(191), '', $curlResult->getBody()); + $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBody()); $serverdata['version'] = trim($serverdata['version'], '"'); $serverdata['network'] = Protocol::OSTATUS; + return $serverdata; } - // Test for GNU Social - $curlResult = Network::curl($url . '/api/gnusocial/version.json'); - + // Test for Statusnet + $curlResult = Network::curl($url . '/api/statusnet/version.json'); if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') && ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) { - $serverdata['platform'] = 'GNU Social'; + $serverdata['platform'] = 'statusnet'; // Remove junk that some GNU Social servers return - $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBody()); + $serverdata['version'] = str_replace(chr(239).chr(187).chr(191), '', $curlResult->getBody()); $serverdata['version'] = trim($serverdata['version'], '"'); $serverdata['network'] = Protocol::OSTATUS; } @@ -673,7 +971,15 @@ class GServer return $serverdata; } - private static function detectFriendica($url, $serverdata) + /** + * Detect if the URL belongs to a Friendica server + * + * @param string $url URL of the given server + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function detectFriendica(string $url, array $serverdata) { $curlResult = Network::curl($url . '/friendica/json'); if (!$curlResult->isSuccess()) { @@ -704,7 +1010,7 @@ class GServer $serverdata['info'] = trim($data['info']); } - $register_policy = defaults($data, 'register_policy', 'REGISTER_CLOSED'); + $register_policy = ($data['register_policy'] ?? '') ?: 'REGISTER_CLOSED'; switch ($register_policy) { case 'REGISTER_OPEN': $serverdata['register_policy'] = Register::OPEN; @@ -724,12 +1030,21 @@ class GServer break; } - $serverdata['platform'] = defaults($data, 'platform', ''); + $serverdata['platform'] = $data['platform'] ?? ''; return $serverdata; } - private static function analyseRootBody($curlResult, $serverdata) + /** + * Analyses the landing page of a given server for hints about type and system of that server + * + * @param object $curlResult result of curl execution + * @param array $serverdata array with server data + * @param string $url Server URL + * + * @return array server data + */ + private static function analyseRootBody($curlResult, array $serverdata, string $url) { $doc = new DOMDocument(); @$doc->loadHTML($curlResult->getBody()); @@ -779,7 +1094,13 @@ class GServer if (in_array($version_part[0], ['WordPress'])) { $serverdata['platform'] = $version_part[0]; $serverdata['version'] = $version_part[1]; - $serverdata['network'] = Protocol::ACTIVITYPUB; + + // We still do need a reliable test if some AP plugin is activated + if (DBA::exists('apcontact', ['baseurl' => $url])) { + $serverdata['network'] = Protocol::ACTIVITYPUB; + } else { + $serverdata['network'] = Protocol::FEED; + } } if (in_array($version_part[0], ['Friendika', 'Friendica'])) { $serverdata['platform'] = $version_part[0]; @@ -838,7 +1159,15 @@ class GServer return $serverdata; } - private static function analyseRootHeader($curlResult, $serverdata) + /** + * Analyses the header data of a given server for hints about type and system of that server + * + * @param object $curlResult result of curl execution + * @param array $serverdata array with server data + * + * @return array server data + */ + private static function analyseRootHeader($curlResult, array $serverdata) { if ($curlResult->getHeader('server') == 'Mastodon') { $serverdata['platform'] = 'mastodon';