]> git.mxchange.org Git - friendica.git/blob - src/Model/GContact.php
Merge remote-tracking branch 'upstream/develop' into aria
[friendica.git] / src / Model / GContact.php
1 <?php
2
3 /**
4  * @file src/Model/GlobalContact.php
5  * @brief This file includes the GlobalContact class with directory related functions
6  */
7 namespace Friendica\Model;
8
9 use DOMDocument;
10 use DOMXPath;
11 use Exception;
12 use Friendica\Core\Config;
13 use Friendica\Core\Logger;
14 use Friendica\Core\Protocol;
15 use Friendica\Core\System;
16 use Friendica\Core\Worker;
17 use Friendica\Database\DBA;
18 use Friendica\Network\Probe;
19 use Friendica\Protocol\ActivityPub;
20 use Friendica\Protocol\PortableContact;
21 use Friendica\Util\DateTimeFormat;
22 use Friendica\Util\Network;
23 use Friendica\Util\Strings;
24
25 /**
26  * @brief This class handles GlobalContact related functions
27  */
28 class GContact
29 {
30         /**
31          * @brief Search global contact table by nick or name
32          *
33          * @param string $search Name or nick
34          * @param string $mode   Search mode (e.g. "community")
35          *
36          * @return array with search results
37          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
38          */
39         public static function searchByName($search, $mode = '')
40         {
41                 if (empty($search)) {
42                         return [];
43                 }
44
45                 // check supported networks
46                 if (Config::get('system', 'diaspora_enabled')) {
47                         $diaspora = Protocol::DIASPORA;
48                 } else {
49                         $diaspora = Protocol::DFRN;
50                 }
51
52                 if (!Config::get('system', 'ostatus_disabled')) {
53                         $ostatus = Protocol::OSTATUS;
54                 } else {
55                         $ostatus = Protocol::DFRN;
56                 }
57
58                 // check if we search only communities or every contact
59                 if ($mode === "community") {
60                         $extra_sql = " AND `community`";
61                 } else {
62                         $extra_sql = "";
63                 }
64
65                 $search .= "%";
66
67                 $results = DBA::p("SELECT `nurl` FROM `gcontact`
68                         WHERE NOT `hide` AND `network` IN (?, ?, ?, ?) AND
69                                 ((`last_contact` >= `last_failure`) OR (`updated` >= `last_failure`)) AND
70                                 (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql
71                                 GROUP BY `nurl` ORDER BY `nurl` DESC LIMIT 1000",
72                         Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, $search, $search, $search
73                 );
74
75                 $gcontacts = [];
76                 while ($result = DBA::fetch($results)) {
77                         $urlparts = parse_url($result["nurl"]);
78
79                         // Ignore results that look strange.
80                         // For historic reasons the gcontact table does contain some garbage.
81                         if (!empty($urlparts['query']) || !empty($urlparts['fragment'])) {
82                                 continue;
83                         }
84
85                         $gcontacts[] = Contact::getDetailsByURL($result["nurl"], local_user());
86                 }
87                 return $gcontacts;
88         }
89
90         /**
91          * @brief Link the gcontact entry with user, contact and global contact
92          *
93          * @param integer $gcid Global contact ID
94          * @param integer $uid  User ID
95          * @param integer $cid  Contact ID
96          * @param integer $zcid Global Contact ID
97          * @return void
98          * @throws Exception
99          */
100         public static function link($gcid, $uid = 0, $cid = 0, $zcid = 0)
101         {
102                 if ($gcid <= 0) {
103                         return;
104                 }
105
106                 $condition = ['cid' => $cid, 'uid' => $uid, 'gcid' => $gcid, 'zcid' => $zcid];
107                 DBA::update('glink', ['updated' => DateTimeFormat::utcNow()], $condition, true);
108         }
109
110         /**
111          * @brief Sanitize the given gcontact data
112          *
113          * Generation:
114          *  0: No definition
115          *  1: Profiles on this server
116          *  2: Contacts of profiles on this server
117          *  3: Contacts of contacts of profiles on this server
118          *  4: ...
119          *
120          * @param array $gcontact array with gcontact data
121          * @return array $gcontact
122          * @throws Exception
123          */
124         public static function sanitize($gcontact)
125         {
126                 if ($gcontact['url'] == "") {
127                         throw new Exception('URL is empty');
128                 }
129
130                 $urlparts = parse_url($gcontact['url']);
131                 if (!isset($urlparts["scheme"])) {
132                         throw new Exception("This (".$gcontact['url'].") doesn't seem to be an url.");
133                 }
134
135                 if (in_array($urlparts["host"], ["twitter.com", "identi.ca"])) {
136                         throw new Exception('Contact from a non federated network ignored. ('.$gcontact['url'].')');
137                 }
138
139                 // Don't store the statusnet connector as network
140                 // We can't simply set this to Protocol::OSTATUS since the connector could have fetched posts from friendica as well
141                 if ($gcontact['network'] == Protocol::STATUSNET) {
142                         $gcontact['network'] = "";
143                 }
144
145                 // Assure that there are no parameter fragments in the profile url
146                 if (empty($gcontact["network"]) || in_array($gcontact["network"], Protocol::FEDERATED)) {
147                         $gcontact['url'] = self::cleanContactUrl($gcontact['url']);
148                 }
149
150                 $alternate = PortableContact::alternateOStatusUrl($gcontact['url']);
151
152                 // The global contacts should contain the original picture, not the cached one
153                 if (($gcontact['generation'] != 1) && stristr(Strings::normaliseLink($gcontact['photo']), Strings::normaliseLink(System::baseUrl()."/photo/"))) {
154                         $gcontact['photo'] = "";
155                 }
156
157                 if (!isset($gcontact['network'])) {
158                         $condition = ["`uid` = 0 AND `nurl` = ? AND `network` != '' AND `network` != ?",
159                                 Strings::normaliseLink($gcontact['url']), Protocol::STATUSNET];
160                         $contact = DBA::selectFirst('contact', ['network'], $condition);
161                         if (DBA::isResult($contact)) {
162                                 $gcontact['network'] = $contact["network"];
163                         }
164
165                         if (($gcontact['network'] == "") || ($gcontact['network'] == Protocol::OSTATUS)) {
166                                 $condition = ["`uid` = 0 AND `alias` IN (?, ?) AND `network` != '' AND `network` != ?",
167                                         $gcontact['url'], Strings::normaliseLink($gcontact['url']), Protocol::STATUSNET];
168                                 $contact = DBA::selectFirst('contact', ['network'], $condition);
169                                 if (DBA::isResult($contact)) {
170                                         $gcontact['network'] = $contact["network"];
171                                 }
172                         }
173                 }
174
175                 $gcontact['server_url'] = '';
176                 $gcontact['network'] = '';
177
178                 $fields = ['network', 'updated', 'server_url', 'url', 'addr'];
179                 $gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($gcontact['url'])]);
180                 if (DBA::isResult($gcnt)) {
181                         if (!isset($gcontact['network']) && ($gcnt["network"] != Protocol::STATUSNET)) {
182                                 $gcontact['network'] = $gcnt["network"];
183                         }
184                         if ($gcontact['updated'] <= DBA::NULL_DATETIME) {
185                                 $gcontact['updated'] = $gcnt["updated"];
186                         }
187                         if (!isset($gcontact['server_url']) && (Strings::normaliseLink($gcnt["server_url"]) != Strings::normaliseLink($gcnt["url"]))) {
188                                 $gcontact['server_url'] = $gcnt["server_url"];
189                         }
190                         if (!isset($gcontact['addr'])) {
191                                 $gcontact['addr'] = $gcnt["addr"];
192                         }
193                 }
194
195                 if ((!isset($gcontact['network']) || !isset($gcontact['name']) || !isset($gcontact['addr']) || !isset($gcontact['photo']) || !isset($gcontact['server_url']) || $alternate)
196                         && GServer::reachable($gcontact['url'], $gcontact['server_url'], $gcontact['network'], false)
197                 ) {
198                         $data = Probe::uri($gcontact['url']);
199
200                         if ($data["network"] == Protocol::PHANTOM) {
201                                 throw new Exception('Probing for URL '.$gcontact['url'].' failed');
202                         }
203
204                         $orig_profile = $gcontact['url'];
205
206                         $gcontact["server_url"] = $data["baseurl"];
207
208                         $gcontact = array_merge($gcontact, $data);
209
210                         if ($alternate && ($gcontact['network'] == Protocol::OSTATUS)) {
211                                 // Delete the old entry - if it exists
212                                 if (DBA::exists('gcontact', ['nurl' => Strings::normaliseLink($orig_profile)])) {
213                                         DBA::delete('gcontact', ['nurl' => Strings::normaliseLink($orig_profile)]);
214                                 }
215                         }
216                 }
217
218                 if (!isset($gcontact['name']) || !isset($gcontact['photo'])) {
219                         throw new Exception('No name and photo for URL '.$gcontact['url']);
220                 }
221
222                 if (!in_array($gcontact['network'], Protocol::FEDERATED)) {
223                         throw new Exception('No federated network ('.$gcontact['network'].') detected for URL '.$gcontact['url']);
224                 }
225
226                 if (!isset($gcontact['server_url'])) {
227                         // We check the server url to be sure that it is a real one
228                         $server_url = Contact::getBasepath($gcontact['url']);
229
230                         // We are now sure that it is a correct URL. So we use it in the future
231                         if ($server_url != "") {
232                                 $gcontact['server_url'] = $server_url;
233                         }
234                 }
235
236                 // The server URL doesn't seem to be valid, so we don't store it.
237                 if (!GServer::check($gcontact['server_url'], $gcontact['network'])) {
238                         $gcontact['server_url'] = "";
239                 }
240
241                 return $gcontact;
242         }
243
244         /**
245          * @param integer $uid id
246          * @param integer $cid id
247          * @return integer
248          * @throws Exception
249          */
250         public static function countCommonFriends($uid, $cid)
251         {
252                 $r = q(
253                         "SELECT count(*) as `total`
254                         FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
255                         WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
256                         ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR
257                         (`gcontact`.`updated` >= `gcontact`.`last_failure`))
258                         AND `gcontact`.`nurl` IN (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 and id != %d) ",
259                         intval($cid),
260                         intval($uid),
261                         intval($uid),
262                         intval($cid)
263                 );
264
265                 // Logger::log("countCommonFriends: $uid $cid {$r[0]['total']}");
266                 if (DBA::isResult($r)) {
267                         return $r[0]['total'];
268                 }
269                 return 0;
270         }
271
272         /**
273          * @param integer $uid  id
274          * @param integer $zcid zcid
275          * @return integer
276          * @throws Exception
277          */
278         public static function countCommonFriendsZcid($uid, $zcid)
279         {
280                 $r = q(
281                         "SELECT count(*) as `total`
282                         FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
283                         where `glink`.`zcid` = %d
284                         and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0) ",
285                         intval($zcid),
286                         intval($uid)
287                 );
288
289                 if (DBA::isResult($r)) {
290                         return $r[0]['total'];
291                 }
292
293                 return 0;
294         }
295
296         /**
297          * @param integer $uid     user
298          * @param integer $cid     cid
299          * @param integer $start   optional, default 0
300          * @param integer $limit   optional, default 9999
301          * @param boolean $shuffle optional, default false
302          * @return object
303          * @throws Exception
304          */
305         public static function commonFriends($uid, $cid, $start = 0, $limit = 9999, $shuffle = false)
306         {
307                 if ($shuffle) {
308                         $sql_extra = " order by rand() ";
309                 } else {
310                         $sql_extra = " order by `gcontact`.`name` asc ";
311                 }
312
313                 $r = q(
314                         "SELECT `gcontact`.*, `contact`.`id` AS `cid`
315                         FROM `glink`
316                         INNER JOIN `gcontact` ON `glink`.`gcid` = `gcontact`.`id`
317                         INNER JOIN `contact` ON `gcontact`.`nurl` = `contact`.`nurl`
318                         WHERE `glink`.`cid` = %d and `glink`.`uid` = %d
319                                 AND `contact`.`uid` = %d AND `contact`.`self` = 0 AND `contact`.`blocked` = 0
320                                 AND `contact`.`hidden` = 0 AND `contact`.`id` != %d
321                                 AND ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
322                                 $sql_extra LIMIT %d, %d",
323                         intval($cid),
324                         intval($uid),
325                         intval($uid),
326                         intval($cid),
327                         intval($start),
328                         intval($limit)
329                 );
330
331                 /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
332                 return $r;
333         }
334
335         /**
336          * @param integer $uid     user
337          * @param integer $zcid    zcid
338          * @param integer $start   optional, default 0
339          * @param integer $limit   optional, default 9999
340          * @param boolean $shuffle optional, default false
341          * @return object
342          * @throws Exception
343          */
344         public static function commonFriendsZcid($uid, $zcid, $start = 0, $limit = 9999, $shuffle = false)
345         {
346                 if ($shuffle) {
347                         $sql_extra = " order by rand() ";
348                 } else {
349                         $sql_extra = " order by `gcontact`.`name` asc ";
350                 }
351
352                 $r = q(
353                         "SELECT `gcontact`.*
354                         FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
355                         where `glink`.`zcid` = %d
356                         and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0)
357                         $sql_extra limit %d, %d",
358                         intval($zcid),
359                         intval($uid),
360                         intval($start),
361                         intval($limit)
362                 );
363
364                 /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
365                 return $r;
366         }
367
368         /**
369          * @param integer $uid user
370          * @param integer $cid cid
371          * @return integer
372          * @throws Exception
373          */
374         public static function countAllFriends($uid, $cid)
375         {
376                 $r = q(
377                         "SELECT count(*) as `total`
378                         FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
379                         where `glink`.`cid` = %d and `glink`.`uid` = %d AND
380                         ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))",
381                         intval($cid),
382                         intval($uid)
383                 );
384
385                 if (DBA::isResult($r)) {
386                         return $r[0]['total'];
387                 }
388
389                 return 0;
390         }
391
392         /**
393          * @param integer $uid   user
394          * @param integer $cid   cid
395          * @param integer $start optional, default 0
396          * @param integer $limit optional, default 80
397          * @return array
398          * @throws Exception
399          */
400         public static function allFriends($uid, $cid, $start = 0, $limit = 80)
401         {
402                 $r = q(
403                         "SELECT `gcontact`.*, `contact`.`id` AS `cid`
404                         FROM `glink`
405                         INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
406                         LEFT JOIN `contact` ON `contact`.`nurl` = `gcontact`.`nurl` AND `contact`.`uid` = %d
407                         WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
408                         ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
409                         ORDER BY `gcontact`.`name` ASC LIMIT %d, %d ",
410                         intval($uid),
411                         intval($cid),
412                         intval($uid),
413                         intval($start),
414                         intval($limit)
415                 );
416
417                 /// @TODO Check all calling-findings of this function if they properly use DBA::isResult()
418                 return $r;
419         }
420
421         /**
422          * @param int     $uid   user
423          * @param integer $start optional, default 0
424          * @param integer $limit optional, default 80
425          * @return array
426          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
427          */
428         public static function suggestionQuery($uid, $start = 0, $limit = 80)
429         {
430                 if (!$uid) {
431                         return [];
432                 }
433
434                 /*
435                 * Uncommented because the result of the queries are to big to store it in the cache.
436                 * We need to decide if we want to change the db column type or if we want to delete it.
437                 */
438                 //$list = Cache::get("suggestion_query:".$uid.":".$start.":".$limit);
439                 //if (!is_null($list)) {
440                 //      return $list;
441                 //}
442
443                 $network = [Protocol::DFRN, Protocol::ACTIVITYPUB];
444
445                 if (Config::get('system', 'diaspora_enabled')) {
446                         $network[] = Protocol::DIASPORA;
447                 }
448
449                 if (!Config::get('system', 'ostatus_disabled')) {
450                         $network[] = Protocol::OSTATUS;
451                 }
452
453                 $sql_network = implode("', '", $network);
454                 $sql_network = "'".$sql_network."'";
455
456                 /// @todo This query is really slow
457                 // By now we cache the data for five minutes
458                 $r = q(
459                         "SELECT count(glink.gcid) as `total`, gcontact.* from gcontact
460                         INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
461                         where uid = %d and not gcontact.nurl in ( select nurl from contact where uid = %d )
462                         AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
463                         AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
464                         AND `gcontact`.`updated` >= '%s' AND NOT `gcontact`.`hide`
465                         AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
466                         AND `gcontact`.`network` IN (%s)
467                         GROUP BY `glink`.`gcid` ORDER BY `gcontact`.`updated` DESC,`total` DESC LIMIT %d, %d",
468                         intval($uid),
469                         intval($uid),
470                         intval($uid),
471                         intval($uid),
472                         DBA::NULL_DATETIME,
473                         $sql_network,
474                         intval($start),
475                         intval($limit)
476                 );
477
478                 if (DBA::isResult($r) && count($r) >= ($limit -1)) {
479                         /*
480                         * Uncommented because the result of the queries are to big to store it in the cache.
481                         * We need to decide if we want to change the db column type or if we want to delete it.
482                         */
483                         //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $r, Cache::FIVE_MINUTES);
484
485                         return $r;
486                 }
487
488                 $r2 = q(
489                         "SELECT gcontact.* FROM gcontact
490                         INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
491                         WHERE `glink`.`uid` = 0 AND `glink`.`cid` = 0 AND `glink`.`zcid` = 0 AND NOT `gcontact`.`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = %d)
492                         AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
493                         AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
494                         AND `gcontact`.`updated` >= '%s'
495                         AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
496                         AND `gcontact`.`network` IN (%s)
497                         ORDER BY rand() LIMIT %d, %d",
498                         intval($uid),
499                         intval($uid),
500                         intval($uid),
501                         DBA::NULL_DATETIME,
502                         $sql_network,
503                         intval($start),
504                         intval($limit)
505                 );
506
507                 $list = [];
508                 foreach ($r2 as $suggestion) {
509                         $list[$suggestion["nurl"]] = $suggestion;
510                 }
511
512                 foreach ($r as $suggestion) {
513                         $list[$suggestion["nurl"]] = $suggestion;
514                 }
515
516                 while (sizeof($list) > ($limit)) {
517                         array_pop($list);
518                 }
519
520                 /*
521                 * Uncommented because the result of the queries are to big to store it in the cache.
522                 * We need to decide if we want to change the db column type or if we want to delete it.
523                 */
524                 //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $list, Cache::FIVE_MINUTES);
525                 return $list;
526         }
527
528         /**
529          * @return void
530          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
531          */
532         public static function updateSuggestions()
533         {
534                 $done = [];
535
536                 /// @TODO Check if it is really neccessary to poll the own server
537                 PortableContact::loadWorker(0, 0, 0, System::baseUrl() . '/poco');
538
539                 $done[] = System::baseUrl() . '/poco';
540
541                 if (strlen(Config::get('system', 'directory'))) {
542                         $x = Network::fetchUrl(get_server()."/pubsites");
543                         if (!empty($x)) {
544                                 $j = json_decode($x);
545                                 if (!empty($j->entries)) {
546                                         foreach ($j->entries as $entry) {
547                                                 GServer::check($entry->url);
548
549                                                 $url = $entry->url . '/poco';
550                                                 if (!in_array($url, $done)) {
551                                                         PortableContact::loadWorker(0, 0, 0, $url);
552                                                         $done[] = $url;
553                                                 }
554                                         }
555                                 }
556                         }
557                 }
558
559                 // Query your contacts from Friendica and Redmatrix/Hubzilla for their contacts
560                 $r = q(
561                         "SELECT DISTINCT(`poco`) AS `poco` FROM `contact` WHERE `network` IN ('%s', '%s')",
562                         DBA::escape(Protocol::DFRN),
563                         DBA::escape(Protocol::DIASPORA)
564                 );
565
566                 if (DBA::isResult($r)) {
567                         foreach ($r as $rr) {
568                                 $base = substr($rr['poco'], 0, strrpos($rr['poco'], '/'));
569                                 if (! in_array($base, $done)) {
570                                         PortableContact::loadWorker(0, 0, 0, $base);
571                                 }
572                         }
573                 }
574         }
575
576         /**
577          * @brief Removes unwanted parts from a contact url
578          *
579          * @param string $url Contact url
580          *
581          * @return string Contact url with the wanted parts
582          * @throws Exception
583          */
584         public static function cleanContactUrl($url)
585         {
586                 $parts = parse_url($url);
587
588                 if (!isset($parts["scheme"]) || !isset($parts["host"])) {
589                         return $url;
590                 }
591
592                 $new_url = $parts["scheme"]."://".$parts["host"];
593
594                 if (isset($parts["port"])) {
595                         $new_url .= ":".$parts["port"];
596                 }
597
598                 if (isset($parts["path"])) {
599                         $new_url .= $parts["path"];
600                 }
601
602                 if ($new_url != $url) {
603                         Logger::log("Cleaned contact url ".$url." to ".$new_url." - Called by: ".System::callstack(), Logger::DEBUG);
604                 }
605
606                 return $new_url;
607         }
608
609         /**
610          * @brief Replace alternate OStatus user format with the primary one
611          *
612          * @param array $contact contact array (called by reference)
613          * @return void
614          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
615          * @throws \ImagickException
616          */
617         public static function fixAlternateContactAddress(&$contact)
618         {
619                 if (($contact["network"] == Protocol::OSTATUS) && PortableContact::alternateOStatusUrl($contact["url"])) {
620                         $data = Probe::uri($contact["url"]);
621                         if ($contact["network"] == Protocol::OSTATUS) {
622                                 Logger::log("Fix primary url from ".$contact["url"]." to ".$data["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
623                                 $contact["url"] = $data["url"];
624                                 $contact["addr"] = $data["addr"];
625                                 $contact["alias"] = $data["alias"];
626                                 $contact["server_url"] = $data["baseurl"];
627                         }
628                 }
629         }
630
631         /**
632          * @brief Fetch the gcontact id, add an entry if not existed
633          *
634          * @param array $contact contact array
635          *
636          * @return bool|int Returns false if not found, integer if contact was found
637          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
638          * @throws \ImagickException
639          */
640         public static function getId($contact)
641         {
642                 $gcontact_id = 0;
643                 $doprobing = false;
644                 $last_failure_str = '';
645                 $last_contact_str = '';
646
647                 if (empty($contact["network"])) {
648                         Logger::log("Empty network for contact url ".$contact["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
649                         return false;
650                 }
651
652                 if (in_array($contact["network"], [Protocol::PHANTOM])) {
653                         Logger::log("Invalid network for contact url ".$contact["url"]." - Called by: ".System::callstack(), Logger::DEBUG);
654                         return false;
655                 }
656
657                 if ($contact["network"] == Protocol::STATUSNET) {
658                         $contact["network"] = Protocol::OSTATUS;
659                 }
660
661                 // All new contacts are hidden by default
662                 if (!isset($contact["hide"])) {
663                         $contact["hide"] = true;
664                 }
665
666                 // Replace alternate OStatus user format with the primary one
667                 self::fixAlternateContactAddress($contact);
668
669                 // Remove unwanted parts from the contact url (e.g. "?zrl=...")
670                 if (in_array($contact["network"], Protocol::FEDERATED)) {
671                         $contact["url"] = self::cleanContactUrl($contact["url"]);
672                 }
673
674                 DBA::lock('gcontact');
675                 $fields = ['id', 'last_contact', 'last_failure', 'network'];
676                 $gcnt = DBA::selectFirst('gcontact', $fields, ['nurl' => Strings::normaliseLink($contact["url"])]);
677                 if (DBA::isResult($gcnt)) {
678                         $gcontact_id = $gcnt["id"];
679
680                         // Update every 90 days
681                         if (in_array($gcnt["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
682                                 $last_failure_str = $gcnt["last_failure"];
683                                 $last_failure = strtotime($gcnt["last_failure"]);
684                                 $last_contact_str = $gcnt["last_contact"];
685                                 $last_contact = strtotime($gcnt["last_contact"]);
686                                 $doprobing = (((time() - $last_contact) > (90 * 86400)) && ((time() - $last_failure) > (90 * 86400)));
687                         }
688                 } else {
689                         $contact['location'] = $contact['location'] ?? '';
690                         $contact['about'] = $contact['about'] ?? '';
691                         $contact['generation'] = $contact['generation'] ?? 0;
692
693                         q(
694                                 "INSERT INTO `gcontact` (`name`, `nick`, `addr` , `network`, `url`, `nurl`, `photo`, `created`, `updated`, `location`, `about`, `hide`, `generation`)
695                                 VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d)",
696                                 DBA::escape($contact["name"]),
697                                 DBA::escape($contact["nick"]),
698                                 DBA::escape($contact["addr"]),
699                                 DBA::escape($contact["network"]),
700                                 DBA::escape($contact["url"]),
701                                 DBA::escape(Strings::normaliseLink($contact["url"])),
702                                 DBA::escape($contact["photo"]),
703                                 DBA::escape(DateTimeFormat::utcNow()),
704                                 DBA::escape(DateTimeFormat::utcNow()),
705                                 DBA::escape($contact["location"]),
706                                 DBA::escape($contact["about"]),
707                                 intval($contact["hide"]),
708                                 intval($contact["generation"])
709                         );
710
711                         $condition = ['nurl' => Strings::normaliseLink($contact["url"])];
712                         $cnt = DBA::selectFirst('gcontact', ['id', 'network'], $condition, ['order' => ['id']]);
713                         if (DBA::isResult($cnt)) {
714                                 $gcontact_id = $cnt["id"];
715                                 $doprobing = in_array($cnt["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""]);
716                         }
717                 }
718                 DBA::unlock();
719
720                 if ($doprobing) {
721                         Logger::log("Last Contact: ". $last_contact_str." - Last Failure: ".$last_failure_str." - Checking: ".$contact["url"], Logger::DEBUG);
722                         Worker::add(PRIORITY_LOW, 'GProbe', $contact["url"]);
723                 }
724
725                 return $gcontact_id;
726         }
727
728         /**
729          * @brief Updates the gcontact table from a given array
730          *
731          * @param array $contact contact array
732          *
733          * @return bool|int Returns false if not found, integer if contact was found
734          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
735          * @throws \ImagickException
736          */
737         public static function update($contact)
738         {
739                 // Check for invalid "contact-type" value
740                 if (isset($contact['contact-type']) && (intval($contact['contact-type']) < 0)) {
741                         $contact['contact-type'] = 0;
742                 }
743
744                 /// @todo update contact table as well
745
746                 $gcontact_id = self::getId($contact);
747
748                 if (!$gcontact_id) {
749                         return false;
750                 }
751
752                 $public_contact = DBA::selectFirst('gcontact', [
753                         'name', 'nick', 'photo', 'location', 'about', 'addr', 'generation', 'birthday', 'gender', 'keywords',
754                         'contact-type', 'hide', 'nsfw', 'network', 'alias', 'notify', 'server_url', 'connect', 'updated', 'url'
755                 ], ['id' => $gcontact_id]);
756
757                 if (!DBA::isResult($public_contact)) {
758                         return false;
759                 }
760
761                 // Get all field names
762                 $fields = [];
763                 foreach ($public_contact as $field => $data) {
764                         $fields[$field] = $data;
765                 }
766
767                 unset($fields['url']);
768                 unset($fields['updated']);
769                 unset($fields['hide']);
770
771                 // Bugfix: We had an error in the storing of keywords which lead to the "0"
772                 // This value is still transmitted via poco.
773                 if (isset($contact['keywords']) && ($contact['keywords'] == '0')) {
774                         unset($contact['keywords']);
775                 }
776
777                 if (isset($public_contact['keywords']) && ($public_contact['keywords'] == '0')) {
778                         $public_contact['keywords'] = '';
779                 }
780
781                 // assign all unassigned fields from the database entry
782                 foreach ($fields as $field => $data) {
783                         if (empty($contact[$field])) {
784                                 $contact[$field] = $public_contact[$field];
785                         }
786                 }
787
788                 if (!isset($contact['hide'])) {
789                         $contact['hide'] = $public_contact['hide'];
790                 }
791
792                 $fields['hide'] = $public_contact['hide'];
793
794                 if ($contact['network'] == Protocol::STATUSNET) {
795                         $contact['network'] = Protocol::OSTATUS;
796                 }
797
798                 // Replace alternate OStatus user format with the primary one
799                 self::fixAlternateContactAddress($contact);
800
801                 if (!isset($contact['updated'])) {
802                         $contact['updated'] = DateTimeFormat::utcNow();
803                 }
804
805                 if ($contact['network'] == Protocol::TWITTER) {
806                         $contact['server_url'] = 'http://twitter.com';
807                 }
808
809                 if (empty($contact['server_url'])) {
810                         $data = Probe::uri($contact['url']);
811                         if ($data['network'] != Protocol::PHANTOM) {
812                                 $contact['server_url'] = $data['baseurl'];
813                         }
814                 } else {
815                         $contact['server_url'] = Strings::normaliseLink($contact['server_url']);
816                 }
817
818                 if (empty($contact['addr']) && !empty($contact['server_url']) && !empty($contact['nick'])) {
819                         $hostname = str_replace('http://', '', $contact['server_url']);
820                         $contact['addr'] = $contact['nick'] . '@' . $hostname;
821                 }
822
823                 // Check if any field changed
824                 $update = false;
825                 unset($fields['generation']);
826
827                 if ((($contact['generation'] > 0) && ($contact['generation'] <= $public_contact['generation'])) || ($public_contact['generation'] == 0)) {
828                         foreach ($fields as $field => $data) {
829                                 if ($contact[$field] != $public_contact[$field]) {
830                                         Logger::debug('Difference found.', ['contact' => $contact["url"], 'field' => $field, 'new' => $contact[$field], 'old' => $public_contact[$field]]);
831                                         $update = true;
832                                 }
833                         }
834
835                         if ($contact['generation'] < $public_contact['generation']) {
836                                 Logger::debug('Difference found.', ['contact' => $contact["url"], 'field' => 'generation', 'new' => $contact['generation'], 'old' => $public_contact['generation']]);
837                                 $update = true;
838                         }
839                 }
840
841                 if ($update) {
842                         Logger::debug('Update gcontact.', ['contact' => $contact['url']]);
843                         $condition = ['`nurl` = ? AND (`generation` = 0 OR `generation` >= ?)',
844                                         Strings::normaliseLink($contact["url"]), $contact["generation"]];
845                         $contact["updated"] = DateTimeFormat::utc($contact["updated"]);
846
847                         $updated = [
848                                 'photo' => $contact['photo'], 'name' => $contact['name'],
849                                 'nick' => $contact['nick'], 'addr' => $contact['addr'],
850                                 'network' => $contact['network'], 'birthday' => $contact['birthday'],
851                                 'gender' => $contact['gender'], 'keywords' => $contact['keywords'],
852                                 'hide' => $contact['hide'], 'nsfw' => $contact['nsfw'],
853                                 'contact-type' => $contact['contact-type'], 'alias' => $contact['alias'],
854                                 'notify' => $contact['notify'], 'url' => $contact['url'],
855                                 'location' => $contact['location'], 'about' => $contact['about'],
856                                 'generation' => $contact['generation'], 'updated' => $contact['updated'],
857                                 'server_url' => $contact['server_url'], 'connect' => $contact['connect']
858                         ];
859
860                         DBA::update('gcontact', $updated, $condition, $fields);
861                 }
862
863                 return $gcontact_id;
864         }
865
866         /**
867          * Set the last date that the contact had posted something
868          *
869          * @param string $data  Probing result
870          * @param bool   $force force updating
871          */
872         public static function setLastUpdate(array $data, bool $force = false)
873         {
874                 // Fetch the global contact
875                 $gcontact = DBA::selectFirst('gcontact', ['created', 'updated', 'last_contact', 'last_failure'],
876                         ['nurl' => Strings::normaliseLink($data['url'])]);
877                 if (!DBA::isResult($gcontact)) {
878                         return;
879                 }
880
881                 if (!$force && !PortableContact::updateNeeded($gcontact['created'], $gcontact['updated'], $gcontact['last_failure'], $gcontact['last_contact'])) {
882                         Logger::info("Don't update profile", ['url' => $data['url'], 'updated' => $gcontact['updated']]);
883                         return;
884                 }
885
886                 if (self::updateFromNoScrape($data)) {
887                         return;
888                 }
889
890                 // When the profile doesn't have got a feed, then we exit here
891                 if (empty($data['poll'])) {
892                         return;
893                 }
894
895                 if ($data['network'] == Protocol::ACTIVITYPUB) {
896                         self::updateFromOutbox($data['poll'], $data);
897                 } else {
898                         self::updateFromFeed($data);
899                 }
900         }
901
902         /**
903          * Update a global contact via the "noscrape" endpoint
904          *
905          * @param string $data Probing result
906          *
907          * @return bool 'true' if update was successful or the server was unreachable
908          */
909         private static function updateFromNoScrape(array $data)
910         {
911                 // Check the 'noscrape' endpoint when it is a Friendica server
912                 $gserver = DBA::selectFirst('gserver', ['noscrape'], ["`nurl` = ? AND `noscrape` != ''",
913                 Strings::normaliseLink($data['baseurl'])]);
914                 if (!DBA::isResult($gserver)) {
915                         return false;
916                 }
917
918                 $curlResult = Network::curl($gserver['noscrape'] . '/' . $data['nick']);
919
920                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
921                         $noscrape = json_decode($curlResult->getBody(), true);
922                         if (!empty($noscrape)) {
923                                 $noscrape['updated'] = DateTimeFormat::utc($noscrape['updated'], DateTimeFormat::MYSQL);
924                                 $fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $noscrape['updated']];
925                                 DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
926                                 return true;
927                         }
928                 } elseif ($curlResult->isTimeout()) {
929                         // On a timeout return the existing value, but mark the contact as failure
930                         $fields = ['last_failure' => DateTimeFormat::utcNow()];
931                         DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
932                         return true;
933                 }
934                 return false;
935         }
936
937         /**
938          * Update a global contact via an ActivityPub Outbox
939          *
940          * @param string $data Probing result
941          */
942         private static function updateFromOutbox(string $feed, array $data)
943         {
944                 $outbox = ActivityPub::fetchContent($feed);
945                 if (empty($outbox)) {
946                         return;
947                 }
948
949                 if (!empty($outbox['orderedItems'])) {
950                         $items = $outbox['orderedItems'];
951                 } elseif (!empty($outbox['first']['orderedItems'])) {
952                         $items = $outbox['first']['orderedItems'];
953                 } elseif (!empty($outbox['first'])) {
954                         self::updateFromOutbox($outbox['first'], $data);
955                         return;
956                 } else {
957                         $items = [];
958                 }
959
960                 $last_updated = '';
961
962                 foreach ($items as $activity) {
963                         if ($last_updated < $activity['published']) {
964                                 $last_updated = $activity['published'];
965                         }
966                 }
967
968                 if (empty($last_updated)) {
969                         return;
970                 }
971
972                 $fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $last_updated];
973                 DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
974         }
975
976         /**
977          * Update a global contact via an XML feed
978          *
979          * @param string $data Probing result
980          */
981         private static function updateFromFeed(array $data)
982         {
983                 // Search for the newest entry in the feed
984                 $curlResult = Network::curl($data['poll']);
985                 if (!$curlResult->isSuccess()) {
986                         $fields = ['last_failure' => DateTimeFormat::utcNow()];
987                         DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
988
989                         Logger::info("Profile wasn't reachable (no feed)", ['url' => $data['url']]);
990                         return;
991                 }
992
993                 $doc = new DOMDocument();
994                 @$doc->loadXML($curlResult->getBody());
995
996                 $xpath = new DOMXPath($doc);
997                 $xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
998
999                 $entries = $xpath->query('/atom:feed/atom:entry');
1000
1001                 $last_updated = '';
1002
1003                 foreach ($entries as $entry) {
1004                         $published_item = $xpath->query('atom:published/text()', $entry)->item(0);
1005                         $updated_item   = $xpath->query('atom:updated/text()'  , $entry)->item(0);
1006                         $published      = !empty($published_item->nodeValue) ? DateTimeFormat::utc($published_item->nodeValue) : null;
1007                         $updated        = !empty($updated_item->nodeValue) ? DateTimeFormat::utc($updated_item->nodeValue) : null;
1008
1009                         if (empty($published) || empty($updated)) {
1010                                 Logger::notice('Invalid entry for XPath.', ['entry' => $entry, 'url' => $data['url']]);
1011                                 continue;
1012                         }
1013
1014                         if ($last_updated < $published) {
1015                                 $last_updated = $published;
1016                         }
1017
1018                         if ($last_updated < $updated) {
1019                                 $last_updated = $updated;
1020                         }
1021                 }
1022
1023                 if (empty($last_updated)) {
1024                         return;
1025                 }
1026
1027                 $fields = ['last_contact' => DateTimeFormat::utcNow(), 'updated' => $last_updated];
1028                 DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($data['url'])]);
1029         }
1030         /**
1031          * @brief Updates the gcontact entry from a given public contact id
1032          *
1033          * @param integer $cid contact id
1034          * @return void
1035          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1036          * @throws \ImagickException
1037          */
1038         public static function updateFromPublicContactID($cid)
1039         {
1040                 self::updateFromPublicContact(['id' => $cid]);
1041         }
1042
1043         /**
1044          * @brief Updates the gcontact entry from a given public contact url
1045          *
1046          * @param string $url contact url
1047          * @return integer gcontact id
1048          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1049          * @throws \ImagickException
1050          */
1051         public static function updateFromPublicContactURL($url)
1052         {
1053                 return self::updateFromPublicContact(['nurl' => Strings::normaliseLink($url)]);
1054         }
1055
1056         /**
1057          * @brief Helper function for updateFromPublicContactID and updateFromPublicContactURL
1058          *
1059          * @param array $condition contact condition
1060          * @return integer gcontact id
1061          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1062          * @throws \ImagickException
1063          */
1064         private static function updateFromPublicContact($condition)
1065         {
1066                 $fields = ['name', 'nick', 'url', 'nurl', 'location', 'about', 'keywords', 'gender',
1067                         'bd', 'contact-type', 'network', 'addr', 'notify', 'alias', 'archive', 'term-date',
1068                         'created', 'updated', 'avatar', 'success_update', 'failure_update', 'forum', 'prv',
1069                         'baseurl', 'sensitive', 'unsearchable'];
1070
1071                 $contact = DBA::selectFirst('contact', $fields, array_merge($condition, ['uid' => 0, 'network' => Protocol::FEDERATED]));
1072                 if (!DBA::isResult($contact)) {
1073                         return 0;
1074                 }
1075
1076                 $fields = ['name', 'nick', 'url', 'nurl', 'location', 'about', 'keywords', 'gender', 'generation',
1077                         'birthday', 'contact-type', 'network', 'addr', 'notify', 'alias', 'archived', 'archive_date',
1078                         'created', 'updated', 'photo', 'last_contact', 'last_failure', 'community', 'connect',
1079                         'server_url', 'nsfw', 'hide', 'id'];
1080
1081                 $old_gcontact = DBA::selectFirst('gcontact', $fields, ['nurl' => $contact['nurl']]);
1082                 $do_insert = !DBA::isResult($old_gcontact);
1083                 if ($do_insert) {
1084                         $old_gcontact = [];
1085                 }
1086
1087                 $gcontact = [];
1088
1089                 // These fields are identical in both contact and gcontact
1090                 $fields = ['name', 'nick', 'url', 'nurl', 'location', 'about', 'keywords', 'gender',
1091                         'contact-type', 'network', 'addr', 'notify', 'alias', 'created', 'updated'];
1092
1093                 foreach ($fields as $field) {
1094                         $gcontact[$field] = $contact[$field];
1095                 }
1096
1097                 // These fields are having different names but the same content
1098                 $gcontact['server_url'] = $contact['baseurl'] ?? ''; // "baseurl" can be null, "server_url" not
1099                 $gcontact['nsfw'] = $contact['sensitive'];
1100                 $gcontact['hide'] = $contact['unsearchable'];
1101                 $gcontact['archived'] = $contact['archive'];
1102                 $gcontact['archive_date'] = $contact['term-date'];
1103                 $gcontact['birthday'] = $contact['bd'];
1104                 $gcontact['photo'] = $contact['avatar'];
1105                 $gcontact['last_contact'] = $contact['success_update'];
1106                 $gcontact['last_failure'] = $contact['failure_update'];
1107                 $gcontact['community'] = ($contact['forum'] || $contact['prv']);
1108
1109                 foreach (['last_contact', 'last_failure', 'updated'] as $field) {
1110                         if (!empty($old_gcontact[$field]) && ($old_gcontact[$field] >= $gcontact[$field])) {
1111                                 unset($gcontact[$field]);
1112                         }
1113                 }
1114
1115                 if (!$gcontact['archived']) {
1116                         $gcontact['archive_date'] = DBA::NULL_DATETIME;
1117                 }
1118
1119                 if (!empty($old_gcontact['created']) && ($old_gcontact['created'] > DBA::NULL_DATETIME)
1120                         && ($old_gcontact['created'] <= $gcontact['created'])) {
1121                         unset($gcontact['created']);
1122                 }
1123
1124                 if (empty($gcontact['birthday']) && ($gcontact['birthday'] <= DBA::NULL_DATETIME)) {
1125                         unset($gcontact['birthday']);
1126                 }
1127
1128                 if (empty($old_gcontact['generation']) || ($old_gcontact['generation'] > 2)) {
1129                         $gcontact['generation'] = 2; // We fetched the data directly from the other server
1130                 }
1131
1132                 if (!$do_insert) {
1133                         DBA::update('gcontact', $gcontact, ['nurl' => $contact['nurl']], $old_gcontact);
1134                         return $old_gcontact['id'];
1135                 } elseif (!$gcontact['archived']) {
1136                         DBA::insert('gcontact', $gcontact);
1137                         return DBA::lastInsertId();
1138                 }
1139         }
1140
1141         /**
1142          * @brief Updates the gcontact entry from probe
1143          *
1144          * @param string  $url   profile link
1145          * @param boolean $force Optional forcing of network probing (otherwise we use the cached data)
1146          *
1147          * @return boolean 'true' when contact had been updated
1148          *
1149          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1150          * @throws \ImagickException
1151          */
1152         public static function updateFromProbe($url, $force = false)
1153         {
1154                 $data = Probe::uri($url, $force);
1155
1156                 if (in_array($data["network"], [Protocol::PHANTOM])) {
1157                         $fields = ['last_failure' => DateTimeFormat::utcNow()];
1158                         DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($url)]);
1159                         Logger::info('Invalid network for contact', ['url' => $data['url'], 'callstack' => System::callstack()]);
1160                         return false;
1161                 }
1162
1163                 $data["server_url"] = $data["baseurl"];
1164
1165                 self::update($data);
1166
1167                 // Set the date of the latest post
1168                 self::setLastUpdate($data, $force);
1169
1170                 return true;
1171         }
1172
1173         /**
1174          * @brief Update the gcontact entry for a given user id
1175          *
1176          * @param int $uid User ID
1177          * @return bool
1178          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1179          * @throws \ImagickException
1180          */
1181         public static function updateForUser($uid)
1182         {
1183                 $r = q(
1184                         "SELECT `profile`.`locality`, `profile`.`region`, `profile`.`country-name`,
1185                                 `profile`.`name`, `profile`.`about`, `profile`.`gender`,
1186                                 `profile`.`pub_keywords`, `profile`.`dob`, `profile`.`photo`,
1187                                 `profile`.`net-publish`, `user`.`nickname`, `user`.`hidewall`,
1188                                 `contact`.`notify`, `contact`.`url`, `contact`.`addr`
1189                         FROM `profile`
1190                                 INNER JOIN `user` ON `user`.`uid` = `profile`.`uid`
1191                                 INNER JOIN `contact` ON `contact`.`uid` = `profile`.`uid`
1192                         WHERE `profile`.`uid` = %d AND `profile`.`is-default` AND `contact`.`self`",
1193                         intval($uid)
1194                 );
1195
1196                 if (!DBA::isResult($r)) {
1197                         Logger::log('Cannot find user with uid=' . $uid, Logger::INFO);
1198                         return false;
1199                 }
1200
1201                 $location = Profile::formatLocation(
1202                         ["locality" => $r[0]["locality"], "region" => $r[0]["region"], "country-name" => $r[0]["country-name"]]
1203                 );
1204
1205                 // The "addr" field was added in 3.4.3 so it can be empty for older users
1206                 if ($r[0]["addr"] != "") {
1207                         $addr = $r[0]["nickname"].'@'.str_replace(["http://", "https://"], "", System::baseUrl());
1208                 } else {
1209                         $addr = $r[0]["addr"];
1210                 }
1211
1212                 $gcontact = ["name" => $r[0]["name"], "location" => $location, "about" => $r[0]["about"],
1213                                 "gender" => $r[0]["gender"], "keywords" => $r[0]["pub_keywords"],
1214                                 "birthday" => $r[0]["dob"], "photo" => $r[0]["photo"],
1215                                 "notify" => $r[0]["notify"], "url" => $r[0]["url"],
1216                                 "hide" => ($r[0]["hidewall"] || !$r[0]["net-publish"]),
1217                                 "nick" => $r[0]["nickname"], "addr" => $addr,
1218                                 "connect" => $addr, "server_url" => System::baseUrl(),
1219                                 "generation" => 1, "network" => Protocol::DFRN];
1220
1221                 self::update($gcontact);
1222         }
1223
1224         /**
1225          * @brief Fetches users of given GNU Social server
1226          *
1227          * If the "Statistics" addon is enabled (See http://gstools.org/ for details) we query user data with this.
1228          *
1229          * @param string $server Server address
1230          * @return bool
1231          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1232          * @throws \ImagickException
1233          */
1234         public static function fetchGsUsers($server)
1235         {
1236                 Logger::log("Fetching users from GNU Social server ".$server, Logger::DEBUG);
1237
1238                 $url = $server."/main/statistics";
1239
1240                 $curlResult = Network::curl($url);
1241                 if (!$curlResult->isSuccess()) {
1242                         return false;
1243                 }
1244
1245                 $statistics = json_decode($curlResult->getBody());
1246
1247                 if (!empty($statistics->config->instance_address)) {
1248                         if (!empty($statistics->config->instance_with_ssl)) {
1249                                 $server = "https://";
1250                         } else {
1251                                 $server = "http://";
1252                         }
1253
1254                         $server .= $statistics->config->instance_address;
1255
1256                         $hostname = $statistics->config->instance_address;
1257                 } elseif (!empty($statistics->instance_address)) {
1258                         if (!empty($statistics->instance_with_ssl)) {
1259                                 $server = "https://";
1260                         } else {
1261                                 $server = "http://";
1262                         }
1263
1264                         $server .= $statistics->instance_address;
1265
1266                         $hostname = $statistics->instance_address;
1267                 }
1268
1269                 if (!empty($statistics->users)) {
1270                         foreach ($statistics->users as $nick => $user) {
1271                                 $profile_url = $server."/".$user->nickname;
1272
1273                                 $contact = ["url" => $profile_url,
1274                                                 "name" => $user->fullname,
1275                                                 "addr" => $user->nickname."@".$hostname,
1276                                                 "nick" => $user->nickname,
1277                                                 "network" => Protocol::OSTATUS,
1278                                                 "photo" => System::baseUrl()."/images/person-300.jpg"];
1279
1280                                 if (isset($user->bio)) {
1281                                         $contact["about"] = $user->bio;
1282                                 }
1283
1284                                 self::getId($contact);
1285                         }
1286                 }
1287         }
1288
1289         /**
1290          * @brief Asking GNU Social server on a regular base for their user data
1291          * @return void
1292          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
1293          * @throws \ImagickException
1294          */
1295         public static function discoverGsUsers()
1296         {
1297                 $requery_days = intval(Config::get("system", "poco_requery_days"));
1298
1299                 $last_update = date("c", time() - (60 * 60 * 24 * $requery_days));
1300
1301                 $r = DBA::select('gserver', ['nurl', 'url'], [
1302                         '`network` = ?
1303                         AND `last_contact` >= `last_failure`
1304                         AND `last_poco_query` < ?',
1305                         Protocol::OSTATUS,
1306                         $last_update
1307                 ], [
1308                         'limit' => 5,
1309                         'order' => ['RAND()']
1310                 ]);
1311
1312                 if (!DBA::isResult($r)) {
1313                         return;
1314                 }
1315
1316                 foreach ($r as $server) {
1317                         self::fetchGsUsers($server["url"]);
1318                         q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", DBA::escape(DateTimeFormat::utcNow()), DBA::escape($server["nurl"]));
1319                 }
1320         }
1321
1322         /**
1323          * Returns a random, global contact of the current node
1324          *
1325          * @return string The profile URL
1326          * @throws Exception
1327          */
1328         public static function getRandomUrl()
1329         {
1330                 $r = DBA::selectFirst('gcontact', ['url'], [
1331                         '`network` = ? 
1332                         AND `last_contact` >= `last_failure`  
1333                         AND `updated` > ?',
1334                         Protocol::DFRN,
1335                         DateTimeFormat::utc('now - 1 month'),
1336                 ], ['order' => ['RAND()']]);
1337
1338                 if (DBA::isResult($r)) {
1339                         return $r['url'];
1340                 }
1341
1342                 return '';
1343         }
1344 }