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.

1010 lines
32 KiB

4 years ago
  1. <?php
  2. /**
  3. * @file src/Model/Contact.php
  4. */
  5. namespace Friendica\Model;
  6. use Friendica\BaseObject;
  7. use Friendica\Core\PConfig;
  8. use Friendica\Core\System;
  9. use Friendica\Core\Worker;
  10. use Friendica\Database\DBM;
  11. use Friendica\Network\Probe;
  12. use Friendica\Model\Photo;
  13. use Friendica\Protocol\Diaspora;
  14. use Friendica\Protocol\DFRN;
  15. use Friendica\Protocol\OStatus;
  16. use Friendica\Protocol\Salmon;
  17. use dba;
  18. require_once 'boot.php';
  19. require_once 'include/dba.php';
  20. require_once 'include/text.php';
  21. /**
  22. * @brief functions for interacting with a contact
  23. */
  24. class Contact extends BaseObject
  25. {
  26. /**
  27. * @brief Returns a list of contacts belonging in a group
  28. *
  29. * @param int $gid
  30. * @return array
  31. */
  32. public static function getByGroupId($gid)
  33. {
  34. $return = [];
  35. if (intval($gid)) {
  36. $stmt = dba::p('SELECT `group_member`.`contact-id`, `contact`.*
  37. FROM `contact`
  38. INNER JOIN `group_member`
  39. ON `contact`.`id` = `group_member`.`contact-id`
  40. WHERE `gid` = ?
  41. AND `contact`.`uid` = ?
  42. AND NOT `contact`.`self`
  43. AND NOT `contact`.`blocked`
  44. AND NOT `contact`.`pending`
  45. ORDER BY `contact`.`name` ASC',
  46. $gid,
  47. local_user()
  48. );
  49. if (DBM::is_result($stmt)) {
  50. $return = dba::inArray($stmt);
  51. }
  52. }
  53. return $return;
  54. }
  55. /**
  56. * @brief Returns the count of OStatus contacts in a group
  57. *
  58. * @param int $gid
  59. * @return int
  60. */
  61. public static function getOStatusCountByGroupId($gid)
  62. {
  63. $return = 0;
  64. if (intval($gid)) {
  65. $contacts = dba::fetch_first('SELECT COUNT(*) AS `count`
  66. FROM `contact`
  67. INNER JOIN `group_member`
  68. ON `contact`.`id` = `group_member`.`contact-id`
  69. WHERE `gid` = ?
  70. AND `contact`.`uid` = ?
  71. AND `contact`.`network` = ?
  72. AND `contact`.`notify` != ""',
  73. $gid,
  74. local_user(),
  75. NETWORK_OSTATUS
  76. );
  77. $return = $contacts['count'];
  78. }
  79. return $return;
  80. }
  81. /**
  82. * Creates the self-contact for the provided user id
  83. *
  84. * @param int $uid
  85. * @return bool Operation success
  86. */
  87. public static function createSelfFromUserId($uid)
  88. {
  89. // Only create the entry if it doesn't exist yet
  90. if (dba::exists('contact', ['uid' => $uid, 'self' => true])) {
  91. return true;
  92. }
  93. $user = dba::select('user', ['uid', 'username', 'nickname'], ['uid' => $uid], ['limit' => 1]);
  94. if (!DBM::is_result($user)) {
  95. return false;
  96. }
  97. $return = dba::insert('contact', [
  98. 'uid' => $user['uid'],
  99. 'created' => datetime_convert(),
  100. 'self' => 1,
  101. 'name' => $user['username'],
  102. 'nick' => $user['nickname'],
  103. 'photo' => System::baseUrl() . '/photo/profile/' . $user['uid'] . '.jpg',
  104. 'thumb' => System::baseUrl() . '/photo/avatar/' . $user['uid'] . '.jpg',
  105. 'micro' => System::baseUrl() . '/photo/micro/' . $user['uid'] . '.jpg',
  106. 'blocked' => 0,
  107. 'pending' => 0,
  108. 'url' => System::baseUrl() . '/profile/' . $user['nickname'],
  109. 'nurl' => normalise_link(System::baseUrl() . '/profile/' . $user['nickname']),
  110. 'addr' => $user['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3),
  111. 'request' => System::baseUrl() . '/dfrn_request/' . $user['nickname'],
  112. 'notify' => System::baseUrl() . '/dfrn_notify/' . $user['nickname'],
  113. 'poll' => System::baseUrl() . '/dfrn_poll/' . $user['nickname'],
  114. 'confirm' => System::baseUrl() . '/dfrn_confirm/' . $user['nickname'],
  115. 'poco' => System::baseUrl() . '/poco/' . $user['nickname'],
  116. 'name-date' => datetime_convert(),
  117. 'uri-date' => datetime_convert(),
  118. 'avatar-date' => datetime_convert(),
  119. 'closeness' => 0
  120. ]);
  121. return $return;
  122. }
  123. /**
  124. * @brief Marks a contact for removal
  125. *
  126. * @param int $id contact id
  127. * @return null
  128. */
  129. public static function remove($id)
  130. {
  131. // We want just to make sure that we don't delete our "self" contact
  132. $r = dba::select('contact', array('uid'), array('id' => $id, 'self' => false), array('limit' => 1));
  133. if (!DBM::is_result($r) || !intval($r['uid'])) {
  134. return;
  135. }
  136. $archive = PConfig::get($r['uid'], 'system', 'archive_removed_contacts');
  137. if ($archive) {
  138. dba::update('contact', array('archive' => true, 'network' => 'none', 'writable' => false), array('id' => $id));
  139. return;
  140. }
  141. dba::delete('contact', array('id' => $id));
  142. // Delete the rest in the background
  143. Worker::add(PRIORITY_LOW, 'RemoveContact', $id);
  144. }
  145. /**
  146. * @brief Sends an unfriend message. Does not remove the contact
  147. *
  148. * @param array $user User unfriending
  149. * @param array $contact Contact unfriended
  150. * @return void
  151. */
  152. public static function terminateFriendship(array $user, array $contact)
  153. {
  154. if ($contact['network'] === NETWORK_OSTATUS) {
  155. // create an unfollow slap
  156. $item = array();
  157. $item['verb'] = NAMESPACE_OSTATUS . "/unfollow";
  158. $item['follow'] = $contact["url"];
  159. $slap = OStatus::salmon($item, $user);
  160. if ((x($contact, 'notify')) && (strlen($contact['notify']))) {
  161. Salmon::slapper($user, $contact['notify'], $slap);
  162. }
  163. } elseif ($contact['network'] === NETWORK_DIASPORA) {
  164. Diaspora::sendUnshare($user, $contact);
  165. } elseif ($contact['network'] === NETWORK_DFRN) {
  166. DFRN::deliver($user, $contact, 'placeholder', 1);
  167. }
  168. }
  169. /**
  170. * @brief Marks a contact for archival after a communication issue delay
  171. *
  172. * Contact has refused to recognise us as a friend. We will start a countdown.
  173. * If they still don't recognise us in 32 days, the relationship is over,
  174. * and we won't waste any more time trying to communicate with them.
  175. * This provides for the possibility that their database is temporarily messed
  176. * up or some other transient event and that there's a possibility we could recover from it.
  177. *
  178. * @param array $contact contact to mark for archival
  179. * @return null
  180. */
  181. public static function markForArchival(array $contact)
  182. {
  183. // Contact already archived or "self" contact? => nothing to do
  184. if ($contact['archive'] || $contact['self']) {
  185. return;
  186. }
  187. if ($contact['term-date'] <= NULL_DATE) {
  188. dba::update('contact', array('term-date' => datetime_convert()), array('id' => $contact['id']));
  189. if ($contact['url'] != '') {
  190. dba::update('contact', array('term-date' => datetime_convert()), array('`nurl` = ? AND `term-date` <= ? AND NOT `self`', normalise_link($contact['url']), NULL_DATE));
  191. }
  192. } else {
  193. /* @todo
  194. * We really should send a notification to the owner after 2-3 weeks
  195. * so they won't be surprised when the contact vanishes and can take
  196. * remedial action if this was a serious mistake or glitch
  197. */
  198. /// @todo Check for contact vitality via probing
  199. $expiry = $contact['term-date'] . ' + 32 days ';
  200. if (datetime_convert() > datetime_convert('UTC', 'UTC', $expiry)) {
  201. /* Relationship is really truly dead. archive them rather than
  202. * delete, though if the owner tries to unarchive them we'll start
  203. * the whole process over again.
  204. */
  205. dba::update('contact', array('archive' => 1), array('id' => $contact['id']));
  206. if ($contact['url'] != '') {
  207. dba::update('contact', array('archive' => 1), array('nurl' => normalise_link($contact['url']), 'self' => false));
  208. }
  209. }
  210. }
  211. }
  212. /**
  213. * @brief Cancels the archival countdown
  214. *
  215. * @see Contact::markForArchival()
  216. *
  217. * @param array $contact contact to be unmarked for archival
  218. * @return null
  219. */
  220. public static function unmarkForArchival(array $contact)
  221. {
  222. $condition = array('`id` = ? AND (`term-date` > ? OR `archive`)', $contact['id'], NULL_DATE);
  223. $exists = dba::exists('contact', $condition);
  224. // We don't need to update, we never marked this contact for archival
  225. if (!$exists) {
  226. return;
  227. }
  228. // It's a miracle. Our dead contact has inexplicably come back to life.
  229. $fields = array('term-date' => NULL_DATE, 'archive' => false);
  230. dba::update('contact', $fields, array('id' => $contact['id']));
  231. if ($contact['url'] != '') {
  232. dba::update('contact', $fields, array('nurl' => normalise_link($contact['url'])));
  233. }
  234. }
  235. /**
  236. * @brief Get contact data for a given profile link
  237. *
  238. * The function looks at several places (contact table and gcontact table) for the contact
  239. * It caches its result for the same script execution to prevent duplicate calls
  240. *
  241. * @param string $url The profile link
  242. * @param int $uid User id
  243. * @param array $default If not data was found take this data as default value
  244. *
  245. * @return array Contact data
  246. */
  247. public static function getDetailsByURL($url, $uid = -1, array $default = [])
  248. {
  249. static $cache = array();
  250. if ($url == '') {
  251. return $default;
  252. }
  253. if ($uid == -1) {
  254. $uid = local_user();
  255. }
  256. if (isset($cache[$url][$uid])) {
  257. return $cache[$url][$uid];
  258. }
  259. $ssl_url = str_replace('http://', 'https://', $url);
  260. // Fetch contact data from the contact table for the given user
  261. $s = dba::p("SELECT `id`, `id` AS `cid`, 0 AS `gid`, 0 AS `zid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`,
  262. `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, `self`
  263. FROM `contact` WHERE `nurl` = ? AND `uid` = ?", normalise_link($url), $uid);
  264. $r = dba::inArray($s);
  265. // Fetch contact data from the contact table for the given user, checking with the alias
  266. if (!DBM::is_result($r)) {
  267. $s = dba::p("SELECT `id`, `id` AS `cid`, 0 AS `gid`, 0 AS `zid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`,
  268. `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, `self`
  269. FROM `contact` WHERE `alias` IN (?, ?, ?) AND `uid` = ?", normalise_link($url), $url, $ssl_url, $uid);
  270. $r = dba::inArray($s);
  271. }
  272. // Fetch the data from the contact table with "uid=0" (which is filled automatically)
  273. if (!DBM::is_result($r)) {
  274. $s = dba::p("SELECT `id`, 0 AS `cid`, `id` AS `zid`, 0 AS `gid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`,
  275. `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, 0 AS `self`
  276. FROM `contact` WHERE `nurl` = ? AND `uid` = 0", normalise_link($url));
  277. $r = dba::inArray($s);
  278. }
  279. // Fetch the data from the contact table with "uid=0" (which is filled automatically) - checked with the alias
  280. if (!DBM::is_result($r)) {
  281. $s = dba::p("SELECT `id`, 0 AS `cid`, `id` AS `zid`, 0 AS `gid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`,
  282. `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, 0 AS `self`
  283. FROM `contact` WHERE `alias` IN (?, ?, ?) AND `uid` = 0", normalise_link($url), $url, $ssl_url);
  284. $r = dba::inArray($s);
  285. }
  286. // Fetch the data from the gcontact table
  287. if (!DBM::is_result($r)) {
  288. $s = dba::p("SELECT 0 AS `id`, 0 AS `cid`, `id` AS `gid`, 0 AS `zid`, 0 AS `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, '' AS `xmpp`,
  289. `keywords`, `gender`, `photo`, `photo` AS `thumb`, `photo` AS `micro`, `community` AS `forum`, 0 AS `prv`, `community`, `contact-type`, `birthday`, 0 AS `self`
  290. FROM `gcontact` WHERE `nurl` = ?", normalise_link($url));
  291. $r = dba::inArray($s);
  292. }
  293. if (DBM::is_result($r)) {
  294. // If there is more than one entry we filter out the connector networks
  295. if (count($r) > 1) {
  296. foreach ($r as $id => $result) {
  297. if ($result["network"] == NETWORK_STATUSNET) {
  298. unset($r[$id]);
  299. }
  300. }
  301. }
  302. $profile = array_shift($r);
  303. // "bd" always contains the upcoming birthday of a contact.
  304. // "birthday" might contain the birthday including the year of birth.
  305. if ($profile["birthday"] > '0001-01-01') {
  306. $bd_timestamp = strtotime($profile["birthday"]);
  307. $month = date("m", $bd_timestamp);
  308. $day = date("d", $bd_timestamp);
  309. $current_timestamp = time();
  310. $current_year = date("Y", $current_timestamp);
  311. $current_month = date("m", $current_timestamp);
  312. $current_day = date("d", $current_timestamp);
  313. $profile["bd"] = $current_year . "-" . $month . "-" . $day;
  314. $current = $current_year . "-" . $current_month . "-" . $current_day;
  315. if ($profile["bd"] < $current) {
  316. $profile["bd"] = ( ++$current_year) . "-" . $month . "-" . $day;
  317. }
  318. } else {
  319. $profile["bd"] = '0001-01-01';
  320. }
  321. } else {
  322. $profile = $default;
  323. }
  324. if (($profile["photo"] == "") && isset($default["photo"])) {
  325. $profile["photo"] = $default["photo"];
  326. }
  327. if (($profile["name"] == "") && isset($default["name"])) {
  328. $profile["name"] = $default["name"];
  329. }
  330. if (($profile["network"] == "") && isset($default["network"])) {
  331. $profile["network"] = $default["network"];
  332. }
  333. if (($profile["thumb"] == "") && isset($profile["photo"])) {
  334. $profile["thumb"] = $profile["photo"];
  335. }
  336. if (($profile["micro"] == "") && isset($profile["thumb"])) {
  337. $profile["micro"] = $profile["thumb"];
  338. }
  339. if ((($profile["addr"] == "") || ($profile["name"] == "")) && ($profile["gid"] != 0)
  340. && in_array($profile["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS))
  341. ) {
  342. Worker::add(PRIORITY_LOW, "UpdateGContact", $profile["gid"]);
  343. }
  344. // Show contact details of Diaspora contacts only if connected
  345. if (($profile["cid"] == 0) && ($profile["network"] == NETWORK_DIASPORA)) {
  346. $profile["location"] = "";
  347. $profile["about"] = "";
  348. $profile["gender"] = "";
  349. $profile["birthday"] = '0001-01-01';
  350. }
  351. $cache[$url][$uid] = $profile;
  352. return $profile;
  353. }
  354. /**
  355. * @brief Get contact data for a given address
  356. *
  357. * The function looks at several places (contact table and gcontact table) for the contact
  358. *
  359. * @param string $addr The profile link
  360. * @param int $uid User id
  361. *
  362. * @return array Contact data
  363. */
  364. public static function getDetailsByAddr($addr, $uid = -1)
  365. {
  366. static $cache = array();
  367. if ($addr == '') {
  368. return array();
  369. }
  370. if ($uid == -1) {
  371. $uid = local_user();
  372. }
  373. // Fetch contact data from the contact table for the given user
  374. $r = q("SELECT `id`, `id` AS `cid`, 0 AS `gid`, 0 AS `zid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`,
  375. `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, `self`
  376. FROM `contact` WHERE `addr` = '%s' AND `uid` = %d", dbesc($addr), intval($uid));
  377. // Fetch the data from the contact table with "uid=0" (which is filled automatically)
  378. if (!DBM::is_result($r))
  379. $r = q("SELECT `id`, 0 AS `cid`, `id` AS `zid`, 0 AS `gid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`,
  380. `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, 0 AS `self`
  381. FROM `contact` WHERE `addr` = '%s' AND `uid` = 0", dbesc($addr));
  382. // Fetch the data from the gcontact table
  383. if (!DBM::is_result($r))
  384. $r = q("SELECT 0 AS `id`, 0 AS `cid`, `id` AS `gid`, 0 AS `zid`, 0 AS `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, '' AS `xmpp`,
  385. `keywords`, `gender`, `photo`, `photo` AS `thumb`, `photo` AS `micro`, `community` AS `forum`, 0 AS `prv`, `community`, `contact-type`, `birthday`, 0 AS `self`
  386. FROM `gcontact` WHERE `addr` = '%s'", dbesc($addr));
  387. if (!DBM::is_result($r)) {
  388. $data = Probe::uri($addr);
  389. $profile = self::getDetailsByURL($data['url'], $uid);
  390. } else {
  391. $profile = $r[0];
  392. }
  393. return $profile;
  394. }
  395. /**
  396. * @brief Returns the data array for the photo menu of a given contact
  397. *
  398. * @param array $contact contact
  399. * @param int $uid optional, default 0
  400. * @return array
  401. */
  402. public static function photoMenu(array $contact, $uid = 0)
  403. {
  404. // @todo Unused, to be removed
  405. $a = get_app();
  406. $contact_url = '';
  407. $pm_url = '';
  408. $status_link = '';
  409. $photos_link = '';
  410. $posts_link = '';
  411. $contact_drop_link = '';
  412. $poke_link = '';
  413. if ($uid == 0) {
  414. $uid = local_user();
  415. }
  416. if ($contact['uid'] != $uid) {
  417. if ($uid == 0) {
  418. $profile_link = zrl($contact['url']);
  419. $menu = array('profile' => array(t('View Profile'), $profile_link, true));
  420. return $menu;
  421. }
  422. $r = dba::select('contact', array(), array('nurl' => $contact['nurl'], 'network' => $contact['network'], 'uid' => $uid), array('limit' => 1));
  423. if ($r) {
  424. return self::photoMenu($r, $uid);
  425. } else {
  426. $profile_link = zrl($contact['url']);
  427. $connlnk = 'follow/?url=' . $contact['url'];
  428. $menu = array(
  429. 'profile' => array(t('View Profile'), $profile_link, true),
  430. 'follow' => array(t('Connect/Follow'), $connlnk, true)
  431. );
  432. return $menu;
  433. }
  434. }
  435. $sparkle = false;
  436. if ($contact['network'] === NETWORK_DFRN) {
  437. $sparkle = true;
  438. $profile_link = System::baseUrl() . '/redir/' . $contact['id'];
  439. } else {
  440. $profile_link = $contact['url'];
  441. }
  442. if ($profile_link === 'mailbox') {
  443. $profile_link = '';
  444. }
  445. if ($sparkle) {
  446. $status_link = $profile_link . '?url=status';
  447. $photos_link = $profile_link . '?url=photos';
  448. $profile_link = $profile_link . '?url=profile';
  449. }
  450. if (in_array($contact['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
  451. $pm_url = System::baseUrl() . '/message/new/' . $contact['id'];
  452. }
  453. if ($contact['network'] == NETWORK_DFRN) {
  454. $poke_link = System::baseUrl() . '/poke/?f=&c=' . $contact['id'];
  455. }
  456. $contact_url = System::baseUrl() . '/contacts/' . $contact['id'];
  457. $posts_link = System::baseUrl() . '/contacts/' . $contact['id'] . '/posts';
  458. $contact_drop_link = System::baseUrl() . '/contacts/' . $contact['id'] . '/drop?confirm=1';
  459. /**
  460. * Menu array:
  461. * "name" => [ "Label", "link", (bool)Should the link opened in a new tab? ]
  462. */
  463. $menu = array(
  464. 'status' => array(t("View Status"), $status_link, true),
  465. 'profile' => array(t("View Profile"), $profile_link, true),
  466. 'photos' => array(t("View Photos"), $photos_link, true),
  467. 'network' => array(t("Network Posts"), $posts_link, false),
  468. 'edit' => array(t("View Contact"), $contact_url, false),
  469. 'drop' => array(t("Drop Contact"), $contact_drop_link, false),
  470. 'pm' => array(t("Send PM"), $pm_url, false),
  471. 'poke' => array(t("Poke"), $poke_link, false),
  472. );
  473. $args = array('contact' => $contact, 'menu' => &$menu);
  474. call_hooks('contact_photo_menu', $args);
  475. $menucondensed = array();
  476. foreach ($menu as $menuname => $menuitem) {
  477. if ($menuitem[1] != '') {
  478. $menucondensed[$menuname] = $menuitem;
  479. }
  480. }
  481. return $menucondensed;
  482. }
  483. /**
  484. * @brief Returns ungrouped contact count or list for user
  485. *
  486. * Returns either the total number of ungrouped contacts for the given user
  487. * id or a paginated list of ungrouped contacts.
  488. *
  489. * @param int $uid uid
  490. * @param int $start optional, default 0
  491. * @param int $count optional, default 0
  492. *
  493. * @return array
  494. */
  495. public static function getUngroupedList($uid, $start = 0, $count = 0)
  496. {
  497. if (!$count) {
  498. $r = q(
  499. "SELECT COUNT(*) AS `total`
  500. FROM `contact`
  501. WHERE `uid` = %d
  502. AND NOT `self`
  503. AND NOT `blocked`
  504. AND NOT `pending`
  505. AND `id` NOT IN (
  506. SELECT DISTINCT(`contact-id`)
  507. FROM `group_member`
  508. WHERE `uid` = %d
  509. )", intval($uid), intval($uid)
  510. );
  511. return $r;
  512. }
  513. $r = q(
  514. "SELECT *
  515. FROM `contact`
  516. WHERE `uid` = %d
  517. AND NOT `self`
  518. AND NOT `blocked`
  519. AND NOT `pending`
  520. AND `id` NOT IN (
  521. SELECT DISTINCT(`contact-id`)
  522. FROM `group_member`
  523. INNER JOIN `group` ON `group`.`id` = `group_member`.`gid`
  524. WHERE `group`.`uid` = %d
  525. )
  526. LIMIT %d, %d", intval($uid), intval($uid), intval($start), intval($count)
  527. );
  528. return $r;
  529. }
  530. /**
  531. * @brief Fetch the contact id for a given url and user
  532. *
  533. * First lookup in the contact table to find a record matching either `url`, `nurl`,
  534. * `addr` or `alias`.
  535. *
  536. * If there's no record and we aren't looking for a public contact, we quit.
  537. * If there's one, we check that it isn't time to update the picture else we
  538. * directly return the found contact id.
  539. *
  540. * Second, we probe the provided $url wether it's http://server.tld/profile or
  541. * nick@server.tld. We quit if we can't get any info back.
  542. *
  543. * Third, we create the contact record if it doesn't exist
  544. *
  545. * Fourth, we update the existing record with the new data (avatar, alias, nick)
  546. * if there's any updates
  547. *
  548. * @param string $url Contact URL
  549. * @param integer $uid The user id for the contact (0 = public contact)
  550. * @param boolean $no_update Don't update the contact
  551. *
  552. * @return integer Contact ID
  553. */
  554. public static function getIdForURL($url, $uid = 0, $no_update = false)
  555. {
  556. logger("Get contact data for url " . $url . " and user " . $uid . " - " . System::callstack(), LOGGER_DEBUG);
  557. $contact_id = 0;
  558. if ($url == '') {
  559. return 0;
  560. }
  561. /// @todo Verify if we can't use Contact::getDetailsByUrl instead of the following
  562. // We first try the nurl (http://server.tld/nick), most common case
  563. $contact = dba::select('contact', array('id', 'avatar-date'), array('nurl' => normalise_link($url), 'uid' => $uid), array('limit' => 1));
  564. // Then the addr (nick@server.tld)
  565. if (!DBM::is_result($contact)) {
  566. $contact = dba::select('contact', array('id', 'avatar-date'), array('addr' => $url, 'uid' => $uid), array('limit' => 1));
  567. }
  568. // Then the alias (which could be anything)
  569. if (!DBM::is_result($contact)) {
  570. // The link could be provided as http although we stored it as https
  571. $ssl_url = str_replace('http://', 'https://', $url);
  572. $r = dba::select('contact', array('id', 'avatar-date'), array('`alias` IN (?, ?, ?) AND `uid` = ?', $url, normalise_link($url), $ssl_url, $uid), array('limit' => 1));
  573. $contact = dba::fetch($r);
  574. dba::close($r);
  575. }
  576. if (DBM::is_result($contact)) {
  577. $contact_id = $contact["id"];
  578. // Update the contact every 7 days
  579. $update_contact = ($contact['avatar-date'] < datetime_convert('', '', 'now -7 days'));
  580. // We force the update if the avatar is empty
  581. if ($contact['avatar'] == '') {
  582. $update_contact = true;
  583. }
  584. if (!$update_contact || $no_update) {
  585. return $contact_id;
  586. }
  587. } elseif ($uid != 0) {
  588. // Non-existing user-specific contact, exiting
  589. return 0;
  590. }
  591. $data = Probe::uri($url, "", $uid);
  592. // Last try in gcontact for unsupported networks
  593. if (!in_array($data["network"], array(NETWORK_DFRN, NETWORK_OSTATUS, NETWORK_DIASPORA, NETWORK_PUMPIO, NETWORK_MAIL))) {
  594. if ($uid != 0) {
  595. return 0;
  596. }
  597. // Get data from the gcontact table
  598. $gcontacts = dba::select('gcontact', array('name', 'nick', 'url', 'photo', 'addr', 'alias', 'network'), array('nurl' => normalise_link($url)), array('limit' => 1));
  599. if (!DBM::is_result($gcontacts)) {
  600. return 0;
  601. }
  602. $data = array_merge($data, $gcontacts);
  603. }
  604. if (!$contact_id && ($data["alias"] != '') && ($data["alias"] != $url)) {
  605. $contact_id = self::getIdForURL($data["alias"], $uid, true);
  606. }
  607. $url = $data["url"];
  608. if (!$contact_id) {
  609. dba::insert(
  610. 'contact', array('uid' => $uid, 'created' => datetime_convert(), 'url' => $data["url"],
  611. 'nurl' => normalise_link($data["url"]), 'addr' => $data["addr"],
  612. 'alias' => $data["alias"], 'notify' => $data["notify"], 'poll' => $data["poll"],
  613. 'name' => $data["name"], 'nick' => $data["nick"], 'photo' => $data["photo"],
  614. 'keywords' => $data["keywords"], 'location' => $data["location"], 'about' => $data["about"],
  615. 'network' => $data["network"], 'pubkey' => $data["pubkey"],
  616. 'rel' => CONTACT_IS_SHARING, 'priority' => $data["priority"],
  617. 'batch' => $data["batch"], 'request' => $data["request"],
  618. 'confirm' => $data["confirm"], 'poco' => $data["poco"],
  619. 'name-date' => datetime_convert(), 'uri-date' => datetime_convert(),
  620. 'avatar-date' => datetime_convert(), 'writable' => 1, 'blocked' => 0,
  621. 'readonly' => 0, 'pending' => 0)
  622. );
  623. $s = dba::select('contact', array('id'), array('nurl' => normalise_link($data["url"]), 'uid' => $uid), array('order' => array('id'), 'limit' => 2));
  624. $contacts = dba::inArray($s);
  625. if (!DBM::is_result($contacts)) {
  626. return 0;
  627. }
  628. $contact_id = $contacts[0]["id"];
  629. // Update the newly created contact from data in the gcontact table
  630. $gcontact = dba::select('gcontact', array('location', 'about', 'keywords', 'gender'), array('nurl' => normalise_link($data["url"])), array('limit' => 1));
  631. if (DBM::is_result($gcontact)) {
  632. // Only use the information when the probing hadn't fetched these values
  633. if ($data['keywords'] != '') {
  634. unset($gcontact['keywords']);
  635. }
  636. if ($data['location'] != '') {
  637. unset($gcontact['location']);
  638. }
  639. if ($data['about'] != '') {
  640. unset($gcontact['about']);
  641. }
  642. dba::update('contact', $gcontact, array('id' => $contact_id));
  643. }
  644. if (count($contacts) > 1 && $uid == 0 && $contact_id != 0 && $data["url"] != "") {
  645. dba::delete('contact', array("`nurl` = ? AND `uid` = 0 AND `id` != ? AND NOT `self`",
  646. normalise_link($data["url"]), $contact_id));
  647. }
  648. }
  649. self::updateAvatar($data["photo"], $uid, $contact_id);
  650. $fields = array('url', 'nurl', 'addr', 'alias', 'name', 'nick', 'keywords', 'location', 'about', 'avatar-date', 'pubkey');
  651. $contact = dba::select('contact', $fields, array('id' => $contact_id), array('limit' => 1));
  652. // This condition should always be true
  653. if (!DBM::is_result($contact)) {
  654. return $contact_id;
  655. }
  656. $updated = array('addr' => $data['addr'],
  657. 'alias' => $data['alias'],
  658. 'url' => $data['url'],
  659. 'nurl' => normalise_link($data['url']),
  660. 'name' => $data['name'],
  661. 'nick' => $data['nick']);
  662. // Only fill the pubkey if it was empty before. We have to prevent identity theft.
  663. if (!empty($contact['pubkey'])) {
  664. unset($contact['pubkey']);
  665. } else {
  666. $updated['pubkey'] = $data['pubkey'];
  667. }
  668. if ($data['keywords'] != '') {
  669. $updated['keywords'] = $data['keywords'];
  670. }
  671. if ($data['location'] != '') {
  672. $updated['location'] = $data['location'];
  673. }
  674. if ($data['about'] != '') {
  675. $updated['about'] = $data['about'];
  676. }
  677. if (($data["addr"] != $contact["addr"]) || ($data["alias"] != $contact["alias"])) {
  678. $updated['uri-date'] = datetime_convert();
  679. }
  680. if (($data["name"] != $contact["name"]) || ($data["nick"] != $contact["nick"])) {
  681. $updated['name-date'] = datetime_convert();
  682. }
  683. $updated['avatar-date'] = datetime_convert();
  684. dba::update('contact', $updated, array('id' => $contact_id), $contact);
  685. return $contact_id;
  686. }
  687. /**
  688. * @brief Checks if the contact is blocked
  689. *
  690. * @param int $cid contact id
  691. *
  692. * @return boolean Is the contact blocked?
  693. */
  694. public static function isBlocked($cid)
  695. {
  696. if ($cid == 0) {
  697. return false;
  698. }
  699. $blocked = dba::select('contact', array('blocked'), array('id' => $cid), array('limit' => 1));
  700. if (!DBM::is_result($blocked)) {
  701. return false;
  702. }
  703. return (bool) $blocked['blocked'];
  704. }
  705. /**
  706. * @brief Checks if the contact is hidden
  707. *
  708. * @param int $cid contact id
  709. *
  710. * @return boolean Is the contact hidden?
  711. */
  712. public static function isHidden($cid)
  713. {
  714. if ($cid == 0) {
  715. return false;
  716. }
  717. $hidden = dba::select('contact', array('hidden'), array('id' => $cid), array('limit' => 1));
  718. if (!DBM::is_result($hidden)) {
  719. return false;
  720. }
  721. return (bool) $hidden['hidden'];
  722. }
  723. /**
  724. * @brief Returns posts from a given contact url
  725. *
  726. * @param string $contact_url Contact URL
  727. *
  728. * @return string posts in HTML
  729. */
  730. public static function getPostsFromUrl($contact_url)
  731. {
  732. $a = self::getApp();
  733. require_once 'include/conversation.php';
  734. // There are no posts with "uid = 0" with connector networks
  735. // This speeds up the query a lot
  736. $r = q("SELECT `network`, `id` AS `author-id`, `contact-type` FROM `contact`
  737. WHERE `contact`.`nurl` = '%s' AND `contact`.`uid` = 0", dbesc(normalise_link($contact_url)));
  738. if (!DBM::is_result($r)) {
  739. return '';
  740. }
  741. if (in_array($r[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) {
  742. $sql = "(`item`.`uid` = 0 OR (`item`.`uid` = %d AND NOT `item`.`global`))";
  743. } else {
  744. $sql = "`item`.`uid` = %d";
  745. }
  746. $author_id = intval($r[0]["author-id"]);
  747. $contact = ($r[0]["contact-type"] == ACCOUNT_TYPE_COMMUNITY ? 'owner-id' : 'author-id');
  748. $r = q(item_query() . " AND `item`.`" . $contact . "` = %d AND " . $sql .
  749. " ORDER BY `item`.`created` DESC LIMIT %d, %d", intval($author_id), intval(local_user()), intval($a->pager['start']), intval($a->pager['itemspage'])
  750. );
  751. $o = conversation($a, $r, 'community', false);
  752. $o .= alt_pager($a, count($r));
  753. return $o;
  754. }
  755. /**
  756. * @brief Returns the account type name
  757. *
  758. * The function can be called with either the user or the contact array
  759. *
  760. * @param array $contact contact or user array
  761. * @return string
  762. */
  763. public static function getAccountType(array $contact)
  764. {
  765. // There are several fields that indicate that the contact or user is a forum
  766. // "page-flags" is a field in the user table,
  767. // "forum" and "prv" are used in the contact table. They stand for PAGE_COMMUNITY and PAGE_PRVGROUP.
  768. // "community" is used in the gcontact table and is true if the contact is PAGE_COMMUNITY or PAGE_PRVGROUP.
  769. if ((isset($contact['page-flags']) && (intval($contact['page-flags']) == PAGE_COMMUNITY))
  770. || (isset($contact['page-flags']) && (intval($contact['page-flags']) == PAGE_PRVGROUP))
  771. || (isset($contact['forum']) && intval($contact['forum']))
  772. || (isset($contact['prv']) && intval($contact['prv']))
  773. || (isset($contact['community']) && intval($contact['community']))
  774. ) {
  775. $type = ACCOUNT_TYPE_COMMUNITY;
  776. } else {
  777. $type = ACCOUNT_TYPE_PERSON;
  778. }
  779. // The "contact-type" (contact table) and "account-type" (user table) are more general then the chaos from above.
  780. if (isset($contact["contact-type"])) {
  781. $type = $contact["contact-type"];
  782. }
  783. if (isset($contact["account-type"])) {
  784. $type = $contact["account-type"];
  785. }
  786. switch ($type) {
  787. case ACCOUNT_TYPE_ORGANISATION:
  788. $account_type = t("Organisation");
  789. break;
  790. case ACCOUNT_TYPE_NEWS:
  791. $account_type = t('News');
  792. break;
  793. case ACCOUNT_TYPE_COMMUNITY:
  794. $account_type = t("Forum");
  795. break;
  796. default:
  797. $account_type = "";
  798. break;
  799. }
  800. return $account_type;
  801. }
  802. /**
  803. * @brief Blocks a contact
  804. *
  805. * @param int $uid
  806. * @return bool
  807. */
  808. public static function block($uid)
  809. {
  810. $return = dba::update('contact', ['blocked' => true], ['id' => $uid]);
  811. return $return;
  812. }
  813. /**
  814. * @brief Unblocks a contact
  815. *
  816. * @param int $uid
  817. * @return bool
  818. */
  819. public static function unblock($uid)
  820. {
  821. $return = dba::update('contact', ['blocked' => false], ['id' => $uid]);
  822. return $return;
  823. }
  824. /**
  825. * @brief Updates the avatar links in a contact only if needed
  826. *
  827. * @param string $avatar Link to avatar picture
  828. * @param int $uid User id of contact owner
  829. * @param int $cid Contact id
  830. * @param bool $force force picture update
  831. *
  832. * @return array Returns array of the different avatar sizes
  833. */
  834. public static function updateAvatar($avatar, $uid, $cid, $force = false)
  835. {
  836. // Limit = 1 returns the row so no need for dba:inArray()
  837. $r = dba::select('contact', array('avatar', 'photo', 'thumb', 'micro', 'nurl'), array('id' => $cid), array('limit' => 1));
  838. if (!DBM::is_result($r)) {
  839. return false;
  840. } else {
  841. $data = array($r["photo"], $r["thumb"], $r["micro"]);
  842. }
  843. if (($r["avatar"] != $avatar) || $force) {
  844. $photos = Photo::importProfilePhoto($avatar, $uid, $cid, true);
  845. if ($photos) {
  846. dba::update(
  847. 'contact',
  848. array('avatar' => $avatar, 'photo' => $photos[0], 'thumb' => $photos[1], 'micro' => $photos[2], 'avatar-date' => datetime_convert()),
  849. array('id' => $cid)
  850. );
  851. // Update the public contact (contact id = 0)
  852. if ($uid != 0) {
  853. $pcontact = dba::select('contact', array('id'), array('nurl' => $r[0]['nurl']), array('limit' => 1));
  854. if (DBM::is_result($pcontact)) {
  855. self::updateAvatar($avatar, 0, $pcontact['id'], $force);
  856. }
  857. }
  858. return $photos;
  859. }
  860. }
  861. return $data;
  862. }
  863. }