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