]> git.mxchange.org Git - friendica.git/blob - src/Model/GServer.php
Improved definition style
[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                 $gserver = DBA::selectFirst('gserver', ['id', 'relay-subscribe', 'relay-scope'], ['nurl' => Strings::normaliseLink($server_url)]);
357                 if (!DBA::isResult($gserver)) {
358                         return;
359                 }
360
361                 if (($gserver['relay-subscribe'] != $data['subscribe']) || ($gserver['relay-scope'] != $data['scope'])) {
362                         $fields = ['relay-subscribe' => $data['subscribe'], 'relay-scope' => $data['scope']];
363                         DBA::update('gserver', $fields, ['id' => $gserver['id']]);
364                 }
365
366                 DBA::delete('gserver-tag', ['gserver-id' => $gserver['id']]);
367
368                 if ($data['scope'] == 'tags') {
369                         // Avoid duplicates
370                         $tags = [];
371                         foreach ($data['tags'] as $tag) {
372                                 $tag = mb_strtolower($tag);
373                                 if (strlen($tag) < 100) {
374                                         $tags[$tag] = $tag;
375                                 }
376                         }
377
378                         foreach ($tags as $tag) {
379                                 DBA::insert('gserver-tag', ['gserver-id' => $gserver['id'], 'tag' => $tag], true);
380                         }
381                 }
382
383                 // Create or update the relay contact
384                 $fields = [];
385                 if (isset($data['protocols'])) {
386                         if (isset($data['protocols']['diaspora'])) {
387                                 $fields['network'] = Protocol::DIASPORA;
388
389                                 if (isset($data['protocols']['diaspora']['receive'])) {
390                                         $fields['batch'] = $data['protocols']['diaspora']['receive'];
391                                 } elseif (is_string($data['protocols']['diaspora'])) {
392                                         $fields['batch'] = $data['protocols']['diaspora'];
393                                 }
394                         }
395
396                         if (isset($data['protocols']['dfrn'])) {
397                                 $fields['network'] = Protocol::DFRN;
398
399                                 if (isset($data['protocols']['dfrn']['receive'])) {
400                                         $fields['batch'] = $data['protocols']['dfrn']['receive'];
401                                 } elseif (is_string($data['protocols']['dfrn'])) {
402                                         $fields['batch'] = $data['protocols']['dfrn'];
403                                 }
404                         }
405                 }
406                 Diaspora::setRelayContact($server_url, $fields);
407         }
408
409         /**
410          * Fetch server data from '/statistics.json' on the given server
411          *
412          * @param string $url URL of the given server
413          *
414          * @return array server data
415          */
416         private static function fetchStatistics(string $url)
417         {
418                 $curlResult = Network::curl($url . '/statistics.json');
419                 if (!$curlResult->isSuccess()) {
420                         return [];
421                 }
422
423                 $data = json_decode($curlResult->getBody(), true);
424                 if (empty($data)) {
425                         return [];
426                 }
427
428                 $serverdata = [];
429
430                 if (!empty($data['version'])) {
431                         $serverdata['version'] = $data['version'];
432                         // Version numbers on statistics.json are presented with additional info, e.g.:
433                         // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
434                         $serverdata['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $serverdata['version']);
435                 }
436
437                 if (!empty($data['name'])) {
438                         $serverdata['site_name'] = $data['name'];
439                 }
440
441                 if (!empty($data['network'])) {
442                         $serverdata['platform'] = strtolower($data['network']);
443
444                         if ($serverdata['platform'] == 'diaspora') {
445                                 $serverdata['network'] = Protocol::DIASPORA;
446                         } elseif ($serverdata['platform'] == 'friendica') {
447                                 $serverdata['network'] = Protocol::DFRN;
448                         } elseif ($serverdata['platform'] == 'hubzilla') {
449                                 $serverdata['network'] = Protocol::ZOT;
450                         } elseif ($serverdata['platform'] == 'redmatrix') {
451                                 $serverdata['network'] = Protocol::ZOT;
452                         }
453                 }
454
455
456                 if (!empty($data['registrations_open'])) {
457                         $serverdata['register_policy'] = Register::OPEN;
458                 } else {
459                         $serverdata['register_policy'] = Register::CLOSED;
460                 }
461
462                 return $serverdata;
463         }
464
465         /**
466          * Detect server type by using the nodeinfo data
467          *
468          * @param string     $url        address of the server
469          * @param CurlResult $curlResult
470          * @return array Server data
471          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
472          */
473         private static function fetchNodeinfo(string $url, CurlResult $curlResult)
474         {
475                 $nodeinfo = json_decode($curlResult->getBody(), true);
476
477                 if (!is_array($nodeinfo) || empty($nodeinfo['links'])) {
478                         return [];
479                 }
480
481                 $nodeinfo1_url = '';
482                 $nodeinfo2_url = '';
483
484                 foreach ($nodeinfo['links'] as $link) {
485                         if (!is_array($link) || empty($link['rel']) || empty($link['href'])) {
486                                 Logger::info('Invalid nodeinfo format', ['url' => $url]);
487                                 continue;
488                         }
489                         if ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/1.0') {
490                                 $nodeinfo1_url = $link['href'];
491                         } elseif ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0') {
492                                 $nodeinfo2_url = $link['href'];
493                         }
494                 }
495
496                 if ($nodeinfo1_url . $nodeinfo2_url == '') {
497                         return [];
498                 }
499
500                 $server = [];
501
502                 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
503                 if (!empty($nodeinfo2_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo2_url, PHP_URL_HOST))) {
504                         $server = self::parseNodeinfo2($nodeinfo2_url);
505                 }
506
507                 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
508                 if (empty($server) && !empty($nodeinfo1_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo1_url, PHP_URL_HOST))) {
509                         $server = self::parseNodeinfo1($nodeinfo1_url);
510                 }
511
512                 return $server;
513         }
514
515         /**
516          * Parses Nodeinfo 1
517          *
518          * @param string $nodeinfo_url address of the nodeinfo path
519          * @return array Server data
520          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
521          */
522         private static function parseNodeinfo1(string $nodeinfo_url)
523         {
524                 $curlResult = Network::curl($nodeinfo_url);
525
526                 if (!$curlResult->isSuccess()) {
527                         return [];
528                 }
529
530                 $nodeinfo = json_decode($curlResult->getBody(), true);
531
532                 if (!is_array($nodeinfo)) {
533                         return [];
534                 }
535
536                 $server = [];
537
538                 $server['register_policy'] = Register::CLOSED;
539
540                 if (!empty($nodeinfo['openRegistrations'])) {
541                         $server['register_policy'] = Register::OPEN;
542                 }
543
544                 if (is_array($nodeinfo['software'])) {
545                         if (!empty($nodeinfo['software']['name'])) {
546                                 $server['platform'] = strtolower($nodeinfo['software']['name']);
547                         }
548
549                         if (!empty($nodeinfo['software']['version'])) {
550                                 $server['version'] = $nodeinfo['software']['version'];
551                                 // Version numbers on Nodeinfo are presented with additional info, e.g.:
552                                 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
553                                 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
554                         }
555                 }
556
557                 if (!empty($nodeinfo['metadata']['nodeName'])) {
558                         $server['site_name'] = $nodeinfo['metadata']['nodeName'];
559                 }
560
561                 if (!empty($nodeinfo['usage']['users']['total'])) {
562                         $server['registered-users'] = $nodeinfo['usage']['users']['total'];
563                 }
564
565                 if (!empty($nodeinfo['protocols']['inbound']) && is_array($nodeinfo['protocols']['inbound'])) {
566                         $protocols = [];
567                         foreach ($nodeinfo['protocols']['inbound'] as $protocol) {
568                                 $protocols[$protocol] = true;
569                         }
570
571                         if (!empty($protocols['friendica'])) {
572                                 $server['network'] = Protocol::DFRN;
573                         } elseif (!empty($protocols['activitypub'])) {
574                                 $server['network'] = Protocol::ACTIVITYPUB;
575                         } elseif (!empty($protocols['diaspora'])) {
576                                 $server['network'] = Protocol::DIASPORA;
577                         } elseif (!empty($protocols['ostatus'])) {
578                                 $server['network'] = Protocol::OSTATUS;
579                         } elseif (!empty($protocols['gnusocial'])) {
580                                 $server['network'] = Protocol::OSTATUS;
581                         } elseif (!empty($protocols['zot'])) {
582                                 $server['network'] = Protocol::ZOT;
583                         }
584                 }
585
586                 if (empty($server)) {
587                         return [];
588                 }
589
590                 return $server;
591         }
592
593         /**
594          * Parses Nodeinfo 2
595          *
596          * @param string $nodeinfo_url address of the nodeinfo path
597          * @return array Server data
598          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
599          */
600         private static function parseNodeinfo2(string $nodeinfo_url)
601         {
602                 $curlResult = Network::curl($nodeinfo_url);
603                 if (!$curlResult->isSuccess()) {
604                         return [];
605                 }
606
607                 $nodeinfo = json_decode($curlResult->getBody(), true);
608
609                 if (!is_array($nodeinfo)) {
610                         return [];
611                 }
612
613                 $server = [];
614
615                 $server['register_policy'] = Register::CLOSED;
616
617                 if (!empty($nodeinfo['openRegistrations'])) {
618                         $server['register_policy'] = Register::OPEN;
619                 }
620
621                 if (is_array($nodeinfo['software'])) {
622                         if (!empty($nodeinfo['software']['name'])) {
623                                 $server['platform'] = strtolower($nodeinfo['software']['name']);
624                         }
625
626                         if (!empty($nodeinfo['software']['version'])) {
627                                 $server['version'] = $nodeinfo['software']['version'];
628                                 // Version numbers on Nodeinfo are presented with additional info, e.g.:
629                                 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
630                                 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
631                         }
632                 }
633
634                 if (!empty($nodeinfo['metadata']['nodeName'])) {
635                         $server['site_name'] = $nodeinfo['metadata']['nodeName'];
636                 }
637
638                 if (!empty($nodeinfo['usage']['users']['total'])) {
639                         $server['registered-users'] = $nodeinfo['usage']['users']['total'];
640                 }
641
642                 if (!empty($nodeinfo['protocols'])) {
643                         $protocols = [];
644                         foreach ($nodeinfo['protocols'] as $protocol) {
645                                 $protocols[$protocol] = true;
646                         }
647
648                         if (!empty($protocols['dfrn'])) {
649                                 $server['network'] = Protocol::DFRN;
650                         } elseif (!empty($protocols['activitypub'])) {
651                                 $server['network'] = Protocol::ACTIVITYPUB;
652                         } elseif (!empty($protocols['diaspora'])) {
653                                 $server['network'] = Protocol::DIASPORA;
654                         } elseif (!empty($protocols['ostatus'])) {
655                                 $server['network'] = Protocol::OSTATUS;
656                         } elseif (!empty($protocols['gnusocial'])) {
657                                 $server['network'] = Protocol::OSTATUS;
658                         } elseif (!empty($protocols['zot'])) {
659                                 $server['network'] = Protocol::ZOT;
660                         }
661                 }
662
663                 if (empty($server)) {
664                         return [];
665                 }
666
667                 return $server;
668         }
669
670         /**
671          * Fetch server information from a 'siteinfo.json' file on the given server
672          *
673          * @param string $url        URL of the given server
674          * @param array  $serverdata array with server data
675          *
676          * @return array server data
677          */
678         private static function fetchSiteinfo(string $url, array $serverdata)
679         {
680                 $curlResult = Network::curl($url . '/siteinfo.json');
681                 if (!$curlResult->isSuccess()) {
682                         return $serverdata;
683                 }
684
685                 $data = json_decode($curlResult->getBody(), true);
686                 if (empty($data)) {
687                         return $serverdata;
688                 }
689
690                 if (!empty($data['url'])) {
691                         $serverdata['platform'] = strtolower($data['platform']);
692                         $serverdata['version'] = $data['version'];
693                 }
694
695                 if (!empty($data['plugins'])) {
696                         if (in_array('pubcrawl', $data['plugins'])) {
697                                 $serverdata['network'] = Protocol::ACTIVITYPUB;
698                         } elseif (in_array('diaspora', $data['plugins'])) {
699                                 $serverdata['network'] = Protocol::DIASPORA;
700                         } elseif (in_array('gnusoc', $data['plugins'])) {
701                                 $serverdata['network'] = Protocol::OSTATUS;
702                         } else {
703                                 $serverdata['network'] = Protocol::ZOT;
704                         }
705                 }
706
707                 if (!empty($data['site_name'])) {
708                         $serverdata['site_name'] = $data['site_name'];
709                 }
710
711                 if (!empty($data['channels_total'])) {
712                         $serverdata['registered-users'] = $data['channels_total'];
713                 }
714
715                 if (!empty($data['register_policy'])) {
716                         switch ($data['register_policy']) {
717                                 case 'REGISTER_OPEN':
718                                         $serverdata['register_policy'] = Register::OPEN;
719                                         break;
720
721                                 case 'REGISTER_APPROVE':
722                                         $serverdata['register_policy'] = Register::APPROVE;
723                                         break;
724
725                                 case 'REGISTER_CLOSED':
726                                 default:
727                                         $serverdata['register_policy'] = Register::CLOSED;
728                                         break;
729                         }
730                 }
731
732                 return $serverdata;
733         }
734
735         /**
736          * Checks if the server contains a valid host meta file
737          *
738          * @param string $url URL of the given server
739          *
740          * @return boolean 'true' if the server seems to be vital
741          */
742         private static function validHostMeta(string $url)
743         {
744                 $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
745                 $curlResult = Network::curl($url . '/.well-known/host-meta', false, ['timeout' => $xrd_timeout]);
746                 if (!$curlResult->isSuccess()) {
747                         return false;
748                 }
749
750                 $xrd = XML::parseString($curlResult->getBody(), false);
751                 if (!is_object($xrd)) {
752                         return false;
753                 }
754
755                 $elements = XML::elementToArray($xrd);
756                 if (empty($elements) || empty($elements['xrd']) || empty($elements['xrd']['link'])) {
757                         return false;
758                 }
759
760                 $valid = false;
761                 foreach ($elements['xrd']['link'] as $link) {
762                         // When there is more than a single "link" element, the array looks slightly different
763                         if (!empty($link['@attributes'])) {
764                                 $link = $link['@attributes'];
765                         }
766
767                         if (empty($link['rel']) || empty($link['template'])) {
768                                 continue;
769                         }
770
771                         if ($link['rel'] == 'lrdd') {
772                                 // When the webfinger host is the same like the system host, it should be ok.
773                                 $valid = (parse_url($url, PHP_URL_HOST) == parse_url($link['template'], PHP_URL_HOST));
774                         }
775                 }
776
777                 return $valid;
778         }
779
780         /**
781          * Detect the network of the given server via their known contacts
782          *
783          * @param string $url        URL of the given server
784          * @param array  $serverdata array with server data
785          *
786          * @return array server data
787          */
788         private static function detectNetworkViaContacts(string $url, array $serverdata)
789         {
790                 $contacts = [];
791
792                 $gcontacts = DBA::select('gcontact', ['url', 'nurl'], ['server_url' => [$url, $serverdata['nurl']]]);
793                 while ($gcontact = DBA::fetch($gcontacts)) {
794                         $contacts[$gcontact['nurl']] = $gcontact['url'];
795                 }
796                 DBA::close($gcontacts);
797
798                 $apcontacts = DBA::select('apcontact', ['url'], ['baseurl' => [$url, $serverdata['nurl']]]);
799                 while ($gcontact = DBA::fetch($gcontacts)) {
800                         $contacts[Strings::normaliseLink($apcontact['url'])] = $apcontact['url'];
801                 }
802                 DBA::close($apcontacts);
803
804                 $pcontacts = DBA::select('contact', ['url', 'nurl'], ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]);
805                 while ($gcontact = DBA::fetch($gcontacts)) {
806                         $contacts[$pcontact['nurl']] = $pcontact['url'];
807                 }
808                 DBA::close($pcontacts);
809
810                 if (empty($contacts)) {
811                         return $serverdata;
812                 }
813
814                 foreach ($contacts as $contact) {
815                         $probed = Probe::uri($contact);
816                         if (in_array($probed['network'], Protocol::FEDERATED)) {
817                                 $serverdata['network'] = $probed['network'];
818                                 break;
819                         }
820                 }
821
822                 $serverdata['registered-users'] = max($serverdata['registered-users'], count($contacts));
823
824                 return $serverdata;
825         }
826
827         /**
828          * Checks if the given server does have a '/poco' endpoint.
829          * This is used for the 'PortableContact' functionality,
830          * which is used by both Friendica and Hubzilla.
831          *
832          * @param string $url        URL of the given server
833          * @param array  $serverdata array with server data
834          *
835          * @return array server data
836          */
837         private static function checkPoCo(string $url, array $serverdata)
838         {
839                 $serverdata['poco'] = '';
840
841                 $curlResult = Network::curl($url. '/poco');
842                 if (!$curlResult->isSuccess()) {
843                         return $serverdata;
844                 }
845
846                 $data = json_decode($curlResult->getBody(), true);
847                 if (empty($data)) {
848                         return $serverdata;
849                 }
850
851                 if (!empty($data['totalResults'])) {
852                         $registeredUsers = $serverdata['registered-users'] ?? 0;
853                         $serverdata['registered-users'] = max($data['totalResults'], $registeredUsers);
854                         $serverdata['directory-type'] = self::DT_POCO;
855                         $serverdata['poco'] = $url . '/poco';
856                 }
857
858                 return $serverdata;
859         }
860
861         /**
862          * Checks if the given server does have a Mastodon style directory endpoint.
863          *
864          * @param string $url        URL of the given server
865          * @param array  $serverdata array with server data
866          *
867          * @return array server data
868          */
869         public static function checkMastodonDirectory(string $url, array $serverdata)
870         {
871                 $curlResult = Network::curl($url . '/api/v1/directory?limit=1');
872                 if (!$curlResult->isSuccess()) {
873                         return $serverdata;
874                 }
875
876                 $data = json_decode($curlResult->getBody(), true);
877                 if (empty($data)) {
878                         return $serverdata;
879                 }
880
881                 if (count($data) == 1) {
882                         $serverdata['directory-type'] = self::DT_MASTODON;
883                 }
884
885                 return $serverdata;
886         }
887
888         /**
889          * Detects the version number of a given server when it was a NextCloud installation
890          *
891          * @param string $url        URL of the given server
892          * @param array  $serverdata array with server data
893          *
894          * @return array server data
895          */
896         private static function detectNextcloud(string $url, array $serverdata)
897         {
898                 $curlResult = Network::curl($url . '/status.php');
899
900                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
901                         return $serverdata;
902                 }
903
904                 $data = json_decode($curlResult->getBody(), true);
905                 if (empty($data)) {
906                         return $serverdata;
907                 }
908
909                 if (!empty($data['version'])) {
910                         $serverdata['platform'] = 'nextcloud';
911                         $serverdata['version'] = $data['version'];
912                         $serverdata['network'] = Protocol::ACTIVITYPUB;
913                 }
914
915                 return $serverdata;
916         }
917
918         /**
919          * Detects data from a given server url if it was a mastodon alike system
920          *
921          * @param string $url        URL of the given server
922          * @param array  $serverdata array with server data
923          *
924          * @return array server data
925          */
926         private static function detectMastodonAlikes(string $url, array $serverdata)
927         {
928                 $curlResult = Network::curl($url . '/api/v1/instance');
929
930                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
931                         return $serverdata;
932                 }
933
934                 $data = json_decode($curlResult->getBody(), true);
935                 if (empty($data)) {
936                         return $serverdata;
937                 }
938
939                 if (!empty($data['version'])) {
940                         $serverdata['platform'] = 'mastodon';
941                         $serverdata['version'] = $data['version'] ?? '';
942                         $serverdata['network'] = Protocol::ACTIVITYPUB;
943                 }
944
945                 if (!empty($data['title'])) {
946                         $serverdata['site_name'] = $data['title'];
947                 }
948
949                 if (!empty($data['title']) && empty($serverdata['platform']) && empty($serverdata['network'])) {
950                         $serverdata['platform'] = 'mastodon';
951                         $serverdata['network'] = Protocol::ACTIVITYPUB;
952                 }
953
954                 if (!empty($data['description'])) {
955                         $serverdata['info'] = trim($data['description']);
956                 }
957
958                 if (!empty($data['stats']['user_count'])) {
959                         $serverdata['registered-users'] = $data['stats']['user_count'];
960                 }
961
962                 if (!empty($serverdata['version']) && preg_match('/.*?\(compatible;\s(.*)\s(.*)\)/ism', $serverdata['version'], $matches)) {
963                         $serverdata['platform'] = strtolower($matches[1]);
964                         $serverdata['version'] = $matches[2];
965                 }
966
967                 if (!empty($serverdata['version']) && strstr(strtolower($serverdata['version']), 'pleroma')) {
968                         $serverdata['platform'] = 'pleroma';
969                         $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version']));
970                 }
971
972                 if (!empty($serverdata['platform']) && strstr($serverdata['platform'], 'pleroma')) {
973                         $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['platform']));
974                         $serverdata['platform'] = 'pleroma';
975                 }
976
977                 return $serverdata;
978         }
979
980         /**
981          * Detects data from typical Hubzilla endpoints
982          *
983          * @param string $url        URL of the given server
984          * @param array  $serverdata array with server data
985          *
986          * @return array server data
987          */
988         private static function detectHubzilla(string $url, array $serverdata)
989         {
990                 $curlResult = Network::curl($url . '/api/statusnet/config.json');
991                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
992                         return $serverdata;
993                 }
994
995                 $data = json_decode($curlResult->getBody(), true);
996                 if (empty($data)) {
997                         return $serverdata;
998                 }
999
1000                 if (!empty($data['site']['name'])) {
1001                         $serverdata['site_name'] = $data['site']['name'];
1002                 }
1003
1004                 if (!empty($data['site']['platform'])) {
1005                         $serverdata['platform'] = strtolower($data['site']['platform']['PLATFORM_NAME']);
1006                         $serverdata['version'] = $data['site']['platform']['STD_VERSION'];
1007                         $serverdata['network'] = Protocol::ZOT;
1008                 }
1009
1010                 if (!empty($data['site']['hubzilla'])) {
1011                         $serverdata['platform'] = strtolower($data['site']['hubzilla']['PLATFORM_NAME']);
1012                         $serverdata['version'] = $data['site']['hubzilla']['RED_VERSION'];
1013                         $serverdata['network'] = Protocol::ZOT;
1014                 }
1015
1016                 if (!empty($data['site']['redmatrix'])) {
1017                         if (!empty($data['site']['redmatrix']['PLATFORM_NAME'])) {
1018                                 $serverdata['platform'] = strtolower($data['site']['redmatrix']['PLATFORM_NAME']);
1019                         } elseif (!empty($data['site']['redmatrix']['RED_PLATFORM'])) {
1020                                 $serverdata['platform'] = strtolower($data['site']['redmatrix']['RED_PLATFORM']);
1021                         }
1022
1023                         $serverdata['version'] = $data['site']['redmatrix']['RED_VERSION'];
1024                         $serverdata['network'] = Protocol::ZOT;
1025                 }
1026
1027                 $private = false;
1028                 $inviteonly = false;
1029                 $closed = false;
1030
1031                 if (!empty($data['site']['closed'])) {
1032                         $closed = self::toBoolean($data['site']['closed']);
1033                 }
1034
1035                 if (!empty($data['site']['private'])) {
1036                         $private = self::toBoolean($data['site']['private']);
1037                 }
1038
1039                 if (!empty($data['site']['inviteonly'])) {
1040                         $inviteonly = self::toBoolean($data['site']['inviteonly']);
1041                 }
1042
1043                 if (!$closed && !$private and $inviteonly) {
1044                         $register_policy = Register::APPROVE;
1045                 } elseif (!$closed && !$private) {
1046                         $register_policy = Register::OPEN;
1047                 } else {
1048                         $register_policy = Register::CLOSED;
1049                 }
1050
1051                 return $serverdata;
1052         }
1053
1054         /**
1055          * Converts input value to a boolean value
1056          *
1057          * @param string|integer $val
1058          *
1059          * @return boolean
1060          */
1061         private static function toBoolean($val)
1062         {
1063                 if (($val == 'true') || ($val == 1)) {
1064                         return true;
1065                 } elseif (($val == 'false') || ($val == 0)) {
1066                         return false;
1067                 }
1068
1069                 return $val;
1070         }
1071
1072         /**
1073          * Detect if the URL belongs to a GNU Social server
1074          *
1075          * @param string $url        URL of the given server
1076          * @param array  $serverdata array with server data
1077          *
1078          * @return array server data
1079          */
1080         private static function detectGNUSocial(string $url, array $serverdata)
1081         {
1082                 // Test for GNU Social
1083                 $curlResult = Network::curl($url . '/api/gnusocial/version.json');
1084                 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
1085                         ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
1086                         $serverdata['platform'] = 'gnusocial';
1087                         // Remove junk that some GNU Social servers return
1088                         $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBody());
1089                         $serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
1090                         $serverdata['version'] = trim($serverdata['version'], '"');
1091                         $serverdata['network'] = Protocol::OSTATUS;
1092                         return $serverdata;
1093                 }
1094
1095                 // Test for Statusnet
1096                 $curlResult = Network::curl($url . '/api/statusnet/version.json');
1097                 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
1098                         ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
1099
1100                         // Remove junk that some GNU Social servers return
1101                         $serverdata['version'] = str_replace(chr(239).chr(187).chr(191), '', $curlResult->getBody());
1102                         $serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
1103                         $serverdata['version'] = trim($serverdata['version'], '"');
1104
1105                         if (!empty($serverdata['version']) && strtolower(substr($serverdata['version'], 0, 7)) == 'pleroma') {
1106                                 $serverdata['platform'] = 'pleroma';
1107                                 $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version']));
1108                                 $serverdata['network'] = Protocol::ACTIVITYPUB;
1109                         } else {
1110                                 $serverdata['platform'] = 'statusnet';
1111                                 $serverdata['network'] = Protocol::OSTATUS;
1112                         }
1113                 }
1114
1115                 return $serverdata;
1116         }
1117
1118         /**
1119          * Detect if the URL belongs to a Friendica server
1120          *
1121          * @param string $url        URL of the given server
1122          * @param array  $serverdata array with server data
1123          *
1124          * @return array server data
1125          */
1126         private static function detectFriendica(string $url, array $serverdata)
1127         {
1128                 $curlResult = Network::curl($url . '/friendica/json');
1129                 if (!$curlResult->isSuccess()) {
1130                         $curlResult = Network::curl($url . '/friendika/json');
1131                 }
1132
1133                 if (!$curlResult->isSuccess()) {
1134                         return $serverdata;
1135                 }
1136
1137                 $data = json_decode($curlResult->getBody(), true);
1138                 if (empty($data) || empty($data['version'])) {
1139                         return $serverdata;
1140                 }
1141
1142                 $serverdata['network'] = Protocol::DFRN;
1143                 $serverdata['version'] = $data['version'];
1144
1145                 if (!empty($data['no_scrape_url'])) {
1146                         $serverdata['noscrape'] = $data['no_scrape_url'];
1147                 }
1148
1149                 if (!empty($data['site_name'])) {
1150                         $serverdata['site_name'] = $data['site_name'];
1151                 }
1152
1153                 if (!empty($data['info'])) {
1154                         $serverdata['info'] = trim($data['info']);
1155                 }
1156
1157                 $register_policy = ($data['register_policy'] ?? '') ?: 'REGISTER_CLOSED';
1158                 switch ($register_policy) {
1159                         case 'REGISTER_OPEN':
1160                                 $serverdata['register_policy'] = Register::OPEN;
1161                                 break;
1162
1163                         case 'REGISTER_APPROVE':
1164                                 $serverdata['register_policy'] = Register::APPROVE;
1165                                 break;
1166
1167                         case 'REGISTER_CLOSED':
1168                         case 'REGISTER_INVITATION':
1169                                 $serverdata['register_policy'] = Register::CLOSED;
1170                                 break;
1171                         default:
1172                                 Logger::info('Register policy is invalid', ['policy' => $register_policy, 'server' => $url]);
1173                                 $serverdata['register_policy'] = Register::CLOSED;
1174                                 break;
1175                 }
1176
1177                 $serverdata['platform'] = strtolower($data['platform'] ?? '');
1178
1179                 return $serverdata;
1180         }
1181
1182         /**
1183          * Analyses the landing page of a given server for hints about type and system of that server
1184          *
1185          * @param object $curlResult result of curl execution
1186          * @param array  $serverdata array with server data
1187          * @param string $url        Server URL
1188          *
1189          * @return array server data
1190          */
1191         private static function analyseRootBody($curlResult, array $serverdata, string $url)
1192         {
1193                 $doc = new DOMDocument();
1194                 @$doc->loadHTML($curlResult->getBody());
1195                 $xpath = new DOMXPath($doc);
1196
1197                 $title = trim(XML::getFirstNodeValue($xpath, '//head/title/text()'));
1198                 if (!empty($title)) {
1199                         $serverdata['site_name'] = $title;
1200                 }
1201
1202                 $list = $xpath->query('//meta[@name]');
1203
1204                 foreach ($list as $node) {
1205                         $attr = [];
1206                         if ($node->attributes->length) {
1207                                 foreach ($node->attributes as $attribute) {
1208                                         $value = trim($attribute->value);
1209                                         if (empty($value)) {
1210                                                 continue;
1211                                         }
1212
1213                                         $attr[$attribute->name] = $value;
1214                                 }
1215
1216                                 if (empty($attr['name']) || empty($attr['content'])) {
1217                                         continue;
1218                                 }
1219                         }
1220
1221                         if ($attr['name'] == 'description') {
1222                                 $serverdata['info'] = $attr['content'];
1223                         }
1224
1225                         if ($attr['name'] == 'application-name') {
1226                                 $serverdata['platform'] = strtolower($attr['content']);
1227                                 if (in_array($attr['content'], ['Misskey', 'Write.as'])) {
1228                                         $serverdata['network'] = Protocol::ACTIVITYPUB;
1229                                 }
1230                         }
1231                         if (($attr['name'] == 'generator') && (empty($serverdata['platform']) || (substr(strtolower($attr['content']), 0, 9) == 'wordpress'))) {
1232                                 $serverdata['platform'] = strtolower($attr['content']);
1233                                 $version_part = explode(' ', $attr['content']);
1234
1235                                 if (count($version_part) == 2) {
1236                                         if (in_array($version_part[0], ['WordPress'])) {
1237                                                 $serverdata['platform'] = strtolower($version_part[0]);
1238                                                 $serverdata['version'] = $version_part[1];
1239
1240                                                 // We still do need a reliable test if some AP plugin is activated
1241                                                 if (DBA::exists('apcontact', ['baseurl' => $url])) {
1242                                                         $serverdata['network'] = Protocol::ACTIVITYPUB;
1243                                                 } else {
1244                                                         $serverdata['network'] = Protocol::FEED;
1245                                                 }
1246                                         }
1247                                         if (in_array($version_part[0], ['Friendika', 'Friendica'])) {
1248                                                 $serverdata['platform'] = strtolower($version_part[0]);
1249                                                 $serverdata['version'] = $version_part[1];
1250                                                 $serverdata['network'] = Protocol::DFRN;
1251                                         }
1252                                 }
1253                         }
1254                 }
1255
1256                 $list = $xpath->query('//meta[@property]');
1257
1258                 foreach ($list as $node) {
1259                         $attr = [];
1260                         if ($node->attributes->length) {
1261                                 foreach ($node->attributes as $attribute) {
1262                                         $value = trim($attribute->value);
1263                                         if (empty($value)) {
1264                                                 continue;
1265                                         }
1266
1267                                         $attr[$attribute->name] = $value;
1268                                 }
1269
1270                                 if (empty($attr['property']) || empty($attr['content'])) {
1271                                         continue;
1272                                 }
1273                         }
1274
1275                         if ($attr['property'] == 'og:site_name') {
1276                                 $serverdata['site_name'] = $attr['content'];
1277                         }
1278
1279                         if ($attr['property'] == 'og:description') {
1280                                 $serverdata['info'] = $attr['content'];
1281                         }
1282
1283                         if ($attr['property'] == 'og:platform') {
1284                                 $serverdata['platform'] = strtolower($attr['content']);
1285
1286                                 if (in_array($attr['content'], ['PeerTube'])) {
1287                                         $serverdata['network'] = Protocol::ACTIVITYPUB;
1288                                 }
1289                         }
1290
1291                         if ($attr['property'] == 'generator') {
1292                                 $serverdata['platform'] = strtolower($attr['content']);
1293
1294                                 if (in_array($attr['content'], ['hubzilla'])) {
1295                                         // We later check which compatible protocol modules are loaded.
1296                                         $serverdata['network'] = Protocol::ZOT;
1297                                 }
1298                         }
1299                 }
1300
1301                 return $serverdata;
1302         }
1303
1304         /**
1305          * Analyses the header data of a given server for hints about type and system of that server
1306          *
1307          * @param object $curlResult result of curl execution
1308          * @param array  $serverdata array with server data
1309          *
1310          * @return array server data
1311          */
1312         private static function analyseRootHeader($curlResult, array $serverdata)
1313         {
1314                 if ($curlResult->getHeader('server') == 'Mastodon') {
1315                         $serverdata['platform'] = 'mastodon';
1316                         $serverdata['network'] = $network = Protocol::ACTIVITYPUB;
1317                 } elseif ($curlResult->inHeader('x-diaspora-version')) {
1318                         $serverdata['platform'] = 'diaspora';
1319                         $serverdata['network'] = $network = Protocol::DIASPORA;
1320                         $serverdata['version'] = $curlResult->getHeader('x-diaspora-version');
1321                 } elseif ($curlResult->inHeader('x-friendica-version')) {
1322                         $serverdata['platform'] = 'friendica';
1323                         $serverdata['network'] = $network = Protocol::DFRN;
1324                         $serverdata['version'] = $curlResult->getHeader('x-friendica-version');
1325                 }
1326                 return $serverdata;
1327         }
1328
1329         /**
1330          * Test if the body contains valid content
1331          *
1332          * @param string $body
1333          * @return boolean
1334          */
1335         private static function invalidBody(string $body)
1336         {
1337                 // Currently we only test for a HTML element.
1338                 // Possibly we enhance this in the future.
1339                 return !strpos($body, '>');
1340         }
1341
1342         /**
1343          * Update the user directory of a given gserver record
1344          *
1345          * @param array $gserver gserver record
1346          */
1347         public static function updateDirectory(array $gserver)
1348         {
1349                 /// @todo Add Mastodon API directory
1350
1351                 if (!empty($gserver['poco'])) {
1352                         PortableContact::discoverSingleServer($gserver['id']);
1353                 }
1354         }
1355
1356         /**
1357          * Update GServer entries
1358          */
1359         public static function discover()
1360         {
1361                 // Update the server list
1362                 self::discoverFederation();
1363
1364                 $no_of_queries = 5;
1365
1366                 $requery_days = intval(DI::config()->get('system', 'poco_requery_days'));
1367
1368                 if ($requery_days == 0) {
1369                         $requery_days = 7;
1370                 }
1371
1372                 $last_update = date('c', time() - (60 * 60 * 24 * $requery_days));
1373
1374                 $gservers = DBA::p("SELECT `id`, `url`, `nurl`, `network`, `poco`
1375                         FROM `gserver`
1376                         WHERE `last_contact` >= `last_failure`
1377                         AND `poco` != ''
1378                         AND `last_poco_query` < ?
1379                         ORDER BY RAND()", $last_update
1380                 );
1381
1382                 while ($gserver = DBA::fetch($gservers)) {
1383                         if (!GServer::check($gserver['url'], $gserver['network'])) {
1384                                 // The server is not reachable? Okay, then we will try it later
1385                                 $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
1386                                 DBA::update('gserver', $fields, ['nurl' => $gserver['nurl']]);
1387                                 continue;
1388                         }
1389
1390                         Logger::info('Update directory', ['server' => $gserver['url'], 'id' => $gserver['id']]);
1391                         Worker::add(PRIORITY_LOW, 'UpdateServerDirectory', $gserver);
1392
1393                         if (--$no_of_queries == 0) {
1394                                 break;
1395                         }
1396                 }
1397
1398                 DBA::close($gservers);
1399         }
1400
1401         /**
1402          * Discover federated servers
1403          */
1404         private static function discoverFederation()
1405         {
1406                 $last = DI::config()->get('poco', 'last_federation_discovery');
1407
1408                 if ($last) {
1409                         $next = $last + (24 * 60 * 60);
1410
1411                         if ($next > time()) {
1412                                 return;
1413                         }
1414                 }
1415
1416                 // Discover federated servers
1417                 $curlResult = Network::fetchUrl("http://the-federation.info/pods.json");
1418
1419                 if (!empty($curlResult)) {
1420                         $servers = json_decode($curlResult, true);
1421
1422                         if (!empty($servers['pods'])) {
1423                                 foreach ($servers['pods'] as $server) {
1424                                         Worker::add(PRIORITY_LOW, 'UpdateGServer', 'https://' . $server['host']);
1425                                 }
1426                         }
1427                 }
1428
1429                 // Disvover Mastodon servers
1430                 $accesstoken = DI::config()->get('system', 'instances_social_key');
1431
1432                 if (!empty($accesstoken)) {
1433                         $api = 'https://instances.social/api/1.0/instances/list?count=0';
1434                         $header = ['Authorization: Bearer '.$accesstoken];
1435                         $curlResult = Network::curl($api, false, ['headers' => $header]);
1436
1437                         if ($curlResult->isSuccess()) {
1438                                 $servers = json_decode($curlResult->getBody(), true);
1439
1440                                 foreach ($servers['instances'] as $server) {
1441                                         $url = (is_null($server['https_score']) ? 'http' : 'https') . '://' . $server['name'];
1442                                         Worker::add(PRIORITY_LOW, 'UpdateGServer', $url);
1443                                 }
1444                         }
1445                 }
1446
1447                 DI::config()->set('poco', 'last_federation_discovery', time());
1448         }
1449 }