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