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