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