diff --git a/appinfo/routes.php b/appinfo/routes.php index 8a2b9be5fe..830ebbb153 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -140,17 +140,25 @@ ['name' => 'board_ocs#index', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'GET'], ['name' => 'board_ocs#read', 'url' => '/api/v{apiVersion}/board/{boardId}', 'verb' => 'GET'], ['name' => 'stack_ocs#index', 'url' => '/api/v{apiVersion}/stacks/{boardId}', 'verb' => 'GET'], + ['name' => 'stack_ocs#getArchived', 'url' => '/api/v{apiVersion}/stacks/{boardId}/archived', 'verb' => 'GET'], ['name' => 'board_ocs#create', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'POST'], ['name' => 'board_ocs#createForTeam', 'url' => '/api/v{apiVersion}/boards/team', 'verb' => 'POST'], + ['name' => 'board_ocs#update', 'url' => '/api/v{apiVersion}/boards/{boardId}', 'verb' => 'PUT'], ['name' => 'board_ocs#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'], + ['name' => 'board_ocs#leave', 'url' => '/api/v{apiVersion}/boards/{boardId}/leave', 'verb' => 'POST'], ['name' => 'card_ocs#create', 'url' => '/api/v{apiVersion}/cards', 'verb' => 'POST'], ['name' => 'card_ocs#update', 'url' => '/api/v{apiVersion}/cards/{cardId}', 'verb' => 'PUT'], + ['name' => 'card_ocs#delete', 'url' => '/api/v{apiVersion}/cards/{cardId}', 'verb' => 'DELETE'], ['name' => 'card_ocs#assignLabel', 'url' => '/api/v{apiVersion}/cards/{cardId}/label/{labelId}', 'verb' => 'POST'], ['name' => 'card_ocs#assignUser', 'url' => '/api/v{apiVersion}/cards/{cardId}/assign', 'verb' => 'POST'], ['name' => 'card_ocs#unAssignUser', 'url' => '/api/v{apiVersion}/cards/{cardId}/unassign', 'verb' => 'PUT'], ['name' => 'card_ocs#removeLabel', 'url' => '/api/v{apiVersion}/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'], ['name' => 'card_ocs#reorder', 'url' => '/api/v{apiVersion}/cards/{cardId}/reorder', 'verb' => 'PUT'], + ['name' => 'card_ocs#archive', 'url' => '/api/v{apiVersion}/cards/{cardId}/archive', 'verb' => 'PUT'], + ['name' => 'card_ocs#unarchive', 'url' => '/api/v{apiVersion}/cards/{cardId}/unarchive', 'verb' => 'PUT'], + ['name' => 'card_ocs#done', 'url' => '/api/v{apiVersion}/cards/{cardId}/done', 'verb' => 'PUT'], + ['name' => 'card_ocs#undone', 'url' => '/api/v{apiVersion}/cards/{cardId}/undone', 'verb' => 'PUT'], ['name' => 'card_ocs#assignDependentCard', 'url' => '/api/v{apiVersion}/cards/{cardId}/dependentCards/{dependentCardId}', 'verb' => 'POST'], ['name' => 'card_ocs#removeDependentCard', 'url' => '/api/v{apiVersion}/cards/{cardId}/dependentCards/{dependentCardId}', 'verb' => 'DELETE'], @@ -161,6 +169,8 @@ ['name' => 'attachment_ocs#getAll', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments', 'verb' => 'GET'], ['name' => 'attachment_ocs#create', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachment', 'verb' => 'POST'], + ['name' => 'attachment_ocs#createRemote', 'url' => '/api/v{apiVersion}/cards/{cardId}/remote-attachment', 'verb' => 'POST'], + ['name' => 'attachment_ocs#shareRemote', 'url' => '/api/v{apiVersion}/cards/{cardId}/share-attachment', 'verb' => 'POST'], ['name' => 'attachment_ocs#update', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'PUT'], ['name' => 'attachment_ocs#delete', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments/{type}:{attachmentId}', 'verb' => 'DELETE'], ['name' => 'attachment_ocs#restore', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments/{attachmentId}/restore', 'verb' => 'PUT'], diff --git a/lib/Controller/AttachmentOcsController.php b/lib/Controller/AttachmentOcsController.php index 734daef49b..9f787499b9 100644 --- a/lib/Controller/AttachmentOcsController.php +++ b/lib/Controller/AttachmentOcsController.php @@ -9,10 +9,13 @@ namespace OCA\Deck\Controller; +use OCA\Deck\BadRequestException; use OCA\Deck\NotImplementedException; use OCA\Deck\Service\AttachmentService; use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\ExternalBoardService; use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IRequest; @@ -23,6 +26,7 @@ public function __construct( IRequest $request, private AttachmentService $attachmentService, private BoardService $boardService, + private ExternalBoardService $externalBoardService, ) { parent::__construct($appName, $request); } @@ -37,19 +41,43 @@ private function ensureLocalBoard(?int $boardId): void { } #[NoAdminRequired] + #[PublicPage] public function getAll(int $cardId, ?int $boardId = null): DataResponse { - $this->ensureLocalBoard($boardId); + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->getAttachmentsFromRemote($board, $cardId)); + } $attachment = $this->attachmentService->findAll($cardId, true); return new DataResponse($attachment); } #[NoAdminRequired] public function create(int $cardId, string $type, string $data = '', ?int $boardId = null): DataResponse { - $this->ensureLocalBoard($boardId); + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->createAttachmentForRemote($board, $cardId)); + } $attachment = $this->attachmentService->create($cardId, $type, $data); return new DataResponse($attachment); } + #[NoAdminRequired] + #[PublicPage] + public function createRemote(int $cardId, string $token): DataResponse { + $this->attachmentService->acceptRemoteAttachment($cardId, $token); + return new DataResponse([]); + } + + #[NoAdminRequired] + public function shareRemote(int $cardId, int $boardId, string $path): DataResponse { + $board = $this->boardService->find($boardId, false); + if (!$board->getExternalId()) { + throw new BadRequestException('board is not federated'); + } + $token = $this->externalBoardService->shareAttachmentForRemote($board, $cardId, $path); + return new DataResponse(['token' => $token]); + } + #[NoAdminRequired] public function update(int $cardId, int $attachmentId, string $data, string $type = 'file', ?int $boardId = null): DataResponse { $this->ensureLocalBoard($boardId); @@ -58,8 +86,12 @@ public function update(int $cardId, int $attachmentId, string $data, string $typ } #[NoAdminRequired] + #[PublicPage] public function delete(int $cardId, int $attachmentId, string $type = 'file', ?int $boardId = null): DataResponse { - $this->ensureLocalBoard($boardId); + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->deleteAttachmentOnRemote($board, $cardId, $attachmentId, $type)); + } $attachment = $this->attachmentService->delete($cardId, $attachmentId, $type); return new DataResponse($attachment); } diff --git a/lib/Controller/BoardController.php b/lib/Controller/BoardController.php index 5c43ab7b12..5383e0adae 100644 --- a/lib/Controller/BoardController.php +++ b/lib/Controller/BoardController.php @@ -67,7 +67,12 @@ public function deleteUndo(int $boardId): Board { #[NoAdminRequired] public function leave(int $boardId) { - return $this->boardService->leave($boardId); + $localBoard = $this->boardService->find($boardId, true, true); + $result = $this->boardService->leave($boardId); + if ($localBoard->getExternalId() !== null) { + $this->externalBoardService->leaveBoardOnRemote($localBoard); + } + return $result; } #[NoAdminRequired] diff --git a/lib/Controller/BoardOcsController.php b/lib/Controller/BoardOcsController.php index fcfe09adbf..4d6d3f13b4 100644 --- a/lib/Controller/BoardOcsController.php +++ b/lib/Controller/BoardOcsController.php @@ -57,6 +57,18 @@ public function createForTeam(string $title, string $teamId, ?string $color = nu return new DataResponse($this->boardService->createForTeam($title, $this->userId, $color, $teamId)); } + #[NoAdminRequired] + #[PublicPage] + public function update(int $boardId, string $title, string $color, bool $archived): DataResponse { + $updatedBoard = $this->boardService->update($boardId, $title, $color, $archived); + + if ($updatedBoard->getExternalId()) { + return $this->externalBoardService->updateBoardOnRemote($updatedBoard, $title, $color, $archived); + } + + return new DataResponse($updatedBoard); + } + #[NoAdminRequired] public function addAcl(int $boardId, int $type, string $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage, ?string $remote = null): DataResponse { return new DataResponse($this->boardService->addAcl($boardId, $type, $participant, $permissionEdit, $permissionShare, $permissionManage)); @@ -66,4 +78,10 @@ public function addAcl(int $boardId, int $type, string $participant, bool $permi public function updateAcl(int $id, bool $permissionEdit, bool $permissionShare, bool $permissionManage): DataResponse { return new DataResponse($this->boardService->updateAcl($id, $permissionEdit, $permissionShare, $permissionManage)); } + + #[NoAdminRequired] + #[PublicPage] + public function leave(int $boardId): DataResponse { + return new DataResponse($this->boardService->leave($boardId)); + } } diff --git a/lib/Controller/CardOcsController.php b/lib/Controller/CardOcsController.php index 6dbdb28138..02a5835856 100644 --- a/lib/Controller/CardOcsController.php +++ b/lib/Controller/CardOcsController.php @@ -120,7 +120,7 @@ public function removeLabel(?int $boardId, int $cardId, int $labelId): DataRespo #[NoAdminRequired] #[PublicPage] - public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, ?int $boardId, array|string|null $owner = null, $archived = null, $startdate = null): DataResponse { + public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, int $boardId, array|string|null $owner = null, $archived = null, $startdate = null): DataResponse { $done = array_key_exists('done', $this->request->getParams()) ? new OptionalNullableValue($this->request->getParam('done', null)) : null; @@ -150,7 +150,9 @@ public function update(int $id, string $title, int $stackId, string $type, int $ $duedate, $deletedAt, $archived, - $done + $done, + $startdate, + $color, )); } } @@ -171,6 +173,18 @@ public function update(int $id, string $title, int $stackId, string $type, int $ )); } + #[NoAdminRequired] + #[PublicPage] + public function delete(int $cardId, ?int $boardId = null): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->deleteCardOnRemote($board, $cardId)); + } + } + return new DataResponse($this->cardService->delete($cardId)); + } + #[NoAdminRequired] #[PublicPage] public function reorder(int $cardId, int $stackId, int $order, ?int $boardId): DataResponse { @@ -183,6 +197,30 @@ public function reorder(int $cardId, int $stackId, int $order, ?int $boardId): D return new DataResponse($this->cardService->reorder($cardId, $stackId, $order)); } + #[NoAdminRequired] + #[PublicPage] + public function archive(int $cardId, int $boardId): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->archiveCardOnRemote($board, $cardId)); + } + } + return new DataResponse($this->cardService->archive($cardId)); + } + + #[NoAdminRequired] + #[PublicPage] + public function unarchive(int $cardId, int $boardId): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->unarchiveCardOnRemote($board, $cardId)); + } + } + return new DataResponse($this->cardService->unarchive($cardId)); + } + #[NoAdminRequired] #[PublicPage] public function assignDependentCard(int $cardId, int $dependentCardId, ?int $boardId = null): DataResponse { @@ -206,4 +244,28 @@ public function removeDependentCard(int $cardId, int $dependentCardId, ?int $boa } return new DataResponse($this->cardService->removeDependentCard($cardId, $dependentCardId)); } + + #[NoAdminRequired] + #[PublicPage] + public function done(int $cardId, ?int $boardId): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->setDoneCardOnRemote($board, $cardId)); + } + } + return new DataResponse($this->cardService->done($cardId)); + } + + #[NoAdminRequired] + #[PublicPage] + public function undone(int $cardId, ?int $boardId): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->setUndoneCardOnRemote($board, $cardId)); + } + } + return new DataResponse($this->cardService->undone($cardId)); + } } diff --git a/lib/Controller/CommentsApiController.php b/lib/Controller/CommentsApiController.php index 1794db5eae..8885660c2f 100644 --- a/lib/Controller/CommentsApiController.php +++ b/lib/Controller/CommentsApiController.php @@ -7,9 +7,12 @@ namespace OCA\Deck\Controller; +use OCA\Deck\Service\BoardService; use OCA\Deck\Service\CommentService; +use OCA\Deck\Service\ExternalBoardService; use OCA\Deck\StatusException; use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IRequest; @@ -22,6 +25,9 @@ public function __construct( string $appName, IRequest $request, private CommentService $commentService, + private BoardService $boardService, + private ExternalBoardService $externalBoardService, + private ?string $userId, string $corsMethods = 'PUT, POST, GET, DELETE, PATCH', string $corsAllowedHeaders = 'Authorization, Content-Type, Accept', int $corsMaxAge = 1728000, @@ -33,7 +39,14 @@ public function __construct( * @throws StatusException */ #[NoAdminRequired] - public function list(int $cardId, int $limit = 20, int $offset = 0): DataResponse { + #[PublicPage] + public function list(int $cardId, int $limit = 20, int $offset = 0, ?int $boardId = null): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->getCardCommentsFromRemote($board, $cardId, $limit, $offset)); + } + } return $this->commentService->list($cardId, $limit, $offset); } @@ -41,7 +54,15 @@ public function list(int $cardId, int $limit = 20, int $offset = 0): DataRespons * @throws StatusException */ #[NoAdminRequired] - public function create(int $cardId, string $message, int $parentId = 0): DataResponse { + #[PublicPage] + public function create(int $cardId, string $message, int $parentId = 0, ?int $boardId = null): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->createCardCommentOnRemote($board, $cardId, $message, $parentId)); + } + } + return $this->commentService->create($cardId, $message, $parentId); } @@ -49,7 +70,14 @@ public function create(int $cardId, string $message, int $parentId = 0): DataRes * @throws StatusException */ #[NoAdminRequired] - public function update(int $cardId, int $commentId, string $message): DataResponse { + #[PublicPage] + public function update(int $cardId, int $commentId, string $message, ?int $boardId = null): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->updateCardCommentOnRemote($board, $cardId, $commentId, $message)); + } + } return $this->commentService->update($cardId, $commentId, $message); } @@ -57,7 +85,14 @@ public function update(int $cardId, int $commentId, string $message): DataRespon * @throws StatusException */ #[NoAdminRequired] - public function delete(int $cardId, int $commentId): DataResponse { + #[PublicPage] + public function delete(int $cardId, int $commentId, ?int $boardId = null): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + return new DataResponse($this->externalBoardService->deleteCardCommentOnRemote($board, $cardId, $commentId)); + } + } return $this->commentService->delete($cardId, $commentId); } } diff --git a/lib/Controller/StackOcsController.php b/lib/Controller/StackOcsController.php index cada24f8e5..ecf73ae175 100644 --- a/lib/Controller/StackOcsController.php +++ b/lib/Controller/StackOcsController.php @@ -48,7 +48,7 @@ public function create(string $title, int $boardId, int $order = 0):DataResponse } else { $stack = $this->stackService->create($title, $boardId, $order); return new DataResponse($stack); - }; + } } #[NoAdminRequired] @@ -91,4 +91,16 @@ public function reorder(int $stackId, int $order, ?int $boardId):DataResponse { return new DataResponse($stacks); } + #[NoAdminRequired] + #[PublicPage] + public function getArchived(int $boardId): DataResponse { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + $stacks = $this->externalBoardService->getArchivedStacksFromRemote($board); + return new DataResponse($stacks); + } + $stacks = $this->stackService->findAllArchived($boardId); + return new DataResponse($stacks); + } + } diff --git a/lib/Cron/ScanFederatedAttachment.php b/lib/Cron/ScanFederatedAttachment.php new file mode 100644 index 0000000000..5cf3bae0e0 --- /dev/null +++ b/lib/Cron/ScanFederatedAttachment.php @@ -0,0 +1,83 @@ +userManager->get($userId); + + if ($user === null) { + return; + } + + $externalShare = $this->externalShareManager->getShare((string)$shareId, $user); + + if ($externalShare === false) { + return; + } + + $this->deckShareProvider->mountExternalShare($externalShare, $user); + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + + $node = $userFolder->get($externalShare->getMountpoint()); + $files = $userFolder->getById($node->getId()); + + if (count($files) === 0) { + // Retry later, maybe the share is not yet mounted + $this->jobList->add(ScanFederatedAttachment::class, [ + 'cardId' => $cardId, + 'shareId' => $shareId, + 'userId' => $userId, + ]); + + // Scan the files to make sure the share is mounted and the files are available + exec('php ' . \OC::$SERVERROOT . '/occ files:scan --path="/' . $userId . '/files' . $externalShare->getMountpoint() . '"'); + return; + } + + $share = $this->shareManager->newShare(); + $share->setNode($node); + $share->setShareType(ISHARE::TYPE_DECK); + $share->setSharedWith((string)$cardId); + $share->setPermissions(Constants::PERMISSION_READ); + $remote = substr($externalShare->getRemote(), 0, strrpos($externalShare->getRemote(), '/')); + $federatedId = $externalShare->getOwner() . '@' . $remote; + $share->setSharedBy($federatedId); + $share->setShareOwner($externalShare->getUser()); + $this->shareManager->createShare($share); + } +} diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 47f3e58af3..f47c7a8b70 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -14,6 +14,7 @@ use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Federation\ICloudIdManager; use OCP\ICache; use OCP\ICacheFactory; use OCP\IDBConnection; @@ -36,6 +37,8 @@ class CardMapper extends QBMapper implements IPermissionMapper { private $notificationManager; /** @var ICache */ private $cache; + /** @var ICloudIdManager */ + private $cloudIdManager; private $databaseType; private $database4ByteSupport; @@ -46,6 +49,7 @@ public function __construct( IGroupManager $groupManager, IManager $notificationManager, ICacheFactory $cacheFactory, + ICloudIdManager $cloudIdManager, $databaseType = 'sqlite3', $database4ByteSupport = true, ) { @@ -55,6 +59,7 @@ public function __construct( $this->groupManager = $groupManager; $this->notificationManager = $notificationManager; $this->cache = $cacheFactory->createDistributed('deck-cardMapper'); + $this->cloudIdManager = $cloudIdManager; $this->databaseType = $databaseType; $this->database4ByteSupport = $database4ByteSupport; } @@ -747,10 +752,15 @@ public function findBoardId(int $id): ?int { public function mapOwner(Card &$card) { $userManager = $this->userManager; - $card->resolveRelation('owner', function ($owner) use (&$userManager) { + $cloudIdManager = $this->cloudIdManager; + $card->resolveRelation('owner', function ($owner) use (&$userManager, &$cloudIdManager) { if ($userManager->userExists($owner)) { return new User($owner, $this->userManager); } + if ($cloudIdManager->isValidCloudId($owner)) { + $cloudId = $cloudIdManager->resolveCloudId($owner); + return new FederatedUser($cloudId); + } return null; }); } diff --git a/lib/Federation/DeckFederationProxy.php b/lib/Federation/DeckFederationProxy.php index 2ab77fd515..8386b918ca 100644 --- a/lib/Federation/DeckFederationProxy.php +++ b/lib/Federation/DeckFederationProxy.php @@ -10,6 +10,10 @@ use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ServerException; use OC\Http\Client\Response; +use OCA\Deck\BadRequestException; +use OCA\Deck\NoPermissionException; +use OCA\Deck\NotFoundException; +use OCA\Deck\StatusException; use OCP\AppFramework\Http; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; @@ -60,7 +64,7 @@ protected function prependProtocolIfNotAvailable(string $url): string { /** * @param 'get'|'post'|'put'|'delete' $verb - * @throws \Exception + * @throws \Exception|StatusException */ protected function request( string $verb, @@ -96,7 +100,18 @@ protected function request( $clientException = new \Exception($e->getMessage(), $status, $e); $this->logger->debug('Client error from remote', ['exception' => $clientException]); - return new Response($e->getResponse(), false); + + switch ($status) { + case 400: + throw new BadRequestException($data['ocs']['meta']['message'] ?? 'Bad request'); + case 401: + case 403: + throw new NoPermissionException($data['ocs']['meta']['message'] ?? 'No permission'); + case 404: + throw new NotFoundException($data['ocs']['meta']['message'] ?? 'Not found'); + default: + return new Response($e->getResponse(), false); + } } catch (ServerException|\Throwable $e) { $serverException = new \Exception($e->getMessage(), $e->getCode(), $e); $this->logger->error('Could not reach remote', ['exception' => $serverException]); diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php index a871119f22..8aedb303db 100644 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -11,6 +11,7 @@ use OCA\Deck\AppInfo\Application; use OCA\Deck\BadRequestException; use OCA\Deck\Cache\AttachmentCacheHelper; +use OCA\Deck\Cron\ScanFederatedAttachment; use OCA\Deck\Db\Acl; use OCA\Deck\Db\Attachment; use OCA\Deck\Db\AttachmentMapper; @@ -19,10 +20,13 @@ use OCA\Deck\InvalidAttachmentType; use OCA\Deck\NoPermissionException; use OCA\Deck\NotFoundException; +use OCA\Deck\Sharing\DeckShareProvider; use OCA\Deck\StatusException; use OCA\Deck\Validators\AttachmentServiceValidator; use OCP\AppFramework\Db\IMapperException; use OCP\AppFramework\Http\Response; +use OCP\BackgroundJob\IJobList; +use OCP\Federation\ICloudIdManager; use OCP\IL10N; use OCP\IUserManager; use Psr\Container\ContainerExceptionInterface; @@ -44,6 +48,9 @@ public function __construct( private readonly IL10N $l10n, private readonly ActivityManager $activityManager, private readonly AttachmentServiceValidator $attachmentServiceValidator, + private readonly DeckShareProvider $deckShareProvider, + private readonly ICloudIdManager $cloudIdManager, + private readonly IJobList $jobList, ) { // Register shipped attachment services // TODO: move this to a plugin based approach once we have different types of attachments @@ -205,6 +212,22 @@ public function create(int $cardId, string $type, string $data = '') { return $attachment; } + public function acceptRemoteAttachment(int $cardId, string $token): void { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); + + $userId = $this->permissionService->getUserId(); + if (!$this->cloudIdManager->isValidCloudId($userId)) { + throw new NoPermissionException('Only federated user is allowed'); + } + + $externalShare = $this->deckShareProvider->ensureAcceptRemoteShare($token, $this->cloudIdManager->resolveCloudId($userId)->getRemote()); + $this->jobList->add(ScanFederatedAttachment::class, [ + 'cardId' => $cardId, + 'shareId' => $externalShare->getId(), + 'userId' => $externalShare->getUser(), + ]); + } + /** * Apply import side effects to keep attachment behavior consistent with regular create flow. */ diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 2edb86a0b4..80864ca406 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -571,7 +571,8 @@ public function leave(int $boardId): ?Acl { throw new BadRequestException('Board owner cannot leave board'); } - $acl = $this->aclMapper->findParticipantFromBoard($boardId, Acl::PERMISSION_TYPE_USER, $this->userId); + $userId = $this->userId ?? $this->permissionService->getUserId(); + $acl = $this->aclMapper->findParticipantFromBoard($boardId, Acl::PERMISSION_TYPE_USER, $userId); if (!$acl) { throw new BadRequestException('Not a participant of this board'); diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 695a249d68..930a5eb0d0 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -87,7 +87,7 @@ public function enrichCards(array $cards): array { // TODO We should find a better way just to get the comment count so we can save 1-3 queries per card here $countComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId()); - $lastRead = $countComments > 0 ? $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user) : null; + $lastRead = $countComments > 0 && $user ? $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user) : null; $countUnreadComments = $lastRead ? $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead) : 0; $card->setCommentsUnread($countUnreadComments); $card->setCommentsCount($countComments); diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index e5d4bc140e..0cdd1e0dd7 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -18,6 +18,8 @@ use OCP\Comments\ICommentsManager; use OCP\Comments\MessageTooLongException; use OCP\Comments\NotFoundException as CommentNotFoundException; +use OCP\Federation\ICloudIdManager; +use OCP\IURLGenerator; use OCP\IUserManager; use OutOfBoundsException; use Psr\Log\LoggerInterface; @@ -30,6 +32,8 @@ public function __construct( private CardMapper $cardMapper, private IUserManager $userManager, private LoggerInterface $logger, + private ICloudIdManager $cloudIdManager, + private IURLGenerator $urlGenerator, private ?string $userId, ) { } @@ -110,7 +114,7 @@ public function create(int $cardId, string $message, int $replyTo = 0): DataResp } try { - $comment = $this->commentsManager->create('users', $this->userId, Application::COMMENT_ENTITY_TYPE, (string)$cardId); + $comment = $this->commentsManager->create('users', $this->userId ?? $this->permissionService->getUserId(), Application::COMMENT_ENTITY_TYPE, (string)$cardId); $comment->setMessage($message); $comment->setVerb('comment'); $comment->setParentId((string)$replyTo); @@ -128,7 +132,8 @@ public function create(int $cardId, string $message, int $replyTo = 0): DataResp public function update(int $cardId, int $commentId, string $message): DataResponse { $comment = $this->get($cardId, $commentId); - if ($comment->getActorType() !== 'users' || $comment->getActorId() !== $this->userId) { + $userId = $this->userId ?? $this->permissionService->getUserId(); + if ($comment->getActorType() !== 'users' || $comment->getActorId() !== $userId) { throw new NoPermissionException('Only authors are allowed to edit their comment.'); } @@ -148,7 +153,8 @@ public function delete(int $cardId, int $commentId): DataResponse { } catch (CommentNotFoundException $e) { throw new NotFoundException('No comment found.'); } - if ($comment->getActorType() !== 'users' || $comment->getActorId() !== $this->userId) { + $userId = $this->userId ?? $this->permissionService->getUserId(); + if ($comment->getActorType() !== 'users' || $comment->getActorId() !== $userId) { throw new NoPermissionException('Only authors are allowed to edit their comment.'); } $this->commentsManager->delete((string)$commentId); @@ -163,20 +169,28 @@ private function formatComment(IComment $comment, bool $addReplyTo = false): arr 'objectId' => (int)$comment->getObjectId(), 'message' => $comment->getMessage(), 'actorId' => $comment->getActorId(), + 'actorRemote' => $this->cloudIdManager->isValidCloudId($comment->getActorId()) ? $this->cloudIdManager->resolveCloudId($comment->getActorId())->getRemote() : null, 'actorType' => $comment->getActorType(), 'actorDisplayName' => $actorDisplayName, 'creationDateTime' => $comment->getCreationDateTime()->format(\DateTime::ATOM), 'mentions' => array_map(function ($mention) { + $remote = $this->cloudIdManager->isValidCloudId($mention['id']) ? $this->cloudIdManager->resolveCloudId($mention['id'])->getRemote() : null; + try { $displayName = $this->commentsManager->resolveDisplayName($mention['type'], $mention['id']); } catch (OutOfBoundsException $e) { $this->logger->warning('Mention type not registered, can not resolve display name.', ['exception' => $e, 'mention_type' => $mention['type']]); // No display name, upon client's discretion what to display. - $displayName = ''; + $displayName = $mention['id'] ?? ''; + if ($remote === $this->urlGenerator->getBaseUrl()) { + $uid = $this->cloudIdManager->resolveCloudId($mention['id'])->getUser(); + $displayName = $this->commentsManager->resolveDisplayName('user', $uid); + } } return [ 'mentionId' => $mention['id'], + 'mentionRemote' => $remote, 'mentionType' => $mention['type'], 'mentionDisplayName' => $displayName ]; diff --git a/lib/Service/ExternalBoardService.php b/lib/Service/ExternalBoardService.php index fd48517ec1..734ec9fb33 100644 --- a/lib/Service/ExternalBoardService.php +++ b/lib/Service/ExternalBoardService.php @@ -16,6 +16,7 @@ use OCA\Deck\Db\User; use OCA\Deck\Federation\DeckFederationProxy; use OCA\Deck\Model\OptionalNullableValue; +use OCA\Deck\Sharing\DeckShareProvider; use OCP\AppFramework\Http\DataResponse; use OCP\Federation\ICloudIdManager; use OCP\IURLGenerator; @@ -27,10 +28,11 @@ public function __construct( private ICloudIdManager $cloudIdManager, private DeckFederationProxy $proxy, private ConfigService $configService, - private BoardService $boardService, private PermissionService $permissionService, private BoardMapper $boardMapper, private IURLGenerator $urlGenerator, + private DeckShareProvider $deckShareProvider, + private FilesAppService $filesAppService, private ?string $userId, ) { } @@ -43,6 +45,15 @@ public function getExternalBoardFromRemote(Board $localBoard):DataResponse { $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/board/' . $localBoard->getExternalId(); $resp = $this->proxy->get($participantCloudId->getId(), $shareToken, $url); $ocs = $this->proxy->getOCSData($resp); + + // Sync local board data with remote data + if ($ocs['title'] !== $localBoard->getTitle() || $ocs['color'] !== $localBoard->getColor() || $ocs['archived'] !== $localBoard->isArchived()) { + $localBoard->setTitle($ocs['title']); + $localBoard->setColor($ocs['color']); + $localBoard->setArchived($ocs['archived']); + $this->boardMapper->update($localBoard); + } + return new DataResponse($this->LocalizeRemoteBoard($ocs, $localBoard)); } public function getExternalStacksFromRemote(Board $localBoard):DataResponse { @@ -63,7 +74,8 @@ public function localizeRemoteUser(Board $localBoard, array $user): array|User|F } // if it's already a valid cloud id the user originates from a third instance and we pass it as is if ($this->cloudIdManager->isValidCloudId($user['uid'])) { - if ($user['remote'] == $this->urlGenerator->getBaseUrl()) { + $cloudId = $this->cloudIdManager->resolveCloudId($user['uid']); + if ($cloudId->getRemote() == $this->urlGenerator->getBaseUrl()) { // local user from remote: return as local user $localuid = $this->cloudIdManager->resolveCloudId($user['uid'])->getUser(); return new User($localuid, $this->userManager); @@ -85,7 +97,8 @@ public function LocalizeRemoteStacks(array $stacks, Board $localBoard) { $stack['cards'][$j]['assignedUsers'] = array_map(function ($assignment) use ($localBoard) { $assignment['participant'] = $this->localizeRemoteUser($localBoard, $assignment['participant']); return $assignment; - }, $card['assignedUsers']); + }, $card['assignedUsers'] ?? []); + $stack['cards'][$j]['owner'] = $this->localizeRemoteUser($localBoard, $card['owner']); } $stacks[$i] = $stack; } @@ -98,6 +111,7 @@ public function LocalizeRemoteBoard(array $remoteBoard, Board $localBoard) { $remoteBoard['acl'] = $localBoard->getAcl(); $remoteBoard['permissions'] = $localBoard->getPermissions(); $remoteBoard['users'] = $this->localizeRemoteUsers($remoteBoard['users'], $localBoard); + $remoteBoard['externalId'] = $localBoard->getExternalId(); return $remoteBoard; } @@ -110,6 +124,56 @@ public function localizeRemoteUsers(array $users, Board $localBoard) { return $localizedUsers; } + public function localizeRemoteComments(Board $localBoard, array $comments): array { + foreach ($comments as $i => $comment) { + // Localize actors + $localizedActor = $this->localizeRemoteUser($localBoard, ['uid' => $comment['actorId'], 'remote' => $comment['actorRemote']]); + if ($localizedActor instanceof FederatedUser) { + $comments[$i]['actorDisplayName'] = $localizedActor->getCloudId()->getId(); + $comments[$i]['actorId'] = $localizedActor->getCloudId()->getId(); + $comments[$i]['actorRemote'] = $localizedActor->getCloudId()->getRemote(); + } + if ($localizedActor instanceof User) { + $comments[$i]['actorDisplayName'] = $localizedActor->getDisplayName(); + $comments[$i]['actorId'] = $localizedActor->getUID(); + $comments[$i]['actorRemote'] = null; + } + if ($comment['replyTo']) { + $localizedReplyActor = $this->localizeRemoteUser($localBoard, ['uid' => $comment['replyTo']['actorId'], 'remote' => $comment['replyTo']['actorRemote']]); + if ($localizedReplyActor instanceof FederatedUser) { + $comments[$i]['replyTo']['actorDisplayName'] = $localizedReplyActor->getCloudId()->getId(); + $comments[$i]['replyTo']['actorId'] = $localizedReplyActor->getCloudId()->getId(); + $comments[$i]['replyTo']['actorRemote'] = $localizedReplyActor->getCloudId()->getRemote(); + } + if ($localizedReplyActor instanceof User) { + $comments[$i]['replyTo']['actorDisplayName'] = $localizedReplyActor->getDisplayName(); + $comments[$i]['replyTo']['actorId'] = $localizedReplyActor->getUID(); + $comments[$i]['replyTo']['actorRemote'] = null; + } + } + + // Localize mentions + foreach ($comment['mentions'] as $j => $mention) { + $localizedMention = $this->localizeRemoteUser($localBoard, ['uid' => $mention['mentionId'], 'remote' => $mention['mentionRemote']]); + if ($localizedMention instanceof User) { + $comments[$i]['message'] = str_replace('@"federated_user/' . $mention['mentionId'], '@"' . $localizedMention->getUID(), $comment['message']); + $comments[$i]['mentions'][$j]['mentionDisplayName'] = $localizedMention->getDisplayName(); + $comments[$i]['mentions'][$j]['mentionId'] = $localizedMention->getUID(); + $comments[$i]['mentions'][$j]['mentionRemote'] = null; + $comments[$i]['mentions'][$j]['mentionType'] = 'user'; + } + if ($localizedMention instanceof FederatedUser) { + $comments[$i]['message'] = str_replace('@' . $mention['mentionId'], '@federated_user/' . $localizedMention->getUID(), $comment['message']); + $comments[$i]['mentions'][$j]['mentionDisplayName'] = $localizedMention->getCloudId()->getId(); + $comments[$i]['mentions'][$j]['mentionId'] = $localizedMention->getCloudId()->getId(); + $comments[$i]['mentions'][$j]['mentionRemote'] = $localizedMention->getCloudId()->getRemote(); + $comments[$i]['mentions'][$j]['mentionType'] = 'federated_user'; + } + } + } + return $comments; + } + public function createCardOnRemote( Board $localBoard, string $title, @@ -154,6 +218,8 @@ public function updateCardOnRemote( ?int $deletedAt = null, ?bool $archived = null, ?OptionalNullableValue $done = null, + ?string $startdate = null, + ?string $color = null, ): array { $this->configService->ensureFederationEnabled(); $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); @@ -173,12 +239,26 @@ public function updateCardOnRemote( 'deletedAt' => $deletedAt, 'archived' => $archived, 'done' => $done->getValue() ?? null, + 'startdate' => $startdate, + 'color' => $color, 'boardId' => $localBoard->getExternalId(), ]; $resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params); return $this->proxy->getOcsData($resp); } + public function deleteCardOnRemote(Board $localBoard, int $cardId): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId; + $resp = $this->proxy->delete($ownerCloudId->getId(), $shareToken, $url, [ + 'boardId' => $localBoard->getExternalId(), + ]); + return $this->proxy->getOcsData($resp); + } + public function assignLabelOnRemote(Board $localBoard, int $cardId, int $labelId): array { $this->configService->ensureFederationEnabled(); $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); @@ -351,4 +431,264 @@ public function reorderStackOnRemote(Board $localBoard, int $stackId, int $order $resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params); return $this->proxy->getOcsData($resp); } + + public function getArchivedStacksFromRemote(Board $localBoard): array { + $this->configService->ensureFederationEnabled(); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/stacks/' . $localBoard->getExternalId() . '/archived'; + $resp = $this->proxy->get($participantCloudId->getId(), $shareToken, $url); + $ocs = $this->proxy->getOCSData($resp); + return $this->LocalizeRemoteStacks($ocs, $localBoard); + } + + public function archiveCardOnRemote(Board $localBoard, int $cardId): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/archive'; + $params = [ + 'boardId' => $localBoard->getExternalId(), + ]; + $resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params); + return $this->proxy->getOcsData($resp); + } + + public function unarchiveCardOnRemote(Board $localBoard, int $cardId): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/unarchive'; + $params = [ + 'boardId' => $localBoard->getExternalId(), + ]; + $resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params); + return $this->proxy->getOcsData($resp); + } + + public function setDoneCardOnRemote(Board $localBoard, int $cardId): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/done'; + $params = [ + 'boardId' => $localBoard->getExternalId(), + ]; + $resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params); + return $this->proxy->getOcsData($resp); + } + + public function setUndoneCardOnRemote(Board $localBoard, int $cardId): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/undone'; + $params = [ + 'boardId' => $localBoard->getExternalId(), + ]; + $resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params); + return $this->proxy->getOcsData($resp); + } + + public function getCardCommentsFromRemote(Board $localBoard, int $cardId, int $limit = 20, int $offset = 0): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_READ, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/comments'; + $params = [ + 'boardId' => $localBoard->getExternalId(), + 'limit' => $limit, + 'offset' => $offset, + ]; + $resp = $this->proxy->get($participantCloudId->getId(), $shareToken, $url, $params); + $comments = $this->proxy->getOcsData($resp); + return $this->localizeRemoteComments($localBoard, $comments); + } + + public function createCardCommentOnRemote(Board $localBoard, int $cardId, string $message, int $parentId = 0): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_READ, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/comments'; + + $params = [ + 'boardId' => $localBoard->getExternalId(), + 'message' => $message, + 'parentId' => $parentId, + ]; + $resp = $this->proxy->post($participantCloudId->getId(), $shareToken, $url, $params); + $newComment = $this->proxy->getOcsData($resp); + return $this->localizeRemoteComments($localBoard, [$newComment])[0]; + } + + public function updateCardCommentOnRemote(Board $localBoard, int $cardId, int $commentId, string $message): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_READ, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/comments/' . $commentId; + $params = [ + 'boardId' => $localBoard->getExternalId(), + 'message' => $message, + ]; + $resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params); + $updatedComment = $this->proxy->getOcsData($resp); + return $this->localizeRemoteComments($localBoard, [$updatedComment])[0]; + } + + public function deleteCardCommentOnRemote(Board $localBoard, int $cardId, int $commentId): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_READ, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/comments/' . $commentId; + $params = [ + 'boardId' => $localBoard->getExternalId(), + ]; + $resp = $this->proxy->delete($participantCloudId->getId(), $shareToken, $url, $params); + return $this->proxy->getOcsData($resp); + } + + public function updateBoardOnRemote(Board $localBoard, string $title, string $color, bool $archived): DataResponse { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_MANAGE, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/boards/' . $localBoard->getExternalId(); + $params = [ + 'title' => $title, + 'color' => $color, + 'archived' => $archived, + ]; + $resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params); + $updatedBoard = $this->proxy->getOcsData($resp); + return new DataResponse($this->LocalizeRemoteBoard($updatedBoard, $localBoard)); + } + + public function leaveBoardOnRemote(Board $localBoard): void { + $this->configService->ensureFederationEnabled(); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/boards/' . $localBoard->getExternalId() . '/leave'; + $this->proxy->post($participantCloudId->getId(), $shareToken, $url); + } + + public function localizeRemoteAttachments(Board $localBoard, array $attachments): array { + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + foreach ($attachments as $i => $attachment) { + $createdByUser = $this->localizeRemoteUser($localBoard, ['uid' => $attachment['createdBy'], 'remote' => $attachment['extendedData']['createdByRemote']]); + if ($createdByUser instanceof FederatedUser) { + $attachments[$i]['createdBy'] = $createdByUser->getCloudId()->getId(); + $attachments[$i]['createdByRemote'] = $createdByUser->getCloudId()->getRemote(); + $attachments[$i]['extendedData']['attachmentCreator']['id'] = $createdByUser->getCloudId()->getId(); + $attachments[$i]['extendedData']['attachmentCreator']['remote'] = $createdByUser->getCloudId()->getRemote(); + $attachments[$i]['extendedData']['attachmentCreator']['displayName'] = $createdByUser->getCloudId()->getId(); + } + if ($createdByUser instanceof User) { + $attachments[$i]['createdBy'] = $createdByUser->getUID(); + $attachments[$i]['createdByRemote'] = null; + $attachments[$i]['extendedData']['attachmentCreator']['id'] = $createdByUser->getUID(); + $attachments[$i]['extendedData']['attachmentCreator']['remote'] = null; + $attachments[$i]['extendedData']['attachmentCreator']['displayName'] = $createdByUser->getDisplayName(); + } + if (!empty($attachment['extendedData']['shareToken'])) { + if ($createdByUser instanceof User) { + $file = $this->deckShareProvider->getLocalFile($attachment['extendedData']['shareToken']); + } else { + $file = $this->deckShareProvider->getRemoteFile($attachment['extendedData']['shareToken'], $ownerCloudId->getRemote()); + } + + if (empty($file)) { + unset($attachments[$i]); + continue; + } + + $attachments[$i]['extendedData'] = array_merge($attachments[$i]['extendedData'], $file); + unset($attachments[$i]['extendedData']['shareToken']); + } + } + return $attachments; + } + + public function getAttachmentsFromRemote(Board $localBoard, int $cardId): array { + $this->configService->ensureFederationEnabled(); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/attachments'; + $resp = $this->proxy->get($participantCloudId->getId(), $shareToken, $url, [ + 'boardId' => $localBoard->getExternalId(), + ]); + $attachments = $this->proxy->getOcsData($resp); + return $this->localizeRemoteAttachments($localBoard, $attachments); + } + + public function createAttachmentForRemote(Board $localBoard, int $cardId): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + + // Upload and create remote share + $shareWithFederatedId = $ownerCloudId->getId(); + $fileShareToken = $this->filesAppService->createForRemote($shareWithFederatedId); + + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/remote-attachment'; + $resp = $this->proxy->post($participantCloudId->getId(), $shareToken, $url, [ + 'boardId' => $localBoard->getExternalId(), + 'cardId' => $cardId, + 'token' => $fileShareToken, + ]); + return $this->proxy->getOcsData($resp); + } + + public function shareAttachmentForRemote(Board $localBoard, int $cardId, string $path): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + + // Create remote share + $shareWithFederatedId = $ownerCloudId->getId(); + $fileShareToken = $this->filesAppService->createRemoteShareForPath($path, $shareWithFederatedId); + + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/remote-attachment'; + $resp = $this->proxy->post($participantCloudId->getId(), $shareToken, $url, [ + 'boardId' => $localBoard->getExternalId(), + 'cardId' => $cardId, + 'token' => $fileShareToken, + ]); + return $this->proxy->getOcsData($resp); + } + + public function deleteAttachmentOnRemote(Board $localBoard, int $cardId, int $attachmentId, string $type = 'file'): array { + $this->configService->ensureFederationEnabled(); + $this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false); + $shareToken = $localBoard->getShareToken(); + $ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner()); + $url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/attachments/' . $type . ':' . $attachmentId; + $resp = $this->proxy->delete($ownerCloudId->getId(), $shareToken, $url, [ + 'boardId' => $localBoard->getExternalId(), + ]); + return $this->proxy->getOcsData($resp); + } } diff --git a/lib/Service/FilesAppService.php b/lib/Service/FilesAppService.php index d6824c51b6..c331302c26 100644 --- a/lib/Service/FilesAppService.php +++ b/lib/Service/FilesAppService.php @@ -19,12 +19,14 @@ use OCP\AppFramework\Http\StreamResponse; use OCP\Constants; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Federation\ICloudIdManager; use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IFilenameValidator; use OCP\Files\IMimeTypeDetector; use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; +use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\IDBConnection; use OCP\IL10N; @@ -43,6 +45,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService { * Reason is not having access to the original filename because of early sanitization in the request lifecycle. */ private const SANITIZED_CHAR_MAPPING = [':' => '/']; + private const FEDERATED_ATTACHMENT_TARGET_PREFIX = 'deck-card-attachment-'; private IRequest $request; private IRootFolder $rootFolder; @@ -58,6 +61,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService { private LoggerInterface $logger; private IDBConnection $connection; private IFilenameValidator $filenameValidator; + private ICloudIdManager $cloudIdManager; public function __construct( IRequest $request, @@ -73,6 +77,7 @@ public function __construct( LoggerInterface $logger, IDBConnection $connection, IFilenameValidator $filenameValidator, + ICloudIdManager $cloudIdManager, ?string $userId, ) { $this->request = $request; @@ -89,10 +94,12 @@ public function __construct( $this->logger = $logger; $this->connection = $connection; $this->filenameValidator = $filenameValidator; + $this->cloudIdManager = $cloudIdManager; } public function listAttachments(int $cardId): array { - $shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0); + $userId = $this->userId ?? $this->permissionService->getUserId(); + $shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0, $userId); return array_filter(array_map(function (IShare $share) use ($cardId) { try { $file = $share->getNode(); @@ -147,26 +154,91 @@ public function getAttachmentCountForCards(array $cardIds): array { } public function extendData(Attachment $attachment) { - $userFolder = $this->rootFolder->getUserFolder($this->userId); $share = $this->getShareForAttachment($attachment); - $files = $userFolder->getById($share->getNode()->getId()); - if (count($files) === 0) { - return $attachment; + $node = $share->getNode(); + + $requestUserId = $this->permissionService->getUserId(); + if ($this->cloudIdManager->isValidCloudId($requestUserId)) { + if ($share->getSharedBy() === $requestUserId) { + $externalShare = $this->shareProvider->findRootExternalShare($share, $requestUserId); + $attachment->setExtendedData([ + 'data' => $node->getName(), + 'shareToken' => $externalShare ? $externalShare->getRefreshToken() : null, + ]); + } else { + $shareToken = $this->ensureFederatedAttachmentShare($share, $node, $requestUserId); + $attachment->setExtendedData([ + 'shareToken' => $shareToken, + ]); + } } - $file = array_shift($files); - $attachment->setExtendedData([ - 'path' => $userFolder->getRelativePath($file->getPath()), - 'fileid' => $file->getId(), - 'data' => $file->getName(), - 'filesize' => $file->getSize(), - 'mimetype' => $file->getMimeType(), - 'info' => pathinfo($file->getName()), - 'hasPreview' => $this->preview->isAvailable($file), - 'permissions' => $share->getPermissions(), - ]); + + if ($this->userId !== null) { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $files = $userFolder->getById($node->getId()); + if (count($files) === 0) { + return $attachment; + } + $file = array_shift($files); + $attachment->setExtendedData([ + 'path' => $userFolder->getRelativePath($file->getPath()), + 'fileid' => $file->getId(), + 'data' => $file->getName(), + 'filesize' => $file->getSize(), + 'mimetype' => $file->getMimeType(), + 'info' => pathinfo($file->getName()), + 'hasPreview' => $this->preview->isAvailable($file), + 'permissions' => $share->getPermissions(), + ]); + } + return $attachment; } + private function ensureFederatedAttachmentShare(IShare $attachmentShare, Node $file, string $federatedCloudId): string { + if ($shareToken = $this->findFederatedAttachmentShareToken($file->getId(), $federatedCloudId)) { + return $shareToken; + } + + $share = $this->shareManager->newShare(); + $share->setNode($file); + $share->setShareType(IShare::TYPE_REMOTE); + $share->setSharedWith($federatedCloudId); + $share->setPermissions(Constants::PERMISSION_READ); + $share->setSharedBy($attachmentShare->getShareOwner()); + $share->setShareOwner($attachmentShare->getShareOwner()); + + $createdShare = $this->shareManager->createShare($share); + + $qb = $this->connection->getQueryBuilder(); + $qb->update('share') + ->set('parent', $qb->createNamedParameter($attachmentShare->getId(), IQueryBuilder::PARAM_INT)) + ->andWhere($qb->expr()->eq('id', $qb->createNamedParameter($createdShare->getId(), IQueryBuilder::PARAM_INT))) + ->executeStatement(); + + return $createdShare->getToken(); + } + + private function findFederatedAttachmentShareToken(int $fileId, string $federatedCloudId): ?string { + $qb = $this->connection->getQueryBuilder(); + $qb->select('id', 'token') + ->from('share') + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_REMOTE))) + ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($federatedCloudId))) + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->setMaxResults(1); + + $cursor = $qb->executeQuery(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + return null; + } + + return $data['token'] ?? null; + } + public function display(Attachment $attachment) { // Problem: Folders /** @psalm-suppress InvalidCatch */ @@ -234,11 +306,70 @@ public function create(Attachment $attachment) { $share->setPermissions(Constants::PERMISSION_READ); $share->setSharedBy($this->userId); $share = $this->shareManager->createShare($share); + $attachment->setId((int)$share->getId()); $attachment->setData($target->getName()); return $attachment; } + public function createForRemote(string $federatedCloudId): string { + $file = $this->getUploadedFile(); + $fileName = $file['name']; + $this->validateFilename($fileName); + + $userFolder = $this->rootFolder->getUserFolder($this->userId); + try { + $folder = $userFolder->get($this->configService->getAttachmentFolder()); + } catch (NotFoundException) { + $folder = $userFolder->newFolder($this->configService->getAttachmentFolder()); + } + + if ($folder->isShared()) { + $folderName = $userFolder->getNonExistingName($this->configService->getAttachmentFolder()); + $folder = $userFolder->newFolder($folderName); + $this->configService->setAttachmentFolder($this->userId, $folderName); + } + + if (!$folder instanceof Folder || $folder->isShared()) { + throw new NotFoundException('No target folder found'); + } + + $fileName = $folder->getNonExistingName($fileName); + $target = $folder->newFile($fileName); + $content = fopen($file['tmp_name'], 'rb'); + if ($content === false) { + throw new StatusException('Could not read file'); + } + $target->putContent($content); + $createdShare = $this->createRemoteShareForNode($target, $federatedCloudId); + + return $createdShare->getToken(); + } + + public function createRemoteShareForPath(string $path, string $federatedCloudId): string { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + try { + $node = $userFolder->get($path); + } catch (NotFoundException) { + throw new NotFoundException('No target file found'); + } + $createdShare = $this->createRemoteShareForNode($node, $federatedCloudId); + + return $createdShare->getToken(); + } + + private function createRemoteShareForNode(Node $node, string $federatedCloudId): IShare { + $share = $this->shareManager->newShare(); + $share->setNode($node); + $share->setShareType(IShare::TYPE_REMOTE); + $share->setSharedWith($federatedCloudId); + $share->setPermissions(Constants::PERMISSION_READ); + $share->setSharedBy($this->userId); + $share->setShareOwner($this->userId); + + return $this->shareManager->createShare($share); + } + /** * Ensure the file exists in the owner’s Deck attachment folder, link it to the * Reuse file/share when already present diff --git a/lib/Service/PermissionService.php b/lib/Service/PermissionService.php index a22c6e12b4..a2dd5ba176 100644 --- a/lib/Service/PermissionService.php +++ b/lib/Service/PermissionService.php @@ -252,7 +252,7 @@ public function userCan(array $acls, $permission, $userId = null) { } // check for users foreach ($acls as $acl) { - if ($acl->getType() === Acl::PERMISSION_TYPE_USER && $acl->getParticipant() === $userId) { + if (in_array($acl->getType(), [Acl::PERMISSION_TYPE_USER, Acl::PERMISSION_TYPE_REMOTE]) && $acl->getParticipant() === $userId) { return $acl->getPermission($permission); } diff --git a/lib/Sharing/DeckShareProvider.php b/lib/Sharing/DeckShareProvider.php index ef41e4301e..ad2b25cc48 100644 --- a/lib/Sharing/DeckShareProvider.php +++ b/lib/Sharing/DeckShareProvider.php @@ -9,26 +9,42 @@ namespace OCA\Deck\Sharing; +use OC\Federation\CloudId; use OC\Files\Cache\Cache; use OCA\Deck\Cache\AttachmentCacheHelper; use OCA\Deck\Db\Acl; use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\FederatedUser; use OCA\Deck\Db\User; use OCA\Deck\NoPermissionException; use OCA\Deck\Service\PermissionService; +use OCA\Files_Sharing\External\ExternalShare; +use OCA\Files_Sharing\External\Manager as ExternalShareManager; +use OCA\Files_Sharing\External\Mount; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Constants; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\File; +use OCP\Federation\ICloudIdManager; +use OCP\Files\Config\IUserMountCache; +use OCP\Files\Events\InvalidateMountCacheEvent; use OCP\Files\Folder; use OCP\Files\IMimeTypeLoader; +use OCP\Files\IRootFolder; use OCP\Files\Node; +use OCP\Http\Client\IClientService; +use OCP\ICertificateManager; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; +use OCP\IPreview; +use OCP\IUser; +use OCP\IUserManager; +use OCP\OCM\IOCMDiscoveryService; +use OCP\Server; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IAttributes; @@ -62,6 +78,15 @@ public function __construct( private IL10N $l, private ITimeFactory $timeFactory, private IMimeTypeLoader $mimeTypeLoader, + private ExternalShareManager $externalShareManager, + private IRootFolder $rootFolder, + private IPreview $preview, + private IConfig $config, + private \OC\Files\Mount\Manager $mountManager, + private IUserManager $userManager, + private IUserMountCache $userMountCache, + private IEventDispatcher $eventDispatcher, + private ICloudIdManager $cloudIdManager, private ?string $userId = null, ) { } @@ -89,7 +114,7 @@ public function create(IShare $share) { } try { - $this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_EDIT); + $this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_EDIT, $share->getSharedBy()); } catch (NoPermissionException $e) { $valid = false; } @@ -337,6 +362,7 @@ public function update(IShare $share) { * @inheritDoc */ public function delete(IShare $share) { + $this->deleteRemoteShares($share); $qb = $this->dbConnection->getQueryBuilder(); $qb->delete('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))); @@ -345,9 +371,70 @@ public function delete(IShare $share) { $qb->executeStatement(); + $this->deleteExternalShare($share); $this->attachmentCacheHelper->clearAttachmentCount((int)$share->getSharedWith()); } + private function deleteRemoteShares(IShare $share) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id', 'token') + ->from('share') + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_REMOTE))); + $stmt = $qb->executeQuery(); + + while ($row = $stmt->fetchAssociative()) { + try { + $remoteShare = $this->shareManager->getShareByToken((string)$row['token']); + $this->shareManager->deleteShare($remoteShare); + } catch (\Exception $e) { + continue; + } + } + } + + private function deleteExternalShare(IShare $share) { + $shareBy = $share->getSharedBy(); + + if (!$this->cloudIdManager->isValidCloudId($shareBy)) { + return; + } + + $externalShare = $this->findRootExternalShare($share, $shareBy); + + if (empty($externalShare)) { + return; + } + + $user = $this->userManager->get($externalShare->getUser()); + $this->externalShareManager->declineShare($externalShare, $user); + } + + public function findRootExternalShare(IShare $share, string $ownerCloudId): ?ExternalShare { + $node = $share->getNode(); + $cloudId = $this->cloudIdManager->resolveCloudId($ownerCloudId); + $mountPoint = str_replace('/' . $share->getShareOwner() . '/files', '', $node->getPath()); + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id', 'refresh_token', 'mountpoint', 'user') + ->from('share_external') + ->andWhere($qb->expr()->eq('remote', $qb->createNamedParameter($cloudId->getRemote() . '/'))) + ->andWhere($qb->expr()->eq('owner', $qb->createNamedParameter($cloudId->getUser()))) + ->andWhere($qb->expr()->eq('mountpoint', $qb->createNamedParameter($mountPoint))) + ->setMaxResults(1); + + $cursor = $qb->executeQuery(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + return null; + } + + $user = $this->userManager->get($data['user']); + return $this->externalShareManager->getShare((string)$data['id'], $user); + } + /** * @inheritDoc */ @@ -870,7 +957,7 @@ private function _getSharedWith( * @param int $offset * @return IShare[] */ - public function getSharedWithByType(int $cardId, int $shareType, $limit, $offset): array { + public function getSharedWithByType(int $cardId, int $shareType, $limit, $offset, ?string $userId = null): array { /** @var IShare[] $shares */ $shares = []; @@ -913,7 +1000,7 @@ public function getSharedWithByType(int $cardId, int $shareType, $limit, $offset } $cursor->closeCursor(); - return $this->resolveSharesForRecipient($shares, $this->userId); + return $this->resolveSharesForRecipient($shares, $userId ?? $this->userId); } public function isAccessibleResult(array $data): bool { @@ -1209,10 +1296,144 @@ public function getUsersForShare(IShare $share): iterable { } foreach ($this->permissionService->findUsers($boardId) as $user) { + if ($user instanceof FederatedUser) { + continue; + } yield $user->getUserObject(); } } return []; } + + public function getRemoteFile(string $shareToken, string $remote): ?array { + $externalShare = $this->ensureAcceptRemoteShare($shareToken, $remote); + $user = $this->userManager->get($externalShare->getUser()); + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $node = $userFolder->get($externalShare->getMountpoint()); + $files = $userFolder->getById($node->getId()); + + if (count($files) === 0) { + return [ + 'path' => $externalShare->getMountpoint(), + 'fileid' => $node->getId(), + 'filesize' => $node->getSize(), + 'mimetype' => $node->getMimetype(), + 'info' => pathinfo($node->getName()), + 'hasPreview' => false, + ]; + } + + $file = array_shift($files); + + return [ + 'path' => $userFolder->getRelativePath($file->getPath()), + 'fileid' => $file->getId(), + 'filesize' => $file->getSize(), + 'mimetype' => $file->getMimeType(), + 'info' => pathinfo($file->getName()), + 'hasPreview' => $this->preview->isAvailable($file), + ]; + } + + public function getLocalFile(string $shareToken): ?array { + try { + $share = $this->shareManager->getShareByToken($shareToken); + } catch (ShareNotFound $e) { + return null; + } + + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); + $node = $share->getNode(); + $files = $userFolder->getById($node->getId()); + + if (count($files) === 0) { + return null; + } + + $file = array_shift($files); + + return [ + 'path' => $userFolder->getRelativePath($file->getPath()), + 'fileid' => $file->getId(), + 'filesize' => $file->getSize(), + 'mimetype' => $file->getMimeType(), + 'info' => pathinfo($file->getName()), + 'hasPreview' => $this->preview->isAvailable($file), + ]; + } + + public function ensureAcceptRemoteShare(string $shareToken, string $remote): ExternalShare { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id', 'accepted', 'user') + ->from('share_external') + ->where($qb->expr()->eq('refresh_token', $qb->createNamedParameter($shareToken))) + ->andWhere($qb->expr()->eq('remote', $qb->createNamedParameter($remote . '/'))) + ->setMaxResults(1); + + $cursor = $qb->executeQuery(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound(); + } + + $user = $this->userManager->get($data['user']); + $externalShare = $this->externalShareManager->getShare((string)$data['id'], $user); + + if ($externalShare === false) { + throw new ShareNotFound(); + } + + if ((int)$data['accepted'] === 0 && !$this->externalShareManager->acceptShare($externalShare, $user)) { + throw new ShareNotFound(); + } + + $externalShare = $this->externalShareManager->getShare((string)$data['id'], $user); + + // Update mount point to be under /Deck + if (strpos($externalShare->getMountpoint(), '/Deck') !== 0) { + $target = '/Deck' . $externalShare->getName(); + $targetHash = md5($target); + $qb = $this->dbConnection->getQueryBuilder(); + $qb->update('share_external') + ->set('mountpoint', $qb->createNamedParameter($target)) + ->set('mountpoint_hash', $qb->createNamedParameter($targetHash)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($externalShare->getId()))) + ->executeStatement(); + $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($user)); + $externalShare->setMountpoint('/Deck' . $externalShare->getName()); + } + + $this->mountExternalShare($externalShare, $user); + + return $externalShare; + } + + public function mountExternalShare(ExternalShare $externalShare, IUser $user): Mount { + $options = [ + 'remote' => $externalShare->getRemote(), + 'token' => $externalShare->getRefreshToken(), + 'password' => $externalShare->getPassword(), + 'access_token' => $externalShare->getAccessToken(), + 'access_token_expires' => $externalShare->getAccessTokenExpires(), + 'mountpoint' => $externalShare->getMountpoint(), + 'owner' => $externalShare->getOwner(), + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates'), + 'HttpClientService' => Server::get(IClientService::class), + 'cloudId' => new CloudId($externalShare->getOwner() . '@' . $externalShare->getRemote(), $externalShare->getOwner(), $externalShare->getRemote()), + 'manager' => $this->externalShareManager, + 'certificateManager' => Server::get(ICertificateManager::class), + 'discoveryService' => Server::get(IOCMDiscoveryService::class), + ]; + $mount = $this->externalShareManager->getMount($options, $user); + $this->mountManager->addMount($mount); + + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $node = $userFolder->get($externalShare->getMountpoint()); + $this->userMountCache->addMount($user, '/' . $user->getUID() . '/files' . $externalShare->getMountpoint() . '/', $node->getData(), $mount->getMountProvider(), $mount->getMountId()); + + return $mount; + } } diff --git a/src/components/board/SharingTabSidebar.vue b/src/components/board/SharingTabSidebar.vue index 02e0e67b81..5518de49bd 100644 --- a/src/components/board/SharingTabSidebar.vue +++ b/src/components/board/SharingTabSidebar.vue @@ -14,21 +14,20 @@ id="shareWithList" class="shareWithList">