]> git.mxchange.org Git - friendica.git/blob - src/Protocol/PortableContact.php
70acf5064c0e643dee8596190eff748678702d68
[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,contactType,generation' : '?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,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                         $contact_type = -1;
138                         $generation = 0;
139
140                         if (!empty($entry['displayName'])) {
141                                 $name = $entry['displayName'];
142                         }
143
144                         if (isset($entry['urls'])) {
145                                 foreach ($entry['urls'] as $url) {
146                                         if ($url['type'] == 'profile') {
147                                                 $profile_url = $url['value'];
148                                                 continue;
149                                         }
150                                         if ($url['type'] == 'webfinger') {
151                                                 $connect_url = str_replace('acct:', '', $url['value']);
152                                                 continue;
153                                         }
154                                 }
155                         }
156                         if (isset($entry['photos'])) {
157                                 foreach ($entry['photos'] as $photo) {
158                                         if ($photo['type'] == 'profile') {
159                                                 $profile_photo = $photo['value'];
160                                                 continue;
161                                         }
162                                 }
163                         }
164
165                         if (isset($entry['updated'])) {
166                                 $updated = date(DateTimeFormat::MYSQL, strtotime($entry['updated']));
167                         }
168
169                         if (isset($entry['network'])) {
170                                 $network = $entry['network'];
171                         }
172
173                         if (isset($entry['currentLocation'])) {
174                                 $location = $entry['currentLocation'];
175                         }
176
177                         if (isset($entry['aboutMe'])) {
178                                 $about = HTML::toBBCode($entry['aboutMe']);
179                         }
180
181                         if (isset($entry['generation']) && ($entry['generation'] > 0)) {
182                                 $generation = ++$entry['generation'];
183                         }
184
185                         if (isset($entry['tags'])) {
186                                 foreach ($entry['tags'] as $tag) {
187                                         $keywords = implode(", ", $tag);
188                                 }
189                         }
190
191                         if (isset($entry['contactType']) && ($entry['contactType'] >= 0)) {
192                                 $contact_type = $entry['contactType'];
193                         }
194
195                         $gcontact = ["url" => $profile_url,
196                                         "name" => $name,
197                                         "network" => $network,
198                                         "photo" => $profile_photo,
199                                         "about" => $about,
200                                         "location" => $location,
201                                         "keywords" => $keywords,
202                                         "connect" => $connect_url,
203                                         "updated" => $updated,
204                                         "contact-type" => $contact_type,
205                                         "generation" => $generation];
206
207                         try {
208                                 $gcontact = GContact::sanitize($gcontact);
209                                 $gcid = GContact::update($gcontact);
210
211                                 GContact::link($gcid, $uid, $cid, $zcid);
212                         } catch (Exception $e) {
213                                 Logger::log($e->getMessage(), Logger::DEBUG);
214                         }
215                 }
216                 Logger::log("load: loaded $total entries", Logger::DEBUG);
217
218                 $condition = ["`cid` = ? AND `uid` = ? AND `zcid` = ? AND `updated` < UTC_TIMESTAMP - INTERVAL 2 DAY", $cid, $uid, $zcid];
219                 DBA::delete('glink', $condition);
220         }
221
222         /**
223          * 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          * 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         public static function discoverSingleServer($id)
279         {
280                 $server = DBA::selectFirst('gserver', ['poco', 'nurl', 'url', 'network'], ['id' => $id]);
281
282                 if (!DBA::isResult($server)) {
283                         return false;
284                 }
285
286                 // Discover new servers out there (Works from Friendica version 3.5.2)
287                 self::fetchServerlist($server["poco"]);
288
289                 // Fetch all users from the other server
290                 $url = $server["poco"] . "/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,contactType,generation";
291
292                 Logger::info("Fetch all users from the server " . $server["url"]);
293
294                 $curlResult = Network::curl($url);
295
296                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
297                         $data = json_decode($curlResult->getBody(), true);
298
299                         if (!empty($data)) {
300                                 self::discoverServer($data, 2);
301                         }
302
303                         if (DI::config()->get('system', 'poco_discovery') >= self::USERS_GCONTACTS) {
304                                 $timeframe = DI::config()->get('system', 'poco_discovery_since');
305
306                                 if ($timeframe == 0) {
307                                         $timeframe = 30;
308                                 }
309
310                                 $updatedSince = date(DateTimeFormat::MYSQL, time() - $timeframe * 86400);
311
312                                 // Fetch all global contacts from the other server (Not working with Redmatrix and Friendica versions before 3.3)
313                                 $url = $server["poco"]."/@global?updatedSince=".$updatedSince."&fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,contactType,generation";
314
315                                 $success = false;
316
317                                 $curlResult = Network::curl($url);
318
319                                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
320                                         Logger::info("Fetch all global contacts from the server " . $server["nurl"]);
321                                         $data = json_decode($curlResult->getBody(), true);
322
323                                         if (!empty($data)) {
324                                                 $success = self::discoverServer($data);
325                                         }
326                                 }
327
328                                 if (!$success && !empty($data) && DI::config()->get('system', 'poco_discovery') >= self::USERS_GCONTACTS_FALLBACK) {
329                                         Logger::info("Fetch contacts from users of the server " . $server["nurl"]);
330                                         self::discoverServerUsers($data, $server);
331                                 }
332                         }
333
334                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
335                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
336
337                         return true;
338                 } else {
339                         // If the server hadn't replied correctly, then force a sanity check
340                         GServer::check($server["url"], $server["network"], true);
341
342                         // If we couldn't reach the server, we will try it some time later
343                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
344                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
345
346                         return false;
347                 }
348         }
349
350         private static function discoverServerUsers(array $data, array $server)
351         {
352                 if (!isset($data['entry'])) {
353                         return;
354                 }
355
356                 foreach ($data['entry'] as $entry) {
357                         $username = '';
358
359                         if (isset($entry['urls'])) {
360                                 foreach ($entry['urls'] as $url) {
361                                         if ($url['type'] == 'profile') {
362                                                 $profile_url = $url['value'];
363                                                 $path_array = explode('/', parse_url($profile_url, PHP_URL_PATH));
364                                                 $username = end($path_array);
365                                         }
366                                 }
367                         }
368
369                         if ($username != '') {
370                                 Logger::log('Fetch contacts for the user ' . $username . ' from the server ' . $server['nurl'], Logger::DEBUG);
371
372                                 // Fetch all contacts from a given user from the other server
373                                 $url = $server['poco'] . '/' . $username . '/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,contactType,generation';
374
375                                 $curlResult = Network::curl($url);
376
377                                 if ($curlResult->isSuccess()) {
378                                         $data = json_decode($curlResult->getBody(), true);
379
380                                         if (!empty($data)) {
381                                                 self::discoverServer($data, 3);
382                                         }
383                                 }
384                         }
385                 }
386         }
387
388         private static function discoverServer(array $data, $default_generation = 0)
389         {
390                 if (empty($data['entry'])) {
391                         return false;
392                 }
393
394                 $success = false;
395
396                 foreach ($data['entry'] as $entry) {
397                         $profile_url = '';
398                         $profile_photo = '';
399                         $connect_url = '';
400                         $name = '';
401                         $network = '';
402                         $updated = DBA::NULL_DATETIME;
403                         $location = '';
404                         $about = '';
405                         $keywords = '';
406                         $contact_type = -1;
407                         $generation = $default_generation;
408
409                         if (!empty($entry['displayName'])) {
410                                 $name = $entry['displayName'];
411                         }
412
413                         if (isset($entry['urls'])) {
414                                 foreach ($entry['urls'] as $url) {
415                                         if ($url['type'] == 'profile') {
416                                                 $profile_url = $url['value'];
417                                                 continue;
418                                         }
419                                         if ($url['type'] == 'webfinger') {
420                                                 $connect_url = str_replace('acct:' , '', $url['value']);
421                                                 continue;
422                                         }
423                                 }
424                         }
425
426                         if (isset($entry['photos'])) {
427                                 foreach ($entry['photos'] as $photo) {
428                                         if ($photo['type'] == 'profile') {
429                                                 $profile_photo = $photo['value'];
430                                                 continue;
431                                         }
432                                 }
433                         }
434
435                         if (isset($entry['updated'])) {
436                                 $updated = date(DateTimeFormat::MYSQL, strtotime($entry['updated']));
437                         }
438
439                         if (isset($entry['network'])) {
440                                 $network = $entry['network'];
441                         }
442
443                         if (isset($entry['currentLocation'])) {
444                                 $location = $entry['currentLocation'];
445                         }
446
447                         if (isset($entry['aboutMe'])) {
448                                 $about = HTML::toBBCode($entry['aboutMe']);
449                         }
450
451                         if (isset($entry['generation']) && ($entry['generation'] > 0)) {
452                                 $generation = ++$entry['generation'];
453                         }
454
455                         if (isset($entry['contactType']) && ($entry['contactType'] >= 0)) {
456                                 $contact_type = $entry['contactType'];
457                         }
458
459                         if (isset($entry['tags'])) {
460                                 foreach ($entry['tags'] as $tag) {
461                                         $keywords = implode(", ", $tag);
462                                 }
463                         }
464
465                         if ($generation > 0) {
466                                 $success = true;
467
468                                 Logger::log("Store profile ".$profile_url, Logger::DEBUG);
469
470                                 $gcontact = ["url" => $profile_url,
471                                                 "name" => $name,
472                                                 "network" => $network,
473                                                 "photo" => $profile_photo,
474                                                 "about" => $about,
475                                                 "location" => $location,
476                                                 "keywords" => $keywords,
477                                                 "connect" => $connect_url,
478                                                 "updated" => $updated,
479                                                 "contact-type" => $contact_type,
480                                                 "generation" => $generation];
481
482                                 try {
483                                         $gcontact = GContact::sanitize($gcontact);
484                                         GContact::update($gcontact);
485                                 } catch (Exception $e) {
486                                         Logger::log($e->getMessage(), Logger::DEBUG);
487                                 }
488
489                                 Logger::log("Done for profile ".$profile_url, Logger::DEBUG);
490                         }
491                 }
492                 return $success;
493         }
494 }