]> git.mxchange.org Git - friendica.git/blob - include/socgraph.php
55e0e6fc3a98a560c32573c442707e7c39de878a
[friendica.git] / include / socgraph.php
1 <?php
2 /**
3  * @file include/socgraph.php
4  *
5  * @todo Move GNU Social URL schemata (http://server.tld/user/number) to http://server.tld/username
6  * @todo Fetch profile data from profile page for Redmatrix users
7  * @todo Detect if it is a forum
8  */
9
10 use Friendica\App;
11 use Friendica\Core\Config;
12 use Friendica\Network\Probe;
13
14 require_once 'include/datetime.php';
15 require_once 'include/probe.php';
16 require_once 'include/network.php';
17 require_once 'include/html2bbcode.php';
18 require_once 'include/Contact.php';
19 require_once 'include/Photo.php';
20
21 /**
22  * @brief Fetch POCO data
23  *
24  * @param integer $cid Contact ID
25  * @param integer $uid User ID
26  * @param integer $zcid Global Contact ID
27  * @param integer $url POCO address that should be polled
28  *
29  * Given a contact-id (minimum), load the PortableContacts friend list for that contact,
30  * and add the entries to the gcontact (Global Contact) table, or update existing entries
31  * if anything (name or photo) has changed.
32  * We use normalised urls for comparison which ignore http vs https and www.domain vs domain
33  *
34  * Once the global contact is stored add (if necessary) the contact linkage which associates
35  * the given uid, cid to the global contact entry. There can be many uid/cid combinations
36  * pointing to the same global contact id.
37  *
38  */
39 function poco_load($cid, $uid = 0, $zcid = 0, $url = null) {
40         // Call the function "poco_load_worker" via the worker
41         proc_run(PRIORITY_LOW, "include/discover_poco.php", "poco_load", (int)$cid, (int)$uid, (int)$zcid, $url);
42 }
43
44 /**
45  * @brief Fetch POCO data from the worker
46  *
47  * @param integer $cid Contact ID
48  * @param integer $uid User ID
49  * @param integer $zcid Global Contact ID
50  * @param integer $url POCO address that should be polled
51  *
52  */
53 function poco_load_worker($cid, $uid, $zcid, $url) {
54         $a = get_app();
55
56         if ($cid) {
57                 if ((! $url) || (! $uid)) {
58                         $r = q("select `poco`, `uid` from `contact` where `id` = %d limit 1",
59                                 intval($cid)
60                         );
61                         if (dbm::is_result($r)) {
62                                 $url = $r[0]['poco'];
63                                 $uid = $r[0]['uid'];
64                         }
65                 }
66                 if (! $uid) {
67                         return;
68                 }
69         }
70
71         if (! $url) {
72                 return;
73         }
74
75         $url = $url . (($uid) ? '/@me/@all?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation' : '?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation') ;
76
77         logger('poco_load: ' . $url, LOGGER_DEBUG);
78
79         $s = fetch_url($url);
80
81         logger('poco_load: returns ' . $s, LOGGER_DATA);
82
83         logger('poco_load: return code: ' . $a->get_curl_code(), LOGGER_DEBUG);
84
85         if (($a->get_curl_code() > 299) || (! $s)) {
86                 return;
87         }
88
89         $j = json_decode($s);
90
91         logger('poco_load: json: ' . print_r($j,true),LOGGER_DATA);
92
93         if (! isset($j->entry)) {
94                 return;
95         }
96
97         $total = 0;
98         foreach ($j->entry as $entry) {
99
100                 $total ++;
101                 $profile_url = '';
102                 $profile_photo = '';
103                 $connect_url = '';
104                 $name = '';
105                 $network = '';
106                 $updated = NULL_DATE;
107                 $location = '';
108                 $about = '';
109                 $keywords = '';
110                 $gender = '';
111                 $contact_type = -1;
112                 $generation = 0;
113
114                 $name = $entry->displayName;
115
116                 if (isset($entry->urls)) {
117                         foreach ($entry->urls as $url) {
118                                 if ($url->type == 'profile') {
119                                         $profile_url = $url->value;
120                                         continue;
121                                 }
122                                 if ($url->type == 'webfinger') {
123                                         $connect_url = str_replace('acct:' , '', $url->value);
124                                         continue;
125                                 }
126                         }
127                 }
128                 if (isset($entry->photos)) {
129                         foreach ($entry->photos as $photo) {
130                                 if ($photo->type == 'profile') {
131                                         $profile_photo = $photo->value;
132                                         continue;
133                                 }
134                         }
135                 }
136
137                 if (isset($entry->updated)) {
138                         $updated = date("Y-m-d H:i:s", strtotime($entry->updated));
139                 }
140
141                 if (isset($entry->network)) {
142                         $network = $entry->network;
143                 }
144
145                 if (isset($entry->currentLocation)) {
146                         $location = $entry->currentLocation;
147                 }
148
149                 if (isset($entry->aboutMe)) {
150                         $about = html2bbcode($entry->aboutMe);
151                 }
152
153                 if (isset($entry->gender)) {
154                         $gender = $entry->gender;
155                 }
156
157                 if (isset($entry->generation) && ($entry->generation > 0)) {
158                         $generation = ++$entry->generation;
159                 }
160
161                 if (isset($entry->tags)) {
162                         foreach ($entry->tags as $tag) {
163                                 $keywords = implode(", ", $tag);
164                         }
165                 }
166
167                 if (isset($entry->contactType) && ($entry->contactType >= 0)) {
168                         $contact_type = $entry->contactType;
169                 }
170
171                 $gcontact = array("url" => $profile_url,
172                                 "name" => $name,
173                                 "network" => $network,
174                                 "photo" => $profile_photo,
175                                 "about" => $about,
176                                 "location" => $location,
177                                 "gender" => $gender,
178                                 "keywords" => $keywords,
179                                 "connect" => $connect_url,
180                                 "updated" => $updated,
181                                 "contact-type" => $contact_type,
182                                 "generation" => $generation);
183
184                 try {
185                         $gcontact = sanitize_gcontact($gcontact);
186                         $gcid = update_gcontact($gcontact);
187
188                         link_gcontact($gcid, $uid, $cid, $zcid);
189                 } catch (Exception $e) {
190                         logger($e->getMessage(), LOGGER_DEBUG);
191                 }
192         }
193         logger("poco_load: loaded $total entries",LOGGER_DEBUG);
194
195         q("DELETE FROM `glink` WHERE `cid` = %d AND `uid` = %d AND `zcid` = %d AND `updated` < UTC_TIMESTAMP - INTERVAL 2 DAY",
196                 intval($cid),
197                 intval($uid),
198                 intval($zcid)
199         );
200
201 }
202 /**
203  * @brief Sanitize the given gcontact data
204  *
205  * @param array $gcontact array with gcontact data
206  * @throw Exception
207  *
208  * Generation:
209  *  0: No definition
210  *  1: Profiles on this server
211  *  2: Contacts of profiles on this server
212  *  3: Contacts of contacts of profiles on this server
213  *  4: ...
214  *
215  */
216 function sanitize_gcontact($gcontact) {
217
218         if ($gcontact['url'] == "") {
219                 throw new Exception('URL is empty');
220         }
221
222         $urlparts = parse_url($gcontact['url']);
223         if (!isset($urlparts["scheme"])) {
224                 throw new Exception("This (".$gcontact['url'].") doesn't seem to be an url.");
225         }
226
227         if (in_array($urlparts["host"], array("www.facebook.com", "facebook.com", "twitter.com",
228                                                 "identi.ca", "alpha.app.net"))) {
229                 throw new Exception('Contact from a non federated network ignored. ('.$gcontact['url'].')');
230         }
231
232         // Don't store the statusnet connector as network
233         // We can't simply set this to NETWORK_OSTATUS since the connector could have fetched posts from friendica as well
234         if ($gcontact['network'] == NETWORK_STATUSNET) {
235                 $gcontact['network'] = "";
236         }
237
238         // Assure that there are no parameter fragments in the profile url
239         if (in_array($gcontact['network'], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) {
240                 $gcontact['url'] = clean_contact_url($gcontact['url']);
241         }
242
243         $alternate = poco_alternate_ostatus_url($gcontact['url']);
244
245         // The global contacts should contain the original picture, not the cached one
246         if (($gcontact['generation'] != 1) && stristr(normalise_link($gcontact['photo']), normalise_link(App::get_baseurl()."/photo/"))) {
247                 $gcontact['photo'] = "";
248         }
249
250         if (!isset($gcontact['network'])) {
251                 $r = q("SELECT `network` FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s' AND `network` != '' AND `network` != '%s' LIMIT 1",
252                         dbesc(normalise_link($gcontact['url'])), dbesc(NETWORK_STATUSNET)
253                 );
254                 if (dbm::is_result($r)) {
255                         $gcontact['network'] = $r[0]["network"];
256                 }
257
258                 if (($gcontact['network'] == "") || ($gcontact['network'] == NETWORK_OSTATUS)) {
259                         $r = q("SELECT `network`, `url` FROM `contact` WHERE `uid` = 0 AND `alias` IN ('%s', '%s') AND `network` != '' AND `network` != '%s' LIMIT 1",
260                                 dbesc($gcontact['url']), dbesc(normalise_link($gcontact['url'])), dbesc(NETWORK_STATUSNET)
261                         );
262                         if (dbm::is_result($r)) {
263                                 $gcontact['network'] = $r[0]["network"];
264                         }
265                 }
266         }
267
268         $gcontact['server_url'] = '';
269         $gcontact['network'] = '';
270
271         $x = q("SELECT * FROM `gcontact` WHERE `nurl` = '%s' LIMIT 1",
272                 dbesc(normalise_link($gcontact['url']))
273         );
274
275         if (dbm::is_result($x)) {
276                 if (!isset($gcontact['network']) && ($x[0]["network"] != NETWORK_STATUSNET)) {
277                         $gcontact['network'] = $x[0]["network"];
278                 }
279                 if ($gcontact['updated'] <= NULL_DATE) {
280                         $gcontact['updated'] = $x[0]["updated"];
281                 }
282                 if (!isset($gcontact['server_url']) && (normalise_link($x[0]["server_url"]) != normalise_link($x[0]["url"]))) {
283                         $gcontact['server_url'] = $x[0]["server_url"];
284                 }
285                 if (!isset($gcontact['addr'])) {
286                         $gcontact['addr'] = $x[0]["addr"];
287                 }
288         }
289
290         if ((!isset($gcontact['network']) || !isset($gcontact['name']) || !isset($gcontact['addr']) || !isset($gcontact['photo']) || !isset($gcontact['server_url']) || $alternate)
291                 && poco_reachable($gcontact['url'], $gcontact['server_url'], $gcontact['network'], false)) {
292                 $data = Probe::uri($gcontact['url']);
293
294                 if ($data["network"] == NETWORK_PHANTOM) {
295                         throw new Exception('Probing for URL '.$gcontact['url'].' failed');
296                 }
297
298                 $orig_profile = $gcontact['url'];
299
300                 $gcontact["server_url"] = $data["baseurl"];
301
302                 $gcontact = array_merge($gcontact, $data);
303
304                 if ($alternate && ($gcontact['network'] == NETWORK_OSTATUS)) {
305                         // Delete the old entry - if it exists
306                         $r = q("SELECT `id` FROM `gcontact` WHERE `nurl` = '%s'", dbesc(normalise_link($orig_profile)));
307                         if (dbm::is_result($r)) {
308                                 q("DELETE FROM `gcontact` WHERE `nurl` = '%s'", dbesc(normalise_link($orig_profile)));
309                                 q("DELETE FROM `glink` WHERE `gcid` = %d", intval($r[0]["id"]));
310                         }
311                 }
312         }
313
314         if (!isset($gcontact['name']) || !isset($gcontact['photo'])) {
315                 throw new Exception('No name and photo for URL '.$gcontact['url']);
316         }
317
318         if (!in_array($gcontact['network'], array(NETWORK_DFRN, NETWORK_OSTATUS, NETWORK_DIASPORA))) {
319                 throw new Exception('No federated network ('.$gcontact['network'].') detected for URL '.$gcontact['url']);
320         }
321
322         if (!isset($gcontact['server_url'])) {
323                 // We check the server url to be sure that it is a real one
324                 $server_url = poco_detect_server($gcontact['url']);
325
326                 // We are now sure that it is a correct URL. So we use it in the future
327                 if ($server_url != "") {
328                         $gcontact['server_url'] = $server_url;
329                 }
330         }
331
332         // The server URL doesn't seem to be valid, so we don't store it.
333         if (!poco_check_server($gcontact['server_url'], $gcontact['network'])) {
334                 $gcontact['server_url'] = "";
335         }
336
337         return $gcontact;
338 }
339
340 /**
341  * @brief Link the gcontact entry with user, contact and global contact
342  *
343  * @param integer $gcid Global contact ID
344  * @param integer $cid Contact ID
345  * @param integer $uid User ID
346  * @param integer $zcid Global Contact ID
347  * *
348  */
349 function link_gcontact($gcid, $uid = 0, $cid = 0, $zcid = 0) {
350
351         if ($gcid <= 0) {
352                 return;
353         }
354
355         $r = q("SELECT * FROM `glink` WHERE `cid` = %d AND `uid` = %d AND `gcid` = %d AND `zcid` = %d LIMIT 1",
356                 intval($cid),
357                 intval($uid),
358                 intval($gcid),
359                 intval($zcid)
360         );
361
362         if (!dbm::is_result($r)) {
363                 q("INSERT INTO `glink` (`cid`, `uid`, `gcid`, `zcid`, `updated`) VALUES (%d, %d, %d, %d, '%s') ",
364                         intval($cid),
365                         intval($uid),
366                         intval($gcid),
367                         intval($zcid),
368                         dbesc(datetime_convert())
369                 );
370         } else {
371                 q("UPDATE `glink` SET `updated` = '%s' WHERE `cid` = %d AND `uid` = %d AND `gcid` = %d AND `zcid` = %d",
372                         dbesc(datetime_convert()),
373                         intval($cid),
374                         intval($uid),
375                         intval($gcid),
376                         intval($zcid)
377                 );
378         }
379 }
380
381 function poco_reachable($profile, $server = "", $network = "", $force = false) {
382
383         if ($server == "") {
384                 $server = poco_detect_server($profile);
385         }
386
387         if ($server == "") {
388                 return true;
389         }
390
391         return poco_check_server($server, $network, $force);
392 }
393
394 function poco_detect_server($profile) {
395
396         // Try to detect the server path based upon some known standard paths
397         $server_url = "";
398
399         if ($server_url == "") {
400                 $friendica = preg_replace("=(https?://)(.*)/profile/(.*)=ism", "$1$2", $profile);
401                 if ($friendica != $profile) {
402                         $server_url = $friendica;
403                         $network = NETWORK_DFRN;
404                 }
405         }
406
407         if ($server_url == "") {
408                 $diaspora = preg_replace("=(https?://)(.*)/u/(.*)=ism", "$1$2", $profile);
409                 if ($diaspora != $profile) {
410                         $server_url = $diaspora;
411                         $network = NETWORK_DIASPORA;
412                 }
413         }
414
415         if ($server_url == "") {
416                 $red = preg_replace("=(https?://)(.*)/channel/(.*)=ism", "$1$2", $profile);
417                 if ($red != $profile) {
418                         $server_url = $red;
419                         $network = NETWORK_DIASPORA;
420                 }
421         }
422
423         // Mastodon
424         if ($server_url == "") {
425                 $mastodon = preg_replace("=(https?://)(.*)/users/(.*)=ism", "$1$2", $profile);
426                 if ($mastodon != $profile) {
427                         $server_url = $mastodon;
428                         $network = NETWORK_OSTATUS;
429                 }
430         }
431
432         // Numeric OStatus variant
433         if ($server_url == "") {
434                 $ostatus = preg_replace("=(https?://)(.*)/user/(.*)=ism", "$1$2", $profile);
435                 if ($ostatus != $profile) {
436                         $server_url = $ostatus;
437                         $network = NETWORK_OSTATUS;
438                 }
439         }
440
441         // Wild guess
442         if ($server_url == "") {
443                 $base = preg_replace("=(https?://)(.*?)/(.*)=ism", "$1$2", $profile);
444                 if ($base != $profile) {
445                         $server_url = $base;
446                         $network = NETWORK_PHANTOM;
447                 }
448         }
449
450         if ($server_url == "") {
451                 return "";
452         }
453
454         $r = q("SELECT `id` FROM `gserver` WHERE `nurl` = '%s' AND `last_contact` > `last_failure`",
455                 dbesc(normalise_link($server_url)));
456         if (dbm::is_result($r)) {
457                 return $server_url;
458         }
459
460         // Fetch the host-meta to check if this really is a server
461         $serverret = z_fetch_url($server_url."/.well-known/host-meta");
462         if (!$serverret["success"]) {
463                 return "";
464         }
465
466         return $server_url;
467 }
468
469 function poco_alternate_ostatus_url($url) {
470         return(preg_match("=https?://.+/user/\d+=ism", $url, $matches));
471 }
472
473 function poco_last_updated($profile, $force = false) {
474
475         $gcontacts = q("SELECT * FROM `gcontact` WHERE `nurl` = '%s'",
476                         dbesc(normalise_link($profile)));
477
478         if (!dbm::is_result($gcontacts)) {
479                 return false;
480         }
481
482         $contact = array("url" => $profile);
483
484         if ($gcontacts[0]["created"] <= NULL_DATE) {
485                 $contact['created'] = datetime_convert();
486         }
487
488         if ($force) {
489                 $server_url = normalise_link(poco_detect_server($profile));
490         }
491
492         if (($server_url == '') && ($gcontacts[0]["server_url"] != "")) {
493                 $server_url = $gcontacts[0]["server_url"];
494         }
495
496         if (!$force && (($server_url == '') || ($gcontacts[0]["server_url"] == $gcontacts[0]["nurl"]))) {
497                 $server_url = normalise_link(poco_detect_server($profile));
498         }
499
500         if (!in_array($gcontacts[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_FEED, NETWORK_OSTATUS, ""))) {
501                 logger("Profile ".$profile.": Network type ".$gcontacts[0]["network"]." can't be checked", LOGGER_DEBUG);
502                 return false;
503         }
504
505         if ($server_url != "") {
506                 if (!poco_check_server($server_url, $gcontacts[0]["network"], $force)) {
507                         if ($force) {
508                                 q("UPDATE `gcontact` SET `last_failure` = '%s' WHERE `nurl` = '%s'",
509                                         dbesc(datetime_convert()), dbesc(normalise_link($profile)));
510                         }
511
512                         logger("Profile ".$profile.": Server ".$server_url." wasn't reachable.", LOGGER_DEBUG);
513                         return false;
514                 }
515                 $contact['server_url'] = $server_url;
516         }
517
518         if (in_array($gcontacts[0]["network"], array("", NETWORK_FEED))) {
519                 $server = q("SELECT `network` FROM `gserver` WHERE `nurl` = '%s' AND `network` != ''",
520                         dbesc(normalise_link($server_url)));
521
522                 if ($server) {
523                         $contact['network'] = $server[0]["network"];
524                 } else {
525                         return false;
526                 }
527         }
528
529         // noscrape is really fast so we don't cache the call.
530         if (($server_url != "") && ($gcontacts[0]["nick"] != "")) {
531
532                 //  Use noscrape if possible
533                 $server = q("SELECT `noscrape`, `network` FROM `gserver` WHERE `nurl` = '%s' AND `noscrape` != ''", dbesc(normalise_link($server_url)));
534
535                 if ($server) {
536                         $noscraperet = z_fetch_url($server[0]["noscrape"]."/".$gcontacts[0]["nick"]);
537
538                         if ($noscraperet["success"] && ($noscraperet["body"] != "")) {
539
540                                 $noscrape = json_decode($noscraperet["body"], true);
541
542                                 if (is_array($noscrape)) {
543                                         $contact["network"] = $server[0]["network"];
544
545                                         if (isset($noscrape["fn"])) {
546                                                 $contact["name"] = $noscrape["fn"];
547                                         }
548                                         if (isset($noscrape["comm"])) {
549                                                 $contact["community"] = $noscrape["comm"];
550                                         }
551                                         if (isset($noscrape["tags"])) {
552                                                 $keywords = implode(" ", $noscrape["tags"]);
553                                                 if ($keywords != "") {
554                                                         $contact["keywords"] = $keywords;
555                                                 }
556                                         }
557
558                                         $location = formatted_location($noscrape);
559                                         if ($location) {
560                                                 $contact["location"] = $location;
561                                         }
562                                         if (isset($noscrape["dfrn-notify"])) {
563                                                 $contact["notify"] = $noscrape["dfrn-notify"];
564                                         }
565                                         // Remove all fields that are not present in the gcontact table
566                                         unset($noscrape["fn"]);
567                                         unset($noscrape["key"]);
568                                         unset($noscrape["homepage"]);
569                                         unset($noscrape["comm"]);
570                                         unset($noscrape["tags"]);
571                                         unset($noscrape["locality"]);
572                                         unset($noscrape["region"]);
573                                         unset($noscrape["country-name"]);
574                                         unset($noscrape["contacts"]);
575                                         unset($noscrape["dfrn-request"]);
576                                         unset($noscrape["dfrn-confirm"]);
577                                         unset($noscrape["dfrn-notify"]);
578                                         unset($noscrape["dfrn-poll"]);
579
580                                         // Set the date of the last contact
581                                         /// @todo By now the function "update_gcontact" doesn't work with this field
582                                         //$contact["last_contact"] = datetime_convert();
583
584                                         $contact = array_merge($contact, $noscrape);
585
586                                         update_gcontact($contact);
587
588                                         if (trim($noscrape["updated"]) != "") {
589                                                 q("UPDATE `gcontact` SET `last_contact` = '%s' WHERE `nurl` = '%s'",
590                                                         dbesc(datetime_convert()), dbesc(normalise_link($profile)));
591
592                                                 logger("Profile ".$profile." was last updated at ".$noscrape["updated"]." (noscrape)", LOGGER_DEBUG);
593
594                                                 return $noscrape["updated"];
595                                         }
596                                 }
597                         }
598                 }
599         }
600
601         // If we only can poll the feed, then we only do this once a while
602         if (!$force && !poco_do_update($gcontacts[0]["created"], $gcontacts[0]["updated"], $gcontacts[0]["last_failure"], $gcontacts[0]["last_contact"])) {
603                 logger("Profile ".$profile." was last updated at ".$gcontacts[0]["updated"]." (cached)", LOGGER_DEBUG);
604
605                 update_gcontact($contact);
606                 return $gcontacts[0]["updated"];
607         }
608
609         $data = Probe::uri($profile);
610
611         // Is the profile link the alternate OStatus link notation? (http://domain.tld/user/4711)
612         // Then check the other link and delete this one
613         if (($data["network"] == NETWORK_OSTATUS) && poco_alternate_ostatus_url($profile) &&
614                 (normalise_link($profile) == normalise_link($data["alias"])) &&
615                 (normalise_link($profile) != normalise_link($data["url"]))) {
616
617                 // Delete the old entry
618                 q("DELETE FROM `gcontact` WHERE `nurl` = '%s'", dbesc(normalise_link($profile)));
619                 q("DELETE FROM `glink` WHERE `gcid` = %d", intval($gcontacts[0]["id"]));
620
621                 $gcontact = array_merge($gcontacts[0], $data);
622
623                 $gcontact["server_url"] = $data["baseurl"];
624
625                 try {
626                         $gcontact = sanitize_gcontact($gcontact);
627                         update_gcontact($gcontact);
628
629                         poco_last_updated($data["url"], $force);
630                 } catch (Exception $e) {
631                         logger($e->getMessage(), LOGGER_DEBUG);
632                 }
633
634                 logger("Profile ".$profile." was deleted", LOGGER_DEBUG);
635                 return false;
636         }
637
638         if (($data["poll"] == "") || (in_array($data["network"], array(NETWORK_FEED, NETWORK_PHANTOM)))) {
639                 q("UPDATE `gcontact` SET `last_failure` = '%s' WHERE `nurl` = '%s'",
640                         dbesc(datetime_convert()), dbesc(normalise_link($profile)));
641
642                 logger("Profile ".$profile." wasn't reachable (profile)", LOGGER_DEBUG);
643                 return false;
644         }
645
646         $contact = array_merge($contact, $data);
647
648         $contact["server_url"] = $data["baseurl"];
649
650         update_gcontact($contact);
651
652         $feedret = z_fetch_url($data["poll"]);
653
654         if (!$feedret["success"]) {
655                 q("UPDATE `gcontact` SET `last_failure` = '%s' WHERE `nurl` = '%s'",
656                         dbesc(datetime_convert()), dbesc(normalise_link($profile)));
657
658                 logger("Profile ".$profile." wasn't reachable (no feed)", LOGGER_DEBUG);
659                 return false;
660         }
661
662         $doc = new DOMDocument();
663         @$doc->loadXML($feedret["body"]);
664
665         $xpath = new DomXPath($doc);
666         $xpath->registerNamespace('atom', "http://www.w3.org/2005/Atom");
667
668         $entries = $xpath->query('/atom:feed/atom:entry');
669
670         $last_updated = "";
671
672         foreach ($entries as $entry) {
673                 $published = $xpath->query('atom:published/text()', $entry)->item(0)->nodeValue;
674                 $updated = $xpath->query('atom:updated/text()', $entry)->item(0)->nodeValue;
675
676                 if ($last_updated < $published)
677                         $last_updated = $published;
678
679                 if ($last_updated < $updated)
680                         $last_updated = $updated;
681         }
682
683         // Maybe there aren't any entries. Then check if it is a valid feed
684         if ($last_updated == "") {
685                 if ($xpath->query('/atom:feed')->length > 0) {
686                         $last_updated = NULL_DATE;
687                 }
688         }
689         q("UPDATE `gcontact` SET `updated` = '%s', `last_contact` = '%s' WHERE `nurl` = '%s'",
690                 dbesc(dbm::date($last_updated)), dbesc(dbm::date()), dbesc(normalise_link($profile)));
691
692         if (($gcontacts[0]["generation"] == 0)) {
693                 q("UPDATE `gcontact` SET `generation` = 9 WHERE `nurl` = '%s'",
694                         dbesc(normalise_link($profile)));
695         }
696
697         logger("Profile ".$profile." was last updated at ".$last_updated, LOGGER_DEBUG);
698
699         return($last_updated);
700 }
701
702 function poco_do_update($created, $updated, $last_failure,  $last_contact) {
703         $now = strtotime(datetime_convert());
704
705         if ($updated > $last_contact) {
706                 $contact_time = strtotime($updated);
707         } else {
708                 $contact_time = strtotime($last_contact);
709         }
710
711         $failure_time = strtotime($last_failure);
712         $created_time = strtotime($created);
713
714         // If there is no "created" time then use the current time
715         if ($created_time <= 0) {
716                 $created_time = $now;
717         }
718
719         // If the last contact was less than 24 hours then don't update
720         if (($now - $contact_time) < (60 * 60 * 24)) {
721                 return false;
722         }
723
724         // If the last failure was less than 24 hours then don't update
725         if (($now - $failure_time) < (60 * 60 * 24)) {
726                 return false;
727         }
728
729         // If the last contact was less than a week ago and the last failure is older than a week then don't update
730         //if ((($now - $contact_time) < (60 * 60 * 24 * 7)) && ($contact_time > $failure_time))
731         //      return false;
732
733         // If the last contact time was more than a week ago and the contact was created more than a week ago, then only try once a week
734         if ((($now - $contact_time) > (60 * 60 * 24 * 7)) && (($now - $created_time) > (60 * 60 * 24 * 7)) && (($now - $failure_time) < (60 * 60 * 24 * 7))) {
735                 return false;
736         }
737
738         // If the last contact time was more than a month ago and the contact was created more than a month ago, then only try once a month
739         if ((($now - $contact_time) > (60 * 60 * 24 * 30)) && (($now - $created_time) > (60 * 60 * 24 * 30)) && (($now - $failure_time) < (60 * 60 * 24 * 30))) {
740                 return false;
741         }
742
743         return true;
744 }
745
746 function poco_to_boolean($val) {
747         if (($val == "true") || ($val == 1)) {
748                 return true;
749         } elseif (($val == "false") || ($val == 0)) {
750                 return false;
751         }
752
753         return $val;
754 }
755
756 /**
757  * @brief Detect server type (Hubzilla or Friendica) via the poco data
758  *
759  * @param object $data POCO data
760  * @return array Server data
761  */
762 function poco_detect_poco_data($data) {
763         $server = false;
764
765         if (!isset($data->entry)) {
766                 return false;
767         }
768
769         if (count($data->entry) == 0) {
770                 return false;
771         }
772
773         if (!isset($data->entry[0]->urls)) {
774                 return false;
775         }
776
777         if (count($data->entry[0]->urls) == 0) {
778                 return false;
779         }
780
781         foreach ($data->entry[0]->urls as $url) {
782                 if ($url->type == 'zot') {
783                         $server = array();
784                         $server["platform"] = 'Hubzilla';
785                         $server["network"] = NETWORK_DIASPORA;
786                         return $server;
787                 }
788         }
789         return false;
790 }
791
792 /**
793  * @brief Detect server type by using the nodeinfo data
794  *
795  * @param string $server_url address of the server
796  * @return array Server data
797  */
798 function poco_fetch_nodeinfo($server_url) {
799         $serverret = z_fetch_url($server_url."/.well-known/nodeinfo");
800         if (!$serverret["success"]) {
801                 return false;
802         }
803
804         $nodeinfo = json_decode($serverret['body']);
805
806         if (!is_object($nodeinfo)) {
807                 return false;
808         }
809
810         if (!is_array($nodeinfo->links)) {
811                 return false;
812         }
813
814         $nodeinfo_url = '';
815
816         foreach ($nodeinfo->links as $link) {
817                 if ($link->rel == 'http://nodeinfo.diaspora.software/ns/schema/1.0') {
818                         $nodeinfo_url = $link->href;
819                 }
820         }
821
822         if ($nodeinfo_url == '') {
823                 return false;
824         }
825
826         $serverret = z_fetch_url($nodeinfo_url);
827         if (!$serverret["success"]) {
828                 return false;
829         }
830
831         $nodeinfo = json_decode($serverret['body']);
832
833         if (!is_object($nodeinfo)) {
834                 return false;
835         }
836
837         $server = array();
838
839         $server['register_policy'] = REGISTER_CLOSED;
840
841         if (is_bool($nodeinfo->openRegistrations) && $nodeinfo->openRegistrations) {
842                 $server['register_policy'] = REGISTER_OPEN;
843         }
844
845         if (is_object($nodeinfo->software)) {
846                 if (isset($nodeinfo->software->name)) {
847                         $server['platform'] = $nodeinfo->software->name;
848                 }
849
850                 if (isset($nodeinfo->software->version)) {
851                         $server['version'] = $nodeinfo->software->version;
852                         // Version numbers on Nodeinfo are presented with additional info, e.g.:
853                         // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
854                         $server['version'] = preg_replace("=(.+)-(.{4,})=ism", "$1", $server['version']);
855                 }
856         }
857
858         if (is_object($nodeinfo->metadata)) {
859                 if (isset($nodeinfo->metadata->nodeName)) {
860                         $server['site_name'] = $nodeinfo->metadata->nodeName;
861                 }
862         }
863
864         $diaspora = false;
865         $friendica = false;
866         $gnusocial = false;
867
868         if (is_array($nodeinfo->protocols->inbound)) {
869                 foreach ($nodeinfo->protocols->inbound as $inbound) {
870                         if ($inbound == 'diaspora') {
871                                 $diaspora = true;
872                         }
873                         if ($inbound == 'friendica') {
874                                 $friendica = true;
875                         }
876                         if ($inbound == 'gnusocial') {
877                                 $gnusocial = true;
878                         }
879                 }
880         }
881
882         if ($gnusocial) {
883                 $server['network'] = NETWORK_OSTATUS;
884         }
885         if ($diaspora) {
886                 $server['network'] = NETWORK_DIASPORA;
887         }
888         if ($friendica) {
889                 $server['network'] = NETWORK_DFRN;
890         }
891
892         if (!$server) {
893                 return false;
894         }
895
896         return $server;
897 }
898
899 /**
900  * @brief Detect server type (Hubzilla or Friendica) via the front page body
901  *
902  * @param string $body Front page of the server
903  * @return array Server data
904  */
905 function poco_detect_server_type($body) {
906         $server = false;
907
908         $doc = new DOMDocument();
909         @$doc->loadHTML($body);
910         $xpath = new DomXPath($doc);
911
912         $list = $xpath->query("//meta[@name]");
913
914         foreach ($list as $node) {
915                 $attr = array();
916                 if ($node->attributes->length) {
917                         foreach ($node->attributes as $attribute) {
918                                 $attr[$attribute->name] = $attribute->value;
919                         }
920                 }
921                 if ($attr['name'] == 'generator') {
922                         $version_part = explode(" ", $attr['content']);
923                         if (count($version_part) == 2) {
924                                 if (in_array($version_part[0], array("Friendika", "Friendica"))) {
925                                         $server = array();
926                                         $server["platform"] = $version_part[0];
927                                         $server["version"] = $version_part[1];
928                                         $server["network"] = NETWORK_DFRN;
929                                 }
930                         }
931                 }
932         }
933
934         if (!$server) {
935                 $list = $xpath->query("//meta[@property]");
936
937                 foreach ($list as $node) {
938                         $attr = array();
939                         if ($node->attributes->length) {
940                                 foreach ($node->attributes as $attribute) {
941                                         $attr[$attribute->name] = $attribute->value;
942                                 }
943                         }
944                         if ($attr['property'] == 'generator' && in_array($attr['content'], array("hubzilla", "BlaBlaNet"))) {
945                                 $server = array();
946                                 $server["platform"] = $attr['content'];
947                                 $server["version"] = "";
948                                 $server["network"] = NETWORK_DIASPORA;
949                         }
950                 }
951         }
952
953         if (!$server) {
954                 return false;
955         }
956
957         $server["site_name"] = $xpath->evaluate($element."//head/title/text()", $context)->item(0)->nodeValue;
958         return $server;
959 }
960
961 function poco_check_server($server_url, $network = "", $force = false) {
962
963         // Unify the server address
964         $server_url = trim($server_url, "/");
965         $server_url = str_replace("/index.php", "", $server_url);
966
967         if ($server_url == "") {
968                 return false;
969         }
970
971         $servers = q("SELECT * FROM `gserver` WHERE `nurl` = '%s'", dbesc(normalise_link($server_url)));
972         if (dbm::is_result($servers)) {
973
974                 if ($servers[0]["created"] <= NULL_DATE) {
975                         q("UPDATE `gserver` SET `created` = '%s' WHERE `nurl` = '%s'",
976                                 dbesc(datetime_convert()), dbesc(normalise_link($server_url)));
977                 }
978                 $poco = $servers[0]["poco"];
979                 $noscrape = $servers[0]["noscrape"];
980
981                 if ($network == "") {
982                         $network = $servers[0]["network"];
983                 }
984
985                 $last_contact = $servers[0]["last_contact"];
986                 $last_failure = $servers[0]["last_failure"];
987                 $version = $servers[0]["version"];
988                 $platform = $servers[0]["platform"];
989                 $site_name = $servers[0]["site_name"];
990                 $info = $servers[0]["info"];
991                 $register_policy = $servers[0]["register_policy"];
992
993                 if (!$force && !poco_do_update($servers[0]["created"], "", $last_failure, $last_contact)) {
994                         logger("Use cached data for server ".$server_url, LOGGER_DEBUG);
995                         return ($last_contact >= $last_failure);
996                 }
997         } else {
998                 $poco = "";
999                 $noscrape = "";
1000                 $version = "";
1001                 $platform = "";
1002                 $site_name = "";
1003                 $info = "";
1004                 $register_policy = -1;
1005
1006                 $last_contact = NULL_DATE;
1007                 $last_failure = NULL_DATE;
1008         }
1009         logger("Server ".$server_url." is outdated or unknown. Start discovery. Force: ".$force." Created: ".$servers[0]["created"]." Failure: ".$last_failure." Contact: ".$last_contact, LOGGER_DEBUG);
1010
1011         $failure = false;
1012         $possible_failure = false;
1013         $orig_last_failure = $last_failure;
1014         $orig_last_contact = $last_contact;
1015
1016         // Check if the page is accessible via SSL.
1017         $orig_server_url = $server_url;
1018         $server_url = str_replace("http://", "https://", $server_url);
1019
1020         // We set the timeout to 20 seconds since this operation should be done in no time if the server was vital
1021         $serverret = z_fetch_url($server_url."/.well-known/host-meta", false, $redirects, array('timeout' => 20));
1022
1023         // Quit if there is a timeout.
1024         // But we want to make sure to only quit if we are mostly sure that this server url fits.
1025         if (dbm::is_result($servers) && ($orig_server_url == $server_url) &&
1026                 ($serverret['errno'] == CURLE_OPERATION_TIMEDOUT)) {
1027                 logger("Connection to server ".$server_url." timed out.", LOGGER_DEBUG);
1028                 dba::p("UPDATE `gserver` SET `last_failure` = ? WHERE `nurl` = ?", datetime_convert(), normalise_link($server_url));
1029                 return false;
1030         }
1031
1032         // Maybe the page is unencrypted only?
1033         $xmlobj = @simplexml_load_string($serverret["body"],'SimpleXMLElement',0, "http://docs.oasis-open.org/ns/xri/xrd-1.0");
1034         if (!$serverret["success"] || ($serverret["body"] == "") || (@sizeof($xmlobj) == 0) || !is_object($xmlobj)) {
1035                 $server_url = str_replace("https://", "http://", $server_url);
1036
1037                 // We set the timeout to 20 seconds since this operation should be done in no time if the server was vital
1038                 $serverret = z_fetch_url($server_url."/.well-known/host-meta", false, $redirects, array('timeout' => 20));
1039
1040                 // Quit if there is a timeout
1041                 if ($serverret['errno'] == CURLE_OPERATION_TIMEDOUT) {
1042                         logger("Connection to server ".$server_url." timed out.", LOGGER_DEBUG);
1043                         dba::p("UPDATE `gserver` SET `last_failure` = ? WHERE `nurl` = ?", datetime_convert(), normalise_link($server_url));
1044                         return false;
1045                 }
1046
1047                 $xmlobj = @simplexml_load_string($serverret["body"],'SimpleXMLElement',0, "http://docs.oasis-open.org/ns/xri/xrd-1.0");
1048         }
1049
1050         if (!$serverret["success"] || ($serverret["body"] == "") || (sizeof($xmlobj) == 0) || !is_object($xmlobj)) {
1051                 // Workaround for bad configured servers (known nginx problem)
1052                 if (!in_array($serverret["debug"]["http_code"], array("403", "404"))) {
1053                         $failure = true;
1054                 }
1055                 $possible_failure = true;
1056         }
1057
1058         // If the server has no possible failure we reset the cached data
1059         if (!$possible_failure) {
1060                 $version = "";
1061                 $platform = "";
1062                 $site_name = "";
1063                 $info = "";
1064                 $register_policy = -1;
1065         }
1066
1067         // Look for poco
1068         if (!$failure) {
1069                 $serverret = z_fetch_url($server_url."/poco");
1070                 if ($serverret["success"]) {
1071                         $data = json_decode($serverret["body"]);
1072                         if (isset($data->totalResults)) {
1073                                 $poco = $server_url."/poco";
1074                                 $server = poco_detect_poco_data($data);
1075                                 if ($server) {
1076                                         $platform = $server['platform'];
1077                                         $network = $server['network'];
1078                                         $version = '';
1079                                         $site_name = '';
1080                                 }
1081                         }
1082                 }
1083         }
1084
1085         if (!$failure) {
1086                 // Test for Diaspora, Hubzilla, Mastodon or older Friendica servers
1087                 $serverret = z_fetch_url($server_url);
1088
1089                 if (!$serverret["success"] || ($serverret["body"] == "")) {
1090                         $failure = true;
1091                 } else {
1092                         $server = poco_detect_server_type($serverret["body"]);
1093                         if ($server) {
1094                                 $platform = $server['platform'];
1095                                 $network = $server['network'];
1096                                 $version = $server['version'];
1097                                 $site_name = $server['site_name'];
1098                         }
1099
1100                         $lines = explode("\n",$serverret["header"]);
1101                         if (count($lines)) {
1102                                 foreach($lines as $line) {
1103                                         $line = trim($line);
1104                                         if (stristr($line,'X-Diaspora-Version:')) {
1105                                                 $platform = "Diaspora";
1106                                                 $version = trim(str_replace("X-Diaspora-Version:", "", $line));
1107                                                 $version = trim(str_replace("x-diaspora-version:", "", $version));
1108                                                 $network = NETWORK_DIASPORA;
1109                                                 $versionparts = explode("-", $version);
1110                                                 $version = $versionparts[0];
1111                                         }
1112
1113                                         if (stristr($line,'Server: Mastodon')) {
1114                                                 $platform = "Mastodon";
1115                                                 $network = NETWORK_OSTATUS;
1116                                         }
1117                                 }
1118                         }
1119                 }
1120         }
1121
1122         if (!$failure && ($poco == "")) {
1123                 // Test for Statusnet
1124                 // Will also return data for Friendica and GNU Social - but it will be overwritten later
1125                 // The "not implemented" is a special treatment for really, really old Friendica versions
1126                 $serverret = z_fetch_url($server_url."/api/statusnet/version.json");
1127                 if ($serverret["success"] && ($serverret["body"] != '{"error":"not implemented"}') &&
1128                         ($serverret["body"] != '') && (strlen($serverret["body"]) < 30)) {
1129                         $platform = "StatusNet";
1130                         // Remove junk that some GNU Social servers return
1131                         $version = str_replace(chr(239).chr(187).chr(191), "", $serverret["body"]);
1132                         $version = trim($version, '"');
1133                         $network = NETWORK_OSTATUS;
1134                 }
1135
1136                 // Test for GNU Social
1137                 $serverret = z_fetch_url($server_url."/api/gnusocial/version.json");
1138                 if ($serverret["success"] && ($serverret["body"] != '{"error":"not implemented"}') &&
1139                         ($serverret["body"] != '') && (strlen($serverret["body"]) < 30)) {
1140                         $platform = "GNU Social";
1141                         // Remove junk that some GNU Social servers return
1142                         $version = str_replace(chr(239).chr(187).chr(191), "", $serverret["body"]);
1143                         $version = trim($version, '"');
1144                         $network = NETWORK_OSTATUS;
1145                 }
1146
1147                 // Test for Mastodon
1148                 $serverret = z_fetch_url($server_url."/api/v1/instance");
1149                 if ($serverret["success"] && ($serverret["body"] != '')) {
1150                         $data = json_decode($serverret["body"]);
1151                         if (isset($data->version)) {
1152                                 $platform = "Mastodon";
1153                                 $version = $data->version;
1154                                 $site_name = $data->title;
1155                                 $info = $data->description;
1156                                 $network = NETWORK_OSTATUS;
1157                         }
1158                 }
1159         }
1160
1161         if (!$failure) {
1162                 // Test for Hubzilla and Red
1163                 $serverret = z_fetch_url($server_url."/siteinfo.json");
1164                 if ($serverret["success"]) {
1165                         $data = json_decode($serverret["body"]);
1166                         if (isset($data->url)) {
1167                                 $platform = $data->platform;
1168                                 $version = $data->version;
1169                                 $network = NETWORK_DIASPORA;
1170                         }
1171                         $site_name = $data->site_name;
1172                         switch ($data->register_policy) {
1173                                 case "REGISTER_OPEN":
1174                                         $register_policy = REGISTER_OPEN;
1175                                         break;
1176                                 case "REGISTER_APPROVE":
1177                                         $register_policy = REGISTER_APPROVE;
1178                                         break;
1179                                 case "REGISTER_CLOSED":
1180                                 default:
1181                                         $register_policy = REGISTER_CLOSED;
1182                                         break;
1183                         }
1184                 } else {
1185                         // Test for Hubzilla, Redmatrix or Friendica
1186                         $serverret = z_fetch_url($server_url."/api/statusnet/config.json");
1187                         if ($serverret["success"]) {
1188                                 $data = json_decode($serverret["body"]);
1189                                 if (isset($data->site->server)) {
1190                                         if (isset($data->site->platform)) {
1191                                                 $platform = $data->site->platform->PLATFORM_NAME;
1192                                                 $version = $data->site->platform->STD_VERSION;
1193                                                 $network = NETWORK_DIASPORA;
1194                                         }
1195                                         if (isset($data->site->BlaBlaNet)) {
1196                                                 $platform = $data->site->BlaBlaNet->PLATFORM_NAME;
1197                                                 $version = $data->site->BlaBlaNet->STD_VERSION;
1198                                                 $network = NETWORK_DIASPORA;
1199                                         }
1200                                         if (isset($data->site->hubzilla)) {
1201                                                 $platform = $data->site->hubzilla->PLATFORM_NAME;
1202                                                 $version = $data->site->hubzilla->RED_VERSION;
1203                                                 $network = NETWORK_DIASPORA;
1204                                         }
1205                                         if (isset($data->site->redmatrix)) {
1206                                                 if (isset($data->site->redmatrix->PLATFORM_NAME)) {
1207                                                         $platform = $data->site->redmatrix->PLATFORM_NAME;
1208                                                 } elseif (isset($data->site->redmatrix->RED_PLATFORM)) {
1209                                                         $platform = $data->site->redmatrix->RED_PLATFORM;
1210                                                 }
1211
1212                                                 $version = $data->site->redmatrix->RED_VERSION;
1213                                                 $network = NETWORK_DIASPORA;
1214                                         }
1215                                         if (isset($data->site->friendica)) {
1216                                                 $platform = $data->site->friendica->FRIENDICA_PLATFORM;
1217                                                 $version = $data->site->friendica->FRIENDICA_VERSION;
1218                                                 $network = NETWORK_DFRN;
1219                                         }
1220
1221                                         $site_name = $data->site->name;
1222
1223                                         $data->site->closed = poco_to_boolean($data->site->closed);
1224                                         $data->site->private = poco_to_boolean($data->site->private);
1225                                         $data->site->inviteonly = poco_to_boolean($data->site->inviteonly);
1226
1227                                         if (!$data->site->closed && !$data->site->private and $data->site->inviteonly) {
1228                                                 $register_policy = REGISTER_APPROVE;
1229                                         } elseif (!$data->site->closed && !$data->site->private) {
1230                                                 $register_policy = REGISTER_OPEN;
1231                                         } else {
1232                                                 $register_policy = REGISTER_CLOSED;
1233                                         }
1234                                 }
1235                         }
1236                 }
1237         }
1238
1239         // Query statistics.json. Optional package for Diaspora, Friendica and Redmatrix
1240         if (!$failure) {
1241                 $serverret = z_fetch_url($server_url."/statistics.json");
1242                 if ($serverret["success"]) {
1243                         $data = json_decode($serverret["body"]);
1244                         if (isset($data->version)) {
1245                                 $version = $data->version;
1246                                 // Version numbers on statistics.json are presented with additional info, e.g.:
1247                                 // 0.6.3.0-p1702cc1c, 0.6.99.0-p1b9ab160 or 3.4.3-2-1191.
1248                                 $version = preg_replace("=(.+)-(.{4,})=ism", "$1", $version);
1249                         }
1250
1251                         $site_name = $data->name;
1252
1253                         if (isset($data->network)) {
1254                                 $platform = $data->network;
1255                         }
1256
1257                         if ($platform == "Diaspora") {
1258                                 $network = NETWORK_DIASPORA;
1259                         }
1260
1261                         if ($data->registrations_open) {
1262                                 $register_policy = REGISTER_OPEN;
1263                         } else {
1264                                 $register_policy = REGISTER_CLOSED;
1265                         }
1266                 }
1267         }
1268
1269         // Query nodeinfo. Working for (at least) Diaspora and Friendica.
1270         if (!$failure) {
1271                 $server = poco_fetch_nodeinfo($server_url);
1272                 if ($server) {
1273                         $register_policy = $server['register_policy'];
1274
1275                         if (isset($server['platform'])) {
1276                                 $platform = $server['platform'];
1277                         }
1278
1279                         if (isset($server['network'])) {
1280                                 $network = $server['network'];
1281                         }
1282
1283                         if (isset($server['version'])) {
1284                                 $version = $server['version'];
1285                         }
1286
1287                         if (isset($server['site_name'])) {
1288                                 $site_name = $server['site_name'];
1289                         }
1290                 }
1291         }
1292
1293         // Check for noscrape
1294         // Friendica servers could be detected as OStatus servers
1295         if (!$failure && in_array($network, array(NETWORK_DFRN, NETWORK_OSTATUS))) {
1296                 $serverret = z_fetch_url($server_url."/friendica/json");
1297
1298                 if (!$serverret["success"]) {
1299                         $serverret = z_fetch_url($server_url."/friendika/json");
1300                 }
1301
1302                 if ($serverret["success"]) {
1303                         $data = json_decode($serverret["body"]);
1304
1305                         if (isset($data->version)) {
1306                                 $network = NETWORK_DFRN;
1307
1308                                 $noscrape = $data->no_scrape_url;
1309                                 $version = $data->version;
1310                                 $site_name = $data->site_name;
1311                                 $info = $data->info;
1312                                 $register_policy_str = $data->register_policy;
1313                                 $platform = $data->platform;
1314
1315                                 switch ($register_policy_str) {
1316                                         case "REGISTER_CLOSED":
1317                                                 $register_policy = REGISTER_CLOSED;
1318                                                 break;
1319                                         case "REGISTER_APPROVE":
1320                                                 $register_policy = REGISTER_APPROVE;
1321                                                 break;
1322                                         case "REGISTER_OPEN":
1323                                                 $register_policy = REGISTER_OPEN;
1324                                                 break;
1325                                 }
1326                         }
1327                 }
1328         }
1329
1330         if ($possible_failure && !$failure) {
1331                 $failure = true;
1332         }
1333
1334         if ($failure) {
1335                 $last_contact = $orig_last_contact;
1336                 $last_failure = datetime_convert();
1337         } else {
1338                 $last_contact = datetime_convert();
1339                 $last_failure = $orig_last_failure;
1340         }
1341
1342         if (($last_contact <= $last_failure) && !$failure) {
1343                 logger("Server ".$server_url." seems to be alive, but last contact wasn't set - could be a bug", LOGGER_DEBUG);
1344         } elseif (($last_contact >= $last_failure) && $failure) {
1345                 logger("Server ".$server_url." seems to be dead, but last failure wasn't set - could be a bug", LOGGER_DEBUG);
1346         }
1347
1348         // Check again if the server exists
1349         $servers = q("SELECT `nurl` FROM `gserver` WHERE `nurl` = '%s'", dbesc(normalise_link($server_url)));
1350
1351         $version = strip_tags($version);
1352         $site_name = strip_tags($site_name);
1353         $info = strip_tags($info);
1354         $platform = strip_tags($platform);
1355
1356         if ($servers) {
1357                  q("UPDATE `gserver` SET `url` = '%s', `version` = '%s', `site_name` = '%s', `info` = '%s', `register_policy` = %d, `poco` = '%s', `noscrape` = '%s',
1358                         `network` = '%s', `platform` = '%s', `last_contact` = '%s', `last_failure` = '%s' WHERE `nurl` = '%s'",
1359                         dbesc($server_url),
1360                         dbesc($version),
1361                         dbesc($site_name),
1362                         dbesc($info),
1363                         intval($register_policy),
1364                         dbesc($poco),
1365                         dbesc($noscrape),
1366                         dbesc($network),
1367                         dbesc($platform),
1368                         dbesc($last_contact),
1369                         dbesc($last_failure),
1370                         dbesc(normalise_link($server_url))
1371                 );
1372         } elseif (!$failure) {
1373                 q("INSERT INTO `gserver` (`url`, `nurl`, `version`, `site_name`, `info`, `register_policy`, `poco`, `noscrape`, `network`, `platform`, `created`, `last_contact`, `last_failure`)
1374                                         VALUES ('%s', '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s')",
1375                                 dbesc($server_url),
1376                                 dbesc(normalise_link($server_url)),
1377                                 dbesc($version),
1378                                 dbesc($site_name),
1379                                 dbesc($info),
1380                                 intval($register_policy),
1381                                 dbesc($poco),
1382                                 dbesc($noscrape),
1383                                 dbesc($network),
1384                                 dbesc($platform),
1385                                 dbesc(datetime_convert()),
1386                                 dbesc($last_contact),
1387                                 dbesc($last_failure),
1388                                 dbesc(datetime_convert())
1389                 );
1390         }
1391         logger("End discovery for server " . $server_url, LOGGER_DEBUG);
1392
1393         return !$failure;
1394 }
1395
1396 function count_common_friends($uid, $cid) {
1397
1398         $r = q("SELECT count(*) as `total`
1399                 FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
1400                 WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
1401                 ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
1402                 AND `gcontact`.`nurl` IN (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 and id != %d ) ",
1403                 intval($cid),
1404                 intval($uid),
1405                 intval($uid),
1406                 intval($cid)
1407         );
1408
1409         // logger("count_common_friends: $uid $cid {$r[0]['total']}");
1410         if (dbm::is_result($r)) {
1411                 return $r[0]['total'];
1412         }
1413         return 0;
1414
1415 }
1416
1417
1418 function common_friends($uid, $cid, $start = 0, $limit = 9999, $shuffle = false) {
1419
1420         if ($shuffle) {
1421                 $sql_extra = " order by rand() ";
1422         } else {
1423                 $sql_extra = " order by `gcontact`.`name` asc ";
1424         }
1425
1426         $r = q("SELECT `gcontact`.*, `contact`.`id` AS `cid`
1427                 FROM `glink`
1428                 INNER JOIN `gcontact` ON `glink`.`gcid` = `gcontact`.`id`
1429                 INNER JOIN `contact` ON `gcontact`.`nurl` = `contact`.`nurl`
1430                 WHERE `glink`.`cid` = %d and `glink`.`uid` = %d
1431                         AND `contact`.`uid` = %d AND `contact`.`self` = 0 AND `contact`.`blocked` = 0
1432                         AND `contact`.`hidden` = 0 AND `contact`.`id` != %d
1433                         AND ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
1434                         $sql_extra LIMIT %d, %d",
1435                 intval($cid),
1436                 intval($uid),
1437                 intval($uid),
1438                 intval($cid),
1439                 intval($start),
1440                 intval($limit)
1441         );
1442
1443         /// @TODO Check all calling-findings of this function if they properly use dbm::is_result()
1444         return $r;
1445
1446 }
1447
1448
1449 function count_common_friends_zcid($uid, $zcid) {
1450
1451         $r = q("SELECT count(*) as `total`
1452                 FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
1453                 where `glink`.`zcid` = %d
1454                 and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 ) ",
1455                 intval($zcid),
1456                 intval($uid)
1457         );
1458
1459         if (dbm::is_result($r)) {
1460                 return $r[0]['total'];
1461         }
1462         return 0;
1463
1464 }
1465
1466 function common_friends_zcid($uid, $zcid, $start = 0, $limit = 9999, $shuffle = false) {
1467
1468         if ($shuffle) {
1469                 $sql_extra = " order by rand() ";
1470         } else {
1471                 $sql_extra = " order by `gcontact`.`name` asc ";
1472         }
1473
1474         $r = q("SELECT `gcontact`.*
1475                 FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
1476                 where `glink`.`zcid` = %d
1477                 and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 )
1478                 $sql_extra limit %d, %d",
1479                 intval($zcid),
1480                 intval($uid),
1481                 intval($start),
1482                 intval($limit)
1483         );
1484
1485         /// @TODO Check all calling-findings of this function if they properly use dbm::is_result()
1486         return $r;
1487
1488 }
1489
1490
1491 function count_all_friends($uid, $cid) {
1492
1493         $r = q("SELECT count(*) as `total`
1494                 FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
1495                 where `glink`.`cid` = %d and `glink`.`uid` = %d AND
1496                 ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))",
1497                 intval($cid),
1498                 intval($uid)
1499         );
1500
1501         if (dbm::is_result($r)) {
1502                 return $r[0]['total'];
1503         }
1504         return 0;
1505
1506 }
1507
1508
1509 function all_friends($uid, $cid, $start = 0, $limit = 80) {
1510
1511         $r = q("SELECT `gcontact`.*, `contact`.`id` AS `cid`
1512                 FROM `glink`
1513                 INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id`
1514                 LEFT JOIN `contact` ON `contact`.`nurl` = `gcontact`.`nurl` AND `contact`.`uid` = %d
1515                 WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND
1516                 ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`))
1517                 ORDER BY `gcontact`.`name` ASC LIMIT %d, %d ",
1518                 intval($uid),
1519                 intval($cid),
1520                 intval($uid),
1521                 intval($start),
1522                 intval($limit)
1523         );
1524
1525         /// @TODO Check all calling-findings of this function if they properly use dbm::is_result()
1526         return $r;
1527 }
1528
1529
1530
1531 function suggestion_query($uid, $start = 0, $limit = 80) {
1532
1533         if (!$uid) {
1534                 return array();
1535         }
1536
1537         /*
1538          * Uncommented because the result of the queries are to big to store it in the cache.
1539          * We need to decide if we want to change the db column type or if we want to delete it.
1540          */
1541         //$list = Cache::get("suggestion_query:".$uid.":".$start.":".$limit);
1542         //if (!is_null($list)) {
1543         //      return $list;
1544         //}
1545
1546         $network = array(NETWORK_DFRN);
1547
1548         if (get_config('system','diaspora_enabled')) {
1549                 $network[] = NETWORK_DIASPORA;
1550         }
1551
1552         if (!get_config('system','ostatus_disabled')) {
1553                 $network[] = NETWORK_OSTATUS;
1554         }
1555
1556         $sql_network = implode("', '", $network);
1557         $sql_network = "'".$sql_network."'";
1558
1559         /// @todo This query is really slow
1560         // By now we cache the data for five minutes
1561         $r = q("SELECT count(glink.gcid) as `total`, gcontact.* from gcontact
1562                 INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
1563                 where uid = %d and not gcontact.nurl in ( select nurl from contact where uid = %d )
1564                 AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
1565                 AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
1566                 AND `gcontact`.`updated` >= '%s'
1567                 AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
1568                 AND `gcontact`.`network` IN (%s)
1569                 GROUP BY `glink`.`gcid` ORDER BY `gcontact`.`updated` DESC,`total` DESC LIMIT %d, %d",
1570                 intval($uid),
1571                 intval($uid),
1572                 intval($uid),
1573                 intval($uid),
1574                 dbesc(NULL_DATE),
1575                 $sql_network,
1576                 intval($start),
1577                 intval($limit)
1578         );
1579
1580         if (dbm::is_result($r) && count($r) >= ($limit -1)) {
1581                 /*
1582                  * Uncommented because the result of the queries are to big to store it in the cache.
1583                  * We need to decide if we want to change the db column type or if we want to delete it.
1584                  */
1585                 //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $r, CACHE_FIVE_MINUTES);
1586
1587                 return $r;
1588         }
1589
1590         $r2 = q("SELECT gcontact.* FROM gcontact
1591                 INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id`
1592                 WHERE `glink`.`uid` = 0 AND `glink`.`cid` = 0 AND `glink`.`zcid` = 0 AND NOT `gcontact`.`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = %d)
1593                 AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d)
1594                 AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d)
1595                 AND `gcontact`.`updated` >= '%s'
1596                 AND `gcontact`.`last_contact` >= `gcontact`.`last_failure`
1597                 AND `gcontact`.`network` IN (%s)
1598                 ORDER BY rand() LIMIT %d, %d",
1599                 intval($uid),
1600                 intval($uid),
1601                 intval($uid),
1602                 dbesc(NULL_DATE),
1603                 $sql_network,
1604                 intval($start),
1605                 intval($limit)
1606         );
1607
1608         $list = array();
1609         foreach ($r2 as $suggestion) {
1610                 $list[$suggestion["nurl"]] = $suggestion;
1611         }
1612
1613         foreach ($r as $suggestion) {
1614                 $list[$suggestion["nurl"]] = $suggestion;
1615         }
1616
1617         while (sizeof($list) > ($limit)) {
1618                 array_pop($list);
1619         }
1620
1621         /*
1622          * Uncommented because the result of the queries are to big to store it in the cache.
1623          * We need to decide if we want to change the db column type or if we want to delete it.
1624          */
1625         //Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $list, CACHE_FIVE_MINUTES);
1626         return $list;
1627 }
1628
1629 function update_suggestions() {
1630
1631         $a = get_app();
1632
1633         $done = array();
1634
1635         /// @TODO Check if it is really neccessary to poll the own server
1636         poco_load(0, 0, 0, App::get_baseurl() . '/poco');
1637
1638         $done[] = App::get_baseurl() . '/poco';
1639
1640         if (strlen(get_config('system','directory'))) {
1641                 $x = fetch_url(get_server()."/pubsites");
1642                 if ($x) {
1643                         $j = json_decode($x);
1644                         if ($j->entries) {
1645                                 foreach ($j->entries as $entry) {
1646
1647                                         poco_check_server($entry->url);
1648
1649                                         $url = $entry->url . '/poco';
1650                                         if (! in_array($url,$done)) {
1651                                                 poco_load(0,0,0,$entry->url . '/poco');
1652                                         }
1653                                 }
1654                         }
1655                 }
1656         }
1657
1658         // Query your contacts from Friendica and Redmatrix/Hubzilla for their contacts
1659         $r = q("SELECT DISTINCT(`poco`) AS `poco` FROM `contact` WHERE `network` IN ('%s', '%s')",
1660                 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA)
1661         );
1662
1663         if (dbm::is_result($r)) {
1664                 foreach ($r as $rr) {
1665                         $base = substr($rr['poco'],0,strrpos($rr['poco'],'/'));
1666                         if (! in_array($base,$done)) {
1667                                 poco_load(0,0,0,$base);
1668                         }
1669                 }
1670         }
1671 }
1672
1673 /**
1674  * @brief Fetch server list from remote servers and adds them when they are new.
1675  *
1676  * @param string $poco URL to the POCO endpoint
1677  */
1678 function poco_fetch_serverlist($poco) {
1679         $serverret = z_fetch_url($poco."/@server");
1680         if (!$serverret["success"]) {
1681                 return;
1682         }
1683         $serverlist = json_decode($serverret['body']);
1684
1685         if (!is_array($serverlist)) {
1686                 return;
1687         }
1688
1689         foreach ($serverlist as $server) {
1690                 $server_url = str_replace("/index.php", "", $server->url);
1691
1692                 $r = q("SELECT `nurl` FROM `gserver` WHERE `nurl` = '%s'", dbesc(normalise_link($server_url)));
1693                 if (!dbm::is_result($r)) {
1694                         logger("Call server check for server ".$server_url, LOGGER_DEBUG);
1695                         proc_run(PRIORITY_LOW, "include/discover_poco.php", "server", $server_url);
1696                 }
1697         }
1698 }
1699
1700 function poco_discover_federation() {
1701         $last = get_config('poco','last_federation_discovery');
1702
1703         if ($last) {
1704                 $next = $last + (24 * 60 * 60);
1705                 if ($next > time()) {
1706                         return;
1707                 }
1708         }
1709
1710         // Discover Friendica, Hubzilla and Diaspora servers
1711         $serverdata = fetch_url("http://the-federation.info/pods.json");
1712
1713         if ($serverdata) {
1714                 $servers = json_decode($serverdata);
1715
1716                 foreach ($servers->pods as $server) {
1717                         proc_run(PRIORITY_LOW, "include/discover_poco.php", "server", "https://".$server->host);
1718                 }
1719         }
1720
1721         // Disvover Mastodon servers
1722         if (!Config::get('system','ostatus_disabled')) {
1723                 $serverdata = fetch_url("https://instances.mastodon.xyz/instances.json");
1724
1725                 if ($serverdata) {
1726                         $servers = json_decode($serverdata);
1727
1728                         foreach ($servers as $server) {
1729                                 $url = (is_null($server->https_score) ? 'http' : 'https').'://'.$server->name;
1730                                 proc_run(PRIORITY_LOW, "include/discover_poco.php", "server", $url);
1731                         }
1732                 }
1733         }
1734
1735         // Currently disabled, since the service isn't available anymore.
1736         // It is not removed since I hope that there will be a successor.
1737         // Discover GNU Social Servers.
1738         //if (!get_config('system','ostatus_disabled')) {
1739         //      $serverdata = "http://gstools.org/api/get_open_instances/";
1740
1741         //      $result = z_fetch_url($serverdata);
1742         //      if ($result["success"]) {
1743         //              $servers = json_decode($result["body"]);
1744
1745         //              foreach($servers->data as $server)
1746         //                      poco_check_server($server->instance_address);
1747         //      }
1748         //}
1749
1750         set_config('poco','last_federation_discovery', time());
1751 }
1752
1753 function poco_discover_single_server($id) {
1754         $r = q("SELECT `poco`, `nurl`, `url`, `network` FROM `gserver` WHERE `id` = %d", intval($id));
1755         if (!dbm::is_result($r)) {
1756                 return false;
1757         }
1758
1759         $server = $r[0];
1760
1761         // Discover new servers out there (Works from Friendica version 3.5.2)
1762         poco_fetch_serverlist($server["poco"]);
1763
1764         // Fetch all users from the other server
1765         $url = $server["poco"]."/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
1766
1767         logger("Fetch all users from the server ".$server["url"], LOGGER_DEBUG);
1768
1769         $retdata = z_fetch_url($url);
1770         if ($retdata["success"]) {
1771                 $data = json_decode($retdata["body"]);
1772
1773                 poco_discover_server($data, 2);
1774
1775                 if (get_config('system','poco_discovery') > 1) {
1776
1777                         $timeframe = get_config('system','poco_discovery_since');
1778                         if ($timeframe == 0) {
1779                                 $timeframe = 30;
1780                         }
1781
1782                         $updatedSince = date("Y-m-d H:i:s", time() - $timeframe * 86400);
1783
1784                         // Fetch all global contacts from the other server (Not working with Redmatrix and Friendica versions before 3.3)
1785                         $url = $server["poco"]."/@global?updatedSince=".$updatedSince."&fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
1786
1787                         $success = false;
1788
1789                         $retdata = z_fetch_url($url);
1790                         if ($retdata["success"]) {
1791                                 logger("Fetch all global contacts from the server ".$server["nurl"], LOGGER_DEBUG);
1792                                 $success = poco_discover_server(json_decode($retdata["body"]));
1793                         }
1794
1795                         if (!$success && (get_config('system','poco_discovery') > 2)) {
1796                                 logger("Fetch contacts from users of the server ".$server["nurl"], LOGGER_DEBUG);
1797                                 poco_discover_server_users($data, $server);
1798                         }
1799                 }
1800
1801                 q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", dbesc(datetime_convert()), dbesc($server["nurl"]));
1802
1803                 return true;
1804         } else {
1805                 // If the server hadn't replied correctly, then force a sanity check
1806                 poco_check_server($server["url"], $server["network"], true);
1807
1808                 // If we couldn't reach the server, we will try it some time later
1809                 q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", dbesc(datetime_convert()), dbesc($server["nurl"]));
1810
1811                 return false;
1812         }
1813 }
1814
1815 function poco_discover($complete = false) {
1816
1817         // Update the server list
1818         poco_discover_federation();
1819
1820         $no_of_queries = 5;
1821
1822         $requery_days = intval(get_config("system", "poco_requery_days"));
1823
1824         if ($requery_days == 0) {
1825                 $requery_days = 7;
1826         }
1827         $last_update = date("c", time() - (60 * 60 * 24 * $requery_days));
1828
1829         $r = q("SELECT `id`, `url`, `network` FROM `gserver` WHERE `last_contact` >= `last_failure` AND `poco` != '' AND `last_poco_query` < '%s' ORDER BY RAND()", dbesc($last_update));
1830         if (dbm::is_result($r)) {
1831                 foreach ($r as $server) {
1832
1833                         if (!poco_check_server($server["url"], $server["network"])) {
1834                                 // The server is not reachable? Okay, then we will try it later
1835                                 q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", dbesc(datetime_convert()), dbesc($server["nurl"]));
1836                                 continue;
1837                         }
1838
1839                         logger('Update directory from server '.$server['url'].' with ID '.$server['id'], LOGGER_DEBUG);
1840                         proc_run(PRIORITY_LOW, "include/discover_poco.php", "update_server_directory", (int)$server['id']);
1841
1842                         if (!$complete && (--$no_of_queries == 0)) {
1843                                 break;
1844                         }
1845                 }
1846         }
1847 }
1848
1849 function poco_discover_server_users($data, $server) {
1850
1851         if (!isset($data->entry)) {
1852                 return;
1853         }
1854
1855         foreach ($data->entry as $entry) {
1856                 $username = "";
1857                 if (isset($entry->urls)) {
1858                         foreach ($entry->urls as $url) {
1859                                 if ($url->type == 'profile') {
1860                                         $profile_url = $url->value;
1861                                         $urlparts = parse_url($profile_url);
1862                                         $username = end(explode("/", $urlparts["path"]));
1863                                 }
1864                         }
1865                 }
1866                 if ($username != "") {
1867                         logger("Fetch contacts for the user ".$username." from the server ".$server["nurl"], LOGGER_DEBUG);
1868
1869                         // Fetch all contacts from a given user from the other server
1870                         $url = $server["poco"]."/".$username."/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
1871
1872                         $retdata = z_fetch_url($url);
1873                         if ($retdata["success"]) {
1874                                 poco_discover_server(json_decode($retdata["body"]), 3);
1875                         }
1876                 }
1877         }
1878 }
1879
1880 function poco_discover_server($data, $default_generation = 0) {
1881
1882         if (!isset($data->entry) || !count($data->entry)) {
1883                 return false;
1884         }
1885
1886         $success = false;
1887
1888         foreach ($data->entry as $entry) {
1889                 $profile_url = '';
1890                 $profile_photo = '';
1891                 $connect_url = '';
1892                 $name = '';
1893                 $network = '';
1894                 $updated = NULL_DATE;
1895                 $location = '';
1896                 $about = '';
1897                 $keywords = '';
1898                 $gender = '';
1899                 $contact_type = -1;
1900                 $generation = $default_generation;
1901
1902                 $name = $entry->displayName;
1903
1904                 if (isset($entry->urls)) {
1905                         foreach ($entry->urls as $url) {
1906                                 if ($url->type == 'profile') {
1907                                         $profile_url = $url->value;
1908                                         continue;
1909                                 }
1910                                 if ($url->type == 'webfinger') {
1911                                         $connect_url = str_replace('acct:' , '', $url->value);
1912                                         continue;
1913                                 }
1914                         }
1915                 }
1916
1917                 if (isset($entry->photos)) {
1918                         foreach ($entry->photos as $photo) {
1919                                 if ($photo->type == 'profile') {
1920                                         $profile_photo = $photo->value;
1921                                         continue;
1922                                 }
1923                         }
1924                 }
1925
1926                 if (isset($entry->updated)) {
1927                         $updated = date("Y-m-d H:i:s", strtotime($entry->updated));
1928                 }
1929
1930                 if (isset($entry->network)) {
1931                         $network = $entry->network;
1932                 }
1933
1934                 if (isset($entry->currentLocation)) {
1935                         $location = $entry->currentLocation;
1936                 }
1937
1938                 if (isset($entry->aboutMe)) {
1939                         $about = html2bbcode($entry->aboutMe);
1940                 }
1941
1942                 if (isset($entry->gender)) {
1943                         $gender = $entry->gender;
1944                 }
1945
1946                 if(isset($entry->generation) && ($entry->generation > 0)) {
1947                         $generation = ++$entry->generation;
1948                 }
1949
1950                 if(isset($entry->contactType) && ($entry->contactType >= 0)) {
1951                         $contact_type = $entry->contactType;
1952                 }
1953
1954                 if (isset($entry->tags)) {
1955                         foreach ($entry->tags as $tag) {
1956                                 $keywords = implode(", ", $tag);
1957                         }
1958                 }
1959
1960                 if ($generation > 0) {
1961                         $success = true;
1962
1963                         logger("Store profile ".$profile_url, LOGGER_DEBUG);
1964
1965                         $gcontact = array("url" => $profile_url,
1966                                         "name" => $name,
1967                                         "network" => $network,
1968                                         "photo" => $profile_photo,
1969                                         "about" => $about,
1970                                         "location" => $location,
1971                                         "gender" => $gender,
1972                                         "keywords" => $keywords,
1973                                         "connect" => $connect_url,
1974                                         "updated" => $updated,
1975                                         "contact-type" => $contact_type,
1976                                         "generation" => $generation);
1977
1978                         try {
1979                                 $gcontact = sanitize_gcontact($gcontact);
1980                                 update_gcontact($gcontact);
1981                         } catch (Exception $e) {
1982                                 logger($e->getMessage(), LOGGER_DEBUG);
1983                         }
1984
1985                         logger("Done for profile ".$profile_url, LOGGER_DEBUG);
1986                 }
1987         }
1988         return $success;
1989 }
1990
1991 /**
1992  * @brief Removes unwanted parts from a contact url
1993  *
1994  * @param string $url Contact url
1995  * @return string Contact url with the wanted parts
1996  */
1997 function clean_contact_url($url) {
1998         $parts = parse_url($url);
1999
2000         if (!isset($parts["scheme"]) || !isset($parts["host"])) {
2001                 return $url;
2002         }
2003
2004         $new_url = $parts["scheme"]."://".$parts["host"];
2005
2006         if (isset($parts["port"])) {
2007                 $new_url .= ":".$parts["port"];
2008         }
2009
2010         if (isset($parts["path"])) {
2011                 $new_url .= $parts["path"];
2012         }
2013
2014         if ($new_url != $url) {
2015                 logger("Cleaned contact url ".$url." to ".$new_url." - Called by: ".App::callstack(), LOGGER_DEBUG);
2016         }
2017
2018         return $new_url;
2019 }
2020
2021 /**
2022  * @brief Replace alternate OStatus user format with the primary one
2023  *
2024  * @param arr $contact contact array (called by reference)
2025  */
2026 function fix_alternate_contact_address(&$contact) {
2027         if (($contact["network"] == NETWORK_OSTATUS) && poco_alternate_ostatus_url($contact["url"])) {
2028                 $data = probe_url($contact["url"]);
2029                 if ($contact["network"] == NETWORK_OSTATUS) {
2030                         logger("Fix primary url from ".$contact["url"]." to ".$data["url"]." - Called by: ".App::callstack(), LOGGER_DEBUG);
2031                         $contact["url"] = $data["url"];
2032                         $contact["addr"] = $data["addr"];
2033                         $contact["alias"] = $data["alias"];
2034                         $contact["server_url"] = $data["baseurl"];
2035                 }
2036         }
2037 }
2038
2039 /**
2040  * @brief Fetch the gcontact id, add an entry if not existed
2041  *
2042  * @param arr $contact contact array
2043  * @return bool|int Returns false if not found, integer if contact was found
2044  */
2045 function get_gcontact_id($contact) {
2046
2047         $gcontact_id = 0;
2048         $doprobing = false;
2049
2050         if (in_array($contact["network"], array(NETWORK_PHANTOM))) {
2051                 logger("Invalid network for contact url ".$contact["url"]." - Called by: ".App::callstack(), LOGGER_DEBUG);
2052                 return false;
2053         }
2054
2055         if ($contact["network"] == NETWORK_STATUSNET) {
2056                 $contact["network"] = NETWORK_OSTATUS;
2057         }
2058
2059         // All new contacts are hidden by default
2060         if (!isset($contact["hide"])) {
2061                 $contact["hide"] = true;
2062         }
2063
2064         // Replace alternate OStatus user format with the primary one
2065         fix_alternate_contact_address($contact);
2066
2067         // Remove unwanted parts from the contact url (e.g. "?zrl=...")
2068         if (in_array($contact["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS))) {
2069                 $contact["url"] = clean_contact_url($contact["url"]);
2070         }
2071
2072         dba::lock('gcontact');
2073         $r = q("SELECT `id`, `last_contact`, `last_failure`, `network` FROM `gcontact` WHERE `nurl` = '%s' LIMIT 1",
2074                 dbesc(normalise_link($contact["url"])));
2075
2076         if (dbm::is_result($r)) {
2077                 $gcontact_id = $r[0]["id"];
2078
2079                 // Update every 90 days
2080                 if (in_array($r[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) {
2081                         $last_failure_str = $r[0]["last_failure"];
2082                         $last_failure = strtotime($r[0]["last_failure"]);
2083                         $last_contact_str = $r[0]["last_contact"];
2084                         $last_contact = strtotime($r[0]["last_contact"]);
2085                         $doprobing = (((time() - $last_contact) > (90 * 86400)) && ((time() - $last_failure) > (90 * 86400)));
2086                 }
2087         } else {
2088                 q("INSERT INTO `gcontact` (`name`, `nick`, `addr` , `network`, `url`, `nurl`, `photo`, `created`, `updated`, `location`, `about`, `hide`, `generation`)
2089                         VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d)",
2090                         dbesc($contact["name"]),
2091                         dbesc($contact["nick"]),
2092                         dbesc($contact["addr"]),
2093                         dbesc($contact["network"]),
2094                         dbesc($contact["url"]),
2095                         dbesc(normalise_link($contact["url"])),
2096                         dbesc($contact["photo"]),
2097                         dbesc(datetime_convert()),
2098                         dbesc(datetime_convert()),
2099                         dbesc($contact["location"]),
2100                         dbesc($contact["about"]),
2101                         intval($contact["hide"]),
2102                         intval($contact["generation"])
2103                 );
2104
2105                 $r = q("SELECT `id`, `network` FROM `gcontact` WHERE `nurl` = '%s' ORDER BY `id` LIMIT 2",
2106                         dbesc(normalise_link($contact["url"])));
2107
2108                 if (dbm::is_result($r)) {
2109                         $gcontact_id = $r[0]["id"];
2110
2111                         $doprobing = in_array($r[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""));
2112                 }
2113         }
2114         dba::unlock();
2115
2116         if ($doprobing) {
2117                 logger("Last Contact: ". $last_contact_str." - Last Failure: ".$last_failure_str." - Checking: ".$contact["url"], LOGGER_DEBUG);
2118                 proc_run(PRIORITY_LOW, 'include/gprobe.php', $contact["url"]);
2119         }
2120
2121         return $gcontact_id;
2122 }
2123
2124 /**
2125  * @brief Updates the gcontact table from a given array
2126  *
2127  * @param arr $contact contact array
2128  * @return bool|int Returns false if not found, integer if contact was found
2129  */
2130 function update_gcontact($contact) {
2131
2132         // Check for invalid "contact-type" value
2133         if (isset($contact['contact-type']) && (intval($contact['contact-type']) < 0)) {
2134                 $contact['contact-type'] = 0;
2135         }
2136
2137         /// @todo update contact table as well
2138
2139         $gcontact_id = get_gcontact_id($contact);
2140
2141         if (!$gcontact_id) {
2142                 return false;
2143         }
2144
2145         $r = q("SELECT `name`, `nick`, `photo`, `location`, `about`, `addr`, `generation`, `birthday`, `gender`, `keywords`,
2146                         `contact-type`, `hide`, `nsfw`, `network`, `alias`, `notify`, `server_url`, `connect`, `updated`, `url`
2147                 FROM `gcontact` WHERE `id` = %d LIMIT 1",
2148                 intval($gcontact_id));
2149
2150         // Get all field names
2151         $fields = array();
2152         foreach ($r[0] as $field => $data) {
2153                 $fields[$field] = $data;
2154         }
2155
2156         unset($fields["url"]);
2157         unset($fields["updated"]);
2158         unset($fields["hide"]);
2159
2160         // Bugfix: We had an error in the storing of keywords which lead to the "0"
2161         // This value is still transmitted via poco.
2162         if ($contact["keywords"] == "0") {
2163                 unset($contact["keywords"]);
2164         }
2165
2166         if ($r[0]["keywords"] == "0") {
2167                 $r[0]["keywords"] = "";
2168         }
2169
2170         // assign all unassigned fields from the database entry
2171         foreach ($fields as $field => $data) {
2172                 if (!isset($contact[$field]) || ($contact[$field] == "")) {
2173                         $contact[$field] = $r[0][$field];
2174                 }
2175         }
2176
2177         if (!isset($contact["hide"])) {
2178                 $contact["hide"] = $r[0]["hide"];
2179         }
2180
2181         $fields["hide"] = $r[0]["hide"];
2182
2183         if ($contact["network"] == NETWORK_STATUSNET) {
2184                 $contact["network"] = NETWORK_OSTATUS;
2185         }
2186
2187         // Replace alternate OStatus user format with the primary one
2188         fix_alternate_contact_address($contact);
2189
2190         if (!isset($contact["updated"])) {
2191                 $contact["updated"] = dbm::date();
2192         }
2193
2194         if ($contact["server_url"] == "") {
2195                 $server_url = $contact["url"];
2196
2197                 $server_url = matching_url($server_url, $contact["alias"]);
2198                 if ($server_url != "") {
2199                         $contact["server_url"] = $server_url;
2200                 }
2201
2202                 $server_url = matching_url($server_url, $contact["photo"]);
2203                 if ($server_url != "") {
2204                         $contact["server_url"] = $server_url;
2205                 }
2206
2207                 $server_url = matching_url($server_url, $contact["notify"]);
2208                 if ($server_url != "") {
2209                         $contact["server_url"] = $server_url;
2210                 }
2211         } else {
2212                 $contact["server_url"] = normalise_link($contact["server_url"]);
2213         }
2214
2215         if (($contact["addr"] == "") && ($contact["server_url"] != "") && ($contact["nick"] != "")) {
2216                 $hostname = str_replace("http://", "", $contact["server_url"]);
2217                 $contact["addr"] = $contact["nick"]."@".$hostname;
2218         }
2219
2220         // Check if any field changed
2221         $update = false;
2222         unset($fields["generation"]);
2223
2224         if ((($contact["generation"] > 0) && ($contact["generation"] <= $r[0]["generation"])) || ($r[0]["generation"] == 0)) {
2225                 foreach ($fields as $field => $data) {
2226                         if ($contact[$field] != $r[0][$field]) {
2227                                 logger("Difference for contact ".$contact["url"]." in field '".$field."'. New value: '".$contact[$field]."', old value '".$r[0][$field]."'", LOGGER_DEBUG);
2228                                 $update = true;
2229                         }
2230                 }
2231
2232                 if ($contact["generation"] < $r[0]["generation"]) {
2233                         logger("Difference for contact ".$contact["url"]." in field 'generation'. new value: '".$contact["generation"]."', old value '".$r[0]["generation"]."'", LOGGER_DEBUG);
2234                         $update = true;
2235                 }
2236         }
2237
2238         if ($update) {
2239                 logger("Update gcontact for ".$contact["url"], LOGGER_DEBUG);
2240
2241                 q("UPDATE `gcontact` SET `photo` = '%s', `name` = '%s', `nick` = '%s', `addr` = '%s', `network` = '%s',
2242                                         `birthday` = '%s', `gender` = '%s', `keywords` = '%s', `hide` = %d, `nsfw` = %d,
2243                                         `contact-type` = %d, `alias` = '%s', `notify` = '%s', `url` = '%s',
2244                                         `location` = '%s', `about` = '%s', `generation` = %d, `updated` = '%s',
2245                                         `server_url` = '%s', `connect` = '%s'
2246                                 WHERE `nurl` = '%s' AND (`generation` = 0 OR `generation` >= %d)",
2247                         dbesc($contact["photo"]), dbesc($contact["name"]), dbesc($contact["nick"]),
2248                         dbesc($contact["addr"]), dbesc($contact["network"]), dbesc($contact["birthday"]),
2249                         dbesc($contact["gender"]), dbesc($contact["keywords"]), intval($contact["hide"]),
2250                         intval($contact["nsfw"]), intval($contact["contact-type"]), dbesc($contact["alias"]),
2251                         dbesc($contact["notify"]), dbesc($contact["url"]), dbesc($contact["location"]),
2252                         dbesc($contact["about"]), intval($contact["generation"]), dbesc(dbm::date($contact["updated"])),
2253                         dbesc($contact["server_url"]), dbesc($contact["connect"]),
2254                         dbesc(normalise_link($contact["url"])), intval($contact["generation"]));
2255
2256
2257                 // Now update the contact entry with the user id "0" as well.
2258                 // This is used for the shadow copies of public items.
2259                 $r = q("SELECT `id` FROM `contact` WHERE `nurl` = '%s' AND `uid` = 0 ORDER BY `id` LIMIT 1",
2260                         dbesc(normalise_link($contact["url"])));
2261
2262                 if (dbm::is_result($r)) {
2263                         logger("Update shadow contact ".$r[0]["id"], LOGGER_DEBUG);
2264
2265                         update_contact_avatar($contact["photo"], 0, $r[0]["id"]);
2266
2267                         q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `addr` = '%s',
2268                                                 `network` = '%s', `bd` = '%s', `gender` = '%s',
2269                                                 `keywords` = '%s', `alias` = '%s', `contact-type` = %d,
2270                                                 `url` = '%s', `location` = '%s', `about` = '%s'
2271                                         WHERE `id` = %d",
2272                                 dbesc($contact["name"]), dbesc($contact["nick"]), dbesc($contact["addr"]),
2273                                 dbesc($contact["network"]), dbesc($contact["birthday"]), dbesc($contact["gender"]),
2274                                 dbesc($contact["keywords"]), dbesc($contact["alias"]), intval($contact["contact-type"]),
2275                                 dbesc($contact["url"]), dbesc($contact["location"]), dbesc($contact["about"]),
2276                                 intval($r[0]["id"]));
2277                 }
2278         }
2279
2280         return $gcontact_id;
2281 }
2282
2283 /**
2284  * @brief Updates the gcontact entry from probe
2285  *
2286  * @param str $url profile link
2287  */
2288 function update_gcontact_from_probe($url) {
2289         $data = probe_url($url);
2290
2291         if (in_array($data["network"], array(NETWORK_PHANTOM))) {
2292                 logger("Invalid network for contact url ".$data["url"]." - Called by: ".App::callstack(), LOGGER_DEBUG);
2293                 return;
2294         }
2295
2296         $data["server_url"] = $data["baseurl"];
2297
2298         update_gcontact($data);
2299 }
2300
2301 /**
2302  * @brief Update the gcontact entry for a given user id
2303  *
2304  * @param int $uid User ID
2305  */
2306 function update_gcontact_for_user($uid) {
2307         $r = q("SELECT `profile`.`locality`, `profile`.`region`, `profile`.`country-name`,
2308                         `profile`.`name`, `profile`.`about`, `profile`.`gender`,
2309                         `profile`.`pub_keywords`, `profile`.`dob`, `profile`.`photo`,
2310                         `profile`.`net-publish`, `user`.`nickname`, `user`.`hidewall`,
2311                         `contact`.`notify`, `contact`.`url`, `contact`.`addr`
2312                 FROM `profile`
2313                         INNER JOIN `user` ON `user`.`uid` = `profile`.`uid`
2314                         INNER JOIN `contact` ON `contact`.`uid` = `profile`.`uid`
2315                 WHERE `profile`.`uid` = %d AND `profile`.`is-default` AND `contact`.`self`",
2316                 intval($uid));
2317
2318         $location = formatted_location(array("locality" => $r[0]["locality"], "region" => $r[0]["region"],
2319                                                 "country-name" => $r[0]["country-name"]));
2320
2321         // The "addr" field was added in 3.4.3 so it can be empty for older users
2322         if ($r[0]["addr"] != "") {
2323                 $addr = $r[0]["nickname"].'@'.str_replace(array("http://", "https://"), "", App::get_baseurl());
2324         } else {
2325                 $addr = $r[0]["addr"];
2326         }
2327
2328         $gcontact = array("name" => $r[0]["name"], "location" => $location, "about" => $r[0]["about"],
2329                         "gender" => $r[0]["gender"], "keywords" => $r[0]["pub_keywords"],
2330                         "birthday" => $r[0]["dob"], "photo" => $r[0]["photo"],
2331                         "notify" => $r[0]["notify"], "url" => $r[0]["url"],
2332                         "hide" => ($r[0]["hidewall"] || !$r[0]["net-publish"]),
2333                         "nick" => $r[0]["nickname"], "addr" => $addr,
2334                         "connect" => $addr, "server_url" => App::get_baseurl(),
2335                         "generation" => 1, "network" => NETWORK_DFRN);
2336
2337         update_gcontact($gcontact);
2338 }
2339
2340 /**
2341  * @brief Fetches users of given GNU Social server
2342  *
2343  * If the "Statistics" plugin is enabled (See http://gstools.org/ for details) we query user data with this.
2344  *
2345  * @param str $server Server address
2346  */
2347 function gs_fetch_users($server) {
2348
2349         logger("Fetching users from GNU Social server ".$server, LOGGER_DEBUG);
2350
2351         $url = $server."/main/statistics";
2352
2353         $result = z_fetch_url($url);
2354         if (!$result["success"]) {
2355                 return false;
2356         }
2357
2358         $statistics = json_decode($result["body"]);
2359
2360         if (is_object($statistics->config)) {
2361                 if ($statistics->config->instance_with_ssl) {
2362                         $server = "https://";
2363                 } else {
2364                         $server = "http://";
2365                 }
2366
2367                 $server .= $statistics->config->instance_address;
2368
2369                 $hostname = $statistics->config->instance_address;
2370         } else {
2371                 /// @TODO is_object() above means here no object, still $statistics is being used as object
2372                 if ($statistics->instance_with_ssl) {
2373                         $server = "https://";
2374                 } else {
2375                         $server = "http://";
2376                 }
2377
2378                 $server .= $statistics->instance_address;
2379
2380                 $hostname = $statistics->instance_address;
2381         }
2382
2383         if (is_object($statistics->users)) {
2384                 foreach ($statistics->users as $nick => $user) {
2385                         $profile_url = $server."/".$user->nickname;
2386
2387                         $contact = array("url" => $profile_url,
2388                                         "name" => $user->fullname,
2389                                         "addr" => $user->nickname."@".$hostname,
2390                                         "nick" => $user->nickname,
2391                                         "about" => $user->bio,
2392                                         "network" => NETWORK_OSTATUS,
2393                                         "photo" => App::get_baseurl()."/images/person-175.jpg");
2394                         get_gcontact_id($contact);
2395                 }
2396         }
2397 }
2398
2399 /**
2400  * @brief Asking GNU Social server on a regular base for their user data
2401  *
2402  */
2403 function gs_discover() {
2404
2405         $requery_days = intval(get_config("system", "poco_requery_days"));
2406
2407         $last_update = date("c", time() - (60 * 60 * 24 * $requery_days));
2408
2409         $r = q("SELECT `nurl`, `url` FROM `gserver` WHERE `last_contact` >= `last_failure` AND `network` = '%s' AND `last_poco_query` < '%s' ORDER BY RAND() LIMIT 5",
2410                 dbesc(NETWORK_OSTATUS), dbesc($last_update));
2411
2412         if (!dbm::is_result($r)) {
2413                 return;
2414         }
2415
2416         foreach ($r as $server) {
2417                 gs_fetch_users($server["url"]);
2418                 q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", dbesc(datetime_convert()), dbesc($server["nurl"]));
2419         }
2420 }
2421
2422 /**
2423  * @brief Returns a list of all known servers
2424  * @return array List of server urls
2425  */
2426 function poco_serverlist() {
2427         $r = q("SELECT `url`, `site_name` AS `displayName`, `network`, `platform`, `version` FROM `gserver`
2428                 WHERE `network` IN ('%s', '%s', '%s') AND `last_contact` > `last_failure`
2429                 ORDER BY `last_contact`
2430                 LIMIT 1000",
2431                 dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS));
2432         if (!dbm::is_result($r)) {
2433                 return false;
2434         }
2435
2436         return $r;
2437 }