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