From 9658ccd8432b1f29502113fae46c2ab839653b50 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 1 Feb 2023 13:11:55 +0100 Subject: [PATCH] refactor CommentService a bit, add BoardReferenceProvider and CommentReferenceProvider (no widgets but resolving) Signed-off-by: Julien Veyssier --- lib/AppInfo/Application.php | 5 +- lib/Reference/BoardReferenceProvider.php | 128 ++++++++++++++ lib/Reference/CommentReferenceProvider.php | 195 +++++++++++++++++++++ lib/Service/CommentService.php | 49 ++++-- 4 files changed, 364 insertions(+), 13 deletions(-) create mode 100644 lib/Reference/BoardReferenceProvider.php create mode 100644 lib/Reference/CommentReferenceProvider.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index b3695e1eb..95b1d155d 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -50,7 +50,9 @@ use OCA\Deck\Listeners\LiveUpdateListener; use OCA\Deck\Middleware\DefaultBoardMiddleware; use OCA\Deck\Middleware\ExceptionMiddleware; use OCA\Deck\Notification\Notifier; +use OCA\Deck\Reference\BoardReferenceProvider; use OCA\Deck\Reference\CardReferenceProvider; +use OCA\Deck\Reference\CommentReferenceProvider; use OCA\Deck\Search\CardCommentProvider; use OCA\Deck\Search\DeckProvider; use OCA\Deck\Service\PermissionService; @@ -131,7 +133,8 @@ class Application extends App implements IBootstrap { // reference widget $context->registerReferenceProvider(CardReferenceProvider::class); - // $context->registerEventListener(RenderReferenceEvent::class, CardReferenceListener::class); + $context->registerReferenceProvider(BoardReferenceProvider::class); + $context->registerReferenceProvider(CommentReferenceProvider::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); diff --git a/lib/Reference/BoardReferenceProvider.php b/lib/Reference/BoardReferenceProvider.php new file mode 100644 index 000000000..f0f9abbfb --- /dev/null +++ b/lib/Reference/BoardReferenceProvider.php @@ -0,0 +1,128 @@ + + * + * @author Julien Veyssier + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\Deck\Reference; + +use OCA\Deck\AppInfo\Application; +use OCA\Deck\Service\BoardService; +use OCP\Collaboration\Reference\IReference; +use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\Collaboration\Reference\Reference; +use OCP\IL10N; +use OCP\IURLGenerator; + +class BoardReferenceProvider implements IReferenceProvider { + private IURLGenerator $urlGenerator; + private BoardService $boardService; + private ?string $userId; + private IL10N $l10n; + + public function __construct(BoardService $boardService, + IURLGenerator $urlGenerator, + IL10N $l10n, + ?string $userId) { + $this->urlGenerator = $urlGenerator; + $this->boardService = $boardService; + $this->userId = $userId; + $this->l10n = $l10n; + } + + /** + * @inheritDoc + */ + public function matchReference(string $referenceText): bool { + $start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID); + $startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID); + + // link example: https://nextcloud.local/index.php/apps/deck/#/board/2 + $noIndexMatch = preg_match('/^' . preg_quote($start, '/') . '\/#\/board\/[0-9]+$/', $referenceText) === 1; + $indexMatch = preg_match('/^' . preg_quote($startIndex, '/') . '\/#\/board\/[0-9]+$/', $referenceText) === 1; + + return $noIndexMatch || $indexMatch; + } + + /** + * @inheritDoc + */ + public function resolveReference(string $referenceText): ?IReference { + if ($this->matchReference($referenceText)) { + $boardId = $this->getBoardId($referenceText); + if ($boardId !== null) { + $board = $this->boardService->find($boardId)->jsonSerialize(); + $board = $this->sanitizeSerializedBoard($board); + /** @var IReference $reference */ + $reference = new Reference($referenceText); + $reference->setTitle($this->l10n->t('Deck board') . ': ' . $board['title']); + $ownerDisplayName = $board['owner']['displayname'] ?? $board['owner']['uid'] ?? '???'; + $reference->setDescription($this->l10n->t('Owned by %1$s', [$ownerDisplayName])); + $imageUrl = $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath(Application::APP_ID, 'deck-dark.svg') + ); + $reference->setImageUrl($imageUrl); + $reference->setRichObject(Application::APP_ID . '-board', [ + 'id' => $boardId, + 'board' => $board, + ]); + return $reference; + } + } + + return null; + } + + private function sanitizeSerializedBoard(array $board): array { + unset($board['labels']); + $board['owner'] = $board['owner']->jsonSerialize(); + unset($board['acl']); + unset($board['users']); + + return $board; + } + + private function getBoardId(string $url): ?int { + $start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID); + $startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID); + + preg_match('/^' . preg_quote($start, '/') . '\/#\/board\/([0-9]+)$/', $url, $matches); + if (!$matches) { + preg_match('/^' . preg_quote($startIndex, '/') . '\/#\/board\/([0-9]+)$/', $url, $matches); + } + if ($matches && count($matches) > 1) { + return (int) $matches[1]; + } + + return null; + } + + public function getCachePrefix(string $referenceId): string { + $boardId = $this->getBoardId($referenceId); + if ($boardId !== null) { + return (string) $boardId; + } + + return $referenceId; + } + + public function getCacheKey(string $referenceId): ?string { + return $this->userId ?? ''; + } +} diff --git a/lib/Reference/CommentReferenceProvider.php b/lib/Reference/CommentReferenceProvider.php new file mode 100644 index 000000000..2b1fed050 --- /dev/null +++ b/lib/Reference/CommentReferenceProvider.php @@ -0,0 +1,195 @@ + + * + * @author Julien Veyssier + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\Deck\Reference; + +use OCA\Deck\AppInfo\Application; +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\Assignment; +use OCA\Deck\Db\Attachment; +use OCA\Deck\Db\Label; +use OCA\Deck\Model\CardDetails; +use OCA\Deck\NotFoundException; +use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\CardService; +use OCA\Deck\Service\CommentService; +use OCA\Deck\Service\PermissionService; +use OCA\Deck\Service\StackService; +use OCP\Collaboration\Reference\IReference; +use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\Collaboration\Reference\Reference; +use OCP\Comments\ICommentsManager; +use OCP\IL10N; +use OCP\IURLGenerator; + +class CommentReferenceProvider implements IReferenceProvider { + private CardService $cardService; + private IURLGenerator $urlGenerator; + private BoardService $boardService; + private StackService $stackService; + private ?string $userId; + private IL10N $l10n; + private CommentService $commentService; + + public function __construct(CardService $cardService, + BoardService $boardService, + StackService $stackService, + CommentService $commentService, + IURLGenerator $urlGenerator, + IL10N $l10n, + ?string $userId) { + $this->cardService = $cardService; + $this->urlGenerator = $urlGenerator; + $this->boardService = $boardService; + $this->stackService = $stackService; + $this->userId = $userId; + $this->l10n = $l10n; + $this->commentService = $commentService; + } + + /** + * @inheritDoc + */ + public function matchReference(string $referenceText): bool { + $start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID); + $startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID); + + // link example: https://nextcloud.local/index.php/apps/deck/#/board/2/card/11/comments/501 + $noIndexMatch = preg_match('/^' . preg_quote($start, '/') . '\/#\/board\/[0-9]+\/card\/[0-9]+\/comments\/\d+$/', $referenceText) === 1; + $indexMatch = preg_match('/^' . preg_quote($startIndex, '/') . '\/#\/board\/[0-9]+\/card\/[0-9]+\/comments\/\d+$/', $referenceText) === 1; + + return $noIndexMatch || $indexMatch; + } + + /** + * @inheritDoc + */ + public function resolveReference(string $referenceText): ?IReference { + if ($this->matchReference($referenceText)) { + $ids = $this->getIds($referenceText); + if ($ids !== null) { + [$boardId, $cardId, $commentId] = $ids; + + $card = $this->cardService->find($cardId)->jsonSerialize(); + $board = $this->boardService->find($boardId)->jsonSerialize(); + $stack = $this->stackService->find((int) $card['stackId'])->jsonSerialize(); + $card = $this->sanitizeSerializedCard($card); + $board = $this->sanitizeSerializedBoard($board); + $stack = $this->sanitizeSerializedStack($stack); + + $comment = $this->commentService->getFormatted($cardId, $commentId); + + /** @var IReference $reference */ + $reference = new Reference($referenceText); + $reference->setTitle($comment['message']); + $boardOwnerDisplayName = $board['owner']['displayname'] ?? $board['owner']['uid'] ?? '???'; + $reference->setDescription( + $this->l10n->t('From %1$s, in %2$s/%3$s, owned by %4$s', [ + $comment['actorDisplayName'], + $board['title'], + $stack['title'], + $boardOwnerDisplayName + ]) + ); + $imageUrl = $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath('core', 'actions/comment.svg') + ); + $reference->setImageUrl($imageUrl); + $reference->setRichObject(Application::APP_ID . '-comment', [ + 'id' => $ids, + 'board' => $board, + 'card' => $card, + 'stack' => $stack, + 'comment' => $comment, + ]); + return $reference; + } + } + + return null; + } + + private function sanitizeSerializedStack(array $stack): array { + $stack['cards'] = array_map(function (CardDetails $cardDetails) { + $result = $cardDetails->jsonSerialize(); + unset($result['assignedUsers']); + return $result; + }, $stack['cards']); + + return $stack; + } + + private function sanitizeSerializedBoard(array $board): array { + unset($board['labels']); + $board['owner'] = $board['owner']->jsonSerialize(); + unset($board['acl']); + unset($board['users']); + + return $board; + } + + private function sanitizeSerializedCard(array $card): array { + $card['labels'] = array_map(function (Label $label) { + return $label->jsonSerialize(); + }, $card['labels']); + $card['assignedUsers'] = array_map(function (Assignment $assignment) { + $result = $assignment->jsonSerialize(); + $result['participant'] = $result['participant']->jsonSerialize(); + return $result; + }, $card['assignedUsers']); + $card['owner'] = $card['owner']->jsonSerialize(); + unset($card['relatedStack']); + unset($card['relatedBoard']); + $card['attachments'] = array_map(function (Attachment $attachment) { + return $attachment->jsonSerialize(); + }, $card['attachments']); + + return $card; + } + + private function getIds(string $url): ?array { + $start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID); + $startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID); + + preg_match('/^' . preg_quote($start, '/') . '\/#\/board\/([0-9]+)\/card\/([0-9]+)\/comments\/(\d+)$/', $url, $matches); + if (!$matches) { + preg_match('/^' . preg_quote($startIndex, '/') . '\/#\/board\/([0-9]+)\/card\/([0-9]+)\/comments\/(\d+)$/', $url, $matches); + } + if ($matches && count($matches) > 3) { + return [ + (int) $matches[1], + (int) $matches[2], + (int) $matches[3], + ]; + } + + return null; + } + + public function getCachePrefix(string $referenceId): string { + return $referenceId; + } + + public function getCacheKey(string $referenceId): ?string { + return $this->userId ?? ''; + } +} diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index e9768d710..0335f4dfa 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -76,6 +76,42 @@ class CommentService { return new DataResponse($result); } + /** + * @param int $cardId + * @param int $commentId + * @return IComment + * @throws NoPermissionException + * @throws NotFoundException + */ + private function get(int $cardId, int $commentId): IComment { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ); + try { + $comment = $this->commentsManager->get($commentId); + if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || (int) $comment->getObjectId() !== $cardId) { + throw new CommentNotFoundException(); + } + } catch (CommentNotFoundException $e) { + throw new NotFoundException('No comment found.'); + } + if ($comment->getParentId() !== '0') { + $this->permissionService->checkPermission($this->cardMapper, $comment->getParentId(), Acl::PERMISSION_READ); + } + + return $comment; + } + + /** + * @param int $cardId + * @param int $commentId + * @return array + * @throws NoPermissionException + * @throws NotFoundException + */ + public function getFormatted(int $cardId, int $commentId): array { + $comment = $this->get($cardId, $commentId); + return $this->formatComment($comment); + } + /** * @param string $cardId * @param string $message @@ -126,21 +162,10 @@ class CommentService { if (!is_numeric($commentId)) { throw new BadRequestException('A valid comment id must be provided'); } - $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ); - try { - $comment = $this->commentsManager->get($commentId); - if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $comment->getObjectId() !== $cardId) { - throw new CommentNotFoundException(); - } - } catch (CommentNotFoundException $e) { - throw new NotFoundException('No comment found.'); - } + $comment = $this->get((int) $cardId, (int) $commentId); if ($comment->getActorType() !== 'users' || $comment->getActorId() !== $this->userId) { throw new NoPermissionException('Only authors are allowed to edit their comment.'); } - if ($comment->getParentId() !== '0') { - $this->permissionService->checkPermission($this->cardMapper, $comment->getParentId(), Acl::PERMISSION_READ); - } $comment->setMessage($message); $this->commentsManager->save($comment);