]> git.mxchange.org Git - friendica.git/blob - src/Model/GServer.php
Code transitioning from PortableContacts.php to GServer.php
[friendica.git] / src / Model / GServer.php
1 <?php
2
3 /**
4  * @file src/Model/GServer.php
5  * @brief This file includes the GServer class to handle with servers
6  */
7 namespace Friendica\Model;
8
9 use DOMDocument;
10 use DOMXPath;
11 use Friendica\Core\Config;
12 use Friendica\Core\Protocol;
13 use Friendica\Database\DBA;
14 use Friendica\Module\Register;
15 use Friendica\Util\Network;
16 use Friendica\Util\DateTimeFormat;
17 use Friendica\Util\Strings;
18 use Friendica\Util\XML;
19 use Friendica\Core\Logger;
20 use Friendica\Protocol\PortableContact;
21 use Friendica\Protocol\Diaspora;
22
23 /**
24  * @brief This class handles GServer related functions
25  */
26 class GServer
27 {
28         public static function check($server_url, $force = false)
29         {
30                 // Unify the server address
31                 $server_url = trim($server_url, '/');
32                 $server_url = str_replace('/index.php', '', $server_url);
33
34                 if ($server_url == '') {
35                         return false;
36                 }
37
38                 $gserver = DBA::selectFirst('gserver', [], ['nurl' => Strings::normaliseLink($server_url)]);
39                 if (DBA::isResult($gserver)) {
40                         if ($gserver['created'] <= DBA::NULL_DATETIME) {
41                                 $fields = ['created' => DateTimeFormat::utcNow()];
42                                 $condition = ['nurl' => Strings::normaliseLink($server_url)];
43                                 DBA::update('gserver', $fields, $condition);
44                         }
45
46                         $last_contact = $gserver["last_contact"];
47                         $last_failure = $gserver["last_failure"];
48
49                         // See discussion under https://forum.friendi.ca/display/0b6b25a8135aabc37a5a0f5684081633
50                         // It can happen that a zero date is in the database, but storing it again is forbidden.
51                         if ($last_contact < DBA::NULL_DATETIME) {
52                                 $last_contact = DBA::NULL_DATETIME;
53                         }
54
55                         if ($last_failure < DBA::NULL_DATETIME) {
56                                 $last_failure = DBA::NULL_DATETIME;
57                         }
58
59                         if (!$force && !PortableContact::updateNeeded($gserver['created'], '', $last_failure, $last_contact)) {
60                                 Logger::info('Use cached data', ['server' => $server_url]);
61                                 return ($last_contact >= $last_failure);
62                         }
63                 }
64                 Logger::info('Server is outdated or unknown. Start discovery.', ['Server' => $server_url, 'Force' => $force, 'Created' => $gserver['created'], 'Failure' => $last_failure, 'Contact' => $last_contact]);
65
66                 return self::detect($server_url);
67         }
68
69         /**
70          * Detect server data (type, protocol, version number, ...)
71          * The detected data is then updated or inserted in the gserver table.
72          *
73          * @param string $url Server url
74          *
75          * @return boolean 'true' if server could be detected
76          */
77         public static function detect($url)
78         {
79                 $serverdata = [];
80
81                 // When a nodeinfo is present, we don't need to dig further
82                 $xrd_timeout = Config::get('system', 'xrd_timeout');
83                 $curlResult = Network::curl($url . '/.well-known/nodeinfo', false, ['timeout' => $xrd_timeout]);
84                 if ($curlResult->isTimeout()) {
85                         DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
86                         return false;
87                 }
88
89                 $nodeinfo = self::fetchNodeinfo($url, $curlResult);
90
91                 // When nodeinfo isn't present, we use the older 'statistics.json' endpoint
92                 if (empty($nodeinfo)) {
93                         $nodeinfo = self::fetchStatistics($url);
94                 }
95
96                 // If that didn't work out well, we use some protocol specific endpoints
97                 if (empty($nodeinfo) || empty($nodeinfo['network']) || ($nodeinfo['network'] == Protocol::DFRN)) {
98                         // Fetch the landing page, possibly it reveals some data
99                         $curlResult = Network::curl($url, false, ['timeout' => $xrd_timeout]);
100                         if ($curlResult->isSuccess()) {
101                                 $serverdata = self::analyseRootHeader($curlResult, $serverdata);
102                                 $serverdata = self::analyseRootBody($curlResult, $serverdata);
103                         }
104
105                         if (!$curlResult->isSuccess() || empty($curlResult->getBody())) {
106                                 DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
107                                 return false;
108                         }
109
110                         if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::DFRN)) {
111                                 $serverdata = self::detectFriendica($url, $serverdata);
112                         }
113
114                         if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ACTIVITYPUB)) {
115                                 $serverdata = self::detectMastodonAlikes($url, $serverdata);
116                         }
117
118                         // the 'siteinfo.json' is some specific endpoint of Hubzilla and Red
119                         if (empty($serverdata['network']) || ($serverdata['network'] == Protocol::ZOT)) {
120                                 $serverdata = self::fetchSiteinfo($url, $serverdata);
121                         }
122
123                         // The 'siteinfo.json' doesn't seem to be present on older Hubzilla installations
124                         if (empty($serverdata['network'])) {
125                                 $serverdata = self::detectHubzilla($url, $serverdata);
126                         }
127
128                         if (empty($serverdata['network'])) {
129                                 $serverdata = self::detectNextcloud($url, $serverdata);
130                         }
131
132                         if (empty($serverdata['network'])) {
133                                 $serverdata = self::detectGNUSocial($url, $serverdata);
134                         }
135                 } else {
136                         $serverdata = $nodeinfo;
137                 }
138
139                 $serverdata = self::checkPoCo($url, $serverdata);
140
141                 // We can't detect the network type. Possibly it is some system that we don't know yet
142                 if (empty($serverdata['network'])) {
143                         $serverdata['network'] = Protocol::PHANTOM;
144                 }
145
146                 // Check host-meta for phantom networks.
147                 // Although this is not needed, it is a good indicator for a living system,
148                 // since most systems had implemented it.
149                 if (($serverdata['network'] == Protocol::PHANTOM) && !self::validHostMeta($url)) {
150                         DBA::update('gserver', ['last_failure' => DateTimeFormat::utcNow()], ['nurl' => Strings::normaliseLink($url)]);
151                         return false;
152                 }
153
154                 $serverdata['url'] = $url;
155                 $serverdata['nurl'] = Strings::normaliseLink($url);
156
157                 // We take the highest number that we do find
158                 $registeredUsers = $serverdata['registered-users'] ?? 0;
159
160                 // On an active server there has to be at least a single user
161                 if (($serverdata['network'] != Protocol::PHANTOM) && ($registeredUsers == 0)) {
162                         $registeredUsers = 1;
163                 }
164
165                 $gcontacts = DBA::count('gcontact', ['server_url' => [$url, $serverdata['nurl']]]);
166                 $apcontacts = DBA::count('apcontact', ['baseurl' => [$url, $serverdata['nurl']]]);
167                 $contacts = DBA::count('contact', ['uid' => 0, 'baseurl' => [$url, $serverdata['nurl']]]);
168                 $serverdata['registered-users'] = max($gcontacts, $apcontacts, $contacts, $registeredUsers);
169
170                 $serverdata['last_contact'] = DateTimeFormat::utcNow();
171
172                 if (!DBA::exists('gserver', ['nurl' => Strings::normaliseLink($url)])) {
173                         $serverdata['created'] = DateTimeFormat::utcNow();
174                         $ret = DBA::insert('gserver', $serverdata);
175                 } else {
176                         $ret = DBA::update('gserver', $serverdata, ['nurl' => $serverdata['nurl']]);
177                 }
178
179                 if (in_array($serverdata['network'], [Protocol::DFRN, Protocol::DIASPORA])) {
180                         self::discoverRelay($url);
181                 }
182
183                 return $ret;
184         }
185
186         /**
187          * @brief Fetch relay data from a given server url
188          *
189          * @param string $server_url address of the server
190          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
191          */
192         private static function discoverRelay($server_url)
193         {
194                 Logger::info('Discover relay data', ['server' => $server_url]);
195
196                 $curlResult = Network::curl($server_url . '/.well-known/x-social-relay');
197                 if (!$curlResult->isSuccess()) {
198                         return;
199                 }
200
201                 $data = json_decode($curlResult->getBody(), true);
202                 if (!is_array($data)) {
203                         return;
204                 }
205
206                 $gserver = DBA::selectFirst('gserver', ['id', 'relay-subscribe', 'relay-scope'], ['nurl' => Strings::normaliseLink($server_url)]);
207                 if (!DBA::isResult($gserver)) {
208                         return;
209                 }
210
211                 if (($gserver['relay-subscribe'] != $data['subscribe']) || ($gserver['relay-scope'] != $data['scope'])) {
212                         $fields = ['relay-subscribe' => $data['subscribe'], 'relay-scope' => $data['scope']];
213                         DBA::update('gserver', $fields, ['id' => $gserver['id']]);
214                 }
215
216                 DBA::delete('gserver-tag', ['gserver-id' => $gserver['id']]);
217
218                 if ($data['scope'] == 'tags') {
219                         // Avoid duplicates
220                         $tags = [];
221                         foreach ($data['tags'] as $tag) {
222                                 $tag = mb_strtolower($tag);
223                                 if (strlen($tag) < 100) {
224                                         $tags[$tag] = $tag;
225                                 }
226                         }
227
228                         foreach ($tags as $tag) {
229                                 DBA::insert('gserver-tag', ['gserver-id' => $gserver['id'], 'tag' => $tag], true);
230                         }
231                 }
232
233                 // Create or update the relay contact
234                 $fields = [];
235                 if (isset($data['protocols'])) {
236                         if (isset($data['protocols']['diaspora'])) {
237                                 $fields['network'] = Protocol::DIASPORA;
238
239                                 if (isset($data['protocols']['diaspora']['receive'])) {
240                                         $fields['batch'] = $data['protocols']['diaspora']['receive'];
241                                 } elseif (is_string($data['protocols']['diaspora'])) {
242                                         $fields['batch'] = $data['protocols']['diaspora'];
243                                 }
244                         }
245
246                         if (isset($data['protocols']['dfrn'])) {
247                                 $fields['network'] = Protocol::DFRN;
248
249                                 if (isset($data['protocols']['dfrn']['receive'])) {
250                                         $fields['batch'] = $data['protocols']['dfrn']['receive'];
251                                 } elseif (is_string($data['protocols']['dfrn'])) {
252                                         $fields['batch'] = $data['protocols']['dfrn'];
253                                 }
254                         }
255                 }
256                 Diaspora::setRelayContact($server_url, $fields);
257         }
258
259         private static function fetchStatistics($url)
260         {
261                 $curlResult = Network::curl($url . '/statistics.json');
262                 if (!$curlResult->isSuccess()) {
263                         return [];
264                 }
265
266                 $data = json_decode($curlResult->getBody(), true);
267                 if (empty($data)) {
268                         return [];
269                 }
270
271                 $serverdata = [];
272
273                 if (!empty($data['version'])) {
274                         $serverdata['version'] = $data['version'];
275                         // Version numbers on statistics.json are presented with additional info, e.g.:
276                         // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
277                         $serverdata['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $serverdata['version']);
278                 }
279
280                 if (!empty($data['name'])) {
281                         $serverdata['site_name'] = $data['name'];
282                 }
283
284                 if (!empty($data['network'])) {
285                         $serverdata['platform'] = $data['network'];
286
287                         if ($serverdata['platform'] == 'Diaspora') {
288                                 $serverdata['network'] = Protocol::DIASPORA;
289                         } elseif ($serverdata['platform'] == 'Friendica') {
290                                 $serverdata['network'] = Protocol::DFRN;
291                         } elseif ($serverdata['platform'] == 'hubzilla') {
292                                 $serverdata['network'] = Protocol::ZOT;
293                         } elseif ($serverdata['platform'] == 'redmatrix') {
294                                 $serverdata['network'] = Protocol::ZOT;
295                         }
296                 }
297
298
299                 if (!empty($data['registrations_open'])) {
300                         $serverdata['register_policy'] = Register::OPEN;
301                 } else {
302                         $serverdata['register_policy'] = Register::CLOSED;
303                 }
304
305                 return $serverdata;
306         }
307
308         /**
309          * @brief Detect server type by using the nodeinfo data
310          *
311          * @param string $url address of the server
312          * @return array Server data
313          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
314          */
315         private static function fetchNodeinfo($url, $curlResult)
316         {
317                 $nodeinfo = json_decode($curlResult->getBody(), true);
318
319                 if (!is_array($nodeinfo) || empty($nodeinfo['links'])) {
320                         return [];
321                 }
322
323                 $nodeinfo1_url = '';
324                 $nodeinfo2_url = '';
325
326                 foreach ($nodeinfo['links'] as $link) {
327                         if (!is_array($link) || empty($link['rel']) || empty($link['href'])) {
328                                 Logger::info('Invalid nodeinfo format', ['url' => $url]);
329                                 continue;
330                         }
331                         if ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/1.0') {
332                                 $nodeinfo1_url = $link['href'];
333                         } elseif ($link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0') {
334                                 $nodeinfo2_url = $link['href'];
335                         }
336                 }
337
338                 if ($nodeinfo1_url . $nodeinfo2_url == '') {
339                         return [];
340                 }
341
342                 $server = [];
343
344                 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
345                 if (!empty($nodeinfo2_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo2_url, PHP_URL_HOST))) {
346                         $server = self::parseNodeinfo2($nodeinfo2_url);
347                 }
348
349                 // When the nodeinfo url isn't on the same host, then there is obviously something wrong
350                 if (empty($server) && !empty($nodeinfo1_url) && (parse_url($url, PHP_URL_HOST) == parse_url($nodeinfo1_url, PHP_URL_HOST))) {
351                         $server = self::parseNodeinfo1($nodeinfo1_url);
352                 }
353
354                 return $server;
355         }
356
357         /**
358          * @brief Parses Nodeinfo 1
359          *
360          * @param string $nodeinfo_url address of the nodeinfo path
361          * @return array Server data
362          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
363          */
364         private static function parseNodeinfo1($nodeinfo_url)
365         {
366                 $curlResult = Network::curl($nodeinfo_url);
367
368                 if (!$curlResult->isSuccess()) {
369                         return false;
370                 }
371
372                 $nodeinfo = json_decode($curlResult->getBody(), true);
373
374                 if (!is_array($nodeinfo)) {
375                         return false;
376                 }
377
378                 $server = [];
379
380                 $server['register_policy'] = Register::CLOSED;
381
382                 if (!empty($nodeinfo['openRegistrations'])) {
383                         $server['register_policy'] = Register::OPEN;
384                 }
385
386                 if (is_array($nodeinfo['software'])) {
387                         if (!empty($nodeinfo['software']['name'])) {
388                                 $server['platform'] = $nodeinfo['software']['name'];
389                         }
390
391                         if (!empty($nodeinfo['software']['version'])) {
392                                 $server['version'] = $nodeinfo['software']['version'];
393                                 // Version numbers on Nodeinfo are presented with additional info, e.g.:
394                                 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
395                                 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
396                         }
397                 }
398
399                 if (!empty($nodeinfo['metadata']['nodeName'])) {
400                         $server['site_name'] = $nodeinfo['metadata']['nodeName'];
401                 }
402
403                 if (!empty($nodeinfo['usage']['users']['total'])) {
404                         $server['registered-users'] = $nodeinfo['usage']['users']['total'];
405                 }
406
407                 if (!empty($nodeinfo['protocols']['inbound']) && is_array($nodeinfo['protocols']['inbound'])) {
408                         $protocols = [];
409                         foreach ($nodeinfo['protocols']['inbound'] as $protocol) {
410                                 $protocols[$protocol] = true;
411                         }
412
413                         if (!empty($protocols['friendica'])) {
414                                 $server['network'] = Protocol::DFRN;
415                         } elseif (!empty($protocols['activitypub'])) {
416                                 $server['network'] = Protocol::ACTIVITYPUB;
417                         } elseif (!empty($protocols['diaspora'])) {
418                                 $server['network'] = Protocol::DIASPORA;
419                         } elseif (!empty($protocols['ostatus'])) {
420                                 $server['network'] = Protocol::OSTATUS;
421                         } elseif (!empty($protocols['gnusocial'])) {
422                                 $server['network'] = Protocol::OSTATUS;
423                         } elseif (!empty($protocols['zot'])) {
424                                 $server['network'] = Protocol::ZOT;
425                         }
426                 }
427
428                 if (!$server) {
429                         return false;
430                 }
431
432                 return $server;
433         }
434
435         /**
436          * @brief Parses Nodeinfo 2
437          *
438          * @param string $nodeinfo_url address of the nodeinfo path
439          * @return array Server data
440          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
441          */
442         private static function parseNodeinfo2($nodeinfo_url)
443         {
444                 $curlResult = Network::curl($nodeinfo_url);
445                 if (!$curlResult->isSuccess()) {
446                         return false;
447                 }
448
449                 $nodeinfo = json_decode($curlResult->getBody(), true);
450
451                 if (!is_array($nodeinfo)) {
452                         return false;
453                 }
454
455                 $server = [];
456
457                 $server['register_policy'] = Register::CLOSED;
458
459                 if (!empty($nodeinfo['openRegistrations'])) {
460                         $server['register_policy'] = Register::OPEN;
461                 }
462
463                 if (is_array($nodeinfo['software'])) {
464                         if (!empty($nodeinfo['software']['name'])) {
465                                 $server['platform'] = $nodeinfo['software']['name'];
466                         }
467
468                         if (!empty($nodeinfo['software']['version'])) {
469                                 $server['version'] = $nodeinfo['software']['version'];
470                                 // Version numbers on Nodeinfo are presented with additional info, e.g.:
471                                 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
472                                 $server['version'] = preg_replace('=(.+)-(.{4,})=ism', '$1', $server['version']);
473                         }
474                 }
475
476                 if (!empty($nodeinfo['metadata']['nodeName'])) {
477                         $server['site_name'] = $nodeinfo['metadata']['nodeName'];
478                 }
479
480                 if (!empty($nodeinfo['usage']['users']['total'])) {
481                         $server['registered-users'] = $nodeinfo['usage']['users']['total'];
482                 }
483
484                 if (!empty($nodeinfo['protocols'])) {
485                         $protocols = [];
486                         foreach ($nodeinfo['protocols'] as $protocol) {
487                                 $protocols[$protocol] = true;
488                         }
489
490                         if (!empty($protocols['friendica'])) {
491                                 $server['network'] = Protocol::DFRN;
492                         } elseif (!empty($protocols['activitypub'])) {
493                                 $server['network'] = Protocol::ACTIVITYPUB;
494                         } elseif (!empty($protocols['diaspora'])) {
495                                 $server['network'] = Protocol::DIASPORA;
496                         } elseif (!empty($protocols['ostatus'])) {
497                                 $server['network'] = Protocol::OSTATUS;
498                         } elseif (!empty($protocols['gnusocial'])) {
499                                 $server['network'] = Protocol::OSTATUS;
500                         } elseif (!empty($protocols['zot'])) {
501                                 $server['network'] = Protocol::ZOT;
502                         }
503                 }
504
505                 if (empty($server)) {
506                         return false;
507                 }
508
509                 return $server;
510         }
511
512         private static function fetchSiteinfo($url, $serverdata)
513         {
514                 $curlResult = Network::curl($url . '/siteinfo.json');
515                 if (!$curlResult->isSuccess()) {
516                         return $serverdata;
517                 }
518
519                 $data = json_decode($curlResult->getBody(), true);
520                 if (empty($data)) {
521                         return $serverdata;
522                 }
523
524                 if (!empty($data['url'])) {
525                         $serverdata['platform'] = $data['platform'];
526                         $serverdata['version'] = $data['version'];
527                 }
528
529                 if (!empty($data['plugins'])) {
530                         if (in_array('pubcrawl', $data['plugins'])) {
531                                 $serverdata['network'] = Protocol::ACTIVITYPUB;
532                         } elseif (in_array('diaspora', $data['plugins'])) {
533                                 $serverdata['network'] = Protocol::DIASPORA;
534                         } elseif (in_array('gnusoc', $data['plugins'])) {
535                                 $serverdata['network'] = Protocol::OSTATUS;
536                         } else {
537                                 $serverdata['network'] = Protocol::ZOT;
538                         }
539                 }
540
541                 if (!empty($data['site_name'])) {
542                         $serverdata['site_name'] = $data['site_name'];
543                 }
544
545                 if (!empty($data['channels_total'])) {
546                         $serverdata['registered-users'] = $data['channels_total'];
547                 }
548
549                 if (!empty($data['register_policy'])) {
550                         switch ($data['register_policy']) {
551                                 case 'REGISTER_OPEN':
552                                         $serverdata['register_policy'] = Register::OPEN;
553                                         break;
554
555                                 case 'REGISTER_APPROVE':
556                                         $serverdata['register_policy'] = Register::APPROVE;
557                                         break;
558
559                                 case 'REGISTER_CLOSED':
560                                 default:
561                                         $serverdata['register_policy'] = Register::CLOSED;
562                                         break;
563                         }
564                 }
565
566                 return $serverdata;
567         }
568
569         private static function validHostMeta($url)
570         {
571                 $xrd_timeout = Config::get('system', 'xrd_timeout');
572                 $curlResult = Network::curl($url . '/.well-known/host-meta', false, ['timeout' => $xrd_timeout]);
573                 if (!$curlResult->isSuccess()) {
574                         return false;
575                 }
576
577                 $xrd = XML::parseString($curlResult->getBody(), false);
578                 if (!is_object($xrd)) {
579                         return false;
580                 }
581
582                 $elements = XML::elementToArray($xrd);
583                 if (empty($elements) || empty($elements['xrd']) || empty($elements['xrd']['link'])) {
584                         return false;
585                 }
586
587                 $valid = false;
588                 foreach ($elements['xrd']['link'] as $link) {
589                         if (empty($link['rel']) || empty($link['type']) || empty($link['template'])) {
590                                 continue;
591                         }
592
593                         if ($link['type'] == 'application/xrd+xml') {
594                                 // When the webfinger host is the same like the system host, it should be ok.
595                                 $valid = (parse_url($url, PHP_URL_HOST) == parse_url($link['template'], PHP_URL_HOST));
596                         }
597                 }
598
599                 return $valid;
600         }
601
602         private static function checkPoCo($url, $serverdata)
603         {
604                 $curlResult = Network::curl($url. '/poco');
605                 if (!$curlResult->isSuccess()) {
606                         return $serverdata;
607                 }
608
609                 $data = json_decode($curlResult->getBody(), true);
610                 if (empty($data)) {
611                         return $serverdata;
612                 }
613
614                 if (!empty($data['totalResults'])) {
615                         $registeredUsers = $serverdata['registered-users'] ?? 0;
616                         $serverdata['registered-users'] = max($data['totalResults'], $registeredUsers);
617                         $serverdata['poco'] = $url . '/poco';
618                 } else {
619                         $serverdata['poco'] = '';
620                 }
621
622                 return $serverdata;
623         }
624
625         private static function detectNextcloud($url, $serverdata)
626         {
627                 $curlResult = Network::curl($url . '/status.php');
628
629                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
630                         return $serverdata;
631                 }
632
633                 $data = json_decode($curlResult->getBody(), true);
634                 if (empty($data)) {
635                         return $serverdata;
636                 }
637
638                 if (!empty($data['version'])) {
639                         $serverdata['platform'] = 'nextcloud';
640                         $serverdata['version'] = $data['version'];
641                         $serverdata['network'] = Protocol::ACTIVITYPUB;
642                 }
643
644                 return $serverdata;
645         }
646
647         private static function detectMastodonAlikes($url, $serverdata)
648         {
649                 $curlResult = Network::curl($url . '/api/v1/instance');
650
651                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
652                         return $serverdata;
653                 }
654
655                 $data = json_decode($curlResult->getBody(), true);
656                 if (empty($data)) {
657                         return $serverdata;
658                 }
659
660                 if (!empty($data['version'])) {
661                         $serverdata['platform'] = 'mastodon';
662                         $serverdata['version'] = defaults($data, 'version', '');
663                         $serverdata['network'] = Protocol::ACTIVITYPUB;
664                 }
665
666                 if (!empty($data['title'])) {
667                         $serverdata['site_name'] = $data['title'];
668                 }
669
670                 if (!empty($data['description'])) {
671                         $serverdata['info'] = trim($data['description']);
672                 }
673
674                 if (!empty($data['stats']['user_count'])) {
675                         $serverdata['registered-users'] = $data['stats']['user_count'];
676                 }
677
678                 if (!empty($serverdata['version']) && preg_match('/.*?\(compatible;\s(.*)\s(.*)\)/ism', $serverdata['version'], $matches)) {
679                         $serverdata['platform'] = $matches[1];
680                         $serverdata['version'] = $matches[2];
681                 }
682
683                 if (!empty($serverdata['version']) && strstr($serverdata['version'], 'Pleroma')) {
684                         $serverdata['platform'] = 'pleroma';
685                         $serverdata['version'] = trim(str_replace('Pleroma', '', $serverdata['version']));
686                 }
687
688                 return $serverdata;
689         }
690
691         private static function detectHubzilla($url, $serverdata)
692         {
693                 $curlResult = Network::curl($url . '/api/statusnet/config.json');
694                 if (!$curlResult->isSuccess() || ($curlResult->getBody() == '')) {
695                         return $serverdata;
696                 }
697
698                 $data = json_decode($curlResult->getBody(), true);
699                 if (empty($data)) {
700                         return $serverdata;
701                 }
702
703                 if (!empty($data['site']['name'])) {
704                         $serverdata['site_name'] = $data['site']['name'];
705                 }
706
707                 if (!empty($data['site']['platform'])) {
708                         $serverdata['platform'] = $data['site']['platform']['PLATFORM_NAME'];
709                         $serverdata['version'] = $data['site']['platform']['STD_VERSION'];
710                         $serverdata['network'] = Protocol::ZOT;
711                 }
712
713                 if (!empty($data['site']['hubzilla'])) {
714                         $serverdata['platform'] = $data['site']['hubzilla']['PLATFORM_NAME'];
715                         $serverdata['version'] = $data['site']['hubzilla']['RED_VERSION'];
716                         $serverdata['network'] = Protocol::ZOT;
717                 }
718
719                 if (!empty($data['site']['redmatrix'])) {
720                         if (!empty($data['site']['redmatrix']['PLATFORM_NAME'])) {
721                                 $serverdata['platform'] = $data['site']['redmatrix']['PLATFORM_NAME'];
722                         } elseif (!empty($data['site']['redmatrix']['RED_PLATFORM'])) {
723                                 $serverdata['platform'] = $data['site']['redmatrix']['RED_PLATFORM'];
724                         }
725
726                         $serverdata['version'] = $data['site']['redmatrix']['RED_VERSION'];
727                         $serverdata['network'] = Protocol::ZOT;
728                 }
729
730                 $private = false;
731                 $inviteonly = false;
732                 $closed = false;
733
734                 if (!empty($data['site']['closed'])) {
735                         $closed = self::toBoolean($data['site']['closed']);
736                 }
737
738                 if (!empty($data['site']['private'])) {
739                         $private = self::toBoolean($data['site']['private']);
740                 }
741
742                 if (!empty($data['site']['inviteonly'])) {
743                         $inviteonly = self::toBoolean($data['site']['inviteonly']);
744                 }
745
746                 if (!$closed && !$private and $inviteonly) {
747                         $register_policy = Register::APPROVE;
748                 } elseif (!$closed && !$private) {
749                         $register_policy = Register::OPEN;
750                 } else {
751                         $register_policy = Register::CLOSED;
752                 }
753
754                 return $serverdata;
755         }
756
757         private static function toBoolean($val)
758         {
759                 if (($val == 'true') || ($val == 1)) {
760                         return true;
761                 } elseif (($val == 'false') || ($val == 0)) {
762                         return false;
763                 }
764
765                 return $val;
766         }
767
768         private static function detectGNUSocial($url, $serverdata)
769         {
770                 $curlResult = Network::curl($url . '/api/statusnet/version.json');
771
772                 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
773                         ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
774                         $serverdata['platform'] = 'StatusNet';
775                         // Remove junk that some GNU Social servers return
776                         $serverdata['version'] = str_replace(chr(239).chr(187).chr(191), '', $curlResult->getBody());
777                         $serverdata['version'] = trim($serverdata['version'], '"');
778                         $serverdata['network'] = Protocol::OSTATUS;
779                 }
780
781                 // Test for GNU Social
782                 $curlResult = Network::curl($url . '/api/gnusocial/version.json');
783
784                 if ($curlResult->isSuccess() && ($curlResult->getBody() != '{"error":"not implemented"}') &&
785                         ($curlResult->getBody() != '') && (strlen($curlResult->getBody()) < 30)) {
786                         $serverdata['platform'] = 'GNU Social';
787                         // Remove junk that some GNU Social servers return
788                         $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBody());
789                         $serverdata['version'] = trim($serverdata['version'], '"');
790                         $serverdata['network'] = Protocol::OSTATUS;
791                 }
792
793                 return $serverdata;
794         }
795
796         private static function detectFriendica($url, $serverdata)
797         {
798                 $curlResult = Network::curl($url . '/friendica/json');
799                 if (!$curlResult->isSuccess()) {
800                         $curlResult = Network::curl($url . '/friendika/json');
801                 }
802
803                 if (!$curlResult->isSuccess()) {
804                         return $serverdata;
805                 }
806
807                 $data = json_decode($curlResult->getBody(), true);
808                 if (empty($data) || empty($data['version'])) {
809                         return $serverdata;
810                 }
811
812                 $serverdata['network'] = Protocol::DFRN;
813                 $serverdata['version'] = $data['version'];
814
815                 if (!empty($data['no_scrape_url'])) {
816                         $serverdata['noscrape'] = $data['no_scrape_url'];
817                 }
818
819                 if (!empty($data['site_name'])) {
820                         $serverdata['site_name'] = $data['site_name'];
821                 }
822
823                 if (!empty($data['info'])) {
824                         $serverdata['info'] = trim($data['info']);
825                 }
826
827                 $register_policy = defaults($data, 'register_policy', 'REGISTER_CLOSED');
828                 switch ($register_policy) {
829                         case 'REGISTER_OPEN':
830                                 $serverdata['register_policy'] = Register::OPEN;
831                                 break;
832
833                         case 'REGISTER_APPROVE':
834                                 $serverdata['register_policy'] = Register::APPROVE;
835                                 break;
836
837                         case 'REGISTER_CLOSED':
838                         case 'REGISTER_INVITATION':
839                                 $serverdata['register_policy'] = Register::CLOSED;
840                                 break;
841                         default:
842                                 Logger::info('Register policy is invalid', ['policy' => $register_policy, 'server' => $url]);
843                                 $serverdata['register_policy'] = Register::CLOSED;
844                                 break;
845                 }
846
847                 $serverdata['platform'] = defaults($data, 'platform', '');
848
849                 return $serverdata;
850         }
851
852         private static function analyseRootBody($curlResult, $serverdata)
853         {
854                 $doc = new DOMDocument();
855                 @$doc->loadHTML($curlResult->getBody());
856                 $xpath = new DOMXPath($doc);
857
858                 $title = trim(XML::getFirstNodeValue($xpath, '//head/title/text()'));
859                 if (!empty($title)) {
860                         $serverdata['site_name'] = $title;
861                 }
862
863                 $list = $xpath->query('//meta[@name]');
864
865                 foreach ($list as $node) {
866                         $attr = [];
867                         if ($node->attributes->length) {
868                                 foreach ($node->attributes as $attribute) {
869                                         $attribute->value = trim($attribute->value);
870                                         if (empty($attribute->value)) {
871                                                 continue;
872                                         }
873
874                                         $attr[$attribute->name] = $attribute->value;
875                                 }
876
877                                 if (empty($attr['name']) || empty($attr['content'])) {
878                                         continue;
879                                 }
880                         }
881
882                         if ($attr['name'] == 'description') {
883                                 $serverdata['info'] = $attr['content'];
884                         }
885
886                         if ($attr['name'] == 'application-name') {
887                                 $serverdata['platform'] = $attr['content'];
888                                 if (in_array($attr['content'], ['Misskey', 'Write.as'])) {
889                                         $serverdata['network'] = Protocol::ACTIVITYPUB;
890                                 }
891                         }
892
893                         if ($attr['name'] == 'generator') {
894                                 $serverdata['platform'] = $attr['content'];
895
896                                 $version_part = explode(' ', $attr['content']);
897
898                                 if (count($version_part) == 2) {
899                                         if (in_array($version_part[0], ['WordPress'])) {
900                                                 $serverdata['platform'] = $version_part[0];
901                                                 $serverdata['version'] = $version_part[1];
902                                                 $serverdata['network'] = Protocol::ACTIVITYPUB;
903                                         }
904                                         if (in_array($version_part[0], ['Friendika', 'Friendica'])) {
905                                                 $serverdata['platform'] = $version_part[0];
906                                                 $serverdata['version'] = $version_part[1];
907                                                 $serverdata['network'] = Protocol::DFRN;
908                                         }
909                                 }
910                         }
911                 }
912
913                 $list = $xpath->query('//meta[@property]');
914
915                 foreach ($list as $node) {
916                         $attr = [];
917                         if ($node->attributes->length) {
918                                 foreach ($node->attributes as $attribute) {
919                                         $attribute->value = trim($attribute->value);
920                                         if (empty($attribute->value)) {
921                                                 continue;
922                                         }
923
924                                         $attr[$attribute->name] = $attribute->value;
925                                 }
926
927                                 if (empty($attr['property']) || empty($attr['content'])) {
928                                         continue;
929                                 }
930                         }
931
932                         if ($attr['property'] == 'og:site_name') {
933                                 $serverdata['site_name'] = $attr['content'];
934                         }
935
936                         if ($attr['property'] == 'og:description') {
937                                 $serverdata['info'] = $attr['content'];
938                         }
939
940                         if ($attr['property'] == 'og:platform') {
941                                 $serverdata['platform'] = $attr['content'];
942
943                                 if (in_array($attr['content'], ['PeerTube'])) {
944                                         $serverdata['network'] = Protocol::ACTIVITYPUB;
945                                 }
946                         }
947
948                         if ($attr['property'] == 'generator') {
949                                 $serverdata['platform'] = $attr['content'];
950
951                                 if (in_array($attr['content'], ['hubzilla'])) {
952                                         // We later check which compatible protocol modules are loaded.
953                                         $serverdata['network'] = Protocol::ZOT;
954                                 }
955                         }
956                 }
957
958                 return $serverdata;
959         }
960
961         private static function analyseRootHeader($curlResult, $serverdata)
962         {
963                 if ($curlResult->getHeader('server') == 'Mastodon') {
964                         $serverdata['platform'] = 'mastodon';
965                         $serverdata['network'] = $network = Protocol::ACTIVITYPUB;
966                 } elseif ($curlResult->inHeader('x-diaspora-version')) {
967                         $serverdata['platform'] = 'diaspora';
968                         $serverdata['network'] = $network = Protocol::DIASPORA;
969                         $serverdata['version'] = $curlResult->getHeader('x-diaspora-version');
970
971                 } elseif ($curlResult->inHeader('x-friendica-version')) {
972                         $serverdata['platform'] = 'friendica';
973                         $serverdata['network'] = $network = Protocol::DFRN;
974                         $serverdata['version'] = $curlResult->getHeader('x-friendica-version');
975                 }
976                 return $serverdata;
977         }
978 }