4 * @file src/Model/GServer.php
5 * This file includes the GServer class to handle with servers
7 namespace Friendica\Model;
11 use Friendica\Core\Config;
12 use Friendica\Core\Protocol;
13 use Friendica\Database\DBA;
14 use Friendica\Module\Register;
15 use Friendica\Util\Network;
16 use Friendica\Util\DateTimeFormat;
17 use Friendica\Util\Strings;
18 use Friendica\Util\XML;
19 use Friendica\Core\Logger;
20 use Friendica\Protocol\PortableContact;
21 use Friendica\Protocol\Diaspora;
22 use Friendica\Network\Probe;
25 * This class handles GServer related functions
30 * Checks if the given server is reachable
32 * @param string $profile URL of the given profile
33 * @param string $server URL of the given server (If empty, taken from profile)
34 * @param string $network Network value that is used, when detection failed
35 * @param boolean $force Force an update.
37 * @return boolean 'true' if server seems vital
39 public static function reachable(string $profile, string $server = '', string $network = '', bool $force = false)
42 $server = Contact::getBasepath($profile);
49 return self::check($server, $network, $force);
53 * Checks the state of the given server.
55 * @param string $server_url URL of the given server
56 * @param string $network Network value that is used, when detection failed
57 * @param boolean $force Force an update.
59 * @return boolean 'true' if server seems vital
61 public static function check(string $server_url, string $network = '', bool $force = false)
63 // Unify the server address
64 $server_url = trim($server_url, '/');
65 $server_url = str_replace('/index.php', '', $server_url);
67 if ($server_url == '') {
71 $gserver = DBA::selectFirst('gserver', [], ['nurl' => Strings::normaliseLink($server_url)]);
72 if (DBA::isResult($gserver)) {
73 if ($gserver['created'] <= DBA::NULL_DATETIME) {
74 $fields = ['created' => DateTimeFormat::utcNow()];
75 $condition = ['nurl' => Strings::normaliseLink($server_url)];
76 DBA::update('gserver', $fields, $condition);
79 $last_contact = $gserver['last_contact'];
80 $last_failure = $gserver['last_failure'];
82 // See discussion under https://forum.friendi.ca/display/0b6b25a8135aabc37a5a0f5684081633
83 // It can happen that a zero date is in the database, but storing it again is forbidden.
84 if ($last_contact < DBA::NULL_DATETIME) {
85 $last_contact = DBA::NULL_DATETIME;
88 if ($last_failure < DBA::NULL_DATETIME) {
89 $last_failure = DBA::NULL_DATETIME;
92 if (!$force && !PortableContact::updateNeeded($gserver['created'], '', $last_failure, $last_contact)) {
93 Logger::info('No update needed', ['server' => $server_url]);
94 return ($last_contact >= $last_failure);
96 Logger::info('Server is outdated. Start discovery.', ['Server' => $server_url, 'Force' => $force, 'Created' => $gserver['created'], 'Failure' => $last_failure, 'Contact' => $last_contact]);
98 Logger::info('Server is unknown. Start discovery.', ['Server' => $server_url]);
101 return self::detect($server_url, $network);
105 * Detect server data (type, protocol, version number, ...)
106 * The detected data is then updated or inserted in the gserver table.
108 * @param string $url URL of the given server
109 * @param string $network Network value that is used, when detection failed
111 * @return boolean 'true' if server could be detected
113 public static function detect(string $url, string $network = '')
117 // When a nodeinfo is present, we don't need to dig further
118 $xrd_timeout = Config::get('system', 'xrd_timeout');
119 $curlResult = Network::curl($url . '/.well-known/nodeinfo', false, ['timeout' => $xrd_timeout]);
120 if ($curlResult->isTimeout()) {
121 DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
125 $nodeinfo = self::fetchNodeinfo($url, $curlResult);
127 // When nodeinfo isn't present, we use the older 'statistics.json' endpoint
128 if (empty($nodeinfo)) {
129 $nodeinfo = self::fetchStatistics($url);
132 // If that didn't work out well, we use some protocol specific endpoints
133 if (empty($nodeinfo) || empty($nodeinfo['network']) || ($nodeinfo['network'] == Protocol::DFRN)) {
134 // Fetch the landing page, possibly it reveals some data
135 $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout]);
136 if ($curlResult->isSuccess()) {
137 $serverdata = self::analyseRootHeader($curlResult, $serverdata);
138 $serverdata = self::analyseRootBody($curlResult, $serverdata);
141 if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
142 DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
146 if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::DFRN)) {
147 $serverdata = self::detectFriendica($url, $serverdata);
150 if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ACTIVITYPUB)) {
151 $serverdata = self::detectMastodonAlikes($url, $serverdata);
154 // the 'siteinfo.json' is some specific endpoint of Hubzilla and Red
155 if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ZOT)) {
156 $serverdata = self::fetchSiteinfo($url, $serverdata);
159 // The 'siteinfo.json' doesn't seem to be present on older Hubzilla installations
160 if (empty($serverdata['network'])) {
161 $serverdata = self::detectHubzilla($url, $serverdata);
164 if (empty($serverdata['network'])) {
165 $serverdata = self::detectNextcloud($url, $serverdata);
168 if (empty($serverdata['network'])) {
169 $serverdata = self::detectGNUSocial($url, $serverdata);
172 $serverdata = $nodeinfo;
175 $serverdata = self::checkPoCo($url, $serverdata);
177 // We can't detect the network type. Possibly it is some system that we don't know yet
178 if (empty($serverdata['network'])) {
179 $serverdata['network'] = Protocol::PHANTOM;
182 // When we hadn't been able to detect the network type, we use the hint from the parameter
183 if (($serverdata['network'] == Protocol::PHANTOM) && !empty($network)) {
184 $serverdata['network'] = $network;
187 // Check host-meta for phantom networks.
188 // Although this is not needed, it is a good indicator for a living system,
189 // since most systems had implemented it.
190 if (($serverdata['network'] == Protocol::PHANTOM) && !self::validHostMeta($url)) {
191 DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
195 $serverdata['url'] = $url;
196 $serverdata['nurl'] = Strings::normaliseLink($url);
198 // We take the highest number that we do find
199 $registeredUsers = $serverdata['registered-users'] ?? 0;
201 // On an active server there has to be at least a single user
202 if (($serverdata['network'] != Protocol::PHANTOM) && ($registeredUsers == 0)) {
203 $registeredUsers = 1;
206 if ($serverdata['network'] != Protocol::PHANTOM) {
207 $gcontacts = DBA::count('gcontact', ['server_url' => [$url, $serverdata['nurl']]]);
208 $apcontacts = DBA::count('apcontact', ['baseurl' => [$url, $serverdata['nurl']]]);
209 $contacts = DBA::count('contact', ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]);
210 $serverdata['registered-users'] = max($gcontacts, $apcontacts, $contacts, $registeredUsers);
212 $serverdata['registered-users'] = $registeredUsers;
213 $serverdata = self::detectNetworkViaContacts($url, $serverdata);
216 $serverdata['last_contact'] = DateTimeFormat::utcNow();
218 $gserver = DBA::selectFirst('gserver', ['network'], ['nurl' => Strings::normaliseLink($url)]);
219 if (!DBA::isResult($gserver)) {
220 $serverdata['created'] = DateTimeFormat::utcNow();
221 $ret = DBA::insert('gserver', $serverdata);
223 // Don't override the network with 'unknown' when there had been a valid entry before
224 if (($serverdata['network'] == Protocol::PHANTOM) && !empty($gserver['network'])) {
225 unset($serverdata['network']);
228 $ret = DBA::update('gserver', $serverdata, ['nurl' => $serverdata['nurl']]);
231 if (!empty($serverdata['network']) && in_array($serverdata['network'], [Protocol::DFRN, Protocol::DIASPORA])) {
232 self::discoverRelay($url);
239 * Fetch relay data from a given server url
241 * @param string $server_url address of the server
242 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
244 private static function discoverRelay(string $server_url)
246 Logger::info('Discover relay data', ['server' => $server_url]);
248 $curlResult = Network::curl($server_url . '/.well-known/x-social-relay');
249 if (!$curlResult->isSuccess()) {
253 $data = json_decode($curlResult->getBody(), true);
254 if (!is_array($data)) {
258 $gserver = DBA::selectFirst('gserver', ['id', 'relay-subscribe', 'relay-scope'], ['nurl' => Strings::normaliseLink($server_url)]);
259 if (!DBA::isResult($gserver)) {
263 if (($gserver['relay-subscribe'] != $data['subscribe']) || ($gserver['relay-scope'] != $data['scope'])) {
264 $fields = ['relay-subscribe' => $data['subscribe'], 'relay-scope' => $data['scope']];
265 DBA::update('gserver', $fields, ['id' => $gserver['id']]);
268 DBA::delete('gserver-tag', ['gserver-id' => $gserver['id']]);
270 if ($data['scope'] == 'tags') {
273 foreach ($data['tags'] as $tag) {
274 $tag = mb_strtolower($tag);
275 if (strlen($tag) < 100) {
280 foreach ($tags as $tag) {
281 DBA::insert('gserver-tag', ['gserver-id' => $gserver['id'], 'tag' => $tag], true);
285 // Create or update the relay contact
287 if (isset($data['protocols'])) {
288 if (isset($data['protocols']['diaspora'])) {
289 $fields['network'] = Protocol::DIASPORA;
291 if (isset($data['protocols']['diaspora']['receive'])) {
292 $fields['batch'] = $data['protocols']['diaspora']['receive'];
293 } elseif (is_string($data['protocols']['diaspora'])) {
294 $fields['batch'] = $data['protocols']['diaspora'];
298 if (isset($data['protocols']['dfrn'])) {
299 $fields['network'] = Protocol::DFRN;
301 if (isset($data['protocols']['dfrn']['receive'])) {
302 $fields['batch'] = $data['protocols']['dfrn']['receive'];
303 } elseif (is_string($data['protocols']['dfrn'])) {
304 $fields['batch'] = $data['protocols']['dfrn'];
308 Diaspora::setRelayContact($server_url, $fields);
312 * Fetch server data from '/statistics.json' on the given server
314 * @param string $url URL of the given server
316 * @return array server data
318 private static function fetchStatistics(string $url)
320 $curlResult = Network::curl($url . '/statistics.json');
321 if (!$curlResult->isSuccess()) {
325 $data = json_decode($curlResult->getBody(), true);
332 if (!empty($data['version'])) {
333 $serverdata['version'] = $data['version'];
334 // Version numbers on statistics.json are presented with additional info, e.g.:
335 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
336 $serverdata['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $serverdata['version']);
339 if (!empty($data['name'])) {
340 $serverdata['site_name'] = $data['name'];
343 if (!empty($data['network'])) {
344 $serverdata['platform'] = $data['network'];
346 if ($serverdata['platform'] == 'Diaspora') {
347 $serverdata['network'] = Protocol::DIASPORA;
348 } elseif ($serverdata['platform'] == 'Friendica') {
349 $serverdata['network'] = Protocol::DFRN;
350 } elseif ($serverdata['platform'] == 'hubzilla') {
351 $serverdata['network'] = Protocol::ZOT;
352 } elseif ($serverdata['platform'] == 'redmatrix') {
353 $serverdata['network'] = Protocol::ZOT;
358 if (!empty($data['registrations_open'])) {
359 $serverdata['register_policy'] = Register::OPEN;
361 $serverdata['register_policy'] = Register::CLOSED;
368 * Detect server type by using the nodeinfo data
370 * @param string $url address of the server
371 * @return array Server data
372 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
374 private static function fetchNodeinfo(string $url, $curlResult)
376 $nodeinfo = json_decode($curlResult->getBody(), true);
378 if (!is_array($nodeinfo) || empty($nodeinfo['links'])) {
385 foreach ($nodeinfo['links'] as $link) {
386 if (!is_array($link) || empty($link['rel']) || empty($link['href'])) {
387 Logger::info('Invalid nodeinfo format', ['url' => $url]);
390 if ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/1.0') {
391 $nodeinfo1_url = $link['href'];
392 } elseif ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0') {
393 $nodeinfo2_url = $link['href'];
397 if ($nodeinfo1_url . $nodeinfo2_url == '') {
403 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
404 if (!empty($nodeinfo2_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo2_url, PHP_URL_HOST))) {
405 $server = self::parseNodeinfo2($nodeinfo2_url);
408 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
409 if (empty($server) && !empty($nodeinfo1_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo1_url, PHP_URL_HOST))) {
410 $server = self::parseNodeinfo1($nodeinfo1_url);
419 * @param string $nodeinfo_url address of the nodeinfo path
420 * @return array Server data
421 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
423 private static function parseNodeinfo1(string $nodeinfo_url)
425 $curlResult = Network::curl($nodeinfo_url);
427 if (!$curlResult->isSuccess()) {
431 $nodeinfo = json_decode($curlResult->getBody(), true);
433 if (!is_array($nodeinfo)) {
439 $server['register_policy'] = Register::CLOSED;
441 if (!empty($nodeinfo['openRegistrations'])) {
442 $server['register_policy'] = Register::OPEN;
445 if (is_array($nodeinfo['software'])) {
446 if (!empty($nodeinfo['software']['name'])) {
447 $server['platform'] = $nodeinfo['software']['name'];
450 if (!empty($nodeinfo['software']['version'])) {
451 $server['version'] = $nodeinfo['software']['version'];
452 // Version numbers on Nodeinfo are presented with additional info, e.g.:
453 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
454 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
458 if (!empty($nodeinfo['metadata']['nodeName'])) {
459 $server['site_name'] = $nodeinfo['metadata']['nodeName'];
462 if (!empty($nodeinfo['usage']['users']['total'])) {
463 $server['registered-users'] = $nodeinfo['usage']['users']['total'];
466 if (!empty($nodeinfo['protocols']['inbound']) && is_array($nodeinfo['protocols']['inbound'])) {
468 foreach ($nodeinfo['protocols']['inbound'] as $protocol) {
469 $protocols[$protocol] = true;
472 if (!empty($protocols['friendica'])) {
473 $server['network'] = Protocol::DFRN;
474 } elseif (!empty($protocols['activitypub'])) {
475 $server['network'] = Protocol::ACTIVITYPUB;
476 } elseif (!empty($protocols['diaspora'])) {
477 $server['network'] = Protocol::DIASPORA;
478 } elseif (!empty($protocols['ostatus'])) {
479 $server['network'] = Protocol::OSTATUS;
480 } elseif (!empty($protocols['gnusocial'])) {
481 $server['network'] = Protocol::OSTATUS;
482 } elseif (!empty($protocols['zot'])) {
483 $server['network'] = Protocol::ZOT;
487 if (empty($server)) {
497 * @param string $nodeinfo_url address of the nodeinfo path
498 * @return array Server data
499 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
501 private static function parseNodeinfo2(string $nodeinfo_url)
503 $curlResult = Network::curl($nodeinfo_url);
504 if (!$curlResult->isSuccess()) {
508 $nodeinfo = json_decode($curlResult->getBody(), true);
510 if (!is_array($nodeinfo)) {
516 $server['register_policy'] = Register::CLOSED;
518 if (!empty($nodeinfo['openRegistrations'])) {
519 $server['register_policy'] = Register::OPEN;
522 if (is_array($nodeinfo['software'])) {
523 if (!empty($nodeinfo['software']['name'])) {
524 $server['platform'] = $nodeinfo['software']['name'];
527 if (!empty($nodeinfo['software']['version'])) {
528 $server['version'] = $nodeinfo['software']['version'];
529 // Version numbers on Nodeinfo are presented with additional info, e.g.:
530 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
531 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
535 if (!empty($nodeinfo['metadata']['nodeName'])) {
536 $server['site_name'] = $nodeinfo['metadata']['nodeName'];
539 if (!empty($nodeinfo['usage']['users']['total'])) {
540 $server['registered-users'] = $nodeinfo['usage']['users']['total'];
543 if (!empty($nodeinfo['protocols'])) {
545 foreach ($nodeinfo['protocols'] as $protocol) {
546 $protocols[$protocol] = true;
549 if (!empty($protocols['friendica'])) {
550 $server['network'] = Protocol::DFRN;
551 } elseif (!empty($protocols['activitypub'])) {
552 $server['network'] = Protocol::ACTIVITYPUB;
553 } elseif (!empty($protocols['diaspora'])) {
554 $server['network'] = Protocol::DIASPORA;
555 } elseif (!empty($protocols['ostatus'])) {
556 $server['network'] = Protocol::OSTATUS;
557 } elseif (!empty($protocols['gnusocial'])) {
558 $server['network'] = Protocol::OSTATUS;
559 } elseif (!empty($protocols['zot'])) {
560 $server['network'] = Protocol::ZOT;
564 if (empty($server)) {
572 * Fetch server information from a 'siteinfo.json' file on the given server
574 * @param string $url URL of the given server
575 * @param array $serverdata array with server data
577 * @return array server data
579 private static function fetchSiteinfo(string $url, array $serverdata)
581 $curlResult = Network::curl($url . '/siteinfo.json');
582 if (!$curlResult->isSuccess()) {
586 $data = json_decode($curlResult->getBody(), true);
591 if (!empty($data['url'])) {
592 $serverdata['platform'] = $data['platform'];
593 $serverdata['version'] = $data['version'];
596 if (!empty($data['plugins'])) {
597 if (in_array('pubcrawl', $data['plugins'])) {
598 $serverdata['network'] = Protocol::ACTIVITYPUB;
599 } elseif (in_array('diaspora', $data['plugins'])) {
600 $serverdata['network'] = Protocol::DIASPORA;
601 } elseif (in_array('gnusoc', $data['plugins'])) {
602 $serverdata['network'] = Protocol::OSTATUS;
604 $serverdata['network'] = Protocol::ZOT;
608 if (!empty($data['site_name'])) {
609 $serverdata['site_name'] = $data['site_name'];
612 if (!empty($data['channels_total'])) {
613 $serverdata['registered-users'] = $data['channels_total'];
616 if (!empty($data['register_policy'])) {
617 switch ($data['register_policy']) {
618 case 'REGISTER_OPEN':
619 $serverdata['register_policy'] = Register::OPEN;
622 case 'REGISTER_APPROVE':
623 $serverdata['register_policy'] = Register::APPROVE;
626 case 'REGISTER_CLOSED':
628 $serverdata['register_policy'] = Register::CLOSED;
637 * Checks if the server contains a valid host meta file
639 * @param string $url URL of the given server
641 * @return boolean 'true' if the server seems to be vital
643 private static function validHostMeta(string $url)
645 $xrd_timeout = Config::get('system', 'xrd_timeout');
646 $curlResult = Network::curl($url . '/.well-known/host-meta', false, ['timeout' => $xrd_timeout]);
647 if (!$curlResult->isSuccess()) {
651 $xrd = XML::parseString($curlResult->getBody(), false);
652 if (!is_object($xrd)) {
656 $elements = XML::elementToArray($xrd);
657 if (empty($elements) || empty($elements['xrd']) || empty($elements['xrd']['link'])) {
662 foreach ($elements['xrd']['link'] as $link) {
663 if (empty($link['rel']) || empty($link['type']) || empty($link['template'])) {
667 if ($link['type'] == 'application/xrd+xml') {
668 // When the webfinger host is the same like the system host, it should be ok.
669 $valid = (parse_url($url, PHP_URL_HOST) == parse_url($link['template'], PHP_URL_HOST));
677 * Detect the network of the given server via their known contacts
679 * @param string $url URL of the given server
680 * @param array $serverdata array with server data
682 * @return array server data
684 private static function detectNetworkViaContacts(string $url, array $serverdata)
688 $gcontacts = DBA::select('gcontact', ['url', 'nurl'], ['server_url' => [$url, $serverdata['nurl']]]);
689 while ($gcontact = DBA::fetch($gcontacts)) {
690 $contacts[$gcontact['nurl']] = $gcontact['url'];
692 DBA::close($gcontacts);
694 $apcontacts = DBA::select('apcontact', ['url'], ['baseurl' => [$url, $serverdata['nurl']]]);
695 while ($gcontact = DBA::fetch($gcontacts)) {
696 $contacts[Strings::normaliseLink($apcontact['url'])] = $apcontact['url'];
698 DBA::close($apcontacts);
700 $pcontacts = DBA::select('contact', ['url', 'nurl'], ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]);
701 while ($gcontact = DBA::fetch($gcontacts)) {
702 $contacts[$pcontact['nurl']] = $pcontact['url'];
704 DBA::close($pcontacts);
706 if (empty($contacts)) {
710 foreach ($contacts as $contact) {
711 $probed = Probe::uri($contact);
712 if (in_array($probed['network'], Protocol::FEDERATED)) {
713 $serverdata['network'] = $probed['network'];
718 $serverdata['registered-users'] = max($serverdata['registered-users'], count($contacts));
724 * Checks if the given server does have a '/poco' endpoint.
725 * This is used for the 'PortableContact' functionality,
726 * which is used by both Friendica and Hubzilla.
728 * @param string $url URL of the given server
729 * @param array $serverdata array with server data
731 * @return array server data
733 private static function checkPoCo(string $url, array $serverdata)
735 $curlResult = Network::curl($url. '/poco');
736 if (!$curlResult->isSuccess()) {
740 $data = json_decode($curlResult->getBody(), true);
745 if (!empty($data['totalResults'])) {
746 $registeredUsers = $serverdata['registered-users'] ?? 0;
747 $serverdata['registered-users'] = max($data['totalResults'], $registeredUsers);
748 $serverdata['poco'] = $url . '/poco';
750 $serverdata['poco'] = '';
757 * Detects the version number of a given server when it was a NextCloud installation
759 * @param string $url URL of the given server
760 * @param array $serverdata array with server data
762 * @return array server data
764 private static function detectNextcloud(string $url, array $serverdata)
766 $curlResult = Network::curl($url . '/status.php');
768 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
772 $data = json_decode($curlResult->getBody(), true);
777 if (!empty($data['version'])) {
778 $serverdata['platform'] = 'nextcloud';
779 $serverdata['version'] = $data['version'];
780 $serverdata['network'] = Protocol::ACTIVITYPUB;
787 * Detects data from a given server url if it was a mastodon alike system
789 * @param string $url URL of the given server
790 * @param array $serverdata array with server data
792 * @return array server data
794 private static function detectMastodonAlikes(string $url, array $serverdata)
796 $curlResult = Network::curl($url . '/api/v1/instance');
798 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
802 $data = json_decode($curlResult->getBody(), true);
807 if (!empty($data['version'])) {
808 $serverdata['platform'] = 'mastodon';
809 $serverdata['version'] = defaults($data, 'version', '');
810 $serverdata['network'] = Protocol::ACTIVITYPUB;
813 if (!empty($data['title'])) {
814 $serverdata['site_name'] = $data['title'];
817 if (!empty($data['description'])) {
818 $serverdata['info'] = trim($data['description']);
821 if (!empty($data['stats']['user_count'])) {
822 $serverdata['registered-users'] = $data['stats']['user_count'];
825 if (!empty($serverdata['version']) && preg_match('/.*?\(compatible;\s(.*)\s(.*)\)/ism', $serverdata['version'], $matches)) {
826 $serverdata['platform'] = $matches[1];
827 $serverdata['version'] = $matches[2];
830 if (!empty($serverdata['version']) && strstr($serverdata['version'], 'Pleroma')) {
831 $serverdata['platform'] = 'pleroma';
832 $serverdata['version'] = trim(str_replace('Pleroma', '', $serverdata['version']));
839 * Detects data from typical Hubzilla endpoints
841 * @param string $url URL of the given server
842 * @param array $serverdata array with server data
844 * @return array server data
846 private static function detectHubzilla(string $url, array $serverdata)
848 $curlResult = Network::curl($url . '/api/statusnet/config.json');
849 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
853 $data = json_decode($curlResult->getBody(), true);
858 if (!empty($data['site']['name'])) {
859 $serverdata['site_name'] = $data['site']['name'];
862 if (!empty($data['site']['platform'])) {
863 $serverdata['platform'] = $data['site']['platform']['PLATFORM_NAME'];
864 $serverdata['version'] = $data['site']['platform']['STD_VERSION'];
865 $serverdata['network'] = Protocol::ZOT;
868 if (!empty($data['site']['hubzilla'])) {
869 $serverdata['platform'] = $data['site']['hubzilla']['PLATFORM_NAME'];
870 $serverdata['version'] = $data['site']['hubzilla']['RED_VERSION'];
871 $serverdata['network'] = Protocol::ZOT;
874 if (!empty($data['site']['redmatrix'])) {
875 if (!empty($data['site']['redmatrix']['PLATFORM_NAME'])) {
876 $serverdata['platform'] = $data['site']['redmatrix']['PLATFORM_NAME'];
877 } elseif (!empty($data['site']['redmatrix']['RED_PLATFORM'])) {
878 $serverdata['platform'] = $data['site']['redmatrix']['RED_PLATFORM'];
881 $serverdata['version'] = $data['site']['redmatrix']['RED_VERSION'];
882 $serverdata['network'] = Protocol::ZOT;
889 if (!empty($data['site']['closed'])) {
890 $closed = self::toBoolean($data['site']['closed']);
893 if (!empty($data['site']['private'])) {
894 $private = self::toBoolean($data['site']['private']);
897 if (!empty($data['site']['inviteonly'])) {
898 $inviteonly = self::toBoolean($data['site']['inviteonly']);
901 if (!$closed && !$private and $inviteonly) {
902 $register_policy = Register::APPROVE;
903 } elseif (!$closed && !$private) {
904 $register_policy = Register::OPEN;
906 $register_policy = Register::CLOSED;
913 * Converts input value to a boolean value
915 * @param string|integer $val
919 private static function toBoolean($val)
921 if (($val == 'true') || ($val == 1)) {
923 } elseif (($val == 'false') || ($val == 0)) {
931 * Detect if the URL belongs to a GNU Social server
933 * @param string $url URL of the given server
934 * @param array $serverdata array with server data
936 * @return array server data
938 private static function detectGNUSocial(string $url, array $serverdata)
940 $curlResult = Network::curl($url . '/api/statusnet/version.json');
942 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
943 ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
944 $serverdata['platform'] = 'StatusNet';
945 // Remove junk that some GNU Social servers return
946 $serverdata['version'] = str_replace(chr(239).chr(187).chr(191), '', $curlResult->getBody());
947 $serverdata['version'] = trim($serverdata['version'], '"');
948 $serverdata['network'] = Protocol::OSTATUS;
951 // Test for GNU Social
952 $curlResult = Network::curl($url . '/api/gnusocial/version.json');
954 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
955 ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
956 $serverdata['platform'] = 'GNU Social';
957 // Remove junk that some GNU Social servers return
958 $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBody());
959 $serverdata['version'] = trim($serverdata['version'], '"');
960 $serverdata['network'] = Protocol::OSTATUS;
967 * Detect if the URL belongs to a Friendica server
969 * @param string $url URL of the given server
970 * @param array $serverdata array with server data
972 * @return array server data
974 private static function detectFriendica(string $url, array $serverdata)
976 $curlResult = Network::curl($url . '/friendica/json');
977 if (!$curlResult->isSuccess()) {
978 $curlResult = Network::curl($url . '/friendika/json');
981 if (!$curlResult->isSuccess()) {
985 $data = json_decode($curlResult->getBody(), true);
986 if (empty($data) || empty($data['version'])) {
990 $serverdata['network'] = Protocol::DFRN;
991 $serverdata['version'] = $data['version'];
993 if (!empty($data['no_scrape_url'])) {
994 $serverdata['noscrape'] = $data['no_scrape_url'];
997 if (!empty($data['site_name'])) {
998 $serverdata['site_name'] = $data['site_name'];
1001 if (!empty($data['info'])) {
1002 $serverdata['info'] = trim($data['info']);
1005 $register_policy = defaults($data, 'register_policy', 'REGISTER_CLOSED');
1006 switch ($register_policy) {
1007 case 'REGISTER_OPEN':
1008 $serverdata['register_policy'] = Register::OPEN;
1011 case 'REGISTER_APPROVE':
1012 $serverdata['register_policy'] = Register::APPROVE;
1015 case 'REGISTER_CLOSED':
1016 case 'REGISTER_INVITATION':
1017 $serverdata['register_policy'] = Register::CLOSED;
1020 Logger::info('Register policy is invalid', ['policy' => $register_policy, 'server' => $url]);
1021 $serverdata['register_policy'] = Register::CLOSED;
1025 $serverdata['platform'] = defaults($data, 'platform', '');
1031 * Analyses the landing page of a given server for hints about type and system of that server
1033 * @param object $curlResult result of curl execution
1034 * @param array $serverdata array with server data
1036 * @return array server data
1038 private static function analyseRootBody($curlResult, array $serverdata)
1040 $doc = new DOMDocument();
1041 @$doc->loadHTML($curlResult->getBody());
1042 $xpath = new DOMXPath($doc);
1044 $title = trim(XML::getFirstNodeValue($xpath, '//head/title/text()'));
1045 if (!empty($title)) {
1046 $serverdata['site_name'] = $title;
1049 $list = $xpath->query('//meta[@name]');
1051 foreach ($list as $node) {
1053 if ($node->attributes->length) {
1054 foreach ($node->attributes as $attribute) {
1055 $attribute->value = trim($attribute->value);
1056 if (empty($attribute->value)) {
1060 $attr[$attribute->name] = $attribute->value;
1063 if (empty($attr['name']) || empty($attr['content'])) {
1068 if ($attr['name'] == 'description') {
1069 $serverdata['info'] = $attr['content'];
1072 if ($attr['name'] == 'application-name') {
1073 $serverdata['platform'] = $attr['content'];
1074 if (in_array($attr['content'], ['Misskey', 'Write.as'])) {
1075 $serverdata['network'] = Protocol::ACTIVITYPUB;
1079 if ($attr['name'] == 'generator') {
1080 $serverdata['platform'] = $attr['content'];
1082 $version_part = explode(' ', $attr['content']);
1084 if (count($version_part) == 2) {
1085 if (in_array($version_part[0], ['WordPress'])) {
1086 $serverdata['platform'] = $version_part[0];
1087 $serverdata['version'] = $version_part[1];
1088 $serverdata['network'] = Protocol::ACTIVITYPUB;
1090 if (in_array($version_part[0], ['Friendika', 'Friendica'])) {
1091 $serverdata['platform'] = $version_part[0];
1092 $serverdata['version'] = $version_part[1];
1093 $serverdata['network'] = Protocol::DFRN;
1099 $list = $xpath->query('//meta[@property]');
1101 foreach ($list as $node) {
1103 if ($node->attributes->length) {
1104 foreach ($node->attributes as $attribute) {
1105 $attribute->value = trim($attribute->value);
1106 if (empty($attribute->value)) {
1110 $attr[$attribute->name] = $attribute->value;
1113 if (empty($attr['property']) || empty($attr['content'])) {
1118 if ($attr['property'] == 'og:site_name') {
1119 $serverdata['site_name'] = $attr['content'];
1122 if ($attr['property'] == 'og:description') {
1123 $serverdata['info'] = $attr['content'];
1126 if ($attr['property'] == 'og:platform') {
1127 $serverdata['platform'] = $attr['content'];
1129 if (in_array($attr['content'], ['PeerTube'])) {
1130 $serverdata['network'] = Protocol::ACTIVITYPUB;
1134 if ($attr['property'] == 'generator') {
1135 $serverdata['platform'] = $attr['content'];
1137 if (in_array($attr['content'], ['hubzilla'])) {
1138 // We later check which compatible protocol modules are loaded.
1139 $serverdata['network'] = Protocol::ZOT;
1148 * Analyses the header data of a given server for hints about type and system of that server
1150 * @param object $curlResult result of curl execution
1151 * @param array $serverdata array with server data
1153 * @return array server data
1155 private static function analyseRootHeader($curlResult, array $serverdata)
1157 if ($curlResult->getHeader('server') == 'Mastodon') {
1158 $serverdata['platform'] = 'mastodon';
1159 $serverdata['network'] = $network = Protocol::ACTIVITYPUB;
1160 } elseif ($curlResult->inHeader('x-diaspora-version')) {
1161 $serverdata['platform'] = 'diaspora';
1162 $serverdata['network'] = $network = Protocol::DIASPORA;
1163 $serverdata['version'] = $curlResult->getHeader('x-diaspora-version');
1165 } elseif ($curlResult->inHeader('x-friendica-version')) {
1166 $serverdata['platform'] = 'friendica';
1167 $serverdata['network'] = $network = Protocol::DFRN;
1168 $serverdata['version'] = $curlResult->getHeader('x-friendica-version');