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