- Add typing for most of the services, controllers and mappers - Add api doc for mappers - Use vendor-bin for psalm - Use attributes for controllers - Fix upload of attachments Signed-off-by: Carl Schwan <carl.schwan@nextcloud.com>
394 lines
12 KiB
PHP
394 lines
12 KiB
PHP
<?php
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\Deck\Service\Importer\Systems;
|
|
|
|
use OC\Comments\Comment;
|
|
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\IL10N;
|
|
use OCP\IURLGenerator;
|
|
use OCP\IUser;
|
|
use OCP\IUserManager;
|
|
|
|
class TrelloJsonService extends ABoardImportService {
|
|
/** @var string */
|
|
public static $name = 'Trello JSON';
|
|
/** @var IUserManager */
|
|
private $userManager;
|
|
/** @var IURLGenerator */
|
|
private $urlGenerator;
|
|
/** @var IL10N */
|
|
private $l10n;
|
|
/** @var IUser[] */
|
|
private $members = [];
|
|
|
|
public function __construct(
|
|
IUserManager $userManager,
|
|
IURLGenerator $urlGenerator,
|
|
IL10N $l10n,
|
|
) {
|
|
$this->userManager = $userManager;
|
|
$this->urlGenerator = $urlGenerator;
|
|
$this->l10n = $l10n;
|
|
}
|
|
|
|
public function bootstrap(): void {
|
|
$this->validateUsers();
|
|
}
|
|
|
|
public function getJsonSchemaPath(): string {
|
|
return implode(DIRECTORY_SEPARATOR, [
|
|
__DIR__,
|
|
'..',
|
|
'fixtures',
|
|
'config-trelloJson-schema.json',
|
|
]);
|
|
}
|
|
|
|
public function validateUsers(): void {
|
|
if (empty($this->getImportService()->getConfig('uidRelation'))) {
|
|
return;
|
|
}
|
|
foreach ($this->getImportService()->getConfig('uidRelation') as $trelloUid => $nextcloudUid) {
|
|
$user = array_filter($this->getImportService()->getData()->members, function (\stdClass $u) use ($trelloUid) {
|
|
return $u->username === $trelloUid;
|
|
});
|
|
if (!$user) {
|
|
throw new \LogicException('Trello user ' . $trelloUid . ' not found in property "members" of json data');
|
|
}
|
|
if (!is_string($nextcloudUid) && !is_numeric($nextcloudUid)) {
|
|
throw new \LogicException('User on setting uidRelation is invalid');
|
|
}
|
|
$nextcloudUid = (string)$nextcloudUid;
|
|
$this->getImportService()->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid);
|
|
if (!$this->getImportService()->getConfig('uidRelation')->$trelloUid) {
|
|
throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid);
|
|
}
|
|
$user = current($user);
|
|
$this->members[$user->id] = $this->getImportService()->getConfig('uidRelation')->$trelloUid;
|
|
}
|
|
}
|
|
|
|
public function getCardAssignments(): array {
|
|
$assignments = [];
|
|
foreach ($this->getImportService()->getData()->cards as $trelloCard) {
|
|
foreach ($trelloCard->idMembers as $idMember) {
|
|
if (empty($this->members[$idMember])) {
|
|
continue;
|
|
}
|
|
$assignment = new Assignment();
|
|
$assignment->setCardId($this->cards[$trelloCard->id]->getId());
|
|
$assignment->setParticipant($this->members[$idMember]->getUID());
|
|
$assignment->setType(Assignment::TYPE_USER);
|
|
$assignments[$trelloCard->id][] = $assignment;
|
|
}
|
|
}
|
|
return $assignments;
|
|
}
|
|
|
|
public function getComments(): array {
|
|
$comments = [];
|
|
foreach ($this->getImportService()->getData()->cards as $trelloCard) {
|
|
$values = array_filter(
|
|
$this->getImportService()->getData()->actions,
|
|
function (\stdClass $a) use ($trelloCard) {
|
|
return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id;
|
|
}
|
|
);
|
|
$keys = array_map(function (\stdClass $c): string {
|
|
return $c->id;
|
|
}, $values);
|
|
$trelloComments = array_combine($keys, $values);
|
|
$trelloComments = $this->sortComments($trelloComments);
|
|
foreach ($trelloComments as $commentId => $trelloComment) {
|
|
$cardId = $this->cards[$trelloCard->id]->getId();
|
|
$comment = new Comment();
|
|
if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) {
|
|
$actor = $this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username};
|
|
} else {
|
|
$actor = $this->getImportService()->getConfig('owner')->getUID();
|
|
}
|
|
$message = $this->replaceUsernames($trelloComment->data->text);
|
|
if (mb_strlen($message, 'UTF-8') > IComment::MAX_MESSAGE_LENGTH) {
|
|
$attachment = new Attachment();
|
|
$attachment->setCardId($cardId);
|
|
$attachment->setType('deck_file');
|
|
$attachment->setCreatedBy($actor);
|
|
$attachment->setLastModified(time());
|
|
$attachment->setCreatedAt(time());
|
|
$attachment->setData('comment_' . $commentId . '.md');
|
|
$attachment = $this->getImportService()->insertAttachment($attachment, $message);
|
|
|
|
$urlToDownloadAttachment = $this->urlGenerator->linkToRouteAbsolute(
|
|
'deck.attachment.display',
|
|
[
|
|
'cardId' => $cardId,
|
|
'attachmentId' => $attachment->getId()
|
|
]
|
|
);
|
|
$message = $this->l10n->t(
|
|
"This comment has more than %s characters.\n" .
|
|
"Added as an attachment to the card with name %s.\n" .
|
|
'Accessible on URL: %s.',
|
|
[
|
|
IComment::MAX_MESSAGE_LENGTH,
|
|
'comment_' . $commentId . '.md',
|
|
$urlToDownloadAttachment
|
|
]
|
|
);
|
|
}
|
|
$comment
|
|
->setActor('users', $actor)
|
|
->setMessage($message)
|
|
->setCreationDateTime(
|
|
\DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date)
|
|
);
|
|
$comments[$cardId][$commentId] = $comment;
|
|
}
|
|
}
|
|
/** @var array<int, array<string, IComment>> */
|
|
return $comments;
|
|
}
|
|
|
|
private function sortComments(array $comments): array {
|
|
$comparison = function (\stdClass $a, \stdClass $b): int {
|
|
if ($a->date == $b->date) {
|
|
return 0;
|
|
}
|
|
return ($a->date < $b->date) ? -1 : 1;
|
|
};
|
|
|
|
usort($comments, $comparison);
|
|
return $comments;
|
|
}
|
|
|
|
public function getCardLabelAssignment(): array {
|
|
$cardsLabels = [];
|
|
foreach ($this->getImportService()->getData()->cards as $trelloCard) {
|
|
foreach ($trelloCard->labels as $label) {
|
|
$cardId = $this->cards[$trelloCard->id]->getId();
|
|
$labelId = $this->labels[$label->id]->getId();
|
|
$cardsLabels[$cardId][] = $labelId;
|
|
}
|
|
}
|
|
return $cardsLabels;
|
|
}
|
|
|
|
public function getBoard(): Board {
|
|
$board = $this->getImportService()->getBoard();
|
|
if (empty($this->getImportService()->getData()->name)) {
|
|
throw new BadRequestException('Invalid name of board');
|
|
}
|
|
$board->setTitle($this->getImportService()->getData()->name);
|
|
$board->setOwner($this->getImportService()->getConfig('owner')->getUID());
|
|
$board->setColor($this->getImportService()->getConfig('color'));
|
|
return $board;
|
|
}
|
|
|
|
/**
|
|
* @return Label[]
|
|
*/
|
|
public function getLabels(): array {
|
|
foreach ($this->getImportService()->getData()->labels as $trelloLabel) {
|
|
$label = new Label();
|
|
if (empty($trelloLabel->name)) {
|
|
$label->setTitle('Unnamed ' . $trelloLabel->color . ' label');
|
|
} else {
|
|
$label->setTitle($trelloLabel->name);
|
|
}
|
|
$label->setColor($this->translateColor($trelloLabel->color));
|
|
$label->setBoardId($this->getImportService()->getBoard()->getId());
|
|
$this->labels[$trelloLabel->id] = $label;
|
|
}
|
|
return $this->labels;
|
|
}
|
|
|
|
/**
|
|
* @return Stack[]
|
|
*/
|
|
public function getStacks(): array {
|
|
$return = [];
|
|
foreach ($this->getImportService()->getData()->lists as $order => $list) {
|
|
$stack = new Stack();
|
|
if ($list->closed) {
|
|
$stack->setDeletedAt(time());
|
|
}
|
|
$stack->setTitle($list->name);
|
|
$stack->setBoardId($this->getImportService()->getBoard()->getId());
|
|
$stack->setOrder($order + 1);
|
|
$return[$list->id] = $stack;
|
|
}
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* @return Card[]
|
|
*/
|
|
public function getCards(): array {
|
|
$checklists = [];
|
|
foreach ($this->getImportService()->getData()->checklists as $checklist) {
|
|
$checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist);
|
|
}
|
|
$this->getImportService()->getData()->checklists = $checklists;
|
|
|
|
$cards = [];
|
|
foreach ($this->getImportService()->getData()->cards as $trelloCard) {
|
|
$card = new Card();
|
|
$lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity);
|
|
$card->setLastModified($lastModified->format('Y-m-d H:i:s'));
|
|
if ($trelloCard->closed) {
|
|
$card->setArchived(true);
|
|
}
|
|
if ((count($trelloCard->idChecklists) !== 0)) {
|
|
foreach ($this->getImportService()->getData()->checklists[$trelloCard->id] as $checklist) {
|
|
$trelloCard->desc .= "\n" . $checklist;
|
|
}
|
|
}
|
|
$this->appendAttachmentsToDescription($trelloCard);
|
|
|
|
$card->setTitle($trelloCard->name);
|
|
$card->setStackId($this->stacks[$trelloCard->idList]->getId());
|
|
$cardsOnStack = $this->stacks[$trelloCard->idList]->getCards();
|
|
$cardsOnStack[] = $card;
|
|
$this->stacks[$trelloCard->idList]->setCards($cardsOnStack);
|
|
$card->setType('plain');
|
|
$card->setOrder($trelloCard->pos);
|
|
$card->setOwner($this->getImportService()->getConfig('owner')->getUID());
|
|
|
|
$lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity);
|
|
$card->setLastModified($lastModified->format('U'));
|
|
|
|
$createCardDate = array_filter(
|
|
$this->getImportService()->getData()->actions,
|
|
function (\stdClass $a) use ($trelloCard) {
|
|
return $a->type === 'createCard' && $a->data->card->id === $trelloCard->id;
|
|
}
|
|
);
|
|
$createCardDate = current($createCardDate);
|
|
$createCardDate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $createCardDate->date);
|
|
if ($createCardDate) {
|
|
$card->setCreatedAt($createCardDate->format('U'));
|
|
} else {
|
|
$card->setCreatedAt($lastModified->format('U'));
|
|
}
|
|
|
|
$card->setDescription($trelloCard->desc);
|
|
if ($trelloCard->due) {
|
|
$duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->due)
|
|
->format('Y-m-d H:i:s');
|
|
$card->setDuedate($duedate);
|
|
}
|
|
$cards[$trelloCard->id] = $card;
|
|
}
|
|
return $cards;
|
|
}
|
|
|
|
/**
|
|
* @return Acl[]
|
|
*/
|
|
public function getAclList(): array {
|
|
$return = [];
|
|
foreach ($this->members as $member) {
|
|
if ($member->getUID() === $this->getImportService()->getConfig('owner')->getUID()) {
|
|
continue;
|
|
}
|
|
$acl = new Acl();
|
|
$acl->setBoardId($this->getImportService()->getBoard()->getId());
|
|
$acl->setType(Acl::PERMISSION_TYPE_USER);
|
|
$acl->setParticipant($member->getUID());
|
|
$acl->setPermissionEdit(false);
|
|
$acl->setPermissionShare(false);
|
|
$acl->setPermissionManage(false);
|
|
$return[] = $acl;
|
|
}
|
|
return $return;
|
|
}
|
|
|
|
private function translateColor(?string $color): string {
|
|
switch ($color) {
|
|
case 'red':
|
|
return 'ff0000';
|
|
case 'yellow':
|
|
return 'ffff00';
|
|
case 'orange':
|
|
return 'ff6600';
|
|
case 'green':
|
|
return '00ff00';
|
|
case 'purple':
|
|
return '9900ff';
|
|
case 'blue':
|
|
return '0000ff';
|
|
case 'sky':
|
|
return '00ccff';
|
|
case 'lime':
|
|
return '00ff99';
|
|
case 'pink':
|
|
return 'ff66cc';
|
|
case 'black':
|
|
return '000000';
|
|
default:
|
|
return 'ffffff';
|
|
}
|
|
}
|
|
|
|
private function replaceUsernames(string $text): string {
|
|
foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) {
|
|
$text = str_replace($trello, $nextcloud, $text);
|
|
}
|
|
return $text;
|
|
}
|
|
|
|
private function checklistItem(\stdClass $item): string {
|
|
if (($item->state == 'incomplete')) {
|
|
$string_start = '- [ ]';
|
|
} else {
|
|
$string_start = '- [x]';
|
|
}
|
|
$check_item_string = $string_start . ' ' . $item->name . "\n";
|
|
return $check_item_string;
|
|
}
|
|
|
|
private function formulateChecklistText(\stdClass $checklist): string {
|
|
$checklist_string = "\n\n## {$checklist->name}\n";
|
|
foreach ($checklist->checkItems as $item) {
|
|
$checklist_item_string = $this->checklistItem($item);
|
|
$checklist_string = $checklist_string . "\n" . $checklist_item_string;
|
|
}
|
|
return $checklist_string;
|
|
}
|
|
|
|
private function appendAttachmentsToDescription(\stdClass $trelloCard): void {
|
|
if (empty($trelloCard->attachments)) {
|
|
return;
|
|
}
|
|
$trelloCard->desc .= "\n\n## {$this->l10n->t('Attachments')}\n";
|
|
$trelloCard->desc .= "| {$this->l10n->t('File')} | {$this->l10n->t('date')} |\n";
|
|
$trelloCard->desc .= "|---|---\n";
|
|
foreach ($trelloCard->attachments as $attachment) {
|
|
$name = mb_strlen($attachment->name, 'UTF-8') ? $attachment->name : $attachment->url;
|
|
$trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n";
|
|
}
|
|
}
|
|
|
|
public function getBoards(): array {
|
|
if ($this->getImportService()->getData()->boards) {
|
|
return $this->getImportService()->getData()->boards;
|
|
}
|
|
|
|
return [$this->getImportService()->getData()];
|
|
}
|
|
}
|