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