AccountMigrator.php 6.3 KB

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