]> git.mxchange.org Git - friendica.git/blob - src/Module/User/Import.php
Remove DI dependency from Module\Contact\Profile
[friendica.git] / src / Module / User / Import.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\User;
23
24 use Friendica\App;
25 use Friendica\Core\Config\Capability\IManageConfigValues;
26 use Friendica\Core\L10n;
27 use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues;
28 use Friendica\Core\Protocol;
29 use Friendica\Core\Renderer;
30 use Friendica\Core\System;
31 use Friendica\Core\Worker;
32 use Friendica\Database\Database;
33 use Friendica\Database\DBA;
34 use Friendica\Database\DBStructure;
35 use Friendica\Model\Photo;
36 use Friendica\Model\Profile;
37 use Friendica\Module\Response;
38 use Friendica\Navigation\SystemMessages;
39 use Friendica\Network\HTTPException;
40 use Friendica\Object\Image;
41 use Friendica\Protocol\Delivery;
42 use Friendica\Security\PermissionSet\Repository\PermissionSet;
43 use Friendica\Util\Profiler;
44 use Friendica\Util\Strings;
45 use Psr\Log\LoggerInterface;
46
47 class Import extends \Friendica\BaseModule
48 {
49         const IMPORT_DEBUG = false;
50
51         /** @var App */
52         private $app;
53
54         /** @var IManageConfigValues */
55         private $config;
56
57         /** @var IManagePersonalConfigValues */
58         private $pconfig;
59
60         /** @var SystemMessages */
61         private $systemMessages;
62
63         /** @var Database */
64         private $database;
65
66         /** @var PermissionSet */
67         private $permissionSet;
68
69         public function __construct(PermissionSet $permissionSet, IManagePersonalConfigValues $pconfig, Database $database, SystemMessages $systemMessages, IManageConfigValues $config, App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
70         {
71                 parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
72
73                 $this->app            = $app;
74                 $this->config         = $config;
75                 $this->pconfig        = $pconfig;
76                 $this->systemMessages = $systemMessages;
77                 $this->database       = $database;
78                 $this->permissionSet  = $permissionSet;
79         }
80
81         protected function post(array $request = [])
82         {
83                 if ($this->config->get('config', 'register_policy') != \Friendica\Module\Register::OPEN && !$this->app->isSiteAdmin()) {
84                         throw new HttpException\ForbiddenException($this->t('Permission denied.'));
85                 }
86
87                 $max_dailies = intval($this->config->get('system', 'max_daily_registrations'));
88                 if ($max_dailies) {
89                         $total = $this->database->count('user', ["`register_date` > UTC_TIMESTAMP - INTERVAL 1 DAY"]);
90                         if ($total >= $max_dailies) {
91                                 throw new HttpException\ForbiddenException($this->t('Permission denied.'));
92                         }
93                 }
94
95                 if (!empty($_FILES['accountfile'])) {
96                         $this->importAccount($_FILES['accountfile']);
97                 }
98         }
99
100         protected function content(array $request = []): string
101         {
102                 if (($this->config->get('config', 'register_policy') != \Friendica\Module\Register::OPEN) && !$this->app->isSiteAdmin()) {
103                         $this->systemMessages->addNotice($this->t('User imports on closed servers can only be done by an administrator.'));
104                 }
105
106                 $max_dailies = intval($this->config->get('system', 'max_daily_registrations'));
107                 if ($max_dailies) {
108                         $total = $this->database->count('user', ["`register_date` > UTC_TIMESTAMP - INTERVAL 1 DAY"]);
109                         if ($total >= $max_dailies) {
110                                 $this->logger->notice('max daily registrations exceeded.');
111                                 $this->systemMessages->addNotice($this->t('This site has exceeded the number of allowed daily account registrations. Please try again tomorrow.'));
112                         }
113                 }
114
115                 $tpl = Renderer::getMarkupTemplate('user/import.tpl');
116                 return Renderer::replaceMacros($tpl, [
117                         '$regbutt' => $this->t('Import'),
118                         '$import'  => [
119                                 'title'    => $this->t('Move account'),
120                                 'intro'    => $this->t('You can import an account from another Friendica server.'),
121                                 'instruct' => $this->t('You need to export your account from the old server and upload it here. We will recreate your old account here with all your contacts. We will try also to inform your friends that you moved here.'),
122                                 'warn'     => $this->t("This feature is experimental. We can't import contacts from the OStatus network (GNU Social/Statusnet) or from Diaspora"),
123                                 'field'    => ['accountfile', $this->t('Account file'), '<input id="id_accountfile" name="accountfile" type="file">', $this->t('To export your account, go to "Settings->Export your personal data" and select "Export account"')],
124                         ],
125                 ]);
126         }
127
128         private function lastInsertId(): int
129         {
130                 if (self::IMPORT_DEBUG) {
131                         return 1;
132                 }
133
134                 return $this->database->lastInsertId();
135         }
136
137         /**
138          * Remove columns from array $arr that aren't in table $table
139          *
140          * @param string $table Table name
141          * @param array &$arr   Column=>Value array from json (by ref)
142          * @throws \Exception
143          */
144         private function checkCols(string $table, array &$arr)
145         {
146                 $tableColumns = DBStructure::getColumns($table);
147
148                 $tcols = [];
149                 $ttype = [];
150                 // get a plain array of column names
151                 foreach ($tableColumns as $tcol) {
152                         $tcols[]               = $tcol['Field'];
153                         $ttype[$tcol['Field']] = $tcol['Type'];
154                 }
155
156                 // remove inexistent columns
157                 foreach ($arr as $icol => $ival) {
158                         if (!in_array($icol, $tcols)) {
159                                 unset($arr[$icol]);
160                                 continue;
161                         }
162
163                         if ($ttype[$icol] === 'datetime') {
164                                 $arr[$icol] = $ival ?? DBA::NULL_DATETIME;
165                         }
166                 }
167         }
168
169         /**
170          * Import data into table $table
171          *
172          * @param string $table Table name
173          * @param array  $arr   Column=>Value array from json
174          * @return bool
175          * @throws \Exception
176          */
177         private function dbImportAssoc(string $table, array $arr): bool
178         {
179                 if (isset($arr['id'])) {
180                         unset($arr['id']);
181                 }
182
183                 $this->checkCols($table, $arr);
184
185                 if (self::IMPORT_DEBUG) {
186                         return true;
187                 }
188
189                 return $this->database->insert($table, $arr);
190         }
191
192         /**
193          * Import account file exported from mod/uexport
194          *
195          * @param array $file array from $_FILES
196          * @return void
197          * @throws HTTPException\FoundException
198          * @throws HTTPException\InternalServerErrorException
199          * @throws HTTPException\MovedPermanentlyException
200          * @throws HTTPException\TemporaryRedirectException
201          * @throws \ImagickException
202          */
203         private function importAccount(array $file)
204         {
205                 $this->logger->notice('Start user import from ' . $file['tmp_name']);
206                 /*
207                 STEPS
208                 1. checks
209                 2. replace old baseurl with new baseurl
210                 3. import data (look at user id and contacts id)
211                 4. archive non-dfrn contacts
212                 5. send message to dfrn contacts
213                 */
214
215                 $account = json_decode(file_get_contents($file['tmp_name']), true);
216                 if ($account === null) {
217                         $this->systemMessages->addNotice($this->t('Error decoding account file'));
218                         return;
219                 }
220
221                 if (empty($account['version'])) {
222                         $this->systemMessages->addNotice($this->t('Error! No version data in file! This is not a Friendica account file?'));
223                         return;
224                 }
225
226                 // check for username
227                 // check if username matches deleted account
228                 if ($this->database->exists('user', ['nickname' => $account['user']['nickname']])
229                         || $this->database->exists('userd', ['username' => $account['user']['nickname']])) {
230                         $this->systemMessages->addNotice($this->t("User '%s' already exists on this server!", $account['user']['nickname']));
231                         return;
232                 }
233
234                 // Backward compatibility
235                 $account['circle'] = $account['circle'] ?? $account['group'];
236                 $account['circle_member'] = $account['circle_member'] ?? $account['group_member'];
237
238                 $oldBaseUrl = $account['baseurl'];
239                 $newBaseUrl = (string)$this->baseUrl;
240
241                 $oldAddr = str_replace('http://', '@', Strings::normaliseLink($oldBaseUrl));
242                 $newAddr = str_replace('http://', '@', Strings::normaliseLink($newBaseUrl));
243
244                 if (!empty($account['profile']['addr'])) {
245                         $oldHandle = $account['profile']['addr'];
246                 } else {
247                         $oldHandle = $account['user']['nickname'] . $oldAddr;
248                 }
249
250                 // Creating a new guid to avoid problems with Diaspora
251                 $account['user']['guid'] = System::createUUID();
252
253                 $oldUid = $account['user']['uid'];
254
255                 unset($account['user']['uid']);
256                 unset($account['user']['account_expired']);
257                 unset($account['user']['account_expires_on']);
258                 unset($account['user']['expire_notification_sent']);
259
260                 array_walk($account['user'], function (&$user) use ($oldBaseUrl, $oldAddr, $newBaseUrl, $newAddr) {
261                         $user = str_replace([$oldBaseUrl, $oldAddr], [$newBaseUrl, $newAddr], $user);
262                 });
263
264                 // import user
265                 if ($this->dbImportAssoc('user', $account['user']) === false) {
266                         $this->logger->warning('Error inserting user', ['user' => $account['user'], 'error' => $this->database->errorMessage()]);
267                         $this->systemMessages->addNotice($this->t('User creation error'));
268                         return;
269                 }
270
271                 $newUid = $this->lastInsertId();
272
273                 $this->pconfig->set($newUid, 'system', 'previous_addr', $oldHandle);
274
275                 $errorCount = 0;
276
277                 array_walk($account['contact'], function (&$contact) use (&$errorCount, $oldUid, $oldBaseUrl, $oldAddr, $newUid, $newBaseUrl, $newAddr) {
278                         if ($contact['uid'] == $oldUid && $contact['self'] == '1') {
279                                 array_walk($contact, function (&$field) use ($oldUid, $oldBaseUrl, $oldAddr, $newUid, $newBaseUrl, $newAddr) {
280                                         $field = str_replace([$oldBaseUrl, $oldAddr], [$newBaseUrl, $newAddr], $field);
281                                         foreach (['profile', 'avatar', 'micro'] as $key) {
282                                                 $field = str_replace($oldBaseUrl . '/photo/' . $key . '/' . $oldUid . '.jpg', $newBaseUrl . '/photo/' . $key . '/' . $newUid . '.jpg', $field);
283                                         }
284                                 });
285                         }
286
287                         if ($contact['uid'] == $oldUid && $contact['self'] == '0') {
288                                 // set contacts 'avatar-date' to NULL_DATE to let worker update the URLs
289                                 $contact['avatar-date'] = DBA::NULL_DATETIME;
290
291                                 switch ($contact['network']) {
292                                         case Protocol::DFRN:
293                                         case Protocol::DIASPORA:
294                                                 //  send relocate message (below)
295                                                 break;
296                                         case Protocol::FEED:
297                                         case Protocol::MAIL:
298                                                 // Nothing to do
299                                                 break;
300                                         default:
301                                                 // archive other contacts
302                                                 $contact['archive'] = '1';
303                                 }
304                         }
305
306                         $contact['uid'] = $newUid;
307                         if ($this->dbImportAssoc('contact', $contact) === false) {
308                                 $this->logger->warning('Error inserting contact', ['nick' => $contact['nick'], 'network' => $contact['network'], 'error' => $this->database->errorMessage()]);
309                                 $errorCount++;
310                         } else {
311                                 $contact['newid'] = $this->lastInsertId();
312                         }
313                 });
314
315                 if ($errorCount > 0) {
316                         $this->systemMessages->addNotice($this->tt('%d contact not imported', '%d contacts not imported', $errorCount));
317                 }
318
319                 array_walk($account['circle'], function (&$circle) use ($newUid) {
320                         $circle['uid'] = $newUid;
321                         if ($this->dbImportAssoc('group', $circle) === false) {
322                                 $this->logger->warning('Error inserting circle', ['name' => $circle['name'], 'error' => $this->database->errorMessage()]);
323                         } else {
324                                 $circle['newid'] = $this->lastInsertId();
325                         }
326                 });
327
328                 foreach ($account['circle_member'] as $circle_member) {
329                         $import = 0;
330                         foreach ($account['circle'] as $circle) {
331                                 if ($circle['id'] == $circle_member['gid'] && isset($circle['newid'])) {
332                                         $circle_member['gid'] = $circle['newid'];
333                                         $import++;
334                                         break;
335                                 }
336                         }
337
338                         foreach ($account['contact'] as $contact) {
339                                 if ($contact['id'] == $circle_member['contact-id'] && isset($contact['newid'])) {
340                                         $circle_member['contact-id'] = $contact['newid'];
341                                         $import++;
342                                         break;
343                                 }
344                         }
345
346                         if ($import == 2 && $this->dbImportAssoc('group_member', $circle_member) === false) {
347                                 $this->logger->warning('Error inserting circle member', ['gid' => $circle_member['id'], 'error' => $this->database->errorMessage()]);
348                         }
349                 }
350
351                 foreach ($account['profile'] as $profile) {
352                         unset($profile['id']);
353                         $profile['uid'] = $newUid;
354
355                         array_walk($profile, function (&$field) use ($oldUid, $oldBaseUrl, $oldAddr, $newUid, $newBaseUrl, $newAddr) {
356                                 $field = str_replace([$oldBaseUrl, $oldAddr], [$newBaseUrl, $newAddr], $field);
357                                 foreach (['profile', 'avatar'] as $key) {
358                                         $field = str_replace($oldBaseUrl . '/photo/' . $key . '/' . $oldUid . '.jpg', $newBaseUrl . '/photo/' . $key . '/' . $newUid . '.jpg', $field);
359                                 }
360                         });
361
362                         if (count($account['profile']) === 1 || $profile['is-default']) {
363                                 if ($this->dbImportAssoc('profile', $profile) === false) {
364                                         $this->logger->warning('Error inserting profile', ['error' => $this->database->errorMessage()]);
365                                         $this->systemMessages->addNotice($this->t('User profile creation error'));
366                                         $this->database->delete('user', ['uid' => $newUid]);
367                                         $this->database->delete('profile_field', ['uid' => $newUid]);
368                                         return;
369                                 }
370
371                                 $profile['id'] = $this->database->lastInsertId();
372                         }
373
374                         Profile::migrate($profile);
375                 }
376
377                 $permissionSet = $this->permissionSet->selectDefaultForUser($newUid);
378
379                 foreach ($account['profile_fields'] ?? [] as $profile_field) {
380                         $profile_field['uid'] = $newUid;
381
382                         ///@TODO Replace with permissionset import
383                         $profile_field['psid'] = $profile_field['psid'] ? $permissionSet->id : PermissionSet::PUBLIC;
384
385                         if ($this->dbImportAssoc('profile_field', $profile_field) === false) {
386                                 $this->logger->info('Error inserting profile field', ['profile_id' => $profile_field['id'], 'error' => $this->database->errorMessage()]);
387                         }
388                 }
389
390                 foreach ($account['photo'] as $photo) {
391                         $photo['uid']  = $newUid;
392                         $photo['data'] = hex2bin($photo['data']);
393
394                         $r = Photo::store(
395                                 new Image($photo['data'], $photo['type']),
396                                 $photo['uid'], $photo['contact-id'], //0
397                                 $photo['resource-id'], $photo['filename'], $photo['album'], $photo['scale'], $photo['profile'], //1
398                                 $photo['allow_cid'], $photo['allow_gid'], $photo['deny_cid'], $photo['deny_gid']
399                         );
400
401                         if ($r === false) {
402                                 $this->logger->warning('Error inserting photo', ['resource-id' => $photo['resource-id'], 'scale' => $photo['scale'], 'error' => $this->database->errorMessage()]);
403                         }
404                 }
405
406                 foreach ($account['pconfig'] as $pconfig) {
407                         $pconfig['uid'] = $newUid;
408                         if ($this->dbImportAssoc('pconfig', $pconfig) === false) {
409                                 $this->logger->warning('Error inserting pconfig', ['pconfig_id' => $pconfig['id'], 'error' => $this->database->errorMessage()]);
410                         }
411                 }
412
413                 // send relocate messages
414                 Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::RELOCATION, $newUid);
415
416                 $this->systemMessages->addInfo($this->t('Done. You can now login with your username and password'));
417                 $this->baseUrl->redirect('login');
418         }
419 }