3 * @file src/Model/Contact.php
5 namespace Friendica\Model;
7 use Friendica\BaseObject;
8 use Friendica\Core\Config;
9 use Friendica\Core\PConfig;
10 use Friendica\Core\System;
11 use Friendica\Core\Worker;
12 use Friendica\Database\DBM;
13 use Friendica\Network\Probe;
14 use Friendica\Model\Photo;
15 use Friendica\Model\Profile;
16 use Friendica\Protocol\Diaspora;
17 use Friendica\Protocol\DFRN;
18 use Friendica\Protocol\OStatus;
19 use Friendica\Protocol\PortableContact;
20 use Friendica\Protocol\Salmon;
23 require_once 'boot.php';
24 require_once 'include/dba.php';
25 require_once 'include/text.php';
28 * @brief functions for interacting with a contact
30 class Contact extends BaseObject
33 * @brief Returns a list of contacts belonging in a group
38 public static function getByGroupId($gid)
42 $stmt = dba::p('SELECT `group_member`.`contact-id`, `contact`.*
44 INNER JOIN `group_member`
45 ON `contact`.`id` = `group_member`.`contact-id`
47 AND `contact`.`uid` = ?
48 AND NOT `contact`.`self`
49 AND NOT `contact`.`blocked`
50 AND NOT `contact`.`pending`
51 ORDER BY `contact`.`name` ASC',
55 if (DBM::is_result($stmt)) {
56 $return = dba::inArray($stmt);
64 * @brief Returns the count of OStatus contacts in a group
69 public static function getOStatusCountByGroupId($gid)
73 $contacts = dba::fetch_first('SELECT COUNT(*) AS `count`
75 INNER JOIN `group_member`
76 ON `contact`.`id` = `group_member`.`contact-id`
78 AND `contact`.`uid` = ?
79 AND `contact`.`network` = ?
80 AND `contact`.`notify` != ""',
85 $return = $contacts['count'];
92 * Creates the self-contact for the provided user id
95 * @return bool Operation success
97 public static function createSelfFromUserId($uid)
99 // Only create the entry if it doesn't exist yet
100 if (dba::exists('contact', ['uid' => $uid, 'self' => true])) {
104 $user = dba::selectFirst('user', ['uid', 'username', 'nickname'], ['uid' => $uid]);
105 if (!DBM::is_result($user)) {
109 $return = dba::insert('contact', [
110 'uid' => $user['uid'],
111 'created' => datetime_convert(),
113 'name' => $user['username'],
114 'nick' => $user['nickname'],
115 'photo' => System::baseUrl() . '/photo/profile/' . $user['uid'] . '.jpg',
116 'thumb' => System::baseUrl() . '/photo/avatar/' . $user['uid'] . '.jpg',
117 'micro' => System::baseUrl() . '/photo/micro/' . $user['uid'] . '.jpg',
120 'url' => System::baseUrl() . '/profile/' . $user['nickname'],
121 'nurl' => normalise_link(System::baseUrl() . '/profile/' . $user['nickname']),
122 'addr' => $user['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3),
123 'request' => System::baseUrl() . '/dfrn_request/' . $user['nickname'],
124 'notify' => System::baseUrl() . '/dfrn_notify/' . $user['nickname'],
125 'poll' => System::baseUrl() . '/dfrn_poll/' . $user['nickname'],
126 'confirm' => System::baseUrl() . '/dfrn_confirm/' . $user['nickname'],
127 'poco' => System::baseUrl() . '/poco/' . $user['nickname'],
128 'name-date' => datetime_convert(),
129 'uri-date' => datetime_convert(),
130 'avatar-date' => datetime_convert(),
138 * @brief Marks a contact for removal
140 * @param int $id contact id
143 public static function remove($id)
145 // We want just to make sure that we don't delete our "self" contact
146 $contact = dba::selectFirst('contact', ['uid'], ['id' => $id, 'self' => false]);
147 if (!DBM::is_result($contact) || !intval($contact['uid'])) {
151 $archive = PConfig::get($contact['uid'], 'system', 'archive_removed_contacts');
153 dba::update('contact', array('archive' => true, 'network' => 'none', 'writable' => false), array('id' => $id));
157 dba::delete('contact', array('id' => $id));
159 // Delete the rest in the background
160 Worker::add(PRIORITY_LOW, 'RemoveContact', $id);
164 * @brief Sends an unfriend message. Does not remove the contact
166 * @param array $user User unfriending
167 * @param array $contact Contact unfriended
170 public static function terminateFriendship(array $user, array $contact)
172 if ($contact['network'] === NETWORK_OSTATUS) {
173 // create an unfollow slap
175 $item['verb'] = NAMESPACE_OSTATUS . "/unfollow";
176 $item['follow'] = $contact["url"];
177 $slap = OStatus::salmon($item, $user);
179 if ((x($contact, 'notify')) && (strlen($contact['notify']))) {
180 Salmon::slapper($user, $contact['notify'], $slap);
182 } elseif ($contact['network'] === NETWORK_DIASPORA) {
183 Diaspora::sendUnshare($user, $contact);
184 } elseif ($contact['network'] === NETWORK_DFRN) {
185 DFRN::deliver($user, $contact, 'placeholder', 1);
190 * @brief Marks a contact for archival after a communication issue delay
192 * Contact has refused to recognise us as a friend. We will start a countdown.
193 * If they still don't recognise us in 32 days, the relationship is over,
194 * and we won't waste any more time trying to communicate with them.
195 * This provides for the possibility that their database is temporarily messed
196 * up or some other transient event and that there's a possibility we could recover from it.
198 * @param array $contact contact to mark for archival
201 public static function markForArchival(array $contact)
203 // Contact already archived or "self" contact? => nothing to do
204 if ($contact['archive'] || $contact['self']) {
208 if ($contact['term-date'] <= NULL_DATE) {
209 dba::update('contact', array('term-date' => datetime_convert()), array('id' => $contact['id']));
211 if ($contact['url'] != '') {
212 dba::update('contact', array('term-date' => datetime_convert()), array('`nurl` = ? AND `term-date` <= ? AND NOT `self`', normalise_link($contact['url']), NULL_DATE));
216 * We really should send a notification to the owner after 2-3 weeks
217 * so they won't be surprised when the contact vanishes and can take
218 * remedial action if this was a serious mistake or glitch
221 /// @todo Check for contact vitality via probing
222 $expiry = $contact['term-date'] . ' + 32 days ';
223 if (datetime_convert() > datetime_convert('UTC', 'UTC', $expiry)) {
224 /* Relationship is really truly dead. archive them rather than
225 * delete, though if the owner tries to unarchive them we'll start
226 * the whole process over again.
228 dba::update('contact', array('archive' => 1), array('id' => $contact['id']));
230 if ($contact['url'] != '') {
231 dba::update('contact', array('archive' => 1), array('nurl' => normalise_link($contact['url']), 'self' => false));
238 * @brief Cancels the archival countdown
240 * @see Contact::markForArchival()
242 * @param array $contact contact to be unmarked for archival
245 public static function unmarkForArchival(array $contact)
247 $condition = array('`id` = ? AND (`term-date` > ? OR `archive`)', $contact['id'], NULL_DATE);
248 $exists = dba::exists('contact', $condition);
250 // We don't need to update, we never marked this contact for archival
255 // It's a miracle. Our dead contact has inexplicably come back to life.
256 $fields = array('term-date' => NULL_DATE, 'archive' => false);
257 dba::update('contact', $fields, array('id' => $contact['id']));
259 if ($contact['url'] != '') {
260 dba::update('contact', $fields, array('nurl' => normalise_link($contact['url'])));
265 * @brief Get contact data for a given profile link
267 * The function looks at several places (contact table and gcontact table) for the contact
268 * It caches its result for the same script execution to prevent duplicate calls
270 * @param string $url The profile link
271 * @param int $uid User id
272 * @param array $default If not data was found take this data as default value
274 * @return array Contact data
276 public static function getDetailsByURL($url, $uid = -1, array $default = [])
278 static $cache = array();
288 if (isset($cache[$url][$uid])) {
289 return $cache[$url][$uid];
292 $ssl_url = str_replace('http://', 'https://', $url);
294 // Fetch contact data from the contact table for the given user
295 $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`,
296 `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, `self`
297 FROM `contact` WHERE `nurl` = ? AND `uid` = ?", normalise_link($url), $uid);
298 $r = dba::inArray($s);
300 // Fetch contact data from the contact table for the given user, checking with the alias
301 if (!DBM::is_result($r)) {
302 $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`,
303 `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, `self`
304 FROM `contact` WHERE `alias` IN (?, ?, ?) AND `uid` = ?", normalise_link($url), $url, $ssl_url, $uid);
305 $r = dba::inArray($s);
308 // Fetch the data from the contact table with "uid=0" (which is filled automatically)
309 if (!DBM::is_result($r)) {
310 $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`,
311 `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, 0 AS `self`
312 FROM `contact` WHERE `nurl` = ? AND `uid` = 0", normalise_link($url));
313 $r = dba::inArray($s);
316 // Fetch the data from the contact table with "uid=0" (which is filled automatically) - checked with the alias
317 if (!DBM::is_result($r)) {
318 $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`,
319 `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, 0 AS `self`
320 FROM `contact` WHERE `alias` IN (?, ?, ?) AND `uid` = 0", normalise_link($url), $url, $ssl_url);
321 $r = dba::inArray($s);
324 // Fetch the data from the gcontact table
325 if (!DBM::is_result($r)) {
326 $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`,
327 `keywords`, `gender`, `photo`, `photo` AS `thumb`, `photo` AS `micro`, `community` AS `forum`, 0 AS `prv`, `community`, `contact-type`, `birthday`, 0 AS `self`
328 FROM `gcontact` WHERE `nurl` = ?", normalise_link($url));
329 $r = dba::inArray($s);
332 if (DBM::is_result($r)) {
333 // If there is more than one entry we filter out the connector networks
335 foreach ($r as $id => $result) {
336 if ($result["network"] == NETWORK_STATUSNET) {
342 $profile = array_shift($r);
344 // "bd" always contains the upcoming birthday of a contact.
345 // "birthday" might contain the birthday including the year of birth.
346 if ($profile["birthday"] > '0001-01-01') {
347 $bd_timestamp = strtotime($profile["birthday"]);
348 $month = date("m", $bd_timestamp);
349 $day = date("d", $bd_timestamp);
351 $current_timestamp = time();
352 $current_year = date("Y", $current_timestamp);
353 $current_month = date("m", $current_timestamp);
354 $current_day = date("d", $current_timestamp);
356 $profile["bd"] = $current_year . "-" . $month . "-" . $day;
357 $current = $current_year . "-" . $current_month . "-" . $current_day;
359 if ($profile["bd"] < $current) {
360 $profile["bd"] = ( ++$current_year) . "-" . $month . "-" . $day;
363 $profile["bd"] = '0001-01-01';
369 if (($profile["photo"] == "") && isset($default["photo"])) {
370 $profile["photo"] = $default["photo"];
373 if (($profile["name"] == "") && isset($default["name"])) {
374 $profile["name"] = $default["name"];
377 if (($profile["network"] == "") && isset($default["network"])) {
378 $profile["network"] = $default["network"];
381 if (($profile["thumb"] == "") && isset($profile["photo"])) {
382 $profile["thumb"] = $profile["photo"];
385 if (($profile["micro"] == "") && isset($profile["thumb"])) {
386 $profile["micro"] = $profile["thumb"];
389 if ((($profile["addr"] == "") || ($profile["name"] == "")) && ($profile["gid"] != 0)
390 && in_array($profile["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS))
392 Worker::add(PRIORITY_LOW, "UpdateGContact", $profile["gid"]);
395 // Show contact details of Diaspora contacts only if connected
396 if (($profile["cid"] == 0) && ($profile["network"] == NETWORK_DIASPORA)) {
397 $profile["location"] = "";
398 $profile["about"] = "";
399 $profile["gender"] = "";
400 $profile["birthday"] = '0001-01-01';
403 $cache[$url][$uid] = $profile;
409 * @brief Get contact data for a given address
411 * The function looks at several places (contact table and gcontact table) for the contact
413 * @param string $addr The profile link
414 * @param int $uid User id
416 * @return array Contact data
418 public static function getDetailsByAddr($addr, $uid = -1)
420 static $cache = array();
430 // Fetch contact data from the contact table for the given user
431 $r = q("SELECT `id`, `id` AS `cid`, 0 AS `gid`, 0 AS `zid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`,
432 `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, `self`
433 FROM `contact` WHERE `addr` = '%s' AND `uid` = %d",
437 // Fetch the data from the contact table with "uid=0" (which is filled automatically)
438 if (!DBM::is_result($r)) {
439 $r = q("SELECT `id`, 0 AS `cid`, `id` AS `zid`, 0 AS `gid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`,
440 `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, 0 AS `self`
441 FROM `contact` WHERE `addr` = '%s' AND `uid` = 0",
446 // Fetch the data from the gcontact table
447 if (!DBM::is_result($r)) {
448 $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`,
449 `keywords`, `gender`, `photo`, `photo` AS `thumb`, `photo` AS `micro`, `community` AS `forum`, 0 AS `prv`, `community`, `contact-type`, `birthday`, 0 AS `self`
450 FROM `gcontact` WHERE `addr` = '%s'",
455 if (!DBM::is_result($r)) {
456 $data = Probe::uri($addr);
458 $profile = self::getDetailsByURL($data['url'], $uid);
467 * @brief Returns the data array for the photo menu of a given contact
469 * @param array $contact contact
470 * @param int $uid optional, default 0
473 public static function photoMenu(array $contact, $uid = 0)
475 // @todo Unused, to be removed
483 $contact_drop_link = '';
490 if ($contact['uid'] != $uid) {
492 $profile_link = Profile::zrl($contact['url']);
493 $menu = array('profile' => array(t('View Profile'), $profile_link, true));
498 // Look for our own contact if the uid doesn't match and isn't public
499 $contact_own = dba::selectFirst('contact', [], ['nurl' => $contact['nurl'], 'network' => $contact['network'], 'uid' => $uid]);
500 if (DBM::is_result($contact_own)) {
501 return self::photoMenu($contact_own, $uid);
503 $profile_link = Profile::zrl($contact['url']);
504 $connlnk = 'follow/?url=' . $contact['url'];
506 'profile' => array(t('View Profile'), $profile_link, true),
507 'follow' => array(t('Connect/Follow'), $connlnk, true)
515 if ($contact['network'] === NETWORK_DFRN) {
517 $profile_link = System::baseUrl() . '/redir/' . $contact['id'];
519 $profile_link = $contact['url'];
522 if ($profile_link === 'mailbox') {
527 $status_link = $profile_link . '?url=status';
528 $photos_link = $profile_link . '?url=photos';
529 $profile_link = $profile_link . '?url=profile';
532 if (in_array($contact['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) {
533 $pm_url = System::baseUrl() . '/message/new/' . $contact['id'];
536 if ($contact['network'] == NETWORK_DFRN) {
537 $poke_link = System::baseUrl() . '/poke/?f=&c=' . $contact['id'];
540 $contact_url = System::baseUrl() . '/contacts/' . $contact['id'];
542 $posts_link = System::baseUrl() . '/contacts/' . $contact['id'] . '/posts';
543 $contact_drop_link = System::baseUrl() . '/contacts/' . $contact['id'] . '/drop?confirm=1';
547 * "name" => [ "Label", "link", (bool)Should the link opened in a new tab? ]
550 'status' => [t("View Status") , $status_link , true],
551 'profile' => [t("View Profile") , $profile_link , true],
552 'photos' => [t("View Photos") , $photos_link , true],
553 'network' => [t("Network Posts"), $posts_link , false],
554 'edit' => [t("View Contact") , $contact_url , false],
555 'drop' => [t("Drop Contact") , $contact_drop_link, false],
556 'pm' => [t("Send PM") , $pm_url , false],
557 'poke' => [t("Poke") , $poke_link , false],
560 $args = ['contact' => $contact, 'menu' => &$menu];
562 call_hooks('contact_photo_menu', $args);
564 $menucondensed = array();
566 foreach ($menu as $menuname => $menuitem) {
567 if ($menuitem[1] != '') {
568 $menucondensed[$menuname] = $menuitem;
572 return $menucondensed;
576 * @brief Returns ungrouped contact count or list for user
578 * Returns either the total number of ungrouped contacts for the given user
579 * id or a paginated list of ungrouped contacts.
581 * @param int $uid uid
582 * @param int $start optional, default 0
583 * @param int $count optional, default 0
587 public static function getUngroupedList($uid, $start = 0, $count = 0)
591 "SELECT COUNT(*) AS `total`
598 SELECT DISTINCT(`contact-id`)
617 SELECT DISTINCT(`contact-id`)
619 INNER JOIN `group` ON `group`.`id` = `group_member`.`gid`
620 WHERE `group`.`uid` = %d
633 * @brief Fetch the contact id for a given URL and user
635 * First lookup in the contact table to find a record matching either `url`, `nurl`,
638 * If there's no record and we aren't looking for a public contact, we quit.
639 * If there's one, we check that it isn't time to update the picture else we
640 * directly return the found contact id.
642 * Second, we probe the provided $url whether it's http://server.tld/profile or
643 * nick@server.tld. We quit if we can't get any info back.
645 * Third, we create the contact record if it doesn't exist
647 * Fourth, we update the existing record with the new data (avatar, alias, nick)
648 * if there's any updates
650 * @param string $url Contact URL
651 * @param integer $uid The user id for the contact (0 = public contact)
652 * @param boolean $no_update Don't update the contact
654 * @return integer Contact ID
656 public static function getIdForURL($url, $uid = 0, $no_update = false)
658 logger("Get contact data for url " . $url . " and user " . $uid . " - " . System::callstack(), LOGGER_DEBUG);
666 /// @todo Verify if we can't use Contact::getDetailsByUrl instead of the following
667 // We first try the nurl (http://server.tld/nick), most common case
668 $contact = dba::selectFirst('contact', ['id', 'avatar-date'], ['nurl' => normalise_link($url), 'uid' => $uid]);
670 // Then the addr (nick@server.tld)
671 if (!DBM::is_result($contact)) {
672 $contact = dba::selectFirst('contact', ['id', 'avatar-date'], ['addr' => $url, 'uid' => $uid]);
675 // Then the alias (which could be anything)
676 if (!DBM::is_result($contact)) {
677 // The link could be provided as http although we stored it as https
678 $ssl_url = str_replace('http://', 'https://', $url);
679 $condition = ['`alias` IN (?, ?, ?) AND `uid` = ?', $url, normalise_link($url), $ssl_url, $uid];
680 $contact = dba::selectFirst('contact', ['id', 'avatar', 'avatar-date'], $condition);
683 if (DBM::is_result($contact)) {
684 $contact_id = $contact["id"];
686 // Update the contact every 7 days
687 $update_contact = ($contact['avatar-date'] < datetime_convert('', '', 'now -7 days'));
689 // We force the update if the avatar is empty
690 if (!x($contact, 'avatar')) {
691 $update_contact = true;
694 if (!$update_contact || $no_update) {
697 } elseif ($uid != 0) {
698 // Non-existing user-specific contact, exiting
702 $data = Probe::uri($url, "", $uid);
704 // Last try in gcontact for unsupported networks
705 if (!in_array($data["network"], array(NETWORK_DFRN, NETWORK_OSTATUS, NETWORK_DIASPORA, NETWORK_PUMPIO, NETWORK_MAIL))) {
710 // Get data from the gcontact table
711 $gcontact = dba::selectFirst('gcontact', ['name', 'nick', 'url', 'photo', 'addr', 'alias', 'network'], ['nurl' => normalise_link($url)]);
712 if (!DBM::is_result($gcontact)) {
716 $data = array_merge($data, $gcontact);
719 if (!$contact_id && ($data["alias"] != '') && ($data["alias"] != $url)) {
720 $contact_id = self::getIdForURL($data["alias"], $uid, true);
725 dba::insert('contact', [
727 'created' => datetime_convert(),
728 'url' => $data["url"],
729 'nurl' => normalise_link($data["url"]),
730 'addr' => $data["addr"],
731 'alias' => $data["alias"],
732 'notify' => $data["notify"],
733 'poll' => $data["poll"],
734 'name' => $data["name"],
735 'nick' => $data["nick"],
736 'photo' => $data["photo"],
737 'keywords' => $data["keywords"],
738 'location' => $data["location"],
739 'about' => $data["about"],
740 'network' => $data["network"],
741 'pubkey' => $data["pubkey"],
742 'rel' => CONTACT_IS_SHARING,
743 'priority' => $data["priority"],
744 'batch' => $data["batch"],
745 'request' => $data["request"],
746 'confirm' => $data["confirm"],
747 'poco' => $data["poco"],
748 'name-date' => datetime_convert(),
749 'uri-date' => datetime_convert(),
750 'avatar-date' => datetime_convert(),
757 $s = dba::select('contact', ['id'], ['nurl' => normalise_link($data["url"]), 'uid' => $uid], ['order' => ['id'], 'limit' => 2]);
758 $contacts = dba::inArray($s);
759 if (!DBM::is_result($contacts)) {
763 $contact_id = $contacts[0]["id"];
765 // Update the newly created contact from data in the gcontact table
766 $gcontact = dba::selectFirst('gcontact', ['location', 'about', 'keywords', 'gender'], ['nurl' => normalise_link($data["url"])]);
767 if (DBM::is_result($gcontact)) {
768 // Only use the information when the probing hadn't fetched these values
769 if ($data['keywords'] != '') {
770 unset($gcontact['keywords']);
772 if ($data['location'] != '') {
773 unset($gcontact['location']);
775 if ($data['about'] != '') {
776 unset($gcontact['about']);
778 dba::update('contact', $gcontact, array('id' => $contact_id));
781 if (count($contacts) > 1 && $uid == 0 && $contact_id != 0 && $data["url"] != "") {
782 dba::delete('contact', ["`nurl` = ? AND `uid` = 0 AND `id` != ? AND NOT `self`",
783 normalise_link($data["url"]), $contact_id]);
787 self::updateAvatar($data["photo"], $uid, $contact_id);
789 $fields = ['url', 'nurl', 'addr', 'alias', 'name', 'nick', 'keywords', 'location', 'about', 'avatar-date', 'pubkey'];
790 $contact = dba::selectFirst('contact', $fields, ['id' => $contact_id]);
792 // This condition should always be true
793 if (!DBM::is_result($contact)) {
797 $updated = array('addr' => $data['addr'],
798 'alias' => $data['alias'],
799 'url' => $data['url'],
800 'nurl' => normalise_link($data['url']),
801 'name' => $data['name'],
802 'nick' => $data['nick']);
804 // Only fill the pubkey if it was empty before. We have to prevent identity theft.
805 if (!empty($contact['pubkey'])) {
806 unset($contact['pubkey']);
808 $updated['pubkey'] = $data['pubkey'];
811 if ($data['keywords'] != '') {
812 $updated['keywords'] = $data['keywords'];
814 if ($data['location'] != '') {
815 $updated['location'] = $data['location'];
817 if ($data['about'] != '') {
818 $updated['about'] = $data['about'];
821 if (($data["addr"] != $contact["addr"]) || ($data["alias"] != $contact["alias"])) {
822 $updated['uri-date'] = datetime_convert();
824 if (($data["name"] != $contact["name"]) || ($data["nick"] != $contact["nick"])) {
825 $updated['name-date'] = datetime_convert();
828 $updated['avatar-date'] = datetime_convert();
830 dba::update('contact', $updated, array('id' => $contact_id), $contact);
836 * @brief Checks if the contact is blocked
838 * @param int $cid contact id
840 * @return boolean Is the contact blocked?
842 public static function isBlocked($cid)
848 $blocked = dba::selectFirst('contact', ['blocked'], ['id' => $cid]);
849 if (!DBM::is_result($blocked)) {
852 return (bool) $blocked['blocked'];
856 * @brief Checks if the contact is hidden
858 * @param int $cid contact id
860 * @return boolean Is the contact hidden?
862 public static function isHidden($cid)
868 $hidden = dba::selectFirst('contact', ['hidden'], ['id' => $cid]);
869 if (!DBM::is_result($hidden)) {
872 return (bool) $hidden['hidden'];
876 * @brief Returns posts from a given contact url
878 * @param string $contact_url Contact URL
880 * @return string posts in HTML
882 public static function getPostsFromUrl($contact_url)
886 require_once 'include/conversation.php';
888 // There are no posts with "uid = 0" with connector networks
889 // This speeds up the query a lot
890 $r = q("SELECT `network`, `id` AS `author-id`, `contact-type` FROM `contact`
891 WHERE `contact`.`nurl` = '%s' AND `contact`.`uid` = 0",
892 dbesc(normalise_link($contact_url))
895 if (!DBM::is_result($r)) {
899 if (in_array($r[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) {
900 $sql = "(`item`.`uid` = 0 OR (`item`.`uid` = %d AND NOT `item`.`global`))";
902 $sql = "`item`.`uid` = %d";
905 $author_id = intval($r[0]["author-id"]);
907 $contact = ($r[0]["contact-type"] == ACCOUNT_TYPE_COMMUNITY ? 'owner-id' : 'author-id');
909 $r = q(item_query() . " AND `item`.`" . $contact . "` = %d AND " . $sql .
910 " AND `item`.`verb` = '%s' ORDER BY `item`.`created` DESC LIMIT %d, %d",
911 intval($author_id), intval(local_user()), dbesc(ACTIVITY_POST),
912 intval($a->pager['start']), intval($a->pager['itemspage'])
915 $o = conversation($a, $r, 'contact-posts', false);
917 $o .= alt_pager($a, count($r));
923 * @brief Returns the account type name
925 * The function can be called with either the user or the contact array
927 * @param array $contact contact or user array
930 public static function getAccountType(array $contact)
932 // There are several fields that indicate that the contact or user is a forum
933 // "page-flags" is a field in the user table,
934 // "forum" and "prv" are used in the contact table. They stand for PAGE_COMMUNITY and PAGE_PRVGROUP.
935 // "community" is used in the gcontact table and is true if the contact is PAGE_COMMUNITY or PAGE_PRVGROUP.
936 if ((isset($contact['page-flags']) && (intval($contact['page-flags']) == PAGE_COMMUNITY))
937 || (isset($contact['page-flags']) && (intval($contact['page-flags']) == PAGE_PRVGROUP))
938 || (isset($contact['forum']) && intval($contact['forum']))
939 || (isset($contact['prv']) && intval($contact['prv']))
940 || (isset($contact['community']) && intval($contact['community']))
942 $type = ACCOUNT_TYPE_COMMUNITY;
944 $type = ACCOUNT_TYPE_PERSON;
947 // The "contact-type" (contact table) and "account-type" (user table) are more general then the chaos from above.
948 if (isset($contact["contact-type"])) {
949 $type = $contact["contact-type"];
952 if (isset($contact["account-type"])) {
953 $type = $contact["account-type"];
957 case ACCOUNT_TYPE_ORGANISATION:
958 $account_type = t("Organisation");
960 case ACCOUNT_TYPE_NEWS:
961 $account_type = t('News');
963 case ACCOUNT_TYPE_COMMUNITY:
964 $account_type = t("Forum");
971 return $account_type;
975 * @brief Blocks a contact
980 public static function block($uid)
982 $return = dba::update('contact', ['blocked' => true], ['id' => $uid]);
988 * @brief Unblocks a contact
993 public static function unblock($uid)
995 $return = dba::update('contact', ['blocked' => false], ['id' => $uid]);
1001 * @brief Updates the avatar links in a contact only if needed
1003 * @param string $avatar Link to avatar picture
1004 * @param int $uid User id of contact owner
1005 * @param int $cid Contact id
1006 * @param bool $force force picture update
1008 * @return array Returns array of the different avatar sizes
1010 public static function updateAvatar($avatar, $uid, $cid, $force = false)
1012 $contact = dba::selectFirst('contact', ['avatar', 'photo', 'thumb', 'micro', 'nurl'], ['id' => $cid]);
1013 if (!DBM::is_result($contact)) {
1016 $data = array($contact["photo"], $contact["thumb"], $contact["micro"]);
1019 if (($contact["avatar"] != $avatar) || $force) {
1020 $photos = Photo::importProfilePhoto($avatar, $uid, $cid, true);
1025 array('avatar' => $avatar, 'photo' => $photos[0], 'thumb' => $photos[1], 'micro' => $photos[2], 'avatar-date' => datetime_convert()),
1029 // Update the public contact (contact id = 0)
1031 $pcontact = dba::selectFirst('contact', ['id'], ['nurl' => $contact['nurl']]);
1032 if (DBM::is_result($pcontact)) {
1033 self::updateAvatar($avatar, 0, $pcontact['id'], $force);
1045 * @param integer $id contact id
1048 public static function updateFromProbe($id)
1051 Warning: Never ever fetch the public key via Probe::uri and write it into the contacts.
1052 This will reliably kill your communication with Friendica contacts.
1055 $fields = ['url', 'nurl', 'addr', 'alias', 'batch', 'notify', 'poll', 'poco', 'network'];
1056 $contact = dba::selectFirst('contact', $fields, ['id' => $id]);
1057 if (!DBM::is_result($contact)) {
1061 $ret = Probe::uri($contact["url"]);
1063 // If Probe::uri fails the network code will be different
1064 if ($ret["network"] != $contact["network"]) {
1070 // make sure to not overwrite existing values with blank entries
1071 foreach ($ret as $key => $val) {
1072 if (isset($contact[$key]) && ($contact[$key] != "") && ($val == "")) {
1073 $ret[$key] = $contact[$key];
1076 if (isset($contact[$key]) && ($ret[$key] != $contact[$key])) {
1087 'url' => $ret['url'],
1088 'nurl' => normalise_link($ret['url']),
1089 'addr' => $ret['addr'],
1090 'alias' => $ret['alias'],
1091 'batch' => $ret['batch'],
1092 'notify' => $ret['notify'],
1093 'poll' => $ret['poll'],
1094 'poco' => $ret['poco']
1099 // Update the corresponding gcontact entry
1100 PortableContact::lastUpdated($ret["url"]);
1106 * Takes a $uid and a url/handle and adds a new contact
1107 * Currently if the contact is DFRN, interactive needs to be true, to redirect to the
1108 * dfrn_request page.
1110 * Otherwise this can be used to bulk add StatusNet contacts, Twitter contacts, etc.
1113 * $return['success'] boolean true if successful
1114 * $return['message'] error text if success is false.
1116 * @brief Takes a $uid and a url/handle and adds a new contact
1118 * @param string $url
1119 * @param bool $interactive
1120 * @param string $network
1121 * @return boolean|string
1123 public static function createFromProbe($uid, $url, $interactive = false, $network = '')
1125 $result = array('cid' => -1, 'success' => false, 'message' => '');
1129 // remove ajax junk, e.g. Twitter
1130 $url = str_replace('/#!/', '/', $url);
1132 if (!allowed_url($url)) {
1133 $result['message'] = t('Disallowed profile URL.');
1137 if (blocked_url($url)) {
1138 $result['message'] = t('Blocked domain');
1143 $result['message'] = t('Connect URL missing.');
1147 $arr = array('url' => $url, 'contact' => array());
1149 call_hooks('follow', $arr);
1151 if (x($arr['contact'], 'name')) {
1152 $ret = $arr['contact'];
1154 $ret = Probe::uri($url, $network, $uid, false);
1157 if (($network != '') && ($ret['network'] != $network)) {
1158 logger('Expected network ' . $network . ' does not match actual network ' . $ret['network']);
1162 if ($ret['network'] === NETWORK_DFRN) {
1164 if (strlen($a->path)) {
1165 $myaddr = bin2hex(System::baseUrl() . '/profile/' . $a->user['nickname']);
1167 $myaddr = bin2hex($a->user['nickname'] . '@' . $a->get_hostname());
1170 goaway($ret['request'] . "&addr=$myaddr");
1174 } elseif (Config::get('system', 'dfrn_only')) {
1175 $result['message'] = t('This site is not configured to allow communications with other networks.') . EOL;
1176 $result['message'] != t('No compatible communication protocols or feeds were discovered.') . EOL;
1180 // This extra param just confuses things, remove it
1181 if ($ret['network'] === NETWORK_DIASPORA) {
1182 $ret['url'] = str_replace('?absolute=true', '', $ret['url']);
1185 // do we have enough information?
1187 if (!((x($ret, 'name')) && (x($ret, 'poll')) && ((x($ret, 'url')) || (x($ret, 'addr'))))) {
1188 $result['message'] .= t('The profile address specified does not provide adequate information.') . EOL;
1189 if (!x($ret, 'poll')) {
1190 $result['message'] .= t('No compatible communication protocols or feeds were discovered.') . EOL;
1192 if (!x($ret, 'name')) {
1193 $result['message'] .= t('An author or name was not found.') . EOL;
1195 if (!x($ret, 'url')) {
1196 $result['message'] .= t('No browser URL could be matched to this address.') . EOL;
1198 if (strpos($url, '@') !== false) {
1199 $result['message'] .= t('Unable to match @-style Identity Address with a known protocol or email contact.') . EOL;
1200 $result['message'] .= t('Use mailto: in front of address to force email check.') . EOL;
1205 if ($ret['network'] === NETWORK_OSTATUS && Config::get('system', 'ostatus_disabled')) {
1206 $result['message'] .= t('The profile address specified belongs to a network which has been disabled on this site.') . EOL;
1207 $ret['notify'] = '';
1210 if (!$ret['notify']) {
1211 $result['message'] .= t('Limited profile. This person will be unable to receive direct/personal notifications from you.') . EOL;
1214 $writeable = ((($ret['network'] === NETWORK_OSTATUS) && ($ret['notify'])) ? 1 : 0);
1216 $subhub = (($ret['network'] === NETWORK_OSTATUS) ? true : false);
1218 $hidden = (($ret['network'] === NETWORK_MAIL) ? 1 : 0);
1220 if (in_array($ret['network'], array(NETWORK_MAIL, NETWORK_DIASPORA))) {
1224 // check if we already have a contact
1225 // the poll url is more reliable than the profile url, as we may have
1226 // indirect links or webfinger links
1228 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `poll` IN ('%s', '%s') AND `network` = '%s' LIMIT 1",
1230 dbesc($ret['poll']),
1231 dbesc(normalise_link($ret['poll'])),
1232 dbesc($ret['network'])
1235 if (!DBM::is_result($r)) {
1236 $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `nurl` = '%s' AND `network` = '%s' LIMIT 1",
1238 dbesc(normalise_link($url)),
1239 dbesc($ret['network'])
1243 if (DBM::is_result($r)) {
1245 $new_relation = (($r[0]['rel'] == CONTACT_IS_FOLLOWER) ? CONTACT_IS_FRIEND : CONTACT_IS_SHARING);
1247 $fields = array('rel' => $new_relation, 'subhub' => $subhub, 'readonly' => false);
1248 dba::update('contact', $fields, array('id' => $r[0]['id']));
1250 $new_relation = ((in_array($ret['network'], array(NETWORK_MAIL))) ? CONTACT_IS_FRIEND : CONTACT_IS_SHARING);
1252 // create contact record
1253 dba::insert('contact', [
1255 'created' => datetime_convert(),
1256 'url' => $ret['url'],
1257 'nurl' => normalise_link($ret['url']),
1258 'addr' => $ret['addr'],
1259 'alias' => $ret['alias'],
1260 'batch' => $ret['batch'],
1261 'notify' => $ret['notify'],
1262 'poll' => $ret['poll'],
1263 'poco' => $ret['poco'],
1264 'name' => $ret['name'],
1265 'nick' => $ret['nick'],
1266 'network' => $ret['network'],
1267 'pubkey' => $ret['pubkey'],
1268 'rel' => $new_relation,
1269 'priority'=> $ret['priority'],
1270 'writable'=> $writeable,
1271 'hidden' => $hidden,
1279 $contact = dba::selectFirst('contact', [], ['url' => $ret['url'], 'network' => $ret['network'], 'uid' => $uid]);
1280 if (!DBM::is_result($contact)) {
1281 $result['message'] .= t('Unable to retrieve contact information.') . EOL;
1285 $contact_id = $contact['id'];
1286 $result['cid'] = $contact_id;
1288 Group::addMember(User::getDefaultGroup($uid, $contact["network"]), $contact_id);
1290 // Update the avatar
1291 self::updateAvatar($ret['photo'], $uid, $contact_id);
1293 // pull feed and consume it, which should subscribe to the hub.
1295 Worker::add(PRIORITY_HIGH, "OnePoll", $contact_id, "force");
1297 $r = q("SELECT `contact`.*, `user`.* FROM `contact` INNER JOIN `user` ON `contact`.`uid` = `user`.`uid`
1298 WHERE `user`.`uid` = %d AND `contact`.`self` LIMIT 1",
1302 if (DBM::is_result($r)) {
1303 if (($contact['network'] == NETWORK_OSTATUS) && (strlen($contact['notify']))) {
1304 // create a follow slap
1306 $item['verb'] = ACTIVITY_FOLLOW;
1307 $item['follow'] = $contact["url"];
1308 $slap = OStatus::salmon($item, $r[0]);
1309 Salmon::slapper($r[0], $contact['notify'], $slap);
1312 if ($contact['network'] == NETWORK_DIASPORA) {
1313 $ret = Diaspora::sendShare($a->user, $contact);
1314 logger('share returns: ' . $ret);
1318 $result['success'] = true;