From 3a5dd3c08ba04ccfce6c68855e8304e23daede84 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 22 Nov 2023 13:39:33 +0100 Subject: [PATCH] feat(attachments): Allow to get attachments without document session For all read-only attachments API endpoints, add support to authorize with user session or share token when no document session is available. Allows to get the attachments list and attachment files from MarkdownContentEditor.vue without a document session. Signed-off-by: Jonas --- lib/Controller/AttachmentController.php | 37 +++++++++++------- lib/Controller/ISessionAwareController.php | 2 + lib/Controller/TSessionAwareController.php | 12 ++++++ ...RequireDocumentSessionUserOrShareToken.php | 9 +++++ lib/Middleware/SessionMiddleware.php | 39 ++++++++++++++++++- lib/Service/AttachmentService.php | 18 ++++++--- src/services/AttachmentResolver.js | 7 ++-- 7 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 lib/Middleware/Attribute/RequireDocumentSessionUserOrShareToken.php diff --git a/lib/Controller/AttachmentController.php b/lib/Controller/AttachmentController.php index d4400869bee..42cdc6b01cf 100644 --- a/lib/Controller/AttachmentController.php +++ b/lib/Controller/AttachmentController.php @@ -26,8 +26,10 @@ namespace OCA\Text\Controller; use Exception; +use OCA\Text\Exception\InvalidSessionException; use OCA\Text\Exception\UploadException; use OCA\Text\Middleware\Attribute\RequireDocumentSession; +use OCA\Text\Middleware\Attribute\RequireDocumentSessionUserOrShareToken; use OCA\Text\Service\AttachmentService; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; @@ -83,15 +85,22 @@ public function __construct( #[NoAdminRequired] #[PublicPage] - #[RequireDocumentSession] + #[RequireDocumentSessionUserOrShareToken] public function getAttachmentList(?string $shareToken = null): DataResponse { - $documentId = $this->getSession()->getDocumentId(); + $documentId = $this->getDocument()->getId(); + try { + $session = $this->getSession(); + } catch (InvalidSessionException) { + $session = null; + } + if ($shareToken) { - $attachments = $this->attachmentService->getAttachmentList($documentId, null, $this->getSession(), $shareToken); + $attachments = $this->attachmentService->getAttachmentList($documentId, null, $session, $shareToken); } else { - $userId = $this->getSession()->getUserId(); - $attachments = $this->attachmentService->getAttachmentList($documentId, $userId, $this->getSession(), null); + $userId = $this->getUserId(); + $attachments = $this->attachmentService->getAttachmentList($documentId, $userId, $session, null); } + return new DataResponse($attachments); } @@ -183,16 +192,16 @@ private function getUploadedFile(string $key): array { #[NoAdminRequired] #[PublicPage] #[NoCSRFRequired] - #[RequireDocumentSession] + #[RequireDocumentSessionUserOrShareToken] public function getImageFile(string $imageFileName, ?string $shareToken = null, int $preferRawImage = 0): DataResponse|DataDownloadResponse { - $documentId = $this->getSession()->getDocumentId(); + $documentId = $this->getDocument()->getId(); try { if ($shareToken) { $imageFile = $this->attachmentService->getImageFilePublic($documentId, $imageFileName, $shareToken, $preferRawImage === 1); } else { - $userId = $this->getSession()->getUserId(); + $userId = $this->getUserId(); $imageFile = $this->attachmentService->getImageFile($documentId, $imageFileName, $userId, $preferRawImage === 1); } return $imageFile !== null @@ -218,15 +227,15 @@ public function getImageFile(string $imageFileName, ?string $shareToken = null, #[NoAdminRequired] #[PublicPage] #[NoCSRFRequired] - #[RequireDocumentSession] + #[RequireDocumentSessionUserOrShareToken] public function getMediaFile(string $mediaFileName, ?string $shareToken = null): DataResponse|DataDownloadResponse { - $documentId = $this->getSession()->getDocumentId(); + $documentId = $this->getDocument()->getId(); try { if ($shareToken) { $mediaFile = $this->attachmentService->getMediaFilePublic($documentId, $mediaFileName, $shareToken); } else { - $userId = $this->getSession()->getUserId(); + $userId = $this->getUserId(); $mediaFile = $this->attachmentService->getMediaFile($documentId, $mediaFileName, $userId); } return $mediaFile !== null @@ -249,15 +258,15 @@ public function getMediaFile(string $mediaFileName, ?string $shareToken = null): #[NoAdminRequired] #[PublicPage] #[NoCSRFRequired] - #[RequireDocumentSession] + #[RequireDocumentSessionUserOrShareToken] public function getMediaFilePreview(string $mediaFileName, ?string $shareToken = null) { - $documentId = $this->getSession()->getDocumentId(); + $documentId = $this->getDocument()->getId(); try { if ($shareToken) { $preview = $this->attachmentService->getMediaFilePreviewPublic($documentId, $mediaFileName, $shareToken); } else { - $userId = $this->getSession()->getUserId(); + $userId = $this->getUserId(); $preview = $this->attachmentService->getMediaFilePreview($documentId, $mediaFileName, $userId); } if ($preview === null) { diff --git a/lib/Controller/ISessionAwareController.php b/lib/Controller/ISessionAwareController.php index 00236bfdc4e..3f82688c633 100644 --- a/lib/Controller/ISessionAwareController.php +++ b/lib/Controller/ISessionAwareController.php @@ -10,4 +10,6 @@ public function getSession(): Session; public function setSession(Session $session): void; public function getDocument(): Document; public function setDocument(Document $document): void; + public function getUserId(): string; + public function setUserId(string $userId): void; } diff --git a/lib/Controller/TSessionAwareController.php b/lib/Controller/TSessionAwareController.php index b4eb200acef..3b0ba6793ba 100644 --- a/lib/Controller/TSessionAwareController.php +++ b/lib/Controller/TSessionAwareController.php @@ -11,6 +11,7 @@ trait TSessionAwareController { private ?Session $textSession = null; private ?Document $document = null; + private ?string $userId = null; public function setSession(?Session $session): void { $this->textSession = $session; @@ -20,6 +21,10 @@ public function setDocument(?Document $document): void { $this->document = $document; } + public function setUserId(?string $userId): void { + $this->userId = $userId; + } + public function getSession(): Session { if ($this->textSession === null) { throw new InvalidSessionException(); @@ -36,4 +41,11 @@ public function getDocument(): Document { return $this->document; } + public function getUserId(): string { + if ($this->userId === null) { + throw new InvalidSessionException(); + } + + return $this->userId; + } } diff --git a/lib/Middleware/Attribute/RequireDocumentSessionUserOrShareToken.php b/lib/Middleware/Attribute/RequireDocumentSessionUserOrShareToken.php new file mode 100644 index 00000000000..b5127e4d354 --- /dev/null +++ b/lib/Middleware/Attribute/RequireDocumentSessionUserOrShareToken.php @@ -0,0 +1,9 @@ +getAttributes(RequireDocumentSessionUserOrShareToken::class))) { + try { + $this->assertDocumentSession($controller); + } catch (InvalidSessionException) { + $this->assertUserOrShareToken($controller); + } + } + if (!empty($reflectionMethod->getAttributes(RequireDocumentSession::class))) { $this->assertDocumentSession($controller); } @@ -41,6 +53,7 @@ private function assertDocumentSession(ISessionAwareController $controller): voi $documentId = (int)$this->request->getParam('documentId'); $sessionId = (int)$this->request->getParam('sessionId'); $token = (string)$this->request->getParam('sessionToken'); + $shareToken = (string)$this->request->getParam('token'); $session = $this->sessionService->getValidSession($documentId, $sessionId, $token); if (!$session) { @@ -54,9 +67,31 @@ private function assertDocumentSession(ISessionAwareController $controller): voi $controller->setSession($session); $controller->setDocument($document); + if (!$shareToken) { + $controller->setUserId($session->getUserId()); + } + } + + private function assertUserOrShareToken(ISessionAwareController $controller): void { + $documentId = (int)$this->request->getParam('documentId'); + if (null !== $userId = $this->userSession->getUser()?->getUID()) { + $controller->setUserId($userId); + // TODO: check if user has access to document + } elseif (null !== $shareToken = (string)$this->request->getParam('shareToken')) { + // TODO: check if shareToken has access to document + } else { + throw new InvalidSessionException(); + } + + $document = $this->documentService->getDocument($documentId); + if (!$document) { + throw new InvalidSessionException(); + } + + $controller->setDocument($document); } - public function afterException($controller, $methodName, \Exception $exception) { + public function afterException($controller, $methodName, \Exception $exception): DataResponse|Response { if ($exception instanceof InvalidSessionException) { return new DataResponse([], 403); } diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php index f6f191e61fc..702163df509 100644 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -233,8 +233,9 @@ public function getAttachmentList(int $documentId, ?string $userId = null, ?Sess $shareTokenUrlString = $shareToken ? '&shareToken=' . urlencode($shareToken) : ''; - // TODO: session might be null - $sessionUrlParamsBase = '?documentId=' . $documentId . '&sessionId=' . $session->getId() . '&sessionToken=' . urlencode($session->getToken()) . $shareTokenUrlString; + $urlParamsBase = $session + ? '?documentId=' . $documentId . '&sessionId=' . $session->getId() . '&sessionToken=' . urlencode($session->getToken()) . $shareTokenUrlString + : '?documentId=' . $documentId . $shareTokenUrlString; $attachments = []; foreach ($attachmentDir->getDirectoryListing() as $node) { @@ -252,11 +253,16 @@ public function getAttachmentList(int $documentId, ?string $userId = null, ?Sess 'mtime' => $node->getMTime(), 'isImage' => $isImage, 'fullUrl' => $isImage - ? $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getImageFile') . $sessionUrlParamsBase . '&imageFileName=' . urlencode($name) . '&preferRawImage=1' - : $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getMediaFile') . $sessionUrlParamsBase . '&mediaFileName=' . urlencode($name), + ? $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getImageFile') . $urlParamsBase . '&imageFileName=' . urlencode($name) . '&preferRawImage=1' + : $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getMediaFile') . $urlParamsBase . '&mediaFileName=' . urlencode($name), 'previewUrl' => $isImage - ? $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getImageFile') . $sessionUrlParamsBase . '&imageFileName=' . urlencode($name) - : $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getMediaFilePreview') . $sessionUrlParamsBase . '&mediaFileName=' . urlencode($name), + ? $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getImageFile') . $urlParamsBase . '&imageFileName=' . urlencode($name) + : $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getMediaFilePreview') . $urlParamsBase . '&mediaFileName=' . urlencode($name), + /* + : ($isImage + ? $this->urlGenerator->linkTo('', 'remote.php') . '/dav/files/' . $userId . '/' . implode('/', array_map('rawurlencode', array_slice(explode('/', $node->getPath()), 3))) + : ''), + */ ]; } diff --git a/src/services/AttachmentResolver.js b/src/services/AttachmentResolver.js index f694e3f3fcd..b7029954bc9 100644 --- a/src/services/AttachmentResolver.js +++ b/src/services/AttachmentResolver.js @@ -34,7 +34,6 @@ export default class AttachmentResolver { #user #shareToken #currentDirectory - #attachmentDirectory #documentId #initAttachmentListPromise @@ -47,7 +46,6 @@ export default class AttachmentResolver { this.#shareToken = shareToken this.#currentDirectory = currentDirectory this.#documentId = fileId ?? session?.documentId - this.#attachmentDirectory = `.attachments.${this.#documentId}` this.#initAttachmentListPromise = this.#updateAttachmentList() } @@ -64,8 +62,9 @@ export default class AttachmentResolver { let attachment // Native attachment - if (src.match(/^\.attachments\.\d+\//)) { - const imageFileName = decodeURIComponent(src.replace(`${this.#attachmentDirectory}/`, '').split('?')[0]) + const directoryRegexp = /^\.attachments\.\d+\// + if (src.match(directoryRegexp)) { + const imageFileName = decodeURIComponent(src.replace(directoryRegexp, '').split('?')[0]) // Wait until attachment list got fetched (initialized by constructor) await this.#initAttachmentListPromise