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