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