Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 0 additions & 59 deletions cypress/e2e/attachments.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,63 +374,4 @@ describe('Test all attachment insertion methods', () => {
.should('not.exist')
})
})

it('[share] check everything behaves correctly on the share target user side', () => {
const fileName = 'testShared.md'
cy.createMarkdown(fileName, '![git](.attachments.123/github.png)', false).then((fileId) => {
const attachmentsFolder = `.attachments.${fileId}`
cy.createFolder(attachmentsFolder)
cy.uploadFile('github.png', 'image/png', `${attachmentsFolder}/github.png`)
cy.shareFileToUser(fileName, recipient)
})

cy.login(recipient)
cy.showHiddenFiles()

cy.visit('/apps/files')
// check the file list
cy.getFile('testShared.md')
.should('exist')
cy.getFile('github.png')
.should('not.exist')

// check the attachment folder is not there
cy.getFile('testShared.md')
.should('exist')
.should('have.attr', 'data-cy-files-list-row-fileid')
.then((documentId) => {
cy.getFile('.attachments.' + documentId)
.should('not.exist')
})

// move the file and check the attachment folder is still not there
cy.moveFile('testShared.md', 'testMoved.md')
cy.reloadFileList()
cy.getFile('testMoved.md')
.should('exist')
.should('have.attr', 'data-cy-files-list-row-fileid')
.then((documentId) => {
cy.getFile('.attachments.' + documentId)
.should('not.exist')
})

// copy the file and check the attachment folder was copied
cy.copyFile('testMoved.md', 'testCopied.md')
cy.reloadFileList()
cy.getFile('testCopied.md')
.should('exist')
.should('have.attr', 'data-cy-files-list-row-fileid')
.then((documentId) => {
const files = attachmentFileNameToId[documentId]
cy.openFolder('.attachments.' + documentId)
for (const name in files) {
cy.getFile(name)
.should('exist')
.should('have.attr', 'data-cy-files-list-row-fileid')
// these are new copied attachment files
// so they should not have the same IDs than the ones created when uploading the files
.should('not.eq', String(files[name]))
}
})
})
})
100 changes: 100 additions & 0 deletions cypress/e2e/shareWithAttachments.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { randUser } from '../utils/index.js'

const user = randUser()
const recipient = randUser()

describe('Share with attachments', () => {
before(() => {
cy.createUser(user)
cy.createUser(recipient)
})

it('handle file operations by recipient user', () => {
cy.login(user)
const fileName = 'testShared.md'
cy.createFile(fileName, '![git](.attachments.123/github.png)')
.as('textFileId')
.then((fileId) => {
const attachmentsFolder = `.attachments.${fileId}`
cy.createFolder(attachmentsFolder)
cy.uploadFile(
'github.png',
'image/png',
`${attachmentsFolder}/github.png`,
).as('attachmentId')
cy.shareFileToUser(fileName, recipient)
})

cy.login(recipient)
cy.showHiddenFiles()

cy.visit('/apps/files')
// check the file list
cy.getFile('testShared.md').should('exist')
cy.getFile('github.png').should('not.exist')

// check the attachment folder is not there
cy.getFileId('testShared.md').then((documentId) => {
cy.getFile('.attachments.' + documentId).should('not.exist')
})

// move the file and check the attachment folder is still not there
cy.moveFile('testShared.md', 'testMoved.md')
cy.reloadFileList()
cy.getFileId('testMoved.md').then((documentId) => {
cy.getFile('.attachments.' + documentId).should('not.exist')
})

// copy the file and check the attachment folder was copied
cy.copyFile('testMoved.md', 'testCopied.md')
cy.reloadFileList()
cy.getFileId('testCopied.md').then((documentId) => {
cy.openFolder('.attachments.' + documentId)
})
cy.get('@attachmentId').then((attachmentId) => {
// these are new copied attachment files
// so they should not have the same IDs than the ones created when uploading the files
cy.getFileId('github.png').should('not.eq', String(attachmentId))
})
})
})

describe('Public Share with attachments', () => {
before(function() {
cy.createUser(user)
})

beforeEach(function() {
cy.login(user)
cy.createTestFolder().as('folder').then(cy.shareFile).as('token')
cy.then(function() {
cy.createFile(
`${this.folder}/Readme.md`,
'![Attached text](.attachments.123/lines.txt)',
).as('fileId')
})
cy.then(function() {
const attachmentsFolder = `${this.folder}/.attachments.${this.fileId}`
cy.createFolder(attachmentsFolder)
cy.uploadFile(
'lines.txt',
'text/plain',
`${attachmentsFolder}/lines.txt`,
)
})
cy.clearCookies()
})

it('open attached files in folder description', function() {
cy.visit(`/s/${this.token}`)
cy.get('.content-wrapper').should('exist')
cy.get('.content-wrapper .name', { timeout: 10_000 }).click()
cy.get('.viewer').should('exist')
cy.get('.language-plaintext').should('contain', 'multiple lines')
})
})
37 changes: 35 additions & 2 deletions lib/Service/AttachmentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,11 @@ public function getAttachmentList(int $documentId, ?string $userId = null, ?Sess
: '?documentId=' . $documentId . $shareTokenUrlString;

$attachments = [];
$userFolder = $userId !== null ? $this->rootFolder->getUserFolder($userId) : null;

// Folder davPath need to be relative to.
$davFolder = $userId !== null
? $this->rootFolder->getUserFolder($userId)
: $this->getShareFolder($shareToken);
foreach ($attachmentDir->getDirectoryListing() as $node) {
if (!($node instanceof File)) {
// Ignore anything but files
Expand All @@ -233,7 +237,7 @@ public function getAttachmentList(int $documentId, ?string $userId = null, ?Sess
'mimetype' => $node->getMimeType(),
'mtime' => $node->getMTime(),
'isImage' => $isImage,
'davPath' => $userFolder?->getRelativePath($node->getPath()),
'davPath' => $davFolder?->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),
Expand Down Expand Up @@ -530,6 +534,35 @@ private function getTextFilePublic(?int $documentId, string $shareToken): File {
throw new NotFoundException('Text file with id=' . $documentId . ' and shareToken ' . $shareToken . ' was not found.');
}

/**
* Get share folder
*
* @param string $shareToken
*
* @throws NotFoundException
*/
private function getShareFolder(string $shareToken): ?Folder {
// is the file shared with this token?
try {
$share = $this->shareManager->getShareByToken($shareToken);
if (in_array($share->getShareType(), [IShare::TYPE_LINK, IShare::TYPE_EMAIL])) {
// shared file or folder?
if ($share->getNodeType() === 'file') {
return null;
} elseif ($share->getNodeType() === 'folder') {
$folder = $share->getNode();
if ($folder instanceof Folder) {
return $folder;
}
throw new NotFoundException('Share folder for ' . $shareToken . ' was not a folder.');
}
}
} catch (ShareNotFound $e) {
// same as below
}
throw new NotFoundException('Share folder for ' . $shareToken . ' was not found.');
}

/**
* Actually delete attachment files which are not pointed in the markdown content
*
Expand Down
Loading