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 a0a7542b8..c8c76742b 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() @@ -126,6 +127,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 * @@ -133,25 +168,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/utils/fileUtils.ts b/src/utils/fileUtils.ts index ad6ac4f3c..21bdf0cf7 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -29,6 +29,8 @@ import camelcase from 'camelcase' import { isNumber } from './numberUtil' export interface FileInfo { + /** ID of the file (not unique if shared, use source instead) */ + fileid?: number /** Filename (name with path) */ filename: string /** Basename of the file */ @@ -47,6 +49,21 @@ export interface FileInfo { isFavorite?: boolean /** File type */ type: 'directory'|'file' + /** Attributes for file shares */ + shareAttributes?: string + + // custom attributes not fetch from API + + /** Does the file has an existing preview */ + hasPreview?: boolean + /** URL of the preview image */ + previewUrl?: string + /** The id of the peer live photo */ + metadataFilesLivePhoto?: number + /** The absolute dav path */ + davPath?: string + /** filename without extension */ + name?: string } /** diff --git a/src/views/Viewer.vue b/src/views/Viewer.vue index b23802802..46c80dfce 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') }} -
+