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