Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

859 lines
25 KiB

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