aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src/services')
-rw-r--r--apps/files_sharing/src/services/ConfigService.js322
-rw-r--r--apps/files_sharing/src/services/ConfigService.ts333
-rw-r--r--apps/files_sharing/src/services/ExternalShareActions.js2
-rw-r--r--apps/files_sharing/src/services/GuestNameValidity.ts45
-rw-r--r--apps/files_sharing/src/services/SharingService.spec.ts234
-rw-r--r--apps/files_sharing/src/services/SharingService.ts96
-rw-r--r--apps/files_sharing/src/services/TabSections.js8
-rw-r--r--apps/files_sharing/src/services/TokenService.ts20
8 files changed, 671 insertions, 389 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 ad76879257f..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 */
-import type { AxiosPromise } from 'axios'
+// 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
+}