AccountMigrator.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\Settings\UserMigration;
  8. use InvalidArgumentException;
  9. use OC\Accounts\TAccountsHelper;
  10. use OC\Core\Db\ProfileConfigMapper;
  11. use OC\NotSquareException;
  12. use OC\Profile\ProfileManager;
  13. use OCA\Settings\AppInfo\Application;
  14. use OCP\Accounts\IAccountManager;
  15. use OCP\IAvatarManager;
  16. use OCP\IL10N;
  17. use OCP\IUser;
  18. use OCP\UserMigration\IExportDestination;
  19. use OCP\UserMigration\IImportSource;
  20. use OCP\UserMigration\IMigrator;
  21. use OCP\UserMigration\ISizeEstimationMigrator;
  22. use OCP\UserMigration\TMigratorBasicVersionHandling;
  23. use Symfony\Component\Console\Output\OutputInterface;
  24. use Throwable;
  25. class AccountMigrator implements IMigrator, ISizeEstimationMigrator {
  26. use TMigratorBasicVersionHandling;
  27. use TAccountsHelper;
  28. private IAccountManager $accountManager;
  29. private IAvatarManager $avatarManager;
  30. private ProfileManager $profileManager;
  31. private ProfileConfigMapper $configMapper;
  32. private IL10N $l10n;
  33. private const PATH_ROOT = Application::APP_ID . '/';
  34. private const PATH_ACCOUNT_FILE = AccountMigrator::PATH_ROOT . 'account.json';
  35. private const AVATAR_BASENAME = 'avatar';
  36. private const PATH_CONFIG_FILE = AccountMigrator::PATH_ROOT . 'config.json';
  37. public function __construct(
  38. IAccountManager $accountManager,
  39. IAvatarManager $avatarManager,
  40. ProfileManager $profileManager,
  41. ProfileConfigMapper $configMapper,
  42. IL10N $l10n,
  43. ) {
  44. $this->accountManager = $accountManager;
  45. $this->avatarManager = $avatarManager;
  46. $this->profileManager = $profileManager;
  47. $this->configMapper = $configMapper;
  48. $this->l10n = $l10n;
  49. }
  50. /**
  51. * {@inheritDoc}
  52. */
  53. public function getEstimatedExportSize(IUser $user): int|float {
  54. $size = 100; // 100KiB for account JSON
  55. try {
  56. $avatar = $this->avatarManager->getAvatar($user->getUID());
  57. if ($avatar->isCustomAvatar()) {
  58. $avatarFile = $avatar->getFile(-1);
  59. $size += $avatarFile->getSize() / 1024;
  60. }
  61. } catch (Throwable $e) {
  62. // Skip avatar in size estimate on failure
  63. }
  64. return ceil($size);
  65. }
  66. /**
  67. * {@inheritDoc}
  68. */
  69. public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void {
  70. $output->writeln('Exporting account information in ' . AccountMigrator::PATH_ACCOUNT_FILE . '…');
  71. try {
  72. $account = $this->accountManager->getAccount($user);
  73. $exportDestination->addFileContents(AccountMigrator::PATH_ACCOUNT_FILE, json_encode($account));
  74. } catch (Throwable $e) {
  75. throw new AccountMigratorException('Could not export account information', 0, $e);
  76. }
  77. try {
  78. $avatar = $this->avatarManager->getAvatar($user->getUID());
  79. if ($avatar->isCustomAvatar()) {
  80. $avatarFile = $avatar->getFile(-1);
  81. $exportPath = AccountMigrator::PATH_ROOT . AccountMigrator::AVATAR_BASENAME . '.' . $avatarFile->getExtension();
  82. $output->writeln('Exporting avatar to ' . $exportPath . '…');
  83. $exportDestination->addFileAsStream($exportPath, $avatarFile->read());
  84. }
  85. } catch (Throwable $e) {
  86. throw new AccountMigratorException('Could not export avatar', 0, $e);
  87. }
  88. try {
  89. $output->writeln('Exporting profile config in ' . AccountMigrator::PATH_CONFIG_FILE . '…');
  90. $config = $this->profileManager->getProfileConfig($user, $user);
  91. $exportDestination->addFileContents(AccountMigrator::PATH_CONFIG_FILE, json_encode($config));
  92. } catch (Throwable $e) {
  93. throw new AccountMigratorException('Could not export profile config', 0, $e);
  94. }
  95. }
  96. /**
  97. * {@inheritDoc}
  98. */
  99. public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void {
  100. if ($importSource->getMigratorVersion($this->getId()) === null) {
  101. $output->writeln('No version for ' . static::class . ', skipping import…');
  102. return;
  103. }
  104. $output->writeln('Importing account information from ' . AccountMigrator::PATH_ACCOUNT_FILE . '…');
  105. $account = $this->accountManager->getAccount($user);
  106. /** @var array<string, array<string, string>>|array<string, array<int, array<string, string>>> $data */
  107. $data = json_decode($importSource->getFileContents(AccountMigrator::PATH_ACCOUNT_FILE), true, 512, JSON_THROW_ON_ERROR);
  108. $account->setAllPropertiesFromJson($data);
  109. try {
  110. $this->accountManager->updateAccount($account);
  111. } catch (InvalidArgumentException $e) {
  112. throw new AccountMigratorException('Failed to import account information');
  113. }
  114. /** @var array<int, string> $avatarFiles */
  115. $avatarFiles = array_filter(
  116. $importSource->getFolderListing(AccountMigrator::PATH_ROOT),
  117. fn (string $filename) => pathinfo($filename, PATHINFO_FILENAME) === AccountMigrator::AVATAR_BASENAME,
  118. );
  119. if (!empty($avatarFiles)) {
  120. if (count($avatarFiles) > 1) {
  121. $output->writeln('Expected single avatar image file, using first file found');
  122. }
  123. $importPath = AccountMigrator::PATH_ROOT . reset($avatarFiles);
  124. $output->writeln('Importing avatar from ' . $importPath . '…');
  125. $stream = $importSource->getFileAsStream($importPath);
  126. $image = new \OCP\Image();
  127. $image->loadFromFileHandle($stream);
  128. try {
  129. $avatar = $this->avatarManager->getAvatar($user->getUID());
  130. $avatar->set($image);
  131. } catch (NotSquareException $e) {
  132. throw new AccountMigratorException('Avatar image must be square');
  133. } catch (Throwable $e) {
  134. throw new AccountMigratorException('Failed to import avatar', 0, $e);
  135. }
  136. }
  137. try {
  138. $output->writeln('Importing profile config from ' . AccountMigrator::PATH_CONFIG_FILE . '…');
  139. /** @var array $configData */
  140. $configData = json_decode($importSource->getFileContents(AccountMigrator::PATH_CONFIG_FILE), true, 512, JSON_THROW_ON_ERROR);
  141. // Ensure that a profile config entry exists in the database
  142. $this->profileManager->getProfileConfig($user, $user);
  143. $config = $this->configMapper->get($user->getUID());
  144. $config->setConfigArray($configData);
  145. $this->configMapper->update($config);
  146. } catch (Throwable $e) {
  147. throw new AccountMigratorException('Failed to import profile config');
  148. }
  149. }
  150. /**
  151. * {@inheritDoc}
  152. */
  153. public function getId(): string {
  154. return 'account';
  155. }
  156. /**
  157. * {@inheritDoc}
  158. */
  159. public function getDisplayName(): string {
  160. return $this->l10n->t('Profile information');
  161. }
  162. /**
  163. * {@inheritDoc}
  164. */
  165. public function getDescription(): string {
  166. return $this->l10n->t('Profile picture, full name, email, phone number, address, website, Twitter, organisation, role, headline, biography, and whether your profile is enabled');
  167. }
  168. }