diff options
Diffstat (limited to 'apps/files_sharing/src/services')
-rw-r--r-- | apps/files_sharing/src/services/ConfigService.js | 322 | ||||
-rw-r--r-- | apps/files_sharing/src/services/ConfigService.ts | 333 | ||||
-rw-r--r-- | apps/files_sharing/src/services/ExternalShareActions.js | 2 | ||||
-rw-r--r-- | apps/files_sharing/src/services/GuestNameValidity.ts | 45 | ||||
-rw-r--r-- | apps/files_sharing/src/services/SharingService.spec.ts | 234 | ||||
-rw-r--r-- | apps/files_sharing/src/services/SharingService.ts | 94 | ||||
-rw-r--r-- | apps/files_sharing/src/services/TabSections.js | 8 | ||||
-rw-r--r-- | apps/files_sharing/src/services/TokenService.ts | 20 | ||||
-rw-r--r-- | apps/files_sharing/src/services/WebdavClient.ts | 18 |
9 files changed, 670 insertions, 406 deletions
diff --git a/apps/files_sharing/src/services/ConfigService.js b/apps/files_sharing/src/services/ConfigService.js deleted file mode 100644 index 3d9e949724e..00000000000 --- a/apps/files_sharing/src/services/ConfigService.js +++ /dev/null @@ -1,322 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { getCapabilities } from '@nextcloud/capabilities' - -export default class Config { - - constructor() { - this._capabilities = getCapabilities() - } - - /** - * Get default share permissions, if any - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get defaultPermissions() { - return this._capabilities.files_sharing?.default_permissions - } - - /** - * Is public upload allowed on link shares ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isPublicUploadEnabled() { - return this._capabilities.files_sharing?.public.upload - } - - /** - * Are link share allowed ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isShareWithLinkAllowed() { - return document.getElementById('allowShareWithLink') - && document.getElementById('allowShareWithLink').value === 'yes' - } - - /** - * Get the federated sharing documentation link - * - * @return {string} - * @readonly - * @memberof Config - */ - get federatedShareDocLink() { - return OC.appConfig.core.federatedCloudShareDoc - } - - /** - * Get the default link share expiration date - * - * @return {Date|null} - * @readonly - * @memberof Config - */ - get defaultExpirationDate() { - if (this.isDefaultExpireDateEnabled) { - return new Date(new Date().setDate(new Date().getDate() + this.defaultExpireDate)) - } - return null - } - - /** - * Get the default internal expiration date - * - * @return {Date|null} - * @readonly - * @memberof Config - */ - get defaultInternalExpirationDate() { - if (this.isDefaultInternalExpireDateEnabled) { - return new Date(new Date().setDate(new Date().getDate() + this.defaultInternalExpireDate)) - } - return null - } - - /** - * Get the default remote expiration date - * - * @return {Date|null} - * @readonly - * @memberof Config - */ - get defaultRemoteExpirationDateString() { - if (this.isDefaultRemoteExpireDateEnabled) { - return new Date(new Date().setDate(new Date().getDate() + this.defaultRemoteExpireDate)) - } - return null - } - - /** - * Are link shares password-enforced ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get enforcePasswordForPublicLink() { - return OC.appConfig.core.enforcePasswordForPublicLink === true - } - - /** - * Is password asked by default on link shares ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get enableLinkPasswordByDefault() { - return OC.appConfig.core.enableLinkPasswordByDefault === true - } - - /** - * Is link shares expiration enforced ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isDefaultExpireDateEnforced() { - return OC.appConfig.core.defaultExpireDateEnforced === true - } - - /** - * Is there a default expiration date for new link shares ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isDefaultExpireDateEnabled() { - return OC.appConfig.core.defaultExpireDateEnabled === true - } - - /** - * Is internal shares expiration enforced ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isDefaultInternalExpireDateEnforced() { - return OC.appConfig.core.defaultInternalExpireDateEnforced === true - } - - /** - * Is remote shares expiration enforced ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isDefaultRemoteExpireDateEnforced() { - return OC.appConfig.core.defaultRemoteExpireDateEnforced === true - } - - /** - * Is there a default expiration date for new internal shares ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isDefaultInternalExpireDateEnabled() { - return OC.appConfig.core.defaultInternalExpireDateEnabled === true - } - - /** - * Is there a default expiration date for new remote shares ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isDefaultRemoteExpireDateEnabled() { - return OC.appConfig.core.defaultRemoteExpireDateEnabled === true - } - - /** - * Are users on this server allowed to send shares to other servers ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isRemoteShareAllowed() { - return OC.appConfig.core.remoteShareAllowed === true - } - - /** - * Is sharing my mail (link share) enabled ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isMailShareAllowed() { - // eslint-disable-next-line camelcase - return this._capabilities?.files_sharing?.sharebymail !== undefined - // eslint-disable-next-line camelcase - && this._capabilities?.files_sharing?.public?.enabled === true - } - - /** - * Get the default days to link shares expiration - * - * @return {number} - * @readonly - * @memberof Config - */ - get defaultExpireDate() { - return OC.appConfig.core.defaultExpireDate - } - - /** - * Get the default days to internal shares expiration - * - * @return {number} - * @readonly - * @memberof Config - */ - get defaultInternalExpireDate() { - return OC.appConfig.core.defaultInternalExpireDate - } - - /** - * Get the default days to remote shares expiration - * - * @return {number} - * @readonly - * @memberof Config - */ - get defaultRemoteExpireDate() { - return OC.appConfig.core.defaultRemoteExpireDate - } - - /** - * Is resharing allowed ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isResharingAllowed() { - return OC.appConfig.core.resharingAllowed === true - } - - /** - * Is password enforced for mail shares ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get isPasswordForMailSharesRequired() { - return (this._capabilities.files_sharing.sharebymail === undefined) ? false : this._capabilities.files_sharing.sharebymail.password.enforced - } - - /** - * @return {boolean} - * @readonly - * @memberof Config - */ - get shouldAlwaysShowUnique() { - return (this._capabilities.files_sharing?.sharee?.always_show_unique === true) - } - - /** - * Is sharing with groups allowed ? - * - * @return {boolean} - * @readonly - * @memberof Config - */ - get allowGroupSharing() { - return OC.appConfig.core.allowGroupSharing === true - } - - /** - * Get the maximum results of a share search - * - * @return {number} - * @readonly - * @memberof Config - */ - get maxAutocompleteResults() { - return parseInt(OC.config['sharing.maxAutocompleteResults'], 10) || 25 - } - - /** - * Get the minimal string length - * to initiate a share search - * - * @return {number} - * @readonly - * @memberof Config - */ - get minSearchStringLength() { - return parseInt(OC.config['sharing.minSearchStringLength'], 10) || 0 - } - - /** - * Get the password policy config - * - * @return {object} - * @readonly - * @memberof Config - */ - get passwordPolicy() { - return this._capabilities.password_policy ? this._capabilities.password_policy : {} - } - -} diff --git a/apps/files_sharing/src/services/ConfigService.ts b/apps/files_sharing/src/services/ConfigService.ts new file mode 100644 index 00000000000..547038f362d --- /dev/null +++ b/apps/files_sharing/src/services/ConfigService.ts @@ -0,0 +1,333 @@ +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { getCapabilities } from '@nextcloud/capabilities' +import { loadState } from '@nextcloud/initial-state' + +type PasswordPolicyCapabilities = { + enforceNonCommonPassword: boolean + enforceNumericCharacters: boolean + enforceSpecialCharacters: boolean + enforceUpperLowerCase: boolean + minLength: number +} + +type FileSharingCapabilities = { + api_enabled: boolean, + public: { + enabled: boolean, + password: { + enforced: boolean, + askForOptionalPassword: boolean + }, + expire_date: { + enabled: boolean, + days: number, + enforced: boolean + }, + multiple_links: boolean, + expire_date_internal: { + enabled: boolean + }, + expire_date_remote: { + enabled: boolean + }, + send_mail: boolean, + upload: boolean, + upload_files_drop: boolean, + custom_tokens: boolean, + }, + resharing: boolean, + user: { + send_mail: boolean, + expire_date: { + enabled: boolean + } + }, + group_sharing: boolean, + group: { + enabled: boolean, + expire_date: { + enabled: true + } + }, + default_permissions: number, + federation: { + outgoing: boolean, + incoming: boolean, + expire_date: { + enabled: boolean + }, + expire_date_supported: { + enabled: boolean + } + }, + sharee: { + query_lookup_default: boolean, + always_show_unique: boolean + }, + sharebymail: { + enabled: boolean, + send_password_by_mail: boolean, + upload_files_drop: { + enabled: boolean + }, + password: { + enabled: boolean, + enforced: boolean + }, + expire_date: { + enabled: boolean, + enforced: boolean + } + } +} + +type Capabilities = { + files_sharing: FileSharingCapabilities + password_policy: PasswordPolicyCapabilities +} + +export default class Config { + + _capabilities: Capabilities + + constructor() { + this._capabilities = getCapabilities() as Capabilities + } + + /** + * Get default share permissions, if any + */ + get defaultPermissions(): number { + return this._capabilities.files_sharing?.default_permissions + } + + /** + * Is public upload allowed on link shares ? + * This covers File request and Full upload/edit option. + */ + get isPublicUploadEnabled(): boolean { + return this._capabilities.files_sharing?.public?.upload === true + } + + /** + * Get the federated sharing documentation link + */ + get federatedShareDocLink() { + return window.OC.appConfig.core.federatedCloudShareDoc + } + + /** + * Get the default link share expiration date + */ + get defaultExpirationDate(): Date|null { + if (this.isDefaultExpireDateEnabled && this.defaultExpireDate !== null) { + return new Date(new Date().setDate(new Date().getDate() + this.defaultExpireDate)) + } + return null + } + + /** + * Get the default internal expiration date + */ + get defaultInternalExpirationDate(): Date|null { + if (this.isDefaultInternalExpireDateEnabled && this.defaultInternalExpireDate !== null) { + return new Date(new Date().setDate(new Date().getDate() + this.defaultInternalExpireDate)) + } + return null + } + + /** + * Get the default remote expiration date + */ + get defaultRemoteExpirationDateString(): Date|null { + if (this.isDefaultRemoteExpireDateEnabled && this.defaultRemoteExpireDate !== null) { + return new Date(new Date().setDate(new Date().getDate() + this.defaultRemoteExpireDate)) + } + return null + } + + /** + * Are link shares password-enforced ? + */ + get enforcePasswordForPublicLink(): boolean { + return window.OC.appConfig.core.enforcePasswordForPublicLink === true + } + + /** + * Is password asked by default on link shares ? + */ + get enableLinkPasswordByDefault(): boolean { + return window.OC.appConfig.core.enableLinkPasswordByDefault === true + } + + /** + * Is link shares expiration enforced ? + */ + get isDefaultExpireDateEnforced(): boolean { + return window.OC.appConfig.core.defaultExpireDateEnforced === true + } + + /** + * Is there a default expiration date for new link shares ? + */ + get isDefaultExpireDateEnabled(): boolean { + return window.OC.appConfig.core.defaultExpireDateEnabled === true + } + + /** + * Is internal shares expiration enforced ? + */ + get isDefaultInternalExpireDateEnforced(): boolean { + return window.OC.appConfig.core.defaultInternalExpireDateEnforced === true + } + + /** + * Is there a default expiration date for new internal shares ? + */ + get isDefaultInternalExpireDateEnabled(): boolean { + return window.OC.appConfig.core.defaultInternalExpireDateEnabled === true + } + + /** + * Is remote shares expiration enforced ? + */ + get isDefaultRemoteExpireDateEnforced(): boolean { + return window.OC.appConfig.core.defaultRemoteExpireDateEnforced === true + } + + /** + * Is there a default expiration date for new remote shares ? + */ + get isDefaultRemoteExpireDateEnabled(): boolean { + return window.OC.appConfig.core.defaultRemoteExpireDateEnabled === true + } + + /** + * Are users on this server allowed to send shares to other servers ? + */ + get isRemoteShareAllowed(): boolean { + return window.OC.appConfig.core.remoteShareAllowed === true + } + + /** + * Is federation enabled ? + */ + get isFederationEnabled(): boolean { + return this._capabilities?.files_sharing?.federation?.outgoing === true + } + + /** + * Is public sharing enabled ? + */ + get isPublicShareAllowed(): boolean { + return this._capabilities?.files_sharing?.public?.enabled === true + } + + /** + * Is sharing my mail (link share) enabled ? + */ + get isMailShareAllowed(): boolean { + // eslint-disable-next-line camelcase + return this._capabilities?.files_sharing?.sharebymail?.enabled === true + // eslint-disable-next-line camelcase + && this.isPublicShareAllowed === true + } + + /** + * Get the default days to link shares expiration + */ + get defaultExpireDate(): number|null { + return window.OC.appConfig.core.defaultExpireDate + } + + /** + * Get the default days to internal shares expiration + */ + get defaultInternalExpireDate(): number|null { + return window.OC.appConfig.core.defaultInternalExpireDate + } + + /** + * Get the default days to remote shares expiration + */ + get defaultRemoteExpireDate(): number|null { + return window.OC.appConfig.core.defaultRemoteExpireDate + } + + /** + * Is resharing allowed ? + */ + get isResharingAllowed(): boolean { + return window.OC.appConfig.core.resharingAllowed === true + } + + /** + * Is password enforced for mail shares ? + */ + get isPasswordForMailSharesRequired(): boolean { + return this._capabilities.files_sharing?.sharebymail?.password?.enforced === true + } + + /** + * Always show the email or userid unique sharee label if enabled by the admin + */ + get shouldAlwaysShowUnique(): boolean { + return this._capabilities.files_sharing?.sharee?.always_show_unique === true + } + + /** + * Is sharing with groups allowed ? + */ + get allowGroupSharing(): boolean { + return window.OC.appConfig.core.allowGroupSharing === true + } + + /** + * Get the maximum results of a share search + */ + get maxAutocompleteResults(): number { + return parseInt(window.OC.config['sharing.maxAutocompleteResults'], 10) || 25 + } + + /** + * Get the minimal string length + * to initiate a share search + */ + get minSearchStringLength(): number { + return parseInt(window.OC.config['sharing.minSearchStringLength'], 10) || 0 + } + + /** + * Get the password policy configuration + */ + get passwordPolicy(): PasswordPolicyCapabilities { + return this._capabilities?.password_policy || {} + } + + /** + * Returns true if custom tokens are allowed + */ + get allowCustomTokens(): boolean { + return this._capabilities?.files_sharing?.public?.custom_tokens + } + + /** + * Show federated shares as internal shares + * @return {boolean} + */ + get showFederatedSharesAsInternal(): boolean { + return loadState('files_sharing', 'showFederatedSharesAsInternal', false) + } + + /** + * Show federated shares to trusted servers as internal shares + * @return {boolean} + */ + get showFederatedSharesToTrustedServersAsInternal(): boolean { + return loadState('files_sharing', 'showFederatedSharesToTrustedServersAsInternal', false) + } + +} diff --git a/apps/files_sharing/src/services/ExternalShareActions.js b/apps/files_sharing/src/services/ExternalShareActions.js index ae1f52e30b4..6ffd7014fe2 100644 --- a/apps/files_sharing/src/services/ExternalShareActions.js +++ b/apps/files_sharing/src/services/ExternalShareActions.js @@ -48,7 +48,7 @@ export default class ExternalShareActions { if (typeof action !== 'object' || typeof action.id !== 'string' || typeof action.data !== 'function' // () => {disabled: true} - || !Array.isArray(action.shareType) // [\@nextcloud/sharing.Types.SHARE_TYPE_LINK, ...] + || !Array.isArray(action.shareType) // [\@nextcloud/sharing.Types.Link, ...] || typeof action.handlers !== 'object' // {click: () => {}, ...} || !Object.values(action.handlers).every(handler => typeof handler === 'function')) { console.error('Invalid action provided', action) diff --git a/apps/files_sharing/src/services/GuestNameValidity.ts b/apps/files_sharing/src/services/GuestNameValidity.ts new file mode 100644 index 00000000000..0557c5253ca --- /dev/null +++ b/apps/files_sharing/src/services/GuestNameValidity.ts @@ -0,0 +1,45 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { InvalidFilenameError, InvalidFilenameErrorReason, validateFilename } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' + +/** + * Get the validity of a filename (empty if valid). + * This can be used for `setCustomValidity` on input elements + * @param name The filename + * @param escape Escape the matched string in the error (only set when used in HTML) + */ +export function getGuestNameValidity(name: string, escape = false): string { + if (name.trim() === '') { + return t('files', 'Names must not be empty.') + } + + if (name.startsWith('.')) { + return t('files', 'Names must not start with a dot.') + } + + try { + validateFilename(name) + return '' + } catch (error) { + if (!(error instanceof InvalidFilenameError)) { + throw error + } + + switch (error.reason) { + case InvalidFilenameErrorReason.Character: + return t('files', '"{char}" is not allowed inside a name.', { char: error.segment }, undefined, { escape }) + case InvalidFilenameErrorReason.ReservedName: + return t('files', '"{segment}" is a reserved name and not allowed.', { segment: error.segment }, undefined, { escape: false }) + case InvalidFilenameErrorReason.Extension: + if (error.segment.match(/\.[a-z]/i)) { + return t('files', '"{extension}" is not an allowed name.', { extension: error.segment }, undefined, { escape: false }) + } + return t('files', 'Names must not end with "{extension}".', { extension: error.segment }, undefined, { escape: false }) + default: + return t('files', 'Invalid name.') + } + } +} diff --git a/apps/files_sharing/src/services/SharingService.spec.ts b/apps/files_sharing/src/services/SharingService.spec.ts index d756c4755d7..936c1afafc4 100644 --- a/apps/files_sharing/src/services/SharingService.spec.ts +++ b/apps/files_sharing/src/services/SharingService.spec.ts @@ -3,28 +3,33 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { OCSResponse } from '@nextcloud/typings/ocs' -import { expect } from '@jest/globals' -import { Type } from '@nextcloud/sharing' -import * as auth from '@nextcloud/auth' -import axios from '@nextcloud/axios' -import { getContents } from './SharingService' import { File, Folder } from '@nextcloud/files' +import { ShareType } from '@nextcloud/sharing' +import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest' + +import { getContents } from './SharingService' +import * as auth from '@nextcloud/auth' import logger from './logger' -global.window.OC = { - TAG_FAVORITE: '_$!<Favorite>!$_', -} +const TAG_FAVORITE = '_$!<Favorite>!$_' + +const axios = vi.hoisted(() => ({ get: vi.fn() })) +vi.mock('@nextcloud/auth') +vi.mock('@nextcloud/axios', () => ({ default: axios })) -// Mock webroot variable +// Mock TAG beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any)._oc_webroot = '' + window.OC = { + ...window.OC, + TAG_FAVORITE, + } }) describe('SharingService methods definitions', () => { - beforeAll(() => { - jest.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => { + beforeEach(() => { + vi.resetAllMocks() + axios.get.mockImplementation(async (): Promise<unknown> => { return { data: { ocs: { @@ -35,20 +40,16 @@ describe('SharingService methods definitions', () => { }, data: [], }, - } as OCSResponse<any>, + } as OCSResponse, } }) }) - afterAll(() => { - jest.restoreAllMocks() - }) - test('Shared with you', async () => { await getContents(true, false, false, false, []) expect(axios.get).toHaveBeenCalledTimes(2) - expect(axios.get).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares', { + expect(axios.get).toHaveBeenNthCalledWith(1, 'http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares', { headers: { 'Content-Type': 'application/json', }, @@ -57,7 +58,7 @@ describe('SharingService methods definitions', () => { include_tags: true, }, }) - expect(axios.get).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/remote_shares', { + expect(axios.get).toHaveBeenNthCalledWith(2, 'http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/remote_shares', { headers: { 'Content-Type': 'application/json', }, @@ -71,7 +72,7 @@ describe('SharingService methods definitions', () => { await getContents(false, true, false, false, []) expect(axios.get).toHaveBeenCalledTimes(1) - expect(axios.get).toHaveBeenCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares', { + expect(axios.get).toHaveBeenCalledWith('http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares', { headers: { 'Content-Type': 'application/json', }, @@ -86,7 +87,7 @@ describe('SharingService methods definitions', () => { await getContents(false, false, true, false, []) expect(axios.get).toHaveBeenCalledTimes(2) - expect(axios.get).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending', { + expect(axios.get).toHaveBeenNthCalledWith(1, 'http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares/pending', { headers: { 'Content-Type': 'application/json', }, @@ -94,7 +95,7 @@ describe('SharingService methods definitions', () => { include_tags: true, }, }) - expect(axios.get).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending', { + expect(axios.get).toHaveBeenNthCalledWith(2, 'http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending', { headers: { 'Content-Type': 'application/json', }, @@ -108,7 +109,7 @@ describe('SharingService methods definitions', () => { await getContents(false, true, false, false, []) expect(axios.get).toHaveBeenCalledTimes(1) - expect(axios.get).toHaveBeenCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares', { + expect(axios.get).toHaveBeenCalledWith('http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares', { headers: { 'Content-Type': 'application/json', }, @@ -120,7 +121,7 @@ describe('SharingService methods definitions', () => { }) test('Unknown owner', async () => { - jest.spyOn(auth, 'getCurrentUser').mockReturnValue(null) + vi.spyOn(auth, 'getCurrentUser').mockReturnValue(null) const results = await getContents(false, true, false, false, []) expect(results.folder.owner).toEqual(null) @@ -128,8 +129,9 @@ describe('SharingService methods definitions', () => { }) describe('SharingService filtering', () => { - beforeAll(() => { - jest.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => { + beforeEach(() => { + vi.resetAllMocks() + axios.get.mockImplementation(async (): Promise<unknown> => { return { data: { ocs: { @@ -141,7 +143,7 @@ describe('SharingService filtering', () => { data: [ { id: '62', - share_type: Type.SHARE_TYPE_USER, + share_type: ShareType.User, uid_owner: 'test', displayname_owner: 'test', permissions: 31, @@ -167,12 +169,8 @@ describe('SharingService filtering', () => { }) }) - afterAll(() => { - jest.restoreAllMocks() - }) - test('Shared with others filtering', async () => { - const shares = await getContents(false, true, false, false, [Type.SHARE_TYPE_USER]) + const shares = await getContents(false, true, false, false, [ShareType.User]) expect(axios.get).toHaveBeenCalledTimes(1) expect(shares.contents).toHaveLength(1) @@ -181,7 +179,7 @@ describe('SharingService filtering', () => { }) test('Shared with others filtering empty', async () => { - const shares = await getContents(false, true, false, false, [Type.SHARE_TYPE_LINK]) + const shares = await getContents(false, true, false, false, [ShareType.Link]) expect(axios.get).toHaveBeenCalledTimes(1) expect(shares.contents).toHaveLength(0) @@ -274,11 +272,65 @@ describe('SharingService share to Node mapping', () => { mail_send: 0, hide_download: 0, attributes: null, - tags: [window.OC.TAG_FAVORITE], + tags: [TAG_FAVORITE], + } + + const remoteFileAccepted = { + mimetype: 'text/markdown', + mtime: 1688721600, + permissions: 19, + type: 'file', + file_id: 1234, + id: 4, + share_type: ShareType.User, + parent: null, + remote: 'http://exampe.com', + remote_id: '12345', + share_token: 'share-token', + name: '/test.md', + mountpoint: '/shares/test.md', + owner: 'owner-uid', + user: 'sharee-uid', + accepted: true, + } + + const remoteFilePending = { + mimetype: 'text/markdown', + mtime: 1688721600, + permissions: 19, + type: 'file', + file_id: 1234, + id: 4, + share_type: ShareType.User, + parent: null, + remote: 'http://exampe.com', + remote_id: '12345', + share_token: 'share-token', + name: '/test.md', + mountpoint: '/shares/test.md', + owner: 'owner-uid', + user: 'sharee-uid', + accepted: false, } + const tempExternalFile = { + id: 65, + share_type: 0, + parent: -1, + remote: 'http://nextcloud1.local/', + remote_id: '71', + share_token: '9GpiAmTIjayclrE', + name: '/test.md', + owner: 'owner-uid', + user: 'sharee-uid', + mountpoint: '{{TemporaryMountPointName#/test.md}}', + accepted: 0, + } + + beforeEach(() => { vi.resetAllMocks() }) + test('File', async () => { - jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({ + axios.get.mockReturnValueOnce(Promise.resolve({ data: { ocs: { data: [shareFile], @@ -294,7 +346,7 @@ describe('SharingService share to Node mapping', () => { const file = shares.contents[0] as File expect(file).toBeInstanceOf(File) expect(file.fileid).toBe(530936) - expect(file.source).toBe('http://localhost/remote.php/dav/files/test/document.md') + expect(file.source).toBe('http://nextcloud.local/remote.php/dav/files/test/document.md') expect(file.owner).toBe('test') expect(file.mime).toBe('text/markdown') expect(file.mtime).toBeInstanceOf(Date) @@ -303,11 +355,18 @@ describe('SharingService share to Node mapping', () => { expect(file.root).toBe('/files/test') expect(file.attributes).toBeInstanceOf(Object) expect(file.attributes['has-preview']).toBe(true) + expect(file.attributes.sharees).toEqual({ + sharee: { + id: 'user00', + 'display-name': 'User00', + type: 0, + }, + }) expect(file.attributes.favorite).toBe(0) }) test('Folder', async () => { - jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({ + axios.get.mockReturnValueOnce(Promise.resolve({ data: { ocs: { data: [shareFolder], @@ -323,7 +382,7 @@ describe('SharingService share to Node mapping', () => { const folder = shares.contents[0] as Folder expect(folder).toBeInstanceOf(Folder) expect(folder.fileid).toBe(531080) - expect(folder.source).toBe('http://localhost/remote.php/dav/files/test/Folder') + expect(folder.source).toBe('http://nextcloud.local/remote.php/dav/files/test/Folder') expect(folder.owner).toBe('test') expect(folder.mime).toBe('httpd/unix-directory') expect(folder.mtime).toBeInstanceOf(Date) @@ -336,9 +395,98 @@ describe('SharingService share to Node mapping', () => { expect(folder.attributes.favorite).toBe(1) }) + describe('Remote file', () => { + test('Accepted', async () => { + axios.get.mockReturnValueOnce(Promise.resolve({ + data: { + ocs: { + data: [remoteFileAccepted], + }, + }, + })) + + const shares = await getContents(false, true, false, false) + + expect(axios.get).toHaveBeenCalledTimes(1) + expect(shares.contents).toHaveLength(1) + + const file = shares.contents[0] as File + expect(file).toBeInstanceOf(File) + expect(file.fileid).toBe(1234) + expect(file.source).toBe('http://nextcloud.local/remote.php/dav/files/test/shares/test.md') + expect(file.owner).toBe('owner-uid') + expect(file.mime).toBe('text/markdown') + expect(file.mtime?.getTime()).toBe(remoteFileAccepted.mtime * 1000) + // not available for remote shares + expect(file.size).toBe(undefined) + expect(file.permissions).toBe(19) + expect(file.root).toBe('/files/test') + expect(file.attributes).toBeInstanceOf(Object) + expect(file.attributes.favorite).toBe(0) + }) + + test('Pending', async () => { + axios.get.mockReturnValueOnce(Promise.resolve({ + data: { + ocs: { + data: [remoteFilePending], + }, + }, + })) + + const shares = await getContents(false, true, false, false) + + expect(axios.get).toHaveBeenCalledTimes(1) + expect(shares.contents).toHaveLength(1) + + const file = shares.contents[0] as File + expect(file).toBeInstanceOf(File) + expect(file.fileid).toBe(1234) + expect(file.source).toBe('http://nextcloud.local/remote.php/dav/files/test/shares/test.md') + expect(file.owner).toBe('owner-uid') + expect(file.mime).toBe('text/markdown') + expect(file.mtime?.getTime()).toBe(remoteFilePending.mtime * 1000) + // not available for remote shares + expect(file.size).toBe(undefined) + expect(file.permissions).toBe(0) + expect(file.root).toBe('/files/test') + expect(file.attributes).toBeInstanceOf(Object) + expect(file.attributes.favorite).toBe(0) + }) + }) + + test('External temp file', async () => { + axios.get.mockReturnValueOnce(Promise.resolve({ + data: { + ocs: { + data: [tempExternalFile], + }, + }, + })) + + const shares = await getContents(false, true, false, false) + + expect(axios.get).toHaveBeenCalledTimes(1) + expect(shares.contents).toHaveLength(1) + + const file = shares.contents[0] as File + expect(file).toBeInstanceOf(File) + expect(file.fileid).toBe(65) + expect(file.source).toBe('http://nextcloud.local/remote.php/dav/files/test/test.md') + expect(file.owner).toBe('owner-uid') + expect(file.mime).toBe('text/markdown') + expect(file.mtime?.getTime()).toBe(undefined) + // not available for remote shares + expect(file.size).toBe(undefined) + expect(file.permissions).toBe(0) + expect(file.root).toBe('/files/test') + expect(file.attributes).toBeInstanceOf(Object) + expect(file.attributes.favorite).toBe(0) + }) + test('Empty', async () => { - jest.spyOn(logger, 'error').mockImplementationOnce(() => {}) - jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({ + vi.spyOn(logger, 'error').mockImplementationOnce(() => {}) + axios.get.mockReturnValueOnce(Promise.resolve({ data: { ocs: { data: [], @@ -352,8 +500,8 @@ describe('SharingService share to Node mapping', () => { }) test('Error', async () => { - jest.spyOn(logger, 'error').mockImplementationOnce(() => {}) - jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({ + vi.spyOn(logger, 'error').mockImplementationOnce(() => {}) + axios.get.mockReturnValueOnce(Promise.resolve({ data: { ocs: { data: [null], diff --git a/apps/files_sharing/src/services/SharingService.ts b/apps/files_sharing/src/services/SharingService.ts index 119b008c64d..41c20f9aa73 100644 --- a/apps/files_sharing/src/services/SharingService.ts +++ b/apps/files_sharing/src/services/SharingService.ts @@ -2,19 +2,21 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -/* eslint-disable camelcase, n/no-extraneous-import */ +// TODO: Fix this instead of disabling ESLint!!! +/* eslint-disable @typescript-eslint/no-explicit-any */ + import type { AxiosPromise } from '@nextcloud/axios' +import type { ContentsWithRoot } from '@nextcloud/files' import type { OCSResponse } from '@nextcloud/typings/ocs' +import type { ShareAttribute } from '../sharing' -import { Folder, File, type ContentsWithRoot, Permission } from '@nextcloud/files' -import { generateOcsUrl, generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' +import { Folder, File, Permission, davRemoteURL, davRootPath } from '@nextcloud/files' +import { generateOcsUrl } from '@nextcloud/router' import axios from '@nextcloud/axios' import logger from './logger' -export const rootPath = `/files/${getCurrentUser()?.uid}` - const headers = { 'Content-Type': 'application/json', } @@ -23,14 +25,27 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu try { // Federated share handling if (ocsEntry?.remote_id !== undefined) { - const mime = (await import('mime')).default - // This won't catch files without an extension, but this is the best we can do - ocsEntry.mimetype = mime.getType(ocsEntry.name) - ocsEntry.item_type = ocsEntry.mimetype ? 'file' : 'folder' - - // Need to set permissions to NONE for federated shares - ocsEntry.item_permissions = Permission.NONE - ocsEntry.permissions = Permission.NONE + if (!ocsEntry.mimetype) { + const mime = (await import('mime')).default + // This won't catch files without an extension, but this is the best we can do + ocsEntry.mimetype = mime.getType(ocsEntry.name) + } + ocsEntry.item_type = ocsEntry.type || (ocsEntry.mimetype ? 'file' : 'folder') + + // different naming for remote shares + ocsEntry.item_mtime = ocsEntry.mtime + ocsEntry.file_target = ocsEntry.file_target || ocsEntry.mountpoint + + if (ocsEntry.file_target.includes('TemporaryMountPointName')) { + ocsEntry.file_target = ocsEntry.name + } + + // If the share is not accepted yet we don't know which permissions it will have + if (!ocsEntry.accepted) { + // Need to set permissions to NONE for federated shares + ocsEntry.item_permissions = Permission.NONE + ocsEntry.permissions = Permission.NONE + } ocsEntry.uid_owner = ocsEntry.owner // TODO: have the real display name stored somewhere @@ -43,18 +58,30 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu // If this is an external share that is not yet accepted, // we don't have an id. We can fallback to the row id temporarily - const fileid = ocsEntry.file_source || ocsEntry.id + // local shares (this server) use `file_source`, but remote shares (federated) use `file_id` + const fileid = ocsEntry.file_source || ocsEntry.file_id || ocsEntry.id // Generate path and strip double slashes - const path = ocsEntry?.path || ocsEntry.file_target || ocsEntry.name - const source = generateRemoteUrl(`dav/${rootPath}/${path}`.replaceAll(/\/\//gm, '/')) + const path = ocsEntry.path || ocsEntry.file_target || ocsEntry.name + const source = `${davRemoteURL}${davRootPath}/${path.replace(/^\/+/, '')}` + let mtime = ocsEntry.item_mtime ? new Date((ocsEntry.item_mtime) * 1000) : undefined // Prefer share time if more recent than item mtime - let mtime = ocsEntry?.item_mtime ? new Date((ocsEntry.item_mtime) * 1000) : undefined if (ocsEntry?.stime > (ocsEntry?.item_mtime || 0)) { mtime = new Date((ocsEntry.stime) * 1000) } + let sharees: { sharee: object } | undefined + if ('share_with' in ocsEntry) { + sharees = { + sharee: { + id: ocsEntry.share_with, + 'display-name': ocsEntry.share_with_displayname || ocsEntry.share_with, + type: ocsEntry.share_type, + }, + } + } + return new Node({ id: fileid, source, @@ -63,15 +90,18 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu mtime, size: ocsEntry?.item_size, permissions: ocsEntry?.item_permissions || ocsEntry?.permissions, - root: rootPath, + root: davRootPath, attributes: { ...ocsEntry, 'has-preview': hasPreview, + 'hide-download': ocsEntry?.hide_download === 1, // Also check the sharingStatusAction.ts code 'owner-id': ocsEntry?.uid_owner, 'owner-display-name': ocsEntry?.displayname_owner, 'share-types': ocsEntry?.share_type, - favorite: ocsEntry?.tags?.includes(window.OC.TAG_FAVORITE) ? 1 : 0, + 'share-attributes': ocsEntry?.attributes || '[]', + sharees, + favorite: ocsEntry?.tags?.includes((window.OC as { TAG_FAVORITE: string }).TAG_FAVORITE) ? 1 : 0, }, }) } catch (error) { @@ -80,12 +110,12 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu } } -const getShares = function(shared_with_me = false): AxiosPromise<OCSResponse<any>> { +const getShares = function(shareWithMe = false): AxiosPromise<OCSResponse<any>> { const url = generateOcsUrl('apps/files_sharing/api/v1/shares') return axios.get(url, { headers, params: { - shared_with_me, + shared_with_me: shareWithMe, include_tags: true, }, }) @@ -140,8 +170,28 @@ const getDeletedShares = function(): AxiosPromise<OCSResponse<any>> { } /** + * Check if a file request is enabled + * @param attributes the share attributes json-encoded array + */ +export const isFileRequest = (attributes = '[]'): boolean => { + const isFileRequest = (attribute) => { + return attribute.scope === 'fileRequest' && attribute.key === 'enabled' && attribute.value === true + } + + try { + const attributesArray = JSON.parse(attributes) as Array<ShareAttribute> + return attributesArray.some(isFileRequest) + } catch (error) { + logger.error('Error while parsing share attributes', { error }) + return false + } +} + +/** * Group an array of objects (here Nodes) by a key * and return an array of arrays of them. + * @param nodes Nodes to group + * @param key The attribute to group by */ const groupBy = function(nodes: (Folder | File)[], key: string) { return Object.values(nodes.reduce(function(acc, curr) { @@ -186,7 +236,7 @@ export const getContents = async (sharedWithYou = true, sharedWithOthers = true, return { folder: new Folder({ id: 0, - source: generateRemoteUrl('dav' + rootPath), + source: `${davRemoteURL}${davRootPath}`, owner: getCurrentUser()?.uid || null, }), contents, diff --git a/apps/files_sharing/src/services/TabSections.js b/apps/files_sharing/src/services/TabSections.js index 8578f8f08d5..ab1237e7044 100644 --- a/apps/files_sharing/src/services/TabSections.js +++ b/apps/files_sharing/src/services/TabSections.js @@ -3,6 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +/** + * Callback to render a section in the sharing tab. + * + * @callback registerSectionCallback + * @param {undefined} el - Deprecated and will always be undefined (formerly the root element) + * @param {object} fileInfo - File info object + */ + export default class TabSections { _sections diff --git a/apps/files_sharing/src/services/TokenService.ts b/apps/files_sharing/src/services/TokenService.ts new file mode 100644 index 00000000000..c497531dfdb --- /dev/null +++ b/apps/files_sharing/src/services/TokenService.ts @@ -0,0 +1,20 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +interface TokenData { + ocs: { + data: { + token: string, + } + } +} + +export const generateToken = async (): Promise<string> => { + const { data } = await axios.get<TokenData>(generateOcsUrl('/apps/files_sharing/api/v1/token')) + return data.ocs.data.token +} diff --git a/apps/files_sharing/src/services/WebdavClient.ts b/apps/files_sharing/src/services/WebdavClient.ts deleted file mode 100644 index cd33147b03f..00000000000 --- a/apps/files_sharing/src/services/WebdavClient.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files' -import type { FileStat, ResponseDataDetailed } from 'webdav' -import type { Node } from '@nextcloud/files' - -export const client = davGetClient() - -export const fetchNode = async (node: Node): Promise<Node> => { - const propfindPayload = davGetDefaultPropfind() - const result = await client.stat(`${davRootPath}${node.path}`, { - details: true, - data: propfindPayload, - }) as ResponseDataDetailed<FileStat> - return davResultToNode(result.data) -} |