4 * @file src/Model/GlobalContact.php
5 * @brief This file includes the GlobalContact class with directory related functions
7 namespace Friendica\Model;
10 use Friendica\Core\Config;
11 use Friendica\Core\Protocol;
12 use Friendica\Core\System;
13 use Friendica\Core\Worker;
14 use Friendica\Database\DBA;
15 use Friendica\Network\Probe;
16 use Friendica\Protocol\PortableContact;
17 use Friendica\Util\DateTimeFormat;
18 use Friendica\Util\Network;
20 require_once 'include/dba.php';
23 * @brief This class handles GlobalContact related functions
28 * @brief Search global contact table by nick or name
30 * @param string $search Name or nick
31 * @param string $mode Search mode (e.g. "community")
33 * @return array with search results
35 public static function searchByName($search, $mode = '')
41 // check supported networks
42 if (Config::get('system', 'diaspora_enabled')) {
43 $diaspora = Protocol::DIASPORA;
45 $diaspora = Protocol::DFRN;
48 if (!Config::get('system', 'ostatus_disabled')) {
49 $ostatus = Protocol::OSTATUS;
51 $ostatus = Protocol::DFRN;
54 // check if we search only communities or every contact
55 if ($mode === "community") {
56 $extra_sql = " AND `community`";
63 $results = DBA::p("SELECT `nurl` FROM `gcontact`
64 WHERE NOT `hide` AND `network` IN (?, ?, ?) AND
65 ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) AND
66 (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql
67 GROUP BY `nurl` ORDER BY `nurl` DESC LIMIT 1000",
68 Protocol::DFRN, $ostatus, $diaspora, $search, $search, $search
72 while ($result = DBA::fetch($results)) {
73 $urlparts = parse_url($result["nurl"]);
75 // Ignore results that look strange.
76 // For historic reasons the gcontact table does contain some garbage.
77 if (!empty($urlparts['query']) || !empty($urlparts['fragment'])) {
81 $gcontacts[] = Contact::getDetailsByURL($result["nurl"], local_user());
87 * @brief Link the gcontact entry with user, contact and global contact
89 * @param integer $gcid Global contact ID
90 * @param integer $uid User ID
91 * @param integer $cid Contact ID
92 * @param integer $zcid Global Contact ID
95 public static function link($gcid, $uid = 0, $cid = 0, $zcid = 0)
101 $condition = ['cid' => $cid, 'uid' => $uid, 'gcid' => $gcid, 'zcid' => $zcid];
102 DBA::update('glink', ['updated' => DateTimeFormat::utcNow()], $condition, true);
106 * @brief Sanitize the given gcontact data
108 * @param array $gcontact array with gcontact data
113 * 1: Profiles on this server
114 * 2: Contacts of profiles on this server
115 * 3: Contacts of contacts of profiles on this server
117 * @return array $gcontact
119 public static function sanitize($gcontact)
121 if ($gcontact['url'] == "") {
122 throw new Exception('URL is empty');
125 $urlparts = parse_url($gcontact['url']);
126 if (!isset($urlparts["scheme"])) {
127 throw new Exception("This (".$gcontact['url'].") doesn't seem to be an url.");
130 if (in_array($urlparts["host"], ["twitter.com", "identi.ca"])) {
131 throw new Exception('Contact from a non federated network ignored. ('.$gcontact['url'].')');
134 // Don't store the statusnet connector as network
135 // We can't simply set this to Protocol::OSTATUS since the connector could have fetched posts from friendica as well
136 if ($gcontact['network'] == Protocol::STATUSNET) {
137 $gcontact['network'] = "";
140 // Assure that there are no parameter fragments in the profile url
141 if (in_array($gcontact['network'], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
142 $gcontact['url'] = self::cleanContactUrl($gcontact['url']);
145 $alternate = PortableContact::alternateOStatusUrl($gcontact['url']);
147 // The global contacts should contain the original picture, not the cached one
148 if (($gcontact['generation'] != 1) && stristr(normalise_link($gcontact['photo']), normalise_link(System::baseUrl()."/photo/"))) {
149 $gcontact['photo'] = "";
152 if (!isset($gcontact['network'])) {
153 $condition = ["`uid` = 0 AND `nurl` = ? AND `network` != '' AND `network` != ?",
154 normalise_link($gcontact['url']), Protocol::STATUSNET];
155 $contact = DBA::selectFirst('contact', ['network'], $condition);
156 if (DBA::isResult($contact)) {
157 $gcontact['network'] = $contact["network"];
160 if (($gcontact['network'] == "") || ($gcontact['network'] == Protocol::OSTATUS)) {
161 $condition = ["`uid` = 0 AND `alias` IN (?, ?) AND `network` != '' AND `network` != ?",
162 $gcontact['url'], normalise_link($gcontact['url']), Protocol::STATUSNET];
163 $contact = DBA::selectFirst('contact', ['network'], $condition);
164 if (DBA::isResult($contact)) {
165 $gcontact['network'] = $contact["network"];
170 $gcontact['server_url'] = '';
171 $gcontact['network'] = '';
173 $fields = ['network', 'updated', 'server_url', 'url', 'addr'];
174 $gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => normalise_link($gcontact['url'])]);
175 if (DBA::isResult($gcnt)) {
176 if (!isset($gcontact['network']) && ($gcnt["network"] != Protocol::STATUSNET)) {
177 $gcontact['network'] = $gcnt["network"];
179 if ($gcontact['updated'] <= NULL_DATE) {
180 $gcontact['updated'] = $gcnt["updated"];
182 if (!isset($gcontact['server_url']) && (normalise_link($gcnt["server_url"]) != normalise_link($gcnt["url"]))) {
183 $gcontact['server_url'] = $gcnt["server_url"];
185 if (!isset($gcontact['addr'])) {
186 $gcontact['addr'] = $gcnt["addr"];
190 if ((!isset($gcontact['network']) || !isset($gcontact['name']) || !isset($gcontact['addr']) || !isset($gcontact['photo']) || !isset($gcontact['server_url']) || $alternate)
191 && PortableContact::reachable($gcontact['url'], $gcontact['server_url'], $gcontact['network'], false)
193 $data = Probe::uri($gcontact['url']);
195 if ($data["network"] == Protocol::PHANTOM) {
196 throw new Exception('Probing for URL '.$gcontact['url'].' failed');
199 $orig_profile = $gcontact['url'];
201 $gcontact["server_url"] = $data["baseurl"];
203 $gcontact = array_merge($gcontact, $data);
205 if ($alternate && ($gcontact['network'] == Protocol::OSTATUS)) {
206 // Delete the old entry - if it exists
207 if (DBA::exists('gcontact', ['nurl' => normalise_link($orig_profile)])) {
208 DBA::delete('gcontact', ['nurl' => normalise_link($orig_profile)]);
213 if (!isset($gcontact['name']) || !isset($gcontact['photo'])) {
214 throw new Exception('No name and photo for URL '.$gcontact['url']);
217 if (!in_array($gcontact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
218 throw new Exception('No federated network ('.$gcontact['network'].') detected for URL '.$gcontact['url']);
221 if (!isset($gcontact['server_url'])) {
222 // We check the server url to be sure that it is a real one
223 $server_url = PortableContact::detectServer($gcontact['url']);
225 // We are now sure that it is a correct URL. So we use it in the future
226 if ($server_url != "") {
227 $gcontact['server_url'] = $server_url;
231 // The server URL doesn't seem to be valid, so we don't store it.
232 if (!PortableContact::checkServer($gcontact['server_url'], $gcontact['network'])) {
233 $gcontact['server_url'] = "";
240 * @param integer $uid id
241 * @param integer $cid id
244 public static function countCommonFriends($uid, $cid)
247 "SELECT count(*) as `total`
248 FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
249 WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
250 ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR
251 (`gcontact`.`updated` >= `gcontact`.`last_failure`))
252 AND `gcontact`.`nurl` IN (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 and id != %d ) ",
259 // logger("countCommonFriends: $uid $cid {$r[0]['total']}");
260 if (DBA::isResult($r)) {
261 return $r[0]['total'];
267 * @param integer $uid id
268 * @param integer $zcid zcid
271 public static function countCommonFriendsZcid($uid, $zcid)
274 "SELECT count(*) as `total`
275 FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
276 where `glink`.`zcid` = %d
277 and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 ) ",
282 if (DBA::isResult($r)) {
283 return $r[0]['total'];
290 * @param integer $uid user
291 * @param integer $cid cid
292 * @param integer $start optional, default 0
293 * @param integer $limit optional, default 9999
294 * @param boolean $shuffle optional, default false
297 public static function commonFriends($uid, $cid, $start = 0, $limit = 9999, $shuffle = false)
300 $sql_extra = " order by rand() ";
302 $sql_extra = " order by `gcontact`.`name` asc ";
306 "SELECT `gcontact`.*, `contact`.`id` AS `cid`
308 INNER JOIN `gcontact` ON `glink`.`gcid` = `gcontact`.`id`
309 INNER JOIN `contact` ON `gcontact`.`nurl` = `contact`.`nurl`
310 WHERE `glink`.`cid` = %d and `glink`.`uid` = %d
311 AND `contact`.`uid` = %d AND `contact`.`self` = 0 AND `contact`.`blocked` = 0
312 AND `contact`.`hidden` = 0 AND `contact`.`id` != %d
313 AND ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
314 $sql_extra LIMIT %d, %d",
323 /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
328 * @param integer $uid user
329 * @param integer $zcid zcid
330 * @param integer $start optional, default 0
331 * @param integer $limit optional, default 9999
332 * @param boolean $shuffle optional, default false
335 public static function commonFriendsZcid($uid, $zcid, $start = 0, $limit = 9999, $shuffle = false)
338 $sql_extra = " order by rand() ";
340 $sql_extra = " order by `gcontact`.`name` asc ";
345 FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
346 where `glink`.`zcid` = %d
347 and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 )
348 $sql_extra limit %d, %d",
355 /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
360 * @param integer $uid user
361 * @param integer $cid cid
364 public static function countAllFriends($uid, $cid)
367 "SELECT count(*) as `total`
368 FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
369 where `glink`.`cid` = %d and `glink`.`uid` = %d AND
370 ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))",
375 if (DBA::isResult($r)) {
376 return $r[0]['total'];
383 * @param integer $uid user
384 * @param integer $cid cid
385 * @param integer $start optional, default 0
386 * @param integer $limit optional, default 80
389 public static function allFriends($uid, $cid, $start = 0, $limit = 80)
392 "SELECT `gcontact`.*, `contact`.`id` AS `cid`
394 INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
395 LEFT JOIN `contact` ON `contact`.`nurl` = `gcontact`.`nurl` AND `contact`.`uid` = %d
396 WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
397 ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
398 ORDER BY `gcontact`.`name` ASC LIMIT %d, %d ",
406 /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
411 * @param object $uid user
412 * @param integer $start optional, default 0
413 * @param integer $limit optional, default 80
416 public static function suggestionQuery($uid, $start = 0, $limit = 80)
423 * Uncommented because the result of the queries are to big to store it in the cache.
424 * We need to decide if we want to change the db column type or if we want to delete it.
426 //$list = Cache::get("suggestion_query:".$uid.":".$start.":".$limit);
427 //if (!is_null($list)) {
431 $network = [Protocol::DFRN];
433 if (Config::get('system', 'diaspora_enabled')) {
434 $network[] = Protocol::DIASPORA;
437 if (!Config::get('system', 'ostatus_disabled')) {
438 $network[] = Protocol::OSTATUS;
441 $sql_network = implode("', '", $network);
442 $sql_network = "'".$sql_network."'";
444 /// @todo This query is really slow
445 // By now we cache the data for five minutes
447 "SELECT count(glink.gcid) as `total`, gcontact.* from gcontact
448 INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
449 where uid = %d and not gcontact.nurl in ( select nurl from contact where uid = %d )
450 AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
451 AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
452 AND `gcontact`.`updated` >= '%s'
453 AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
454 AND `gcontact`.`network` IN (%s)
455 GROUP BY `glink`.`gcid` ORDER BY `gcontact`.`updated` DESC,`total` DESC LIMIT %d, %d",
460 DBA::escape(NULL_DATE),
466 if (DBA::isResult($r) && count($r) >= ($limit -1)) {
468 * Uncommented because the result of the queries are to big to store it in the cache.
469 * We need to decide if we want to change the db column type or if we want to delete it.
471 //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $r, CACHE_FIVE_MINUTES);
477 "SELECT gcontact.* FROM gcontact
478 INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
479 WHERE `glink`.`uid` = 0 AND `glink`.`cid` = 0 AND `glink`.`zcid` = 0 AND NOT `gcontact`.`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = %d)
480 AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
481 AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
482 AND `gcontact`.`updated` >= '%s'
483 AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
484 AND `gcontact`.`network` IN (%s)
485 ORDER BY rand() LIMIT %d, %d",
489 DBA::escape(NULL_DATE),
496 foreach ($r2 as $suggestion) {
497 $list[$suggestion["nurl"]] = $suggestion;
500 foreach ($r as $suggestion) {
501 $list[$suggestion["nurl"]] = $suggestion;
504 while (sizeof($list) > ($limit)) {
509 * Uncommented because the result of the queries are to big to store it in the cache.
510 * We need to decide if we want to change the db column type or if we want to delete it.
512 //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $list, CACHE_FIVE_MINUTES);
519 public static function updateSuggestions()
525 /// @TODO Check if it is really neccessary to poll the own server
526 PortableContact::loadWorker(0, 0, 0, System::baseUrl() . '/poco');
528 $done[] = System::baseUrl() . '/poco';
530 if (strlen(Config::get('system', 'directory'))) {
531 $x = Network::fetchUrl(get_server()."/pubsites");
533 $j = json_decode($x);
534 if (!empty($j->entries)) {
535 foreach ($j->entries as $entry) {
536 PortableContact::checkServer($entry->url);
538 $url = $entry->url . '/poco';
539 if (!in_array($url, $done)) {
540 PortableContact::loadWorker(0, 0, 0, $url);
548 // Query your contacts from Friendica and Redmatrix/Hubzilla for their contacts
550 "SELECT DISTINCT(`poco`) AS `poco` FROM `contact` WHERE `network` IN ('%s', '%s')",
551 DBA::escape(Protocol::DFRN),
552 DBA::escape(Protocol::DIASPORA)
555 if (DBA::isResult($r)) {
556 foreach ($r as $rr) {
557 $base = substr($rr['poco'], 0, strrpos($rr['poco'], '/'));
558 if (! in_array($base, $done)) {
559 PortableContact::loadWorker(0, 0, 0, $base);
566 * @brief Removes unwanted parts from a contact url
568 * @param string $url Contact url
570 * @return string Contact url with the wanted parts
572 public static function cleanContactUrl($url)
574 $parts = parse_url($url);
576 if (!isset($parts["scheme"]) || !isset($parts["host"])) {
580 $new_url = $parts["scheme"]."://".$parts["host"];
582 if (isset($parts["port"])) {
583 $new_url .= ":".$parts["port"];
586 if (isset($parts["path"])) {
587 $new_url .= $parts["path"];
590 if ($new_url != $url) {
591 logger("Cleaned contact url ".$url." to ".$new_url." - Called by: ".System::callstack(), LOGGER_DEBUG);
598 * @brief Replace alternate OStatus user format with the primary one
600 * @param array $contact contact array (called by reference)
603 public static function fixAlternateContactAddress(&$contact)
605 if (($contact["network"] == Protocol::OSTATUS) && PortableContact::alternateOStatusUrl($contact["url"])) {
606 $data = Probe::uri($contact["url"]);
607 if ($contact["network"] == Protocol::OSTATUS) {
608 logger("Fix primary url from ".$contact["url"]." to ".$data["url"]." - Called by: ".System::callstack(), LOGGER_DEBUG);
609 $contact["url"] = $data["url"];
610 $contact["addr"] = $data["addr"];
611 $contact["alias"] = $data["alias"];
612 $contact["server_url"] = $data["baseurl"];
618 * @brief Fetch the gcontact id, add an entry if not existed
620 * @param array $contact contact array
622 * @return bool|int Returns false if not found, integer if contact was found
624 public static function getId($contact)
628 $last_failure_str = '';
629 $last_contact_str = '';
631 if (empty($contact["network"])) {
632 logger("Empty network for contact url ".$contact["url"]." - Called by: ".System::callstack(), LOGGER_DEBUG);
636 if (in_array($contact["network"], [Protocol::PHANTOM])) {
637 logger("Invalid network for contact url ".$contact["url"]." - Called by: ".System::callstack(), LOGGER_DEBUG);
641 if ($contact["network"] == Protocol::STATUSNET) {
642 $contact["network"] = Protocol::OSTATUS;
645 // All new contacts are hidden by default
646 if (!isset($contact["hide"])) {
647 $contact["hide"] = true;
650 // Replace alternate OStatus user format with the primary one
651 self::fixAlternateContactAddress($contact);
653 // Remove unwanted parts from the contact url (e.g. "?zrl=...")
654 if (in_array($contact["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
655 $contact["url"] = self::cleanContactUrl($contact["url"]);
658 DBA::lock('gcontact');
659 $fields = ['id', 'last_contact', 'last_failure', 'network'];
660 $gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => normalise_link($contact["url"])]);
661 if (DBA::isResult($gcnt)) {
662 $gcontact_id = $gcnt["id"];
664 // Update every 90 days
665 if (in_array($gcnt["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
666 $last_failure_str = $gcnt["last_failure"];
667 $last_failure = strtotime($gcnt["last_failure"]);
668 $last_contact_str = $gcnt["last_contact"];
669 $last_contact = strtotime($gcnt["last_contact"]);
670 $doprobing = (((time() - $last_contact) > (90 * 86400)) && ((time() - $last_failure) > (90 * 86400)));
673 $contact['location'] = defaults($contact, 'location', '');
674 $contact['about'] = defaults($contact, 'about', '');
675 $contact['generation'] = defaults($contact, 'generation', 0);
678 "INSERT INTO `gcontact` (`name`, `nick`, `addr` , `network`, `url`, `nurl`, `photo`, `created`, `updated`, `location`, `about`, `hide`, `generation`)
679 VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d)",
680 DBA::escape($contact["name"]),
681 DBA::escape($contact["nick"]),
682 DBA::escape($contact["addr"]),
683 DBA::escape($contact["network"]),
684 DBA::escape($contact["url"]),
685 DBA::escape(normalise_link($contact["url"])),
686 DBA::escape($contact["photo"]),
687 DBA::escape(DateTimeFormat::utcNow()),
688 DBA::escape(DateTimeFormat::utcNow()),
689 DBA::escape($contact["location"]),
690 DBA::escape($contact["about"]),
691 intval($contact["hide"]),
692 intval($contact["generation"])
695 $condition = ['nurl' => normalise_link($contact["url"])];
696 $cnt = DBA::selectFirst('gcontact', ['id', 'network'], $condition, ['order' => ['id']]);
697 if (DBA::isResult($cnt)) {
698 $gcontact_id = $cnt["id"];
699 $doprobing = in_array($cnt["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""]);
705 logger("Last Contact: ". $last_contact_str." - Last Failure: ".$last_failure_str." - Checking: ".$contact["url"], LOGGER_DEBUG);
706 Worker::add(PRIORITY_LOW, 'GProbe', $contact["url"]);
713 * @brief Updates the gcontact table from a given array
715 * @param array $contact contact array
717 * @return bool|int Returns false if not found, integer if contact was found
719 public static function update($contact)
721 // Check for invalid "contact-type" value
722 if (isset($contact['contact-type']) && (intval($contact['contact-type']) < 0)) {
723 $contact['contact-type'] = 0;
726 /// @todo update contact table as well
728 $gcontact_id = self::getId($contact);
735 "SELECT `name`, `nick`, `photo`, `location`, `about`, `addr`, `generation`, `birthday`, `gender`, `keywords`,
736 `contact-type`, `hide`, `nsfw`, `network`, `alias`, `notify`, `server_url`, `connect`, `updated`, `url`
737 FROM `gcontact` WHERE `id` = %d LIMIT 1",
741 // Get all field names
743 foreach ($public_contact[0] as $field => $data) {
744 $fields[$field] = $data;
747 unset($fields["url"]);
748 unset($fields["updated"]);
749 unset($fields["hide"]);
751 // Bugfix: We had an error in the storing of keywords which lead to the "0"
752 // This value is still transmitted via poco.
753 if (!empty($contact["keywords"]) && ($contact["keywords"] == "0")) {
754 unset($contact["keywords"]);
757 if (!empty($public_contact[0]["keywords"]) && ($public_contact[0]["keywords"] == "0")) {
758 $public_contact[0]["keywords"] = "";
761 // assign all unassigned fields from the database entry
762 foreach ($fields as $field => $data) {
763 if (!isset($contact[$field]) || ($contact[$field] == "")) {
764 $contact[$field] = $public_contact[0][$field];
768 if (!isset($contact["hide"])) {
769 $contact["hide"] = $public_contact[0]["hide"];
772 $fields["hide"] = $public_contact[0]["hide"];
774 if ($contact["network"] == Protocol::STATUSNET) {
775 $contact["network"] = Protocol::OSTATUS;
778 // Replace alternate OStatus user format with the primary one
779 self::fixAlternateContactAddress($contact);
781 if (!isset($contact["updated"])) {
782 $contact["updated"] = DateTimeFormat::utcNow();
785 if ($contact["network"] == Protocol::TWITTER) {
786 $contact["server_url"] = 'http://twitter.com';
789 if ($contact["server_url"] == "") {
790 $data = Probe::uri($contact["url"]);
791 if ($data["network"] != Protocol::PHANTOM) {
792 $contact["server_url"] = $data['baseurl'];
795 $contact["server_url"] = normalise_link($contact["server_url"]);
798 if (($contact["addr"] == "") && ($contact["server_url"] != "") && ($contact["nick"] != "")) {
799 $hostname = str_replace("http://", "", $contact["server_url"]);
800 $contact["addr"] = $contact["nick"]."@".$hostname;
803 // Check if any field changed
805 unset($fields["generation"]);
807 if ((($contact["generation"] > 0) && ($contact["generation"] <= $public_contact[0]["generation"])) || ($public_contact[0]["generation"] == 0)) {
808 foreach ($fields as $field => $data) {
809 if ($contact[$field] != $public_contact[0][$field]) {
810 logger("Difference for contact ".$contact["url"]." in field '".$field."'. New value: '".$contact[$field]."', old value '".$public_contact[0][$field]."'", LOGGER_DEBUG);
815 if ($contact["generation"] < $public_contact[0]["generation"]) {
816 logger("Difference for contact ".$contact["url"]." in field 'generation'. new value: '".$contact["generation"]."', old value '".$public_contact[0]["generation"]."'", LOGGER_DEBUG);
822 logger("Update gcontact for ".$contact["url"], LOGGER_DEBUG);
823 $condition = ['`nurl` = ? AND (`generation` = 0 OR `generation` >= ?)',
824 normalise_link($contact["url"]), $contact["generation"]];
825 $contact["updated"] = DateTimeFormat::utc($contact["updated"]);
827 $updated = ['photo' => $contact['photo'], 'name' => $contact['name'],
828 'nick' => $contact['nick'], 'addr' => $contact['addr'],
829 'network' => $contact['network'], 'birthday' => $contact['birthday'],
830 'gender' => $contact['gender'], 'keywords' => $contact['keywords'],
831 'hide' => $contact['hide'], 'nsfw' => $contact['nsfw'],
832 'contact-type' => $contact['contact-type'], 'alias' => $contact['alias'],
833 'notify' => $contact['notify'], 'url' => $contact['url'],
834 'location' => $contact['location'], 'about' => $contact['about'],
835 'generation' => $contact['generation'], 'updated' => $contact['updated'],
836 'server_url' => $contact['server_url'], 'connect' => $contact['connect']];
838 DBA::update('gcontact', $updated, $condition, $fields);
840 // Now update the contact entry with the user id "0" as well.
841 // This is used for the shadow copies of public items.
842 /// @todo Check if we really should do this.
843 // The quality of the gcontact table is mostly lower than the public contact
844 $public_contact = DBA::selectFirst('contact', ['id'], ['nurl' => normalise_link($contact["url"]), 'uid' => 0]);
845 if (DBA::isResult($public_contact)) {
846 logger("Update public contact ".$public_contact["id"], LOGGER_DEBUG);
848 Contact::updateAvatar($contact["photo"], 0, $public_contact["id"]);
850 $fields = ['name', 'nick', 'addr',
851 'network', 'bd', 'gender',
852 'keywords', 'alias', 'contact-type',
853 'url', 'location', 'about'];
854 $old_contact = DBA::selectFirst('contact', $fields, ['id' => $public_contact["id"]]);
856 // Update it with the current values
857 $fields = ['name' => $contact['name'], 'nick' => $contact['nick'],
858 'addr' => $contact['addr'], 'network' => $contact['network'],
859 'bd' => $contact['birthday'], 'gender' => $contact['gender'],
860 'keywords' => $contact['keywords'], 'alias' => $contact['alias'],
861 'contact-type' => $contact['contact-type'], 'url' => $contact['url'],
862 'location' => $contact['location'], 'about' => $contact['about']];
864 // Don't update the birthday field if not set or invalid
865 if (empty($contact['birthday']) || ($contact['birthday'] < '0001-01-01')) {
866 unset($fields['bd']);
870 DBA::update('contact', $fields, ['id' => $public_contact["id"]], $old_contact);
878 * @brief Updates the gcontact entry from probe
880 * @param string $url profile link
883 public static function updateFromProbe($url)
885 $data = Probe::uri($url);
887 if (in_array($data["network"], [Protocol::PHANTOM])) {
888 logger("Invalid network for contact url ".$data["url"]." - Called by: ".System::callstack(), LOGGER_DEBUG);
892 $data["server_url"] = $data["baseurl"];
898 * @brief Update the gcontact entry for a given user id
900 * @param int $uid User ID
903 public static function updateForUser($uid)
906 "SELECT `profile`.`locality`, `profile`.`region`, `profile`.`country-name`,
907 `profile`.`name`, `profile`.`about`, `profile`.`gender`,
908 `profile`.`pub_keywords`, `profile`.`dob`, `profile`.`photo`,
909 `profile`.`net-publish`, `user`.`nickname`, `user`.`hidewall`,
910 `contact`.`notify`, `contact`.`url`, `contact`.`addr`
912 INNER JOIN `user` ON `user`.`uid` = `profile`.`uid`
913 INNER JOIN `contact` ON `contact`.`uid` = `profile`.`uid`
914 WHERE `profile`.`uid` = %d AND `profile`.`is-default` AND `contact`.`self`",
918 $location = Profile::formatLocation(
919 ["locality" => $r[0]["locality"], "region" => $r[0]["region"], "country-name" => $r[0]["country-name"]]
922 // The "addr" field was added in 3.4.3 so it can be empty for older users
923 if ($r[0]["addr"] != "") {
924 $addr = $r[0]["nickname"].'@'.str_replace(["http://", "https://"], "", System::baseUrl());
926 $addr = $r[0]["addr"];
929 $gcontact = ["name" => $r[0]["name"], "location" => $location, "about" => $r[0]["about"],
930 "gender" => $r[0]["gender"], "keywords" => $r[0]["pub_keywords"],
931 "birthday" => $r[0]["dob"], "photo" => $r[0]["photo"],
932 "notify" => $r[0]["notify"], "url" => $r[0]["url"],
933 "hide" => ($r[0]["hidewall"] || !$r[0]["net-publish"]),
934 "nick" => $r[0]["nickname"], "addr" => $addr,
935 "connect" => $addr, "server_url" => System::baseUrl(),
936 "generation" => 1, "network" => Protocol::DFRN];
938 self::update($gcontact);
942 * @brief Fetches users of given GNU Social server
944 * If the "Statistics" addon is enabled (See http://gstools.org/ for details) we query user data with this.
946 * @param string $server Server address
949 public static function fetchGsUsers($server)
951 logger("Fetching users from GNU Social server ".$server, LOGGER_DEBUG);
953 $url = $server."/main/statistics";
955 $result = Network::curl($url);
956 if (!$result["success"]) {
960 $statistics = json_decode($result["body"]);
962 if (!empty($statistics->config)) {
963 if ($statistics->config->instance_with_ssl) {
964 $server = "https://";
969 $server .= $statistics->config->instance_address;
971 $hostname = $statistics->config->instance_address;
972 } elseif (!empty($statistics)) {
973 if ($statistics->instance_with_ssl) {
974 $server = "https://";
979 $server .= $statistics->instance_address;
981 $hostname = $statistics->instance_address;
984 if (!empty($statistics->users)) {
985 foreach ($statistics->users as $nick => $user) {
986 $profile_url = $server."/".$user->nickname;
988 $contact = ["url" => $profile_url,
989 "name" => $user->fullname,
990 "addr" => $user->nickname."@".$hostname,
991 "nick" => $user->nickname,
992 "network" => Protocol::OSTATUS,
993 "photo" => System::baseUrl()."/images/person-175.jpg"];
995 if (isset($user->bio)) {
996 $contact["about"] = $user->bio;
999 self::getId($contact);
1005 * @brief Asking GNU Social server on a regular base for their user data
1008 public static function discoverGsUsers()
1010 $requery_days = intval(Config::get("system", "poco_requery_days"));
1012 $last_update = date("c", time() - (60 * 60 * 24 * $requery_days));
1015 "SELECT `nurl`, `url` FROM `gserver` WHERE `last_contact` >= `last_failure` AND `network` = '%s' AND `last_poco_query` < '%s' ORDER BY RAND() LIMIT 5",
1016 DBA::escape(Protocol::OSTATUS),
1017 DBA::escape($last_update)
1020 if (!DBA::isResult($r)) {
1024 foreach ($r as $server) {
1025 self::fetchGsUsers($server["url"]);
1026 q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", DBA::escape(DateTimeFormat::utcNow()), DBA::escape($server["nurl"]));
1033 public static function getRandomUrl()
1036 "SELECT `url` FROM `gcontact` WHERE `network` = '%s'
1037 AND `last_contact` >= `last_failure`
1038 AND `updated` > UTC_TIMESTAMP - INTERVAL 1 MONTH
1039 ORDER BY rand() LIMIT 1",
1040 DBA::escape(Protocol::DFRN)
1043 if (DBA::isResult($r)) {
1044 return dirname($r[0]['url']);