]> git.mxchange.org Git - friendica.git/blob - src/Protocol/PortableContact.php
Code transitioning from PortableContacts.php to GServer.php
[friendica.git] / src / Protocol / PortableContact.php
1 <?php
2 /**
3  * @file src/Protocol/PortableContact.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 namespace Friendica\Protocol;
11
12 use DOMDocument;
13 use DOMXPath;
14 use Exception;
15 use Friendica\Content\Text\HTML;
16 use Friendica\Core\Config;
17 use Friendica\Core\Logger;
18 use Friendica\Core\Protocol;
19 use Friendica\Core\Worker;
20 use Friendica\Database\DBA;
21 use Friendica\Model\Contact;
22 use Friendica\Model\GContact;
23 use Friendica\Model\GServer;
24 use Friendica\Model\Profile;
25 use Friendica\Module\Register;
26 use Friendica\Network\Probe;
27 use Friendica\Util\DateTimeFormat;
28 use Friendica\Util\Network;
29 use Friendica\Util\Strings;
30 use Friendica\Util\XML;
31
32 class PortableContact
33 {
34         const DISABLED = 0;
35         const USERS = 1;
36         const USERS_GCONTACTS = 2;
37         const USERS_GCONTACTS_FALLBACK = 3;
38
39         /**
40          * @brief Fetch POCO data
41          *
42          * @param integer $cid  Contact ID
43          * @param integer $uid  User ID
44          * @param integer $zcid Global Contact ID
45          * @param integer $url  POCO address that should be polled
46          *
47          * Given a contact-id (minimum), load the PortableContacts friend list for that contact,
48          * and add the entries to the gcontact (Global Contact) table, or update existing entries
49          * if anything (name or photo) has changed.
50          * We use normalised urls for comparison which ignore http vs https and www.domain vs domain
51          *
52          * Once the global contact is stored add (if necessary) the contact linkage which associates
53          * the given uid, cid to the global contact entry. There can be many uid/cid combinations
54          * pointing to the same global contact id.
55          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
56          */
57         public static function loadWorker($cid, $uid = 0, $zcid = 0, $url = null)
58         {
59                 // Call the function "load" via the worker
60                 Worker::add(PRIORITY_LOW, "DiscoverPoCo", "load", (int)$cid, (int)$uid, (int)$zcid, $url);
61         }
62
63         /**
64          * @brief Fetch POCO data from the worker
65          *
66          * @param integer $cid  Contact ID
67          * @param integer $uid  User ID
68          * @param integer $zcid Global Contact ID
69          * @param integer $url  POCO address that should be polled
70          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
71          */
72         public static function load($cid, $uid, $zcid, $url)
73         {
74                 if ($cid) {
75                         if (!$url || !$uid) {
76                                 $contact = DBA::selectFirst('contact', ['poco', 'uid'], ['id' => $cid]);
77                                 if (DBA::isResult($contact)) {
78                                         $url = $contact['poco'];
79                                         $uid = $contact['uid'];
80                                 }
81                         }
82                         if (!$uid) {
83                                 return;
84                         }
85                 }
86
87                 if (!$url) {
88                         return;
89                 }
90
91                 $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');
92
93                 Logger::log('load: ' . $url, Logger::DEBUG);
94
95                 $fetchresult = Network::fetchUrlFull($url);
96                 $s = $fetchresult->getBody();
97
98                 Logger::log('load: returns ' . $s, Logger::DATA);
99
100                 Logger::log('load: return code: ' . $fetchresult->getReturnCode(), Logger::DEBUG);
101
102                 if (($fetchresult->getReturnCode() > 299) || (! $s)) {
103                         return;
104                 }
105
106                 $j = json_decode($s, true);
107
108                 Logger::log('load: json: ' . print_r($j, true), Logger::DATA);
109
110                 if (!isset($j['entry'])) {
111                         return;
112                 }
113
114                 $total = 0;
115                 foreach ($j['entry'] as $entry) {
116                         $total ++;
117                         $profile_url = '';
118                         $profile_photo = '';
119                         $connect_url = '';
120                         $name = '';
121                         $network = '';
122                         $updated = DBA::NULL_DATETIME;
123                         $location = '';
124                         $about = '';
125                         $keywords = '';
126                         $gender = '';
127                         $contact_type = -1;
128                         $generation = 0;
129
130                         if (!empty($entry['displayName'])) {
131                                 $name = $entry['displayName'];
132                         }
133
134                         if (isset($entry['urls'])) {
135                                 foreach ($entry['urls'] as $url) {
136                                         if ($url['type'] == 'profile') {
137                                                 $profile_url = $url['value'];
138                                                 continue;
139                                         }
140                                         if ($url['type'] == 'webfinger') {
141                                                 $connect_url = str_replace('acct:', '', $url['value']);
142                                                 continue;
143                                         }
144                                 }
145                         }
146                         if (isset($entry['photos'])) {
147                                 foreach ($entry['photos'] as $photo) {
148                                         if ($photo['type'] == 'profile') {
149                                                 $profile_photo = $photo['value'];
150                                                 continue;
151                                         }
152                                 }
153                         }
154
155                         if (isset($entry['updated'])) {
156                                 $updated = date(DateTimeFormat::MYSQL, strtotime($entry['updated']));
157                         }
158
159                         if (isset($entry['network'])) {
160                                 $network = $entry['network'];
161                         }
162
163                         if (isset($entry['currentLocation'])) {
164                                 $location = $entry['currentLocation'];
165                         }
166
167                         if (isset($entry['aboutMe'])) {
168                                 $about = HTML::toBBCode($entry['aboutMe']);
169                         }
170
171                         if (isset($entry['gender'])) {
172                                 $gender = $entry['gender'];
173                         }
174
175                         if (isset($entry['generation']) && ($entry['generation'] > 0)) {
176                                 $generation = ++$entry['generation'];
177                         }
178
179                         if (isset($entry['tags'])) {
180                                 foreach ($entry['tags'] as $tag) {
181                                         $keywords = implode(", ", $tag);
182                                 }
183                         }
184
185                         if (isset($entry['contactType']) && ($entry['contactType'] >= 0)) {
186                                 $contact_type = $entry['contactType'];
187                         }
188
189                         $gcontact = ["url" => $profile_url,
190                                         "name" => $name,
191                                         "network" => $network,
192                                         "photo" => $profile_photo,
193                                         "about" => $about,
194                                         "location" => $location,
195                                         "gender" => $gender,
196                                         "keywords" => $keywords,
197                                         "connect" => $connect_url,
198                                         "updated" => $updated,
199                                         "contact-type" => $contact_type,
200                                         "generation" => $generation];
201
202                         try {
203                                 $gcontact = GContact::sanitize($gcontact);
204                                 $gcid = GContact::update($gcontact);
205
206                                 GContact::link($gcid, $uid, $cid, $zcid);
207                         } catch (Exception $e) {
208                                 Logger::log($e->getMessage(), Logger::DEBUG);
209                         }
210                 }
211                 Logger::log("load: loaded $total entries", Logger::DEBUG);
212
213                 $condition = ["`cid` = ? AND `uid` = ? AND `zcid` = ? AND `updated` < UTC_TIMESTAMP - INTERVAL 2 DAY", $cid, $uid, $zcid];
214                 DBA::delete('glink', $condition);
215         }
216
217         public static function reachable($profile, $server = "", $network = "", $force = false)
218         {
219                 if ($server == "") {
220                         $server = Contact::getBasepath($profile);
221                 }
222
223                 if ($server == "") {
224                         return true;
225                 }
226
227                 return GServer::check($server, $force);
228         }
229
230         public static function alternateOStatusUrl($url)
231         {
232                 return(preg_match("=https?://.+/user/\d+=ism", $url, $matches));
233         }
234
235         public static function lastUpdated($profile, $force = false)
236         {
237                 $gcontacts = q(
238                         "SELECT * FROM `gcontact` WHERE `nurl` = '%s'",
239                         DBA::escape(Strings::normaliseLink($profile))
240                 );
241
242                 if (!DBA::isResult($gcontacts)) {
243                         return false;
244                 }
245
246                 $contact = ["url" => $profile];
247
248                 if ($gcontacts[0]["created"] <= DBA::NULL_DATETIME) {
249                         $contact['created'] = DateTimeFormat::utcNow();
250                 }
251
252                 $server_url = '';
253                 if ($force) {
254                         $server_url = Strings::normaliseLink(Contact::getBasepath($profile));
255                 }
256
257                 if (($server_url == '') && ($gcontacts[0]["server_url"] != "")) {
258                         $server_url = $gcontacts[0]["server_url"];
259                 }
260
261                 if (!$force && (($server_url == '') || ($gcontacts[0]["server_url"] == $gcontacts[0]["nurl"]))) {
262                         $server_url = Strings::normaliseLink(Contact::getBasepath($profile));
263                 }
264
265                 if (!in_array($gcontacts[0]["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::FEED, Protocol::OSTATUS, ""])) {
266                         Logger::log("Profile ".$profile.": Network type ".$gcontacts[0]["network"]." can't be checked", Logger::DEBUG);
267                         return false;
268                 }
269
270                 if ($server_url != "") {
271                         if (!GServer::check($server_url, $force)) {
272                                 if ($force) {
273                                         $fields = ['last_failure' => DateTimeFormat::utcNow()];
274                                         DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
275                                 }
276
277                                 Logger::log("Profile ".$profile.": Server ".$server_url." wasn't reachable.", Logger::DEBUG);
278                                 return false;
279                         }
280                         $contact['server_url'] = $server_url;
281                 }
282
283                 if (in_array($gcontacts[0]["network"], ["", Protocol::FEED])) {
284                         $server = q(
285                                 "SELECT `network` FROM `gserver` WHERE `nurl` = '%s' AND `network` != ''",
286                                 DBA::escape(Strings::normaliseLink($server_url))
287                         );
288
289                         if ($server) {
290                                 $contact['network'] = $server[0]["network"];
291                         } else {
292                                 return false;
293                         }
294                 }
295
296                 // noscrape is really fast so we don't cache the call.
297                 if (($server_url != "") && ($gcontacts[0]["nick"] != "")) {
298                         //  Use noscrape if possible
299                         $server = q("SELECT `noscrape`, `network` FROM `gserver` WHERE `nurl` = '%s' AND `noscrape` != ''", DBA::escape(Strings::normaliseLink($server_url)));
300
301                         if ($server) {
302                                 $curlResult = Network::curl($server[0]["noscrape"]."/".$gcontacts[0]["nick"]);
303
304                                 if ($curlResult->isSuccess() && ($curlResult->getBody() != "")) {
305                                         $noscrape = json_decode($curlResult->getBody(), true);
306
307                                         if (is_array($noscrape)) {
308                                                 $contact["network"] = $server[0]["network"];
309
310                                                 if (isset($noscrape["fn"])) {
311                                                         $contact["name"] = $noscrape["fn"];
312                                                 }
313                                                 if (isset($noscrape["comm"])) {
314                                                         $contact["community"] = $noscrape["comm"];
315                                                 }
316                                                 if (isset($noscrape["tags"])) {
317                                                         $keywords = implode(" ", $noscrape["tags"]);
318                                                         if ($keywords != "") {
319                                                                 $contact["keywords"] = $keywords;
320                                                         }
321                                                 }
322
323                                                 $location = Profile::formatLocation($noscrape);
324                                                 if ($location) {
325                                                         $contact["location"] = $location;
326                                                 }
327                                                 if (isset($noscrape["dfrn-notify"])) {
328                                                         $contact["notify"] = $noscrape["dfrn-notify"];
329                                                 }
330                                                 // Remove all fields that are not present in the gcontact table
331                                                 unset($noscrape["fn"]);
332                                                 unset($noscrape["key"]);
333                                                 unset($noscrape["homepage"]);
334                                                 unset($noscrape["comm"]);
335                                                 unset($noscrape["tags"]);
336                                                 unset($noscrape["locality"]);
337                                                 unset($noscrape["region"]);
338                                                 unset($noscrape["country-name"]);
339                                                 unset($noscrape["contacts"]);
340                                                 unset($noscrape["dfrn-request"]);
341                                                 unset($noscrape["dfrn-confirm"]);
342                                                 unset($noscrape["dfrn-notify"]);
343                                                 unset($noscrape["dfrn-poll"]);
344
345                                                 // Set the date of the last contact
346                                                 /// @todo By now the function "update_gcontact" doesn't work with this field
347                                                 //$contact["last_contact"] = DateTimeFormat::utcNow();
348
349                                                 $contact = array_merge($contact, $noscrape);
350
351                                                 GContact::update($contact);
352
353                                                 if (!empty($noscrape["updated"])) {
354                                                         $fields = ['last_contact' => DateTimeFormat::utcNow()];
355                                                         DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
356
357                                                         Logger::log("Profile ".$profile." was last updated at ".$noscrape["updated"]." (noscrape)", Logger::DEBUG);
358
359                                                         return $noscrape["updated"];
360                                                 }
361                                         }
362                                 }
363                         }
364                 }
365
366                 // If we only can poll the feed, then we only do this once a while
367                 if (!$force && !self::updateNeeded($gcontacts[0]["created"], $gcontacts[0]["updated"], $gcontacts[0]["last_failure"], $gcontacts[0]["last_contact"])) {
368                         Logger::log("Profile ".$profile." was last updated at ".$gcontacts[0]["updated"]." (cached)", Logger::DEBUG);
369
370                         GContact::update($contact);
371                         return $gcontacts[0]["updated"];
372                 }
373
374                 $data = Probe::uri($profile);
375
376                 // Is the profile link the alternate OStatus link notation? (http://domain.tld/user/4711)
377                 // Then check the other link and delete this one
378                 if (($data["network"] == Protocol::OSTATUS) && self::alternateOStatusUrl($profile)
379                         && (Strings::normaliseLink($profile) == Strings::normaliseLink($data["alias"]))
380                         && (Strings::normaliseLink($profile) != Strings::normaliseLink($data["url"]))
381                 ) {
382                         // Delete the old entry
383                         DBA::delete('gcontact', ['nurl' => Strings::normaliseLink($profile)]);
384
385                         $gcontact = array_merge($gcontacts[0], $data);
386
387                         $gcontact["server_url"] = $data["baseurl"];
388
389                         try {
390                                 $gcontact = GContact::sanitize($gcontact);
391                                 GContact::update($gcontact);
392
393                                 self::lastUpdated($data["url"], $force);
394                         } catch (Exception $e) {
395                                 Logger::log($e->getMessage(), Logger::DEBUG);
396                         }
397
398                         Logger::log("Profile ".$profile." was deleted", Logger::DEBUG);
399                         return false;
400                 }
401
402                 if (($data["poll"] == "") || (in_array($data["network"], [Protocol::FEED, Protocol::PHANTOM]))) {
403                         $fields = ['last_failure' => DateTimeFormat::utcNow()];
404                         DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
405
406                         Logger::log("Profile ".$profile." wasn't reachable (profile)", Logger::DEBUG);
407                         return false;
408                 }
409
410                 $contact = array_merge($contact, $data);
411
412                 $contact["server_url"] = $data["baseurl"];
413
414                 GContact::update($contact);
415
416                 $curlResult = Network::curl($data["poll"]);
417
418                 if (!$curlResult->isSuccess()) {
419                         $fields = ['last_failure' => DateTimeFormat::utcNow()];
420                         DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
421
422                         Logger::log("Profile ".$profile." wasn't reachable (no feed)", Logger::DEBUG);
423                         return false;
424                 }
425
426                 $doc = new DOMDocument();
427                 /// @TODO Avoid error supression here
428                 @$doc->loadXML($curlResult->getBody());
429
430                 $xpath = new DOMXPath($doc);
431                 $xpath->registerNamespace('atom', "http://www.w3.org/2005/Atom");
432
433                 $entries = $xpath->query('/atom:feed/atom:entry');
434
435                 $last_updated = "";
436
437                 foreach ($entries as $entry) {
438                         $published_item = $xpath->query('atom:published/text()', $entry)->item(0);
439                         $updated_item   = $xpath->query('atom:updated/text()'  , $entry)->item(0);
440                         $published      = isset($published_item->nodeValue) ? DateTimeFormat::utc($published_item->nodeValue) : null;
441                         $updated        = isset($updated_item->nodeValue) ? DateTimeFormat::utc($updated_item->nodeValue) : null;
442
443                         if (!isset($published) || !isset($updated)) {
444                                 Logger::notice('Invalid entry for XPath.', ['entry' => $entry, 'profile' => $profile]);
445                                 continue;
446                         }
447
448                         if ($last_updated < $published) {
449                                 $last_updated = $published;
450                         }
451
452                         if ($last_updated < $updated) {
453                                 $last_updated = $updated;
454                         }
455                 }
456
457                 // Maybe there aren't any entries. Then check if it is a valid feed
458                 if ($last_updated == "") {
459                         if ($xpath->query('/atom:feed')->length > 0) {
460                                 $last_updated = DBA::NULL_DATETIME;
461                         }
462                 }
463
464                 $fields = ['last_contact' => DateTimeFormat::utcNow()];
465
466                 if (!empty($last_updated)) {
467                         $fields['updated'] = $last_updated;
468                 }
469
470                 DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
471
472                 if (($gcontacts[0]["generation"] == 0)) {
473                         $fields = ['generation' => 9];
474                         DBA::update('gcontact', $fields, ['nurl' => Strings::normaliseLink($profile)]);
475                 }
476
477                 Logger::log("Profile ".$profile." was last updated at ".$last_updated, Logger::DEBUG);
478
479                 return $last_updated;
480         }
481
482         public static function updateNeeded($created, $updated, $last_failure, $last_contact)
483         {
484                 $now = strtotime(DateTimeFormat::utcNow());
485
486                 if ($updated > $last_contact) {
487                         $contact_time = strtotime($updated);
488                 } else {
489                         $contact_time = strtotime($last_contact);
490                 }
491
492                 $failure_time = strtotime($last_failure);
493                 $created_time = strtotime($created);
494
495                 // If there is no "created" time then use the current time
496                 if ($created_time <= 0) {
497                         $created_time = $now;
498                 }
499
500                 // If the last contact was less than 24 hours then don't update
501                 if (($now - $contact_time) < (60 * 60 * 24)) {
502                         return false;
503                 }
504
505                 // If the last failure was less than 24 hours then don't update
506                 if (($now - $failure_time) < (60 * 60 * 24)) {
507                         return false;
508                 }
509
510                 // If the last contact was less than a week ago and the last failure is older than a week then don't update
511                 //if ((($now - $contact_time) < (60 * 60 * 24 * 7)) && ($contact_time > $failure_time))
512                 //      return false;
513
514                 // 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
515                 if ((($now - $contact_time) > (60 * 60 * 24 * 7)) && (($now - $created_time) > (60 * 60 * 24 * 7)) && (($now - $failure_time) < (60 * 60 * 24 * 7))) {
516                         return false;
517                 }
518
519                 // 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
520                 if ((($now - $contact_time) > (60 * 60 * 24 * 30)) && (($now - $created_time) > (60 * 60 * 24 * 30)) && (($now - $failure_time) < (60 * 60 * 24 * 30))) {
521                         return false;
522                 }
523
524                 return true;
525         }
526
527         /**
528          * @brief Returns a list of all known servers
529          * @return array List of server urls
530          * @throws Exception
531          */
532         public static function serverlist()
533         {
534                 $r = q(
535                         "SELECT `url`, `site_name` AS `displayName`, `network`, `platform`, `version` FROM `gserver`
536                         WHERE `network` IN ('%s', '%s', '%s') AND `last_contact` > `last_failure`
537                         ORDER BY `last_contact`
538                         LIMIT 1000",
539                         DBA::escape(Protocol::DFRN),
540                         DBA::escape(Protocol::DIASPORA),
541                         DBA::escape(Protocol::OSTATUS)
542                 );
543
544                 if (!DBA::isResult($r)) {
545                         return false;
546                 }
547
548                 return $r;
549         }
550
551         /**
552          * @brief Fetch server list from remote servers and adds them when they are new.
553          *
554          * @param string $poco URL to the POCO endpoint
555          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
556          */
557         private static function fetchServerlist($poco)
558         {
559                 $curlResult = Network::curl($poco . "/@server");
560
561                 if (!$curlResult->isSuccess()) {
562                         return;
563                 }
564
565                 $serverlist = json_decode($curlResult->getBody(), true);
566
567                 if (!is_array($serverlist)) {
568                         return;
569                 }
570
571                 foreach ($serverlist as $server) {
572                         $server_url = str_replace("/index.php", "", $server['url']);
573
574                         $r = q("SELECT `nurl` FROM `gserver` WHERE `nurl` = '%s'", DBA::escape(Strings::normaliseLink($server_url)));
575
576                         if (!DBA::isResult($r)) {
577                                 Logger::log("Call server check for server ".$server_url, Logger::DEBUG);
578                                 Worker::add(PRIORITY_LOW, "DiscoverPoCo", "server", $server_url);
579                         }
580                 }
581         }
582
583         private static function discoverFederation()
584         {
585                 $last = Config::get('poco', 'last_federation_discovery');
586
587                 if ($last) {
588                         $next = $last + (24 * 60 * 60);
589
590                         if ($next > time()) {
591                                 return;
592                         }
593                 }
594
595                 // Discover Friendica, Hubzilla and Diaspora servers
596                 $curlResult = Network::fetchUrl("http://the-federation.info/pods.json");
597
598                 if (!empty($curlResult)) {
599                         $servers = json_decode($curlResult, true);
600
601                         if (!empty($servers['pods'])) {
602                                 foreach ($servers['pods'] as $server) {
603                                         Worker::add(PRIORITY_LOW, "DiscoverPoCo", "server", "https://" . $server['host']);
604                                 }
605                         }
606                 }
607
608                 // Disvover Mastodon servers
609                 if (!Config::get('system', 'ostatus_disabled')) {
610                         $accesstoken = Config::get('system', 'instances_social_key');
611
612                         if (!empty($accesstoken)) {
613                                 $api = 'https://instances.social/api/1.0/instances/list?count=0';
614                                 $header = ['Authorization: Bearer '.$accesstoken];
615                                 $curlResult = Network::curl($api, false, ['headers' => $header]);
616
617                                 if ($curlResult->isSuccess()) {
618                                         $servers = json_decode($curlResult->getBody(), true);
619
620                                         foreach ($servers['instances'] as $server) {
621                                                 $url = (is_null($server['https_score']) ? 'http' : 'https') . '://' . $server['name'];
622                                                 Worker::add(PRIORITY_LOW, "DiscoverPoCo", "server", $url);
623                                         }
624                                 }
625                         }
626                 }
627
628                 // Currently disabled, since the service isn't available anymore.
629                 // It is not removed since I hope that there will be a successor.
630                 // Discover GNU Social Servers.
631                 //if (!Config::get('system','ostatus_disabled')) {
632                 //      $serverdata = "http://gstools.org/api/get_open_instances/";
633
634                 //      $curlResult = Network::curl($serverdata);
635                 //      if ($curlResult->isSuccess()) {
636                 //              $servers = json_decode($result->getBody(), true);
637
638                 //              foreach($servers['data'] as $server)
639                 //                      GServer::check($server['instance_address']);
640                 //      }
641                 //}
642
643                 Config::set('poco', 'last_federation_discovery', time());
644         }
645
646         public static function discoverSingleServer($id)
647         {
648                 $server = DBA::selectFirst('gserver', ['poco', 'nurl', 'url', 'network'], ['id' => $id]);
649
650                 if (!DBA::isResult($server)) {
651                         return false;
652                 }
653
654                 // Discover new servers out there (Works from Friendica version 3.5.2)
655                 self::fetchServerlist($server["poco"]);
656
657                 // Fetch all users from the other server
658                 $url = $server["poco"] . "/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
659
660                 Logger::info("Fetch all users from the server " . $server["url"]);
661
662                 $curlResult = Network::curl($url);
663
664                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
665                         $data = json_decode($curlResult->getBody(), true);
666
667                         if (!empty($data)) {
668                                 self::discoverServer($data, 2);
669                         }
670
671                         if (Config::get('system', 'poco_discovery') >= self::USERS_GCONTACTS) {
672                                 $timeframe = Config::get('system', 'poco_discovery_since');
673
674                                 if ($timeframe == 0) {
675                                         $timeframe = 30;
676                                 }
677
678                                 $updatedSince = date(DateTimeFormat::MYSQL, time() - $timeframe * 86400);
679
680                                 // Fetch all global contacts from the other server (Not working with Redmatrix and Friendica versions before 3.3)
681                                 $url = $server["poco"]."/@global?updatedSince=".$updatedSince."&fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
682
683                                 $success = false;
684
685                                 $curlResult = Network::curl($url);
686
687                                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
688                                         Logger::info("Fetch all global contacts from the server " . $server["nurl"]);
689                                         $data = json_decode($curlResult->getBody(), true);
690
691                                         if (!empty($data)) {
692                                                 $success = self::discoverServer($data);
693                                         }
694                                 }
695
696                                 if (!$success && !empty($data) && Config::get('system', 'poco_discovery') >= self::USERS_GCONTACTS_FALLBACK) {
697                                         Logger::info("Fetch contacts from users of the server " . $server["nurl"]);
698                                         self::discoverServerUsers($data, $server);
699                                 }
700                         }
701
702                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
703                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
704
705                         return true;
706                 } else {
707                         // If the server hadn't replied correctly, then force a sanity check
708                         GServer::check($server["url"], true);
709
710                         // If we couldn't reach the server, we will try it some time later
711                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
712                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
713
714                         return false;
715                 }
716         }
717
718         public static function discover($complete = false)
719         {
720                 // Update the server list
721                 self::discoverFederation();
722
723                 $no_of_queries = 5;
724
725                 $requery_days = intval(Config::get('system', 'poco_requery_days'));
726
727                 if ($requery_days == 0) {
728                         $requery_days = 7;
729                 }
730
731                 $last_update = date('c', time() - (60 * 60 * 24 * $requery_days));
732
733                 $gservers = q("SELECT `id`, `url`, `nurl`, `network`
734                         FROM `gserver`
735                         WHERE `last_contact` >= `last_failure`
736                         AND `poco` != ''
737                         AND `last_poco_query` < '%s'
738                         ORDER BY RAND()", DBA::escape($last_update)
739                 );
740
741                 if (DBA::isResult($gservers)) {
742                         foreach ($gservers as $gserver) {
743                                 if (!GServer::check($gserver['url'])) {
744                                         // The server is not reachable? Okay, then we will try it later
745                                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
746                                         DBA::update('gserver', $fields, ['nurl' => $gserver['nurl']]);
747                                         continue;
748                                 }
749
750                                 Logger::log('Update directory from server ' . $gserver['url'] . ' with ID ' . $gserver['id'], Logger::DEBUG);
751                                 Worker::add(PRIORITY_LOW, 'DiscoverPoCo', 'update_server_directory', (int) $gserver['id']);
752
753                                 if (!$complete && ( --$no_of_queries == 0)) {
754                                         break;
755                                 }
756                         }
757                 }
758         }
759
760         private static function discoverServerUsers(array $data, array $server)
761         {
762                 if (!isset($data['entry'])) {
763                         return;
764                 }
765
766                 foreach ($data['entry'] as $entry) {
767                         $username = '';
768
769                         if (isset($entry['urls'])) {
770                                 foreach ($entry['urls'] as $url) {
771                                         if ($url['type'] == 'profile') {
772                                                 $profile_url = $url['value'];
773                                                 $path_array = explode('/', parse_url($profile_url, PHP_URL_PATH));
774                                                 $username = end($path_array);
775                                         }
776                                 }
777                         }
778
779                         if ($username != '') {
780                                 Logger::log('Fetch contacts for the user ' . $username . ' from the server ' . $server['nurl'], Logger::DEBUG);
781
782                                 // Fetch all contacts from a given user from the other server
783                                 $url = $server['poco'] . '/' . $username . '/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation';
784
785                                 $curlResult = Network::curl($url);
786
787                                 if ($curlResult->isSuccess()) {
788                                         $data = json_decode($curlResult->getBody(), true);
789
790                                         if (!empty($data)) {
791                                                 self::discoverServer($data, 3);
792                                         }
793                                 }
794                         }
795                 }
796         }
797
798         private static function discoverServer(array $data, $default_generation = 0)
799         {
800                 if (empty($data['entry'])) {
801                         return false;
802                 }
803
804                 $success = false;
805
806                 foreach ($data['entry'] as $entry) {
807                         $profile_url = '';
808                         $profile_photo = '';
809                         $connect_url = '';
810                         $name = '';
811                         $network = '';
812                         $updated = DBA::NULL_DATETIME;
813                         $location = '';
814                         $about = '';
815                         $keywords = '';
816                         $gender = '';
817                         $contact_type = -1;
818                         $generation = $default_generation;
819
820                         if (!empty($entry['displayName'])) {
821                                 $name = $entry['displayName'];
822                         }
823
824                         if (isset($entry['urls'])) {
825                                 foreach ($entry['urls'] as $url) {
826                                         if ($url['type'] == 'profile') {
827                                                 $profile_url = $url['value'];
828                                                 continue;
829                                         }
830                                         if ($url['type'] == 'webfinger') {
831                                                 $connect_url = str_replace('acct:' , '', $url['value']);
832                                                 continue;
833                                         }
834                                 }
835                         }
836
837                         if (isset($entry['photos'])) {
838                                 foreach ($entry['photos'] as $photo) {
839                                         if ($photo['type'] == 'profile') {
840                                                 $profile_photo = $photo['value'];
841                                                 continue;
842                                         }
843                                 }
844                         }
845
846                         if (isset($entry['updated'])) {
847                                 $updated = date(DateTimeFormat::MYSQL, strtotime($entry['updated']));
848                         }
849
850                         if (isset($entry['network'])) {
851                                 $network = $entry['network'];
852                         }
853
854                         if (isset($entry['currentLocation'])) {
855                                 $location = $entry['currentLocation'];
856                         }
857
858                         if (isset($entry['aboutMe'])) {
859                                 $about = HTML::toBBCode($entry['aboutMe']);
860                         }
861
862                         if (isset($entry['gender'])) {
863                                 $gender = $entry['gender'];
864                         }
865
866                         if (isset($entry['generation']) && ($entry['generation'] > 0)) {
867                                 $generation = ++$entry['generation'];
868                         }
869
870                         if (isset($entry['contactType']) && ($entry['contactType'] >= 0)) {
871                                 $contact_type = $entry['contactType'];
872                         }
873
874                         if (isset($entry['tags'])) {
875                                 foreach ($entry['tags'] as $tag) {
876                                         $keywords = implode(", ", $tag);
877                                 }
878                         }
879
880                         if ($generation > 0) {
881                                 $success = true;
882
883                                 Logger::log("Store profile ".$profile_url, Logger::DEBUG);
884
885                                 $gcontact = ["url" => $profile_url,
886                                                 "name" => $name,
887                                                 "network" => $network,
888                                                 "photo" => $profile_photo,
889                                                 "about" => $about,
890                                                 "location" => $location,
891                                                 "gender" => $gender,
892                                                 "keywords" => $keywords,
893                                                 "connect" => $connect_url,
894                                                 "updated" => $updated,
895                                                 "contact-type" => $contact_type,
896                                                 "generation" => $generation];
897
898                                 try {
899                                         $gcontact = GContact::sanitize($gcontact);
900                                         GContact::update($gcontact);
901                                 } catch (Exception $e) {
902                                         Logger::log($e->getMessage(), Logger::DEBUG);
903                                 }
904
905                                 Logger::log("Done for profile ".$profile_url, Logger::DEBUG);
906                         }
907                 }
908                 return $success;
909         }
910
911 }