Update copyright
[friendica.git/.git] / src / Module / Settings / UserExport.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, 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\Renderer;
27 use Friendica\Database\DBA;
28 use Friendica\Database\DBStructure;
29 use Friendica\DI;
30 use Friendica\Model\Item;
31 use Friendica\Model\Post;
32 use Friendica\Module\BaseSettings;
33
34 /**
35  * Module to export user data
36  **/
37 class UserExport extends BaseSettings
38 {
39         /**
40          * Handle the request to export data.
41          * At the moment one can export three different data set
42          * 1. The profile data that can be used by uimport to resettle
43          *    to a different Friendica instance
44          * 2. The entire data-set, profile plus postings
45          * 3. A list of contacts as CSV file similar to the export of Mastodon
46          *
47          * If there is an action required through the URL / path, react
48          * accordingly and export the requested data.
49          **/
50         public static function content(array $parameters = [])
51         {
52                 parent::content($parameters);
53
54                 /**
55                  * options shown on "Export personal data" page
56                  * list of array( 'link url', 'link text', 'help text' )
57                  */
58                 $options = [
59                         ['settings/userexport/account', DI::l10n()->t('Export account'), DI::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.')],
60                         ['settings/userexport/backup', DI::l10n()->t('Export all'), DI::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 \x28photos are not exported\x29")],
61                         ['settings/userexport/contact', DI::l10n()->t('Export Contacts to CSV'), DI::l10n()->t("Export the list of the accounts you are following as CSV file. Compatible to e.g. Mastodon.")],
62                 ];
63                 Hook::callAll('uexport_options', $options);
64
65                 $tpl = Renderer::getMarkupTemplate("settings/userexport.tpl");
66                 return Renderer::replaceMacros($tpl, [
67                         '$title' => DI::l10n()->t('Export personal data'),
68                         '$options' => $options
69                 ]);
70         }
71         /**
72          * raw content generated for the different choices made
73          * by the user. At the moment this returns a JSON file
74          * to the browser which then offers a save / open dialog
75          * to the user.
76          **/
77         public static function rawContent(array $parameters = [])
78         {
79                 $args = DI::args();
80                 if ($args->getArgc() == 3) {
81                         // @TODO Replace with router-provided arguments
82                         $action = $args->get(2);
83                         $user = DI::app()->user;
84                         switch ($action) {
85                                 case "backup":
86                                         header("Content-type: application/json");
87                                         header('Content-Disposition: attachment; filename="' . $user['nickname'] . '.' . $action . '"');
88                                         self::exportAll(DI::app());
89                                         exit();
90                                         break;
91                                 case "account":
92                                         header("Content-type: application/json");
93                                         header('Content-Disposition: attachment; filename="' . $user['nickname'] . '.' . $action . '"');
94                                         self::exportAccount(DI::app());
95                                         exit();
96                                         break;
97                                 case "contact":
98                                         header("Content-type: application/csv");
99                                         header('Content-Disposition: attachment; filename="' . $user['nickname'] . '-contacts.csv'. '"');
100                                         self::exportContactsAsCSV();
101                                         exit();
102                                         break;
103                                 default:
104                                         exit();
105                         }
106                 }
107         }
108         private static function exportMultiRow(string $query)
109         {
110                 $dbStructure = DBStructure::definition(DI::app()->getBasePath(), false);
111
112                 preg_match("/\s+from\s+`?([a-z\d_]+)`?/i", $query, $match);
113                 $table = $match[1];
114
115                 $result = [];
116                 $rows = DBA::p($query);
117                 while ($row = DBA::fetch($rows)) {
118                         $p = [];
119                         foreach ($dbStructure[$table]['fields'] as $column => $field) {
120                                 if (!isset($row[$column])) {
121                                         continue;
122                                 }
123                                 if ($field['type'] == 'datetime') {
124                                         $p[$column] = $row[$column] ?? DBA::NULL_DATETIME;
125                                 } else {
126                                         $p[$column] = $row[$column];
127                                 }
128                         }
129                         $result[] = $p;
130                 }
131                 DBA::close($rows);
132                 return $result;
133         }
134
135         private static function exportRow(string $query)
136         {
137                 $dbStructure = DBStructure::definition(DI::app()->getBasePath(), false);
138
139                 preg_match("/\s+from\s+`?([a-z\d_]+)`?/i", $query, $match);
140                 $table = $match[1];
141
142                 $result = [];
143                 $r = q($query);
144                 if (DBA::isResult($r)) {
145
146                         foreach ($r as $rr) {
147                                 foreach ($rr as $k => $v) {
148                                         if (empty($dbStructure[$table]['fields'][$k])) {
149                                                 continue;
150                                         }
151                                         switch ($dbStructure[$table]['fields'][$k]['type']) {
152                                                 case 'datetime':
153                                                         $result[$k] = $v ?? DBA::NULL_DATETIME;
154                                                         break;
155                                                 default:
156                                                         $result[$k] = $v;
157                                                         break;
158                                         }
159                                 }
160                         }
161                 }
162                 return $result;
163         }
164
165         /**
166          * Export a list of the contacts as CSV file as e.g. Mastodon and Pleroma are doing.
167          **/
168         private static function exportContactsAsCSV()
169         {
170                 // write the table header (like Mastodon)
171                 echo "Account address, Show boosts\n";
172                 // get all the contacts
173                 $contacts = DBA::select('contact', ['addr', 'url'], ['uid' => $_SESSION['uid'], 'self' => false, 'rel' => [1,3], 'deleted' => false]);
174                 while ($contact = DBA::fetch($contacts)) {
175                         echo ($contact['addr'] ?: $contact['url']) . ", true\n";
176                 }
177                 DBA::close($contacts);
178         }
179         private static function exportAccount(App $a)
180         {
181                 $user = self::exportRow(
182                         sprintf("SELECT * FROM `user` WHERE `uid` = %d LIMIT 1", intval(local_user()))
183                 );
184
185                 $contact = self::exportMultiRow(
186                         sprintf("SELECT * FROM `contact` WHERE `uid` = %d ", intval(local_user()))
187                 );
188
189
190                 $profile = self::exportMultiRow(
191                         sprintf("SELECT *, 'default' AS `profile_name`, 1 AS `is-default` FROM `profile` WHERE `uid` = %d ", intval(local_user()))
192                 );
193
194                 $profile_fields = self::exportMultiRow(
195                         sprintf("SELECT * FROM `profile_field` WHERE `uid` = %d ", intval(local_user()))
196                 );
197
198                 $photo = self::exportMultiRow(
199                         sprintf("SELECT * FROM `photo` WHERE uid = %d AND profile = 1", intval(local_user()))
200                 );
201                 foreach ($photo as &$p) {
202                         $p['data'] = bin2hex($p['data']);
203                 }
204
205                 $pconfig = self::exportMultiRow(
206                         sprintf("SELECT * FROM `pconfig` WHERE uid = %d", intval(local_user()))
207                 );
208
209                 $group = self::exportMultiRow(
210                         sprintf("SELECT * FROM `group` WHERE uid = %d", intval(local_user()))
211                 );
212
213                 $group_member = self::exportMultiRow(
214                         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", intval(local_user()))
215                 );
216
217                 $output = [
218                         'version' => FRIENDICA_VERSION,
219                         'schema' => DB_UPDATE_VERSION,
220                         'baseurl' => DI::baseUrl(),
221                         'user' => $user,
222                         'contact' => $contact,
223                         'profile' => $profile,
224                         'profile_fields' => $profile_fields,
225                         'photo' => $photo,
226                         'pconfig' => $pconfig,
227                         'group' => $group,
228                         'group_member' => $group_member,
229                 ];
230
231                 echo json_encode($output, JSON_PARTIAL_OUTPUT_ON_ERROR);
232         }
233
234         /**
235          * echoes account data and items as separated json, one per line
236          *
237          * @param App $a
238          * @throws \Exception
239          */
240         private static function exportAll(App $a)
241         {
242                 self::exportAccount($a);
243                 echo "\n";
244
245                 $total = Post::count(['uid' => local_user()]);
246                 // chunk the output to avoid exhausting memory
247
248                 for ($x = 0; $x < $total; $x += 500) {
249                         $items = Post::selectToArray(Item::ITEM_FIELDLIST, ['uid' => local_user()], ['limit' => [$x, 500]]);
250                         $output = ['item' => $items];
251                         echo json_encode($output, JSON_PARTIAL_OUTPUT_ON_ERROR). "\n";
252                 }
253         }
254 }