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.

1068 lines
32 KiB

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