summaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src')
-rw-r--r--apps/files_sharing/src/actions/sharingStatusAction.ts26
-rw-r--r--apps/files_sharing/src/components/SharingEntryLink.vue116
-rw-r--r--apps/files_sharing/src/mixins/ShareDetails.js3
-rw-r--r--apps/files_sharing/src/services/SharingService.spec.ts17
-rw-r--r--apps/files_sharing/src/services/SharingService.ts30
-rw-r--r--apps/files_sharing/src/views/SharingDetailsTab.vue45
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)
}