AccountMigrator.php 7.1 KB

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