]> git.mxchange.org Git - friendica.git/blob - src/Model/GServer.php
f750ed99ea778b31034135c02850e3ad941c2622
[friendica.git] / src / Model / GServer.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
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.
11  *
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.
16  *
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/>.
19  *
20  */
21
22 namespace Friendica\Model;
23
24 use DOMDocument;
25 use DOMXPath;
26 use Friendica\Core\Protocol;
27 use Friendica\Core\Worker;
28 use Friendica\Database\DBA;
29 use Friendica\DI;
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\Protocol\PortableContact;
38 use Friendica\Protocol\Diaspora;
39 use Friendica\Network\Probe;
40
41 /**
42  * This class handles GServer related functions
43  */
44 class GServer
45 {
46         // Directory types
47         const DT_NONE = 0;
48         const DT_POCO = 1;
49         const DT_MASTODON = 2;
50         /**
51          * Checks if the given server is reachable
52          *
53          * @param string  $profile URL of the given profile
54          * @param string  $server  URL of the given server (If empty, taken from profile)
55          * @param string  $network Network value that is used, when detection failed
56          * @param boolean $force   Force an update.
57          *
58          * @return boolean 'true' if server seems vital
59          */
60         public static function reachable(string $profile, string $server = '', string $network = '', bool $force = false)
61         {
62                 if ($server == '') {
63                         $server = GContact::getBasepath($profile);
64                 }
65
66                 if ($server == '') {
67                         return true;
68                 }
69
70                 return self::check($server, $network, $force);
71         }
72
73         /**
74          * Decides if a server needs to be updated, based upon several date fields
75          *
76          * @param date $created      Creation date of that server entry
77          * @param date $updated      When had the server entry be updated
78          * @param date $last_failure Last failure when contacting that server
79          * @param date $last_contact Last time the server had been contacted
80          *
81          * @return boolean Does the server record needs an update?
82          */
83         public static function updateNeeded($created, $updated, $last_failure, $last_contact)
84         {
85                 $now = strtotime(DateTimeFormat::utcNow());
86
87                 if ($updated > $last_contact) {
88                         $contact_time = strtotime($updated);
89                 } else {
90                         $contact_time = strtotime($last_contact);
91                 }
92
93                 $failure_time = strtotime($last_failure);
94                 $created_time = strtotime($created);
95
96                 // If there is no "created" time then use the current time
97                 if ($created_time <= 0) {
98                         $created_time = $now;
99                 }
100
101                 // If the last contact was less than 24 hours then don't update
102                 if (($now - $contact_time) < (60 * 60 * 24)) {
103                         return false;
104                 }
105
106                 // If the last failure was less than 24 hours then don't update
107                 if (($now - $failure_time) < (60 * 60 * 24)) {
108                         return false;
109                 }
110
111                 // If the last contact was less than a week ago and the last failure is older than a week then don't update
112                 //if ((($now - $contact_time) < (60 * 60 * 24 * 7)) && ($contact_time > $failure_time))
113                 //      return false;
114
115                 // 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
116                 if ((($now - $contact_time) > (60 * 60 * 24 * 7)) && (($now - $created_time) > (60 * 60 * 24 * 7)) && (($now - $failure_time) < (60 * 60 * 24 * 7))) {
117                         return false;
118                 }
119
120                 // 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
121                 if ((($now - $contact_time) > (60 * 60 * 24 * 30)) && (($now - $created_time) > (60 * 60 * 24 * 30)) && (($now - $failure_time) < (60 * 60 * 24 * 30))) {
122                         return false;
123                 }
124
125                 return true;
126         }
127
128         /**
129          * Checks the state of the given server.
130          *
131          * @param string  $server_url URL of the given server
132          * @param string  $network    Network value that is used, when detection failed
133          * @param boolean $force      Force an update.
134          *
135          * @return boolean 'true' if server seems vital
136          */
137         public static function check(string $server_url, string $network = '', bool $force = false)
138         {
139                 // Unify the server address
140                 $server_url = trim($server_url, '/');
141                 $server_url = str_replace('/index.php', '', $server_url);
142
143                 if ($server_url == '') {
144                         return false;
145                 }
146
147                 $gserver = DBA::selectFirst('gserver', [], ['nurl' => Strings::normaliseLink($server_url)]);
148                 if (DBA::isResult($gserver)) {
149                         if ($gserver['created'] <= DBA::NULL_DATETIME) {
150                                 $fields = ['created' => DateTimeFormat::utcNow()];
151                                 $condition = ['nurl' => Strings::normaliseLink($server_url)];
152                                 DBA::update('gserver', $fields, $condition);
153                         }
154
155                         $last_contact = $gserver['last_contact'];
156                         $last_failure = $gserver['last_failure'];
157
158                         // See discussion under https://forum.friendi.ca/display/0b6b25a8135aabc37a5a0f5684081633
159                         // It can happen that a zero date is in the database, but storing it again is forbidden.
160                         if ($last_contact < DBA::NULL_DATETIME) {
161                                 $last_contact = DBA::NULL_DATETIME;
162                         }
163
164                         if ($last_failure < DBA::NULL_DATETIME) {
165                                 $last_failure = DBA::NULL_DATETIME;
166                         }
167
168                         if (!$force && !self::updateNeeded($gserver['created'], '', $last_failure, $last_contact)) {
169                                 Logger::info('No update needed', ['server' => $server_url]);
170                                 return ($last_contact >= $last_failure);
171                         }
172                         Logger::info('Server is outdated. Start discovery.', ['Server' => $server_url, 'Force' => $force, 'Created' => $gserver['created'], 'Failure' => $last_failure, 'Contact' => $last_contact]);
173                 } else {
174                         Logger::info('Server is unknown. Start discovery.', ['Server' => $server_url]);
175                 }
176
177                 return self::detect($server_url, $network);
178         }
179
180         /**
181          * Detect server data (type, protocol, version number, ...)
182          * The detected data is then updated or inserted in the gserver table.
183          *
184          * @param string  $url     URL of the given server
185          * @param string  $network Network value that is used, when detection failed
186          *
187          * @return boolean 'true' if server could be detected
188          */
189         public static function detect(string $url, string $network = '')
190         {
191                 Logger::info('Detect server type', ['server' => $url]);
192                 $serverdata = [];
193
194                 $original_url = $url;
195
196                 // Remove URL content that is not supposed to exist for a server url
197                 $urlparts = parse_url($url);
198                 unset($urlparts['user']);
199                 unset($urlparts['pass']);
200                 unset($urlparts['query']);
201                 unset($urlparts['fragment']);
202                 $url = Network::unparseURL($urlparts);
203
204                 // If the URL missmatches, then we mark the old entry as failure
205                 if ($url != $original_url) {
206                         DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($original_url)]);
207                 }
208
209                 // When a nodeinfo is present, we don't need to dig further
210                 $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
211                 $curlResult = Network::curl($url . '/.well-known/nodeinfo', false, ['timeout' => $xrd_timeout]);
212                 if ($curlResult->isTimeout()) {
213                         DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
214                         return false;
215                 }
216
217                 $nodeinfo = self::fetchNodeinfo($url, $curlResult);
218
219                 // When nodeinfo isn't present, we use the older 'statistics.json' endpoint
220                 if (empty($nodeinfo)) {
221                         $nodeinfo = self::fetchStatistics($url);
222                 }
223
224                 // If that didn't work out well, we use some protocol specific endpoints
225                 // For Friendica and Zot based networks we have to dive deeper to reveal more details
226                 if (empty($nodeinfo['network']) || in_array($nodeinfo['network'], [Protocol::DFRN, Protocol::ZOT])) {
227                         // Fetch the landing page, possibly it reveals some data
228                         if (empty($nodeinfo['network'])) {
229                                 $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout]);
230                                 if ($curlResult->isSuccess()) {
231                                         $serverdata = self::analyseRootHeader($curlResult, $serverdata);
232                                         $serverdata = self::analyseRootBody($curlResult, $serverdata, $url);
233                                 }
234
235                                 if (!$curlResult->isSuccess() || empty($curlResult->getBody()) || self::invalidBody($curlResult->getBody())) {
236                                         DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
237                                         return false;
238                                 }
239                         }
240
241                         if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ACTIVITYPUB)) {
242                                 $serverdata = self::detectMastodonAlikes($url, $serverdata);
243                         }
244
245                         // All following checks are done for systems that always have got a "host-meta" endpoint.
246                         // With this check we don't have to waste time and ressources for dead systems.
247                         // Also this hopefully prevents us from receiving abuse messages.
248                         if (empty($serverdata['network']) && !self::validHostMeta($url)) {
249                                 DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
250                                 return false;
251                         }
252
253                         if (empty($serverdata['network']) || in_array($serverdata['network'], [Protocol::DFRN, Protocol::ACTIVITYPUB])) {
254                                 $serverdata = self::detectFriendica($url, $serverdata);
255                         }
256
257                         // the 'siteinfo.json' is some specific endpoint of Hubzilla and Red
258                         if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ZOT)) {
259                                 $serverdata = self::fetchSiteinfo($url, $serverdata);
260                         }
261
262                         // The 'siteinfo.json' doesn't seem to be present on older Hubzilla installations
263                         if (empty($serverdata['network'])) {
264                                 $serverdata = self::detectHubzilla($url, $serverdata);
265                         }
266
267                         if (empty($serverdata['network'])) {
268                                 $serverdata = self::detectNextcloud($url, $serverdata);
269                         }
270
271                         if (empty($serverdata['network'])) {
272                                 $serverdata = self::detectGNUSocial($url, $serverdata);
273                         }
274                 } else {
275                         $serverdata = $nodeinfo;
276                 }
277
278                 // Detect the directory type
279                 $serverdata['directory-type'] = self::DT_NONE;
280                 $serverdata = self::checkPoCo($url, $serverdata);
281                 $serverdata = self::checkMastodonDirectory($url, $serverdata);
282
283                 // We can't detect the network type. Possibly it is some system that we don't know yet
284                 if (empty($serverdata['network'])) {
285                         $serverdata['network'] = Protocol::PHANTOM;
286                 }
287
288                 // When we hadn't been able to detect the network type, we use the hint from the parameter
289                 if (($serverdata['network'] == Protocol::PHANTOM) && !empty($network)) {
290                         $serverdata['network'] = $network;
291                 }
292
293                 $serverdata['url'] = $url;
294                 $serverdata['nurl'] = Strings::normaliseLink($url);
295
296                 // We take the highest number that we do find
297                 $registeredUsers = $serverdata['registered-users'] ?? 0;
298
299                 // On an active server there has to be at least a single user
300                 if (($serverdata['network'] != Protocol::PHANTOM) && ($registeredUsers == 0)) {
301                         $registeredUsers = 1;
302                 }
303
304                 if ($serverdata['network'] != Protocol::PHANTOM) {
305                         $gcontacts = DBA::count('gcontact', ['server_url' => [$url, $serverdata['nurl']]]);
306                         $apcontacts = DBA::count('apcontact', ['baseurl' => [$url, $serverdata['nurl']]]);
307                         $contacts = DBA::count('contact', ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]);
308                         $serverdata['registered-users'] = max($gcontacts, $apcontacts, $contacts, $registeredUsers);
309                 } else {
310                         $serverdata['registered-users'] = $registeredUsers;
311                         $serverdata = self::detectNetworkViaContacts($url, $serverdata);
312                 }
313
314                 $serverdata['last_contact'] = DateTimeFormat::utcNow();
315
316                 $gserver = DBA::selectFirst('gserver', ['network'], ['nurl' => Strings::normaliseLink($url)]);
317                 if (!DBA::isResult($gserver)) {
318                         $serverdata['created'] = DateTimeFormat::utcNow();
319                         $ret = DBA::insert('gserver', $serverdata);
320                 } else {
321                         // Don't override the network with 'unknown' when there had been a valid entry before
322                         if (($serverdata['network'] == Protocol::PHANTOM) && !empty($gserver['network'])) {
323                                 unset($serverdata['network']);
324                         }
325
326                         $ret = DBA::update('gserver', $serverdata, ['nurl' => $serverdata['nurl']]);
327                 }
328
329                 if (!empty($serverdata['network']) && in_array($serverdata['network'], [Protocol::DFRN, Protocol::DIASPORA])) {
330                         self::discoverRelay($url);
331                 }
332
333                 return $ret;
334         }
335
336         /**
337          * Fetch relay data from a given server url
338          *
339          * @param string $server_url address of the server
340          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
341          */
342         private static function discoverRelay(string $server_url)
343         {
344                 Logger::info('Discover relay data', ['server' => $server_url]);
345
346                 $curlResult = Network::curl($server_url . '/.well-known/x-social-relay');
347                 if (!$curlResult->isSuccess()) {
348                         return;
349                 }
350
351                 $data = json_decode($curlResult->getBody(), true);
352                 if (!is_array($data)) {
353                         return;
354                 }
355
356                 // Sanitize incoming data, see https://github.com/friendica/friendica/issues/8565
357                 $data['subscribe'] = (bool)$data['subscribe'] ?? false;
358
359                 if (!$data['subscribe'] || empty($data['scope']) || !in_array(strtolower($data['scope']), ['all', 'tags'])) {
360                         $data['scope'] = '';
361                         $data['subscribe'] = false;
362                         $data['tags'] = [];
363                 }
364
365                 $gserver = DBA::selectFirst('gserver', ['id', 'relay-subscribe', 'relay-scope'], ['nurl' => Strings::normaliseLink($server_url)]);
366                 if (!DBA::isResult($gserver)) {
367                         return;
368                 }
369
370                 if (($gserver['relay-subscribe'] != $data['subscribe']) || ($gserver['relay-scope'] != $data['scope'])) {
371                         $fields = ['relay-subscribe' => $data['subscribe'], 'relay-scope' => $data['scope']];
372                         DBA::update('gserver', $fields, ['id' => $gserver['id']]);
373                 }
374
375                 DBA::delete('gserver-tag', ['gserver-id' => $gserver['id']]);
376
377                 if ($data['scope'] == 'tags') {
378                         // Avoid duplicates
379                         $tags = [];
380                         foreach ($data['tags'] as $tag) {
381                                 $tag = mb_strtolower($tag);
382                                 if (strlen($tag) < 100) {
383                                         $tags[$tag] = $tag;
384                                 }
385                         }
386
387                         foreach ($tags as $tag) {
388                                 DBA::insert('gserver-tag', ['gserver-id' => $gserver['id'], 'tag' => $tag], true);
389                         }
390                 }
391
392                 // Create or update the relay contact
393                 $fields = [];
394                 if (isset($data['protocols'])) {
395                         if (isset($data['protocols']['diaspora'])) {
396                                 $fields['network'] = Protocol::DIASPORA;
397
398                                 if (isset($data['protocols']['diaspora']['receive'])) {
399                                         $fields['batch'] = $data['protocols']['diaspora']['receive'];
400                                 } elseif (is_string($data['protocols']['diaspora'])) {
401                                         $fields['batch'] = $data['protocols']['diaspora'];
402                                 }
403                         }
404
405                         if (isset($data['protocols']['dfrn'])) {
406                                 $fields['network'] = Protocol::DFRN;
407
408                                 if (isset($data['protocols']['dfrn']['receive'])) {
409                                         $fields['batch'] = $data['protocols']['dfrn']['receive'];
410                                 } elseif (is_string($data['protocols']['dfrn'])) {
411                                         $fields['batch'] = $data['protocols']['dfrn'];
412                                 }
413                         }
414                 }
415                 Diaspora::setRelayContact($server_url, $fields);
416         }
417
418         /**
419          * Fetch server data from '/statistics.json' on the given server
420          *
421          * @param string $url URL of the given server
422          *
423          * @return array server data
424          */
425         private static function fetchStatistics(string $url)
426         {
427                 $curlResult = Network::curl($url . '/statistics.json');
428                 if (!$curlResult->isSuccess()) {
429                         return [];
430                 }
431
432                 $data = json_decode($curlResult->getBody(), true);
433                 if (empty($data)) {
434                         return [];
435                 }
436
437                 $serverdata = [];
438
439                 if (!empty($data['version'])) {
440                         $serverdata['version'] = $data['version'];
441                         // Version numbers on statistics.json are presented with additional info, e.g.:
442                         // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
443                         $serverdata['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $serverdata['version']);
444                 }
445
446                 if (!empty($data['name'])) {
447                         $serverdata['site_name'] = $data['name'];
448                 }
449
450                 if (!empty($data['network'])) {
451                         $serverdata['platform'] = strtolower($data['network']);
452
453                         if ($serverdata['platform'] == 'diaspora') {
454                                 $serverdata['network'] = Protocol::DIASPORA;
455                         } elseif ($serverdata['platform'] == 'friendica') {
456                                 $serverdata['network'] = Protocol::DFRN;
457                         } elseif ($serverdata['platform'] == 'hubzilla') {
458                                 $serverdata['network'] = Protocol::ZOT;
459                         } elseif ($serverdata['platform'] == 'redmatrix') {
460                                 $serverdata['network'] = Protocol::ZOT;
461                         }
462                 }
463
464
465                 if (!empty($data['registrations_open'])) {
466                         $serverdata['register_policy'] = Register::OPEN;
467                 } else {
468                         $serverdata['register_policy'] = Register::CLOSED;
469                 }
470
471                 return $serverdata;
472         }
473
474         /**
475          * Detect server type by using the nodeinfo data
476          *
477          * @param string     $url        address of the server
478          * @param CurlResult $curlResult
479          * @return array Server data
480          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
481          */
482         private static function fetchNodeinfo(string $url, CurlResult $curlResult)
483         {
484                 $nodeinfo = json_decode($curlResult->getBody(), true);
485
486                 if (!is_array($nodeinfo) || empty($nodeinfo['links'])) {
487                         return [];
488                 }
489
490                 $nodeinfo1_url = '';
491                 $nodeinfo2_url = '';
492
493                 foreach ($nodeinfo['links'] as $link) {
494                         if (!is_array($link) || empty($link['rel']) || empty($link['href'])) {
495                                 Logger::info('Invalid nodeinfo format', ['url' => $url]);
496                                 continue;
497                         }
498                         if ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/1.0') {
499                                 $nodeinfo1_url = $link['href'];
500                         } elseif ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0') {
501                                 $nodeinfo2_url = $link['href'];
502                         }
503                 }
504
505                 if ($nodeinfo1_url . $nodeinfo2_url == '') {
506                         return [];
507                 }
508
509                 $server = [];
510
511                 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
512                 if (!empty($nodeinfo2_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo2_url, PHP_URL_HOST))) {
513                         $server = self::parseNodeinfo2($nodeinfo2_url);
514                 }
515
516                 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
517                 if (empty($server) && !empty($nodeinfo1_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo1_url, PHP_URL_HOST))) {
518                         $server = self::parseNodeinfo1($nodeinfo1_url);
519                 }
520
521                 return $server;
522         }
523
524         /**
525          * Parses Nodeinfo 1
526          *
527          * @param string $nodeinfo_url address of the nodeinfo path
528          * @return array Server data
529          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
530          */
531         private static function parseNodeinfo1(string $nodeinfo_url)
532         {
533                 $curlResult = Network::curl($nodeinfo_url);
534
535                 if (!$curlResult->isSuccess()) {
536                         return [];
537                 }
538
539                 $nodeinfo = json_decode($curlResult->getBody(), true);
540
541                 if (!is_array($nodeinfo)) {
542                         return [];
543                 }
544
545                 $server = [];
546
547                 $server['register_policy'] = Register::CLOSED;
548
549                 if (!empty($nodeinfo['openRegistrations'])) {
550                         $server['register_policy'] = Register::OPEN;
551                 }
552
553                 if (is_array($nodeinfo['software'])) {
554                         if (!empty($nodeinfo['software']['name'])) {
555                                 $server['platform'] = strtolower($nodeinfo['software']['name']);
556                         }
557
558                         if (!empty($nodeinfo['software']['version'])) {
559                                 $server['version'] = $nodeinfo['software']['version'];
560                                 // Version numbers on Nodeinfo are presented with additional info, e.g.:
561                                 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
562                                 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
563                         }
564                 }
565
566                 if (!empty($nodeinfo['metadata']['nodeName'])) {
567                         $server['site_name'] = $nodeinfo['metadata']['nodeName'];
568                 }
569
570                 if (!empty($nodeinfo['usage']['users']['total'])) {
571                         $server['registered-users'] = $nodeinfo['usage']['users']['total'];
572                 }
573
574                 if (!empty($nodeinfo['protocols']['inbound']) && is_array($nodeinfo['protocols']['inbound'])) {
575                         $protocols = [];
576                         foreach ($nodeinfo['protocols']['inbound'] as $protocol) {
577                                 $protocols[$protocol] = true;
578                         }
579
580                         if (!empty($protocols['friendica'])) {
581                                 $server['network'] = Protocol::DFRN;
582                         } elseif (!empty($protocols['activitypub'])) {
583                                 $server['network'] = Protocol::ACTIVITYPUB;
584                         } elseif (!empty($protocols['diaspora'])) {
585                                 $server['network'] = Protocol::DIASPORA;
586                         } elseif (!empty($protocols['ostatus'])) {
587                                 $server['network'] = Protocol::OSTATUS;
588                         } elseif (!empty($protocols['gnusocial'])) {
589                                 $server['network'] = Protocol::OSTATUS;
590                         } elseif (!empty($protocols['zot'])) {
591                                 $server['network'] = Protocol::ZOT;
592                         }
593                 }
594
595                 if (empty($server)) {
596                         return [];
597                 }
598
599                 return $server;
600         }
601
602         /**
603          * Parses Nodeinfo 2
604          *
605          * @param string $nodeinfo_url address of the nodeinfo path
606          * @return array Server data
607          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
608          */
609         private static function parseNodeinfo2(string $nodeinfo_url)
610         {
611                 $curlResult = Network::curl($nodeinfo_url);
612                 if (!$curlResult->isSuccess()) {
613                         return [];
614                 }
615
616                 $nodeinfo = json_decode($curlResult->getBody(), true);
617
618                 if (!is_array($nodeinfo)) {
619                         return [];
620                 }
621
622                 $server = [];
623
624                 $server['register_policy'] = Register::CLOSED;
625
626                 if (!empty($nodeinfo['openRegistrations'])) {
627                         $server['register_policy'] = Register::OPEN;
628                 }
629
630                 if (is_array($nodeinfo['software'])) {
631                         if (!empty($nodeinfo['software']['name'])) {
632                                 $server['platform'] = strtolower($nodeinfo['software']['name']);
633                         }
634
635                         if (!empty($nodeinfo['software']['version'])) {
636                                 $server['version'] = $nodeinfo['software']['version'];
637                                 // Version numbers on Nodeinfo are presented with additional info, e.g.:
638                                 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
639                                 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
640                         }
641                 }
642
643                 if (!empty($nodeinfo['metadata']['nodeName'])) {
644                         $server['site_name'] = $nodeinfo['metadata']['nodeName'];
645                 }
646
647                 if (!empty($nodeinfo['usage']['users']['total'])) {
648                         $server['registered-users'] = $nodeinfo['usage']['users']['total'];
649                 }
650
651                 if (!empty($nodeinfo['protocols'])) {
652                         $protocols = [];
653                         foreach ($nodeinfo['protocols'] as $protocol) {
654                                 $protocols[$protocol] = true;
655                         }
656
657                         if (!empty($protocols['dfrn'])) {
658                                 $server['network'] = Protocol::DFRN;
659                         } elseif (!empty($protocols['activitypub'])) {
660                                 $server['network'] = Protocol::ACTIVITYPUB;
661                         } elseif (!empty($protocols['diaspora'])) {
662                                 $server['network'] = Protocol::DIASPORA;
663                         } elseif (!empty($protocols['ostatus'])) {
664                                 $server['network'] = Protocol::OSTATUS;
665                         } elseif (!empty($protocols['gnusocial'])) {
666                                 $server['network'] = Protocol::OSTATUS;
667                         } elseif (!empty($protocols['zot'])) {
668                                 $server['network'] = Protocol::ZOT;
669                         }
670                 }
671
672                 if (empty($server)) {
673                         return [];
674                 }
675
676                 return $server;
677         }
678
679         /**
680          * Fetch server information from a 'siteinfo.json' file on the given server
681          *
682          * @param string $url        URL of the given server
683          * @param array  $serverdata array with server data
684          *
685          * @return array server data
686          */
687         private static function fetchSiteinfo(string $url, array $serverdata)
688         {
689                 $curlResult = Network::curl($url . '/siteinfo.json');
690                 if (!$curlResult->isSuccess()) {
691                         return $serverdata;
692                 }
693
694                 $data = json_decode($curlResult->getBody(), true);
695                 if (empty($data)) {
696                         return $serverdata;
697                 }
698
699                 if (!empty($data['url'])) {
700                         $serverdata['platform'] = strtolower($data['platform']);
701                         $serverdata['version'] = $data['version'];
702                 }
703
704                 if (!empty($data['plugins'])) {
705                         if (in_array('pubcrawl', $data['plugins'])) {
706                                 $serverdata['network'] = Protocol::ACTIVITYPUB;
707                         } elseif (in_array('diaspora', $data['plugins'])) {
708                                 $serverdata['network'] = Protocol::DIASPORA;
709                         } elseif (in_array('gnusoc', $data['plugins'])) {
710                                 $serverdata['network'] = Protocol::OSTATUS;
711                         } else {
712                                 $serverdata['network'] = Protocol::ZOT;
713                         }
714                 }
715
716                 if (!empty($data['site_name'])) {
717                         $serverdata['site_name'] = $data['site_name'];
718                 }
719
720                 if (!empty($data['channels_total'])) {
721                         $serverdata['registered-users'] = $data['channels_total'];
722                 }
723
724                 if (!empty($data['register_policy'])) {
725                         switch ($data['register_policy']) {
726                                 case 'REGISTER_OPEN':
727                                         $serverdata['register_policy'] = Register::OPEN;
728                                         break;
729
730                                 case 'REGISTER_APPROVE':
731                                         $serverdata['register_policy'] = Register::APPROVE;
732                                         break;
733
734                                 case 'REGISTER_CLOSED':
735                                 default:
736                                         $serverdata['register_policy'] = Register::CLOSED;
737                                         break;
738                         }
739                 }
740
741                 return $serverdata;
742         }
743
744         /**
745          * Checks if the server contains a valid host meta file
746          *
747          * @param string $url URL of the given server
748          *
749          * @return boolean 'true' if the server seems to be vital
750          */
751         private static function validHostMeta(string $url)
752         {
753                 $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
754                 $curlResult = Network::curl($url . '/.well-known/host-meta', false, ['timeout' => $xrd_timeout]);
755                 if (!$curlResult->isSuccess()) {
756                         return false;
757                 }
758
759                 $xrd = XML::parseString($curlResult->getBody());
760                 if (!is_object($xrd)) {
761                         return false;
762                 }
763
764                 $elements = XML::elementToArray($xrd);
765                 if (empty($elements) || empty($elements['xrd']) || empty($elements['xrd']['link'])) {
766                         return false;
767                 }
768
769                 $valid = false;
770                 foreach ($elements['xrd']['link'] as $link) {
771                         // When there is more than a single "link" element, the array looks slightly different
772                         if (!empty($link['@attributes'])) {
773                                 $link = $link['@attributes'];
774                         }
775
776                         if (empty($link['rel']) || empty($link['template'])) {
777                                 continue;
778                         }
779
780                         if ($link['rel'] == 'lrdd') {
781                                 // When the webfinger host is the same like the system host, it should be ok.
782                                 $valid = (parse_url($url, PHP_URL_HOST) == parse_url($link['template'], PHP_URL_HOST));
783                         }
784                 }
785
786                 return $valid;
787         }
788
789         /**
790          * Detect the network of the given server via their known contacts
791          *
792          * @param string $url        URL of the given server
793          * @param array  $serverdata array with server data
794          *
795          * @return array server data
796          */
797         private static function detectNetworkViaContacts(string $url, array $serverdata)
798         {
799                 $contacts = [];
800
801                 $gcontacts = DBA::select('gcontact', ['url', 'nurl'], ['server_url' => [$url, $serverdata['nurl']]]);
802                 while ($gcontact = DBA::fetch($gcontacts)) {
803                         $contacts[$gcontact['nurl']] = $gcontact['url'];
804                 }
805                 DBA::close($gcontacts);
806
807                 $apcontacts = DBA::select('apcontact', ['url'], ['baseurl' => [$url, $serverdata['nurl']]]);
808                 while ($apcontact = DBA::fetch($apcontacts)) {
809                         $contacts[Strings::normaliseLink($apcontact['url'])] = $apcontact['url'];
810                 }
811                 DBA::close($apcontacts);
812
813                 $pcontacts = DBA::select('contact', ['url', 'nurl'], ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]);
814                 while ($pcontact = DBA::fetch($pcontacts)) {
815                         $contacts[$pcontact['nurl']] = $pcontact['url'];
816                 }
817                 DBA::close($pcontacts);
818
819                 if (empty($contacts)) {
820                         return $serverdata;
821                 }
822
823                 foreach ($contacts as $contact) {
824                         $probed = Probe::uri($contact);
825                         if (in_array($probed['network'], Protocol::FEDERATED)) {
826                                 $serverdata['network'] = $probed['network'];
827                                 break;
828                         }
829                 }
830
831                 $serverdata['registered-users'] = max($serverdata['registered-users'], count($contacts));
832
833                 return $serverdata;
834         }
835
836         /**
837          * Checks if the given server does have a '/poco' endpoint.
838          * This is used for the 'PortableContact' functionality,
839          * which is used by both Friendica and Hubzilla.
840          *
841          * @param string $url        URL of the given server
842          * @param array  $serverdata array with server data
843          *
844          * @return array server data
845          */
846         private static function checkPoCo(string $url, array $serverdata)
847         {
848                 $serverdata['poco'] = '';
849
850                 $curlResult = Network::curl($url. '/poco');
851                 if (!$curlResult->isSuccess()) {
852                         return $serverdata;
853                 }
854
855                 $data = json_decode($curlResult->getBody(), true);
856                 if (empty($data)) {
857                         return $serverdata;
858                 }
859
860                 if (!empty($data['totalResults'])) {
861                         $registeredUsers = $serverdata['registered-users'] ?? 0;
862                         $serverdata['registered-users'] = max($data['totalResults'], $registeredUsers);
863                         $serverdata['directory-type'] = self::DT_POCO;
864                         $serverdata['poco'] = $url . '/poco';
865                 }
866
867                 return $serverdata;
868         }
869
870         /**
871          * Checks if the given server does have a Mastodon style directory endpoint.
872          *
873          * @param string $url        URL of the given server
874          * @param array  $serverdata array with server data
875          *
876          * @return array server data
877          */
878         public static function checkMastodonDirectory(string $url, array $serverdata)
879         {
880                 $curlResult = Network::curl($url . '/api/v1/directory?limit=1');
881                 if (!$curlResult->isSuccess()) {
882                         return $serverdata;
883                 }
884
885                 $data = json_decode($curlResult->getBody(), true);
886                 if (empty($data)) {
887                         return $serverdata;
888                 }
889
890                 if (count($data) == 1) {
891                         $serverdata['directory-type'] = self::DT_MASTODON;
892                 }
893
894                 return $serverdata;
895         }
896
897         /**
898          * Detects the version number of a given server when it was a NextCloud installation
899          *
900          * @param string $url        URL of the given server
901          * @param array  $serverdata array with server data
902          *
903          * @return array server data
904          */
905         private static function detectNextcloud(string $url, array $serverdata)
906         {
907                 $curlResult = Network::curl($url . '/status.php');
908
909                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
910                         return $serverdata;
911                 }
912
913                 $data = json_decode($curlResult->getBody(), true);
914                 if (empty($data)) {
915                         return $serverdata;
916                 }
917
918                 if (!empty($data['version'])) {
919                         $serverdata['platform'] = 'nextcloud';
920                         $serverdata['version'] = $data['version'];
921                         $serverdata['network'] = Protocol::ACTIVITYPUB;
922                 }
923
924                 return $serverdata;
925         }
926
927         /**
928          * Detects data from a given server url if it was a mastodon alike system
929          *
930          * @param string $url        URL of the given server
931          * @param array  $serverdata array with server data
932          *
933          * @return array server data
934          */
935         private static function detectMastodonAlikes(string $url, array $serverdata)
936         {
937                 $curlResult = Network::curl($url . '/api/v1/instance');
938
939                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
940                         return $serverdata;
941                 }
942
943                 $data = json_decode($curlResult->getBody(), true);
944                 if (empty($data)) {
945                         return $serverdata;
946                 }
947
948                 if (!empty($data['version'])) {
949                         $serverdata['platform'] = 'mastodon';
950                         $serverdata['version'] = $data['version'] ?? '';
951                         $serverdata['network'] = Protocol::ACTIVITYPUB;
952                 }
953
954                 if (!empty($data['title'])) {
955                         $serverdata['site_name'] = $data['title'];
956                 }
957
958                 if (!empty($data['title']) && empty($serverdata['platform']) && empty($serverdata['network'])) {
959                         $serverdata['platform'] = 'mastodon';
960                         $serverdata['network'] = Protocol::ACTIVITYPUB;
961                 }
962
963                 if (!empty($data['description'])) {
964                         $serverdata['info'] = trim($data['description']);
965                 }
966
967                 if (!empty($data['stats']['user_count'])) {
968                         $serverdata['registered-users'] = $data['stats']['user_count'];
969                 }
970
971                 if (!empty($serverdata['version']) && preg_match('/.*?\(compatible;\s(.*)\s(.*)\)/ism', $serverdata['version'], $matches)) {
972                         $serverdata['platform'] = strtolower($matches[1]);
973                         $serverdata['version'] = $matches[2];
974                 }
975
976                 if (!empty($serverdata['version']) && strstr(strtolower($serverdata['version']), 'pleroma')) {
977                         $serverdata['platform'] = 'pleroma';
978                         $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version']));
979                 }
980
981                 if (!empty($serverdata['platform']) && strstr($serverdata['platform'], 'pleroma')) {
982                         $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['platform']));
983                         $serverdata['platform'] = 'pleroma';
984                 }
985
986                 return $serverdata;
987         }
988
989         /**
990          * Detects data from typical Hubzilla endpoints
991          *
992          * @param string $url        URL of the given server
993          * @param array  $serverdata array with server data
994          *
995          * @return array server data
996          */
997         private static function detectHubzilla(string $url, array $serverdata)
998         {
999                 $curlResult = Network::curl($url . '/api/statusnet/config.json');
1000                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
1001                         return $serverdata;
1002                 }
1003
1004                 $data = json_decode($curlResult->getBody(), true);
1005                 if (empty($data)) {
1006                         return $serverdata;
1007                 }
1008
1009                 if (!empty($data['site']['name'])) {
1010                         $serverdata['site_name'] = $data['site']['name'];
1011                 }
1012
1013                 if (!empty($data['site']['platform'])) {
1014                         $serverdata['platform'] = strtolower($data['site']['platform']['PLATFORM_NAME']);
1015                         $serverdata['version'] = $data['site']['platform']['STD_VERSION'];
1016                         $serverdata['network'] = Protocol::ZOT;
1017                 }
1018
1019                 if (!empty($data['site']['hubzilla'])) {
1020                         $serverdata['platform'] = strtolower($data['site']['hubzilla']['PLATFORM_NAME']);
1021                         $serverdata['version'] = $data['site']['hubzilla']['RED_VERSION'];
1022                         $serverdata['network'] = Protocol::ZOT;
1023                 }
1024
1025                 if (!empty($data['site']['redmatrix'])) {
1026                         if (!empty($data['site']['redmatrix']['PLATFORM_NAME'])) {
1027                                 $serverdata['platform'] = strtolower($data['site']['redmatrix']['PLATFORM_NAME']);
1028                         } elseif (!empty($data['site']['redmatrix']['RED_PLATFORM'])) {
1029                                 $serverdata['platform'] = strtolower($data['site']['redmatrix']['RED_PLATFORM']);
1030                         }
1031
1032                         $serverdata['version'] = $data['site']['redmatrix']['RED_VERSION'];
1033                         $serverdata['network'] = Protocol::ZOT;
1034                 }
1035
1036                 $private = false;
1037                 $inviteonly = false;
1038                 $closed = false;
1039
1040                 if (!empty($data['site']['closed'])) {
1041                         $closed = self::toBoolean($data['site']['closed']);
1042                 }
1043
1044                 if (!empty($data['site']['private'])) {
1045                         $private = self::toBoolean($data['site']['private']);
1046                 }
1047
1048                 if (!empty($data['site']['inviteonly'])) {
1049                         $inviteonly = self::toBoolean($data['site']['inviteonly']);
1050                 }
1051
1052                 if (!$closed && !$private and $inviteonly) {
1053                         $register_policy = Register::APPROVE;
1054                 } elseif (!$closed && !$private) {
1055                         $register_policy = Register::OPEN;
1056                 } else {
1057                         $register_policy = Register::CLOSED;
1058                 }
1059
1060                 return $serverdata;
1061         }
1062
1063         /**
1064          * Converts input value to a boolean value
1065          *
1066          * @param string|integer $val
1067          *
1068          * @return boolean
1069          */
1070         private static function toBoolean($val)
1071         {
1072                 if (($val == 'true') || ($val == 1)) {
1073                         return true;
1074                 } elseif (($val == 'false') || ($val == 0)) {
1075                         return false;
1076                 }
1077
1078                 return $val;
1079         }
1080
1081         /**
1082          * Detect if the URL belongs to a GNU Social server
1083          *
1084          * @param string $url        URL of the given server
1085          * @param array  $serverdata array with server data
1086          *
1087          * @return array server data
1088          */
1089         private static function detectGNUSocial(string $url, array $serverdata)
1090         {
1091                 // Test for GNU Social
1092                 $curlResult = Network::curl($url . '/api/gnusocial/version.json');
1093                 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
1094                         ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
1095                         $serverdata['platform'] = 'gnusocial';
1096                         // Remove junk that some GNU Social servers return
1097                         $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBody());
1098                         $serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
1099                         $serverdata['version'] = trim($serverdata['version'], '"');
1100                         $serverdata['network'] = Protocol::OSTATUS;
1101                         return $serverdata;
1102                 }
1103
1104                 // Test for Statusnet
1105                 $curlResult = Network::curl($url . '/api/statusnet/version.json');
1106                 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
1107                         ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
1108
1109                         // Remove junk that some GNU Social servers return
1110                         $serverdata['version'] = str_replace(chr(239).chr(187).chr(191), '', $curlResult->getBody());
1111                         $serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
1112                         $serverdata['version'] = trim($serverdata['version'], '"');
1113
1114                         if (!empty($serverdata['version']) && strtolower(substr($serverdata['version'], 0, 7)) == 'pleroma') {
1115                                 $serverdata['platform'] = 'pleroma';
1116                                 $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version']));
1117                                 $serverdata['network'] = Protocol::ACTIVITYPUB;
1118                         } else {
1119                                 $serverdata['platform'] = 'statusnet';
1120                                 $serverdata['network'] = Protocol::OSTATUS;
1121                         }
1122                 }
1123
1124                 return $serverdata;
1125         }
1126
1127         /**
1128          * Detect if the URL belongs to a Friendica server
1129          *
1130          * @param string $url        URL of the given server
1131          * @param array  $serverdata array with server data
1132          *
1133          * @return array server data
1134          */
1135         private static function detectFriendica(string $url, array $serverdata)
1136         {
1137                 $curlResult = Network::curl($url . '/friendica/json');
1138                 if (!$curlResult->isSuccess()) {
1139                         $curlResult = Network::curl($url . '/friendika/json');
1140                 }
1141
1142                 if (!$curlResult->isSuccess()) {
1143                         return $serverdata;
1144                 }
1145
1146                 $data = json_decode($curlResult->getBody(), true);
1147                 if (empty($data) || empty($data['version'])) {
1148                         return $serverdata;
1149                 }
1150
1151                 $serverdata['network'] = Protocol::DFRN;
1152                 $serverdata['version'] = $data['version'];
1153
1154                 if (!empty($data['no_scrape_url'])) {
1155                         $serverdata['noscrape'] = $data['no_scrape_url'];
1156                 }
1157
1158                 if (!empty($data['site_name'])) {
1159                         $serverdata['site_name'] = $data['site_name'];
1160                 }
1161
1162                 if (!empty($data['info'])) {
1163                         $serverdata['info'] = trim($data['info']);
1164                 }
1165
1166                 $register_policy = ($data['register_policy'] ?? '') ?: 'REGISTER_CLOSED';
1167                 switch ($register_policy) {
1168                         case 'REGISTER_OPEN':
1169                                 $serverdata['register_policy'] = Register::OPEN;
1170                                 break;
1171
1172                         case 'REGISTER_APPROVE':
1173                                 $serverdata['register_policy'] = Register::APPROVE;
1174                                 break;
1175
1176                         case 'REGISTER_CLOSED':
1177                         case 'REGISTER_INVITATION':
1178                                 $serverdata['register_policy'] = Register::CLOSED;
1179                                 break;
1180                         default:
1181                                 Logger::info('Register policy is invalid', ['policy' => $register_policy, 'server' => $url]);
1182                                 $serverdata['register_policy'] = Register::CLOSED;
1183                                 break;
1184                 }
1185
1186                 $serverdata['platform'] = strtolower($data['platform'] ?? '');
1187
1188                 return $serverdata;
1189         }
1190
1191         /**
1192          * Analyses the landing page of a given server for hints about type and system of that server
1193          *
1194          * @param object $curlResult result of curl execution
1195          * @param array  $serverdata array with server data
1196          * @param string $url        Server URL
1197          *
1198          * @return array server data
1199          */
1200         private static function analyseRootBody($curlResult, array $serverdata, string $url)
1201         {
1202                 $doc = new DOMDocument();
1203                 @$doc->loadHTML($curlResult->getBody());
1204                 $xpath = new DOMXPath($doc);
1205
1206                 $title = trim(XML::getFirstNodeValue($xpath, '//head/title/text()'));
1207                 if (!empty($title)) {
1208                         $serverdata['site_name'] = $title;
1209                 }
1210
1211                 $list = $xpath->query('//meta[@name]');
1212
1213                 foreach ($list as $node) {
1214                         $attr = [];
1215                         if ($node->attributes->length) {
1216                                 foreach ($node->attributes as $attribute) {
1217                                         $value = trim($attribute->value);
1218                                         if (empty($value)) {
1219                                                 continue;
1220                                         }
1221
1222                                         $attr[$attribute->name] = $value;
1223                                 }
1224
1225                                 if (empty($attr['name']) || empty($attr['content'])) {
1226                                         continue;
1227                                 }
1228                         }
1229
1230                         if ($attr['name'] == 'description') {
1231                                 $serverdata['info'] = $attr['content'];
1232                         }
1233
1234                         if ($attr['name'] == 'application-name') {
1235                                 $serverdata['platform'] = strtolower($attr['content']);
1236                                 if (in_array($attr['content'], ['Misskey', 'Write.as'])) {
1237                                         $serverdata['network'] = Protocol::ACTIVITYPUB;
1238                                 }
1239                         }
1240                         if (($attr['name'] == 'generator') && (empty($serverdata['platform']) || (substr(strtolower($attr['content']), 0, 9) == 'wordpress'))) {
1241                                 $serverdata['platform'] = strtolower($attr['content']);
1242                                 $version_part = explode(' ', $attr['content']);
1243
1244                                 if (count($version_part) == 2) {
1245                                         if (in_array($version_part[0], ['WordPress'])) {
1246                                                 $serverdata['platform'] = strtolower($version_part[0]);
1247                                                 $serverdata['version'] = $version_part[1];
1248
1249                                                 // We still do need a reliable test if some AP plugin is activated
1250                                                 if (DBA::exists('apcontact', ['baseurl' => $url])) {
1251                                                         $serverdata['network'] = Protocol::ACTIVITYPUB;
1252                                                 } else {
1253                                                         $serverdata['network'] = Protocol::FEED;
1254                                                 }
1255                                         }
1256                                         if (in_array($version_part[0], ['Friendika', 'Friendica'])) {
1257                                                 $serverdata['platform'] = strtolower($version_part[0]);
1258                                                 $serverdata['version'] = $version_part[1];
1259                                                 $serverdata['network'] = Protocol::DFRN;
1260                                         }
1261                                 }
1262                         }
1263                 }
1264
1265                 $list = $xpath->query('//meta[@property]');
1266
1267                 foreach ($list as $node) {
1268                         $attr = [];
1269                         if ($node->attributes->length) {
1270                                 foreach ($node->attributes as $attribute) {
1271                                         $value = trim($attribute->value);
1272                                         if (empty($value)) {
1273                                                 continue;
1274                                         }
1275
1276                                         $attr[$attribute->name] = $value;
1277                                 }
1278
1279                                 if (empty($attr['property']) || empty($attr['content'])) {
1280                                         continue;
1281                                 }
1282                         }
1283
1284                         if ($attr['property'] == 'og:site_name') {
1285                                 $serverdata['site_name'] = $attr['content'];
1286                         }
1287
1288                         if ($attr['property'] == 'og:description') {
1289                                 $serverdata['info'] = $attr['content'];
1290                         }
1291
1292                         if ($attr['property'] == 'og:platform') {
1293                                 $serverdata['platform'] = strtolower($attr['content']);
1294
1295                                 if (in_array($attr['content'], ['PeerTube'])) {
1296                                         $serverdata['network'] = Protocol::ACTIVITYPUB;
1297                                 }
1298                         }
1299
1300                         if ($attr['property'] == 'generator') {
1301                                 $serverdata['platform'] = strtolower($attr['content']);
1302
1303                                 if (in_array($attr['content'], ['hubzilla'])) {
1304                                         // We later check which compatible protocol modules are loaded.
1305                                         $serverdata['network'] = Protocol::ZOT;
1306                                 }
1307                         }
1308                 }
1309
1310                 return $serverdata;
1311         }
1312
1313         /**
1314          * Analyses the header data of a given server for hints about type and system of that server
1315          *
1316          * @param object $curlResult result of curl execution
1317          * @param array  $serverdata array with server data
1318          *
1319          * @return array server data
1320          */
1321         private static function analyseRootHeader($curlResult, array $serverdata)
1322         {
1323                 if ($curlResult->getHeader('server') == 'Mastodon') {
1324                         $serverdata['platform'] = 'mastodon';
1325                         $serverdata['network'] = $network = Protocol::ACTIVITYPUB;
1326                 } elseif ($curlResult->inHeader('x-diaspora-version')) {
1327                         $serverdata['platform'] = 'diaspora';
1328                         $serverdata['network'] = $network = Protocol::DIASPORA;
1329                         $serverdata['version'] = $curlResult->getHeader('x-diaspora-version');
1330                 } elseif ($curlResult->inHeader('x-friendica-version')) {
1331                         $serverdata['platform'] = 'friendica';
1332                         $serverdata['network'] = $network = Protocol::DFRN;
1333                         $serverdata['version'] = $curlResult->getHeader('x-friendica-version');
1334                 }
1335                 return $serverdata;
1336         }
1337
1338         /**
1339          * Test if the body contains valid content
1340          *
1341          * @param string $body
1342          * @return boolean
1343          */
1344         private static function invalidBody(string $body)
1345         {
1346                 // Currently we only test for a HTML element.
1347                 // Possibly we enhance this in the future.
1348                 return !strpos($body, '>');
1349         }
1350
1351         /**
1352          * Update the user directory of a given gserver record
1353          *
1354          * @param array $gserver gserver record
1355          */
1356         public static function updateDirectory(array $gserver)
1357         {
1358                 /// @todo Add Mastodon API directory
1359
1360                 if (!empty($gserver['poco'])) {
1361                         PortableContact::discoverSingleServer($gserver['id']);
1362                 }
1363         }
1364
1365         /**
1366          * Update GServer entries
1367          */
1368         public static function discover()
1369         {
1370                 // Update the server list
1371                 self::discoverFederation();
1372
1373                 $no_of_queries = 5;
1374
1375                 $requery_days = intval(DI::config()->get('system', 'poco_requery_days'));
1376
1377                 if ($requery_days == 0) {
1378                         $requery_days = 7;
1379                 }
1380
1381                 $last_update = date('c', time() - (60 * 60 * 24 * $requery_days));
1382
1383                 $gservers = DBA::p("SELECT `id`, `url`, `nurl`, `network`, `poco`
1384                         FROM `gserver`
1385                         WHERE `last_contact` >= `last_failure`
1386                         AND `poco` != ''
1387                         AND `last_poco_query` < ?
1388                         ORDER BY RAND()", $last_update
1389                 );
1390
1391                 while ($gserver = DBA::fetch($gservers)) {
1392                         if (!GServer::check($gserver['url'], $gserver['network'])) {
1393                                 // The server is not reachable? Okay, then we will try it later
1394                                 $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
1395                                 DBA::update('gserver', $fields, ['nurl' => $gserver['nurl']]);
1396                                 continue;
1397                         }
1398
1399                         Logger::info('Update directory', ['server' => $gserver['url'], 'id' => $gserver['id']]);
1400                         Worker::add(PRIORITY_LOW, 'UpdateServerDirectory', $gserver);
1401
1402                         if (--$no_of_queries == 0) {
1403                                 break;
1404                         }
1405                 }
1406
1407                 DBA::close($gservers);
1408         }
1409
1410         /**
1411          * Discover federated servers
1412          */
1413         private static function discoverFederation()
1414         {
1415                 $last = DI::config()->get('poco', 'last_federation_discovery');
1416
1417                 if ($last) {
1418                         $next = $last + (24 * 60 * 60);
1419
1420                         if ($next > time()) {
1421                                 return;
1422                         }
1423                 }
1424
1425                 // Discover federated servers
1426                 $curlResult = Network::fetchUrl("http://the-federation.info/pods.json");
1427
1428                 if (!empty($curlResult)) {
1429                         $servers = json_decode($curlResult, true);
1430
1431                         if (!empty($servers['pods'])) {
1432                                 foreach ($servers['pods'] as $server) {
1433                                         Worker::add(PRIORITY_LOW, 'UpdateGServer', 'https://' . $server['host']);
1434                                 }
1435                         }
1436                 }
1437
1438                 // Disvover Mastodon servers
1439                 $accesstoken = DI::config()->get('system', 'instances_social_key');
1440
1441                 if (!empty($accesstoken)) {
1442                         $api = 'https://instances.social/api/1.0/instances/list?count=0';
1443                         $header = ['Authorization: Bearer '.$accesstoken];
1444                         $curlResult = Network::curl($api, false, ['headers' => $header]);
1445
1446                         if ($curlResult->isSuccess()) {
1447                                 $servers = json_decode($curlResult->getBody(), true);
1448
1449                                 foreach ($servers['instances'] as $server) {
1450                                         $url = (is_null($server['https_score']) ? 'http' : 'https') . '://' . $server['name'];
1451                                         Worker::add(PRIORITY_LOW, 'UpdateGServer', $url);
1452                                 }
1453                         }
1454                 }
1455
1456                 DI::config()->set('poco', 'last_federation_discovery', time());
1457         }
1458 }