* * @author Christopher Ng * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ namespace OCA\Settings\UserMigration; use InvalidArgumentException; use OC\Accounts\TAccountsHelper; use OC\Core\Db\ProfileConfigMapper; use OC\NotSquareException; use OC\Profile\ProfileManager; use OCA\Settings\AppInfo\Application; use OCP\Accounts\IAccountManager; use OCP\IAvatarManager; use OCP\IL10N; use OCP\IUser; use OCP\UserMigration\IExportDestination; use OCP\UserMigration\IImportSource; use OCP\UserMigration\IMigrator; use OCP\UserMigration\ISizeEstimationMigrator; use OCP\UserMigration\TMigratorBasicVersionHandling; use Symfony\Component\Console\Output\OutputInterface; use Throwable; class AccountMigrator implements IMigrator, ISizeEstimationMigrator { use TMigratorBasicVersionHandling; use TAccountsHelper; private IAccountManager $accountManager; private IAvatarManager $avatarManager; private ProfileManager $profileManager; private ProfileConfigMapper $configMapper; private IL10N $l10n; private const PATH_ROOT = Application::APP_ID . '/'; private const PATH_ACCOUNT_FILE = AccountMigrator::PATH_ROOT . 'account.json'; private const AVATAR_BASENAME = 'avatar'; private const PATH_CONFIG_FILE = AccountMigrator::PATH_ROOT . 'config.json'; public function __construct( IAccountManager $accountManager, IAvatarManager $avatarManager, ProfileManager $profileManager, ProfileConfigMapper $configMapper, IL10N $l10n ) { $this->accountManager = $accountManager; $this->avatarManager = $avatarManager; $this->profileManager = $profileManager; $this->configMapper = $configMapper; $this->l10n = $l10n; } /** * {@inheritDoc} */ public function getEstimatedExportSize(IUser $user): int|float { $size = 100; // 100KiB for account JSON try { $avatar = $this->avatarManager->getAvatar($user->getUID()); if ($avatar->isCustomAvatar()) { $avatarFile = $avatar->getFile(-1); $size += $avatarFile->getSize() / 1024; } } catch (Throwable $e) { // Skip avatar in size estimate on failure } return ceil($size); } /** * {@inheritDoc} */ public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void { $output->writeln('Exporting account information in ' . AccountMigrator::PATH_ACCOUNT_FILE . '…'); try { $account = $this->accountManager->getAccount($user); $exportDestination->addFileContents(AccountMigrator::PATH_ACCOUNT_FILE, json_encode($account)); } catch (Throwable $e) { throw new AccountMigratorException('Could not export account information', 0, $e); } try { $avatar = $this->avatarManager->getAvatar($user->getUID()); if ($avatar->isCustomAvatar()) { $avatarFile = $avatar->getFile(-1); $exportPath = AccountMigrator::PATH_ROOT . AccountMigrator::AVATAR_BASENAME . '.' . $avatarFile->getExtension(); $output->writeln('Exporting avatar to ' . $exportPath . '…'); $exportDestination->addFileAsStream($exportPath, $avatarFile->read()); } } catch (Throwable $e) { throw new AccountMigratorException('Could not export avatar', 0, $e); } try { $output->writeln('Exporting profile config in ' . AccountMigrator::PATH_CONFIG_FILE . '…'); $config = $this->profileManager->getProfileConfig($user, $user); $exportDestination->addFileContents(AccountMigrator::PATH_CONFIG_FILE, json_encode($config)); } catch (Throwable $e) { throw new AccountMigratorException('Could not export profile config', 0, $e); } } /** * {@inheritDoc} */ public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void { if ($importSource->getMigratorVersion($this->getId()) === null) { $output->writeln('No version for ' . static::class . ', skipping import…'); return; } $output->writeln('Importing account information from ' . AccountMigrator::PATH_ACCOUNT_FILE . '…'); $account = $this->accountManager->getAccount($user); /** @var array>|array>> $data */ $data = json_decode($importSource->getFileContents(AccountMigrator::PATH_ACCOUNT_FILE), true, 512, JSON_THROW_ON_ERROR); $account->setAllPropertiesFromJson($data); try { $this->accountManager->updateAccount($account); } catch (InvalidArgumentException $e) { throw new AccountMigratorException('Failed to import account information'); } /** @var array $avatarFiles */ $avatarFiles = array_filter( $importSource->getFolderListing(AccountMigrator::PATH_ROOT), fn (string $filename) => pathinfo($filename, PATHINFO_FILENAME) === AccountMigrator::AVATAR_BASENAME, ); if (!empty($avatarFiles)) { if (count($avatarFiles) > 1) { $output->writeln('Expected single avatar image file, using first file found'); } $importPath = AccountMigrator::PATH_ROOT . reset($avatarFiles); $output->writeln('Importing avatar from ' . $importPath . '…'); $stream = $importSource->getFileAsStream($importPath); $image = new \OCP\Image(); $image->loadFromFileHandle($stream); try { $avatar = $this->avatarManager->getAvatar($user->getUID()); $avatar->set($image); } catch (NotSquareException $e) { throw new AccountMigratorException('Avatar image must be square'); } catch (Throwable $e) { throw new AccountMigratorException('Failed to import avatar', 0, $e); } } try { $output->writeln('Importing profile config from ' . AccountMigrator::PATH_CONFIG_FILE . '…'); /** @var array $configData */ $configData = json_decode($importSource->getFileContents(AccountMigrator::PATH_CONFIG_FILE), true, 512, JSON_THROW_ON_ERROR); // Ensure that a profile config entry exists in the database $this->profileManager->getProfileConfig($user, $user); $config = $this->configMapper->get($user->getUID()); $config->setConfigArray($configData); $this->configMapper->update($config); } catch (Throwable $e) { throw new AccountMigratorException('Failed to import profile config'); } } /** * {@inheritDoc} */ public function getId(): string { return 'account'; } /** * {@inheritDoc} */ public function getDisplayName(): string { return $this->l10n->t('Profile information'); } /** * {@inheritDoc} */ public function getDescription(): string { return $this->l10n->t('Profile picture, full name, email, phone number, address, website, Twitter, organisation, role, headline, biography, and whether your profile is enabled'); } }