]> git.mxchange.org Git - friendica.git/blob - src/Module/Settings/UserExport.php
Merge pull request #12060 from MrPetovan/bug/9920-contact-export
[friendica.git] / src / Module / Settings / UserExport.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
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\Module\Settings;
23
24 use Friendica\App;
25 use Friendica\Core\Hook;
26 use Friendica\Core\L10n;
27 use Friendica\Core\Renderer;
28 use Friendica\Core\Session\Capability\IHandleUserSessions;
29 use Friendica\Core\System;
30 use Friendica\Database\DBA;
31 use Friendica\Database\Definition\DbaDefinition;
32 use Friendica\DI;
33 use Friendica\Model\Contact;
34 use Friendica\Model\Item;
35 use Friendica\Model\Post;
36 use Friendica\Module\BaseSettings;
37 use Friendica\Module\Response;
38 use Friendica\Network\HTTPException;
39 use Friendica\Network\HTTPException\ForbiddenException;
40 use Friendica\Network\HTTPException\InternalServerErrorException;
41 use Friendica\Network\HTTPException\ServiceUnavailableException;
42 use Friendica\Util\Profiler;
43 use Psr\Log\LoggerInterface;
44
45 /**
46  * Module to export user data
47  **/
48 class UserExport extends BaseSettings
49 {
50         /** @var IHandleUserSessions */
51         private $session;
52
53         /** @var DbaDefinition */
54         private $dbaDefinition;
55
56         public function __construct(DbaDefinition $dbaDefinition, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
57         {
58                 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
59
60                 $this->session       = $session;
61                 $this->dbaDefinition = $dbaDefinition;
62         }
63
64         /**
65          * Handle the request to export data.
66          * At the moment one can export three different data set
67          * 1. The profile data that can be used by uimport to resettle
68          *    to a different Friendica instance
69          * 2. The entire data-set, profile plus postings
70          * 3. A list of contacts as CSV file similar to the export of Mastodon
71          *
72          * If there is an action required through the URL / path, react
73          * accordingly and export the requested data.
74          *
75          * @param array $request
76          * @return string
77          * @throws ForbiddenException
78          * @throws InternalServerErrorException
79          * @throws ServiceUnavailableException
80          */
81         protected function content(array $request = []): string
82         {
83                 if (!$this->session->getLocalUserId()) {
84                         throw new HTTPException\ForbiddenException($this->l10n->t('Permission denied.'));
85                 }
86
87                 parent::content();
88
89                 /**
90                  * options shown on "Export personal data" page
91                  * list of array( 'link url', 'link text', 'help text' )
92                  */
93                 $options = [
94                         ['settings/userexport/account', $this->l10n->t('Export account'), $this->l10n->t('Export your account info and contacts. Use this to make a backup of your account and/or to move it to another server.')],
95                         ['settings/userexport/backup', $this->l10n->t('Export all'), $this->l10n->t('Export your account info, contacts and all your items as json. Could be a very big file, and could take a lot of time. Use this to make a full backup of your account (photos are not exported)')],
96                         ['settings/userexport/contact', $this->l10n->t('Export Contacts to CSV'), $this->l10n->t('Export the list of the accounts you are following as CSV file. Compatible to e.g. Mastodon.')],
97                 ];
98                 Hook::callAll('uexport_options', $options);
99
100                 $tpl = Renderer::getMarkupTemplate('settings/userexport.tpl');
101                 return Renderer::replaceMacros($tpl, [
102                         '$title' => $this->l10n->t('Export personal data'),
103                         '$options' => $options
104                 ]);
105         }
106
107         /**
108          * raw content generated for the different choices made
109          * by the user. At the moment this returns a JSON file
110          * to the browser which then offers a save / open dialog
111          * to the user.
112          *
113          * @throws HTTPException\ForbiddenException
114          */
115         protected function rawContent(array $request = [])
116         {
117                 if (!$this->session->getLocalUserId()) {
118                         throw new HTTPException\ForbiddenException($this->l10n->t('Permission denied.'));
119                 }
120
121                 if (isset($this->parameters['action'])) {
122                         switch ($this->parameters['action']) {
123                                 case 'backup':
124                                         header('Content-type: application/json');
125                                         header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '.' . $this->parameters['action'] . '"');
126                                         $this->echoAll($this->session->getLocalUserId());
127                                         break;
128                                 case 'account':
129                                         header('Content-type: application/json');
130                                         header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '.' . $this->parameters['action'] . '"');
131                                         $this->echoAccount($this->session->getLocalUserId());
132                                         break;
133                                 case 'contact':
134                                         header('Content-type: application/csv');
135                                         header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '-contacts.csv' . '"');
136                                         $this->echoContactsAsCSV($this->session->getLocalUserId());
137                                         break;
138                         }
139                         System::exit();
140                 }
141         }
142
143         /**
144          * @param string $query
145          * @return array
146          * @throws \Exception
147          */
148         private function exportMultiRow(string $query): array
149         {
150                 $dbStructure = $this->dbaDefinition->getAll();
151
152                 preg_match('/\s+from\s+`?([a-z\d_]+)`?/i', $query, $match);
153                 $table = $match[1];
154
155                 $result = [];
156                 $rows = DBA::p($query);
157                 while ($row = DBA::fetch($rows)) {
158                         $p = [];
159                         foreach ($dbStructure[$table]['fields'] as $column => $field) {
160                                 if (!isset($row[$column])) {
161                                         continue;
162                                 }
163                                 if ($field['type'] == 'datetime') {
164                                         $p[$column] = $row[$column] ?? DBA::NULL_DATETIME;
165                                 } else {
166                                         $p[$column] = $row[$column];
167                                 }
168                         }
169                         $result[] = $p;
170                 }
171                 DBA::close($rows);
172                 return $result;
173         }
174
175         /**
176          * @param string $query
177          * @return array
178          * @throws \Exception
179          */
180         private function exportRow(string $query): array
181         {
182                 $dbStructure = $this->dbaDefinition->getAll();
183
184                 preg_match('/\s+from\s+`?([a-z\d_]+)`?/i', $query, $match);
185                 $table = $match[1];
186
187                 $result = [];
188                 $rows = DBA::p($query);
189                 while ($row = DBA::fetch($rows)) {
190                         foreach ($row as $k => $v) {
191                                 if (empty($dbStructure[$table]['fields'][$k])) {
192                                         continue;
193                                 }
194
195                                 switch ($dbStructure[$table]['fields'][$k]['type']) {
196                                         case 'datetime':
197                                                 $result[$k] = $v ?? DBA::NULL_DATETIME;
198                                                 break;
199                                         default:
200                                                 $result[$k] = $v;
201                                                 break;
202                                 }
203                         }
204                 }
205                 DBA::close($rows);
206
207                 return $result;
208         }
209
210         /**
211          * Export a list of the contacts as CSV file as e.g. Mastodon and Pleroma are doing.
212          *
213          * @param int $user_id
214          * @throws \Exception
215          */
216         private function echoContactsAsCSV(int $user_id)
217         {
218                 if (!$user_id) {
219                         throw new \RuntimeException($this->l10n->t('Permission denied.'));
220                 }
221
222                 // write the table header (like Mastodon)
223                 echo "Account address, Show boosts\n";
224                 // get all the contacts
225                 $contacts = DBA::select('contact', ['addr', 'url'], ['uid' => $user_id, 'self' => false, 'rel' => [Contact::SHARING, Contact::FRIEND], 'deleted' => false, 'archive' => false]);
226                 while ($contact = DBA::fetch($contacts)) {
227                         echo ($contact['addr'] ?: $contact['url']) . ", true\n";
228                 }
229                 DBA::close($contacts);
230         }
231
232         /**
233          * @param int $user_id
234          * @throws \Exception
235          */
236         private function echoAccount(int $user_id)
237         {
238                 if (!$user_id) {
239                         throw new \RuntimeException($this->l10n->t('Permission denied.'));
240                 }
241
242                 $user = $this->exportRow(
243                         sprintf("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1", $user_id)
244                 );
245
246                 $contact = $this->exportMultiRow(
247                         sprintf("SELECT * FROM `contact` WHERE `uid` = %d ", $user_id)
248                 );
249
250
251                 $profile = $this->exportMultiRow(
252                         sprintf("SELECT *, 'default' AS `profile_name`, 1 AS `is-default` FROM `profile` WHERE `uid` = %d ", $user_id)
253                 );
254
255                 $profile_fields = $this->exportMultiRow(
256                         sprintf("SELECT * FROM `profile_field` WHERE `uid` = %d ", $user_id)
257                 );
258
259                 $photo = $this->exportMultiRow(
260                         sprintf("SELECT * FROM `photo` WHERE uid = %d AND profile = 1", $user_id)
261                 );
262                 foreach ($photo as &$p) {
263                         $p['data'] = bin2hex($p['data']);
264                 }
265
266                 $pconfig = $this->exportMultiRow(
267                         sprintf("SELECT * FROM `pconfig` WHERE uid = %d", $user_id)
268                 );
269
270                 $group = $this->exportMultiRow(
271                         sprintf("SELECT * FROM `group` WHERE uid = %d", $user_id)
272                 );
273
274                 $group_member = $this->exportMultiRow(
275                         sprintf("SELECT `group_member`.`gid`, `group_member`.`contact-id` FROM `group_member` INNER JOIN `group` ON `group`.`id` = `group_member`.`gid` WHERE `group`.`uid` = %d", $user_id)
276                 );
277
278                 $output = [
279                         'version' => App::VERSION,
280                         'schema' => DB_UPDATE_VERSION,
281                         'baseurl' => $this->baseUrl,
282                         'user' => $user,
283                         'contact' => $contact,
284                         'profile' => $profile,
285                         'profile_fields' => $profile_fields,
286                         'photo' => $photo,
287                         'pconfig' => $pconfig,
288                         'group' => $group,
289                         'group_member' => $group_member,
290                 ];
291
292                 echo json_encode($output, JSON_PARTIAL_OUTPUT_ON_ERROR);
293         }
294
295         /**
296          * echoes account data and items as separated json, one per line
297          *
298          * @param int $user_id
299          * @throws \Exception
300          */
301         private function echoAll(int $user_id)
302         {
303                 if (!$user_id) {
304                         throw new \RuntimeException($this->l10n->t('Permission denied.'));
305                 }
306
307                 $this->echoAccount($user_id);
308                 echo "\n";
309
310                 $total = Post::count(['uid' => $user_id]);
311                 // chunk the output to avoid exhausting memory
312
313                 for ($x = 0; $x < $total; $x += 500) {
314                         $items = Post::selectToArray(Item::ITEM_FIELDLIST, ['uid' => $user_id], ['limit' => [$x, 500]]);
315                         $output = ['item' => $items];
316                         echo json_encode($output, JSON_PARTIAL_OUTPUT_ON_ERROR) . "\n";
317                 }
318         }
319 }