]> git.mxchange.org Git - friendica.git/blob - mod/dfrn_notify.php
AP Improvements for forums
[friendica.git] / mod / dfrn_notify.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  * The dfrn notify endpoint
21  *
22  * @see PDF with dfrn specs: https://github.com/friendica/friendica/blob/stable/spec/dfrn2.pdf
23  */
24
25 use Friendica\App;
26 use Friendica\Core\Logger;
27 use Friendica\Core\System;
28 use Friendica\Database\DBA;
29 use Friendica\DI;
30 use Friendica\Model\Contact;
31 use Friendica\Model\User;
32 use Friendica\Protocol\DFRN;
33 use Friendica\Protocol\Diaspora;
34 use Friendica\Util\Network;
35 use Friendica\Util\Strings;
36
37 function dfrn_notify_post(App $a) {
38         Logger::log(__function__, Logger::TRACE);
39
40         $postdata = Network::postdata();
41
42         if (empty($_POST) || !empty($postdata)) {
43                 $data = json_decode($postdata);
44                 if (is_object($data)) {
45                         $nick = $a->argv[1] ?? '';
46
47                         $user = DBA::selectFirst('user', [], ['nickname' => $nick, 'account_expired' => false, 'account_removed' => false]);
48                         if (!DBA::isResult($user)) {
49                                 throw new \Friendica\Network\HTTPException\InternalServerErrorException();
50                         }
51                         dfrn_dispatch_private($user, $postdata);
52                 } elseif (!dfrn_dispatch_public($postdata)) {
53                         require_once 'mod/salmon.php';
54                         salmon_post($a, $postdata);
55                 }
56         }
57
58         $dfrn_id      = (!empty($_POST['dfrn_id'])      ? Strings::escapeTags(trim($_POST['dfrn_id']))   : '');
59         $dfrn_version = (!empty($_POST['dfrn_version']) ? (float) $_POST['dfrn_version']    : 2.0);
60         $challenge    = (!empty($_POST['challenge'])    ? Strings::escapeTags(trim($_POST['challenge'])) : '');
61         $data         = $_POST['data'] ?? '';
62         $key          = $_POST['key'] ?? '';
63         $rino_remote  = (!empty($_POST['rino'])         ? intval($_POST['rino'])            :  0);
64         $dissolve     = (!empty($_POST['dissolve'])     ? intval($_POST['dissolve'])        :  0);
65         $perm         = (!empty($_POST['perm'])         ? Strings::escapeTags(trim($_POST['perm']))      : 'r');
66         $ssl_policy   = (!empty($_POST['ssl_policy'])   ? Strings::escapeTags(trim($_POST['ssl_policy'])): 'none');
67         $page         = (!empty($_POST['page'])         ? intval($_POST['page'])            :  0);
68
69         $forum = (($page == 1) ? 1 : 0);
70         $prv   = (($page == 2) ? 1 : 0);
71
72         $writable = (-1);
73         if ($dfrn_version >= 2.21) {
74                 $writable = (($perm === 'rw') ? 1 : 0);
75         }
76
77         $direction = (-1);
78         if (strpos($dfrn_id, ':') == 1) {
79                 $direction = intval(substr($dfrn_id, 0, 1));
80                 $dfrn_id = substr($dfrn_id, 2);
81         }
82
83         if (!DBA::exists('challenge', ['dfrn-id' => $dfrn_id, 'challenge' => $challenge])) {
84                 Logger::log('could not match challenge to dfrn_id ' . $dfrn_id . ' challenge=' . $challenge);
85                 System::xmlExit(3, 'Could not match challenge');
86         }
87
88         DBA::delete('challenge', ['dfrn-id' => $dfrn_id, 'challenge' => $challenge]);
89
90         $user = DBA::selectFirst('user', ['uid'], ['nickname' => $a->argv[1]]);
91         if (!DBA::isResult($user)) {
92                 Logger::log('User not found for nickname ' . $a->argv[1]);
93                 System::xmlExit(3, 'User not found');
94         }
95
96         // find the local user who owns this relationship.
97         $condition = [];
98         switch ($direction) {
99                 case (-1):
100                         $condition = ["(`issued-id` = ? OR `dfrn-id` = ?) AND `uid` = ?", $dfrn_id, $dfrn_id, $user['uid']];
101                         break;
102                 case 0:
103                         $condition = ['issued-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
104                         break;
105                 case 1:
106                         $condition = ['dfrn-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
107                         break;
108                 default:
109                         System::xmlExit(3, 'Invalid direction');
110                         break; // NOTREACHED
111         }
112
113         $contact = DBA::selectFirst('contact', ['id'], $condition);
114         if (!DBA::isResult($contact)) {
115                 Logger::log('contact not found for dfrn_id ' . $dfrn_id);
116                 System::xmlExit(3, 'Contact not found');
117         }
118
119         // $importer in this case contains the contact record for the remote contact joined with the user record of our user.
120         $importer = DFRN::getImporter($contact['id'], $user['uid']);
121
122         if ((($writable != (-1)) && ($writable != $importer['writable'])) || ($importer['forum'] != $forum) || ($importer['prv'] != $prv)) {
123                 $fields = ['writable' => ($writable == (-1)) ? $importer['writable'] : $writable,
124                         'forum' => $forum, 'prv' => $prv];
125                 DBA::update('contact', $fields, ['id' => $importer['id']]);
126
127                 if ($writable != (-1)) {
128                         $importer['writable'] = $writable;
129                 }
130                 $importer['forum'] = $page;
131         }
132
133
134         // if contact's ssl policy changed, update our links
135
136         $importer = Contact::updateSslPolicy($importer, $ssl_policy);
137
138         Logger::log('data: ' . $data, Logger::DATA);
139
140         if ($dissolve == 1) {
141                 // Relationship is dissolved permanently
142                 Contact::remove($importer['id']);
143                 Logger::log('relationship dissolved : ' . $importer['name'] . ' dissolved ' . $importer['username']);
144                 System::xmlExit(0, 'relationship dissolved');
145         }
146
147         $rino = DI::config()->get('system', 'rino_encrypt');
148         $rino = intval($rino);
149
150         if (strlen($key)) {
151
152                 // if local rino is lower than remote rino, abort: should not happen!
153                 // but only for $remote_rino > 1, because old code did't send rino version
154                 if ($rino_remote > 1 && $rino < $rino_remote) {
155                         Logger::log("rino version '$rino_remote' is lower than supported '$rino'");
156                         System::xmlExit(0, "rino version '$rino_remote' is lower than supported '$rino'");
157                 }
158
159                 $rawkey = hex2bin(trim($key));
160                 Logger::log('rino: md5 raw key: ' . md5($rawkey), Logger::DATA);
161
162                 $final_key = '';
163
164                 if ($dfrn_version >= 2.1) {
165                         if (($importer['duplex'] && strlen($importer['cprvkey'])) || !strlen($importer['cpubkey'])) {
166                                 openssl_private_decrypt($rawkey, $final_key, $importer['cprvkey']);
167                         } else {
168                                 openssl_public_decrypt($rawkey, $final_key, $importer['cpubkey']);
169                         }
170                 } else {
171                         if (($importer['duplex'] && strlen($importer['cpubkey'])) || !strlen($importer['cprvkey'])) {
172                                 openssl_public_decrypt($rawkey, $final_key, $importer['cpubkey']);
173                         } else {
174                                 openssl_private_decrypt($rawkey, $final_key, $importer['cprvkey']);
175                         }
176                 }
177
178                 switch ($rino_remote) {
179                         case 0:
180                         case 1:
181                                 // we got a key. old code send only the key, without RINO version.
182                                 // we assume RINO 1 if key and no RINO version
183                                 $data = DFRN::aesDecrypt(hex2bin($data), $final_key);
184                                 break;
185                         default:
186                                 Logger::log("rino: invalid sent version '$rino_remote'");
187                                 System::xmlExit(0, "Invalid sent version '$rino_remote'");
188                 }
189
190                 Logger::log('rino: decrypted data: ' . $data, Logger::DATA);
191         }
192
193         Logger::log('Importing post from ' . $importer['addr'] . ' to ' . $importer['nickname'] . ' with the RINO ' . $rino_remote . ' encryption.', Logger::DEBUG);
194
195         $ret = DFRN::import($data, $importer);
196         System::xmlExit($ret, 'Processed');
197
198         // NOTREACHED
199 }
200
201 function dfrn_dispatch_public($postdata)
202 {
203         $msg = Diaspora::decodeRaw($postdata, '', true);
204         if (!$msg) {
205                 // We have to fail silently to be able to hand it over to the salmon parser
206                 return false;
207         }
208
209         // Fetch the corresponding public contact
210         $contact_id = Contact::getIdForURL($msg['author']);
211         if (empty($contact_id)) {
212                 Logger::log('Contact not found for address ' . $msg['author']);
213                 System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
214         }
215
216         $importer = DFRN::getImporter($contact_id);
217
218         // This should never fail
219         if (empty($importer)) {
220                 Logger::log('Contact not found for address ' . $msg['author']);
221                 System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
222         }
223
224         Logger::log('Importing post from ' . $msg['author'] . ' with the public envelope.', Logger::DEBUG);
225
226         // Now we should be able to import it
227         $ret = DFRN::import($msg['message'], $importer);
228         System::xmlExit($ret, 'Done');
229 }
230
231 function dfrn_dispatch_private($user, $postdata)
232 {
233         $msg = Diaspora::decodeRaw($postdata, $user['prvkey'] ?? '');
234         if (!$msg) {
235                 System::xmlExit(4, 'Unable to parse message');
236         }
237
238         // Check if the user has got this contact
239         $cid = Contact::getIdForURL($msg['author'], $user['uid']);
240         if (!$cid) {
241                 // Otherwise there should be a public contact
242                 $cid = Contact::getIdForURL($msg['author']);
243                 if (!$cid) {
244                         Logger::log('Contact not found for address ' . $msg['author']);
245                         System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
246                 }
247         }
248
249         $importer = DFRN::getImporter($cid, $user['uid']);
250
251         // This should never fail
252         if (empty($importer)) {
253                 Logger::log('Contact not found for address ' . $msg['author']);
254                 System::xmlExit(3, 'Contact ' . $msg['author'] . ' not found');
255         }
256
257         Logger::log('Importing post from ' . $msg['author'] . ' to ' . $user['nickname'] . ' with the private envelope.', Logger::DEBUG);
258
259         // Now we should be able to import it
260         $ret = DFRN::import($msg['message'], $importer);
261         System::xmlExit($ret, 'Done');
262 }
263
264 function dfrn_notify_content(App $a) {
265
266         if (!empty($_GET['dfrn_id'])) {
267
268                 /*
269                  * initial communication from external contact, $direction is their direction.
270                  * If this is a duplex communication, ours will be the opposite.
271                  */
272
273                 $dfrn_id = Strings::escapeTags(trim($_GET['dfrn_id']));
274                 $rino_remote = (!empty($_GET['rino']) ? intval($_GET['rino']) : 0);
275                 $type = "";
276                 $last_update = "";
277
278                 Logger::log('new notification dfrn_id=' . $dfrn_id);
279
280                 $direction = (-1);
281                 if (strpos($dfrn_id,':') == 1) {
282                         $direction = intval(substr($dfrn_id,0,1));
283                         $dfrn_id = substr($dfrn_id,2);
284                 }
285
286                 $hash = Strings::getRandomHex();
287
288                 $status = 0;
289
290                 DBA::delete('challenge', ["`expire` < ?", time()]);
291
292                 $fields = ['challenge' => $hash, 'dfrn-id' => $dfrn_id, 'expire' => time() + 90,
293                         'type' => $type, 'last_update' => $last_update];
294                 DBA::insert('challenge', $fields);
295
296                 Logger::log('challenge=' . $hash, Logger::DATA);
297
298                 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $a->argv[1]]);
299                 if (!DBA::isResult($user)) {
300                         Logger::log('User not found for nickname ' . $a->argv[1]);
301                         exit();
302                 }
303
304                 $condition = [];
305                 switch ($direction) {
306                         case (-1):
307                                 $condition = ["(`issued-id` = ? OR `dfrn-id` = ?) AND `uid` = ?", $dfrn_id, $dfrn_id, $user['uid']];
308                                 $my_id = $dfrn_id;
309                                 break;
310                         case 0:
311                                 $condition = ['issued-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
312                                 $my_id = '1:' . $dfrn_id;
313                                 break;
314                         case 1:
315                                 $condition = ['dfrn-id' => $dfrn_id, 'duplex' => true, 'uid' => $user['uid']];
316                                 $my_id = '0:' . $dfrn_id;
317                                 break;
318                         default:
319                                 $status = 1;
320                                 $my_id = '';
321                                 break;
322                 }
323
324                 $contact = DBA::selectFirst('contact', ['id'], $condition);
325                 if (!DBA::isResult($contact)) {
326                         Logger::log('contact not found for dfrn_id ' . $dfrn_id);
327                         System::xmlExit(3, 'Contact not found');
328                 }
329
330                 // $importer in this case contains the contact record for the remote contact joined with the user record of our user.
331                 $importer = DFRN::getImporter($contact['id'], $user['uid']);
332                 if (empty($importer)) {
333                         Logger::log('No importer data found for user ' . $a->argv[1] . ' and contact ' . $dfrn_id);
334                         exit();
335                 }
336
337                 Logger::log("Remote rino version: ".$rino_remote." for ".$importer["url"], Logger::DATA);
338
339                 $challenge    = '';
340                 $encrypted_id = '';
341                 $id_str       = $my_id . '.' . mt_rand(1000,9999);
342
343                 $prv_key = trim($importer['cprvkey']);
344                 $pub_key = trim($importer['cpubkey']);
345                 $dplx    = intval($importer['duplex']);
346
347                 if (($dplx && strlen($prv_key)) || (strlen($prv_key) && !strlen($pub_key))) {
348                         openssl_private_encrypt($hash, $challenge, $prv_key);
349                         openssl_private_encrypt($id_str, $encrypted_id, $prv_key);
350                 } elseif (strlen($pub_key)) {
351                         openssl_public_encrypt($hash, $challenge, $pub_key);
352                         openssl_public_encrypt($id_str, $encrypted_id, $pub_key);
353                 } else {
354                         /// @TODO these kind of else-blocks are making the code harder to understand
355                         $status = 1;
356                 }
357
358                 $challenge    = bin2hex($challenge);
359                 $encrypted_id = bin2hex($encrypted_id);
360
361
362                 $rino = DI::config()->get('system', 'rino_encrypt');
363                 $rino = intval($rino);
364
365                 Logger::log("Local rino version: ". $rino, Logger::DATA);
366
367                 // if requested rino is lower than enabled local rino, lower local rino version
368                 // if requested rino is higher than enabled local rino, reply with local rino
369                 if ($rino_remote < $rino) {
370                         $rino = $rino_remote;
371                 }
372
373                 if (($importer['rel'] && ($importer['rel'] != Contact::SHARING)) || ($importer['page-flags'] == User::PAGE_FLAGS_COMMUNITY)) {
374                         $perm = 'rw';
375                 } else {
376                         $perm = 'r';
377                 }
378
379                 header("Content-type: text/xml");
380
381                 echo '<?xml version="1.0" encoding="UTF-8"?>' . "\r\n"
382                         . '<dfrn_notify>' . "\r\n"
383                         . "\t" . '<status>' . $status . '</status>' . "\r\n"
384                         . "\t" . '<dfrn_version>' . DFRN_PROTOCOL_VERSION . '</dfrn_version>' . "\r\n"
385                         . "\t" . '<rino>' . $rino . '</rino>' . "\r\n"
386                         . "\t" . '<perm>' . $perm . '</perm>' . "\r\n"
387                         . "\t" . '<dfrn_id>' . $encrypted_id . '</dfrn_id>' . "\r\n"
388                         . "\t" . '<challenge>' . $challenge . '</challenge>' . "\r\n"
389                         . '</dfrn_notify>' . "\r\n";
390
391                 exit();
392         }
393 }