]> git.mxchange.org Git - friendica.git/blob - src/Protocol/PortableContact.php
Merge pull request #7770 from nupplaphil/task/Activity
[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 alternateOStatusUrl($url)
218         {
219                 return(preg_match("=https?://.+/user/\d+=ism", $url, $matches));
220         }
221
222         public static function updateNeeded($created, $updated, $last_failure, $last_contact)
223         {
224                 $now = strtotime(DateTimeFormat::utcNow());
225
226                 if ($updated > $last_contact) {
227                         $contact_time = strtotime($updated);
228                 } else {
229                         $contact_time = strtotime($last_contact);
230                 }
231
232                 $failure_time = strtotime($last_failure);
233                 $created_time = strtotime($created);
234
235                 // If there is no "created" time then use the current time
236                 if ($created_time <= 0) {
237                         $created_time = $now;
238                 }
239
240                 // If the last contact was less than 24 hours then don't update
241                 if (($now - $contact_time) < (60 * 60 * 24)) {
242                         return false;
243                 }
244
245                 // If the last failure was less than 24 hours then don't update
246                 if (($now - $failure_time) < (60 * 60 * 24)) {
247                         return false;
248                 }
249
250                 // If the last contact was less than a week ago and the last failure is older than a week then don't update
251                 //if ((($now - $contact_time) < (60 * 60 * 24 * 7)) && ($contact_time > $failure_time))
252                 //      return false;
253
254                 // 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
255                 if ((($now - $contact_time) > (60 * 60 * 24 * 7)) && (($now - $created_time) > (60 * 60 * 24 * 7)) && (($now - $failure_time) < (60 * 60 * 24 * 7))) {
256                         return false;
257                 }
258
259                 // 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
260                 if ((($now - $contact_time) > (60 * 60 * 24 * 30)) && (($now - $created_time) > (60 * 60 * 24 * 30)) && (($now - $failure_time) < (60 * 60 * 24 * 30))) {
261                         return false;
262                 }
263
264                 return true;
265         }
266
267         /**
268          * @brief Returns a list of all known servers
269          * @return array List of server urls
270          * @throws Exception
271          */
272         public static function serverlist()
273         {
274                 $r = q(
275                         "SELECT `url`, `site_name` AS `displayName`, `network`, `platform`, `version` FROM `gserver`
276                         WHERE `network` IN ('%s', '%s', '%s') AND `last_contact` > `last_failure`
277                         ORDER BY `last_contact`
278                         LIMIT 1000",
279                         DBA::escape(Protocol::DFRN),
280                         DBA::escape(Protocol::DIASPORA),
281                         DBA::escape(Protocol::OSTATUS)
282                 );
283
284                 if (!DBA::isResult($r)) {
285                         return false;
286                 }
287
288                 return $r;
289         }
290
291         /**
292          * @brief Fetch server list from remote servers and adds them when they are new.
293          *
294          * @param string $poco URL to the POCO endpoint
295          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
296          */
297         private static function fetchServerlist($poco)
298         {
299                 $curlResult = Network::curl($poco . "/@server");
300
301                 if (!$curlResult->isSuccess()) {
302                         return;
303                 }
304
305                 $serverlist = json_decode($curlResult->getBody(), true);
306
307                 if (!is_array($serverlist)) {
308                         return;
309                 }
310
311                 foreach ($serverlist as $server) {
312                         $server_url = str_replace("/index.php", "", $server['url']);
313
314                         $r = q("SELECT `nurl` FROM `gserver` WHERE `nurl` = '%s'", DBA::escape(Strings::normaliseLink($server_url)));
315
316                         if (!DBA::isResult($r)) {
317                                 Logger::log("Call server check for server ".$server_url, Logger::DEBUG);
318                                 Worker::add(PRIORITY_LOW, "DiscoverPoCo", "server", $server_url);
319                         }
320                 }
321         }
322
323         private static function discoverFederation()
324         {
325                 $last = Config::get('poco', 'last_federation_discovery');
326
327                 if ($last) {
328                         $next = $last + (24 * 60 * 60);
329
330                         if ($next > time()) {
331                                 return;
332                         }
333                 }
334
335                 // Discover Friendica, Hubzilla and Diaspora servers
336                 $curlResult = Network::fetchUrl("http://the-federation.info/pods.json");
337
338                 if (!empty($curlResult)) {
339                         $servers = json_decode($curlResult, true);
340
341                         if (!empty($servers['pods'])) {
342                                 foreach ($servers['pods'] as $server) {
343                                         Worker::add(PRIORITY_LOW, "DiscoverPoCo", "server", "https://" . $server['host']);
344                                 }
345                         }
346                 }
347
348                 // Disvover Mastodon servers
349                 if (!Config::get('system', 'ostatus_disabled')) {
350                         $accesstoken = Config::get('system', 'instances_social_key');
351
352                         if (!empty($accesstoken)) {
353                                 $api = 'https://instances.social/api/1.0/instances/list?count=0';
354                                 $header = ['Authorization: Bearer '.$accesstoken];
355                                 $curlResult = Network::curl($api, false, ['headers' => $header]);
356
357                                 if ($curlResult->isSuccess()) {
358                                         $servers = json_decode($curlResult->getBody(), true);
359
360                                         foreach ($servers['instances'] as $server) {
361                                                 $url = (is_null($server['https_score']) ? 'http' : 'https') . '://' . $server['name'];
362                                                 Worker::add(PRIORITY_LOW, "DiscoverPoCo", "server", $url);
363                                         }
364                                 }
365                         }
366                 }
367
368                 // Currently disabled, since the service isn't available anymore.
369                 // It is not removed since I hope that there will be a successor.
370                 // Discover GNU Social Servers.
371                 //if (!Config::get('system','ostatus_disabled')) {
372                 //      $serverdata = "http://gstools.org/api/get_open_instances/";
373
374                 //      $curlResult = Network::curl($serverdata);
375                 //      if ($curlResult->isSuccess()) {
376                 //              $servers = json_decode($result->getBody(), true);
377
378                 //              foreach($servers['data'] as $server)
379                 //                      GServer::check($server['instance_address']);
380                 //      }
381                 //}
382
383                 Config::set('poco', 'last_federation_discovery', time());
384         }
385
386         public static function discoverSingleServer($id)
387         {
388                 $server = DBA::selectFirst('gserver', ['poco', 'nurl', 'url', 'network'], ['id' => $id]);
389
390                 if (!DBA::isResult($server)) {
391                         return false;
392                 }
393
394                 // Discover new servers out there (Works from Friendica version 3.5.2)
395                 self::fetchServerlist($server["poco"]);
396
397                 // Fetch all users from the other server
398                 $url = $server["poco"] . "/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
399
400                 Logger::info("Fetch all users from the server " . $server["url"]);
401
402                 $curlResult = Network::curl($url);
403
404                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
405                         $data = json_decode($curlResult->getBody(), true);
406
407                         if (!empty($data)) {
408                                 self::discoverServer($data, 2);
409                         }
410
411                         if (Config::get('system', 'poco_discovery') >= self::USERS_GCONTACTS) {
412                                 $timeframe = Config::get('system', 'poco_discovery_since');
413
414                                 if ($timeframe == 0) {
415                                         $timeframe = 30;
416                                 }
417
418                                 $updatedSince = date(DateTimeFormat::MYSQL, time() - $timeframe * 86400);
419
420                                 // Fetch all global contacts from the other server (Not working with Redmatrix and Friendica versions before 3.3)
421                                 $url = $server["poco"]."/@global?updatedSince=".$updatedSince."&fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
422
423                                 $success = false;
424
425                                 $curlResult = Network::curl($url);
426
427                                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
428                                         Logger::info("Fetch all global contacts from the server " . $server["nurl"]);
429                                         $data = json_decode($curlResult->getBody(), true);
430
431                                         if (!empty($data)) {
432                                                 $success = self::discoverServer($data);
433                                         }
434                                 }
435
436                                 if (!$success && !empty($data) && Config::get('system', 'poco_discovery') >= self::USERS_GCONTACTS_FALLBACK) {
437                                         Logger::info("Fetch contacts from users of the server " . $server["nurl"]);
438                                         self::discoverServerUsers($data, $server);
439                                 }
440                         }
441
442                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
443                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
444
445                         return true;
446                 } else {
447                         // If the server hadn't replied correctly, then force a sanity check
448                         GServer::check($server["url"], $server["network"], true);
449
450                         // If we couldn't reach the server, we will try it some time later
451                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
452                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
453
454                         return false;
455                 }
456         }
457
458         public static function discover($complete = false)
459         {
460                 // Update the server list
461                 self::discoverFederation();
462
463                 $no_of_queries = 5;
464
465                 $requery_days = intval(Config::get('system', 'poco_requery_days'));
466
467                 if ($requery_days == 0) {
468                         $requery_days = 7;
469                 }
470
471                 $last_update = date('c', time() - (60 * 60 * 24 * $requery_days));
472
473                 $gservers = q("SELECT `id`, `url`, `nurl`, `network`
474                         FROM `gserver`
475                         WHERE `last_contact` >= `last_failure`
476                         AND `poco` != ''
477                         AND `last_poco_query` < '%s'
478                         ORDER BY RAND()", DBA::escape($last_update)
479                 );
480
481                 if (DBA::isResult($gservers)) {
482                         foreach ($gservers as $gserver) {
483                                 if (!GServer::check($gserver['url'], $gserver['network'])) {
484                                         // The server is not reachable? Okay, then we will try it later
485                                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
486                                         DBA::update('gserver', $fields, ['nurl' => $gserver['nurl']]);
487                                         continue;
488                                 }
489
490                                 Logger::log('Update directory from server ' . $gserver['url'] . ' with ID ' . $gserver['id'], Logger::DEBUG);
491                                 Worker::add(PRIORITY_LOW, 'DiscoverPoCo', 'update_server_directory', (int) $gserver['id']);
492
493                                 if (!$complete && ( --$no_of_queries == 0)) {
494                                         break;
495                                 }
496                         }
497                 }
498         }
499
500         private static function discoverServerUsers(array $data, array $server)
501         {
502                 if (!isset($data['entry'])) {
503                         return;
504                 }
505
506                 foreach ($data['entry'] as $entry) {
507                         $username = '';
508
509                         if (isset($entry['urls'])) {
510                                 foreach ($entry['urls'] as $url) {
511                                         if ($url['type'] == 'profile') {
512                                                 $profile_url = $url['value'];
513                                                 $path_array = explode('/', parse_url($profile_url, PHP_URL_PATH));
514                                                 $username = end($path_array);
515                                         }
516                                 }
517                         }
518
519                         if ($username != '') {
520                                 Logger::log('Fetch contacts for the user ' . $username . ' from the server ' . $server['nurl'], Logger::DEBUG);
521
522                                 // Fetch all contacts from a given user from the other server
523                                 $url = $server['poco'] . '/' . $username . '/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation';
524
525                                 $curlResult = Network::curl($url);
526
527                                 if ($curlResult->isSuccess()) {
528                                         $data = json_decode($curlResult->getBody(), true);
529
530                                         if (!empty($data)) {
531                                                 self::discoverServer($data, 3);
532                                         }
533                                 }
534                         }
535                 }
536         }
537
538         private static function discoverServer(array $data, $default_generation = 0)
539         {
540                 if (empty($data['entry'])) {
541                         return false;
542                 }
543
544                 $success = false;
545
546                 foreach ($data['entry'] as $entry) {
547                         $profile_url = '';
548                         $profile_photo = '';
549                         $connect_url = '';
550                         $name = '';
551                         $network = '';
552                         $updated = DBA::NULL_DATETIME;
553                         $location = '';
554                         $about = '';
555                         $keywords = '';
556                         $gender = '';
557                         $contact_type = -1;
558                         $generation = $default_generation;
559
560                         if (!empty($entry['displayName'])) {
561                                 $name = $entry['displayName'];
562                         }
563
564                         if (isset($entry['urls'])) {
565                                 foreach ($entry['urls'] as $url) {
566                                         if ($url['type'] == 'profile') {
567                                                 $profile_url = $url['value'];
568                                                 continue;
569                                         }
570                                         if ($url['type'] == 'webfinger') {
571                                                 $connect_url = str_replace('acct:' , '', $url['value']);
572                                                 continue;
573                                         }
574                                 }
575                         }
576
577                         if (isset($entry['photos'])) {
578                                 foreach ($entry['photos'] as $photo) {
579                                         if ($photo['type'] == 'profile') {
580                                                 $profile_photo = $photo['value'];
581                                                 continue;
582                                         }
583                                 }
584                         }
585
586                         if (isset($entry['updated'])) {
587                                 $updated = date(DateTimeFormat::MYSQL, strtotime($entry['updated']));
588                         }
589
590                         if (isset($entry['network'])) {
591                                 $network = $entry['network'];
592                         }
593
594                         if (isset($entry['currentLocation'])) {
595                                 $location = $entry['currentLocation'];
596                         }
597
598                         if (isset($entry['aboutMe'])) {
599                                 $about = HTML::toBBCode($entry['aboutMe']);
600                         }
601
602                         if (isset($entry['gender'])) {
603                                 $gender = $entry['gender'];
604                         }
605
606                         if (isset($entry['generation']) && ($entry['generation'] > 0)) {
607                                 $generation = ++$entry['generation'];
608                         }
609
610                         if (isset($entry['contactType']) && ($entry['contactType'] >= 0)) {
611                                 $contact_type = $entry['contactType'];
612                         }
613
614                         if (isset($entry['tags'])) {
615                                 foreach ($entry['tags'] as $tag) {
616                                         $keywords = implode(", ", $tag);
617                                 }
618                         }
619
620                         if ($generation > 0) {
621                                 $success = true;
622
623                                 Logger::log("Store profile ".$profile_url, Logger::DEBUG);
624
625                                 $gcontact = ["url" => $profile_url,
626                                                 "name" => $name,
627                                                 "network" => $network,
628                                                 "photo" => $profile_photo,
629                                                 "about" => $about,
630                                                 "location" => $location,
631                                                 "gender" => $gender,
632                                                 "keywords" => $keywords,
633                                                 "connect" => $connect_url,
634                                                 "updated" => $updated,
635                                                 "contact-type" => $contact_type,
636                                                 "generation" => $generation];
637
638                                 try {
639                                         $gcontact = GContact::sanitize($gcontact);
640                                         GContact::update($gcontact);
641                                 } catch (Exception $e) {
642                                         Logger::log($e->getMessage(), Logger::DEBUG);
643                                 }
644
645                                 Logger::log("Done for profile ".$profile_url, Logger::DEBUG);
646                         }
647                 }
648                 return $success;
649         }
650
651 }