3 * @copyright Copyright (C) 2020, Friendica
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Model;
26 use Friendica\Core\Protocol;
27 use Friendica\Core\Worker;
28 use Friendica\Database\DBA;
30 use Friendica\Module\Register;
31 use Friendica\Network\CurlResult;
32 use Friendica\Util\Network;
33 use Friendica\Util\DateTimeFormat;
34 use Friendica\Util\Strings;
35 use Friendica\Util\XML;
36 use Friendica\Core\Logger;
37 use Friendica\Core\System;
38 use Friendica\Protocol\PortableContact;
39 use Friendica\Protocol\Diaspora;
40 use Friendica\Network\Probe;
43 * This class handles GServer related functions
50 const DT_MASTODON = 2;
52 // Methods to detect server types
54 // Non endpoint specific methods
55 const DETECT_MANUAL = 0;
56 const DETECT_HEADER = 1;
57 const DETECT_BODY = 2;
59 // Implementation specific endpoints
60 const DETECT_FRIENDIKA = 10;
61 const DETECT_FRIENDICA = 11;
62 const DETECT_STATUSNET = 12;
63 const DETECT_GNUSOCIAL = 13;
64 const DETECT_CONFIG_JSON = 14; // Statusnet, GNU Social, Older Hubzilla/Redmatrix
65 const DETECT_SITEINFO_JSON = 15; // Newer Hubzilla
66 const DETECT_MASTODON_API = 16;
67 const DETECT_STATUS_PHP = 17; // Nextcloud
69 // Standardized endpoints
70 const DETECT_STATISTICS_JSON = 100;
71 const DETECT_NODEINFO_1 = 101;
72 const DETECT_NODEINFO_2 = 102;
75 * Get the ID for the given server URL
78 * @param boolean $no_check Don't check if the server hadn't been found
79 * @return int gserver id
81 public static function getID(string $url, bool $no_check = false)
87 $url = self::cleanURL($url);
89 $gserver = DBA::selectFirst('gserver', ['id'], ['nurl' => Strings::normaliseLink($url)]);
90 if (DBA::isResult($gserver)) {
91 Logger::info('Got ID for URL', ['id' => $gserver['id'], 'url' => $url, 'callstack' => System::callstack(20)]);
92 return $gserver['id'];
95 if ($no_check || !self::check($url)) {
99 return self::getID($url, true);
103 * Checks if the given server is reachable
105 * @param string $profile URL of the given profile
106 * @param string $server URL of the given server (If empty, taken from profile)
107 * @param string $network Network value that is used, when detection failed
108 * @param boolean $force Force an update.
110 * @return boolean 'true' if server seems vital
112 public static function reachable(string $profile, string $server = '', string $network = '', bool $force = false)
115 $server = GContact::getBasepath($profile);
122 return self::check($server, $network, $force);
126 * Decides if a server needs to be updated, based upon several date fields
128 * @param date $created Creation date of that server entry
129 * @param date $updated When had the server entry be updated
130 * @param date $last_failure Last failure when contacting that server
131 * @param date $last_contact Last time the server had been contacted
133 * @return boolean Does the server record needs an update?
135 public static function updateNeeded($created, $updated, $last_failure, $last_contact)
137 $now = strtotime(DateTimeFormat::utcNow());
139 if ($updated > $last_contact) {
140 $contact_time = strtotime($updated);
142 $contact_time = strtotime($last_contact);
145 $failure_time = strtotime($last_failure);
146 $created_time = strtotime($created);
148 // If there is no "created" time then use the current time
149 if ($created_time <= 0) {
150 $created_time = $now;
153 // If the last contact was less than 24 hours then don't update
154 if (($now - $contact_time) < (60 * 60 * 24)) {
158 // If the last failure was less than 24 hours then don't update
159 if (($now - $failure_time) < (60 * 60 * 24)) {
163 // If the last contact was less than a week ago and the last failure is older than a week then don't update
164 //if ((($now - $contact_time) < (60 * 60 * 24 * 7)) && ($contact_time > $failure_time))
167 // If the last contact time was more than a week ago and the contact was created more than a week ago, then only try once a week
168 if ((($now - $contact_time) > (60 * 60 * 24 * 7)) && (($now - $created_time) > (60 * 60 * 24 * 7)) && (($now - $failure_time) < (60 * 60 * 24 * 7))) {
172 // If the last contact time was more than a month ago and the contact was created more than a month ago, then only try once a month
173 if ((($now - $contact_time) > (60 * 60 * 24 * 30)) && (($now - $created_time) > (60 * 60 * 24 * 30)) && (($now - $failure_time) < (60 * 60 * 24 * 30))) {
181 * Checks the state of the given server.
183 * @param string $server_url URL of the given server
184 * @param string $network Network value that is used, when detection failed
185 * @param boolean $force Force an update.
186 * @param boolean $only_nodeinfo Only use nodeinfo for server detection
188 * @return boolean 'true' if server seems vital
190 public static function check(string $server_url, string $network = '', bool $force = false, bool $only_nodeinfo = false)
192 $server_url = self::cleanURL($server_url);
194 if ($server_url == '') {
198 $gserver = DBA::selectFirst('gserver', [], ['nurl' => Strings::normaliseLink($server_url)]);
199 if (DBA::isResult($gserver)) {
200 if ($gserver['created'] <= DBA::NULL_DATETIME) {
201 $fields = ['created' => DateTimeFormat::utcNow()];
202 $condition = ['nurl' => Strings::normaliseLink($server_url)];
203 DBA::update('gserver', $fields, $condition);
206 $last_contact = $gserver['last_contact'];
207 $last_failure = $gserver['last_failure'];
209 // See discussion under https://forum.friendi.ca/display/0b6b25a8135aabc37a5a0f5684081633
210 // It can happen that a zero date is in the database, but storing it again is forbidden.
211 if ($last_contact < DBA::NULL_DATETIME) {
212 $last_contact = DBA::NULL_DATETIME;
215 if ($last_failure < DBA::NULL_DATETIME) {
216 $last_failure = DBA::NULL_DATETIME;
219 if (!$force && !self::updateNeeded($gserver['created'], '', $last_failure, $last_contact)) {
220 Logger::info('No update needed', ['server' => $server_url]);
221 return ($last_contact >= $last_failure);
223 Logger::info('Server is outdated. Start discovery.', ['Server' => $server_url, 'Force' => $force, 'Created' => $gserver['created'], 'Failure' => $last_failure, 'Contact' => $last_contact]);
225 Logger::info('Server is unknown. Start discovery.', ['Server' => $server_url]);
228 return self::detect($server_url, $network, $only_nodeinfo);
232 * Set failed server status
236 private static function setFailure(string $url)
238 if (DBA::exists('gserver', ['nurl' => Strings::normaliseLink($url)])) {
239 DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow(), 'detection-method' => null],
240 ['nurl' => Strings::normaliseLink($url)]);
241 Logger::info('Set failed status for existing server', ['url' => $url]);
244 DBA::insert('gserver', ['url' => $url, 'nurl' => Strings::normaliseLink($url),
245 'network' => Protocol::PHANTOM, 'created' => DateTimeFormat::utcNow(),
246 'last_failure' => DateTimeFormat::utcNow()]);
247 Logger::info('Set failed status for new server', ['url' => $url]);
251 * Remove unwanted content from the given URL
254 * @return string cleaned URL
256 public static function cleanURL(string $url)
258 $url = trim($url, '/');
259 $url = str_replace('/index.php', '', $url);
261 $urlparts = parse_url($url);
262 unset($urlparts['user']);
263 unset($urlparts['pass']);
264 unset($urlparts['query']);
265 unset($urlparts['fragment']);
266 return Network::unparseURL($urlparts);
270 * Return the base URL
273 * @return string base URL
275 private static function getBaseURL(string $url)
277 $urlparts = parse_url(self::cleanURL($url));
278 unset($urlparts['path']);
279 return Network::unparseURL($urlparts);
283 * Detect server data (type, protocol, version number, ...)
284 * The detected data is then updated or inserted in the gserver table.
286 * @param string $url URL of the given server
287 * @param string $network Network value that is used, when detection failed
288 * @param boolean $only_nodeinfo Only use nodeinfo for server detection
290 * @return boolean 'true' if server could be detected
292 public static function detect(string $url, string $network = '', bool $only_nodeinfo = false)
294 Logger::info('Detect server type', ['server' => $url]);
295 $serverdata = ['detection-method' => self::DETECT_MANUAL];
297 $original_url = $url;
299 // Remove URL content that is not supposed to exist for a server url
300 $url = self::cleanURL($url);
303 $baseurl = self::getBaseURL($url);
305 // If the URL missmatches, then we mark the old entry as failure
306 if ($url != $original_url) {
307 DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($original_url)]);
310 // When a nodeinfo is present, we don't need to dig further
311 $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
312 $curlResult = Network::curl($url . '/.well-known/nodeinfo', false, ['timeout' => $xrd_timeout]);
313 if ($curlResult->isTimeout()) {
314 self::setFailure($url);
318 $nodeinfo = self::fetchNodeinfo($url, $curlResult);
319 if ($only_nodeinfo && empty($nodeinfo)) {
320 Logger::info('Invalid nodeinfo in nodeinfo-mode, server is marked as failure', ['url' => $url]);
321 self::setFailure($url);
325 // When nodeinfo isn't present, we use the older 'statistics.json' endpoint
326 if (empty($nodeinfo)) {
327 $nodeinfo = self::fetchStatistics($url);
330 // If that didn't work out well, we use some protocol specific endpoints
331 // For Friendica and Zot based networks we have to dive deeper to reveal more details
332 if (empty($nodeinfo['network']) || in_array($nodeinfo['network'], [Protocol::DFRN, Protocol::ZOT])) {
333 if (!empty($nodeinfo['detection-method'])) {
334 $serverdata['detection-method'] = $nodeinfo['detection-method'];
337 // Fetch the landing page, possibly it reveals some data
338 if (empty($nodeinfo['network'])) {
339 if ($baseurl == $url) {
340 $basedata = $serverdata;
342 $basedata = ['detection-method' => self::DETECT_MANUAL];
345 $curlResult = Network::curl($baseurl, false, ['timeout' => $xrd_timeout]);
346 if ($curlResult->isSuccess()) {
347 $basedata = self::analyseRootHeader($curlResult, $basedata);
348 $basedata = self::analyseRootBody($curlResult, $basedata, $baseurl);
351 if (!$curlResult->isSuccess() || empty($curlResult->getBody()) || self::invalidBody($curlResult->getBody())) {
352 self::setFailure($url);
356 if ($baseurl == $url) {
357 $serverdata = $basedata;
359 // When the base path doesn't seem to contain a social network we try the complete path.
360 // Most detectable system have to be installed in the root directory.
361 // We checked the base to avoid false positives.
362 $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout]);
363 if ($curlResult->isSuccess()) {
364 $urldata = self::analyseRootHeader($curlResult, $serverdata);
365 $urldata = self::analyseRootBody($curlResult, $urldata, $url);
367 $comparebase = $basedata;
368 unset($comparebase['info']);
369 unset($comparebase['site_name']);
370 $compareurl = $urldata;
371 unset($compareurl['info']);
372 unset($compareurl['site_name']);
374 // We assume that no one will install the identical system in the root and a subfolder
375 if (!empty(array_diff($comparebase, $compareurl))) {
376 $serverdata = $urldata;
382 if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ACTIVITYPUB)) {
383 $serverdata = self::detectMastodonAlikes($url, $serverdata);
386 // All following checks are done for systems that always have got a "host-meta" endpoint.
387 // With this check we don't have to waste time and ressources for dead systems.
388 // Also this hopefully prevents us from receiving abuse messages.
389 if (empty($serverdata['network']) && !self::validHostMeta($url)) {
390 self::setFailure($url);
394 if (empty($serverdata['network']) || in_array($serverdata['network'], [Protocol::DFRN, Protocol::ACTIVITYPUB])) {
395 $serverdata = self::detectFriendica($url, $serverdata);
398 // the 'siteinfo.json' is some specific endpoint of Hubzilla and Red
399 if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ZOT)) {
400 $serverdata = self::fetchSiteinfo($url, $serverdata);
403 // The 'siteinfo.json' doesn't seem to be present on older Hubzilla installations
404 if (empty($serverdata['network'])) {
405 $serverdata = self::detectHubzilla($url, $serverdata);
408 if (empty($serverdata['network'])) {
409 $serverdata = self::detectNextcloud($url, $serverdata);
412 if (empty($serverdata['network'])) {
413 $serverdata = self::detectGNUSocial($url, $serverdata);
416 $serverdata = array_merge($nodeinfo, $serverdata);
418 $serverdata = $nodeinfo;
421 // Detect the directory type
422 $serverdata['directory-type'] = self::DT_NONE;
423 $serverdata = self::checkPoCo($url, $serverdata);
424 $serverdata = self::checkMastodonDirectory($url, $serverdata);
426 // We can't detect the network type. Possibly it is some system that we don't know yet
427 if (empty($serverdata['network'])) {
428 $serverdata['network'] = Protocol::PHANTOM;
431 // When we hadn't been able to detect the network type, we use the hint from the parameter
432 if (($serverdata['network'] == Protocol::PHANTOM) && !empty($network)) {
433 $serverdata['network'] = $network;
436 $serverdata['url'] = $url;
437 $serverdata['nurl'] = Strings::normaliseLink($url);
439 // We take the highest number that we do find
440 $registeredUsers = $serverdata['registered-users'] ?? 0;
442 // On an active server there has to be at least a single user
443 if (($serverdata['network'] != Protocol::PHANTOM) && ($registeredUsers == 0)) {
444 $registeredUsers = 1;
447 if ($serverdata['network'] == Protocol::PHANTOM) {
448 $serverdata['registered-users'] = $registeredUsers;
449 $serverdata = self::detectNetworkViaContacts($url, $serverdata);
452 $serverdata['last_contact'] = DateTimeFormat::utcNow();
454 $gserver = DBA::selectFirst('gserver', ['network'], ['nurl' => Strings::normaliseLink($url)]);
455 if (!DBA::isResult($gserver)) {
456 $serverdata['created'] = DateTimeFormat::utcNow();
457 $ret = DBA::insert('gserver', $serverdata);
458 $id = DBA::lastInsertId();
460 // Don't override the network with 'unknown' when there had been a valid entry before
461 if (($serverdata['network'] == Protocol::PHANTOM) && !empty($gserver['network'])) {
462 unset($serverdata['network']);
465 $ret = DBA::update('gserver', $serverdata, ['nurl' => $serverdata['nurl']]);
466 $gserver = DBA::selectFirst('gserver', ['id'], ['nurl' => $serverdata['nurl']]);
467 if (DBA::isResult($gserver)) {
468 $id = $gserver['id'];
472 if (!empty($id) && ($serverdata['network'] != Protocol::PHANTOM)) {
473 $gcontacts = DBA::count('gcontact', ['gsid' => $id]);
474 $apcontacts = DBA::count('apcontact', ['gsid' => $id]);
475 $contacts = DBA::count('contact', ['uid' => 0, 'gsid' => $id]);
476 $max_users = max($gcontacts, $apcontacts, $contacts, $registeredUsers);
477 if ($max_users > $registeredUsers) {
478 Logger::info('Update registered users', ['id' => $id, 'url' => $serverdata['nurl'], 'registered-users' => $max_users]);
479 DBA::update('gserver', ['registered-users' => $max_users], ['id' => $id]);
483 if (!empty($serverdata['network']) && in_array($serverdata['network'], [Protocol::DFRN, Protocol::DIASPORA])) {
484 self::discoverRelay($url);
491 * Fetch relay data from a given server url
493 * @param string $server_url address of the server
494 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
496 private static function discoverRelay(string $server_url)
498 Logger::info('Discover relay data', ['server' => $server_url]);
500 $curlResult = Network::curl($server_url . '/.well-known/x-social-relay');
501 if (!$curlResult->isSuccess()) {
505 $data = json_decode($curlResult->getBody(), true);
506 if (!is_array($data)) {
510 // Sanitize incoming data, see https://github.com/friendica/friendica/issues/8565
511 $data['subscribe'] = (bool)$data['subscribe'] ?? false;
513 if (!$data['subscribe'] || empty($data['scope']) || !in_array(strtolower($data['scope']), ['all', 'tags'])) {
515 $data['subscribe'] = false;
519 $gserver = DBA::selectFirst('gserver', ['id', 'relay-subscribe', 'relay-scope'], ['nurl' => Strings::normaliseLink($server_url)]);
520 if (!DBA::isResult($gserver)) {
524 if (($gserver['relay-subscribe'] != $data['subscribe']) || ($gserver['relay-scope'] != $data['scope'])) {
525 $fields = ['relay-subscribe' => $data['subscribe'], 'relay-scope' => $data['scope']];
526 DBA::update('gserver', $fields, ['id' => $gserver['id']]);
529 DBA::delete('gserver-tag', ['gserver-id' => $gserver['id']]);
531 if ($data['scope'] == 'tags') {
534 foreach ($data['tags'] as $tag) {
535 $tag = mb_strtolower($tag);
536 if (strlen($tag) < 100) {
541 foreach ($tags as $tag) {
542 DBA::insert('gserver-tag', ['gserver-id' => $gserver['id'], 'tag' => $tag], true);
546 // Create or update the relay contact
548 if (isset($data['protocols'])) {
549 if (isset($data['protocols']['diaspora'])) {
550 $fields['network'] = Protocol::DIASPORA;
552 if (isset($data['protocols']['diaspora']['receive'])) {
553 $fields['batch'] = $data['protocols']['diaspora']['receive'];
554 } elseif (is_string($data['protocols']['diaspora'])) {
555 $fields['batch'] = $data['protocols']['diaspora'];
559 if (isset($data['protocols']['dfrn'])) {
560 $fields['network'] = Protocol::DFRN;
562 if (isset($data['protocols']['dfrn']['receive'])) {
563 $fields['batch'] = $data['protocols']['dfrn']['receive'];
564 } elseif (is_string($data['protocols']['dfrn'])) {
565 $fields['batch'] = $data['protocols']['dfrn'];
569 Diaspora::setRelayContact($server_url, $fields);
573 * Fetch server data from '/statistics.json' on the given server
575 * @param string $url URL of the given server
577 * @return array server data
579 private static function fetchStatistics(string $url)
581 $curlResult = Network::curl($url . '/statistics.json');
582 if (!$curlResult->isSuccess()) {
586 $data = json_decode($curlResult->getBody(), true);
591 $serverdata = ['detection-method' => self::DETECT_STATISTICS_JSON];
593 if (!empty($data['version'])) {
594 $serverdata['version'] = $data['version'];
595 // Version numbers on statistics.json are presented with additional info, e.g.:
596 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
597 $serverdata['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $serverdata['version']);
600 if (!empty($data['name'])) {
601 $serverdata['site_name'] = $data['name'];
604 if (!empty($data['network'])) {
605 $serverdata['platform'] = strtolower($data['network']);
607 if ($serverdata['platform'] == 'diaspora') {
608 $serverdata['network'] = Protocol::DIASPORA;
609 } elseif ($serverdata['platform'] == 'friendica') {
610 $serverdata['network'] = Protocol::DFRN;
611 } elseif ($serverdata['platform'] == 'hubzilla') {
612 $serverdata['network'] = Protocol::ZOT;
613 } elseif ($serverdata['platform'] == 'redmatrix') {
614 $serverdata['network'] = Protocol::ZOT;
619 if (!empty($data['registrations_open'])) {
620 $serverdata['register_policy'] = Register::OPEN;
622 $serverdata['register_policy'] = Register::CLOSED;
629 * Detect server type by using the nodeinfo data
631 * @param string $url address of the server
632 * @param CurlResult $curlResult
633 * @return array Server data
634 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
636 private static function fetchNodeinfo(string $url, CurlResult $curlResult)
638 if (!$curlResult->isSuccess()) {
642 $nodeinfo = json_decode($curlResult->getBody(), true);
644 if (!is_array($nodeinfo) || empty($nodeinfo['links'])) {
651 foreach ($nodeinfo['links'] as $link) {
652 if (!is_array($link) || empty($link['rel']) || empty($link['href'])) {
653 Logger::info('Invalid nodeinfo format', ['url' => $url]);
656 if ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/1.0') {
657 $nodeinfo1_url = $link['href'];
658 } elseif ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0') {
659 $nodeinfo2_url = $link['href'];
663 if ($nodeinfo1_url . $nodeinfo2_url == '') {
669 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
670 if (!empty($nodeinfo2_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo2_url, PHP_URL_HOST))) {
671 $server = self::parseNodeinfo2($nodeinfo2_url);
674 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
675 if (empty($server) && !empty($nodeinfo1_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo1_url, PHP_URL_HOST))) {
676 $server = self::parseNodeinfo1($nodeinfo1_url);
685 * @param string $nodeinfo_url address of the nodeinfo path
686 * @return array Server data
687 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
689 private static function parseNodeinfo1(string $nodeinfo_url)
691 $curlResult = Network::curl($nodeinfo_url);
692 if (!$curlResult->isSuccess()) {
696 $nodeinfo = json_decode($curlResult->getBody(), true);
698 if (!is_array($nodeinfo)) {
702 $server = ['detection-method' => self::DETECT_NODEINFO_1,
703 'register_policy' => Register::CLOSED];
705 if (!empty($nodeinfo['openRegistrations'])) {
706 $server['register_policy'] = Register::OPEN;
709 if (is_array($nodeinfo['software'])) {
710 if (!empty($nodeinfo['software']['name'])) {
711 $server['platform'] = strtolower($nodeinfo['software']['name']);
714 if (!empty($nodeinfo['software']['version'])) {
715 $server['version'] = $nodeinfo['software']['version'];
716 // Version numbers on Nodeinfo are presented with additional info, e.g.:
717 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
718 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
722 if (!empty($nodeinfo['metadata']['nodeName'])) {
723 $server['site_name'] = $nodeinfo['metadata']['nodeName'];
726 if (!empty($nodeinfo['usage']['users']['total'])) {
727 $server['registered-users'] = $nodeinfo['usage']['users']['total'];
730 if (!empty($nodeinfo['protocols']['inbound']) && is_array($nodeinfo['protocols']['inbound'])) {
732 foreach ($nodeinfo['protocols']['inbound'] as $protocol) {
733 $protocols[$protocol] = true;
736 if (!empty($protocols['friendica'])) {
737 $server['network'] = Protocol::DFRN;
738 } elseif (!empty($protocols['activitypub'])) {
739 $server['network'] = Protocol::ACTIVITYPUB;
740 } elseif (!empty($protocols['diaspora'])) {
741 $server['network'] = Protocol::DIASPORA;
742 } elseif (!empty($protocols['ostatus'])) {
743 $server['network'] = Protocol::OSTATUS;
744 } elseif (!empty($protocols['gnusocial'])) {
745 $server['network'] = Protocol::OSTATUS;
746 } elseif (!empty($protocols['zot'])) {
747 $server['network'] = Protocol::ZOT;
751 if (empty($server)) {
761 * @param string $nodeinfo_url address of the nodeinfo path
762 * @return array Server data
763 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
765 private static function parseNodeinfo2(string $nodeinfo_url)
767 $curlResult = Network::curl($nodeinfo_url);
768 if (!$curlResult->isSuccess()) {
772 $nodeinfo = json_decode($curlResult->getBody(), true);
774 if (!is_array($nodeinfo)) {
778 $server = ['detection-method' => self::DETECT_NODEINFO_2,
779 'register_policy' => Register::CLOSED];
781 if (!empty($nodeinfo['openRegistrations'])) {
782 $server['register_policy'] = Register::OPEN;
785 if (is_array($nodeinfo['software'])) {
786 if (!empty($nodeinfo['software']['name'])) {
787 $server['platform'] = strtolower($nodeinfo['software']['name']);
790 if (!empty($nodeinfo['software']['version'])) {
791 $server['version'] = $nodeinfo['software']['version'];
792 // Version numbers on Nodeinfo are presented with additional info, e.g.:
793 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
794 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
798 if (!empty($nodeinfo['metadata']['nodeName'])) {
799 $server['site_name'] = $nodeinfo['metadata']['nodeName'];
802 if (!empty($nodeinfo['usage']['users']['total'])) {
803 $server['registered-users'] = $nodeinfo['usage']['users']['total'];
806 if (!empty($nodeinfo['protocols'])) {
808 foreach ($nodeinfo['protocols'] as $protocol) {
809 $protocols[$protocol] = true;
812 if (!empty($protocols['dfrn'])) {
813 $server['network'] = Protocol::DFRN;
814 } elseif (!empty($protocols['activitypub'])) {
815 $server['network'] = Protocol::ACTIVITYPUB;
816 } elseif (!empty($protocols['diaspora'])) {
817 $server['network'] = Protocol::DIASPORA;
818 } elseif (!empty($protocols['ostatus'])) {
819 $server['network'] = Protocol::OSTATUS;
820 } elseif (!empty($protocols['gnusocial'])) {
821 $server['network'] = Protocol::OSTATUS;
822 } elseif (!empty($protocols['zot'])) {
823 $server['network'] = Protocol::ZOT;
827 if (empty($server)) {
835 * Fetch server information from a 'siteinfo.json' file on the given server
837 * @param string $url URL of the given server
838 * @param array $serverdata array with server data
840 * @return array server data
842 private static function fetchSiteinfo(string $url, array $serverdata)
844 $curlResult = Network::curl($url . '/siteinfo.json');
845 if (!$curlResult->isSuccess()) {
849 $data = json_decode($curlResult->getBody(), true);
854 if (in_array($serverdata['detection-method'], [self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_MANUAL])) {
855 $serverdata['detection-method'] = self::DETECT_SITEINFO_JSON;
858 if (!empty($data['url'])) {
859 $serverdata['platform'] = strtolower($data['platform']);
860 $serverdata['version'] = $data['version'];
863 if (!empty($data['plugins'])) {
864 if (in_array('pubcrawl', $data['plugins'])) {
865 $serverdata['network'] = Protocol::ACTIVITYPUB;
866 } elseif (in_array('diaspora', $data['plugins'])) {
867 $serverdata['network'] = Protocol::DIASPORA;
868 } elseif (in_array('gnusoc', $data['plugins'])) {
869 $serverdata['network'] = Protocol::OSTATUS;
871 $serverdata['network'] = Protocol::ZOT;
875 if (!empty($data['site_name'])) {
876 $serverdata['site_name'] = $data['site_name'];
879 if (!empty($data['channels_total'])) {
880 $serverdata['registered-users'] = $data['channels_total'];
883 if (!empty($data['register_policy'])) {
884 switch ($data['register_policy']) {
885 case 'REGISTER_OPEN':
886 $serverdata['register_policy'] = Register::OPEN;
889 case 'REGISTER_APPROVE':
890 $serverdata['register_policy'] = Register::APPROVE;
893 case 'REGISTER_CLOSED':
895 $serverdata['register_policy'] = Register::CLOSED;
904 * Checks if the server contains a valid host meta file
906 * @param string $url URL of the given server
908 * @return boolean 'true' if the server seems to be vital
910 private static function validHostMeta(string $url)
912 $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
913 $curlResult = Network::curl($url . '/.well-known/host-meta', false, ['timeout' => $xrd_timeout]);
914 if (!$curlResult->isSuccess()) {
918 $xrd = XML::parseString($curlResult->getBody());
919 if (!is_object($xrd)) {
923 $elements = XML::elementToArray($xrd);
924 if (empty($elements) || empty($elements['xrd']) || empty($elements['xrd']['link'])) {
929 foreach ($elements['xrd']['link'] as $link) {
930 // When there is more than a single "link" element, the array looks slightly different
931 if (!empty($link['@attributes'])) {
932 $link = $link['@attributes'];
935 if (empty($link['rel']) || empty($link['template'])) {
939 if ($link['rel'] == 'lrdd') {
940 // When the webfinger host is the same like the system host, it should be ok.
941 $valid = (parse_url($url, PHP_URL_HOST) == parse_url($link['template'], PHP_URL_HOST));
949 * Detect the network of the given server via their known contacts
951 * @param string $url URL of the given server
952 * @param array $serverdata array with server data
954 * @return array server data
956 private static function detectNetworkViaContacts(string $url, array $serverdata)
960 $gcontacts = DBA::select('gcontact', ['url', 'nurl'], ['server_url' => [$url, $serverdata['nurl']]]);
961 while ($gcontact = DBA::fetch($gcontacts)) {
962 $contacts[$gcontact['nurl']] = $gcontact['url'];
964 DBA::close($gcontacts);
966 $apcontacts = DBA::select('apcontact', ['url'], ['baseurl' => [$url, $serverdata['nurl']]]);
967 while ($apcontact = DBA::fetch($apcontacts)) {
968 $contacts[Strings::normaliseLink($apcontact['url'])] = $apcontact['url'];
970 DBA::close($apcontacts);
972 $pcontacts = DBA::select('contact', ['url', 'nurl'], ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]);
973 while ($pcontact = DBA::fetch($pcontacts)) {
974 $contacts[$pcontact['nurl']] = $pcontact['url'];
976 DBA::close($pcontacts);
978 if (empty($contacts)) {
982 foreach ($contacts as $contact) {
983 $probed = Probe::uri($contact);
984 if (in_array($probed['network'], Protocol::FEDERATED)) {
985 $serverdata['network'] = $probed['network'];
990 $serverdata['registered-users'] = max($serverdata['registered-users'], count($contacts));
996 * Checks if the given server does have a '/poco' endpoint.
997 * This is used for the 'PortableContact' functionality,
998 * which is used by both Friendica and Hubzilla.
1000 * @param string $url URL of the given server
1001 * @param array $serverdata array with server data
1003 * @return array server data
1005 private static function checkPoCo(string $url, array $serverdata)
1007 $serverdata['poco'] = '';
1009 $curlResult = Network::curl($url. '/poco');
1010 if (!$curlResult->isSuccess()) {
1014 $data = json_decode($curlResult->getBody(), true);
1019 if (!empty($data['totalResults'])) {
1020 $registeredUsers = $serverdata['registered-users'] ?? 0;
1021 $serverdata['registered-users'] = max($data['totalResults'], $registeredUsers);
1022 $serverdata['directory-type'] = self::DT_POCO;
1023 $serverdata['poco'] = $url . '/poco';
1030 * Checks if the given server does have a Mastodon style directory endpoint.
1032 * @param string $url URL of the given server
1033 * @param array $serverdata array with server data
1035 * @return array server data
1037 public static function checkMastodonDirectory(string $url, array $serverdata)
1039 $curlResult = Network::curl($url . '/api/v1/directory?limit=1');
1040 if (!$curlResult->isSuccess()) {
1044 $data = json_decode($curlResult->getBody(), true);
1049 if (count($data) == 1) {
1050 $serverdata['directory-type'] = self::DT_MASTODON;
1057 * Detects the version number of a given server when it was a NextCloud installation
1059 * @param string $url URL of the given server
1060 * @param array $serverdata array with server data
1062 * @return array server data
1064 private static function detectNextcloud(string $url, array $serverdata)
1066 $curlResult = Network::curl($url . '/status.php');
1067 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
1071 $data = json_decode($curlResult->getBody(), true);
1076 if (!empty($data['version'])) {
1077 $serverdata['platform'] = 'nextcloud';
1078 $serverdata['version'] = $data['version'];
1079 $serverdata['network'] = Protocol::ACTIVITYPUB;
1081 if (in_array($serverdata['detection-method'], [self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_MANUAL])) {
1082 $serverdata['detection-method'] = self::DETECT_STATUS_PHP;
1090 * Detects data from a given server url if it was a mastodon alike system
1092 * @param string $url URL of the given server
1093 * @param array $serverdata array with server data
1095 * @return array server data
1097 private static function detectMastodonAlikes(string $url, array $serverdata)
1099 $curlResult = Network::curl($url . '/api/v1/instance');
1100 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
1104 $data = json_decode($curlResult->getBody(), true);
1109 if (in_array($serverdata['detection-method'], [self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_MANUAL])) {
1110 $serverdata['detection-method'] = self::DETECT_MASTODON_API;
1113 if (!empty($data['version'])) {
1114 $serverdata['platform'] = 'mastodon';
1115 $serverdata['version'] = $data['version'] ?? '';
1116 $serverdata['network'] = Protocol::ACTIVITYPUB;
1119 if (!empty($data['title'])) {
1120 $serverdata['site_name'] = $data['title'];
1123 if (!empty($data['title']) && empty($serverdata['platform']) && empty($serverdata['network'])) {
1124 $serverdata['platform'] = 'mastodon';
1125 $serverdata['network'] = Protocol::ACTIVITYPUB;
1128 if (!empty($data['description'])) {
1129 $serverdata['info'] = trim($data['description']);
1132 if (!empty($data['stats']['user_count'])) {
1133 $serverdata['registered-users'] = $data['stats']['user_count'];
1136 if (!empty($serverdata['version']) && preg_match('/.*?\(compatible;\s(.*)\s(.*)\)/ism', $serverdata['version'], $matches)) {
1137 $serverdata['platform'] = strtolower($matches[1]);
1138 $serverdata['version'] = $matches[2];
1141 if (!empty($serverdata['version']) && strstr(strtolower($serverdata['version']), 'pleroma')) {
1142 $serverdata['platform'] = 'pleroma';
1143 $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version']));
1146 if (!empty($serverdata['platform']) && strstr($serverdata['platform'], 'pleroma')) {
1147 $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['platform']));
1148 $serverdata['platform'] = 'pleroma';
1155 * Detects data from typical Hubzilla endpoints
1157 * @param string $url URL of the given server
1158 * @param array $serverdata array with server data
1160 * @return array server data
1162 private static function detectHubzilla(string $url, array $serverdata)
1164 $curlResult = Network::curl($url . '/api/statusnet/config.json');
1165 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
1169 $data = json_decode($curlResult->getBody(), true);
1170 if (empty($data) || empty($data['site'])) {
1174 if (!empty($data['site']['name'])) {
1175 $serverdata['site_name'] = $data['site']['name'];
1178 if (!empty($data['site']['platform'])) {
1179 $serverdata['platform'] = strtolower($data['site']['platform']['PLATFORM_NAME']);
1180 $serverdata['version'] = $data['site']['platform']['STD_VERSION'];
1181 $serverdata['network'] = Protocol::ZOT;
1184 if (!empty($data['site']['hubzilla'])) {
1185 $serverdata['platform'] = strtolower($data['site']['hubzilla']['PLATFORM_NAME']);
1186 $serverdata['version'] = $data['site']['hubzilla']['RED_VERSION'];
1187 $serverdata['network'] = Protocol::ZOT;
1190 if (!empty($data['site']['redmatrix'])) {
1191 if (!empty($data['site']['redmatrix']['PLATFORM_NAME'])) {
1192 $serverdata['platform'] = strtolower($data['site']['redmatrix']['PLATFORM_NAME']);
1193 } elseif (!empty($data['site']['redmatrix']['RED_PLATFORM'])) {
1194 $serverdata['platform'] = strtolower($data['site']['redmatrix']['RED_PLATFORM']);
1197 $serverdata['version'] = $data['site']['redmatrix']['RED_VERSION'];
1198 $serverdata['network'] = Protocol::ZOT;
1202 $inviteonly = false;
1205 if (!empty($data['site']['closed'])) {
1206 $closed = self::toBoolean($data['site']['closed']);
1209 if (!empty($data['site']['private'])) {
1210 $private = self::toBoolean($data['site']['private']);
1213 if (!empty($data['site']['inviteonly'])) {
1214 $inviteonly = self::toBoolean($data['site']['inviteonly']);
1217 if (!$closed && !$private and $inviteonly) {
1218 $serverdata['register_policy'] = Register::APPROVE;
1219 } elseif (!$closed && !$private) {
1220 $serverdata['register_policy'] = Register::OPEN;
1222 $serverdata['register_policy'] = Register::CLOSED;
1225 if (!empty($serverdata['network']) && in_array($serverdata['detection-method'],
1226 [self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_MANUAL])) {
1227 $serverdata['detection-method'] = self::DETECT_CONFIG_JSON;
1234 * Converts input value to a boolean value
1236 * @param string|integer $val
1240 private static function toBoolean($val)
1242 if (($val == 'true') || ($val == 1)) {
1244 } elseif (($val == 'false') || ($val == 0)) {
1252 * Detect if the URL belongs to a GNU Social server
1254 * @param string $url URL of the given server
1255 * @param array $serverdata array with server data
1257 * @return array server data
1259 private static function detectGNUSocial(string $url, array $serverdata)
1261 // Test for GNU Social
1262 $curlResult = Network::curl($url . '/api/gnusocial/version.json');
1263 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
1264 ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
1265 $serverdata['platform'] = 'gnusocial';
1266 // Remove junk that some GNU Social servers return
1267 $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBody());
1268 $serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
1269 $serverdata['version'] = trim($serverdata['version'], '"');
1270 $serverdata['network'] = Protocol::OSTATUS;
1272 if (in_array($serverdata['detection-method'], [self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_MANUAL])) {
1273 $serverdata['detection-method'] = self::DETECT_GNUSOCIAL;
1279 // Test for Statusnet
1280 $curlResult = Network::curl($url . '/api/statusnet/version.json');
1281 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
1282 ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
1284 // Remove junk that some GNU Social servers return
1285 $serverdata['version'] = str_replace(chr(239).chr(187).chr(191), '', $curlResult->getBody());
1286 $serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
1287 $serverdata['version'] = trim($serverdata['version'], '"');
1289 if (!empty($serverdata['version']) && strtolower(substr($serverdata['version'], 0, 7)) == 'pleroma') {
1290 $serverdata['platform'] = 'pleroma';
1291 $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version']));
1292 $serverdata['network'] = Protocol::ACTIVITYPUB;
1294 $serverdata['platform'] = 'statusnet';
1295 $serverdata['network'] = Protocol::OSTATUS;
1298 if (in_array($serverdata['detection-method'], [self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_MANUAL])) {
1299 $serverdata['detection-method'] = self::DETECT_STATUSNET;
1307 * Detect if the URL belongs to a Friendica server
1309 * @param string $url URL of the given server
1310 * @param array $serverdata array with server data
1312 * @return array server data
1314 private static function detectFriendica(string $url, array $serverdata)
1316 $curlResult = Network::curl($url . '/friendica/json');
1317 if (!$curlResult->isSuccess()) {
1318 $curlResult = Network::curl($url . '/friendika/json');
1320 $platform = 'Friendika';
1323 $platform = 'Friendica';
1326 if (!$curlResult->isSuccess()) {
1330 $data = json_decode($curlResult->getBody(), true);
1331 if (empty($data) || empty($data['version'])) {
1335 if (in_array($serverdata['detection-method'], [self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_MANUAL])) {
1336 $serverdata['detection-method'] = $friendika ? self::DETECT_FRIENDIKA : self::DETECT_FRIENDICA;
1339 $serverdata['network'] = Protocol::DFRN;
1340 $serverdata['version'] = $data['version'];
1342 if (!empty($data['no_scrape_url'])) {
1343 $serverdata['noscrape'] = $data['no_scrape_url'];
1346 if (!empty($data['site_name'])) {
1347 $serverdata['site_name'] = $data['site_name'];
1350 if (!empty($data['info'])) {
1351 $serverdata['info'] = trim($data['info']);
1354 $register_policy = ($data['register_policy'] ?? '') ?: 'REGISTER_CLOSED';
1355 switch ($register_policy) {
1356 case 'REGISTER_OPEN':
1357 $serverdata['register_policy'] = Register::OPEN;
1360 case 'REGISTER_APPROVE':
1361 $serverdata['register_policy'] = Register::APPROVE;
1364 case 'REGISTER_CLOSED':
1365 case 'REGISTER_INVITATION':
1366 $serverdata['register_policy'] = Register::CLOSED;
1369 Logger::info('Register policy is invalid', ['policy' => $register_policy, 'server' => $url]);
1370 $serverdata['register_policy'] = Register::CLOSED;
1374 $serverdata['platform'] = strtolower($data['platform'] ?? $platform);
1380 * Analyses the landing page of a given server for hints about type and system of that server
1382 * @param object $curlResult result of curl execution
1383 * @param array $serverdata array with server data
1384 * @param string $url Server URL
1386 * @return array server data
1388 private static function analyseRootBody($curlResult, array $serverdata, string $url)
1390 $doc = new DOMDocument();
1391 @$doc->loadHTML($curlResult->getBody());
1392 $xpath = new DOMXPath($doc);
1394 $title = trim(XML::getFirstNodeValue($xpath, '//head/title/text()'));
1395 if (!empty($title)) {
1396 $serverdata['site_name'] = $title;
1399 $list = $xpath->query('//meta[@name]');
1401 foreach ($list as $node) {
1403 if ($node->attributes->length) {
1404 foreach ($node->attributes as $attribute) {
1405 $value = trim($attribute->value);
1406 if (empty($value)) {
1410 $attr[$attribute->name] = $value;
1413 if (empty($attr['name']) || empty($attr['content'])) {
1418 if ($attr['name'] == 'description') {
1419 $serverdata['info'] = $attr['content'];
1422 if (in_array($attr['name'], ['application-name', 'al:android:app_name', 'al:ios:app_name',
1423 'twitter:app:name:googleplay', 'twitter:app:name:iphone', 'twitter:app:name:ipad'])) {
1424 $serverdata['platform'] = strtolower($attr['content']);
1425 if (in_array($attr['content'], ['Misskey', 'Write.as'])) {
1426 $serverdata['network'] = Protocol::ACTIVITYPUB;
1429 if (($attr['name'] == 'generator') && (empty($serverdata['platform']) || (substr(strtolower($attr['content']), 0, 9) == 'wordpress'))) {
1430 $serverdata['platform'] = strtolower($attr['content']);
1431 $version_part = explode(' ', $attr['content']);
1433 if (count($version_part) == 2) {
1434 if (in_array($version_part[0], ['WordPress'])) {
1435 $serverdata['platform'] = strtolower($version_part[0]);
1436 $serverdata['version'] = $version_part[1];
1438 // We still do need a reliable test if some AP plugin is activated
1439 if (DBA::exists('apcontact', ['baseurl' => $url])) {
1440 $serverdata['network'] = Protocol::ACTIVITYPUB;
1442 $serverdata['network'] = Protocol::FEED;
1445 if ($serverdata['detection-method'] == self::DETECT_MANUAL) {
1446 $serverdata['detection-method'] = self::DETECT_BODY;
1449 if (in_array($version_part[0], ['Friendika', 'Friendica'])) {
1450 $serverdata['platform'] = strtolower($version_part[0]);
1451 $serverdata['version'] = $version_part[1];
1452 $serverdata['network'] = Protocol::DFRN;
1458 $list = $xpath->query('//meta[@property]');
1460 foreach ($list as $node) {
1462 if ($node->attributes->length) {
1463 foreach ($node->attributes as $attribute) {
1464 $value = trim($attribute->value);
1465 if (empty($value)) {
1469 $attr[$attribute->name] = $value;
1472 if (empty($attr['property']) || empty($attr['content'])) {
1477 if ($attr['property'] == 'og:site_name') {
1478 $serverdata['site_name'] = $attr['content'];
1481 if ($attr['property'] == 'og:description') {
1482 $serverdata['info'] = $attr['content'];
1485 if ($attr['property'] == 'og:platform') {
1486 $serverdata['platform'] = strtolower($attr['content']);
1488 if (in_array($attr['content'], ['PeerTube'])) {
1489 $serverdata['network'] = Protocol::ACTIVITYPUB;
1493 if ($attr['property'] == 'generator') {
1494 $serverdata['platform'] = strtolower($attr['content']);
1496 if (in_array($attr['content'], ['hubzilla'])) {
1497 // We later check which compatible protocol modules are loaded.
1498 $serverdata['network'] = Protocol::ZOT;
1503 if (!empty($serverdata['network']) && ($serverdata['detection-method'] == self::DETECT_MANUAL)) {
1504 $serverdata['detection-method'] = self::DETECT_BODY;
1511 * Analyses the header data of a given server for hints about type and system of that server
1513 * @param object $curlResult result of curl execution
1514 * @param array $serverdata array with server data
1516 * @return array server data
1518 private static function analyseRootHeader($curlResult, array $serverdata)
1520 if ($curlResult->getHeader('server') == 'Mastodon') {
1521 $serverdata['platform'] = 'mastodon';
1522 $serverdata['network'] = Protocol::ACTIVITYPUB;
1523 } elseif ($curlResult->inHeader('x-diaspora-version')) {
1524 $serverdata['platform'] = 'diaspora';
1525 $serverdata['network'] = Protocol::DIASPORA;
1526 $serverdata['version'] = $curlResult->getHeader('x-diaspora-version');
1527 } elseif ($curlResult->inHeader('x-friendica-version')) {
1528 $serverdata['platform'] = 'friendica';
1529 $serverdata['network'] = Protocol::DFRN;
1530 $serverdata['version'] = $curlResult->getHeader('x-friendica-version');
1535 if ($serverdata['detection-method'] == self::DETECT_MANUAL) {
1536 $serverdata['detection-method'] = self::DETECT_HEADER;
1543 * Test if the body contains valid content
1545 * @param string $body
1548 private static function invalidBody(string $body)
1550 // Currently we only test for a HTML element.
1551 // Possibly we enhance this in the future.
1552 return !strpos($body, '>');
1556 * Update the user directory of a given gserver record
1558 * @param array $gserver gserver record
1560 public static function updateDirectory(array $gserver)
1562 /// @todo Add Mastodon API directory
1564 if (!empty($gserver['poco'])) {
1565 PortableContact::discoverSingleServer($gserver['id']);
1570 * Update GServer entries
1572 public static function discover()
1574 // Update the server list
1575 self::discoverFederation();
1579 $requery_days = intval(DI::config()->get('system', 'poco_requery_days'));
1581 if ($requery_days == 0) {
1585 $last_update = date('c', time() - (60 * 60 * 24 * $requery_days));
1587 $gservers = DBA::p("SELECT `id`, `url`, `nurl`, `network`, `poco`
1589 WHERE `last_contact` >= `last_failure`
1591 AND `last_poco_query` < ?
1592 ORDER BY RAND()", $last_update
1595 while ($gserver = DBA::fetch($gservers)) {
1596 if (!GServer::check($gserver['url'], $gserver['network'])) {
1597 // The server is not reachable? Okay, then we will try it later
1598 $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
1599 DBA::update('gserver', $fields, ['nurl' => $gserver['nurl']]);
1603 Logger::info('Update directory', ['server' => $gserver['url'], 'id' => $gserver['id']]);
1604 Worker::add(PRIORITY_LOW, 'UpdateServerDirectory', $gserver);
1606 if (--$no_of_queries == 0) {
1611 DBA::close($gservers);
1615 * Discover federated servers
1617 private static function discoverFederation()
1619 $last = DI::config()->get('poco', 'last_federation_discovery');
1622 $next = $last + (24 * 60 * 60);
1624 if ($next > time()) {
1629 // Discover federated servers
1630 $curlResult = Network::fetchUrl("http://the-federation.info/pods.json");
1632 if (!empty($curlResult)) {
1633 $servers = json_decode($curlResult, true);
1635 if (!empty($servers['pods'])) {
1636 foreach ($servers['pods'] as $server) {
1637 // Using "only_nodeinfo" since servers that are listed on that page should always have it.
1638 Worker::add(PRIORITY_LOW, 'UpdateGServer', 'https://' . $server['host'], true);
1643 // Disvover Mastodon servers
1644 $accesstoken = DI::config()->get('system', 'instances_social_key');
1646 if (!empty($accesstoken)) {
1647 $api = 'https://instances.social/api/1.0/instances/list?count=0';
1648 $header = ['Authorization: Bearer '.$accesstoken];
1649 $curlResult = Network::curl($api, false, ['headers' => $header]);
1650 if ($curlResult->isSuccess()) {
1651 $servers = json_decode($curlResult->getBody(), true);
1653 foreach ($servers['instances'] as $server) {
1654 $url = (is_null($server['https_score']) ? 'http' : 'https') . '://' . $server['name'];
1655 Worker::add(PRIORITY_LOW, 'UpdateGServer', $url);
1660 DI::config()->set('poco', 'last_federation_discovery', time());