diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 234af609f..42c0fcae7 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -51,6 +51,7 @@ use OCA\Deck\Sharing\DeckShareProvider; use OCA\Deck\Sharing\Listener; use OCA\Deck\Teams\DeckTeamResourceProvider; +use OCA\Deck\UserMigration\DeckMigrator; use OCA\Text\Event\LoadEditor; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -181,6 +182,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(LoadAdditionalScriptsEvent::class, ResourceAdditionalScriptsListener::class); $context->registerTeamResourceProvider(DeckTeamResourceProvider::class); + $context->registerUserMigrator(DeckMigrator::class); } public function registerCommentsEntity(IEventDispatcher $eventDispatcher): void { diff --git a/lib/Db/BoardMapper.php b/lib/Db/BoardMapper.php index 358966819..522ad29d8 100644 --- a/lib/Db/BoardMapper.php +++ b/lib/Db/BoardMapper.php @@ -583,4 +583,9 @@ public function flushCache(?int $boardId = null, ?string $userId = null) { $this->userBoardCache = new CappedMemoryCache(); } } + + public function getDbConnection() { + + return $this->db; + } } diff --git a/lib/Errors/InternalError.php b/lib/Errors/InternalError.php new file mode 100644 index 000000000..a85abd6a9 --- /dev/null +++ b/lib/Errors/InternalError.php @@ -0,0 +1,14 @@ +assignmentServiceValidator = $assignmentServiceValidator; $this->permissionService = $permissionService; @@ -88,6 +95,7 @@ public function __construct( $this->activityManager = $activityManager; $this->eventDispatcher = $eventDispatcher; $this->currentUser = $userId; + $this->logger = $logger; } /** @@ -169,4 +177,26 @@ public function unassignUser(int $cardId, string $userId, int $type = 0): Assign } throw new NotFoundException('No assignment for ' . $userId . 'found.'); } + + /** + * @param int $cardId + * @param array $assignedUser + * + * @return void + * + * @throws InternalError + */ + public function importAssignedUser(int $cardId, array $assignedUser): void { + $newAssignedUser = new Assignment(); + $newAssignedUser->setCardId($cardId); + $newAssignedUser->setParticipant($assignedUser['participant']['uid']); + $newAssignedUser->setType($assignedUser['type']); + + try { + $this->assignedUsersMapper->insert($newAssignedUser); + } catch (\Exception $e) { + $this->logger->error('importAssignedUser insert error: ' . $e->getMessage()); + throw new InternalError('importAssignedUser insert error: ' . $e->getMessage()); + } + } } diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index faf7102bc..c04afad3b 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -27,6 +27,7 @@ use OCA\Deck\Db\SessionMapper; use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; +use OCA\Deck\Errors\InternalError; use OCA\Deck\Event\AclCreatedEvent; use OCA\Deck\Event\AclDeletedEvent; use OCA\Deck\Event\AclUpdatedEvent; @@ -52,6 +53,7 @@ use OCP\Server; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; class BoardService { private ?array $boardsCacheFull = null; @@ -83,6 +85,7 @@ public function __construct( private ISecureRandom $random, private ConfigService $configService, private ?string $userId, + private LoggerInterface $logger, ) { } @@ -848,4 +851,53 @@ private function enrichWithCards(Board $board): void { $board->setStacks($stacks); } + + /** + * @param array $board + * @param string $userId + * + * @return Board + * + * @throws InternalError + */ + public function importBoard(array $board, string $userId): Board { + $item = new Board(); + $item->setTitle($board['title']); + $item->setOwner($userId); + $item->setColor($board['color']); + $item->setArchived((bool)$board['archived']); + $item->setDeletedAt($board['deletedAt']); + $item->setLastModified($board['lastModified']); + try { + $newBoard = $this->boardMapper->insert($item); + } catch (\Exception $e) { + $this->logger->error('importBoard insert error: ' . $e->getMessage()); + throw new InternalError('importBoard insert error: ' . $e->getMessage()); + } + return $newBoard; + } + + /** + * @param Board $board + * @param array $acl + * + * @return void + * + * @throws InternalError + */ + public function importAcl(Board $board, array $acl): void { + $aclEntity = new Acl(); + $aclEntity->setBoardId($board->getId()); + $aclEntity->setType((int)$acl['type']); + $aclEntity->setParticipant($acl['participant']); + $aclEntity->setPermissionEdit((bool)$acl['permissionEdit']); + $aclEntity->setPermissionShare((bool)$acl['permissionShare']); + $aclEntity->setPermissionManage((bool)$acl['permissionManage']); + try { + $this->aclMapper->insert($aclEntity); + } catch (\Exception $e) { + $this->logger->error('importAcl insert error: ' . $e->getMessage()); + throw new InternalError('importAcl insert error: ' . $e->getMessage()); + } + } } diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 8f0406700..155dc4587 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -20,6 +20,7 @@ use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\StackMapper; +use OCA\Deck\Errors\InternalError; use OCA\Deck\Event\CardCreatedEvent; use OCA\Deck\Event\CardDeletedEvent; use OCA\Deck\Event\CardUpdatedEvent; @@ -640,4 +641,57 @@ public function getCardUrl(int $cardId): string { public function getRedirectUrlForCard(int $cardId): string { return $this->urlGenerator->linkToRouteAbsolute('deck.page.redirectToCard', ['cardId' => $cardId]); } + + /** + * @param int $stackId + * @param array $card + * + * @return int + * + * @throws InternalError + */ + public function importCard(int $stackId, array $card): int { + $item = new Card(); + $item->setStackId($stackId); + $item->setTitle($card['title']); + $item->setType($card['type']); + $item->setOrder($card['order']); + $item->setOwner($card['owner']); + $item->setDescription($card['description']); + $item->setDuedate($card['duedate']); + $item->setLastModified($card['lastModified']); + $item->setLastEditor($card['lastEditor']); + $item->setCreatedAt($card['createdAt']); + $item->setArchived($card['archived']); + $item->setDeletedAt($card['deletedAt']); + $item->setDone($card['done']); + $item->setNotified($card['notified']); + + try { + $newCard = $this->cardMapper->insert($item); + } catch (\Exception $e) { + $this->logger->error('importCard insert error: ' . $e->getMessage()); + throw new InternalError('importCard insert error: ' . $e->getMessage()); + } + + return $newCard->getId(); + } + + /** + * @param int $cardId + * @param int $boardId + * @param array $importedLabel + * + * @return void + */ + public function importLabels(int $cardId, int $boardId, array $importedLabel): void { + $labels = $this->labelMapper->findAll($boardId); + + foreach ($labels as $label) { + if ($label->getTitle() === $importedLabel['title'] && $label->getColor() === $importedLabel['color']) { + $this->cardMapper->assignLabel($cardId, $label->getId()); + } + } + $this->changeHelper->cardChanged($cardId); + } } diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index 72a48c66c..9c3adec57 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -7,10 +7,12 @@ namespace OCA\Deck\Service; +use OC\Comments\Comment; use OCA\Deck\AppInfo\Application; use OCA\Deck\BadRequestException; use OCA\Deck\Db\Acl; use OCA\Deck\Db\CardMapper; +use OCA\Deck\Errors\InternalError; use OCA\Deck\NoPermissionException; use OCA\Deck\NotFoundException; use OCP\AppFramework\Http\DataResponse; @@ -188,4 +190,54 @@ private function formatComment(IComment $comment, bool $addReplyTo = false): arr } return $formattedComment; } + + public function exportAllForCard(int $cardId, int $limit = 1000, int $offset = 0): array { + $comments = $this->commentsManager->getForObject( + Application::COMMENT_ENTITY_TYPE, + (string)$cardId, + $limit, + $offset + ); + $allComments = []; + foreach ($comments as $comment) { + $formattedComment = $this->formatComment($comment); + try { + if ($comment->getParentId() !== '0' && $replyTo = $this->commentsManager->get($comment->getParentId())) { + $formattedComment['replyTo'] = $this->formatComment($replyTo); + } + } catch (CommentNotFoundException $e) { + } + $allComments[] = $formattedComment; + } + return $allComments; + } + + /** + * @param int $cardId + * @param array $comment + * @param string $parentId + * + * @return int + */ + public function importComment(int $cardId, array $comment, string $parentId = '0'): int { + try { + $newComment = $this->commentsManager->create( + $comment['actorType'], + $comment['actorId'], + Application::COMMENT_ENTITY_TYPE, + (string)$cardId + ); + $newComment->setMessage($comment['message']); + $newComment->setObject(Application::COMMENT_ENTITY_TYPE, (string)$cardId); + $newComment->setVerb('comment'); + $newComment->setParentId($parentId); + $newComment->setActor($comment['actorType'], $comment['actorId']); + $newComment->setCreationDateTime(new \DateTime($comment['creationDateTime'])); + $this->commentsManager->save($newComment); + } catch (\Exception $e) { + $this->logger->error('importComment insert error: ' . $e->getMessage()); + throw new InternalError('importComment insert error: ' . $e->getMessage()); + } + return (int)$newComment->getId(); + } } diff --git a/lib/Service/FilesAppService.php b/lib/Service/FilesAppService.php index 931668d03..349e08a82 100644 --- a/lib/Service/FilesAppService.php +++ b/lib/Service/FilesAppService.php @@ -9,10 +9,12 @@ namespace OCA\Deck\Service; +use Doctrine\DBAL\Connection; use OCA\Deck\BadRequestException; use OCA\Deck\Db\Acl; use OCA\Deck\Db\Attachment; use OCA\Deck\Db\CardMapper; +use OCA\Deck\Errors\InternalError; use OCA\Deck\NoPermissionException; use OCA\Deck\Sharing\DeckShareProvider; use OCA\Deck\StatusException; @@ -339,4 +341,70 @@ private function validateFilename(string $fileName): void { throw new BadRequestException($errorMessage); } } + + /** + * @param int[] $cardIds + * @return array + */ + public function getAllDeckSharesForCards(array $cardIds): array { + $qb = $this->connection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(12))) + ->andWhere($qb->expr()->in('share_with', $qb->createParameter('cardIds'))); + /** @psalm-suppress UndefinedClass */ + $qb->setParameter('cardIds', $cardIds, Connection::PARAM_STR_ARRAY); + + return $qb->executeQuery()->fetchAllAssociative(); + } + + /** + * @param int $cardId + * @param array $shareData + * @param string $userId + * + * @return void + * + * @throws InternalError + */ + public function importDeckSharesForCard(int $cardId, array $shareData, int $fileId, string $userId): void { + try { + $insert = [ + 'share_type' => $shareData['share_type'] ?? 12, + 'share_with' => (string)$cardId, + 'uid_owner' => $userId, + 'uid_initiator' => $userId, + 'file_source' => $fileId, + 'file_target' => $shareData['file_target'], + 'permissions' => $shareData['permissions'] ?? 1, + 'stime' => $shareData['stime'] ?? time(), + 'parent' => $shareData['parent'], + 'item_type' => $shareData['item_type'] ?? 'file', + 'item_source' => $fileId, + 'item_target' => $shareData['item_target'], + 'token' => $shareData['token'], + 'mail_send' => $shareData['mail_send'], + 'share_name' => $shareData['share_name'], + 'note' => $shareData['note'], + 'label' => $shareData['label'], + 'hide_download' => $shareData['hide_download'], + 'expiration' => $shareData['expiration'], + 'accepted' => $shareData['accepted'], + 'password' => $shareData['password'], + 'password_by_talk' => $shareData['password_by_talk'], + 'attributes' => $shareData['attributes'], + 'password_expiration_time' => $shareData['password_expiration_time'], + 'reminder_sent' => $shareData['reminder_sent'], + ]; + $qb = $this->connection->getQueryBuilder(); + $qb->insert('share'); + foreach ($insert as $key => $value) { + $qb->setValue($key, $qb->createNamedParameter($value)); + } + $qb->executeStatement(); + } catch (\Throwable $e) { + $this->logger->error('importDeckSharesForCard insert error: ' . $e->getMessage()); + throw new InternalError('importDeckSharesForCard insert error: ' . $e->getMessage()); + } + } } diff --git a/lib/Service/LabelService.php b/lib/Service/LabelService.php index 309100c74..859313f0f 100644 --- a/lib/Service/LabelService.php +++ b/lib/Service/LabelService.php @@ -9,11 +9,14 @@ use OCA\Deck\BadRequestException; use OCA\Deck\Db\Acl; +use OCA\Deck\Db\Board; use OCA\Deck\Db\ChangeHelper; use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; +use OCA\Deck\Errors\InternalError; use OCA\Deck\StatusException; use OCA\Deck\Validators\LabelServiceValidator; +use Psr\Log\LoggerInterface; class LabelService { @@ -23,6 +26,7 @@ public function __construct( private BoardService $boardService, private ChangeHelper $changeHelper, private LabelServiceValidator $labelServiceValidator, + private LoggerInterface $logger, ) { } @@ -136,4 +140,26 @@ public function update(int $id, string $title, string $color): Label { return $this->labelMapper->update($label); } + + /** + * @param Board $board + * @param array $label + * + * @return void + * + * @throws InternalError + */ + public function importBoardLabel(Board $board, array $label): void { + $labelEntity = new Label(); + $labelEntity->setBoardId($board->getId()); + $labelEntity->setTitle($label['title']); + $labelEntity->setColor($label['color']); + $labelEntity->setLastModified($label['lastModified']); + try { + $this->labelMapper->insert($labelEntity); + } catch (\Throwable $e) { + $this->logger->error('importBoardLabel insert error: ' . $e->getMessage()); + throw new InternalError('importBoardLabel insert error: ' . $e->getMessage()); + } + } } diff --git a/lib/Service/StackService.php b/lib/Service/StackService.php index c2aed66e5..8ef5261b8 100644 --- a/lib/Service/StackService.php +++ b/lib/Service/StackService.php @@ -19,6 +19,7 @@ use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; +use OCA\Deck\Errors\InternalError; use OCA\Deck\Event\BoardUpdatedEvent; use OCA\Deck\Model\CardDetails; use OCA\Deck\NoPermissionException; @@ -304,4 +305,29 @@ public function reorder(int $id, int $order): array { return $result; } + + /** + * @param int $boardId + * @param array $stack + * + * @return int + * + * @throws InternalError + */ + public function importStack(int $boardId, array $stack): int { + $item = new Stack(); + $item->setBoardId($boardId); + $item->setTitle($stack['title']); + $item->setDeletedAt($stack['deletedAt']); + $item->setLastModified($stack['lastModified']); + $item->setOrder($stack['order']); + try { + $newStack = $this->stackMapper->insert($item); + } catch (\Exception $e) { + $this->logger->error('importStack insert error: ' . $e->getMessage()); + throw new InternalError('importStack insert error: ' . $e->getMessage()); + } + + return $newStack->getId(); + } } diff --git a/lib/UserMigration/DeckMigrator.php b/lib/UserMigration/DeckMigrator.php new file mode 100644 index 000000000..abc56f162 --- /dev/null +++ b/lib/UserMigration/DeckMigrator.php @@ -0,0 +1,476 @@ +getUID(); + + try { + $boards = $this->boardMapper->findAllByUser($userId); + $exportDestination->addFileContents(self::BOARDS, json_encode($boards)); + + $boardAcls = []; + $boardLabels = []; + $boardStacks = []; + $stackCards = []; + $cardComments = []; + $cardAttachments = []; + foreach ($boards as $board) { + $acls = $this->aclMapper->findAll($board->getId()); + foreach ($acls as $acl) { + $boardAcls[] = $acl; + } + + $labels = $this->labelMapper->findAll($board->getId()); + $boardLabels = array_merge($boardLabels, $labels); + + [$newStacks, $newCards, $newComments] = $this->exportStacksCardsCommentsSimple($board->getId()); + $boardStacks = array_merge($boardStacks, $newStacks); + $stackCards = array_merge($stackCards, $newCards); + $cardComments = array_merge($cardComments, $newComments); + } + + $exportDestination->addFileContents(self::BOARD_ACLS, json_encode($boardAcls)); + $exportDestination->addFileContents(self::BOARD_LABELS, json_encode($boardLabels)); + $exportDestination->addFileContents(self::BOARD_STACKS, json_encode($boardStacks)); + $exportDestination->addFileContents(self::STACK_CARDS, json_encode($stackCards)); + usort($cardComments, function ($firstComment, $secondComment) { + return ($firstComment['id'] ?? 0) <=> ($secondComment['id'] ?? 0); + }); + $exportDestination->addFileContents(self::CARD_COMMENTS, json_encode($cardComments)); + + $cardIds = array_map(fn ($c) => $c->getId(), $stackCards); + $cardAssignments = $this->assignmentMapper->findIn($cardIds); + $exportDestination->addFileContents(self::CARD_ASSIGNED, json_encode($cardAssignments)); + + $cardLabels = $this->labelMapper->findAssignedLabelsForCards($cardIds); + $exportDestination->addFileContents(self::CARD_LABELS, json_encode($cardLabels)); + + $cardAttachments = $this->filesAppService->getAllDeckSharesForCards($cardIds); + $exportDestination->addFileContents(self::CARD_ATTACHMENTS, json_encode($cardAttachments)); + + $this->exportCardAttachmentsFiles($cardAttachments, $userId, $exportDestination); + + $output->writeln('Export completed.'); + } catch (\Throwable $e) { + throw new InternalError('Deck export error: ' . $e->getMessage()); + } + + } + + /** + * @param array $cardAttachments + * @param string $userId + * @param IExportDestination $exportDestination + * + * @return void + */ + private function exportCardAttachmentsFiles(array $cardAttachments, string $userId, IExportDestination $exportDestination): void { + foreach ($cardAttachments as $share) { + if (!empty($share['file_target'])) { + $fileTarget = $share['file_target']; + $fileTargetClean = str_replace('{DECK_PLACEHOLDER}/', '', ltrim($fileTarget, '/')); + if (strpos($fileTargetClean, 'Deck/') !== 0) { + $fileTargetClean = 'Deck/' . $fileTargetClean; + } + $baseDataDir = $this->config->getSystemValue('datadirectory', '/var/www/html/data'); + $username = $share['uid_owner'] ?? $userId; + $filePath = $baseDataDir . '/' . $username . '/files/' . $fileTargetClean; + if (is_readable($filePath)) { + $fileContents = file_get_contents($filePath); + $exportPath = 'files/' . $fileTargetClean; + $exportDestination->addFileContents($exportPath, $fileContents); + } + } + } + } + + /** + * @param int $boardId + * + * @return array[] [$stacks, $cards, $comments] + */ + private function exportStacksCardsCommentsSimple(int $boardId): array { + $stacks = $this->stackMapper->findAll($boardId); + $allBoardCards = []; + $allComments = []; + foreach ($stacks as $stack) { + $stackUnarchivedCards = $this->cardMapper->findAll($stack->getId()); + $stackArchivedCards = $this->cardMapper->findAllArchived($stack->getId()); + $allBoardCards = array_merge($allBoardCards, $stackUnarchivedCards, $stackArchivedCards); + $stackAllCards = array_merge($stackUnarchivedCards, $stackArchivedCards); + foreach ($stackAllCards as $card) { + $comments = $this->commentService->exportAllForCard($card->getId()); + $allComments = array_merge($allComments, $comments); + } + } + return [$stacks, $allBoardCards, $allComments]; + } + + /** + * {@inheritDoc} + */ + public function import( + IUser $user, + IImportSource $importSource, + OutputInterface $output, + ): void { + if ($importSource->getMigratorVersion($this->getId()) === null) { + $output->writeln('No version for migrator ' . $this->getId() . ' (' . static::class . '), skipping import…'); + return; + } + $output->writeln('Importing boards, cards, comments, shares...'); + + $boards = json_decode($importSource->getFileContents(self::BOARDS), true, self::JSON_DEPTH, self::JSON_OPTIONS); + $userId = $user->getUID(); + $connection = $this->boardMapper->getDbConnection(); + $connection->beginTransaction(); + + try { + $boardIdMap = []; + $cardIdMap = []; + $stackIdMap = []; + $commentIdMap = []; + foreach ($boards as $board) { + $newBoard = $this->boardService->importBoard($board, $userId); + $boardIdMap[$board['id']] = $newBoard->getId(); + + $this->importBoardLabels($importSource, $newBoard, $board['id']); + $this->importBoardAcls($importSource, $newBoard, $board['id']); + } + + $this->importBoardStacks($importSource, $boardIdMap, $stackIdMap); + $this->importStackCards($importSource, $stackIdMap, $cardIdMap); + $this->importCardLabels($importSource, $cardIdMap, $boardIdMap); + $this->importCardAssignments($importSource, $cardIdMap); + $this->importCardComments($importSource, $cardIdMap, $commentIdMap); + $this->importCardAttachment($importSource, $userId, $cardIdMap); + + + $connection->commit(); + } catch (\Throwable $e) { + $connection->rollBack(); + throw $e; + } + + $output->writeln('Import completed.'); + } + + /** + * @param IImportSource $importSource + * @param string $userId + * @param array $cardIdMap + * + * @return void + */ + private function importCardAttachment(IImportSource $importSource, string $userId, array $cardIdMap): void { + $cardAttachments = json_decode($importSource->getFileContents(self::CARD_ATTACHMENTS), true); + $userFolder = $this->rootFolder->getUserFolder($userId); + try { + $deckFolder = $userFolder->get('Deck'); + } catch (NotFoundException $e) { + $deckFolder = $userFolder->newFolder('Deck'); + } + + foreach ($cardAttachments as $share) { + $this->importDeckAttachment($share, $deckFolder, $importSource, $cardIdMap, $userId); + } + } + + /** + * @param array $share + * @param Folder $deckFolder + * @param IImportSource $importSource + * @param array $cardIdMap + * @param string $userId + */ + private function importDeckAttachment( + array $share, + Folder $deckFolder, + IImportSource $importSource, + array $cardIdMap, + string $userId, + ): void { + $fileTarget = $share['file_target']; + $fileTargetClean = str_replace('{DECK_PLACEHOLDER}/', '', ltrim($fileTarget, '/')); + if (strpos($fileTargetClean, 'Deck/') !== 0) { + $fileTargetClean = 'Deck/' . $fileTargetClean; + } + $importPath = 'files/' . $fileTargetClean; + $relativePath = substr($fileTargetClean, strlen('Deck/')); + $parts = explode('/', $relativePath); + if (empty($parts)) { + return; + } + + $currentFolder = $this->traverseOrCreateFolders($deckFolder, $parts); + $fileName = array_pop($parts); + if ($fileName === null) { + return; + } + + $fileId = null; + if ($currentFolder->nodeExists($fileName)) { + $file = $currentFolder->get($fileName); + $fileId = $file->getId(); + } else { + try { + $fileContents = $importSource->getFileContents($importPath); + } catch (\Throwable $e) { + return; + } + $file = $currentFolder->newFile($fileName); + $file->putContent($fileContents); + $fileId = $file->getId(); + } + + $newCardId = $cardIdMap[$share['share_with']] ?? $share['share_with']; + $this->filesAppService->importDeckSharesForCard($newCardId, $share, $fileId, $userId); + } + + /** + * @param Folder $baseFolder + * @param array $parts + * @return Folder + */ + private function traverseOrCreateFolders(Folder $baseFolder, array $parts): Folder { + $currentFolder = $baseFolder; + for ($i = 0, $n = count($parts) - 1; $i < $n; $i++) { + $part = $parts[$i]; + try { + $currentFolder = $currentFolder->get($part); + } catch (NotFoundException $e) { + $currentFolder = $currentFolder->newFolder($part); + } + } + return $currentFolder; + } + + /** + * @param IImportSource $importSource + * @param Board $newBoard + * @param int $oldBoardId + * + * @return void + */ + private function importBoardLabels(IImportSource $importSource, Board $newBoard, int $oldBoardId): void { + $boardLabels = json_decode($importSource->getFileContents(self::BOARD_LABELS), true); + foreach ($boardLabels as $label) { + if ($label['boardId'] === $oldBoardId) { + $this->labelService->importBoardLabel($newBoard, $label); + } + } + } + + /** + * @param IImportSource $importSource + * @param Board $newBoard + * @param int $oldBoardId + * + * @return void + */ + private function importBoardAcls(IImportSource $importSource, Board $newBoard, int $oldBoardId): void { + $boardAcls = json_decode($importSource->getFileContents(self::BOARD_ACLS), true); + foreach ($boardAcls as $acl) { + if ($acl['boardId'] === $oldBoardId) { + $this->boardService->importAcl($newBoard, $acl); + } + } + } + + /** + * @param IImportSource $importSource + * @param array $boardIdMap + * @param array $stackIdMap + * + * @return void + */ + private function importBoardStacks(IImportSource $importSource, array $boardIdMap, array &$stackIdMap): void { + $stacks = json_decode($importSource->getFileContents(self::BOARD_STACKS), true, self::JSON_DEPTH, self::JSON_OPTIONS); + foreach ($stacks as $stack) { + $newStackId = $this->stackService->importStack($boardIdMap[$stack['boardId']], $stack); + $stackIdMap[$stack['id']] = $newStackId; + } + } + + /** + * @param IImportSource $importSource + * @param array $stackIdMap + * @param array $cardIdMap + * + * @return void + */ + private function importStackCards(IImportSource $importSource, array $stackIdMap, array &$cardIdMap): void { + $stackCards = json_decode($importSource->getFileContents(self::STACK_CARDS), true); + foreach ($stackCards as $card) { + if (isset($stackIdMap[$card['stackId']])) { + $newCardId = $this->cardService->importCard($stackIdMap[$card['stackId']], $card); + $cardIdMap[$card['id']] = $newCardId; + } + } + } + + /** + * @param IImportSource $importSource + * @param array $cardIdMap + * @param array $boardIdMap + * + * @return void + */ + private function importCardLabels(IImportSource $importSource, array $cardIdMap, array $boardIdMap): void { + $cardLabels = json_decode($importSource->getFileContents(self::CARD_LABELS), true); + foreach ($cardLabels as $label) { + $newCardId = $cardIdMap[$label['cardId']] ?? null; + $newBoardId = $boardIdMap[$label['boardId']] ?? null; + if ($newCardId && $newBoardId) { + $this->cardService->importLabels($newCardId, $newBoardId, $label); + } + } + } + + /** + * @param IImportSource $importSource + * @param array $cardIdMap + * @param array $commentIdMap + * + * @return void + */ + private function importCardComments(IImportSource $importSource, array $cardIdMap, array $commentIdMap): void { + $cardComments = json_decode($importSource->getFileContents(self::CARD_COMMENTS), true); + + foreach ($cardComments as $comment) { + $cardId = $cardIdMap[$comment['objectId']] ?? null; + $parentId = '0'; + $replyToId = $comment['replyTo']['id'] ?? null; + if (isset($replyToId) && isset($commentIdMap[$replyToId])) { + $parentId = (string)$commentIdMap[$replyToId]; + } + $newCommentId = $this->commentService->importComment($cardId, $comment, $parentId); + $commentIdMap[$comment['id']] = $newCommentId; + } + } + + /** + * @param IImportSource $importSource + * @param array $cardIdMap + * + * @return void + */ + private function importCardAssignments(IImportSource $importSource, array $cardIdMap): void { + $cardAssignedUsers = json_decode($importSource->getFileContents(self::CARD_ASSIGNED), true); + foreach ($cardAssignedUsers as $assignedUser) { + if (isset($cardIdMap[$assignedUser['cardId']])) { + $this->assignmentService->importAssignedUser($cardIdMap[$assignedUser['cardId']], $assignedUser); + } + } + } + + /** + * {@inheritDoc} + */ + public function getId(): string { + return 'decks'; + } + + /** + * {@inheritDoc} + */ + public function getDisplayName(): string { + return $this->l10n->t('Decks'); + } + + /** + * {@inheritDoc} + */ + public function getDescription(): string { + return $this->l10n->t('All Boards, cards, comments, shares, and relations'); + } +} diff --git a/tests/unit/DeckMigratorTest.php b/tests/unit/DeckMigratorTest.php new file mode 100644 index 000000000..31d44e141 --- /dev/null +++ b/tests/unit/DeckMigratorTest.php @@ -0,0 +1,141 @@ +rootFolder = $this->createMock(IRootFolder::class); + $this->l10n = $this->createMock(IL10N::class); + $this->userFolder = $this->createMock(Folder::class); + $this->userFolder + ->method('getPath') + ->willReturn('/tmp/testuser'); + $this->rootFolder + ->method('getUserFolder') + ->willReturn($this->userFolder); + + $mockBoardService = $this->createMock(BoardService::class); + $mockBoardMapper = $this->createMock(BoardMapper::class); + $mockConnection = $this->getMockBuilder('stdClass') + ->addMethods(['beginTransaction', 'commit', 'rollBack']) + ->getMock(); + $mockConnection->method('beginTransaction')->willReturn(null); + $mockConnection->method('commit')->willReturn(null); + $mockConnection->method('rollBack')->willReturn(null); + $mockBoardMapper->method('getDbConnection')->willReturn($mockConnection); + $mockAclMapper = $this->createMock(AclMapper::class); + $mockLabelMapper = $this->createMock(LabelMapper::class); + $mockStackMapper = $this->createMock(StackMapper::class); + $mockCardMapper = $this->createMock(CardMapper::class); + $mockAssignmentMapper = $this->createMock(AssignmentMapper::class); + $mockCommentService = $this->createMock(CommentService::class); + $mockFilesAppService = $this->createMock(FilesAppService::class); + $mockConfig = $this->createMock(IConfig::class); + $mockStackService = $this->createMock(StackService::class); + $mockLabelService = $this->createMock(LabelService::class); + $mockCardService = $this->createMock(CardService::class); + $mockAssignmentService = $this->createMock(AssignmentService::class); + $mockLogger = $this->createMock(LoggerInterface::class); + + $this->deckMigrator = new DeckMigrator( + $this->l10n, + $this->rootFolder, + $mockBoardService, + $mockBoardMapper, + $mockAclMapper, + $mockLabelMapper, + $mockStackMapper, + $mockCardMapper, + $mockAssignmentMapper, + $mockCommentService, + $mockFilesAppService, + $mockConfig, + $mockStackService, + $mockLabelService, + $mockCardService, + $mockAssignmentService, + $mockLogger + ); + } + + public function testGetEstimatedExportSize(): void { + $user = $this->createMock(IUser::class); + $this->assertIsNumeric($this->deckMigrator->getEstimatedExportSize($user)); + } + + public function testExport(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('testuser'); + $exportDestination = $this->createMock(IExportDestination::class); + $output = $this->createMock(OutputInterface::class); + + $this->deckMigrator->export($user, $exportDestination, $output); + } + + public function testImport(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('testuser'); + $importSource = $this->createMock(IImportSource::class); + $output = $this->createMock(OutputInterface::class); + + $importSource + ->method('getMigratorVersion') + ->with('decks') + ->willReturn(1); + + $importSource + ->method('getFileContents') + ->willReturnMap([ + ['boards.json', '[]'], + ['board_labels.json', '[]'], + ['board_acl.json', '[]'], + ['board_stacks.json', '[]'], + ['stack_cards.json', '[]'], + ['card_labels.json', '[]'], + ['card_assigned.json', '[]'], + ['card_comments.json', '[]'], + ['card_attachments.json', '[]'], + ]); + + $this->deckMigrator->import($user, $importSource, $output); + } +}