diff --git a/appinfo/routes.php b/appinfo/routes.php index dfc8a7375cb..9ab025773fe 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -29,6 +29,8 @@ return [ 'routes' => [ + /** @see Controller\AttachmentController::getAttachmentList() */ + ['name' => 'Attachment#getAttachmentList', 'url' => '/attachments', 'verb' => 'POST'], /** @see Controller\AttachmentController::insertAttachmentFile() */ ['name' => 'Attachment#insertAttachmentFile', 'url' => '/attachment/filepath', 'verb' => 'POST'], /** @see Controller\AttachmentController::uploadAttachment() */ @@ -39,8 +41,6 @@ ['name' => 'Attachment#getMediaFile', 'url' => '/media', 'verb' => 'GET'], /** @see Controller\AttachmentController::getMediaFilePreview() */ ['name' => 'Attachment#getMediaFilePreview', 'url' => '/mediaPreview', 'verb' => 'GET'], - /** @see Controller\AttachmentController::getMediaFileMetadata() */ - ['name' => 'Attachment#getMediaFileMetadata', 'url' => '/mediaMetadata', 'verb' => 'GET'], /** @see Controller\SessionController::create() */ ['name' => 'Session#create', 'url' => '/session/{documentId}/create', 'verb' => 'PUT'], diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index c8db5c76589..da85631f4b9 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -45,6 +45,7 @@ 'OCA\\Text\\Listeners\\NodeCopiedListener' => $baseDir . '/../lib/Listeners/NodeCopiedListener.php', 'OCA\\Text\\Listeners\\RegisterDirectEditorEventListener' => $baseDir . '/../lib/Listeners/RegisterDirectEditorEventListener.php', 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSession' => $baseDir . '/../lib/Middleware/Attribute/RequireDocumentSession.php', + 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSessionOrUserOrShareToken' => $baseDir . '/../lib/Middleware/Attribute/RequireDocumentSessionOrUserOrShareToken.php', 'OCA\\Text\\Middleware\\SessionMiddleware' => $baseDir . '/../lib/Middleware/SessionMiddleware.php', 'OCA\\Text\\Migration\\ResetSessionsBeforeYjs' => $baseDir . '/../lib/Migration/ResetSessionsBeforeYjs.php', 'OCA\\Text\\Migration\\Version010000Date20190617184535' => $baseDir . '/../lib/Migration/Version010000Date20190617184535.php', diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index b920a5f9b38..8732ef4a1dc 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -60,6 +60,7 @@ class ComposerStaticInitText 'OCA\\Text\\Listeners\\NodeCopiedListener' => __DIR__ . '/..' . '/../lib/Listeners/NodeCopiedListener.php', 'OCA\\Text\\Listeners\\RegisterDirectEditorEventListener' => __DIR__ . '/..' . '/../lib/Listeners/RegisterDirectEditorEventListener.php', 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSession' => __DIR__ . '/..' . '/../lib/Middleware/Attribute/RequireDocumentSession.php', + 'OCA\\Text\\Middleware\\Attribute\\RequireDocumentSessionOrUserOrShareToken' => __DIR__ . '/..' . '/../lib/Middleware/Attribute/RequireDocumentSessionOrUserOrShareToken.php', 'OCA\\Text\\Middleware\\SessionMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/SessionMiddleware.php', 'OCA\\Text\\Migration\\ResetSessionsBeforeYjs' => __DIR__ . '/..' . '/../lib/Migration/ResetSessionsBeforeYjs.php', 'OCA\\Text\\Migration\\Version010000Date20190617184535' => __DIR__ . '/..' . '/../lib/Migration/Version010000Date20190617184535.php', diff --git a/cypress/e2e/attachments.spec.js b/cypress/e2e/attachments.spec.js index 7bbff855bfb..952ac851279 100644 --- a/cypress/e2e/attachments.spec.js +++ b/cypress/e2e/attachments.spec.js @@ -111,7 +111,7 @@ const checkAttachment = (documentId, fileName, fileId, index, isImage = true) => .find('img') .should('have.attr', 'src') .should('contain', 'apps/text/' + srcPathEnd + '?documentId=' + documentId) - .should('contain', srcFileNameParam + '=' + encodeURIComponent(fileName)) + .should('contain', srcFileNameParam + '=' + fixedEncodeURIComponent(fileName)) return isImage ? cy.wrap($el) diff --git a/cypress/e2e/nodes/ImageView.spec.js b/cypress/e2e/nodes/ImageView.spec.js index c945239c72f..eb648583639 100644 --- a/cypress/e2e/nodes/ImageView.spec.js +++ b/cypress/e2e/nodes/ImageView.spec.js @@ -79,23 +79,6 @@ describe('Image View', () => { .should('have.attr', 'src') .should('contain', `/dav/files/${user.userId}/github.png`) }) - - it('with preview', () => { - cy.getFile('github.png') - .should('have.attr', 'data-cy-files-list-row-fileid') - .then(imageId => { - const fileName = `${Cypress.currentTest.title}.md` - - createMarkdown(fileName, `# from image id\n\n ![${imageId}](github.png?fileId=${imageId}&hasPreview=true)`) - - cy.openFile(fileName, { force: true }) - - cy.getContent() - .find('[data-component="image-view"] img') - .should('have.attr', 'src') - .should('contains', `core/preview?fileId=${imageId}&file=${encodeURIComponent('/github.png')}`, { timeout: 5000 }) - }) - }) }) describe('fail to load', () => { @@ -123,4 +106,46 @@ describe('Image View', () => { .should('have.value', 'yaha') }) }) + + describe('native attachments', () => { + before(() => { + cy.login(user) + cy.visit('/apps/files') + const fileName = 'native attachments.md' + createMarkdown(fileName, '# open image in modal\n\n ![git](.attachments.123/github.png)\n\n ![file.txt.gz](.attachments.123/file.txt.gz)') + + cy.getFileId(fileName).then((fileId) => { + const attachmentsFolder = `.attachments.${fileId}` + cy.createFolder(attachmentsFolder) + cy.uploadFile('github.png', 'image/png', `${attachmentsFolder}/github.png`) + cy.uploadFile('file.txt.gz', 'application/gzip', `${attachmentsFolder}/file.txt.gz`) + }) + }) + + it('open image in modal', () => { + const fileName = 'native attachments.md' + cy.openFile(fileName) + + cy.getContent() + .find('[data-component="image-view"][data-src=".attachments.123/github.png"] img') + .click() + + cy.get('.modal__content img') + .should('have.attr', 'src') + .should('contain', 'imageFileName=github.png') + }) + + it('download non-image gzip attachment', () => { + const fileName = 'native attachments.md' + cy.openFile(fileName) + + cy.getContent() + .find('[data-component="image-view"][data-src=".attachments.123/file.txt.gz"] img') + .click() + + const downloadsFolder = Cypress.config('downloadsFolder') + cy.log(`downloadsFolder: ${downloadsFolder}`) + cy.readFile(`${downloadsFolder}/file.txt.gz`) + }) + }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a39fbbe3444..04787814f28 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -323,6 +323,11 @@ Cypress.Commands.add('openFolder', (name) => { cy.wait(`@open-${name}`) }) +Cypress.Commands.add('getFileId', (fileName, params = {}) => { + return cy.get(`[data-cy-files-list] tr[data-cy-files-list-row-name="${fileName}"]`) + .invoke('attr', 'data-cy-files-list-row-fileid') +}) + Cypress.Commands.add('openFile', (fileName, params = {}) => { cy.get(`[data-cy-files-list] tr[data-cy-files-list-row-name="${fileName}"] a[data-cy-files-list-row-name-link]`).click(params) }) diff --git a/lib/Controller/AttachmentController.php b/lib/Controller/AttachmentController.php index 68d2f50a38f..19ec885da8d 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\RequireDocumentSessionOrUserOrShareToken; use OCA\Text\Service\AttachmentService; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; @@ -81,6 +83,27 @@ public function __construct( parent::__construct($appName, $request); } + #[NoAdminRequired] + #[PublicPage] + #[RequireDocumentSessionOrUserOrShareToken] + public function getAttachmentList(?string $shareToken = null): DataResponse { + $documentId = $this->getDocument()->getId(); + try { + $session = $this->getSession(); + } catch (InvalidSessionException) { + $session = null; + } + + if ($shareToken) { + $attachments = $this->attachmentService->getAttachmentList($documentId, null, $session, $shareToken); + } else { + $userId = $this->getUserId(); + $attachments = $this->attachmentService->getAttachmentList($documentId, $userId, $session, null); + } + + return new DataResponse($attachments); + } + #[NoAdminRequired] #[PublicPage] #[RequireDocumentSession] @@ -169,22 +192,22 @@ private function getUploadedFile(string $key): array { #[NoAdminRequired] #[PublicPage] #[NoCSRFRequired] - #[RequireDocumentSession] + #[RequireDocumentSessionOrUserOrShareToken] 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 ? new DataDownloadResponse( $imageFile->getContent(), - (string) Http::STATUS_OK, + $imageFile->getName(), $this->getSecureMimeType($imageFile->getMimeType()) ) : new DataResponse('', Http::STATUS_NOT_FOUND); @@ -204,21 +227,21 @@ public function getImageFile(string $imageFileName, ?string $shareToken = null, #[NoAdminRequired] #[PublicPage] #[NoCSRFRequired] - #[RequireDocumentSession] + #[RequireDocumentSessionOrUserOrShareToken] 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 ? new DataDownloadResponse( $mediaFile->getContent(), - (string) Http::STATUS_OK, + $mediaFile->getName(), $this->getSecureMimeType($mediaFile->getMimeType()) ) : new DataResponse('', Http::STATUS_NOT_FOUND); @@ -235,15 +258,15 @@ public function getMediaFile(string $mediaFileName, ?string $shareToken = null): #[NoAdminRequired] #[PublicPage] #[NoCSRFRequired] - #[RequireDocumentSession] + #[RequireDocumentSessionOrUserOrShareToken] 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) { @@ -252,7 +275,7 @@ public function getMediaFilePreview(string $mediaFileName, ?string $shareToken = if ($preview['type'] === 'file') { return new DataDownloadResponse( $preview['file']->getContent(), - (string) Http::STATUS_OK, + $mediaFileName, $this->getSecureMimeType($preview['file']->getMimeType()) ); } elseif ($preview['type'] === 'icon') { @@ -264,32 +287,6 @@ public function getMediaFilePreview(string $mediaFileName, ?string $shareToken = return new DataResponse('', Http::STATUS_NOT_FOUND); } - /** - * Serve the media files metadata in the editor - */ - #[NoAdminRequired] - #[PublicPage] - #[NoCSRFRequired] - #[RequireDocumentSession] - public function getMediaFileMetadata(string $mediaFileName, ?string $shareToken = null): DataResponse { - $documentId = $this->getSession()->getDocumentId(); - try { - if ($shareToken) { - $metadata = $this->attachmentService->getMediaFileMetadataPublic($documentId, $mediaFileName, $shareToken); - } else { - $userId = $this->getSession()->getUserId(); - $metadata = $this->attachmentService->getMediaFileMetadataPrivate($documentId, $mediaFileName, $userId); - } - if ($metadata === null) { - return new DataResponse('', Http::STATUS_NOT_FOUND); - } - return new DataResponse($metadata); - } catch (Exception $e) { - $this->logger->error('getMediaFileMetadata error', ['exception' => $e]); - return new DataResponse('', Http::STATUS_NOT_FOUND); - } - } - /** * Allow all supported mimetypes * Use mimetype detector for the other ones 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/RequireDocumentSessionOrUserOrShareToken.php b/lib/Middleware/Attribute/RequireDocumentSessionOrUserOrShareToken.php new file mode 100644 index 00000000000..d9cc863fe17 --- /dev/null +++ b/lib/Middleware/Attribute/RequireDocumentSessionOrUserOrShareToken.php @@ -0,0 +1,9 @@ +getAttributes(RequireDocumentSessionOrUserOrShareToken::class))) { + try { + $this->assertDocumentSession($controller); + } catch (InvalidSessionException) { + $this->assertUserOrShareToken($controller); + } + } + if (!empty($reflectionMethod->getAttributes(RequireDocumentSession::class))) { $this->assertDocumentSession($controller); } } + /** + * @throws InvalidSessionException + */ private function assertDocumentSession(ISessionAwareController $controller): void { $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 +78,47 @@ private function assertDocumentSession(ISessionAwareController $controller): voi $controller->setSession($session); $controller->setDocument($document); + if (!$shareToken) { + $controller->setUserId($session->getUserId()); + } + } + + /** + * @throws NotPermittedException + * @throws NoUserException + * @throws InvalidSessionException + */ + private function assertUserOrShareToken(ISessionAwareController $controller): void { + $documentId = (int)$this->request->getParam('documentId'); + if (null !== $userId = $this->userSession->getUser()?->getUID()) { + // Check if user has access to document + if (count($this->rootFolder->getUserFolder($userId)->getById($documentId)) === 0) { + throw new InvalidSessionException(); + } + $controller->setUserId($userId); + } elseif ('' !== $shareToken = (string)$this->request->getParam('shareToken')) { + try { + $share = $this->shareManager->getShareByToken($shareToken); + } catch (ShareNotFound) { + throw new InvalidSessionException(); + } + // Check if shareToken has access to document + if (count($this->rootFolder->getUserFolder($share->getShareOwner())->getById($documentId)) === 0) { + throw new InvalidSessionException(); + } + } 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 459e9981cfe..968bbf03e81 100644 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -29,6 +29,7 @@ use OC\User\NoUserException; use OCA\Files_Sharing\SharedStorage; use OCA\Text\Controller\AttachmentController; +use OCA\Text\Db\Session; use OCP\Constants; use OCP\Files\File; use OCP\Files\Folder; @@ -39,42 +40,24 @@ use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\IPreview; +use OCP\IURLGenerator; +use OCP\Lock\LockedException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as ShareManager; use OCP\Share\IShare; use OCP\Util; class AttachmentService { - - /** - * @var ShareManager - */ - private $shareManager; - /** - * @var IRootFolder - */ - private $rootFolder; - /** - * @var IPreview - */ - private $previewManager; - /** - * @var IMimeTypeDetector - */ - private $mimeTypeDetector; - - public function __construct(IRootFolder $rootFolder, - ShareManager $shareManager, - IPreview $previewManager, - IMimeTypeDetector $mimeTypeDetector) { - $this->rootFolder = $rootFolder; - $this->shareManager = $shareManager; - $this->previewManager = $previewManager; - $this->mimeTypeDetector = $mimeTypeDetector; + public function __construct(private IRootFolder $rootFolder, + private ShareManager $shareManager, + private IPreview $previewManager, + private IMimeTypeDetector $mimeTypeDetector, + private IURLGenerator $urlGenerator) { } /** * Get image content or preview from file name + * * @throws InvalidPathException * @throws NoUserException * @throws NotFoundException @@ -87,6 +70,7 @@ public function getImageFile(int $documentId, string $imageFileName, string $use /** * Get image content or preview from file id in public context + * * @throws NotFoundException * @throws NotPermittedException * @throws InvalidPathException @@ -106,13 +90,13 @@ public function getImageFilePublic(int $documentId, string $imageFileName, strin private function getImageFileContent(string $imageFileName, File $textFile, bool $preferRawImage): File|ISimpleFile|null { $attachmentFolder = $this->getAttachmentDirectoryForFile($textFile, true); $imageFile = $attachmentFolder->get($imageFileName); - if ($imageFile instanceof File && in_array($imageFile->getMimetype(), AttachmentController::IMAGE_MIME_TYPES)) { + if ($imageFile instanceof File && in_array($imageFile->getMimetype(), AttachmentController::IMAGE_MIME_TYPES, true)) { // previews of gifs are static images, always provide the real gif if ($imageFile->getMimetype() === 'image/gif') { return $imageFile; } // we might prefer the raw image - if ($preferRawImage && in_array($imageFile->getMimetype(), AttachmentController::BROWSER_SUPPORTED_IMAGE_MIME_TYPES)) { + if ($preferRawImage && in_array($imageFile->getMimetype(), AttachmentController::BROWSER_SUPPORTED_IMAGE_MIME_TYPES, true)) { return $imageFile; } if ($this->previewManager->isMimeSupported($imageFile->getMimeType())) { @@ -128,8 +112,9 @@ private function getImageFileContent(string $imageFileName, File $textFile, bool * Get media file from file name * * @throws NotFoundException - * @throws \OCP\Files\InvalidPathException + * @throws InvalidPathException * @throws NotPermittedException + * @throws NoUserException */ public function getMediaFile(int $documentId, string $mediaFileName, string $userId): File|null { $textFile = $this->getTextFile($documentId, $userId); @@ -138,10 +123,11 @@ public function getMediaFile(int $documentId, string $mediaFileName, string $use /** * Get image content or preview from file id in public context + * * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws NoUserException */ public function getMediaFilePublic(int $documentId, string $mediaFileName, string $shareToken): File|null { $textFile = $this->getTextFilePublic($documentId, $shareToken); @@ -151,8 +137,8 @@ public function getMediaFilePublic(int $documentId, string $mediaFileName, strin /** * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws NoUserException */ private function getMediaFullFile(string $mediaFileName, File $textFile): ?File { $attachmentFolder = $this->getAttachmentDirectoryForFile($textFile, true); @@ -166,18 +152,19 @@ private function getMediaFullFile(string $mediaFileName, File $textFile): ?File /** * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws NoUserException */ public function getMediaFilePreview(int $documentId, string $mediaFileName, string $userId): ?array { $textFile = $this->getTextFile($documentId, $userId); return $this->getMediaFilePreviewFile($mediaFileName, $textFile); } + /** * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws NoUserException */ public function getMediaFilePreviewPublic(int $documentId, string $mediaFileName, string $shareToken): ?array { $textFile = $this->getTextFilePublic($documentId, $shareToken); @@ -186,10 +173,11 @@ public function getMediaFilePreviewPublic(int $documentId, string $mediaFileName /** * Get media preview or mimetype icon address + * * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws NoUserException */ private function getMediaFilePreviewFile(string $mediaFileName, File $textFile): ?array { $attachmentFolder = $this->getAttachmentDirectoryForFile($textFile, true); @@ -215,71 +203,81 @@ private function getMediaFilePreviewFile(string $mediaFileName, File $textFile): } /** - * @param int $documentId - * @param string $mediaFileName - * @param string $userId - * @return array|null + * @param int $documentId + * @param string|null $userId + * @param Session|null $session + * @param string|null $shareToken + * + * @return array + * @throws InvalidPathException + * @throws NoUserException * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException */ - public function getMediaFileMetadataPrivate(int $documentId, string $mediaFileName, string $userId): ?array { - $textFile = $this->getTextFile($documentId, $userId); - return $this->getMediaFileMetadata($mediaFileName, $textFile); - } + public function getAttachmentList(int $documentId, ?string $userId = null, ?Session $session = null, ?string $shareToken = null): array { + if ($shareToken) { + $textFile = $this->getTextFilePublic($documentId, $shareToken); + } elseif ($userId) { + $textFile = $this->getTextFile($documentId, $userId); + } else { + throw new NotPermittedException('Unable to read document'); + } - /** - * @param int $documentId - * @param string $mediaFileName - * @param string $shareToken - * @return array|null - * @throws NotFoundException - * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException - */ - public function getMediaFileMetadataPublic(int $documentId, string $mediaFileName, string $shareToken): ?array { - $textFile = $this->getTextFilePublic($documentId, $shareToken); - return $this->getMediaFileMetadata($mediaFileName, $textFile); - } + try { + $attachmentDir = $this->getAttachmentDirectoryForFile($textFile); + } catch (NotFoundException) { + return []; + } - /** - * @param string $mediaFileName - * @param File $textFile - * @return array|null - * @throws NotFoundException - * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException - */ - private function getMediaFileMetadata(string $mediaFileName, File $textFile): ?array { - $attachmentFolder = $this->getAttachmentDirectoryForFile($textFile, true); - $mediaFile = $attachmentFolder->get($mediaFileName); - if ($mediaFile instanceof File) { - return [ - 'size' => Util::humanFileSize($mediaFile->getSize()), - 'mtime' => $mediaFile->getMTime(), + $shareTokenUrlString = $shareToken + ? '&shareToken=' . rawurlencode($shareToken) + : ''; + $urlParamsBase = $session + ? '?documentId=' . $documentId . '&sessionId=' . $session->getId() . '&sessionToken=' . rawurlencode($session->getToken()) . $shareTokenUrlString + : '?documentId=' . $documentId . $shareTokenUrlString; + + $attachments = []; + $userFolder = $userId ? $this->rootFolder->getUserFolder($userId) : null; + foreach ($attachmentDir->getDirectoryListing() as $node) { + if (!($node instanceof File)) { + // Ignore anything but files + continue; + } + $isImage = in_array($node->getMimetype(), AttachmentController::IMAGE_MIME_TYPES, true); + $name = $node->getName(); + $attachments[] = [ + 'fileId' => $node->getId(), + 'name' => $name, + 'size' => Util::humanFileSize($node->getSize()), + 'mimetype' => $node->getMimeType(), + 'mtime' => $node->getMTime(), + 'isImage' => $isImage, + 'davPath' => $userFolder?->getRelativePath($node->getPath()), + 'fullUrl' => $isImage + ? $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getImageFile') . $urlParamsBase . '&imageFileName=' . rawurlencode($name) . '&preferRawImage=1' + : $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getMediaFile') . $urlParamsBase . '&mediaFileName=' . rawurlencode($name), + 'previewUrl' => $isImage + ? $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getImageFile') . $urlParamsBase . '&imageFileName=' . rawurlencode($name) + : $this->urlGenerator->linkToRouteAbsolute('text.Attachment.getMediaFilePreview') . $urlParamsBase . '&mediaFileName=' . rawurlencode($name), ]; } - return null; + + return $attachments; } /** * Save an uploaded file in the attachment folder * - * @param int $documentId - * @param string $newFileName - * @param string $newFileContent - * @param string $userId + * @param int $documentId + * @param string $newFileName * @param resource $newFileResource + * @param string $userId * * @return array - * + * @throws InvalidPathException + * @throws NoUserException * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException */ public function uploadAttachment(int $documentId, string $newFileName, $newFileResource, string $userId): array { $textFile = $this->getTextFile($documentId, $userId); @@ -287,7 +285,7 @@ public function uploadAttachment(int $documentId, string $newFileName, $newFileR throw new NotPermittedException('No write permissions'); } $saveDir = $this->getAttachmentDirectoryForFile($textFile, true); - $fileName = $this->getUniqueFileName($saveDir, $newFileName); + $fileName = self::getUniqueFileName($saveDir, $newFileName); $savedFile = $saveDir->newFile($fileName, $newFileResource); return [ 'name' => $fileName, @@ -302,16 +300,14 @@ public function uploadAttachment(int $documentId, string $newFileName, $newFileR * * @param int|null $documentId * @param string $newFileName - * @param string $newFileContent - * @param string $shareToken * @param resource $newFileResource + * @param string $shareToken * * @return array - * * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws NoUserException */ public function uploadAttachmentPublic(?int $documentId, string $newFileName, $newFileResource, string $shareToken): array { if (!$this->hasUpdatePermissions($shareToken)) { @@ -319,7 +315,7 @@ public function uploadAttachmentPublic(?int $documentId, string $newFileName, $n } $textFile = $this->getTextFilePublic($documentId, $shareToken); $saveDir = $this->getAttachmentDirectoryForFile($textFile, true); - $fileName = $this->getUniqueFileName($saveDir, $newFileName); + $fileName = self::getUniqueFileName($saveDir, $newFileName); $savedFile = $saveDir->newFile($fileName, $newFileResource); return [ 'name' => $fileName, @@ -335,11 +331,12 @@ public function uploadAttachmentPublic(?int $documentId, string $newFileName, $n * @param int $documentId * @param string $path * @param string $userId + * * @return array * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws NoUserException */ public function insertAttachmentFile(int $documentId, string $path, string $userId): array { $textFile = $this->getTextFile($documentId, $userId); @@ -355,12 +352,13 @@ public function insertAttachmentFile(int $documentId, string $path, string $user * @param File $originalFile * @param Folder $saveDir * @param File $textFile + * * @return array * @throws NotFoundException - * @throws \OCP\Files\InvalidPathException + * @throws InvalidPathException */ private function copyFile(File $originalFile, Folder $saveDir, File $textFile): array { - $fileName = $this->getUniqueFileName($saveDir, $originalFile->getName()); + $fileName = self::getUniqueFileName($saveDir, $originalFile->getName()); $targetPath = $saveDir->getPath() . '/' . $fileName; $targetFile = $originalFile->copy($targetPath); return [ @@ -374,8 +372,10 @@ private function copyFile(File $originalFile, Folder $saveDir, File $textFile): /** * Get unique file name in a directory. Add '(n)' suffix. + * * @param Folder $dir * @param string $fileName + * * @return string */ public static function getUniqueFileName(Folder $dir, string $fileName): string { @@ -400,6 +400,7 @@ public static function getUniqueFileName(Folder $dir, string $fileName): string * Check if the shared access has write permissions * * @param string $shareToken + * * @return bool */ private function hasUpdatePermissions(string $shareToken): bool { @@ -422,11 +423,12 @@ private function hasUpdatePermissions(string $shareToken): bool { * * @param File $textFile * @param bool $create + * * @return Folder * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws NoUserException */ private function getAttachmentDirectoryForFile(File $textFile, bool $create = false): Folder { $owner = $textFile->getOwner(); @@ -454,9 +456,9 @@ private function getAttachmentDirectoryForFile(File $textFile, bool $create = fa /** * Get a user file from file ID - * * @throws NotFoundException * @throws NotPermittedException + * @throws NoUserException */ private function getFileFromPath(string $filePath, string $userId): File { $userFolder = $this->rootFolder->getUserFolder($userId); @@ -469,6 +471,12 @@ private function getFileFromPath(string $filePath, string $userId): File { throw new NotFoundException(); } + /** + * @param File $file + * + * @return bool + * @throws NotFoundException + */ private function isDownloadDisabled(File $file): bool { $storage = $file->getStorage(); if ($storage->instanceOfStorage(SharedStorage::class)) { @@ -486,12 +494,13 @@ private function isDownloadDisabled(File $file): bool { /** * Get a user file from file ID * - * @param int $documentId - * @param string $userIdd + * @param int $documentId + * @param string $userId + * * @return File + * @throws NoUserException * @throws NotFoundException * @throws NotPermittedException - * @throws \OC\User\NoUserException */ private function getTextFile(int $documentId, string $userId): File { $userFolder = $this->rootFolder->getUserFolder($userId); @@ -508,6 +517,7 @@ private function getTextFile(int $documentId, string $userId): File { * * @param int|null $documentId * @param string $shareToken + * * @return File * @throws NotFoundException */ @@ -543,12 +553,13 @@ private function getTextFilePublic(?int $documentId, string $shareToken): File { * Actually delete attachment files which are not pointed in the markdown content * * @param int $fileId + * * @return int The number of deleted files * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OCP\Lock\LockedException - * @throws \OC\User\NoUserException + * @throws InvalidPathException + * @throws LockedException + * @throws NoUserException */ public function cleanupAttachments(int $fileId): int { $textFile = $this->rootFolder->getById($fileId); @@ -567,7 +578,7 @@ public function cleanupAttachments(int $fileId): int { $attachmentsByName[$attNode->getName()] = $attNode; } - $contentAttachmentNames = $this->getAttachmentNamesFromContent($textFile->getContent(), $fileId); + $contentAttachmentNames = self::getAttachmentNamesFromContent($textFile->getContent(), $fileId); $toDelete = array_diff(array_keys($attachmentsByName), $contentAttachmentNames); foreach ($toDelete as $name) { @@ -584,24 +595,11 @@ public function cleanupAttachments(int $fileId): int { * Get attachment file names listed in the markdown file content * * @param string $content + * @param int $fileId + * * @return array */ public static function getAttachmentNamesFromContent(string $content, int $fileId): array { - $oldMatches = []; - preg_match_all( - // simple version with .+ between the brackets - // '/\!\[.+\]\(text:\/\/image\?[^)]*imageFileName=([^)&]+)\)/', - // complex version of php-markdown - // matches ![ANY_CONSIDERED_CORRECT_BY_PHP-MARKDOWN](text://image?ANYTHING&imageFileName=FILE_NAME) and captures FILE_NAME - '/\!\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[(?>[^\[\]]+|\[\])*\])*\])*\])*\])*\])*\]\(text:\/\/image\?[^)]*imageFileName=([^)&]+)\)/', - $content, - $oldMatches, - PREG_SET_ORDER - ); - $oldNames = array_map(static function (array $match) { - return urldecode($match[1]); - }, $oldMatches); - $matches = []; // matches ![ANY_CONSIDERED_CORRECT_BY_PHP-MARKDOWN](.attachments.DOCUMENT_ID/ANY_FILE_NAME) and captures FILE_NAME preg_match_all( @@ -610,20 +608,19 @@ public static function getAttachmentNamesFromContent(string $content, int $fileI $matches, PREG_SET_ORDER ); - $names = array_map(static function (array $match) { + return array_map(static function (array $match) { return urldecode($match[1]); }, $matches); - - return array_merge($names, $oldNames); } /** * @param File $source * @param File $target + * * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OCP\Lock\LockedException + * @throws InvalidPathException + * @throws LockedException */ public function moveAttachments(File $source, File $target): void { // if the parent directory has changed @@ -645,9 +642,11 @@ public function moveAttachments(File $source, File $target): void { /** * @param File $source + * * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException + * @throws InvalidPathException + * @throws NoUserException */ public function deleteAttachments(File $source): void { // if there is an attachment dir for this file @@ -663,11 +662,12 @@ public function deleteAttachments(File $source): void { /** * @param File $source * @param File $target - * @return void + * + * @throws InvalidPathException + * @throws NoUserException * @throws NotFoundException * @throws NotPermittedException - * @throws \OCP\Files\InvalidPathException - * @throws \OCP\Lock\LockedException + * @throws LockedException */ public function copyAttachments(File $source, File $target): void { try { diff --git a/src/components/Editor/MarkdownContentEditor.vue b/src/components/Editor/MarkdownContentEditor.vue index adc8edb2bd8..65933e7789c 100644 --- a/src/components/Editor/MarkdownContentEditor.vue +++ b/src/components/Editor/MarkdownContentEditor.vue @@ -61,7 +61,7 @@ export default { get: () => this.$editor, }, [ATTACHMENT_RESOLVER]: { - get: () => this.$attachmentResolver, + get: () => this.$attachmentResolver ?? null, }, [IS_RICH_EDITOR]: { get: () => true, @@ -72,6 +72,10 @@ export default { }, props: { + fileId: { + type: Number, + default: null, + }, content: { type: String, required: true, @@ -110,11 +114,14 @@ export default { created() { this.$editor = this.createEditor() this.$editor.setEditable(!this.readOnly) - this.$attachmentResolver = new AttachmentResolver({ - currentDirectory: this.relativePath?.match(/.*\//), - user: getCurrentUser(), - shareToken: this.shareToken, - }) + if (this.fileId) { + this.$attachmentResolver = new AttachmentResolver({ + currentDirectory: this.relativePath?.match(/.*\//), + user: getCurrentUser(), + shareToken: this.shareToken, + fileId: this.fileId, + }) + } }, beforeDestroy() { diff --git a/src/components/ImageView/ShowImageModal.vue b/src/components/ImageView/ShowImageModal.vue index 114dcc895e3..8d3334cee76 100644 --- a/src/components/ImageView/ShowImageModal.vue +++ b/src/components/ImageView/ShowImageModal.vue @@ -1,7 +1,7 @@ - + @@ -27,14 +27,7 @@ export default { props: { images: { type: Array, - default() { - return [] - }, - validator(imagesList) { - return (imagesList.length === 0) - ? true - : imagesList.every(image => image.basename && image.source) - }, + required: true, }, startIndex: { type: Number, @@ -56,20 +49,18 @@ export default { }, }, watch: { - startIndex(val) { + 'startIndex'(val) { this.currentImageIndex = val }, }, methods: { showNextImage() { this.currentImageIndex = (this.currentImageIndex + 1) % this.images.length - this.currentImage = this.images[this.currentImageIndex] }, showPreviousImage() { this.currentImageIndex = this.currentImageIndex <= 0 ? this.images.length - 1 : this.currentImageIndex - 1 - this.currentImage = this.images[this.currentImageIndex] }, }, } diff --git a/src/editor.js b/src/editor.js index 4ee1ef9a25e..d60f7c9ce7e 100644 --- a/src/editor.js +++ b/src/editor.js @@ -27,7 +27,7 @@ import { ACTION_ATTACHMENT_PROMPT } from './components/Editor/MediaHandler.provi __webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line __webpack_public_path__ = OC.linkTo('text', 'js/') // eslint-disable-line -const apiVersion = '1.0' +const apiVersion = '1.1' Vue.prototype.t = window.t Vue.prototype.n = window.n @@ -133,8 +133,10 @@ window.OCA.Text.createEditor = async function({ // Element to render the editor to el, - // File mode is enabled by setting the fileId, otherwise content needs to be provided + // Session editor with file mode is enabled by setting the fileId and useSession. + // Otherwise, content needs to be provided. fileId = undefined, + useSession = true, filePath = undefined, shareToken = null, @@ -164,14 +166,16 @@ window.OCA.Text.createEditor = async function({ content, }) + const sessionEditor = fileId && useSession + const vm = new Vue({ provide() { return { [HOOK_LINK_CLICK]: onLinkClick, [ACTION_ATTACHMENT_PROMPT]: onFileInsert, - [EDITOR_UPLOAD]: !!fileId, - [HOOK_MENTION_SEARCH]: fileId ? true : onMentionSearch, - [HOOK_MENTION_INSERT]: fileId ? true : onMentionInsert, + [EDITOR_UPLOAD]: !!sessionEditor, + [HOOK_MENTION_SEARCH]: sessionEditor ? true : onMentionSearch, + [HOOK_MENTION_INSERT]: sessionEditor ? true : onMentionInsert, [ATTACHMENT_RESOLVER]: { resolve(src, preferRaw) { return [{ @@ -196,7 +200,7 @@ window.OCA.Text.createEditor = async function({ } : {} - return fileId + return sessionEditor ? h(Editor, { props: { fileId, @@ -211,6 +215,7 @@ window.OCA.Text.createEditor = async function({ }) : h(MarkdownContentEditor, { props: { + fileId, content: data.content, relativePath: filePath, shareToken, diff --git a/src/helpers/mime.js b/src/helpers/mime.js index ce938125e39..576b24a8883 100644 --- a/src/helpers/mime.js +++ b/src/helpers/mime.js @@ -20,18 +20,6 @@ * */ -const mimetypesImages = [ - 'image/png', - 'image/jpeg', - 'image/jpg', - 'image/gif', - 'image/x-xbitmap', - 'image/x-ms-bmp', - 'image/bmp', - 'image/svg+xml', - 'image/webp', -] - const openMimetypesMarkdown = [ 'text/markdown', ] @@ -68,7 +56,6 @@ if (!OC.appswebroots?.richdocuments && !OC.appswebroots?.onlyoffice) { const openMimetypes = [...openMimetypesMarkdown, ...openMimetypesPlainText] export { - mimetypesImages, openMimetypes, openMimetypesMarkdown, openMimetypesPlainText, diff --git a/src/nodes/ImageView.vue b/src/nodes/ImageView.vue index 966642d4584..50860bea64e 100644 --- a/src/nodes/ImageView.vue +++ b/src/nodes/ImageView.vue @@ -24,6 +24,7 @@ + @click="handleAttachmentClick"> {{ alt }} - {{ attachmentMetadata.size }} + {{ attachmentSize }} @@ -101,7 +102,7 @@ - @@ -109,13 +110,6 @@ - - - {{ alt }} - - - -