diff --git a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue index f9173d84c1850..07a422d47dc6e 100644 --- a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue +++ b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue @@ -95,17 +95,17 @@ export default { }, preSelectedOption() { - const permissions = this.share.permissions - const basePermissions = this.bundledPermissions - if (permissions === basePermissions.READ_ONLY) { - return this.canViewText - } else if (permissions === basePermissions.ALL || permissions === basePermissions.ALL_FILE) { - return this.canEditText - } else if (permissions === basePermissions.FILE_DROP) { - return this.fileDropText + switch (this.permissionsBundle) { + case 'READ_ONLY': + return this.canViewText + case 'ALL': + case 'ALL_FILE': + return this.canEditText + case 'FILE_DROP': + return this.fileDropText + default: + return this.customPermissionsText } - - return this.customPermissionsText }, options() { diff --git a/apps/files_sharing/src/lib/SharePermissionsToolBox.js b/apps/files_sharing/src/lib/SharePermissionsToolBox.js index 3638d94f5f607..6462874637fd7 100644 --- a/apps/files_sharing/src/lib/SharePermissionsToolBox.js +++ b/apps/files_sharing/src/lib/SharePermissionsToolBox.js @@ -122,3 +122,33 @@ export function togglePermissions(initialPermissionSet, permissionsToToggle) { export function canTogglePermissions(permissionSet, permissionsToToggle) { return permissionsSetIsValid(togglePermissions(permissionSet, permissionsToToggle)) } + +/** + * The permission bundles the share editor offers, in the order they are matched. + * + * @type {string[]} + */ +const EDITOR_BUNDLES = ['READ_ONLY', 'ALL', 'ALL_FILE', 'FILE_DROP'] + +/** + * Find the permission bundle a share's permissions correspond to. + * + * Link and email shares carry the SHARE permission whenever federation on + * public shares is enabled: the server adds it on top of whatever bundle was + * picked, so it must be ignored when matching those shares against a bundle. + * + * @param {number} permissions - the share permissions. + * @param {object} [options] - matching options. + * @param {boolean} [options.isPublicShare] - whether the share is a link or email share. + * @param {boolean} [options.excludeReshareFromEdit] - whether SHARE is excluded from the editing bundles. + * + * @return {string|null} the name of the matching bundle, or `null` for custom permissions. + */ +export function matchBundledPermissions(permissions, { isPublicShare = false, excludeReshareFromEdit = false } = {}) { + const bundles = getBundledPermissions(isPublicShare || excludeReshareFromEdit) + const comparablePermissions = isPublicShare + ? subtractPermissions(permissions, ATOMIC_PERMISSIONS.SHARE) + : permissions + + return EDITOR_BUNDLES.find((bundle) => bundles[bundle] === comparablePermissions) ?? null +} diff --git a/apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js b/apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js index 14ac7bfbbbb74..f8f04d29edd41 100644 --- a/apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js +++ b/apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js @@ -9,6 +9,7 @@ import { canTogglePermissions, getBundledPermissions, hasPermissions, + matchBundledPermissions, permissionsSetIsValid, subtractPermissions, togglePermissions, @@ -144,4 +145,51 @@ describe('SharePermissionsToolBox', () => { // BUNDLED_PERMISSIONS.ALL_FILE already includes SHARE expect(BUNDLED_PERMISSIONS.ALL_FILE).toBe(permissionsWithShare.ALL_FILE) }) + + describe('Matching bundled permissions', () => { + const { READ, UPDATE, CREATE, DELETE, SHARE } = ATOMIC_PERMISSIONS + + test('matches the bundles of an internal share', () => { + expect(matchBundledPermissions(READ)).toBe('READ_ONLY') + expect(matchBundledPermissions(CREATE)).toBe('FILE_DROP') + expect(matchBundledPermissions(READ | UPDATE | CREATE | DELETE | SHARE)).toBe('ALL') + expect(matchBundledPermissions(READ | UPDATE | SHARE)).toBe('ALL_FILE') + }) + + test('reports permissions outside of a bundle as custom', () => { + expect(matchBundledPermissions(READ | UPDATE)).toBe(null) + expect(matchBundledPermissions(READ | CREATE)).toBe(null) + expect(matchBundledPermissions(ATOMIC_PERMISSIONS.NONE)).toBe(null) + }) + + test('matches the editing bundle without SHARE when resharing is excluded from editing', () => { + const options = { excludeReshareFromEdit: true } + expect(matchBundledPermissions(READ | UPDATE | CREATE | DELETE, options)).toBe('ALL') + expect(matchBundledPermissions(READ | UPDATE, options)).toBe('ALL_FILE') + // With resharing excluded, a share that grants it is no longer the editing bundle + expect(matchBundledPermissions(READ | UPDATE | CREATE | DELETE | SHARE, options)).toBe(null) + }) + + test('ignores the SHARE permission the server adds to public shares', () => { + const options = { isPublicShare: true } + // Link and email shares carry SHARE for federation, whatever bundle was picked + expect(matchBundledPermissions(READ | SHARE, options)).toBe('READ_ONLY') + expect(matchBundledPermissions(CREATE | SHARE, options)).toBe('FILE_DROP') + expect(matchBundledPermissions(READ | UPDATE | CREATE | DELETE | SHARE, options)).toBe('ALL') + expect(matchBundledPermissions(READ | UPDATE | SHARE, options)).toBe('ALL_FILE') + }) + + test('matches public shares the same way with resharing excluded from editing', () => { + const options = { isPublicShare: true, excludeReshareFromEdit: true } + expect(matchBundledPermissions(READ | SHARE, options)).toBe('READ_ONLY') + expect(matchBundledPermissions(READ | UPDATE | CREATE | DELETE | SHARE, options)).toBe('ALL') + expect(matchBundledPermissions(READ | UPDATE | CREATE | DELETE, options)).toBe('ALL') + }) + + test('still reports custom permissions on a public share', () => { + const options = { isPublicShare: true } + expect(matchBundledPermissions(READ | CREATE | SHARE, options)).toBe(null) + expect(matchBundledPermissions(READ | UPDATE | DELETE | SHARE, options)).toBe(null) + }) + }) }) diff --git a/apps/files_sharing/src/mixins/SharesMixin.js b/apps/files_sharing/src/mixins/SharesMixin.js index 9a84bee0206c4..bdf07d79160fe 100644 --- a/apps/files_sharing/src/mixins/SharesMixin.js +++ b/apps/files_sharing/src/mixins/SharesMixin.js @@ -10,7 +10,7 @@ import { ShareType } from '@nextcloud/sharing' import debounce from 'debounce' import PQueue from 'p-queue' import { fetchNode } from '../../../files/src/services/WebdavClient.ts' -import { getBundledPermissions } from '../lib/SharePermissionsToolBox.js' +import { matchBundledPermissions } from '../lib/SharePermissionsToolBox.js' import Share from '../models/Share.ts' import Config from '../services/ConfigService.ts' import logger from '../services/logger.ts' @@ -134,15 +134,14 @@ export default { } return this.config.isDefaultInternalExpireDateEnforced }, + permissionsBundle() { + return matchBundledPermissions(this.share.permissions, { + isPublicShare: this.isPublicShare, + excludeReshareFromEdit: this.config.excludeReshareFromEdit, + }) + }, hasCustomPermissions() { - const basePermissions = getBundledPermissions(this.config.excludeReshareFromEdit) - const bundledPermissions = [ - basePermissions.ALL, - basePermissions.ALL_FILE, - basePermissions.READ_ONLY, - basePermissions.FILE_DROP, - ] - return !bundledPermissions.includes(this.share.permissions) + return this.permissionsBundle === null }, maxExpirationDateEnforced() { if (this.isExpiryDateEnforced) {