Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,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;
Expand Down Expand Up @@ -180,6 +181,8 @@ 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 {
Expand Down
12 changes: 12 additions & 0 deletions lib/Service/AttachmentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,18 @@ public function create(int $cardId, string $type, string $data = '') {
return $attachment;
}

/**
* Apply import side effects to keep attachment behavior consistent with regular create flow.
*/
public function syncAttachmentCreateSideEffects(Attachment $attachment): Attachment {
$cardId = $attachment->getCardId();
$this->attachmentCacheHelper->clearAttachmentCount($cardId);
$this->addCreator($attachment);
$this->changeHelper->cardChanged($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_CREATE);

return $attachment;
}

/**
* Display the attachment
Expand Down
93 changes: 93 additions & 0 deletions lib/Service/FilesAppService.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
Expand Down Expand Up @@ -238,6 +239,98 @@ public function create(Attachment $attachment) {
return $attachment;
}

/**
* Ensure the file exists in the owner’s Deck attachment folder, link it to the
* Reuse file/share when already present
*
* @param Attachment $attachment
* @param string $content
* @return Attachment
* @throws BadRequestException
* @throws NoPermissionException
* @throws NotFoundException
* @throws StatusException
*/
public function createFromImport(Attachment $attachment, string $content): Attachment {
$fileName = $attachment->getData();
$this->validateFilename($fileName);

$ownerId = $attachment->getCreatedBy() ?: $this->userId;
if (!is_string($ownerId) || $ownerId === '') {
throw new StatusException('Could not resolve owner for imported attachment');
}

$userFolder = $this->rootFolder->getUserFolder($ownerId);
$attachmentFolderName = $this->configService->getAttachmentFolder($ownerId);
try {
$folder = $userFolder->get($attachmentFolderName);
} catch (NotFoundException) {
$folder = $userFolder->newFolder($attachmentFolderName);
}
if (!$folder instanceof Folder || $folder->isShared()) {
throw new NotFoundException('No target folder found');
}

if ($folder->nodeExists($fileName)) {
$node = $folder->get($fileName);
if (!($node instanceof File)) {
throw new StatusException('Attachment target is not a file');
}
$target = $node;
} else {
$target = $folder->newFile($fileName);
$target->putContent($content);
}

$cardId = $attachment->getCardId();
foreach ($this->shareProvider->getSharesByPath($target) as $share) {
if ((int)$share->getSharedWith() === $cardId) {
$attachment->setId((int)$share->getId());
$attachment->setData($target->getName());
return $attachment;
}
}

$this->permissionService->checkPermission(
$this->cardMapper,
$cardId,
Acl::PERMISSION_EDIT,
$ownerId,
true,
true
);
// Import usually runs in background jobs without request user context.
// Set the actor explicitly so DeckShareProvider permission checks evaluate correctly.
$this->permissionService->setUserId($ownerId);

$share = $this->shareManager->newShare();
$share->setNode($target);
$share->setShareType(IShare::TYPE_DECK);
$share->setSharedWith((string)$cardId);
$share->setPermissions(Constants::PERMISSION_READ);
$share->setSharedBy($ownerId);
$share->setShareOwner($ownerId);

try {
$share = $this->shareManager->createShare($share);
} catch (GenericShareException) {
$share = null;
foreach ($this->shareProvider->getSharesByPath($target) as $existing) {
if ((int)$existing->getSharedWith() === $cardId) {
$share = $existing;
break;
}
}
if ($share === null) {
throw new StatusException('Could not create deck share for imported attachment');
}
}

$attachment->setId((int)$share->getId());
$attachment->setData($target->getName());
return $attachment;
}

/**
* @return array
* @throws StatusException
Expand Down
2 changes: 2 additions & 0 deletions lib/Service/Importer/ABoardImportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ abstract public function getCardLabelAssignment(): array;
*/
abstract public function getComments(): array;

abstract public function importAttachments(): void;

/** @return Label[] */
abstract public function getLabels(): array;

Expand Down
41 changes: 26 additions & 15 deletions lib/Service/Importer/BoardImportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
use OCA\Deck\Event\BoardImportGetAllowedEvent;
use OCA\Deck\Exceptions\ConflictException;
use OCA\Deck\NotFoundException;
use OCA\Deck\Service\FileService;
use OCA\Deck\Service\AttachmentService;
use OCA\Deck\Service\FilesAppService;
use OCA\Deck\Service\Importer\Systems\DeckJsonService;
use OCA\Deck\Service\Importer\Systems\TrelloApiService;
use OCA\Deck\Service\Importer\Systems\TrelloJsonService;
Expand Down Expand Up @@ -73,6 +74,7 @@ public function __construct(
private ICommentsManager $commentsManager,
private IEventDispatcher $eventDispatcher,
private LoggerInterface $logger,
private AttachmentService $attachmentService,
) {
$this->board = new Board();
$this->disableCommentsEvents();
Expand Down Expand Up @@ -125,6 +127,7 @@ public function import(): void {
$this->importLabels();
$this->importStacks();
$this->importCards();
$this->getImportSystem()->importAttachments();
$this->assignCardsToLabels();
$this->importComments();
$this->importCardAssignments();
Expand Down Expand Up @@ -319,11 +322,23 @@ public function assignCardsToLabels(): void {
}
}

/**
* Insert parsed comments by card and remap parent IDs from source to imported comments.
*
* @return void
*/
public function importComments(): void {
$allComments = $this->getImportSystem()->getComments();
foreach ($allComments as $cardId => $comments) {
$commentsByCard = $this->getImportSystem()->getComments();
$sourceToImportedCommentId = [];
foreach ($commentsByCard as $cardId => $comments) {
foreach ($comments as $commentId => $comment) {
$metaData = $comment->getMetaData() ?? [];
$sourceParentId = $comment->getParentId();
$comment->setParentId($sourceToImportedCommentId[$sourceParentId] ?? '0');

$this->insertComment((int)$cardId, $comment);
$sourceId = (string)($metaData['deckImportSourceId'] ?? $commentId);
$sourceToImportedCommentId[$sourceId] = $comment->getId();
$this->getImportSystem()->updateComment((int)$cardId, $commentId, $comment);
}
}
Expand Down Expand Up @@ -369,20 +384,16 @@ public function importCardAssignments(): void {
}

public function insertAttachment(Attachment $attachment, string $content): Attachment {
$service = Server::get(FileService::class);
$folder = $service->getFolder($attachment);

if ($folder->fileExists($attachment->getData())) {
$attachment = $this->attachmentMapper->findByData($attachment->getCardId(), $attachment->getData());
throw new ConflictException('File already exists.', $attachment);
}

$target = $folder->newFile($attachment->getData());
$target->putContent($content);
$attachment->setType('file');
$attachment->setLastModified(time());
$attachment->setCreatedAt(time());

$attachment = $this->attachmentMapper->insert($attachment);
/** @var FilesAppService $fileService */
$fileService = $this->attachmentService->getService('file');
$fileService->createFromImport($attachment, $content);
$fileService->extendData($attachment);
$this->attachmentService->syncAttachmentCreateSideEffects($attachment);

$service->extendData($attachment);
return $attachment;
}

Expand Down
100 changes: 93 additions & 7 deletions lib/Service/Importer/Systems/DeckJsonService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
use OCA\Deck\BadRequestException;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCA\Deck\Service\Importer\ABoardImportService;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\IUser;
use OCP\IUserManager;

Expand Down Expand Up @@ -94,6 +96,9 @@ public function mapOwner(string $uid): string {
public function getCardAssignments(): array {
$assignments = [];
foreach ($this->tmpCards as $sourceCard) {
if (!property_exists($sourceCard, 'assignedUsers') || !is_iterable($sourceCard->assignedUsers)) {
continue;
}
foreach ($sourceCard->assignedUsers as $idMember) {
$assignment = new Assignment();
$assignment->setCardId($this->cards[$sourceCard->id]->getId());
Expand All @@ -105,24 +110,105 @@ public function getCardAssignments(): array {
return $assignments;
}

/**
* Parse comments from source cards and return them as an array of comments by card ID.
*
*/
public function getComments(): array {
$comments = [];
foreach ($this->tmpCards as $sourceCard) {
if (!property_exists($sourceCard, 'comments')) {
if (!property_exists($sourceCard, 'comments') || !isset($this->cards[$sourceCard->id])) {
continue;
}
$targetCardId = $this->cards[$sourceCard->id]->getId();
if (!is_iterable($sourceCard->comments)) {
continue;
}
$commentsOriginal = $sourceCard->comments;
foreach ($commentsOriginal as $commentOriginal) {
foreach ($sourceCard->comments as $commentOriginal) {
$commentId = (string)($commentOriginal->id ?? '');
if ($commentId === '') {
continue;
}
$parentId = (string)($commentOriginal->parentId ?? '0');
[$actorType, $actorId] = $this->resolveCommentActor($commentOriginal);
$comment = new Comment();
$comment->setActor($commentOriginal->actorType, $commentOriginal->actorId)
->setMessage($commentOriginal->message)->setCreationDateTime(\DateTime::createFromFormat('Y-m-d\TH:i:sP', $commentOriginal->creationDateTime));
$comments[$this->cards[$sourceCard->id]->getId()][$commentOriginal->id] = $comment;
$comment->setActor($actorType, $actorId)
->setParentId($parentId)
->setMessage($commentOriginal->message)
->setCreationDateTime(\DateTime::createFromFormat('Y-m-d\TH:i:sP', $commentOriginal->creationDateTime));
$comment->setMetaData([
'deckImportSourceId' => $commentId,
'deckImportParentId' => $parentId,
]);
$comments[$targetCardId][$commentId] = $comment;
}
}
/** @var array<int, array<string, IComment>> */
return $comments;
}

public function importAttachments(): void {
foreach ($this->tmpCards as $sourceCard) {
$this->importAttachmentsForCard($sourceCard);
}
}

/**
* @return array{0: string, 1: string}
*/
private function resolveCommentActor(object $commentOriginal): array {
$actorType = (string)($commentOriginal->actorType ?? 'users');
$actorId = $commentOriginal->actorId ?? null;

if (!is_string($actorId) || $actorId === '') {
return [ICommentsManager::DELETED_USER, ICommentsManager::DELETED_USER];
}

if ($actorType === 'users' && !$this->userManager->userExists($actorId)) {
return [ICommentsManager::DELETED_USER, ICommentsManager::DELETED_USER];
}

return [$actorType, $actorId];
}

private function importAttachmentsForCard(object $sourceCard): void {
if (!property_exists($sourceCard, 'attachments') || !isset($this->cards[$sourceCard->id])) {
return;
}

$targetCardId = $this->cards[$sourceCard->id]->getId();
foreach ($sourceCard->attachments as $sourceAttachment) {
if (($sourceAttachment->type ?? null) !== 'file') {
continue;
}
if (!isset($sourceAttachment->data) || !isset($sourceAttachment->contentBase64)) {
continue;
}

$attachment = new Attachment();
$attachment->setCardId($targetCardId);
$attachment->setType('file');
$attachment->setData((string)$sourceAttachment->data);
$attachment->setCreatedBy(
$this->mapMember($sourceAttachment->createdBy ?? $this->getImportService()->getData()->owner)
?? $this->mapOwner($this->getImportService()->getData()->owner)
);
$attachment->setCreatedAt((int)($sourceAttachment->createdAt ?? time()));
$attachment->setLastModified((int)($sourceAttachment->lastModified ?? time()));

$content = base64_decode((string)$sourceAttachment->contentBase64, true);
if ($content === false) {
continue;
}

try {
$this->getImportService()->insertAttachment($attachment, $content);
} catch (\Throwable $e) {
continue;
}
}
}

public function getCardLabelAssignment(): array {
$cardsLabels = [];
foreach ($this->tmpCards as $sourceCard) {
Expand Down Expand Up @@ -184,7 +270,7 @@ public function getStacks(): array {

if (isset($source->cards)) {
foreach ($source->cards as $card) {
$card->stackId = $index;
$card->stackId = $source->id;
$this->tmpCards[] = $card;
}
}
Expand Down
3 changes: 3 additions & 0 deletions lib/Service/Importer/Systems/TrelloJsonService.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ function (\stdClass $a) use ($trelloCard) {
return $comments;
}

public function importAttachments(): void {
}

private function sortComments(array $comments): array {
$comparison = function (\stdClass $a, \stdClass $b): int {
if ($a->date === $b->date) {
Expand Down
Loading
Loading