From 77bd39621eee0b549cf0261b6ebd1a5761c5b113 Mon Sep 17 00:00:00 2001 From: samin-z Date: Mon, 9 Mar 2026 12:04:18 +0100 Subject: [PATCH 1/6] add function for import/export Signed-off-by: samin-z --- lib/Db/BoardMapper.php | 5 +++ lib/Service/AssignmentService.php | 28 +++++++++++++ lib/Service/BoardService.php | 50 +++++++++++++++++++++++ lib/Service/CardService.php | 54 +++++++++++++++++++++++++ lib/Service/CommentService.php | 47 ++++++++++++++++++++++ lib/Service/FilesAppService.php | 66 +++++++++++++++++++++++++++++++ lib/Service/LabelService.php | 24 +++++++++++ lib/Service/StackService.php | 26 ++++++++++++ 8 files changed, 300 insertions(+) diff --git a/lib/Db/BoardMapper.php b/lib/Db/BoardMapper.php index 3589668190..522ad29d89 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/Service/AssignmentService.php b/lib/Service/AssignmentService.php index 4d9a97486d..a4b663c90e 100644 --- a/lib/Service/AssignmentService.php +++ b/lib/Service/AssignmentService.php @@ -23,6 +23,8 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\EventDispatcher\IEventDispatcher; +use Psr\Log\LoggerInterface; +use Symfony\Component\CssSelector\Exception\InternalErrorException; class AssignmentService { @@ -64,6 +66,10 @@ class AssignmentService { * @var AssignmentServiceValidator */ private $assignmentServiceValidator; + /** + * @var LoggerInterface + */ + private $logger; public function __construct( @@ -77,6 +83,7 @@ public function __construct( IEventDispatcher $eventDispatcher, AssignmentServiceValidator $assignmentServiceValidator, $userId, + LoggerInterface $logger, ) { $this->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,24 @@ 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 + */ + 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 InternalErrorException('importAssignedUser insert error: ' . $e->getMessage()); + } + } } diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index faf7102bc1..4b39ceaf91 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -52,6 +52,8 @@ use OCP\Server; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\CssSelector\Exception\InternalErrorException; 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,51 @@ private function enrichWithCards(Board $board): void { $board->setStacks($stacks); } + + /** + * @param array $board + * @param string $userId + * + * @return Board + * + * @throws InternalErrorException + */ + 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 InternalErrorException('importBoard insert error: ' . $e->getMessage()); + } + return $newBoard; + } + + /** + * @param Board $board + * @param array $acl + * + * @return void + */ + 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 InternalErrorException('importAcl insert error: ' . $e->getMessage()); + } + } } diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 8f0406700d..08dd56671d 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -36,6 +36,7 @@ use OCP\IURLGenerator; use OCP\IUserManager; use Psr\Log\LoggerInterface; +use Symfony\Component\CssSelector\Exception\InternalErrorException; class CardService { public function __construct( @@ -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 Card + * + * @throws InternalErrorException + */ + public function importCard (int $stackId, array $card): Card { + $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->isArchived($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 InternalErrorException('importCard insert error: ' . $e->getMessage()); + } + + return $newCard; + } + + /** + * @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 72a48c66c8..550ee56da1 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -7,6 +7,7 @@ namespace OCA\Deck\Service; +use OC\Comments\Comment; use OCA\Deck\AppInfo\Application; use OCA\Deck\BadRequestException; use OCA\Deck\Db\Acl; @@ -21,6 +22,7 @@ use OCP\IUserManager; use OutOfBoundsException; use Psr\Log\LoggerInterface; +use Symfony\Component\CssSelector\Exception\InternalErrorException; class CommentService { @@ -188,4 +190,49 @@ 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 = new Comment(); + $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 InternalErrorException('importComment insert error: ' . $e->getMessage()); + } + return (int)$newComment->getId(); + } } diff --git a/lib/Service/FilesAppService.php b/lib/Service/FilesAppService.php index 931668d037..76d0a45785 100644 --- a/lib/Service/FilesAppService.php +++ b/lib/Service/FilesAppService.php @@ -33,6 +33,7 @@ use OCP\Share\IManager; use OCP\Share\IShare; use Psr\Log\LoggerInterface; +use Symfony\Component\CssSelector\Exception\InternalErrorException; class FilesAppService implements IAttachmentService, ICustomAttachmentService { /** @@ -339,4 +340,69 @@ 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'))); + $qb->setParameter('cardIds', $cardIds, Connection::PARAM_STR_ARRAY); + + $sql = $qb->getSQL(); + $params = $qb->getParameters(); + $shares = $qb->executeQuery()->fetchAllAssociative(); + return $shares; + } + + /** + * @param int $cardId + * @param array $shareData + * @param string $userId + * @return bool + */ + 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 InternalErrorException('importDeckSharesForCard insert error: ' . $e->getMessage()); + } + } } diff --git a/lib/Service/LabelService.php b/lib/Service/LabelService.php index 309100c748..5611c4f542 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\StatusException; use OCA\Deck\Validators\LabelServiceValidator; +use Psr\Log\LoggerInterface; +use Symfony\Component\CssSelector\Exception\InternalErrorException; class LabelService { @@ -23,6 +26,7 @@ public function __construct( private BoardService $boardService, private ChangeHelper $changeHelper, private LabelServiceValidator $labelServiceValidator, + private LoggerInterface $logger, ) { } @@ -136,4 +140,24 @@ public function update(int $id, string $title, string $color): Label { return $this->labelMapper->update($label); } + + /** + * @param Board $board + * @param array $$label + * + * @return void + */ + 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 InternalErrorException('importBoardLabel insert error: ' . $e->getMessage()); + } + } } diff --git a/lib/Service/StackService.php b/lib/Service/StackService.php index c2aed66e57..dc5ff4ee16 100644 --- a/lib/Service/StackService.php +++ b/lib/Service/StackService.php @@ -26,6 +26,7 @@ use OCA\Deck\Validators\StackServiceValidator; use OCP\EventDispatcher\IEventDispatcher; use Psr\Log\LoggerInterface; +use Symfony\Component\CssSelector\Exception\InternalErrorException; class StackService { private StackMapper $stackMapper; @@ -304,4 +305,29 @@ public function reorder(int $id, int $order): array { return $result; } + + /** + * @param int $boardId + * @param array $stack + * + * @return Stack + * + * @throws InternalErrorException + */ + public function importStack(int $boardId, array $stack): Stack { + $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 InternalErrorException('importStack insert error: ' . $e->getMessage()); + } + + return $newStack; + } } From 34b098d54bb3c586debe7b373883d7db9aa04bc9 Mon Sep 17 00:00:00 2001 From: samin-z Date: Mon, 16 Mar 2026 18:15:44 +0100 Subject: [PATCH 2/6] update services Signed-off-by: samin-z --- lib/Errors/InternalError.php | 16 ++++++++++++++++ lib/Service/AssignmentService.php | 6 ++++-- lib/Service/BoardService.php | 5 ++++- lib/Service/CardService.php | 14 +++++++------- lib/Service/CommentService.php | 3 ++- lib/Service/FilesAppService.php | 8 ++++++-- lib/Service/LabelService.php | 8 +++++--- lib/Service/StackService.php | 12 ++++++------ 8 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 lib/Errors/InternalError.php diff --git a/lib/Errors/InternalError.php b/lib/Errors/InternalError.php new file mode 100644 index 0000000000..8790991b41 --- /dev/null +++ b/lib/Errors/InternalError.php @@ -0,0 +1,16 @@ +assignedUsersMapper->insert($newAssignedUser); } catch (\Exception $e) { $this->logger->error('importAssignedUser insert error: ' . $e->getMessage()); - throw new InternalErrorException('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 4b39ceaf91..8d30ac743d 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; @@ -882,6 +883,8 @@ public function importBoard(array $board, string $userId): Board { * @param array $acl * * @return void + * + * @throws InternalError */ public function importAcl(Board $board, array $acl): void { $aclEntity = new Acl(); @@ -895,7 +898,7 @@ public function importAcl(Board $board, array $acl): void { $this->aclMapper->insert($aclEntity); } catch (\Exception $e) { $this->logger->error('importAcl insert error: ' . $e->getMessage()); - throw new InternalErrorException('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 08dd56671d..8244f13962 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; @@ -36,7 +37,6 @@ use OCP\IURLGenerator; use OCP\IUserManager; use Psr\Log\LoggerInterface; -use Symfony\Component\CssSelector\Exception\InternalErrorException; class CardService { public function __construct( @@ -646,11 +646,11 @@ public function getRedirectUrlForCard(int $cardId): string { * @param int $stackId * @param array $card * - * @return Card + * @return int * - * @throws InternalErrorException + * @throws InternalError */ - public function importCard (int $stackId, array $card): Card { + public function importCard (int $stackId, array $card): int { $item = new Card(); $item->setStackId($stackId); $item->setTitle($card['title']); @@ -662,7 +662,7 @@ public function importCard (int $stackId, array $card): Card { $item->setLastModified($card['lastModified']); $item->setLastEditor($card['lastEditor']); $item->setCreatedAt($card['createdAt']); - $item->isArchived($card['archived']); + $item->setArchived($card['archived']); $item->setDeletedAt($card['deletedAt']); $item->setDone($card['done']); $item->setNotified($card['notified']); @@ -671,10 +671,10 @@ public function importCard (int $stackId, array $card): Card { $newCard = $this->cardMapper->insert($item); } catch (\Exception $e) { $this->logger->error('importCard insert error: ' . $e->getMessage()); - throw new InternalErrorException('importCard insert error: ' . $e->getMessage()); + throw new InternalError('importCard insert error: ' . $e->getMessage()); } - return $newCard; + return $newCard->getId(); } /** diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index 550ee56da1..f7b42da350 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -12,6 +12,7 @@ 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; @@ -231,7 +232,7 @@ public function importComment(int $cardId, array $comment, string $parentId = '0 $this->commentsManager->save($newComment); } catch (\Exception $e) { $this->logger->error('importComment insert error: ' . $e->getMessage()); - throw new InternalErrorException('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 76d0a45785..0efe2b23b4 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; @@ -33,7 +35,6 @@ use OCP\Share\IManager; use OCP\Share\IShare; use Psr\Log\LoggerInterface; -use Symfony\Component\CssSelector\Exception\InternalErrorException; class FilesAppService implements IAttachmentService, ICustomAttachmentService { /** @@ -363,7 +364,10 @@ public function getAllDeckSharesForCards(array $cardIds): array { * @param int $cardId * @param array $shareData * @param string $userId + * * @return bool + * + * @throws InternalError */ public function importDeckSharesForCard(int $cardId, array $shareData, int $fileId, string $userId): void { try { @@ -402,7 +406,7 @@ public function importDeckSharesForCard(int $cardId, array $shareData, int $file $qb->executeStatement(); } catch (\Throwable $e) { $this->logger->error('importDeckSharesForCard insert error: ' . $e->getMessage()); - throw new InternalErrorException('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 5611c4f542..859313f0fc 100644 --- a/lib/Service/LabelService.php +++ b/lib/Service/LabelService.php @@ -13,10 +13,10 @@ 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; -use Symfony\Component\CssSelector\Exception\InternalErrorException; class LabelService { @@ -143,9 +143,11 @@ public function update(int $id, string $title, string $color): Label { /** * @param Board $board - * @param array $$label + * @param array $label * * @return void + * + * @throws InternalError */ public function importBoardLabel(Board $board, array $label): void { $labelEntity = new Label(); @@ -157,7 +159,7 @@ public function importBoardLabel(Board $board, array $label): void { $this->labelMapper->insert($labelEntity); } catch (\Throwable $e) { $this->logger->error('importBoardLabel insert error: ' . $e->getMessage()); - throw new InternalErrorException('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 dc5ff4ee16..8ef5261b8c 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; @@ -26,7 +27,6 @@ use OCA\Deck\Validators\StackServiceValidator; use OCP\EventDispatcher\IEventDispatcher; use Psr\Log\LoggerInterface; -use Symfony\Component\CssSelector\Exception\InternalErrorException; class StackService { private StackMapper $stackMapper; @@ -310,11 +310,11 @@ public function reorder(int $id, int $order): array { * @param int $boardId * @param array $stack * - * @return Stack + * @return int * - * @throws InternalErrorException + * @throws InternalError */ - public function importStack(int $boardId, array $stack): Stack { + public function importStack(int $boardId, array $stack): int { $item = new Stack(); $item->setBoardId($boardId); $item->setTitle($stack['title']); @@ -325,9 +325,9 @@ public function importStack(int $boardId, array $stack): Stack { $newStack = $this->stackMapper->insert($item); } catch (\Exception $e) { $this->logger->error('importStack insert error: ' . $e->getMessage()); - throw new InternalErrorException('importStack insert error: ' . $e->getMessage()); + throw new InternalError('importStack insert error: ' . $e->getMessage()); } - return $newStack; + return $newStack->getId(); } } From 80c6128b4e56a8fbafaaa5e0c692577286b7132a Mon Sep 17 00:00:00 2001 From: samin-z Date: Tue, 17 Mar 2026 10:43:50 +0100 Subject: [PATCH 3/6] cs fixes Signed-off-by: samin-z --- lib/Errors/InternalError.php | 4 +--- lib/Service/AssignmentService.php | 2 +- lib/Service/CardService.php | 2 +- lib/Service/CommentService.php | 1 - 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/Errors/InternalError.php b/lib/Errors/InternalError.php index 8790991b41..a85abd6a96 100644 --- a/lib/Errors/InternalError.php +++ b/lib/Errors/InternalError.php @@ -9,8 +9,6 @@ namespace OCA\Deck\Errors; -class InternalError extends \Exception -{ +class InternalError extends \Exception { } - diff --git a/lib/Service/AssignmentService.php b/lib/Service/AssignmentService.php index a3a69b1342..62d6ec7292 100644 --- a/lib/Service/AssignmentService.php +++ b/lib/Service/AssignmentService.php @@ -15,12 +15,12 @@ use OCA\Deck\Db\AssignmentMapper; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\ChangeHelper; +use OCA\Deck\Errors\InternalError; use OCA\Deck\Event\CardUpdatedEvent; use OCA\Deck\NoPermissionException; use OCA\Deck\NotFoundException; use OCA\Deck\Notification\NotificationHelper; use OCA\Deck\Validators\AssignmentServiceValidator; -use OCA\Deck\Errors\InternalError; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\EventDispatcher\IEventDispatcher; diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 8244f13962..155dc45876 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -650,7 +650,7 @@ public function getRedirectUrlForCard(int $cardId): string { * * @throws InternalError */ - public function importCard (int $stackId, array $card): int { + public function importCard(int $stackId, array $card): int { $item = new Card(); $item->setStackId($stackId); $item->setTitle($card['title']); diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index f7b42da350..f58ade4ac1 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -23,7 +23,6 @@ use OCP\IUserManager; use OutOfBoundsException; use Psr\Log\LoggerInterface; -use Symfony\Component\CssSelector\Exception\InternalErrorException; class CommentService { From 4999a763f300a00414e92fe8c6acccc76b16cc29 Mon Sep 17 00:00:00 2001 From: samin-z Date: Tue, 17 Mar 2026 14:03:25 +0100 Subject: [PATCH 4/6] add DeckMigrator plus test Signed-off-by: samin-z --- lib/AppInfo/Application.php | 2 + lib/UserMigration/DeckMigrator.php | 476 +++++++++++++++++++++++++++++ tests/unit/DeckMigratorTest.php | 142 +++++++++ 3 files changed, 620 insertions(+) create mode 100644 lib/UserMigration/DeckMigrator.php create mode 100644 tests/unit/DeckMigratorTest.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 234af609fa..9f59f32550 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -52,6 +52,7 @@ use OCA\Deck\Sharing\Listener; use OCA\Deck\Teams\DeckTeamResourceProvider; use OCA\Text\Event\LoadEditor; +use OCA\Deck\UserMigration\DeckMigrator; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -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/UserMigration/DeckMigrator.php b/lib/UserMigration/DeckMigrator.php new file mode 100644 index 0000000000..a3e98529eb --- /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); + + list($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 0000000000..25598e90f1 --- /dev/null +++ b/tests/unit/DeckMigratorTest.php @@ -0,0 +1,142 @@ +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); + } +} From 2d7a551c9cae6e1bafa46505d6a71cf292b7cb02 Mon Sep 17 00:00:00 2001 From: samin-z Date: Tue, 17 Mar 2026 17:03:33 +0100 Subject: [PATCH 5/6] cs and psalm fix Signed-off-by: samin-z --- lib/AppInfo/Application.php | 2 +- lib/Service/BoardService.php | 5 ++-- lib/Service/CommentService.php | 7 ++++- lib/Service/FilesAppService.php | 3 +- lib/UserMigration/DeckMigrator.php | 46 +++++++++++++++--------------- tests/unit/DeckMigratorTest.php | 1 - 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 9f59f32550..42c0fcae79 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -51,8 +51,8 @@ use OCA\Deck\Sharing\DeckShareProvider; use OCA\Deck\Sharing\Listener; use OCA\Deck\Teams\DeckTeamResourceProvider; -use OCA\Text\Event\LoadEditor; use OCA\Deck\UserMigration\DeckMigrator; +use OCA\Text\Event\LoadEditor; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 8d30ac743d..c04afad3b5 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -54,7 +54,6 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; -use Symfony\Component\CssSelector\Exception\InternalErrorException; class BoardService { private ?array $boardsCacheFull = null; @@ -859,7 +858,7 @@ private function enrichWithCards(Board $board): void { * * @return Board * - * @throws InternalErrorException + * @throws InternalError */ public function importBoard(array $board, string $userId): Board { $item = new Board(); @@ -873,7 +872,7 @@ public function importBoard(array $board, string $userId): Board { $newBoard = $this->boardMapper->insert($item); } catch (\Exception $e) { $this->logger->error('importBoard insert error: ' . $e->getMessage()); - throw new InternalErrorException('importBoard insert error: ' . $e->getMessage()); + throw new InternalError('importBoard insert error: ' . $e->getMessage()); } return $newBoard; } diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index f58ade4ac1..9c3adec575 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -221,7 +221,12 @@ public function exportAllForCard(int $cardId, int $limit = 1000, int $offset = 0 */ public function importComment(int $cardId, array $comment, string $parentId = '0'): int { try { - $newComment = new Comment(); + $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'); diff --git a/lib/Service/FilesAppService.php b/lib/Service/FilesAppService.php index 0efe2b23b4..0909d6723d 100644 --- a/lib/Service/FilesAppService.php +++ b/lib/Service/FilesAppService.php @@ -352,6 +352,7 @@ public function getAllDeckSharesForCards(array $cardIds): array { ->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); $sql = $qb->getSQL(); @@ -365,7 +366,7 @@ public function getAllDeckSharesForCards(array $cardIds): array { * @param array $shareData * @param string $userId * - * @return bool + * @return void * * @throws InternalError */ diff --git a/lib/UserMigration/DeckMigrator.php b/lib/UserMigration/DeckMigrator.php index a3e98529eb..e2e0642f79 100644 --- a/lib/UserMigration/DeckMigrator.php +++ b/lib/UserMigration/DeckMigrator.php @@ -25,9 +25,10 @@ use OCA\Deck\Service\LabelService; use OCA\Deck\Service\StackService; use OCP\Files\Folder; +use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; +use OCP\IConfig; use OCP\IL10N; -use OCP\Files\IRootFolder; use OCP\IUser; use OCP\UserMigration\IExportDestination; use OCP\UserMigration\IImportSource; @@ -35,9 +36,8 @@ use OCP\UserMigration\ISizeEstimationMigrator; use OCP\UserMigration\TMigratorBasicVersionHandling; use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Output\OutputInterface; -use OCP\IConfig; +use Symfony\Component\Console\Output\OutputInterface; class DeckMigrator implements IMigrator, ISizeEstimationMigrator { @@ -112,7 +112,7 @@ public function export( $labels = $this->labelMapper->findAll($board->getId()); $boardLabels = array_merge($boardLabels, $labels); - list($newStacks, $newCards, $newComments) = $this->exportStacksCardsCommentsSimple($board->getId()); + [$newStacks, $newCards, $newComments] = $this->exportStacksCardsCommentsSimple($board->getId()); $boardStacks = array_merge($boardStacks, $newStacks); $stackCards = array_merge($stackCards, $newCards); $cardComments = array_merge($cardComments, $newComments); @@ -122,9 +122,9 @@ public function export( $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); - }); + 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); @@ -179,20 +179,20 @@ private function exportCardAttachmentsFiles(array $cardAttachments, string $user * @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]; + $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]; } /** @@ -251,7 +251,7 @@ public function import( * * @return void */ - private function importCardAttachment(IImportSource $importSource, string $userId, array $cardIdMap ): 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 { @@ -277,7 +277,7 @@ private function importDeckAttachment( Folder $deckFolder, IImportSource $importSource, array $cardIdMap, - string $userId + string $userId, ): void { $fileTarget = $share['file_target']; $fileTargetClean = str_replace('{DECK_PLACEHOLDER}/', '', ltrim($fileTarget, '/')); diff --git a/tests/unit/DeckMigratorTest.php b/tests/unit/DeckMigratorTest.php index 25598e90f1..31d44e1413 100644 --- a/tests/unit/DeckMigratorTest.php +++ b/tests/unit/DeckMigratorTest.php @@ -32,7 +32,6 @@ use OCP\UserMigration\IImportSource; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Output\OutputInterface; -use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class DeckMigratorTest extends TestCase { From ace4fd68e17c0b36b84008eceb2befe2ef614539 Mon Sep 17 00:00:00 2001 From: samin-z Date: Tue, 17 Mar 2026 17:17:57 +0100 Subject: [PATCH 6/6] fixes Signed-off-by: samin-z --- lib/Service/FilesAppService.php | 5 +---- lib/UserMigration/DeckMigrator.php | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/Service/FilesAppService.php b/lib/Service/FilesAppService.php index 0909d6723d..349e08a82f 100644 --- a/lib/Service/FilesAppService.php +++ b/lib/Service/FilesAppService.php @@ -355,10 +355,7 @@ public function getAllDeckSharesForCards(array $cardIds): array { /** @psalm-suppress UndefinedClass */ $qb->setParameter('cardIds', $cardIds, Connection::PARAM_STR_ARRAY); - $sql = $qb->getSQL(); - $params = $qb->getParameters(); - $shares = $qb->executeQuery()->fetchAllAssociative(); - return $shares; + return $qb->executeQuery()->fetchAllAssociative(); } /** diff --git a/lib/UserMigration/DeckMigrator.php b/lib/UserMigration/DeckMigrator.php index e2e0642f79..abc56f1620 100644 --- a/lib/UserMigration/DeckMigrator.php +++ b/lib/UserMigration/DeckMigrator.php @@ -112,7 +112,7 @@ public function export( $labels = $this->labelMapper->findAll($board->getId()); $boardLabels = array_merge($boardLabels, $labels); - [$newStacks, $newCards, $newComments] = $this->exportStacksCardsCommentsSimple($board->getId()); + [$newStacks, $newCards, $newComments] = $this->exportStacksCardsCommentsSimple($board->getId()); $boardStacks = array_merge($boardStacks, $newStacks); $stackCards = array_merge($stackCards, $newCards); $cardComments = array_merge($cardComments, $newComments);