]> git.mxchange.org Git - friendica.git/blob - src/Protocol/PortableContact.php
ceafffa832a65cdaac0933a645b87c6ebb0a03d2
[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          * 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          * 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         /**
218          * Returns a list of all known servers
219          * @return array List of server urls
220          * @throws Exception
221          */
222         public static function serverlist()
223         {
224                 $r = q(
225                         "SELECT `url`, `site_name` AS `displayName`, `network`, `platform`, `version` FROM `gserver`
226                         WHERE `network` IN ('%s', '%s', '%s') AND `last_contact` > `last_failure`
227                         ORDER BY `last_contact`
228                         LIMIT 1000",
229                         DBA::escape(Protocol::DFRN),
230                         DBA::escape(Protocol::DIASPORA),
231                         DBA::escape(Protocol::OSTATUS)
232                 );
233
234                 if (!DBA::isResult($r)) {
235                         return false;
236                 }
237
238                 return $r;
239         }
240
241         /**
242          * Fetch server list from remote servers and adds them when they are new.
243          *
244          * @param string $poco URL to the POCO endpoint
245          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
246          */
247         private static function fetchServerlist($poco)
248         {
249                 $curlResult = Network::curl($poco . "/@server");
250
251                 if (!$curlResult->isSuccess()) {
252                         return;
253                 }
254
255                 $serverlist = json_decode($curlResult->getBody(), true);
256
257                 if (!is_array($serverlist)) {
258                         return;
259                 }
260
261                 foreach ($serverlist as $server) {
262                         $server_url = str_replace("/index.php", "", $server['url']);
263
264                         $r = q("SELECT `nurl` FROM `gserver` WHERE `nurl` = '%s'", DBA::escape(Strings::normaliseLink($server_url)));
265
266                         if (!DBA::isResult($r)) {
267                                 Logger::log("Call server check for server ".$server_url, Logger::DEBUG);
268                                 Worker::add(PRIORITY_LOW, 'UpdateGServer', $server_url);
269                         }
270                 }
271         }
272
273         public static function discoverSingleServer($id)
274         {
275                 $server = DBA::selectFirst('gserver', ['poco', 'nurl', 'url', 'network'], ['id' => $id]);
276
277                 if (!DBA::isResult($server)) {
278                         return false;
279                 }
280
281                 // Discover new servers out there (Works from Friendica version 3.5.2)
282                 self::fetchServerlist($server["poco"]);
283
284                 // Fetch all users from the other server
285                 $url = $server["poco"] . "/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
286
287                 Logger::info("Fetch all users from the server " . $server["url"]);
288
289                 $curlResult = Network::curl($url);
290
291                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
292                         $data = json_decode($curlResult->getBody(), true);
293
294                         if (!empty($data)) {
295                                 self::discoverServer($data, 2);
296                         }
297
298                         if (Config::get('system', 'poco_discovery') >= self::USERS_GCONTACTS) {
299                                 $timeframe = Config::get('system', 'poco_discovery_since');
300
301                                 if ($timeframe == 0) {
302                                         $timeframe = 30;
303                                 }
304
305                                 $updatedSince = date(DateTimeFormat::MYSQL, time() - $timeframe * 86400);
306
307                                 // Fetch all global contacts from the other server (Not working with Redmatrix and Friendica versions before 3.3)
308                                 $url = $server["poco"]."/@global?updatedSince=".$updatedSince."&fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation";
309
310                                 $success = false;
311
312                                 $curlResult = Network::curl($url);
313
314                                 if ($curlResult->isSuccess() && !empty($curlResult->getBody())) {
315                                         Logger::info("Fetch all global contacts from the server " . $server["nurl"]);
316                                         $data = json_decode($curlResult->getBody(), true);
317
318                                         if (!empty($data)) {
319                                                 $success = self::discoverServer($data);
320                                         }
321                                 }
322
323                                 if (!$success && !empty($data) && Config::get('system', 'poco_discovery') >= self::USERS_GCONTACTS_FALLBACK) {
324                                         Logger::info("Fetch contacts from users of the server " . $server["nurl"]);
325                                         self::discoverServerUsers($data, $server);
326                                 }
327                         }
328
329                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
330                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
331
332                         return true;
333                 } else {
334                         // If the server hadn't replied correctly, then force a sanity check
335                         GServer::check($server["url"], $server["network"], true);
336
337                         // If we couldn't reach the server, we will try it some time later
338                         $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
339                         DBA::update('gserver', $fields, ['nurl' => $server["nurl"]]);
340
341                         return false;
342                 }
343         }
344
345         private static function discoverServerUsers(array $data, array $server)
346         {
347                 if (!isset($data['entry'])) {
348                         return;
349                 }
350
351                 foreach ($data['entry'] as $entry) {
352                         $username = '';
353
354                         if (isset($entry['urls'])) {
355                                 foreach ($entry['urls'] as $url) {
356                                         if ($url['type'] == 'profile') {
357                                                 $profile_url = $url['value'];
358                                                 $path_array = explode('/', parse_url($profile_url, PHP_URL_PATH));
359                                                 $username = end($path_array);
360                                         }
361                                 }
362                         }
363
364                         if ($username != '') {
365                                 Logger::log('Fetch contacts for the user ' . $username . ' from the server ' . $server['nurl'], Logger::DEBUG);
366
367                                 // Fetch all contacts from a given user from the other server
368                                 $url = $server['poco'] . '/' . $username . '/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation';
369
370                                 $curlResult = Network::curl($url);
371
372                                 if ($curlResult->isSuccess()) {
373                                         $data = json_decode($curlResult->getBody(), true);
374
375                                         if (!empty($data)) {
376                                                 self::discoverServer($data, 3);
377                                         }
378                                 }
379                         }
380                 }
381         }
382
383         private static function discoverServer(array $data, $default_generation = 0)
384         {
385                 if (empty($data['entry'])) {
386                         return false;
387                 }
388
389                 $success = false;
390
391                 foreach ($data['entry'] as $entry) {
392                         $profile_url = '';
393                         $profile_photo = '';
394                         $connect_url = '';
395                         $name = '';
396                         $network = '';
397                         $updated = DBA::NULL_DATETIME;
398                         $location = '';
399                         $about = '';
400                         $keywords = '';
401                         $gender = '';
402                         $contact_type = -1;
403                         $generation = $default_generation;
404
405                         if (!empty($entry['displayName'])) {
406                                 $name = $entry['displayName'];
407                         }
408
409                         if (isset($entry['urls'])) {
410                                 foreach ($entry['urls'] as $url) {
411                                         if ($url['type'] == 'profile') {
412                                                 $profile_url = $url['value'];
413                                                 continue;
414                                         }
415                                         if ($url['type'] == 'webfinger') {
416                                                 $connect_url = str_replace('acct:' , '', $url['value']);
417                                                 continue;
418                                         }
419                                 }
420                         }
421
422                         if (isset($entry['photos'])) {
423                                 foreach ($entry['photos'] as $photo) {
424                                         if ($photo['type'] == 'profile') {
425                                                 $profile_photo = $photo['value'];
426                                                 continue;
427                                         }
428                                 }
429                         }
430
431                         if (isset($entry['updated'])) {
432                                 $updated = date(DateTimeFormat::MYSQL, strtotime($entry['updated']));
433                         }
434
435                         if (isset($entry['network'])) {
436                                 $network = $entry['network'];
437                         }
438
439                         if (isset($entry['currentLocation'])) {
440                                 $location = $entry['currentLocation'];
441                         }
442
443                         if (isset($entry['aboutMe'])) {
444                                 $about = HTML::toBBCode($entry['aboutMe']);
445                         }
446
447                         if (isset($entry['gender'])) {
448                                 $gender = $entry['gender'];
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                                                 "gender" => $gender,
477                                                 "keywords" => $keywords,
478                                                 "connect" => $connect_url,
479                                                 "updated" => $updated,
480                                                 "contact-type" => $contact_type,
481                                                 "generation" => $generation];
482
483                                 try {
484                                         $gcontact = GContact::sanitize($gcontact);
485                                         GContact::update($gcontact);
486                                 } catch (Exception $e) {
487                                         Logger::log($e->getMessage(), Logger::DEBUG);
488                                 }
489
490                                 Logger::log("Done for profile ".$profile_url, Logger::DEBUG);
491                         }
492                 }
493                 return $success;
494         }
495 }