diff --git a/lib/UserMigration/MailAccountMigrator.php b/lib/UserMigration/MailAccountMigrator.php index e39b45636f..14cde18cfb 100644 --- a/lib/UserMigration/MailAccountMigrator.php +++ b/lib/UserMigration/MailAccountMigrator.php @@ -15,6 +15,7 @@ use OCA\Mail\UserMigration\Service\AccountMigrationService; use OCA\Mail\UserMigration\Service\AppConfigMigrationService; use OCA\Mail\UserMigration\Service\InternalAddressesMigrationService; +use OCA\Mail\UserMigration\Service\QuickActionsMigrationService; use OCA\Mail\UserMigration\Service\SMIMEMigrationService; use OCA\Mail\UserMigration\Service\TagsMigrationService; use OCA\Mail\UserMigration\Service\TextBlocksMigrationService; @@ -43,6 +44,7 @@ public function __construct( private readonly TextBlocksMigrationService $textBlocksMigrationService, private readonly TagsMigrationService $tagsMigrationService, private readonly SMIMEMigrationService $sMimeMigrationService, + private readonly QuickActionsMigrationService $quickActionsMigrationService, ) { } @@ -60,6 +62,7 @@ public function export(IUser $user, $this->tagsMigrationService->exportTags($user, $exportDestination, $output); $this->sMimeMigrationService->exportCertificates($user, $exportDestination, $output); $this->accountMigrationService->exportAccounts($user, $exportDestination, $output); + $this->quickActionsMigrationService->exportQuickActions($user, $exportDestination, $output); } #[\Override] @@ -73,8 +76,9 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface $this->trustedSendersMigrationService->importTrustedSenders($user, $importSource, $output); $this->textBlocksMigrationService->importTextBlocks($user, $importSource, $output); $newCertificateIds = $this->sMimeMigrationService->importCertificates($user, $importSource, $output); - $newAccountIds = $this->accountMigrationService->importAccounts($user, $importSource, $output, $newCertificateIds); + $newAccountAndMailboxIds = $this->accountMigrationService->importAccounts($user, $importSource, $output, $newCertificateIds); $newTagIds = $this->tagsMigrationService->importTags($user, $importSource, $output); + $this->quickActionsMigrationService->importQuickActions($user, $importSource, $output, $newAccountAndMailboxIds, $newTagIds); $this->accountMigrationService->scheduleBackgroundJobs($user, $output); } @@ -92,6 +96,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface private function deleteExistingData(IUser $user, OutputInterface $output): void { $output->writeln($this->l10n->t("Deleting existing mail data for user {$user->getUID()}"), OutputInterface::VERBOSITY_VERBOSE); + $this->quickActionsMigrationService->deleteAllQuickActions($user, $output); $this->accountMigrationService->deleteAllAccounts($user, $output); $this->appConfigMigrationService->deleteAppConfiguration($user, $output); $this->internalAddressesMigrationService->removeInternalAddresses($user, $output); diff --git a/lib/UserMigration/Service/QuickActionsMigrationService.php b/lib/UserMigration/Service/QuickActionsMigrationService.php new file mode 100644 index 0000000000..162f23dd83 --- /dev/null +++ b/lib/UserMigration/Service/QuickActionsMigrationService.php @@ -0,0 +1,181 @@ +writeln( + $this->l10n->t('Exporting quick actions for user %s', [$user->getUID()]), + OutputInterface::VERBOSITY_VERBOSE + ); + + $quickActions = $this->quickActionsService->findAll($user->getUID()); + + try { + $exportDestination->addFileContents(self::QUICK_ACTIONS_FILE, json_encode($quickActions, JSON_THROW_ON_ERROR)); + } catch (JsonException|UserMigrationException $exception) { + throw new UserMigrationException( + "Failed to export quick actions for user {$user->getUID()}", + previous: $exception + ); + } + } + + /** + * Import all quick actions the user defined across + * their accounts. + * + * @throws UserMigrationException + * @throws Exception + * @throws JsonException + * @throws \OCA\Mail\Exception\ServiceException + */ + public function importQuickActions(IUser $user, IImportSource $importSource, OutputInterface $output, array $accountAndMailboxMapping, array $tagMapping): void { + $output->writeln( + $this->l10n->t('Importing quick actions for user %s', [$user->getUID()]), + OutputInterface::VERBOSITY_VERBOSE + ); + + $quickActions = json_decode($importSource->getFileContents(self::QUICK_ACTIONS_FILE), true); + $this->validateQuickActions($quickActions); + + foreach ($quickActions as $quickAction) { + $createdQuickAction = $this->quickActionsService->create($quickAction['name'], $accountAndMailboxMapping['accounts'][$quickAction['accountId']]); + + foreach ($quickAction['actionSteps'] as $actionStep) { + $this->quickActionsService->createActionStep($actionStep['name'], $actionStep['order'], $createdQuickAction->getId(), $tagMapping[$actionStep['tagId']] ?? null, $accountAndMailboxMapping['mailboxes'][$actionStep['mailboxId']] ?? null); + } + } + } + + public function deleteAllQuickActions(IUser $user, OutputInterface $output): void { + $output->writeln( + $this->l10n->t('Deleting all quick actions for user %s', [$user->getUID()]), + OutputInterface::VERBOSITY_VERBOSE + ); + + $this->quickActionsService->deleteAll($user->getUID()); + } + + /** + * Validate the parsed quick actions to ensure they + * have the expected structure and types. + * + * @throws UserMigrationException + */ + private function validateQuickActions(mixed $quickActions): void { + $quickActionsArrayIsValid = is_array($quickActions) && array_is_list($quickActions); + if (!$quickActionsArrayIsValid) { + throw new UserMigrationException('Invalid quick actions export structure'); + } + + foreach ($quickActions as $quickAction) { + $quickActionArrayIsValid = is_array($quickAction); + + $idIsValid = $quickActionArrayIsValid + && array_key_exists('id', $quickAction) + && is_int($quickAction['id']); + + $nameIsValid = $quickActionArrayIsValid + && array_key_exists('name', $quickAction) + && is_string($quickAction['name']); + + $orderIsValid = $quickActionArrayIsValid + && array_key_exists('accountId', $quickAction) + && is_int($quickAction['accountId']); + + $actionStepsArrayIsValid = $quickActionArrayIsValid + && array_key_exists('actionSteps', $quickAction) + && is_array($quickAction['actionSteps']) + && array_is_list($quickAction['actionSteps']) + && $this->validateQuickSteps($quickAction['actionSteps']); + + if ( + !$idIsValid + || !$nameIsValid + || !$orderIsValid + || !$actionStepsArrayIsValid + ) { + throw new UserMigrationException('Invalid quick action entry'); + } + } + } + + private function validateQuickSteps(mixed $quickSteps): bool { + $quickStepsArrayIsValid = true; + + foreach ($quickSteps as $actionStep) { + $actionStepArrayIsValid = is_array($actionStep); + + $idIsValid = $actionStepArrayIsValid + && array_key_exists('id', $actionStep) + && is_int($actionStep['id']); + + $nameIsValid = $actionStepArrayIsValid + && array_key_exists('name', $actionStep) + && is_string($actionStep['name']); + + $orderIsValid = $actionStepArrayIsValid + && array_key_exists('order', $actionStep) + && is_int($actionStep['order']); + + $actionIdIsValid = $actionStepArrayIsValid + && array_key_exists('actionId', $actionStep) + && is_int($actionStep['actionId']); + + $tagIdIsValid = $actionStepArrayIsValid + && array_key_exists('tagId', $actionStep) + && (is_int($actionStep['tagId']) || is_null($actionStep['tagId'])); + + $mailboxIdIsValid = $actionStepArrayIsValid + && array_key_exists('mailboxId', $actionStep) + && (is_int($actionStep['mailboxId']) || is_null($actionStep['mailboxId'])); + + $actionStepIsValid = $idIsValid + && $nameIsValid + && $orderIsValid + && $actionIdIsValid + && $tagIdIsValid + && $mailboxIdIsValid; + + $quickStepsArrayIsValid = $quickStepsArrayIsValid && $actionStepIsValid; + } + + return $quickStepsArrayIsValid; + } +} diff --git a/tests/Unit/UserMigration/Service/QuickActionsMigrationServiceTest.php b/tests/Unit/UserMigration/Service/QuickActionsMigrationServiceTest.php new file mode 100644 index 0000000000..ad4be3310e --- /dev/null +++ b/tests/Unit/UserMigration/Service/QuickActionsMigrationServiceTest.php @@ -0,0 +1,190 @@ +output = $this->createMock(OutputInterface::class); + $this->exportDestination = $this->createMock(IExportDestination::class); + $this->importSource = $this->createMock(IImportSource::class); + + $this->user = $this->createMock(IUser::class); + $this->user->method('getUID')->willReturn(self::USER_ID); + + $this->serviceMock = $this->createServiceMock(QuickActionsMigrationService::class); + $this->migrationService = $this->serviceMock->getService(); + } + + public function testExportsMultipleQuickActions(): void { + $quickActions = $this->getQuickActions(); + $this->exportDestination->expects(self::once())->method('addFileContents')->with(QuickActionsMigrationService::QUICK_ACTIONS_FILE, json_encode($quickActions)); + + $this->serviceMock->getParameter('quickActionsService')->method('findAll')->with(self::USER_ID)->willReturn($quickActions); + $this->migrationService->exportQuickActions($this->user, $this->exportDestination, $this->output); + } + + public function testExportsNoQuickActions(): void { + $this->serviceMock->getParameter('quickActionsService')->method('findAll')->with(self::USER_ID)->willReturn([]); + $this->exportDestination->expects(self::once())->method('addFileContents')->with(QuickActionsMigrationService::QUICK_ACTIONS_FILE, json_encode([])); + + $this->migrationService->exportQuickActions($this->user, $this->exportDestination, $this->output); + } + + public function testImportMultipleQuickActions(): void { + $quickActions = $this->getQuickActions(); + $this->importSource->expects(self::once())->method('getFileContents')->with(QuickActionsMigrationService::QUICK_ACTIONS_FILE)->willReturn(json_encode($quickActions)); + + $accountMapping['accounts'] = [5 => rand(100, 199)]; + $accountMapping['mailboxes'] = [5 => rand(200, 299)]; + $tagMapping = [2 => rand(300, 399)]; + + $this->serviceMock->getParameter('quickActionsService')->expects(self::exactly(2))->method('create')->with(self::callback(function ($quickActionName) use ($quickActions): bool { + return !empty(array_filter($quickActions, function ($quickAction) use ($quickActionName): bool { + return $quickAction->getName() === $quickActionName; + })); + }), $accountMapping['accounts'][5])->willReturnCallback(function ($name, $mappedAccount): Actions { + $newActions = new Actions(); + $newActions->setId(rand(400, 499)); + $newActions->setName($name); + $newActions->setAccountId($mappedAccount); + return $newActions; + }); + + $this->serviceMock->getParameter('quickActionsService')->expects(self::exactly(4))->method('createActionStep')->with(self::callback(function ($actionStepName): bool { + return $this->actionStepNameExists($actionStepName); + }), self::callback(function ($actionStepOrder): bool { + return $this->actionStepOrderExists($actionStepOrder); + }), $this->greaterThanOrEqual(400), $this->logicalOr($this->equalTo($tagMapping[2]), $this->isNull()), $this->logicalOr($this->equalTo($accountMapping['mailboxes'][5]), $this->isNull())); + + $this->migrationService->importQuickActions($this->user, $this->importSource, $this->output, $accountMapping, $tagMapping); + } + + public function testImportNoQuickActions(): void { + $this->importSource->expects(self::once())->method('getFileContents')->with(QuickActionsMigrationService::QUICK_ACTIONS_FILE)->willReturn(json_encode([])); + $this->serviceMock->getParameter('quickActionsService')->expects(self::never())->method('create'); + $this->serviceMock->getParameter('quickActionsService')->expects(self::never())->method('createActionStep'); + $this->migrationService->importQuickActions($this->user, $this->importSource, $this->output, [], [], []); + } + + private function getFirstQuickAction(): Actions { + $firstAction = new Actions(); + + $firstAction->setId(self::FIRST_QUICK_STEP_ID); + $firstAction->setAccountId(self::QUICK_STEP_ACCOUNT_ID); + $firstAction->setName('First quick action'); + + $actionSteps = $this->getActionStepsForFirstQuickAction(); + $firstAction->setActionSteps($actionSteps); + + return $firstAction; + } + + private function getActionStepsForFirstQuickAction(): array { + $firstStep = new ActionStep(); + $firstStep->setId(1); + $firstStep->setActionId(self::QUICK_STEP_ACCOUNT_ID); + $firstStep->setName('markAsRead'); + $firstStep->setOrder(1); + + $secondStep = new ActionStep(); + $secondStep->setId(2); + $secondStep->setActionId(self::QUICK_STEP_ACCOUNT_ID); + $secondStep->setMailboxId(5); + $secondStep->setName('moveThread'); + $secondStep->setOrder(2); + + return [$firstStep, $secondStep]; + } + + private function getSecondQuickAction(): Actions { + $secondAction = new Actions(); + + $secondAction->setId(self::SECOND_QUICK_STEP_ID); + $secondAction->setAccountId(self::QUICK_STEP_ACCOUNT_ID); + $secondAction->setName('Second quick action'); + + $actionSteps = $this->getActionStepsForSecondQuickAction(); + $secondAction->setActionSteps($actionSteps); + + return $secondAction; + } + + private function getActionStepsForSecondQuickAction(): array { + $firstStep = new ActionStep(); + $firstStep->setId(3); + $firstStep->setActionId(self::SECOND_QUICK_STEP_ID); + $firstStep->setName('markAsImportant'); + $firstStep->setOrder(1); + + $secondStep = new ActionStep(); + $secondStep->setId(4); + $secondStep->setActionId(self::SECOND_QUICK_STEP_ID); + $secondStep->setTagId(2); + $secondStep->setName('applyTag'); + $secondStep->setOrder(2); + + return [$firstStep, $secondStep]; + } + + private function getQuickActions(): array { + return [$this->getFirstQuickAction(), $this->getSecondQuickAction()]; + } + + private function getActionSteps(): array { + $actionSteps = [$this->getActionStepsForFirstQuickAction(), $this->getActionStepsForSecondQuickAction()]; + $flattenedActionSteps = []; + + array_walk_recursive($actionSteps, function ($item) use (&$flattenedActionSteps) { + $flattenedActionSteps[] = $item; + }); + + return $flattenedActionSteps; + } + + private function actionStepNameExists(string $actionStepName): bool { + $quickActions = $this->getActionSteps(); + + return !empty(array_filter($quickActions, function ($quickAction) use ($actionStepName): bool { + return $quickAction->getName() === $actionStepName; + })); + } + + private function actionStepOrderExists(int $actionStepOrder): bool { + $quickActions = $this->getActionSteps(); + + return !empty(array_filter($quickActions, function ($quickAction) use ($actionStepOrder): bool { + return $quickAction->getOrder() === $actionStepOrder; + })); + } +}