]> git.mxchange.org Git - friendica.git/blob - src/Protocol/PortableContact.php
4ff9ae98bf45b77b7f4d7cb2c22f50af1e435738
[friendica.git] / src / Protocol / PortableContact.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Protocol;
23
24 use Exception;
25 use Friendica\Content\Text\HTML;
26 use Friendica\Core\Logger;
27 use Friendica\Core\Protocol;
28 use Friendica\Core\Worker;
29 use Friendica\Database\DBA;
30 use Friendica\DI;
31 use Friendica\Model\GContact;
32 use Friendica\Model\GServer;
33 use Friendica\Util\DateTimeFormat;
34 use Friendica\Util\Network;
35 use Friendica\Util\Strings;
36
37 /**
38  *
39  * @todo Move GNU Social URL schemata (http://server.tld/user/number) to http://server.tld/username
40  * @todo Fetch profile data from profile page for Redmatrix users
41  * @todo Detect if it is a forum
42  */
43 class PortableContact
44 {
45         const DISABLED = 0;
46         const USERS = 1;
47         const USERS_GCONTACTS = 2;
48         const USERS_GCONTACTS_FALLBACK = 3;
49
50         /**
51          * Fetch POCO data
52          *
53          * @param integer $cid  Contact ID
54          * @param integer $uid  User ID
55          * @param integer $zcid Global Contact ID
56          * @param integer $url  POCO address that should be polled
57          *
58          * Given a contact-id (minimum), load the PortableContacts friend list for that contact,
59          * and add the entries to the gcontact (Global Contact) table, or update existing entries
60          * if anything (name or photo) has changed.
61          * We use normalised urls for comparison which ignore http vs https and www.domain vs domain
62          *
63          * Once the global contact is stored add (if necessary) the contact linkage which associates
64          * the given uid, cid to the global contact entry. There can be many uid/cid combinations
65          * pointing to the same global contact id.
66          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
67          */
68         public static function loadWorker($cid, $uid = 0, $zcid = 0, $url = null)
69         {
70                 // Call the function "load" via the worker
71                 Worker::add(PRIORITY_LOW, 'FetchPoCo', (int)$cid, (int)$uid, (int)$zcid, $url);
72         }
73
74         /**
75          * Fetch POCO data from the worker
76          *
77          * @param integer $cid  Contact ID
78          * @param integer $uid  User ID
79          * @param integer $zcid Global Contact ID
80          * @param integer $url  POCO address that should be polled
81          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
82          */
83         public static function load($cid, $uid, $zcid, $url)
84         {
85                 if ($cid) {
86                         if (!$url || !$uid) {
87                                 $contact = DBA::selectFirst('contact', ['poco', 'uid'], ['id' => $cid]);
88                                 if (DBA::isResult($contact)) {
89                                         $url = $contact['poco'];
90                                         $uid = $contact['uid'];
91                                 }
92                         }
93                         if (!$uid) {
94                                 return;
95                         }
96                 }
97
98                 if (!$url) {
99                         return;
100                 }
101
102                 $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');
103
104                 Logger::log('load: ' . $url, Logger::DEBUG);
105
106                 $fetchresult = Network::fetchUrlFull($url);
107                 $s = $fetchresult->getBody();
108
109                 Logger::log('load: returns ' . $s, Logger::DATA);
110
111                 Logger::log('load: return code: ' . $fetchresult->getReturnCode(), Logger::DEBUG);
112
113                 if (($fetchresult->getReturnCode() > 299) || (! $s)) {
114                         return;
115                 }
116
117                 $j = json_decode($s, true);
118
119                 Logger::log('load: json: ' . print_r($j, true), Logger::DATA);
120
121                 if (!isset($j['entry'])) {
122                         return;
123                 }
124
125                 $total = 0;
126                 foreach ($j['entry'] as $entry) {
127                         $total ++;
128                         $profile_url = '';
129                         $profile_photo = '';
130                         $connect_url = '';
131                         $name = '';
132                         $network = '';
133                         $updated = DBA::NULL_DATETIME;
134                         $location = '';
135                         $about = '';
136                         $keywords = '';
137                         $gender = '';
138                         $contact_type = -1;
139                         $generation = 0;
140
141                         if (!empty($entry['displayName'])) {
142                                 $name = $entry['displayName'];
143                         }
144
145                         if (isset($entry['urls'])) {
146                                 foreach ($entry['urls'] as $url) {
147                                         if ($url['type'] == 'profile') {
148                                                 $profile_url = $url['value'];
149                                                 continue;
150                                         }
151                                         if ($url['type'] == 'webfinger') {
152                                                 $connect_url = str_replace('acct:', '', $url['value']);
153                                                 continue;
154                                         }
155                                 }
156                         }
157                         if (isset($entry['photos'])) {
158                                 foreach ($entry['photos'] as $photo) {
159                                         if ($photo['type'] == 'profile') {
160                                                 $profile_photo = $photo['value'];
161                                                 continue;
162                                         }
163                                 }
164                         }
165
166                         if (isset($entry['updated'])) {
167                                 $updated = date(DateTimeFormat::MYSQL, strtotime($entry['updated']));
168                         }
169
170                         if (isset($entry['network'])) {
171                                 $network = $entry['network'];
172                         }
173
174                         if (isset($entry['currentLocation'])) {
175                                 $location = $entry['currentLocation'];
176                         }
177
178                         if (isset($entry['aboutMe'])) {
179                                 $about = HTML::toBBCode($entry['aboutMe']);
180                         }
181
182                         if (isset($entry['gender'])) {
183                                 $gender = $entry['gender'];
184                         }
185
186                         if (isset($entry['generation']) && ($entry['generation'] > 0)) {
187                                 $generation = ++$entry['generation'];
188                         }
189
190                         if (isset($entry['tags'])) {
191                                 foreach ($entry['tags'] as $tag) {
192                                         $keywords = implode(", ", $tag);
193                                 }
194                         }
195
196                         if (isset($entry['contactType']) && ($entry['contactType'] >= 0)) {
197                                 $contact_type = $entry['contactType'];
198                         }
199
200                         $gcontact = ["url" => $profile_url,
201                                         "name" => $name,
202                                         "network" => $network,
203                                         "photo" => $profile_photo,
204                                         "about" => $about,
205                                         "location" => $location,
206                                         "gender" => $gender,
207                                         "keywords" => $keywords,
208                                         "connect" => $connect_url,
209                                         "updated" => $updated,
210                                         "contact-type" => $contact_type,
211                                         "generation" => $generation];
212
213                         try {
214                                 $gcontact = GContact::sanitize($gcontact);
215                                 $gcid = GContact::update($gcontact);
216
217                                 GContact::link($gcid, $uid, $cid, $zcid);
218                         } catch (Exception $e) {
219                                 Logger::log($e->getMessage(), Logger::DEBUG);
220                         }
221                 }
222                 Logger::log("load: loaded $total entries", Logger::DEBUG);
223
224                 $condition = ["`cid` = ? AND `uid` = ? AND `zcid` = ? AND `updated` < UTC_TIMESTAMP - INTERVAL 2 DAY", $cid, $uid, $zcid];
225                 DBA::delete('glink', $condition);
226         }
227
228         /**
229          * Returns a list of all known servers
230          * @return array List of server urls
231          * @throws Exception
232          */
233         public static function serverlist()
234         {
235                 $r = q(
236                         "SELECT `url`, `site_name` AS `displayName`, `network`, `platform`, `version` FROM `gserver`
237                         WHERE `network` IN ('%s', '%s', '%s') AND `last_contact` > `last_failure`
238                         ORDER BY `last_contact`
239                         LIMIT 1000",
240                         DBA::escape(Protocol::DFRN),
241                         DBA::escape(Protocol::DIASPORA),
242                         DBA::escape(Protocol::OSTATUS)
243                 );
244
245                 if (!DBA::isResult($r)) {
246                         return false;
247                 }
248
249                 return $r;
250         }
251
252         /**
253          * Fetch server list from remote servers and adds them when they are new.
254          *
255          * @param string $poco URL to the POCO endpoint
256          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
257          */
258         private static function fetchServerlist($poco)
259         {
260                 $curlResult = Network::curl($poco . "/@server");
261
262                 if (!$curlResult->isSuccess()) {
263                         return;
264                 }
265
266                 $serverlist = json_decode($curlResult->getBody(), true);
267
268                 if (!is_array($serverlist)) {
269                         return;
270                 }
271
272                 foreach ($serverlist as $server) {
273                         $server_url = str_replace("/index.php", "", $server['url']);
274
275                         $r = q("SELECT `nurl` FROM `gserver` WHERE `nurl` = '%s'", DBA::escape(Strings::normaliseLink($server_url)));
276
277                         if (!DBA::isResult($r)) {
278                                 Logger::log("Call server check for server ".$server_url, Logger::DEBUG);
279                                 Worker::add(PRIORITY_LOW, 'UpdateGServer', $server_url);
280                         }
281                 }
282         }
283
284         public static function discoverSingleServer($id)
285         {
286                 $server = DBA::selectFirst('gserver', ['poco', 'nurl', 'url', 'network'], ['id' => $id]);
287
288                 if (!DBA::isResult($server)) {
289                         return false;
290                 }
291
292                 // Discover new servers out there (Works from Friendica version 3.5.2)
293                 self::fetchServerlist($server["poco"]);
294
295                 // Fetch all users from the other server
296                 $url = $server["poco"] . "/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
297
298                 Logger::info("Fetch all users from the server " . $server["url"]);
299
300                 $curlResult = Network::curl($url);
301
302                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
303                         $data = json_decode($curlResult->getBody(), true);
304
305                         if (!empty($data)) {
306                                 self::discoverServer($data, 2);
307                         }
308
309                         if (DI::config()->get('system', 'poco_discovery') >= self::USERS_GCONTACTS) {
310                                 $timeframe = DI::config()->get('system', 'poco_discovery_since');
311
312                                 if ($timeframe == 0) {
313                                         $timeframe = 30;
314                                 }
315
316                                 $updatedSince = date(DateTimeFormat::MYSQL, time() - $timeframe * 86400);
317
318                                 // Fetch all global contacts from the other server (Not working with Redmatrix and Friendica versions before 3.3)
319                                 $url = $server["poco"]."/@global?updatedSince=".$updatedSince."&fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
320
321                                 $success = false;
322
323                                 $curlResult = Network::curl($url);
324
325                                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
326                                         Logger::info("Fetch all global contacts from the server " . $server["nurl"]);
327                                         $data = json_decode($curlResult->getBody(), true);
328
329                                         if (!empty($data)) {
330                                                 $success = self::discoverServer($data);
331                                         }
332                                 }
333
334                                 if (!$success && !empty($data) && DI::config()->get('system', 'poco_discovery') >= self::USERS_GCONTACTS_FALLBACK) {
335                                         Logger::info("Fetch contacts from users of the server " . $server["nurl"]);
336                                         self::discoverServerUsers($data, $server);
337                                 }
338                         }
339
340                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
341                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
342
343                         return true;
344                 } else {
345                         // If the server hadn't replied correctly, then force a sanity check
346                         GServer::check($server["url"], $server["network"], true);
347
348                         // If we couldn't reach the server, we will try it some time later
349                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
350                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
351
352                         return false;
353                 }
354         }
355
356         private static function discoverServerUsers(array $data, array $server)
357         {
358                 if (!isset($data['entry'])) {
359                         return;
360                 }
361
362                 foreach ($data['entry'] as $entry) {
363                         $username = '';
364
365                         if (isset($entry['urls'])) {
366                                 foreach ($entry['urls'] as $url) {
367                                         if ($url['type'] == 'profile') {
368                                                 $profile_url = $url['value'];
369                                                 $path_array = explode('/', parse_url($profile_url, PHP_URL_PATH));
370                                                 $username = end($path_array);
371                                         }
372                                 }
373                         }
374
375                         if ($username != '') {
376                                 Logger::log('Fetch contacts for the user ' . $username . ' from the server ' . $server['nurl'], Logger::DEBUG);
377
378                                 // Fetch all contacts from a given user from the other server
379                                 $url = $server['poco'] . '/' . $username . '/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation';
380
381                                 $curlResult = Network::curl($url);
382
383                                 if ($curlResult->isSuccess()) {
384                                         $data = json_decode($curlResult->getBody(), true);
385
386                                         if (!empty($data)) {
387                                                 self::discoverServer($data, 3);
388                                         }
389                                 }
390                         }
391                 }
392         }
393
394         private static function discoverServer(array $data, $default_generation = 0)
395         {
396                 if (empty($data['entry'])) {
397                         return false;
398                 }
399
400                 $success = false;
401
402                 foreach ($data['entry'] as $entry) {
403                         $profile_url = '';
404                         $profile_photo = '';
405                         $connect_url = '';
406                         $name = '';
407                         $network = '';
408                         $updated = DBA::NULL_DATETIME;
409                         $location = '';
410                         $about = '';
411                         $keywords = '';
412                         $gender = '';
413                         $contact_type = -1;
414                         $generation = $default_generation;
415
416                         if (!empty($entry['displayName'])) {
417                                 $name = $entry['displayName'];
418                         }
419
420                         if (isset($entry['urls'])) {
421                                 foreach ($entry['urls'] as $url) {
422                                         if ($url['type'] == 'profile') {
423                                                 $profile_url = $url['value'];
424                                                 continue;
425                                         }
426                                         if ($url['type'] == 'webfinger') {
427                                                 $connect_url = str_replace('acct:' , '', $url['value']);
428                                                 continue;
429                                         }
430                                 }
431                         }
432
433                         if (isset($entry['photos'])) {
434                                 foreach ($entry['photos'] as $photo) {
435                                         if ($photo['type'] == 'profile') {
436                                                 $profile_photo = $photo['value'];
437                                                 continue;
438                                         }
439                                 }
440                         }
441
442                         if (isset($entry['updated'])) {
443                                 $updated = date(DateTimeFormat::MYSQL, strtotime($entry['updated']));
444                         }
445
446                         if (isset($entry['network'])) {
447                                 $network = $entry['network'];
448                         }
449
450                         if (isset($entry['currentLocation'])) {
451                                 $location = $entry['currentLocation'];
452                         }
453
454                         if (isset($entry['aboutMe'])) {
455                                 $about = HTML::toBBCode($entry['aboutMe']);
456                         }
457
458                         if (isset($entry['gender'])) {
459                                 $gender = $entry['gender'];
460                         }
461
462                         if (isset($entry['generation']) && ($entry['generation'] > 0)) {
463                                 $generation = ++$entry['generation'];
464                         }
465
466                         if (isset($entry['contactType']) && ($entry['contactType'] >= 0)) {
467                                 $contact_type = $entry['contactType'];
468                         }
469
470                         if (isset($entry['tags'])) {
471                                 foreach ($entry['tags'] as $tag) {
472                                         $keywords = implode(", ", $tag);
473                                 }
474                         }
475
476                         if ($generation > 0) {
477                                 $success = true;
478
479                                 Logger::log("Store profile ".$profile_url, Logger::DEBUG);
480
481                                 $gcontact = ["url" => $profile_url,
482                                                 "name" => $name,
483                                                 "network" => $network,
484                                                 "photo" => $profile_photo,
485                                                 "about" => $about,
486                                                 "location" => $location,
487                                                 "gender" => $gender,
488                                                 "keywords" => $keywords,
489                                                 "connect" => $connect_url,
490                                                 "updated" => $updated,
491                                                 "contact-type" => $contact_type,
492                                                 "generation" => $generation];
493
494                                 try {
495                                         $gcontact = GContact::sanitize($gcontact);
496                                         GContact::update($gcontact);
497                                 } catch (Exception $e) {
498                                         Logger::log($e->getMessage(), Logger::DEBUG);
499                                 }
500
501                                 Logger::log("Done for profile ".$profile_url, Logger::DEBUG);
502                         }
503                 }
504                 return $success;
505         }
506 }