]> git.mxchange.org Git - friendica.git/blob - mod/dfrn_confirm.php
Merge pull request #7343 from MrPetovan/bug/notices
[friendica.git] / mod / dfrn_confirm.php
1 <?php
2 /**
3  * @file mod/dfrn_confirm.php
4  * @brief Module: dfrn_confirm
5  * Purpose: Friendship acceptance for DFRN contacts
6  *
7  * There are two possible entry points and three scenarios.
8  *
9  *   1. A form was submitted by our user approving a friendship that originated elsewhere.
10  *      This may also be called from dfrn_request to automatically approve a friendship.
11  *
12  *   2. We may be the target or other side of the conversation to scenario 1, and will
13  *      interact with that process on our own user's behalf.
14  *
15  *  @see PDF with dfrn specs: https://github.com/friendica/friendica/blob/master/spec/dfrn2.pdf
16  *    You also find a graphic which describes the confirmation process at
17  *    https://github.com/friendica/friendica/blob/master/spec/dfrn2_contact_confirmation.png
18  */
19
20 use Friendica\App;
21 use Friendica\Core\Config;
22 use Friendica\Core\L10n;
23 use Friendica\Core\Logger;
24 use Friendica\Core\Protocol;
25 use Friendica\Core\System;
26 use Friendica\Database\DBA;
27 use Friendica\Model\Contact;
28 use Friendica\Model\Group;
29 use Friendica\Model\User;
30 use Friendica\Network\Probe;
31 use Friendica\Util\Crypto;
32 use Friendica\Util\DateTimeFormat;
33 use Friendica\Util\Network;
34 use Friendica\Util\Strings;
35 use Friendica\Util\XML;
36
37 function dfrn_confirm_post(App $a, $handsfree = null)
38 {
39         $node = null;
40         if (is_array($handsfree)) {
41                 /*
42                  * We were called directly from dfrn_request due to automatic friend acceptance.
43                  * Any $_POST parameters we may require are supplied in the $handsfree array.
44                  *
45                  */
46                 $node = $handsfree['node'];
47                 $a->interactive = false; // notice() becomes a no-op since nobody is there to see it
48         } elseif ($a->argc > 1) {
49                 $node = $a->argv[1];
50         }
51
52         /*
53          * Main entry point. Scenario 1. Our user received a friend request notification (perhaps
54          * from another site) and clicked 'Approve'.
55          * $POST['source_url'] is not set. If it is, it indicates Scenario 2.
56          *
57          * We may also have been called directly from dfrn_request ($handsfree != null) due to
58          * this being a page type which supports automatic friend acceptance. That is also Scenario 1
59          * since we are operating on behalf of our registered user to approve a friendship.
60          */
61         if (empty($_POST['source_url'])) {
62                 $uid = defaults($handsfree, 'uid', local_user());
63                 if (!$uid) {
64                         notice(L10n::t('Permission denied.') . EOL);
65                         return;
66                 }
67
68                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
69                 if (!DBA::isResult($user)) {
70                         notice(L10n::t('Profile not found.') . EOL);
71                         return;
72                 }
73
74                 // These data elements may come from either the friend request notification form or $handsfree array.
75                 if (is_array($handsfree)) {
76                         Logger::log('Confirm in handsfree mode');
77                         $dfrn_id  = $handsfree['dfrn_id'];
78                         $intro_id = $handsfree['intro_id'];
79                         $duplex   = $handsfree['duplex'];
80                         $cid      = 0;
81                         $hidden   = intval(defaults($handsfree, 'hidden'  , 0));
82                 } else {
83                         $dfrn_id  = Strings::escapeTags(trim(defaults($_POST, 'dfrn_id'   , '')));
84                         $intro_id =      intval(defaults($_POST, 'intro_id'  , 0));
85                         $duplex   =      intval(defaults($_POST, 'duplex'    , 0));
86                         $cid      =      intval(defaults($_POST, 'contact_id', 0));
87                         $hidden   =      intval(defaults($_POST, 'hidden'    , 0));
88                 }
89
90                 /*
91                  * Ensure that dfrn_id has precedence when we go to find the contact record.
92                  * We only want to search based on contact id if there is no dfrn_id,
93                  * e.g. for OStatus network followers.
94                  */
95                 if (strlen($dfrn_id)) {
96                         $cid = 0;
97                 }
98
99                 Logger::log('Confirming request for dfrn_id (issued) ' . $dfrn_id);
100                 if ($cid) {
101                         Logger::log('Confirming follower with contact_id: ' . $cid);
102                 }
103
104                 /*
105                  * The other person will have been issued an ID when they first requested friendship.
106                  * Locate their record. At this time, their record will have both pending and blocked set to 1.
107                  * There won't be any dfrn_id if this is a network follower, so use the contact_id instead.
108                  */
109                 $r = q("SELECT *
110                         FROM `contact`
111                         WHERE (
112                                 (`issued-id` != '' AND `issued-id` = '%s')
113                                 OR
114                                 (`id` = %d AND `id` != 0)
115                         )
116                         AND `uid` = %d
117                         AND `duplex` = 0
118                         LIMIT 1",
119                         DBA::escape($dfrn_id),
120                         intval($cid),
121                         intval($uid)
122                 );
123                 if (!DBA::isResult($r)) {
124                         Logger::log('Contact not found in DB.');
125                         notice(L10n::t('Contact not found.') . EOL);
126                         notice(L10n::t('This may occasionally happen if contact was requested by both persons and it has already been approved.') . EOL);
127                         return;
128                 }
129
130                 $contact = $r[0];
131
132                 $contact_id   = $contact['id'];
133                 $relation     = $contact['rel'];
134                 $site_pubkey  = $contact['site-pubkey'];
135                 $dfrn_confirm = $contact['confirm'];
136                 $aes_allow    = $contact['aes_allow'];
137                 $protocol     = $contact['network'];
138
139                 /*
140                  * Generate a key pair for all further communications with this person.
141                  * We have a keypair for every contact, and a site key for unknown people.
142                  * This provides a means to carry on relationships with other people if
143                  * any single key is compromised. It is a robust key. We're much more
144                  * worried about key leakage than anybody cracking it.
145                  */
146                 $res = Crypto::newKeypair(4096);
147
148                 $private_key = $res['prvkey'];
149                 $public_key  = $res['pubkey'];
150
151                 // Save the private key. Send them the public key.
152                 $fields = ['prvkey' => $private_key, 'protocol' => Protocol::DFRN];
153                 DBA::update('contact', $fields, ['id' => $contact_id]);
154
155                 $params = [];
156
157                 /*
158                  * Per the DFRN protocol, we will verify both ends by encrypting the dfrn_id with our
159                  * site private key (person on the other end can decrypt it with our site public key).
160                  * Then encrypt our profile URL with the other person's site public key. They can decrypt
161                  * it with their site private key. If the decryption on the other end fails for either
162                  * item, it indicates tampering or key failure on at least one site and we will not be
163                  * able to provide a secure communication pathway.
164                  *
165                  * If other site is willing to accept full encryption, (aes_allow is 1 AND we have php5.3
166                  * or later) then we encrypt the personal public key we send them using AES-256-CBC and a
167                  * random key which is encrypted with their site public key.
168                  */
169
170                 $src_aes_key = openssl_random_pseudo_bytes(64);
171
172                 $result = '';
173                 openssl_private_encrypt($dfrn_id, $result, $user['prvkey']);
174
175                 $params['dfrn_id'] = bin2hex($result);
176                 $params['public_key'] = $public_key;
177
178                 $my_url = System::baseUrl() . '/profile/' . $user['nickname'];
179
180                 openssl_public_encrypt($my_url, $params['source_url'], $site_pubkey);
181                 $params['source_url'] = bin2hex($params['source_url']);
182
183                 if ($aes_allow && function_exists('openssl_encrypt')) {
184                         openssl_public_encrypt($src_aes_key, $params['aes_key'], $site_pubkey);
185                         $params['aes_key'] = bin2hex($params['aes_key']);
186                         $params['public_key'] = bin2hex(openssl_encrypt($public_key, 'AES-256-CBC', $src_aes_key));
187                 }
188
189                 $params['dfrn_version'] = DFRN_PROTOCOL_VERSION;
190                 if ($duplex == 1) {
191                         $params['duplex'] = 1;
192                 }
193
194                 if ($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY) {
195                         $params['page'] = 1;
196                 }
197
198                 if ($user['page-flags'] == User::PAGE_FLAGS_PRVGROUP) {
199                         $params['page'] = 2;
200                 }
201
202                 Logger::log('Confirm: posting data to ' . $dfrn_confirm . ': ' . print_r($params, true), Logger::DATA);
203
204                 /*
205                  *
206                  * POST all this stuff to the other site.
207                  * Temporarily raise the network timeout to 120 seconds because the default 60
208                  * doesn't always give the other side quite enough time to decrypt everything.
209                  *
210                  */
211
212                 $res = Network::post($dfrn_confirm, $params, [], 120)->getBody();
213
214                 Logger::log(' Confirm: received data: ' . $res, Logger::DATA);
215
216                 // Now figure out what they responded. Try to be robust if the remote site is
217                 // having difficulty and throwing up errors of some kind.
218
219                 $leading_junk = substr($res, 0, strpos($res, '<?xml'));
220
221                 $res = substr($res, strpos($res, '<?xml'));
222                 if (!strlen($res)) {
223                         // No XML at all, this exchange is messed up really bad.
224                         // We shouldn't proceed, because the xml parser might choke,
225                         // and $status is going to be zero, which indicates success.
226                         // We can hardly call this a success.
227                         notice(L10n::t('Response from remote site was not understood.') . EOL);
228                         return;
229                 }
230
231                 if (strlen($leading_junk) && Config::get('system', 'debugging')) {
232                         // This might be more common. Mixed error text and some XML.
233                         // If we're configured for debugging, show the text. Proceed in either case.
234                         notice(L10n::t('Unexpected response from remote site: ') . EOL . $leading_junk . EOL);
235                 }
236
237                 if (stristr($res, "<status") === false) {
238                         // wrong xml! stop here!
239                         Logger::log('Unexpected response posting to ' . $dfrn_confirm);
240                         notice(L10n::t('Unexpected response from remote site: ') . EOL . htmlspecialchars($res) . EOL);
241                         return;
242                 }
243
244                 $xml = XML::parseString($res);
245                 $status = (int) $xml->status;
246                 $message = XML::unescape($xml->message);   // human readable text of what may have gone wrong.
247                 switch ($status) {
248                         case 0:
249                                 info(L10n::t("Confirmation completed successfully.") . EOL);
250                                 break;
251                         case 1:
252                                 // birthday paradox - generate new dfrn-id and fall through.
253                                 $new_dfrn_id = Strings::getRandomHex();
254                                 q("UPDATE contact SET `issued-id` = '%s' WHERE `id` = %d AND `uid` = %d",
255                                         DBA::escape($new_dfrn_id),
256                                         intval($contact_id),
257                                         intval($uid)
258                                 );
259
260                         case 2:
261                                 notice(L10n::t("Temporary failure. Please wait and try again.") . EOL);
262                                 break;
263                         case 3:
264                                 notice(L10n::t("Introduction failed or was revoked.") . EOL);
265                                 break;
266                 }
267
268                 if (strlen($message)) {
269                         notice(L10n::t('Remote site reported: ') . $message . EOL);
270                 }
271
272                 if (($status == 0) && $intro_id) {
273                         $intro = DBA::selectFirst('intro', ['note'], ['id' => $intro_id]);
274                         if (DBA::isResult($intro)) {
275                                 DBA::update('contact', ['reason' => $intro['note']], ['id' => $contact_id]);
276                         }
277
278                         // Success. Delete the notification.
279                         DBA::delete('intro', ['id' => $intro_id]);
280                 }
281
282                 if ($status != 0) {
283                         return;
284                 }
285
286                 /*
287                  * We have now established a relationship with the other site.
288                  * Let's make our own personal copy of their profile photo so we don't have
289                  * to always load it from their site.
290                  *
291                  * We will also update the contact record with the nature and scope of the relationship.
292                  */
293                 Contact::updateAvatar($contact['photo'], $uid, $contact_id);
294
295                 Logger::log('dfrn_confirm: confirm - imported photos');
296
297                 $new_relation = Contact::FOLLOWER;
298
299                 if (($relation == Contact::SHARING) || ($duplex)) {
300                         $new_relation = Contact::FRIEND;
301                 }
302
303                 if (($relation == Contact::SHARING) && ($duplex)) {
304                         $duplex = 0;
305                 }
306
307                 $r = q("UPDATE `contact` SET `rel` = %d,
308                         `name-date` = '%s',
309                         `uri-date` = '%s',
310                         `blocked` = 0,
311                         `pending` = 0,
312                         `duplex` = %d,
313                         `hidden` = %d,
314                         `network` = '%s' WHERE `id` = %d
315                 ",
316                         intval($new_relation),
317                         DBA::escape(DateTimeFormat::utcNow()),
318                         DBA::escape(DateTimeFormat::utcNow()),
319                         intval($duplex),
320                         intval($hidden),
321                         DBA::escape(Protocol::DFRN),
322                         intval($contact_id)
323                 );
324
325                 // reload contact info
326                 $contact = DBA::selectFirst('contact', [], ['id' => $contact_id]);
327
328                 Group::addMember(User::getDefaultGroup($uid, $contact["network"]), $contact['id']);
329
330                 // Let's send our user to the contact editor in case they want to
331                 // do anything special with this new friend.
332                 if ($handsfree === null) {
333                         $a->internalRedirect('contact/' . intval($contact_id));
334                 } else {
335                         return;
336                 }
337                 //NOTREACHED
338         }
339
340         /*
341          * End of Scenario 1. [Local confirmation of remote friend request].
342          *
343          * Begin Scenario 2. This is the remote response to the above scenario.
344          * This will take place on the site that originally initiated the friend request.
345          * In the section above where the confirming party makes a POST and
346          * retrieves xml status information, they are communicating with the following code.
347          */
348         if (!empty($_POST['source_url'])) {
349                 // We are processing an external confirmation to an introduction created by our user.
350                 $public_key =         defaults($_POST, 'public_key', '');
351                 $dfrn_id    = hex2bin(defaults($_POST, 'dfrn_id'   , ''));
352                 $source_url = hex2bin(defaults($_POST, 'source_url', ''));
353                 $aes_key    =         defaults($_POST, 'aes_key'   , '');
354                 $duplex     =  intval(defaults($_POST, 'duplex'    , 0));
355                 $page       =  intval(defaults($_POST, 'page'      , 0));
356
357                 $forum = (($page == 1) ? 1 : 0);
358                 $prv   = (($page == 2) ? 1 : 0);
359
360                 Logger::log('dfrn_confirm: requestee contacted: ' . $node);
361
362                 Logger::log('dfrn_confirm: request: POST=' . print_r($_POST, true), Logger::DATA);
363
364                 // If $aes_key is set, both of these items require unpacking from the hex transport encoding.
365
366                 if (!empty($aes_key)) {
367                         $aes_key = hex2bin($aes_key);
368                         $public_key = hex2bin($public_key);
369                 }
370
371                 // Find our user's account
372                 $user = DBA::selectFirst('user', [], ['nickname' => $node]);
373                 if (!DBA::isResult($user)) {
374                         $message = L10n::t('No user record found for \'%s\' ', $node);
375                         System::xmlExit(3, $message); // failure
376                         // NOTREACHED
377                 }
378
379                 $my_prvkey = $user['prvkey'];
380                 $local_uid = $user['uid'];
381
382
383                 if (!strstr($my_prvkey, 'PRIVATE KEY')) {
384                         $message = L10n::t('Our site encryption key is apparently messed up.');
385                         System::xmlExit(3, $message);
386                 }
387
388                 // verify everything
389
390                 $decrypted_source_url = "";
391                 openssl_private_decrypt($source_url, $decrypted_source_url, $my_prvkey);
392
393
394                 if (!strlen($decrypted_source_url)) {
395                         $message = L10n::t('Empty site URL was provided or URL could not be decrypted by us.');
396                         System::xmlExit(3, $message);
397                         // NOTREACHED
398                 }
399
400                 $contact = DBA::selectFirst('contact', [], ['url' => $decrypted_source_url, 'uid' => $local_uid]);
401                 if (!DBA::isResult($contact)) {
402                         if (strstr($decrypted_source_url, 'http:')) {
403                                 $newurl = str_replace('http:', 'https:', $decrypted_source_url);
404                         } else {
405                                 $newurl = str_replace('https:', 'http:', $decrypted_source_url);
406                         }
407
408                         $contact = DBA::selectFirst('contact', [], ['url' => $newurl, 'uid' => $local_uid]);
409                         if (!DBA::isResult($contact)) {
410                                 // this is either a bogus confirmation (?) or we deleted the original introduction.
411                                 $message = L10n::t('Contact record was not found for you on our site.');
412                                 System::xmlExit(3, $message);
413                                 return; // NOTREACHED
414                         }
415                 }
416
417                 $relation = $contact['rel'];
418
419                 // Decrypt all this stuff we just received
420
421                 $foreign_pubkey = $contact['site-pubkey'];
422                 $dfrn_record = $contact['id'];
423
424                 if (!$foreign_pubkey) {
425                         $message = L10n::t('Site public key not available in contact record for URL %s.', $decrypted_source_url);
426                         System::xmlExit(3, $message);
427                 }
428
429                 $decrypted_dfrn_id = "";
430                 openssl_public_decrypt($dfrn_id, $decrypted_dfrn_id, $foreign_pubkey);
431
432                 if (strlen($aes_key)) {
433                         $decrypted_aes_key = "";
434                         openssl_private_decrypt($aes_key, $decrypted_aes_key, $my_prvkey);
435                         $dfrn_pubkey = openssl_decrypt($public_key, 'AES-256-CBC', $decrypted_aes_key);
436                 } else {
437                         $dfrn_pubkey = $public_key;
438                 }
439
440                 if (DBA::exists('contact', ['dfrn-id' => $decrypted_dfrn_id])) {
441                         $message = L10n::t('The ID provided by your system is a duplicate on our system. It should work if you try again.');
442                         System::xmlExit(1, $message); // Birthday paradox - duplicate dfrn-id
443                         // NOTREACHED
444                 }
445
446                 $r = q("UPDATE `contact` SET `dfrn-id` = '%s', `pubkey` = '%s' WHERE `id` = %d",
447                         DBA::escape($decrypted_dfrn_id),
448                         DBA::escape($dfrn_pubkey),
449                         intval($dfrn_record)
450                 );
451                 if (!DBA::isResult($r)) {
452                         $message = L10n::t('Unable to set your contact credentials on our system.');
453                         System::xmlExit(3, $message);
454                 }
455
456                 // It's possible that the other person also requested friendship.
457                 // If it is a duplex relationship, ditch the issued-id if one exists.
458
459                 if ($duplex) {
460                         q("UPDATE `contact` SET `issued-id` = '' WHERE `id` = %d",
461                                 intval($dfrn_record)
462                         );
463                 }
464
465                 // We're good but now we have to scrape the profile photo and send notifications.
466                 $contact = DBA::selectFirst('contact', ['photo'], ['id' => $dfrn_record]);
467                 if (DBA::isResult($contact)) {
468                         $photo = $contact['photo'];
469                 } else {
470                         $photo = System::baseUrl() . '/images/person-300.jpg';
471                 }
472
473                 Contact::updateAvatar($photo, $local_uid, $dfrn_record);
474
475                 Logger::log('dfrn_confirm: request - photos imported');
476
477                 $new_relation = Contact::SHARING;
478
479                 if (($relation == Contact::FOLLOWER) || ($duplex)) {
480                         $new_relation = Contact::FRIEND;
481                 }
482
483                 if (($relation == Contact::FOLLOWER) && ($duplex)) {
484                         $duplex = 0;
485                 }
486
487                 $r = q("UPDATE `contact` SET
488                         `rel` = %d,
489                         `name-date` = '%s',
490                         `uri-date` = '%s',
491                         `blocked` = 0,
492                         `pending` = 0,
493                         `duplex` = %d,
494                         `forum` = %d,
495                         `prv` = %d,
496                         `network` = '%s' WHERE `id` = %d
497                 ",
498                         intval($new_relation),
499                         DBA::escape(DateTimeFormat::utcNow()),
500                         DBA::escape(DateTimeFormat::utcNow()),
501                         intval($duplex),
502                         intval($forum),
503                         intval($prv),
504                         DBA::escape(Protocol::DFRN),
505                         intval($dfrn_record)
506                 );
507                 if (!DBA::isResult($r)) {       // indicates schema is messed up or total db failure
508                         $message = L10n::t('Unable to update your contact profile details on our system');
509                         System::xmlExit(3, $message);
510                 }
511
512                 // Otherwise everything seems to have worked and we are almost done. Yay!
513                 // Send an email notification
514
515                 Logger::log('dfrn_confirm: request: info updated');
516
517                 $combined = null;
518                 $r = q("SELECT `contact`.*, `user`.*
519                         FROM `contact`
520                         LEFT JOIN `user` ON `contact`.`uid` = `user`.`uid`
521                         WHERE `contact`.`id` = %d
522                         LIMIT 1",
523                         intval($dfrn_record)
524                 );
525                 if (DBA::isResult($r)) {
526                         $combined = $r[0];
527
528                         if ($combined['notify-flags'] & NOTIFY_CONFIRM) {
529                                 $mutual = ($new_relation == Contact::FRIEND);
530                                 notification([
531                                         'type'         => NOTIFY_CONFIRM,
532                                         'notify_flags' => $combined['notify-flags'],
533                                         'language'     => $combined['language'],
534                                         'to_name'      => $combined['username'],
535                                         'to_email'     => $combined['email'],
536                                         'uid'          => $combined['uid'],
537                                         'link'         => System::baseUrl() . '/contact/' . $dfrn_record,
538                                         'source_name'  => ((strlen(stripslashes($combined['name']))) ? stripslashes($combined['name']) : L10n::t('[Name Withheld]')),
539                                         'source_link'  => $combined['url'],
540                                         'source_photo' => $combined['photo'],
541                                         'verb'         => ($mutual?ACTIVITY_FRIEND:ACTIVITY_FOLLOW),
542                                         'otype'        => 'intro'
543                                 ]);
544                         }
545                 }
546
547                 System::xmlExit(0); // Success
548                 return; // NOTREACHED
549                 ////////////////////// End of this scenario ///////////////////////////////////////////////
550         }
551
552         // somebody arrived here by mistake or they are fishing. Send them to the homepage.
553         $a->internalRedirect();
554         // NOTREACHED
555 }