diff options
Diffstat (limited to 'apps/files_sharing/src')
-rw-r--r-- | apps/files_sharing/src/actions/sharingStatusAction.ts | 26 | ||||
-rw-r--r-- | apps/files_sharing/src/components/SharingEntryLink.vue | 116 | ||||
-rw-r--r-- | apps/files_sharing/src/mixins/ShareDetails.js | 3 | ||||
-rw-r--r-- | apps/files_sharing/src/services/SharingService.spec.ts | 17 | ||||
-rw-r--r-- | apps/files_sharing/src/services/SharingService.ts | 30 | ||||
-rw-r--r-- | apps/files_sharing/src/views/SharingDetailsTab.vue | 45 |
6 files changed, 166 insertions, 71 deletions
diff --git a/apps/files_sharing/src/actions/sharingStatusAction.ts b/apps/files_sharing/src/actions/sharingStatusAction.ts index 98a7d3d6112..4f9648fa27f 100644 --- a/apps/files_sharing/src/actions/sharingStatusAction.ts +++ b/apps/files_sharing/src/actions/sharingStatusAction.ts @@ -34,14 +34,22 @@ import { getCurrentUser } from '@nextcloud/auth' import './sharingStatusAction.scss' -const generateAvatarSvg = (userId: string) => { - const avatarUrl = generateUrl('/avatar/{userId}/32', { userId }) +const isDarkMode = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches === true + || document.querySelector('[data-themes*=dark]') !== null + +const generateAvatarSvg = (userId: string, isGuest = false) => { + const url = isDarkMode ? '/avatar/{userId}/32/dark' : '/avatar/{userId}/32' + const avatarUrl = generateUrl(isGuest ? url : url + '?guestFallback=true', { userId }) return `<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" class="sharing-status__avatar"> <image href="${avatarUrl}" height="32" width="32" /> </svg>` } +const isExternal = (node: Node) => { + return node.attributes.remote_id !== undefined +} + export const action = new FileAction({ id: 'sharing-status', displayName(nodes: Node[]) { @@ -50,7 +58,7 @@ export const action = new FileAction({ const ownerId = node?.attributes?.['owner-id'] if (shareTypes.length > 0 - || (ownerId && ownerId !== getCurrentUser()?.uid)) { + || (ownerId !== getCurrentUser()?.uid || isExternal(node))) { return t('files_sharing', 'Shared') } @@ -63,11 +71,11 @@ export const action = new FileAction({ const ownerDisplayName = node?.attributes?.['owner-display-name'] // Mixed share types - if (Array.isArray(node.attributes?.['share-types'])) { + if (Array.isArray(node.attributes?.['share-types']) && node.attributes?.['share-types'].length > 1) { return t('files_sharing', 'Shared multiple times with different people') } - if (ownerId && ownerId !== getCurrentUser()?.uid) { + if (ownerId && (ownerId !== getCurrentUser()?.uid || isExternal(node))) { return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName }) } @@ -79,7 +87,7 @@ export const action = new FileAction({ const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[] // Mixed share types - if (Array.isArray(node.attributes?.['share-types'])) { + if (Array.isArray(node.attributes?.['share-types']) && node.attributes?.['share-types'].length > 1) { return AccountPlusSvg } @@ -101,8 +109,8 @@ export const action = new FileAction({ } const ownerId = node?.attributes?.['owner-id'] - if (ownerId && ownerId !== getCurrentUser()?.uid) { - return generateAvatarSvg(ownerId) + if (ownerId && (ownerId !== getCurrentUser()?.uid || isExternal(node))) { + return generateAvatarSvg(ownerId, isExternal(node)) } return AccountPlusSvg @@ -124,7 +132,7 @@ export const action = new FileAction({ } // If the node is shared by someone else - if (ownerId && ownerId !== getCurrentUser()?.uid) { + if (ownerId && (ownerId !== getCurrentUser()?.uid || isExternal(node))) { return true } diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue index a1fba7663d0..680901244a2 100644 --- a/apps/files_sharing/src/components/SharingEntryLink.vue +++ b/apps/files_sharing/src/components/SharingEntryLink.vue @@ -205,6 +205,7 @@ import GeneratePassword from '../utils/GeneratePassword.js' import Share from '../models/Share.js' import SharesMixin from '../mixins/SharesMixin.js' import ShareDetails from '../mixins/ShareDetails.js' +import { getLoggerBuilder } from '@nextcloud/logger' export default { name: 'SharingEntryLink', @@ -237,6 +238,7 @@ export default { data() { return { + shareCreationComplete: false, copySuccess: true, copied: false, @@ -245,6 +247,10 @@ export default { ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state, ExternalShareActions: OCA.Sharing.ExternalShareActions.state, + logger: getLoggerBuilder() + .setApp('files_sharing') + .detectUser() + .build(), } }, @@ -405,6 +411,32 @@ export default { return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id }, + sharePolicyHasRequiredProperties() { + return this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced + }, + + requiredPropertiesMissing() { + // Ensure share exist and the share policy has required properties + if (!this.sharePolicyHasRequiredProperties) { + return false + } + + if (!this.share) { + // if no share, we can't tell if properties are missing or not so we assume properties are missing + return true + } + + // If share has ID, then this is an incoming link share created from the existing link share + // Hence assume required properties + if (this.share.id) { + return true + } + // Check if either password or expiration date is missing and enforced + const isPasswordMissing = this.config.enforcePasswordForPublicLink && !this.share.password + const isExpireDateMissing = this.config.isDefaultExpireDateEnforced && !this.share.expireDate + + return isPasswordMissing || isExpireDateMissing + }, // if newPassword exists, but is empty, it means // the user deleted the original password hasUnsavedPassword() { @@ -481,6 +513,7 @@ export default { * Create a new share link and append it to the list */ async onNewLinkShare() { + this.logger.debug('onNewLinkShare called (with this.share)', this.share) // do not run again if already loading if (this.loading) { return @@ -495,28 +528,13 @@ export default { shareDefaults.expiration = this.formatDateToString(this.config.defaultExpirationDate) } + this.logger.debug('Missing required properties?', this.requiredPropertiesMissing) // do not push yet if we need a password or an expiration date: show pending menu - if (this.config.enableLinkPasswordByDefault || this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced) { + if (this.sharePolicyHasRequiredProperties && this.requiredPropertiesMissing) { this.pending = true + this.shareCreationComplete = false - // if a share already exists, pushing it - if (this.share && !this.share.id) { - // if the share is valid, create it on the server - if (this.checkShare(this.share)) { - try { - await this.pushNewLinkShare(this.share, true) - } catch (e) { - this.pending = false - console.error(e) - return false - } - return true - } else { - this.open = true - OC.Notification.showTemporary(t('files_sharing', 'Error, please enter proper password and/or expiration date')) - return false - } - } + this.logger.info('Share policy requires mandated properties (password)...') // ELSE, show the pending popovermenu // if password default or enforced, pre-fill with random one @@ -538,8 +556,32 @@ export default { // Nothing is enforced, creating share directly } else { + + // if a share already exists, pushing it + if (this.share && !this.share.id) { + // if the share is valid, create it on the server + if (this.checkShare(this.share)) { + try { + this.logger.info('Sending existing share to server', this.share) + await this.pushNewLinkShare(this.share, true) + this.shareCreationComplete = true + this.logger.info('Share created on server', this.share) + } catch (e) { + this.pending = false + this.logger.error('Error creating share', e) + return false + } + return true + } else { + this.open = true + showError(t('files_sharing', 'Error, please enter proper password and/or expiration date')) + return false + } + } + const share = new Share(shareDefaults) await this.pushNewLinkShare(share) + this.shareCreationComplete = true } }, @@ -579,8 +621,8 @@ export default { const newShare = await this.createShare(options) this.open = false + this.shareCreationComplete = true console.debug('Link share created', newShare) - // if share already exists, copy link directly on next tick let component if (update) { @@ -622,8 +664,10 @@ export default { this.onSyncError('pending', message) } throw data + } finally { this.loading = false + this.shareCreationComplete = true } }, async copyLink() { @@ -726,7 +770,9 @@ export default { // this.share already exists at this point, // but is incomplete as not pushed to server // YET. We can safely delete the share :) - this.$emit('remove:share', this.share) + if (!this.shareCreationComplete) { + this.$emit('remove:share', this.share) + } }, }, } @@ -747,25 +793,21 @@ export default { min-width: 0; } - &__desc { - display: flex; - flex-direction: column; - line-height: 1.2em; + &__desc { + display: flex; + flex-direction: column; + line-height: 1.2em; - p { - color: var(--color-text-maxcontrast); - } + p { + color: var(--color-text-maxcontrast); + } - &__title { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + &__title { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } } - } - - &__copy { - - } &:not(.sharing-entry--share) &__actions { .new-share-link { diff --git a/apps/files_sharing/src/mixins/ShareDetails.js b/apps/files_sharing/src/mixins/ShareDetails.js index 3884d22dae6..60645ef89c8 100644 --- a/apps/files_sharing/src/mixins/ShareDetails.js +++ b/apps/files_sharing/src/mixins/ShareDetails.js @@ -1,4 +1,5 @@ import Share from '../models/Share.js' +import Config from '../services/ConfigService.js' export default { methods: { @@ -50,7 +51,7 @@ export default { user: shareRequestObject.shareWith, share_with_displayname: shareRequestObject.displayName, subtitle: shareRequestObject.subtitle, - permissions: shareRequestObject.permissions, + permissions: shareRequestObject.permissions ?? new Config().defaultPermissions, expiration: '', } diff --git a/apps/files_sharing/src/services/SharingService.spec.ts b/apps/files_sharing/src/services/SharingService.spec.ts index 54e2355b082..ed3a0cb6590 100644 --- a/apps/files_sharing/src/services/SharingService.spec.ts +++ b/apps/files_sharing/src/services/SharingService.spec.ts @@ -346,12 +346,27 @@ describe('SharingService share to Node mapping', () => { expect(folder.attributes.favorite).toBe(1) }) + test('Empty', async () => { + jest.spyOn(logger, 'error').mockImplementationOnce(() => {}) + jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({ + data: { + ocs: { + data: [], + }, + }, + })) + + const shares = await getContents(false, true, false, false) + expect(shares.contents).toHaveLength(0) + expect(logger.error).toHaveBeenCalledTimes(0) + }) + test('Error', async () => { jest.spyOn(logger, 'error').mockImplementationOnce(() => {}) jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({ data: { ocs: { - data: [{}], + data: [null], }, }, })) diff --git a/apps/files_sharing/src/services/SharingService.ts b/apps/files_sharing/src/services/SharingService.ts index 2f167dab535..a4c63bec7b0 100644 --- a/apps/files_sharing/src/services/SharingService.ts +++ b/apps/files_sharing/src/services/SharingService.ts @@ -22,7 +22,7 @@ /* eslint-disable camelcase, n/no-extraneous-import */ import type { AxiosPromise } from 'axios' -import { Folder, File, type ContentsWithRoot } from '@nextcloud/files' +import { Folder, File, type ContentsWithRoot, Permission } from '@nextcloud/files' import { generateOcsUrl, generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' import axios from '@nextcloud/axios' @@ -46,16 +46,34 @@ const headers = { 'Content-Type': 'application/json', } -const ocsEntryToNode = function(ocsEntry: any): Folder | File | null { +const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | null> { 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 + + ocsEntry.uid_owner = ocsEntry.owner + // TODO: have the real display name stored somewhere + ocsEntry.displayname_owner = ocsEntry.owner + } + const isFolder = ocsEntry?.item_type === 'folder' const hasPreview = ocsEntry?.has_preview === true const Node = isFolder ? Folder : File - const fileid = ocsEntry.file_source + // 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 // Generate path and strip double slashes - const path = ocsEntry?.path || ocsEntry.file_target + const path = ocsEntry?.path || ocsEntry.file_target || ocsEntry.name const source = generateRemoteUrl(`dav/${rootPath}/${path}`.replaceAll(/\/\//gm, '/')) // Prefer share time if more recent than item mtime @@ -68,7 +86,7 @@ const ocsEntryToNode = function(ocsEntry: any): Folder | File | null { id: fileid, source, owner: ocsEntry?.uid_owner, - mime: ocsEntry?.mimetype, + mime: ocsEntry?.mimetype || 'application/octet-stream', mtime, size: ocsEntry?.item_size, permissions: ocsEntry?.item_permissions || ocsEntry?.permissions, @@ -177,7 +195,7 @@ export const getContents = async (sharedWithYou = true, sharedWithOthers = true, const responses = await Promise.all(promises) const data = responses.map((response) => response.data.ocs.data).flat() - let contents = data.map(ocsEntryToNode) + let contents = (await Promise.all(data.map(ocsEntryToNode))) .filter((node) => node !== null) as (Folder | File)[] if (filterTypes.length > 0) { diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue index f3ad52eeb87..6f6a6d60002 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -305,24 +305,34 @@ export default { computed: { title() { - let title = t('files_sharing', 'Share with ') - if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_USER) { - title = title + this.share.shareWithDisplayName - } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_LINK) { - title = t('files_sharing', 'Share link') - } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP) { - title += ` (${t('files_sharing', 'group')})` - } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_ROOM) { - title += ` (${t('files_sharing', 'conversation')})` - } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE) { - title += ` (${t('files_sharing', 'remote')})` - } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP) { - title += ` (${t('files_sharing', 'remote group')})` - } else if (this.share.type === this.SHARE_TYPES.SHARE_TYPE_GUEST) { - title += ` (${t('files_sharing', 'guest')})` + switch (this.share.type) { + case this.SHARE_TYPES.SHARE_TYPE_USER: + return t('files_sharing', 'Share with {userName}', { userName: this.share.shareWithDisplayName }) + case this.SHARE_TYPES.SHARE_TYPE_EMAIL: + return t('files_sharing', 'Share with email {email}', { email: this.share.shareWith }) + case this.SHARE_TYPES.SHARE_TYPE_LINK: + return t('files_sharing', 'Share link') + case this.SHARE_TYPES.SHARE_TYPE_GROUP: + return t('files_sharing', 'Share with group') + case this.SHARE_TYPES.SHARE_TYPE_ROOM: + return t('files_sharing', 'Share in conversation') + case this.SHARE_TYPES.SHARE_TYPE_REMOTE: { + const [user, server] = this.share.shareWith.split('@') + return t('files_sharing', 'Share with {user} on remote server {server}', { user, server }) + } + case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP: + return t('files_sharing', 'Share with remote group') + case this.SHARE_TYPES.SHARE_TYPE_GUEST: + return t('files_sharing', 'Share with guest') + default: { + if (this.share.id) { + // Share already exists + return t('files_sharing', 'Update share') + } else { + return t('files_sharing', 'Create share') + } + } } - - return title }, /** * Can the sharee edit the shared file ? @@ -834,6 +844,7 @@ export default { this.share = share this.$emit('add:share', this.share) } else { + this.$emit('update:share', this.share) this.queueUpdate(...permissionsAndAttributes) } |