From d81ceef8ccedbe1bb703a1cce302c7503b1bc217 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sun, 8 Sep 2024 21:17:21 +0200 Subject: [PATCH] fix: Hide download action when file does not provide download permissions This is not only a possibility for public shares but also for internal shares, the current code only "checked" public shares. This adds the same logic we use in the files app. Probably something to move to `@nextcloud/sharing` but for the moment lets just reuse here. Signed-off-by: Ferdinand Thiessen [skip ci] --- cypress/e2e/download-forbidden.cy.ts | 67 ++++++++++++++++++++++++++++ cypress/support/commands.ts | 61 ++++++++++++++++--------- src/utils/canDownload.js | 25 ----------- src/utils/canDownload.ts | 24 ++++++++++ src/views/Viewer.vue | 33 ++++++++++---- 5 files changed, 155 insertions(+), 55 deletions(-) create mode 100644 cypress/e2e/download-forbidden.cy.ts delete mode 100644 src/utils/canDownload.js create mode 100644 src/utils/canDownload.ts diff --git a/cypress/e2e/download-forbidden.cy.ts b/cypress/e2e/download-forbidden.cy.ts new file mode 100644 index 000000000..4c432b6bd --- /dev/null +++ b/cypress/e2e/download-forbidden.cy.ts @@ -0,0 +1,67 @@ +/** + * SPDX-License: AGPL-3.0-or-later + * SPDX-: Nextcloud GmbH and Nextcloud contributors + */ + +import type { User } from '@nextcloud/cypress' +import { ShareType } from '@nextcloud/sharing' + +describe('Disable download button if forbidden', { testIsolation: true }, () => { + let sharee: User + + before(() => { + cy.createRandomUser().then((user) => { sharee = user }) + cy.createRandomUser().then((user) => { + // Upload test files + cy.createFolder(user, '/Photos') + cy.uploadFile(user, 'image1.jpg', 'image/jpeg', '/Photos/image1.jpg') + + cy.login(user) + cy.createShare('/Photos', + { shareWith: sharee.userId, shareType: ShareType.User, attributes: [{ scope: 'permissions', key: 'download', value: false }] }, + ) + cy.logout() + }) + }) + + beforeEach(() => { + cy.login(sharee) + cy.visit('/apps/files') + cy.openFile('Photos') + }) + + it('See the shared folder and images in files list', () => { + cy.getFile('image1.jpg', { timeout: 10000 }) + .should('contain', 'image1 .jpg') + }) + + // TODO: Fix no-download files on server + it.skip('See the image can be shown', () => { + cy.getFile('image1.jpg').should('be.visible') + cy.openFile('image1.jpg') + cy.get('body > .viewer').should('be.visible') + + cy.get('body > .viewer', { timeout: 10000 }) + .should('be.visible') + .and('have.class', 'modal-mask') + .and('not.have.class', 'icon-loading') + }) + + it('See the title on the viewer header but not the Download nor the menu button', () => { + cy.getFile('image1.jpg').should('be.visible') + cy.openFile('image1.jpg') + cy.get('body > .viewer .modal-header__name').should('contain', 'image1.jpg') + + cy.get('[role="dialog"]') + .should('be.visible') + .find('button[aria-label="Actions"]') + .click() + + cy.get('[role="menu"]:visible') + .find('button') + .should('have.length', 2) + .each(($el) => { + expect($el.text()).to.match(/(Full screen|Open sidebar)/i) + }) + }) +}) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 25027f89d..fb3c99f79 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -20,10 +20,11 @@ * */ -import { addCommands, User } from '@nextcloud/cypress' -import { basename } from 'path' -import axios from '@nextcloud/axios' +import { addCommands } from '@nextcloud/cypress' +import { Permission } from '@nextcloud/files' +import { ShareType } from '@nextcloud/sharing' import { addCompareSnapshotCommand } from 'cypress-visual-regression/dist/command' +import { basename } from 'path' addCommands() addCompareSnapshotCommand() @@ -125,6 +126,40 @@ Cypress.Commands.add( }, ) +interface ShareOptions { + shareType: number + shareWith?: string + permissions: number + attributes?: { value: boolean, key: string, scope: string}[] +} + +Cypress.Commands.add('createShare', (path: string, shareOptions?: ShareOptions) => { + return cy.request('/csrftoken').then(({ body }) => { + const requesttoken = body.token + + return cy.request({ + method: 'POST', + url: '../ocs/v2.php/apps/files_sharing/api/v1/shares?format=json', + headers: { + requesttoken, + }, + body: { + path, + permissions: Permission.READ, + ...shareOptions, + attributes: shareOptions?.attributes && JSON.stringify(shareOptions.attributes), + }, + }).then(({ body }) => { + const shareToken = body.ocs?.data?.token + if (shareToken === undefined) { + throw new Error('Invalid OCS response') + } + cy.log('Share link created', shareToken) + return cy.wrap(shareToken) + }) + }) +}) + /** * Create a share link and return the share url * @@ -132,25 +167,7 @@ Cypress.Commands.add( * @return {string} the share link url */ Cypress.Commands.add('createLinkShare', path => { - return cy.window().then(async window => { - try { - const request = await axios.post(`${Cypress.env('baseUrl')}/ocs/v2.php/apps/files_sharing/api/v1/shares`, { - path, - shareType: window.OC.Share.SHARE_TYPE_LINK, - }, { - headers: { - requesttoken: window.OC.requestToken, - }, - }) - if (!('ocs' in request.data) || !('token' in request.data.ocs.data && request.data.ocs.data.token.length > 0)) { - throw request - } - cy.log('Share link created', request.data.ocs.data.token) - return cy.wrap(request.data.ocs.data.token) - } catch (error) { - console.error(error) - } - }).should('have.length', 15) + return cy.createShare(path, { shareType: ShareType.Link }) }) Cypress.Commands.overwrite('compareSnapshot', (originalFn, subject, name, options) => { diff --git a/src/utils/canDownload.js b/src/utils/canDownload.js deleted file mode 100644 index f132ea772..000000000 --- a/src/utils/canDownload.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @copyright Copyright (c) 2020 John Molakvoæ - * - * @author John Molakvoæ - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -const hideDownloadElmt = document.getElementById('hideDownload') -// true = hidden download -export default () => !hideDownloadElmt || (hideDownloadElmt && hideDownloadElmt.value !== 'true') diff --git a/src/utils/canDownload.ts b/src/utils/canDownload.ts new file mode 100644 index 000000000..c7567d2c3 --- /dev/null +++ b/src/utils/canDownload.ts @@ -0,0 +1,24 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { FileInfo } from './fileUtils' + +/** + * Check if download permissions are granted for a file + * @param fileInfo The file info to check + */ +export function canDownload(fileInfo: FileInfo) { + // TODO: This should probably be part of `@nextcloud/sharing` + // check share attributes + const shareAttributes = JSON.parse(fileInfo.shareAttributes || '[]') + + if (shareAttributes && shareAttributes.length > 0) { + const downloadAttribute = shareAttributes.find(({ scope, key }) => scope === 'permissions' && key === 'download') + // We only forbid download if the attribute is *explicitly* set to 'false' + return downloadAttribute?.value !== false + } + // otherwise return true (as the file needs read permission otherwise we would not have opened it) + return true +} diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index 72523ad92..beabd7a5a 100644 --- a/src/views/Viewer.vue +++ b/src/views/Viewer.vue @@ -62,7 +62,6 @@ :spread-navigation="true" :style="{ width: isSidebarShown ? `${sidebarPosition}px` : null }" :name="currentFile.basename" - :view="currentFile.modal" class="viewer" size="full" @close="close" @@ -99,7 +98,7 @@ :close-after-click="true" :href="downloadPath"> {{ t('viewer', 'Download') }} @@ -107,13 +106,16 @@ :close-after-click="true" @click="onDelete"> {{ t('viewer', 'Delete') }} -
+