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.

1055 lines
33KB

  1. <?php
  2. /**
  3. * @file src/Model/GlobalContact.php
  4. * @brief This file includes the GlobalContact class with directory related functions
  5. */
  6. namespace Friendica\Model;
  7. use Exception;
  8. use Friendica\Core\Config;
  9. use Friendica\Core\Logger;
  10. use Friendica\Core\Protocol;
  11. use Friendica\Core\System;
  12. use Friendica\Core\Worker;
  13. use Friendica\Database\DBA;
  14. use Friendica\Network\Probe;
  15. use Friendica\Protocol\PortableContact;
  16. use Friendica\Util\DateTimeFormat;
  17. use Friendica\Util\Network;
  18. use Friendica\Util\Strings;
  19. /**
  20. * @brief This class handles GlobalContact related functions
  21. */
  22. class GContact
  23. {
  24. /**
  25. * @brief Search global contact table by nick or name
  26. *
  27. * @param string $search Name or nick
  28. * @param string $mode Search mode (e.g. "community")
  29. *
  30. * @return array with search results
  31. */
  32. public static function searchByName($search, $mode = '')
  33. {
  34. if (empty($search)) {
  35. return [];
  36. }
  37. // check supported networks
  38. if (Config::get('system', 'diaspora_enabled')) {
  39. $diaspora = Protocol::DIASPORA;
  40. } else {
  41. $diaspora = Protocol::DFRN;
  42. }
  43. if (!Config::get('system', 'ostatus_disabled')) {
  44. $ostatus = Protocol::OSTATUS;
  45. } else {
  46. $ostatus = Protocol::DFRN;
  47. }
  48. // check if we search only communities or every contact
  49. if ($mode === "community") {
  50. $extra_sql = " AND `community`";
  51. } else {
  52. $extra_sql = "";
  53. }
  54. $search .= "%";
  55. $results = DBA::p("SELECT `nurl` FROM `gcontact`
  56. WHERE NOT `hide` AND `network` IN (?, ?, ?, ?) AND
  57. ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) AND
  58. (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql
  59. GROUP BY `nurl` ORDER BY `nurl` DESC LIMIT 1000",
  60. Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, $search, $search, $search
  61. );
  62. $gcontacts = [];
  63. while ($result = DBA::fetch($results)) {
  64. $urlparts = parse_url($result["nurl"]);
  65. // Ignore results that look strange.
  66. // For historic reasons the gcontact table does contain some garbage.
  67. if (!empty($urlparts['query']) || !empty($urlparts['fragment'])) {
  68. continue;
  69. }
  70. $gcontacts[] = Contact::getDetailsByURL($result["nurl"], local_user());
  71. }
  72. return $gcontacts;
  73. }
  74. /**
  75. * @brief Link the gcontact entry with user, contact and global contact
  76. *
  77. * @param integer $gcid Global contact ID
  78. * @param integer $uid User ID
  79. * @param integer $cid Contact ID
  80. * @param integer $zcid Global Contact ID
  81. * @return void
  82. */
  83. public static function link($gcid, $uid = 0, $cid = 0, $zcid = 0)
  84. {
  85. if ($gcid <= 0) {
  86. return;
  87. }
  88. $condition = ['cid' => $cid, 'uid' => $uid, 'gcid' => $gcid, 'zcid' => $zcid];
  89. DBA::update('glink', ['updated' => DateTimeFormat::utcNow()], $condition, true);
  90. }
  91. /**
  92. * @brief Sanitize the given gcontact data
  93. *
  94. * @param array $gcontact array with gcontact data
  95. * @throw Exception
  96. *
  97. * Generation:
  98. * 0: No definition
  99. * 1: Profiles on this server
  100. * 2: Contacts of profiles on this server
  101. * 3: Contacts of contacts of profiles on this server
  102. * 4: ...
  103. * @return array $gcontact
  104. */
  105. public static function sanitize($gcontact)
  106. {
  107. if ($gcontact['url'] == "") {
  108. throw new Exception('URL is empty');
  109. }
  110. $urlparts = parse_url($gcontact['url']);
  111. if (!isset($urlparts["scheme"])) {
  112. throw new Exception("This (".$gcontact['url'].") doesn't seem to be an url.");
  113. }
  114. if (in_array($urlparts["host"], ["twitter.com", "identi.ca"])) {
  115. throw new Exception('Contact from a non federated network ignored. ('.$gcontact['url'].')');
  116. }
  117. // Don't store the statusnet connector as network
  118. // We can't simply set this to Protocol::OSTATUS since the connector could have fetched posts from friendica as well
  119. if ($gcontact['network'] == Protocol::STATUSNET) {
  120. $gcontact['network'] = "";
  121. }
  122. // Assure that there are no parameter fragments in the profile url
  123. if (in_array($gcontact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
  124. $gcontact['url'] = self::cleanContactUrl($gcontact['url']);
  125. }
  126. $alternate = PortableContact::alternateOStatusUrl($gcontact['url']);
  127. // The global contacts should contain the original picture, not the cached one
  128. if (($gcontact['generation'] != 1) && stristr(Strings::normaliseLink($gcontact['photo']), Strings::normaliseLink(System::baseUrl()."/photo/"))) {
  129. $gcontact['photo'] = "";
  130. }
  131. if (!isset($gcontact['network'])) {
  132. $condition = ["`uid` = 0 AND `nurl` = ? AND `network` != '' AND `network` != ?",
  133. Strings::normaliseLink($gcontact['url']), Protocol::STATUSNET];
  134. $contact = DBA::selectFirst('contact', ['network'], $condition);
  135. if (DBA::isResult($contact)) {
  136. $gcontact['network'] = $contact["network"];
  137. }
  138. if (($gcontact['network'] == "") || ($gcontact['network'] == Protocol::OSTATUS)) {
  139. $condition = ["`uid` = 0 AND `alias` IN (?, ?) AND `network` != '' AND `network` != ?",
  140. $gcontact['url'], Strings::normaliseLink($gcontact['url']), Protocol::STATUSNET];
  141. $contact = DBA::selectFirst('contact', ['network'], $condition);
  142. if (DBA::isResult($contact)) {
  143. $gcontact['network'] = $contact["network"];
  144. }
  145. }
  146. }
  147. $gcontact['server_url'] = '';
  148. $gcontact['network'] = '';
  149. $fields = ['network', 'updated', 'server_url', 'url', 'addr'];
  150. $gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($gcontact['url'])]);
  151. if (DBA::isResult($gcnt)) {
  152. if (!isset($gcontact['network']) && ($gcnt["network"] != Protocol::STATUSNET)) {
  153. $gcontact['network'] = $gcnt["network"];
  154. }
  155. if ($gcontact['updated'] <= DBA::NULL_DATETIME) {
  156. $gcontact['updated'] = $gcnt["updated"];
  157. }
  158. if (!isset($gcontact['server_url']) && (Strings::normaliseLink($gcnt["server_url"]) != Strings::normaliseLink($gcnt["url"]))) {
  159. $gcontact['server_url'] = $gcnt["server_url"];
  160. }
  161. if (!isset($gcontact['addr'])) {
  162. $gcontact['addr'] = $gcnt["addr"];
  163. }
  164. }
  165. if ((!isset($gcontact['network']) || !isset($gcontact['name']) || !isset($gcontact['addr']) || !isset($gcontact['photo']) || !isset($gcontact['server_url']) || $alternate)
  166. && PortableContact::reachable($gcontact['url'], $gcontact['server_url'], $gcontact['network'], false)
  167. ) {
  168. $data = Probe::uri($gcontact['url']);
  169. if ($data["network"] == Protocol::PHANTOM) {
  170. throw new Exception('Probing for URL '.$gcontact['url'].' failed');
  171. }
  172. $orig_profile = $gcontact['url'];
  173. $gcontact["server_url"] = $data["baseurl"];
  174. $gcontact = array_merge($gcontact, $data);
  175. if ($alternate && ($gcontact['network'] == Protocol::OSTATUS)) {
  176. // Delete the old entry - if it exists
  177. if (DBA::exists('gcontact', ['nurl' => Strings::normaliseLink($orig_profile)])) {
  178. DBA::delete('gcontact', ['nurl' => Strings::normaliseLink($orig_profile)]);
  179. }
  180. }
  181. }
  182. if (!isset($gcontact['name']) || !isset($gcontact['photo'])) {
  183. throw new Exception('No name and photo for URL '.$gcontact['url']);
  184. }
  185. if (!in_array($gcontact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
  186. throw new Exception('No federated network ('.$gcontact['network'].') detected for URL '.$gcontact['url']);
  187. }
  188. if (!isset($gcontact['server_url'])) {
  189. // We check the server url to be sure that it is a real one
  190. $server_url = PortableContact::detectServer($gcontact['url']);
  191. // We are now sure that it is a correct URL. So we use it in the future
  192. if ($server_url != "") {
  193. $gcontact['server_url'] = $server_url;
  194. }
  195. }
  196. // The server URL doesn't seem to be valid, so we don't store it.
  197. if (!PortableContact::checkServer($gcontact['server_url'], $gcontact['network'])) {
  198. $gcontact['server_url'] = "";
  199. }
  200. return $gcontact;
  201. }
  202. /**
  203. * @param integer $uid id
  204. * @param integer $cid id
  205. * @return integer
  206. */
  207. public static function countCommonFriends($uid, $cid)
  208. {
  209. $r = q(
  210. "SELECT count(*) as `total`
  211. FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  212. WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
  213. ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR
  214. (`gcontact`.`updated` >= `gcontact`.`last_failure`))
  215. AND `gcontact`.`nurl` IN (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 and id != %d ) ",
  216. intval($cid),
  217. intval($uid),
  218. intval($uid),
  219. intval($cid)
  220. );
  221. // Logger::log("countCommonFriends: $uid $cid {$r[0]['total']}");
  222. if (DBA::isResult($r)) {
  223. return $r[0]['total'];
  224. }
  225. return 0;
  226. }
  227. /**
  228. * @param integer $uid id
  229. * @param integer $zcid zcid
  230. * @return integer
  231. */
  232. public static function countCommonFriendsZcid($uid, $zcid)
  233. {
  234. $r = q(
  235. "SELECT count(*) as `total`
  236. FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  237. where `glink`.`zcid` = %d
  238. and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 ) ",
  239. intval($zcid),
  240. intval($uid)
  241. );
  242. if (DBA::isResult($r)) {
  243. return $r[0]['total'];
  244. }
  245. return 0;
  246. }
  247. /**
  248. * @param integer $uid user
  249. * @param integer $cid cid
  250. * @param integer $start optional, default 0
  251. * @param integer $limit optional, default 9999
  252. * @param boolean $shuffle optional, default false
  253. * @return object
  254. */
  255. public static function commonFriends($uid, $cid, $start = 0, $limit = 9999, $shuffle = false)
  256. {
  257. if ($shuffle) {
  258. $sql_extra = " order by rand() ";
  259. } else {
  260. $sql_extra = " order by `gcontact`.`name` asc ";
  261. }
  262. $r = q(
  263. "SELECT `gcontact`.*, `contact`.`id` AS `cid`
  264. FROM `glink`
  265. INNER JOIN `gcontact` ON `glink`.`gcid` = `gcontact`.`id`
  266. INNER JOIN `contact` ON `gcontact`.`nurl` = `contact`.`nurl`
  267. WHERE `glink`.`cid` = %d and `glink`.`uid` = %d
  268. AND `contact`.`uid` = %d AND `contact`.`self` = 0 AND `contact`.`blocked` = 0
  269. AND `contact`.`hidden` = 0 AND `contact`.`id` != %d
  270. AND ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
  271. $sql_extra LIMIT %d, %d",
  272. intval($cid),
  273. intval($uid),
  274. intval($uid),
  275. intval($cid),
  276. intval($start),
  277. intval($limit)
  278. );
  279. /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
  280. return $r;
  281. }
  282. /**
  283. * @param integer $uid user
  284. * @param integer $zcid zcid
  285. * @param integer $start optional, default 0
  286. * @param integer $limit optional, default 9999
  287. * @param boolean $shuffle optional, default false
  288. * @return object
  289. */
  290. public static function commonFriendsZcid($uid, $zcid, $start = 0, $limit = 9999, $shuffle = false)
  291. {
  292. if ($shuffle) {
  293. $sql_extra = " order by rand() ";
  294. } else {
  295. $sql_extra = " order by `gcontact`.`name` asc ";
  296. }
  297. $r = q(
  298. "SELECT `gcontact`.*
  299. FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  300. where `glink`.`zcid` = %d
  301. and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 )
  302. $sql_extra limit %d, %d",
  303. intval($zcid),
  304. intval($uid),
  305. intval($start),
  306. intval($limit)
  307. );
  308. /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
  309. return $r;
  310. }
  311. /**
  312. * @param integer $uid user
  313. * @param integer $cid cid
  314. * @return integer
  315. */
  316. public static function countAllFriends($uid, $cid)
  317. {
  318. $r = q(
  319. "SELECT count(*) as `total`
  320. FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  321. where `glink`.`cid` = %d and `glink`.`uid` = %d AND
  322. ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))",
  323. intval($cid),
  324. intval($uid)
  325. );
  326. if (DBA::isResult($r)) {
  327. return $r[0]['total'];
  328. }
  329. return 0;
  330. }
  331. /**
  332. * @param integer $uid user
  333. * @param integer $cid cid
  334. * @param integer $start optional, default 0
  335. * @param integer $limit optional, default 80
  336. * @return array
  337. */
  338. public static function allFriends($uid, $cid, $start = 0, $limit = 80)
  339. {
  340. $r = q(
  341. "SELECT `gcontact`.*, `contact`.`id` AS `cid`
  342. FROM `glink`
  343. INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
  344. LEFT JOIN `contact` ON `contact`.`nurl` = `gcontact`.`nurl` AND `contact`.`uid` = %d
  345. WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
  346. ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
  347. ORDER BY `gcontact`.`name` ASC LIMIT %d, %d ",
  348. intval($uid),
  349. intval($cid),
  350. intval($uid),
  351. intval($start),
  352. intval($limit)
  353. );
  354. /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
  355. return $r;
  356. }
  357. /**
  358. * @param object $uid user
  359. * @param integer $start optional, default 0
  360. * @param integer $limit optional, default 80
  361. * @return array
  362. */
  363. public static function suggestionQuery($uid, $start = 0, $limit = 80)
  364. {
  365. if (!$uid) {
  366. return [];
  367. }
  368. /*
  369. * Uncommented because the result of the queries are to big to store it in the cache.
  370. * We need to decide if we want to change the db column type or if we want to delete it.
  371. */
  372. //$list = Cache::get("suggestion_query:".$uid.":".$start.":".$limit);
  373. //if (!is_null($list)) {
  374. // return $list;
  375. //}
  376. $network = [Protocol::DFRN, Protocol::ACTIVITYPUB];
  377. if (Config::get('system', 'diaspora_enabled')) {
  378. $network[] = Protocol::DIASPORA;
  379. }
  380. if (!Config::get('system', 'ostatus_disabled')) {
  381. $network[] = Protocol::OSTATUS;
  382. }
  383. $sql_network = implode("', '", $network);
  384. $sql_network = "'".$sql_network."'";
  385. /// @todo This query is really slow
  386. // By now we cache the data for five minutes
  387. $r = q(
  388. "SELECT count(glink.gcid) as `total`, gcontact.* from gcontact
  389. INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
  390. where uid = %d and not gcontact.nurl in ( select nurl from contact where uid = %d )
  391. AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
  392. AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
  393. AND `gcontact`.`updated` >= '%s' AND NOT `gcontact`.`hide`
  394. AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
  395. AND `gcontact`.`network` IN (%s)
  396. GROUP BY `glink`.`gcid` ORDER BY `gcontact`.`updated` DESC,`total` DESC LIMIT %d, %d",
  397. intval($uid),
  398. intval($uid),
  399. intval($uid),
  400. intval($uid),
  401. DBA::NULL_DATETIME,
  402. $sql_network,
  403. intval($start),
  404. intval($limit)
  405. );
  406. if (DBA::isResult($r) && count($r) >= ($limit -1)) {
  407. /*
  408. * Uncommented because the result of the queries are to big to store it in the cache.
  409. * We need to decide if we want to change the db column type or if we want to delete it.
  410. */
  411. //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $r, Cache::FIVE_MINUTES);
  412. return $r;
  413. }
  414. $r2 = q(
  415. "SELECT gcontact.* FROM gcontact
  416. INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
  417. WHERE `glink`.`uid` = 0 AND `glink`.`cid` = 0 AND `glink`.`zcid` = 0 AND NOT `gcontact`.`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = %d)
  418. AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
  419. AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
  420. AND `gcontact`.`updated` >= '%s'
  421. AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
  422. AND `gcontact`.`network` IN (%s)
  423. ORDER BY rand() LIMIT %d, %d",
  424. intval($uid),
  425. intval($uid),
  426. intval($uid),
  427. DBA::NULL_DATETIME,
  428. $sql_network,
  429. intval($start),
  430. intval($limit)
  431. );
  432. $list = [];
  433. foreach ($r2 as $suggestion) {
  434. $list[$suggestion["nurl"]] = $suggestion;
  435. }
  436. foreach ($r as $suggestion) {
  437. $list[$suggestion["nurl"]] = $suggestion;
  438. }
  439. while (sizeof($list) > ($limit)) {
  440. array_pop($list);
  441. }
  442. /*
  443. * Uncommented because the result of the queries are to big to store it in the cache.
  444. * We need to decide if we want to change the db column type or if we want to delete it.
  445. */
  446. //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $list, Cache::FIVE_MINUTES);
  447. return $list;
  448. }
  449. /**
  450. * @return void
  451. */
  452. public static function updateSuggestions()
  453. {
  454. $a = get_app();
  455. $done = [];
  456. /// @TODO Check if it is really neccessary to poll the own server
  457. PortableContact::loadWorker(0, 0, 0, System::baseUrl() . '/poco');
  458. $done[] = System::baseUrl() . '/poco';
  459. if (strlen(Config::get('system', 'directory'))) {
  460. $x = Network::fetchUrl(get_server()."/pubsites");
  461. if (!empty($x)) {
  462. $j = json_decode($x);
  463. if (!empty($j->entries)) {
  464. foreach ($j->entries as $entry) {
  465. PortableContact::checkServer($entry->url);
  466. $url = $entry->url . '/poco';
  467. if (!in_array($url, $done)) {
  468. PortableContact::loadWorker(0, 0, 0, $url);
  469. $done[] = $url;
  470. }
  471. }
  472. }
  473. }
  474. }
  475. // Query your contacts from Friendica and Redmatrix/Hubzilla for their contacts
  476. $r = q(
  477. "SELECT DISTINCT(`poco`) AS `poco` FROM `contact` WHERE `network` IN ('%s', '%s')",
  478. DBA::escape(Protocol::DFRN),
  479. DBA::escape(Protocol::DIASPORA)
  480. );
  481. if (DBA::isResult($r)) {
  482. foreach ($r as $rr) {
  483. $base = substr($rr['poco'], 0, strrpos($rr['poco'], '/'));
  484. if (! in_array($base, $done)) {
  485. PortableContact::loadWorker(0, 0, 0, $base);
  486. }
  487. }
  488. }
  489. }
  490. /**
  491. * @brief Removes unwanted parts from a contact url
  492. *
  493. * @param string $url Contact url
  494. *
  495. * @return string Contact url with the wanted parts
  496. */
  497. public static function cleanContactUrl($url)
  498. {
  499. $parts = parse_url($url);
  500. if (!isset($parts["scheme"]) || !isset($parts["host"])) {
  501. return $url;
  502. }
  503. $new_url = $parts["scheme"]."://".$parts["host"];
  504. if (isset($parts["port"])) {
  505. $new_url .= ":".$parts["port"];
  506. }
  507. if (isset($parts["path"])) {
  508. $new_url .= $parts["path"];
  509. }
  510. if ($new_url != $url) {
  511. Logger::log("Cleaned contact url ".$url." to ".$new_url." - Called by: ".System::callstack(), Logger::DEBUG);
  512. }
  513. return $new_url;
  514. }
  515. /**
  516. * @brief Replace alternate OStatus user format with the primary one
  517. *
  518. * @param array $contact contact array (called by reference)
  519. * @return void
  520. */
  521. public static function fixAlternateContactAddress(&$contact)
  522. {
  523. if (($contact["network"] == Protocol::OSTATUS) && PortableContact::alternateOStatusUrl($contact["url"])) {
  524. $data = Probe::uri($contact["url"]);
  525. if ($contact["network"] == Protocol::OSTATUS) {
  526. Logger::log("Fix primary url from ".$contact["url"]." to ".$data["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
  527. $contact["url"] = $data["url"];
  528. $contact["addr"] = $data["addr"];
  529. $contact["alias"] = $data["alias"];
  530. $contact["server_url"] = $data["baseurl"];
  531. }
  532. }
  533. }
  534. /**
  535. * @brief Fetch the gcontact id, add an entry if not existed
  536. *
  537. * @param array $contact contact array
  538. *
  539. * @return bool|int Returns false if not found, integer if contact was found
  540. */
  541. public static function getId($contact)
  542. {
  543. $gcontact_id = 0;
  544. $doprobing = false;
  545. $last_failure_str = '';
  546. $last_contact_str = '';
  547. if (empty($contact["network"])) {
  548. Logger::log("Empty network for contact url ".$contact["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
  549. return false;
  550. }
  551. if (in_array($contact["network"], [Protocol::PHANTOM])) {
  552. Logger::log("Invalid network for contact url ".$contact["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
  553. return false;
  554. }
  555. if ($contact["network"] == Protocol::STATUSNET) {
  556. $contact["network"] = Protocol::OSTATUS;
  557. }
  558. // All new contacts are hidden by default
  559. if (!isset($contact["hide"])) {
  560. $contact["hide"] = true;
  561. }
  562. // Replace alternate OStatus user format with the primary one
  563. self::fixAlternateContactAddress($contact);
  564. // Remove unwanted parts from the contact url (e.g. "?zrl=...")
  565. if (in_array($contact["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
  566. $contact["url"] = self::cleanContactUrl($contact["url"]);
  567. }
  568. DBA::lock('gcontact');
  569. $fields = ['id', 'last_contact', 'last_failure', 'network'];
  570. $gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($contact["url"])]);
  571. if (DBA::isResult($gcnt)) {
  572. $gcontact_id = $gcnt["id"];
  573. // Update every 90 days
  574. if (in_array($gcnt["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
  575. $last_failure_str = $gcnt["last_failure"];
  576. $last_failure = strtotime($gcnt["last_failure"]);
  577. $last_contact_str = $gcnt["last_contact"];
  578. $last_contact = strtotime($gcnt["last_contact"]);
  579. $doprobing = (((time() - $last_contact) > (90 * 86400)) && ((time() - $last_failure) > (90 * 86400)));
  580. }
  581. } else {
  582. $contact['location'] = defaults($contact, 'location', '');
  583. $contact['about'] = defaults($contact, 'about', '');
  584. $contact['generation'] = defaults($contact, 'generation', 0);
  585. q(
  586. "INSERT INTO `gcontact` (`name`, `nick`, `addr` , `network`, `url`, `nurl`, `photo`, `created`, `updated`, `location`, `about`, `hide`, `generation`)
  587. VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d)",
  588. DBA::escape($contact["name"]),
  589. DBA::escape($contact["nick"]),
  590. DBA::escape($contact["addr"]),
  591. DBA::escape($contact["network"]),
  592. DBA::escape($contact["url"]),
  593. DBA::escape(Strings::normaliseLink($contact["url"])),
  594. DBA::escape($contact["photo"]),
  595. DBA::escape(DateTimeFormat::utcNow()),
  596. DBA::escape(DateTimeFormat::utcNow()),
  597. DBA::escape($contact["location"]),
  598. DBA::escape($contact["about"]),
  599. intval($contact["hide"]),
  600. intval($contact["generation"])
  601. );
  602. $condition = ['nurl' => Strings::normaliseLink($contact["url"])];
  603. $cnt = DBA::selectFirst('gcontact', ['id', 'network'], $condition, ['order' => ['id']]);
  604. if (DBA::isResult($cnt)) {
  605. $gcontact_id = $cnt["id"];
  606. $doprobing = in_array($cnt["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""]);
  607. }
  608. }
  609. DBA::unlock();
  610. if ($doprobing) {
  611. Logger::log("Last Contact: ". $last_contact_str." - Last Failure: ".$last_failure_str." - Checking: ".$contact["url"], Logger::DEBUG);
  612. Worker::add(PRIORITY_LOW, 'GProbe', $contact["url"]);
  613. }
  614. return $gcontact_id;
  615. }
  616. /**
  617. * @brief Updates the gcontact table from a given array
  618. *
  619. * @param array $contact contact array
  620. *
  621. * @return bool|int Returns false if not found, integer if contact was found
  622. */
  623. public static function update($contact)
  624. {
  625. // Check for invalid "contact-type" value
  626. if (isset($contact['contact-type']) && (intval($contact['contact-type']) < 0)) {
  627. $contact['contact-type'] = 0;
  628. }
  629. /// @todo update contact table as well
  630. $gcontact_id = self::getId($contact);
  631. if (!$gcontact_id) {
  632. return false;
  633. }
  634. $public_contact = q(
  635. "SELECT `name`, `nick`, `photo`, `location`, `about`, `addr`, `generation`, `birthday`, `gender`, `keywords`,
  636. `contact-type`, `hide`, `nsfw`, `network`, `alias`, `notify`, `server_url`, `connect`, `updated`, `url`
  637. FROM `gcontact` WHERE `id` = %d LIMIT 1",
  638. intval($gcontact_id)
  639. );
  640. // Get all field names
  641. $fields = [];
  642. foreach ($public_contact[0] as $field => $data) {
  643. $fields[$field] = $data;
  644. }
  645. unset($fields["url"]);
  646. unset($fields["updated"]);
  647. unset($fields["hide"]);
  648. // Bugfix: We had an error in the storing of keywords which lead to the "0"
  649. // This value is still transmitted via poco.
  650. if (!empty($contact["keywords"]) && ($contact["keywords"] == "0")) {
  651. unset($contact["keywords"]);
  652. }
  653. if (!empty($public_contact[0]["keywords"]) && ($public_contact[0]["keywords"] == "0")) {
  654. $public_contact[0]["keywords"] = "";
  655. }
  656. // assign all unassigned fields from the database entry
  657. foreach ($fields as $field => $data) {
  658. if (!isset($contact[$field]) || ($contact[$field] == "")) {
  659. $contact[$field] = $public_contact[0][$field];
  660. }
  661. }
  662. if (!isset($contact["hide"])) {
  663. $contact["hide"] = $public_contact[0]["hide"];
  664. }
  665. $fields["hide"] = $public_contact[0]["hide"];
  666. if ($contact["network"] == Protocol::STATUSNET) {
  667. $contact["network"] = Protocol::OSTATUS;
  668. }
  669. // Replace alternate OStatus user format with the primary one
  670. self::fixAlternateContactAddress($contact);
  671. if (!isset($contact["updated"])) {
  672. $contact["updated"] = DateTimeFormat::utcNow();
  673. }
  674. if ($contact["network"] == Protocol::TWITTER) {
  675. $contact["server_url"] = 'http://twitter.com';
  676. }
  677. if ($contact["server_url"] == "") {
  678. $data = Probe::uri($contact["url"]);
  679. if ($data["network"] != Protocol::PHANTOM) {
  680. $contact["server_url"] = $data['baseurl'];
  681. }
  682. } else {
  683. $contact["server_url"] = Strings::normaliseLink($contact["server_url"]);
  684. }
  685. if (($contact["addr"] == "") && ($contact["server_url"] != "") && ($contact["nick"] != "")) {
  686. $hostname = str_replace("http://", "", $contact["server_url"]);
  687. $contact["addr"] = $contact["nick"]."@".$hostname;
  688. }
  689. // Check if any field changed
  690. $update = false;
  691. unset($fields["generation"]);
  692. if ((($contact["generation"] > 0) && ($contact["generation"] <= $public_contact[0]["generation"])) || ($public_contact[0]["generation"] == 0)) {
  693. foreach ($fields as $field => $data) {
  694. if ($contact[$field] != $public_contact[0][$field]) {
  695. Logger::log("Difference for contact ".$contact["url"]." in field '".$field."'. New value: '".$contact[$field]."', old value '".$public_contact[0][$field]."'", Logger::DEBUG);
  696. $update = true;
  697. }
  698. }
  699. if ($contact["generation"] < $public_contact[0]["generation"]) {
  700. Logger::log("Difference for contact ".$contact["url"]." in field 'generation'. new value: '".$contact["generation"]."', old value '".$public_contact[0]["generation"]."'", Logger::DEBUG);
  701. $update = true;
  702. }
  703. }
  704. if ($update) {
  705. Logger::log("Update gcontact for ".$contact["url"], Logger::DEBUG);
  706. $condition = ['`nurl` = ? AND (`generation` = 0 OR `generation` >= ?)',
  707. Strings::normaliseLink($contact["url"]), $contact["generation"]];
  708. $contact["updated"] = DateTimeFormat::utc($contact["updated"]);
  709. $updated = ['photo' => $contact['photo'], 'name' => $contact['name'],
  710. 'nick' => $contact['nick'], 'addr' => $contact['addr'],
  711. 'network' => $contact['network'], 'birthday' => $contact['birthday'],
  712. 'gender' => $contact['gender'], 'keywords' => $contact['keywords'],
  713. 'hide' => $contact['hide'], 'nsfw' => $contact['nsfw'],
  714. 'contact-type' => $contact['contact-type'], 'alias' => $contact['alias'],
  715. 'notify' => $contact['notify'], 'url' => $contact['url'],
  716. 'location' => $contact['location'], 'about' => $contact['about'],
  717. 'generation' => $contact['generation'], 'updated' => $contact['updated'],
  718. 'server_url' => $contact['server_url'], 'connect' => $contact['connect']];
  719. DBA::update('gcontact', $updated, $condition, $fields);
  720. // Now update the contact entry with the user id "0" as well.
  721. // This is used for the shadow copies of public items.
  722. /// @todo Check if we really should do this.
  723. // The quality of the gcontact table is mostly lower than the public contact
  724. $public_contact = DBA::selectFirst('contact', ['id'], ['nurl' => Strings::normaliseLink($contact["url"]), 'uid' => 0]);
  725. if (DBA::isResult($public_contact)) {
  726. Logger::log("Update public contact ".$public_contact["id"], Logger::DEBUG);
  727. Contact::updateAvatar($contact["photo"], 0, $public_contact["id"]);
  728. $fields = ['name', 'nick', 'addr',
  729. 'network', 'bd', 'gender',
  730. 'keywords', 'alias', 'contact-type',
  731. 'url', 'location', 'about'];
  732. $old_contact = DBA::selectFirst('contact', $fields, ['id' => $public_contact["id"]]);
  733. // Update it with the current values
  734. $fields = ['name' => $contact['name'], 'nick' => $contact['nick'],
  735. 'addr' => $contact['addr'], 'network' => $contact['network'],
  736. 'bd' => $contact['birthday'], 'gender' => $contact['gender'],
  737. 'keywords' => $contact['keywords'], 'alias' => $contact['alias'],
  738. 'contact-type' => $contact['contact-type'], 'url' => $contact['url'],
  739. 'location' => $contact['location'], 'about' => $contact['about']];
  740. // Don't update the birthday field if not set or invalid
  741. if (empty($contact['birthday']) || ($contact['birthday'] <= DBA::NULL_DATE)) {
  742. unset($fields['bd']);
  743. }
  744. DBA::update('contact', $fields, ['id' => $public_contact["id"]], $old_contact);
  745. }
  746. }
  747. return $gcontact_id;
  748. }
  749. /**
  750. * @brief Updates the gcontact entry from probe
  751. *
  752. * @param string $url profile link
  753. * @return void
  754. */
  755. public static function updateFromProbe($url)
  756. {
  757. $data = Probe::uri($url);
  758. if (in_array($data["network"], [Protocol::PHANTOM])) {
  759. Logger::log("Invalid network for contact url ".$data["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
  760. return;
  761. }
  762. $data["server_url"] = $data["baseurl"];
  763. self::update($data);
  764. }
  765. /**
  766. * @brief Update the gcontact entry for a given user id
  767. *
  768. * @param int $uid User ID
  769. * @return void
  770. */
  771. public static function updateForUser($uid)
  772. {
  773. $r = q(
  774. "SELECT `profile`.`locality`, `profile`.`region`, `profile`.`country-name`,
  775. `profile`.`name`, `profile`.`about`, `profile`.`gender`,
  776. `profile`.`pub_keywords`, `profile`.`dob`, `profile`.`photo`,
  777. `profile`.`net-publish`, `user`.`nickname`, `user`.`hidewall`,
  778. `contact`.`notify`, `contact`.`url`, `contact`.`addr`
  779. FROM `profile`
  780. INNER JOIN `user` ON `user`.`uid` = `profile`.`uid`
  781. INNER JOIN `contact` ON `contact`.`uid` = `profile`.`uid`
  782. WHERE `profile`.`uid` = %d AND `profile`.`is-default` AND `contact`.`self`",
  783. intval($uid)
  784. );
  785. if (!DBA::isResult($r)) {
  786. Logger::log('Cannot find user with uid=' . $uid, Logger::INFO);
  787. return false;
  788. }
  789. $location = Profile::formatLocation(
  790. ["locality" => $r[0]["locality"], "region" => $r[0]["region"], "country-name" => $r[0]["country-name"]]
  791. );
  792. // The "addr" field was added in 3.4.3 so it can be empty for older users
  793. if ($r[0]["addr"] != "") {
  794. $addr = $r[0]["nickname"].'@'.str_replace(["http://", "https://"], "", System::baseUrl());
  795. } else {
  796. $addr = $r[0]["addr"];
  797. }
  798. $gcontact = ["name" => $r[0]["name"], "location" => $location, "about" => $r[0]["about"],
  799. "gender" => $r[0]["gender"], "keywords" => $r[0]["pub_keywords"],
  800. "birthday" => $r[0]["dob"], "photo" => $r[0]["photo"],
  801. "notify" => $r[0]["notify"], "url" => $r[0]["url"],
  802. "hide" => ($r[0]["hidewall"] || !$r[0]["net-publish"]),
  803. "nick" => $r[0]["nickname"], "addr" => $addr,
  804. "connect" => $addr, "server_url" => System::baseUrl(),
  805. "generation" => 1, "network" => Protocol::DFRN];
  806. self::update($gcontact);
  807. }
  808. /**
  809. * @brief Fetches users of given GNU Social server
  810. *
  811. * If the "Statistics" addon is enabled (See http://gstools.org/ for details) we query user data with this.
  812. *
  813. * @param string $server Server address
  814. * @return void
  815. */
  816. public static function fetchGsUsers($server)
  817. {
  818. Logger::log("Fetching users from GNU Social server ".$server, Logger::DEBUG);
  819. $url = $server."/main/statistics";
  820. $curlResult = Network::curl($url);
  821. if (!$curlResult->isSuccess()) {
  822. return false;
  823. }
  824. $statistics = json_decode($curlResult->getBody());
  825. if (!empty($statistics->config->instance_address)) {
  826. if (!empty($statistics->config->instance_with_ssl)) {
  827. $server = "https://";
  828. } else {
  829. $server = "http://";
  830. }
  831. $server .= $statistics->config->instance_address;
  832. $hostname = $statistics->config->instance_address;
  833. } elseif (!empty($statistics->instance_address)) {
  834. if (!empty($statistics->instance_with_ssl)) {
  835. $server = "https://";
  836. } else {
  837. $server = "http://";
  838. }
  839. $server .= $statistics->instance_address;
  840. $hostname = $statistics->instance_address;
  841. }
  842. if (!empty($statistics->users)) {
  843. foreach ($statistics->users as $nick => $user) {
  844. $profile_url = $server."/".$user->nickname;
  845. $contact = ["url" => $profile_url,
  846. "name" => $user->fullname,
  847. "addr" => $user->nickname."@".$hostname,
  848. "nick" => $user->nickname,
  849. "network" => Protocol::OSTATUS,
  850. "photo" => System::baseUrl()."/images/person-300.jpg"];
  851. if (isset($user->bio)) {
  852. $contact["about"] = $user->bio;
  853. }
  854. self::getId($contact);
  855. }
  856. }
  857. }
  858. /**
  859. * @brief Asking GNU Social server on a regular base for their user data
  860. * @return void
  861. */
  862. public static function discoverGsUsers()
  863. {
  864. $requery_days = intval(Config::get("system", "poco_requery_days"));
  865. $last_update = date("c", time() - (60 * 60 * 24 * $requery_days));
  866. $r = q(
  867. "SELECT `nurl`, `url` FROM `gserver` WHERE `last_contact` >= `last_failure` AND `network` = '%s' AND `last_poco_query` < '%s' ORDER BY RAND() LIMIT 5",
  868. DBA::escape(Protocol::OSTATUS),
  869. DBA::escape($last_update)
  870. );
  871. if (!DBA::isResult($r)) {
  872. return;
  873. }
  874. foreach ($r as $server) {
  875. self::fetchGsUsers($server["url"]);
  876. q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", DBA::escape(DateTimeFormat::utcNow()), DBA::escape($server["nurl"]));
  877. }
  878. }
  879. /**
  880. * @return string
  881. */
  882. public static function getRandomUrl()
  883. {
  884. $r = q(
  885. "SELECT `url` FROM `gcontact` WHERE `network` = '%s'
  886. AND `last_contact` >= `last_failure`
  887. AND `updated` > UTC_TIMESTAMP - INTERVAL 1 MONTH
  888. ORDER BY rand() LIMIT 1",
  889. DBA::escape(Protocol::DFRN)
  890. );
  891. if (DBA::isResult($r)) {
  892. return dirname($r[0]['url']);
  893. }
  894. return '';
  895. }
  896. }