aboutsummaryrefslogtreecommitdiffstats
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/additionalScripts.js3
-rw-r--r--apps/files_sharing/src/collaborationresourceshandler.js3
-rw-r--r--apps/files_sharing/src/components/FileListFilterAccount.vue83
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog.vue4
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue30
-rw-r--r--apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue2
-rw-r--r--apps/files_sharing/src/components/ShareExpiryTime.vue2
-rw-r--r--apps/files_sharing/src/components/SharingEntry.vue4
-rw-r--r--apps/files_sharing/src/components/SharingEntryLink.vue79
-rw-r--r--apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue2
-rw-r--r--apps/files_sharing/src/components/SharingInput.vue35
-rw-r--r--apps/files_sharing/src/files_actions/openInFilesAction.spec.ts2
-rw-r--r--apps/files_sharing/src/files_actions/openInFilesAction.ts4
-rw-r--r--apps/files_sharing/src/files_actions/sharingStatusAction.ts4
-rw-r--r--apps/files_sharing/src/files_filters/AccountFilter.ts77
-rw-r--r--apps/files_sharing/src/files_newMenu/newFileRequest.ts2
-rw-r--r--apps/files_sharing/src/files_views/publicFileDrop.ts7
-rw-r--r--apps/files_sharing/src/files_views/shares.ts6
-rw-r--r--apps/files_sharing/src/init.ts4
-rw-r--r--apps/files_sharing/src/mixins/ShareDetails.js14
-rw-r--r--apps/files_sharing/src/mixins/ShareRequests.js6
-rw-r--r--apps/files_sharing/src/mixins/SharesMixin.js59
-rw-r--r--apps/files_sharing/src/models/Share.ts16
-rw-r--r--apps/files_sharing/src/public-file-request.ts57
-rw-r--r--apps/files_sharing/src/public-nickname-handler.ts86
-rw-r--r--apps/files_sharing/src/services/ConfigService.ts17
-rw-r--r--apps/files_sharing/src/services/GuestNameValidity.ts45
-rw-r--r--apps/files_sharing/src/services/SharingService.ts1
-rw-r--r--apps/files_sharing/src/utils/GeneratePassword.ts21
-rw-r--r--apps/files_sharing/src/views/CollaborationView.vue36
-rw-r--r--apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue6
-rw-r--r--apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue95
-rw-r--r--apps/files_sharing/src/views/PublicAuthPrompt.vue123
-rw-r--r--apps/files_sharing/src/views/SharingDetailsTab.vue106
-rw-r--r--apps/files_sharing/src/views/SharingLinkList.vue12
-rw-r--r--apps/files_sharing/src/views/SharingTab.vue95
36 files changed, 674 insertions, 474 deletions
diff --git a/apps/files_sharing/src/additionalScripts.js b/apps/files_sharing/src/additionalScripts.js
index 18812eec537..e8807a7325e 100644
--- a/apps/files_sharing/src/additionalScripts.js
+++ b/apps/files_sharing/src/additionalScripts.js
@@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import { getCSPNonce } from '@nextcloud/auth'
import './share.js'
import './sharebreadcrumbview.js'
@@ -9,6 +10,6 @@ import './style/sharebreadcrumb.scss'
import './collaborationresourceshandler.js'
// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(OC.requestToken)
+__webpack_nonce__ = getCSPNonce()
window.OCA.Sharing = OCA.Sharing
diff --git a/apps/files_sharing/src/collaborationresourceshandler.js b/apps/files_sharing/src/collaborationresourceshandler.js
index adb9cb02fbc..6f3645385b7 100644
--- a/apps/files_sharing/src/collaborationresourceshandler.js
+++ b/apps/files_sharing/src/collaborationresourceshandler.js
@@ -2,9 +2,10 @@
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import { getCSPNonce } from '@nextcloud/auth'
// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(OC.requestToken)
+__webpack_nonce__ = getCSPNonce()
window.OCP.Collaboration.registerType('file', {
action: () => {
diff --git a/apps/files_sharing/src/components/FileListFilterAccount.vue b/apps/files_sharing/src/components/FileListFilterAccount.vue
index f4e4eefb6e2..150516e139b 100644
--- a/apps/files_sharing/src/components/FileListFilterAccount.vue
+++ b/apps/files_sharing/src/components/FileListFilterAccount.vue
@@ -8,7 +8,7 @@
:filter-name="t('files_sharing', 'People')"
@reset-filter="resetFilter">
<template #icon>
- <NcIconSvgWrapper :path="mdiAccountMultiple" />
+ <NcIconSvgWrapper :path="mdiAccountMultipleOutline" />
</template>
<NcActionInput v-if="availableAccounts.length > 1"
:label="t('files_sharing', 'Filter accounts')"
@@ -36,14 +36,11 @@
</template>
<script setup lang="ts">
-import type { IAccountData } from '../filters/AccountFilter.ts'
+import type { IAccountData } from '../files_filters/AccountFilter.ts'
import { translate as t } from '@nextcloud/l10n'
-import { ShareType } from '@nextcloud/sharing'
-import { mdiAccountMultiple } from '@mdi/js'
-import { useBrowserLocation } from '@vueuse/core'
+import { mdiAccountMultipleOutline } from '@mdi/js'
import { computed, ref, watch } from 'vue'
-import { useNavigation } from '../../../files/src/composables/useNavigation.ts'
import FileListFilter from '../../../files/src/components/FileListFilter/FileListFilter.vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
@@ -61,8 +58,6 @@ const emit = defineEmits<{
(event: 'update:accounts', value: IAccountData[]): void
}>()
-const { currentView } = useNavigation()
-const currentLocation = useBrowserLocation()
const accountFilter = ref('')
const availableAccounts = ref<IUserSelectData[]>([])
const selectedAccounts = ref<IUserSelectData[]>([])
@@ -106,71 +101,27 @@ watch(selectedAccounts, () => {
})
/**
- * Update the accounts owning nodes or have nodes shared to them
- * @param path The path inside the current view to load for accounts
- */
-async function updateAvailableAccounts(path: string = '/') {
- availableAccounts.value = []
- if (!currentView.value) {
- return
- }
-
- const { contents } = await currentView.value.getContents(path)
- const available = new Map<string, IUserSelectData>()
- for (const node of contents) {
- const owner = node.owner
- if (owner && !available.has(owner)) {
- available.set(owner, {
- id: owner,
- user: owner,
- displayName: node.attributes['owner-display-name'] ?? node.owner,
- })
- }
-
- const sharees = node.attributes.sharees?.sharee
- if (sharees) {
- // ensure sharees is an array (if only one share then it is just an object)
- for (const sharee of [sharees].flat()) {
- // Skip link shares and other without user
- if (sharee.id === '') {
- continue
- }
- if (sharee.type !== ShareType.User && sharee.type !== ShareType.Remote) {
- continue
- }
- // Add if not already added
- if (!available.has(sharee.id)) {
- available.set(sharee.id, {
- id: sharee.id,
- user: sharee.id,
- displayName: sharee['display-name'],
- })
- }
- }
- }
- }
- availableAccounts.value = [...available.values()]
-}
-
-/**
* Reset this filter
*/
function resetFilter() {
selectedAccounts.value = []
accountFilter.value = ''
}
-defineExpose({ resetFilter, toggleAccount })
-// When the current view changes or the current directory,
-// then we need to rebuild the available accounts
-watch([currentView, currentLocation], () => {
- if (currentView.value) {
- // we have no access to the files router here...
- const path = (currentLocation.value.search ?? '?dir=/').match(/(?<=&|\?)dir=([^&#]+)/)?.[1]
- resetFilter()
- updateAvailableAccounts(decodeURIComponent(path ?? '/'))
- }
-}, { immediate: true })
+/**
+ * Update list of available accounts in current view.
+ *
+ * @param accounts - Accounts to use
+ */
+function setAvailableAccounts(accounts: IAccountData[]): void {
+ availableAccounts.value = accounts.map(({ uid, displayName }) => ({ displayName, id: uid, user: uid }))
+}
+
+defineExpose({
+ resetFilter,
+ setAvailableAccounts,
+ toggleAccount,
+})
</script>
<style scoped lang="scss">
diff --git a/apps/files_sharing/src/components/NewFileRequestDialog.vue b/apps/files_sharing/src/components/NewFileRequestDialog.vue
index 102d1a0fed9..392f286e104 100644
--- a/apps/files_sharing/src/components/NewFileRequestDialog.vue
+++ b/apps/files_sharing/src/components/NewFileRequestDialog.vue
@@ -296,8 +296,8 @@ export default defineComponent({
path: this.destination,
note: this.note,
- password: this.password || undefined,
- expireDate: expireDate || undefined,
+ password: this.password || '',
+ expireDate: expireDate || '',
// Empty string
shareWith: '',
diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue
index 4c14b21e1d5..7e6d56e8794 100644
--- a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue
+++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogDatePassword.vue
@@ -14,9 +14,9 @@
<fieldset class="file-request-dialog__expiration" data-cy-file-request-dialog-fieldset="expiration">
<!-- Enable expiration -->
<legend>{{ t('files_sharing', 'When should the request expire?') }}</legend>
- <NcCheckboxRadioSwitch v-show="!defaultExpireDateEnforced"
- :checked="defaultExpireDateEnforced || expirationDate !== null"
- :disabled="disabled || defaultExpireDateEnforced"
+ <NcCheckboxRadioSwitch v-show="!isExpirationDateEnforced"
+ :checked="isExpirationDateEnforced || expirationDate !== null"
+ :disabled="disabled || isExpirationDateEnforced"
@update:checked="onToggleDeadline">
{{ t('files_sharing', 'Set a submission expiration date') }}
</NcCheckboxRadioSwitch>
@@ -46,9 +46,9 @@
<fieldset class="file-request-dialog__password" data-cy-file-request-dialog-fieldset="password">
<!-- Enable password -->
<legend>{{ t('files_sharing', 'What password should be used for the request?') }}</legend>
- <NcCheckboxRadioSwitch v-show="!enforcePasswordForPublicLink"
- :checked="enforcePasswordForPublicLink || password !== null"
- :disabled="disabled || enforcePasswordForPublicLink"
+ <NcCheckboxRadioSwitch v-show="!isPasswordEnforced"
+ :checked="isPasswordEnforced || password !== null"
+ :disabled="disabled || isPasswordEnforced"
@update:checked="onTogglePassword">
{{ t('files_sharing', 'Set a password') }}
</NcCheckboxRadioSwitch>
@@ -59,7 +59,7 @@
:disabled="disabled"
:label="t('files_sharing', 'Password')"
:placeholder="t('files_sharing', 'Enter a valid password')"
- :required="false"
+ :required="enforcePasswordForPublicLink"
:value="password"
name="password"
@update:value="$emit('update:password', $event)" />
@@ -180,6 +180,18 @@ export default defineComponent({
return ''
},
+
+ isExpirationDateEnforced(): boolean {
+ // Both fields needs to be enabled in the settings
+ return this.defaultExpireDateEnabled
+ && this.defaultExpireDateEnforced
+ },
+
+ isPasswordEnforced(): boolean {
+ // Both fields needs to be enabled in the settings
+ return this.enableLinkPasswordByDefault
+ && this.enforcePasswordForPublicLink
+ },
},
mounted() {
@@ -189,12 +201,12 @@ export default defineComponent({
}
// If enforced, we cannot set a date before the default expiration days (see admin settings)
- if (this.defaultExpireDateEnforced) {
+ if (this.isExpirationDateEnforced) {
this.maxDate = sharingConfig.defaultExpirationDate
}
// If enabled by default, we generate a valid password
- if (this.enableLinkPasswordByDefault) {
+ if (this.isPasswordEnforced) {
this.generatePassword()
}
},
diff --git a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue
index 2d4d8eafa2b..5ac60c37e29 100644
--- a/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue
+++ b/apps/files_sharing/src/components/NewFileRequestDialog/NewFileRequestDialogIntro.vue
@@ -78,7 +78,7 @@ import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import IconFolder from 'vue-material-design-icons/Folder.vue'
-import IconInfo from 'vue-material-design-icons/Information.vue'
+import IconInfo from 'vue-material-design-icons/InformationOutline.vue'
import IconLock from 'vue-material-design-icons/Lock.vue'
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
import NcTextField from '@nextcloud/vue/components/NcTextField'
diff --git a/apps/files_sharing/src/components/ShareExpiryTime.vue b/apps/files_sharing/src/components/ShareExpiryTime.vue
index b789bc92db5..939142616e9 100644
--- a/apps/files_sharing/src/components/ShareExpiryTime.vue
+++ b/apps/files_sharing/src/components/ShareExpiryTime.vue
@@ -9,7 +9,7 @@
<NcButton v-if="expiryTime"
class="hint-icon"
type="tertiary"
- :aria-label="t('files_sharing', 'Share expiration: ') + new Date(expiryTime).toLocaleString()">
+ :aria-label="t('files_sharing', 'Share expiration: {date}', { date: new Date(expiryTime).toLocaleString() })">
<template #icon>
<ClockIcon :size="20" />
</template>
diff --git a/apps/files_sharing/src/components/SharingEntry.vue b/apps/files_sharing/src/components/SharingEntry.vue
index 4ff5fae364b..1fbe740cb11 100644
--- a/apps/files_sharing/src/components/SharingEntry.vue
+++ b/apps/files_sharing/src/components/SharingEntry.vue
@@ -77,9 +77,9 @@ export default {
title += ` (${t('files_sharing', 'group')})`
} else if (this.share.type === ShareType.Room) {
title += ` (${t('files_sharing', 'conversation')})`
- } else if (this.share.type === ShareType.Remote) {
+ } else if (this.share.type === ShareType.Remote && !this.share.isTrustedServer) {
title += ` (${t('files_sharing', 'remote')})`
- } else if (this.share.type === ShareType.RemoteGroup) {
+ } else if (this.share.type === ShareType.RemoteGroup && !this.share.isTrustedServer) {
title += ` (${t('files_sharing', 'remote group')})`
} else if (this.share.type === ShareType.Guest) {
title += ` (${t('files_sharing', 'guest')})`
diff --git a/apps/files_sharing/src/components/SharingEntryLink.vue b/apps/files_sharing/src/components/SharingEntryLink.vue
index 9427bd78967..6a456fa0a15 100644
--- a/apps/files_sharing/src/components/SharingEntryLink.vue
+++ b/apps/files_sharing/src/components/SharingEntryLink.vue
@@ -74,10 +74,10 @@
{{ config.enforcePasswordForPublicLink ? t('files_sharing', 'Password protection (enforced)') : t('files_sharing', 'Password protection') }}
</NcActionCheckbox>
- <NcActionInput v-if="pendingEnforcedPassword || share.password"
+ <NcActionInput v-if="pendingEnforcedPassword || isPasswordProtected"
class="share-link-password"
:label="t('files_sharing', 'Enter a password')"
- :value.sync="share.password"
+ :value.sync="share.newPassword"
:disabled="saving"
:required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink"
:minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength"
@@ -108,13 +108,15 @@
type="date"
:min="dateTomorrow"
:max="maxExpirationDateEnforced"
- @change="expirationDateChanged($event)">
+ @update:model-value="onExpirationChange"
+ @change="expirationDateChanged">
<template #icon>
<IconCalendarBlank :size="20" />
</template>
</NcActionInput>
- <NcActionButton @click.prevent.stop="onNewLinkShare(true)">
+ <NcActionButton :disabled="pendingEnforcedPassword && !share.newPassword"
+ @click.prevent.stop="onNewLinkShare(true)">
<template #icon>
<CheckIcon :size="20" />
</template>
@@ -222,14 +224,14 @@
</template>
<script>
+import { showError, showSuccess } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
+import { t } from '@nextcloud/l10n'
+import moment from '@nextcloud/moment'
import { generateUrl, getBaseUrl } from '@nextcloud/router'
-import { showError, showSuccess } from '@nextcloud/dialogs'
import { ShareType } from '@nextcloud/sharing'
-import VueQrcode from '@chenfengyuan/vue-qrcode'
-import moment from '@nextcloud/moment'
-import Vue from 'vue'
+import VueQrcode from '@chenfengyuan/vue-qrcode'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
@@ -241,10 +243,10 @@ import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import Tune from 'vue-material-design-icons/Tune.vue'
-import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
+import IconCalendarBlank from 'vue-material-design-icons/CalendarBlankOutline.vue'
import IconQr from 'vue-material-design-icons/Qrcode.vue'
import ErrorIcon from 'vue-material-design-icons/Exclamation.vue'
-import LockIcon from 'vue-material-design-icons/Lock.vue'
+import LockIcon from 'vue-material-design-icons/LockOutline.vue'
import CheckIcon from 'vue-material-design-icons/CheckBold.vue'
import ClipboardIcon from 'vue-material-design-icons/ContentCopy.vue'
import CloseIcon from 'vue-material-design-icons/Close.vue'
@@ -258,7 +260,7 @@ import GeneratePassword from '../utils/GeneratePassword.ts'
import Share from '../models/Share.ts'
import SharesMixin from '../mixins/SharesMixin.js'
import ShareDetails from '../mixins/ShareDetails.js'
-import { getLoggerBuilder } from '@nextcloud/logger'
+import logger from '../services/logger.ts'
export default {
name: 'SharingEntryLink',
@@ -313,10 +315,6 @@ export default {
ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state,
ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
- logger: getLoggerBuilder()
- .setApp('files_sharing')
- .detectUser()
- .build(),
// tracks whether modal should be opened or not
showQRCode: false,
@@ -330,6 +328,8 @@ export default {
* @return {string}
*/
title() {
+ const l10nOptions = { escape: false /* no escape as this string is already escaped by Vue */ }
+
// if we have a valid existing share (not pending)
if (this.share && this.share.id) {
if (!this.isShareOwner && this.share.ownerDisplayName) {
@@ -337,26 +337,26 @@ export default {
return t('files_sharing', '{shareWith} by {initiator}', {
shareWith: this.share.shareWith,
initiator: this.share.ownerDisplayName,
- })
+ }, l10nOptions)
}
return t('files_sharing', 'Shared via link by {initiator}', {
initiator: this.share.ownerDisplayName,
- })
+ }, l10nOptions)
}
if (this.share.label && this.share.label.trim() !== '') {
if (this.isEmailShareType) {
if (this.isFileRequest) {
return t('files_sharing', 'File request ({label})', {
label: this.share.label.trim(),
- })
+ }, l10nOptions)
}
return t('files_sharing', 'Mail share ({label})', {
label: this.share.label.trim(),
- })
+ }, l10nOptions)
}
return t('files_sharing', 'Share link ({label})', {
label: this.share.label.trim(),
- })
+ }, l10nOptions)
}
if (this.isEmailShareType) {
if (!this.share.shareWith || this.share.shareWith.trim() === '') {
@@ -391,22 +391,6 @@ export default {
}
return null
},
- /**
- * Is the current share password protected ?
- *
- * @return {boolean}
- */
- isPasswordProtected: {
- get() {
- return this.config.enforcePasswordForPublicLink
- || !!this.share.password
- },
- async set(enabled) {
- // TODO: directly save after generation to make sure the share is always protected
- Vue.set(this.share, 'password', enabled ? await GeneratePassword(true) : '')
- Vue.set(this.share, 'newPassword', this.share.password)
- },
- },
passwordExpirationTime() {
if (this.share.passwordExpirationTime === null) {
@@ -630,7 +614,7 @@ export default {
* @param {boolean} shareReviewComplete if the share was reviewed
*/
async onNewLinkShare(shareReviewComplete = false) {
- this.logger.debug('onNewLinkShare called (with this.share)', this.share)
+ logger.debug('onNewLinkShare called (with this.share)', this.share)
// do not run again if already loading
if (this.loading) {
return
@@ -645,7 +629,7 @@ export default {
shareDefaults.expiration = this.formatDateToString(this.config.defaultExpirationDate)
}
- this.logger.debug('Missing required properties?', this.enforcedPropertiesMissing)
+ logger.debug('Missing required properties?', this.enforcedPropertiesMissing)
// Do not push yet if we need a password or an expiration date: show pending menu
// A share would require a review for example is default expiration date is set but not enforced, this allows
// the user to review the share and remove the expiration date if they don't want it
@@ -653,7 +637,7 @@ export default {
this.pending = true
this.shareCreationComplete = false
- this.logger.info('Share policy requires a review or has mandated properties (password, expirationDate)...')
+ logger.info('Share policy requires a review or has mandated properties (password, expirationDate)...')
// ELSE, show the pending popovermenu
// if password default or enforced, pre-fill with random one
@@ -663,6 +647,7 @@ export default {
// create share & close menu
const share = new Share(shareDefaults)
+ share.newPassword = share.password
const component = await new Promise(resolve => {
this.$emit('add:share', share, resolve)
})
@@ -681,13 +666,13 @@ export default {
// 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)
+ 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)
+ logger.info('Share created on server', this.share)
} catch (e) {
this.pending = false
- this.logger.error('Error creating share', e)
+ logger.error('Error creating share', e)
return false
}
return true
@@ -855,7 +840,7 @@ export default {
*/
onPasswordSubmit() {
if (this.hasUnsavedPassword) {
- this.share.password = this.share.newPassword.trim()
+ this.share.newPassword = this.share.newPassword.trim()
this.queueUpdate('password')
}
},
@@ -870,7 +855,7 @@ export default {
*/
onPasswordProtectedByTalkChange() {
if (this.hasUnsavedPassword) {
- this.share.password = this.share.newPassword.trim()
+ this.share.newPassword = this.share.newPassword.trim()
}
this.queueUpdate('sendPasswordByTalk', 'password')
@@ -892,9 +877,9 @@ export default {
},
expirationDateChanged(event) {
- const date = event.target.value
- this.onExpirationChange(date)
- this.defaultExpirationDateEnabled = !!date
+ const value = event?.target?.value
+ const isValid = !!value && !isNaN(new Date(value).getTime())
+ this.defaultExpirationDateEnabled = isValid
},
/**
diff --git a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue
index 041841201d0..102eea63cb6 100644
--- a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue
+++ b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue
@@ -36,7 +36,7 @@ import ShareDetails from '../mixins/ShareDetails.js'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import IconEyeOutline from 'vue-material-design-icons/EyeOutline.vue'
-import IconPencil from 'vue-material-design-icons/Pencil.vue'
+import IconPencil from 'vue-material-design-icons/PencilOutline.vue'
import IconFileUpload from 'vue-material-design-icons/FileUpload.vue'
import IconTune from 'vue-material-design-icons/Tune.vue'
diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue
index 49a39915e5e..f50dc96fc08 100644
--- a/apps/files_sharing/src/components/SharingInput.vue
+++ b/apps/files_sharing/src/components/SharingInput.vue
@@ -192,14 +192,27 @@ export default {
lookup = true
}
- let shareType = []
+ const remoteTypes = [ShareType.Remote, ShareType.RemoteGroup]
+ const shareType = []
+
+ const showFederatedAsInternal
+ = this.config.showFederatedSharesAsInternal
+ || this.config.showFederatedSharesToTrustedServersAsInternal
+
+ const shouldAddRemoteTypes
+ // For internal users, add remote types if config says to show them as internal
+ = (!this.isExternal && showFederatedAsInternal)
+ // For external users, add them if config *doesn't* say to show them as internal
+ || (this.isExternal && !showFederatedAsInternal)
+ // Edge case: federated-to-trusted is a separate "add" trigger for external users
+ || (this.isExternal && this.config.showFederatedSharesToTrustedServersAsInternal)
if (this.isExternal) {
- shareType.push(ShareType.Remote)
- shareType.push(ShareType.RemoteGroup)
+ if (getCapabilities().files_sharing.public.enabled === true) {
+ shareType.push(ShareType.Email)
+ }
} else {
- // Merge shareType array
- shareType = shareType.concat([
+ shareType.push(
ShareType.User,
ShareType.Group,
ShareType.Team,
@@ -207,12 +220,11 @@ export default {
ShareType.Guest,
ShareType.Deck,
ShareType.ScienceMesh,
- ])
-
+ )
}
- if (getCapabilities().files_sharing.public.enabled === true && this.isExternal) {
- shareType.push(ShareType.Email)
+ if (shouldAddRemoteTypes) {
+ shareType.push(...remoteTypes)
}
let request = null
@@ -363,6 +375,11 @@ export default {
// filter out existing mail shares
if (share.value.shareType === ShareType.Email) {
+ // When sharing internally, we don't want to suggest email addresses
+ // that the user previously created shares to
+ if (!this.isExternal) {
+ return arr
+ }
const emails = this.linkShares.map(elem => elem.shareWith)
if (emails.indexOf(share.value.shareWith.trim()) !== -1) {
return arr
diff --git a/apps/files_sharing/src/files_actions/openInFilesAction.spec.ts b/apps/files_sharing/src/files_actions/openInFilesAction.spec.ts
index 95bd2812db7..23c0938545c 100644
--- a/apps/files_sharing/src/files_actions/openInFilesAction.spec.ts
+++ b/apps/files_sharing/src/files_actions/openInFilesAction.spec.ts
@@ -29,7 +29,7 @@ const invalidViews = [
describe('Open in files action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
- expect(action.id).toBe('open-in-files')
+ expect(action.id).toBe('files_sharing:open-in-files')
expect(action.displayName([], validViews[0])).toBe('Open in Files')
expect(action.iconSvgInline([], validViews[0])).toBe('')
expect(action.default).toBe(DefaultType.HIDDEN)
diff --git a/apps/files_sharing/src/files_actions/openInFilesAction.ts b/apps/files_sharing/src/files_actions/openInFilesAction.ts
index e9e07179fc4..133b4531bb5 100644
--- a/apps/files_sharing/src/files_actions/openInFilesAction.ts
+++ b/apps/files_sharing/src/files_actions/openInFilesAction.ts
@@ -2,15 +2,15 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+
import type { Node } from '@nextcloud/files'
import { registerFileAction, FileAction, DefaultType, FileType } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
-
import { sharesViewId, sharedWithYouViewId, sharedWithOthersViewId, sharingByLinksViewId } from '../files_views/shares'
export const action = new FileAction({
- id: 'open-in-files',
+ id: 'files_sharing:open-in-files',
displayName: () => t('files_sharing', 'Open in Files'),
iconSvgInline: () => '',
diff --git a/apps/files_sharing/src/files_actions/sharingStatusAction.ts b/apps/files_sharing/src/files_actions/sharingStatusAction.ts
index 75fe7d54096..2dfd8467c5b 100644
--- a/apps/files_sharing/src/files_actions/sharingStatusAction.ts
+++ b/apps/files_sharing/src/files_actions/sharingStatusAction.ts
@@ -8,8 +8,8 @@ import { translate as t } from '@nextcloud/l10n'
import { ShareType } from '@nextcloud/sharing'
import { isPublicShare } from '@nextcloud/sharing/public'
-import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw'
-import AccountPlusSvg from '@mdi/svg/svg/account-plus.svg?raw'
+import AccountGroupSvg from '@mdi/svg/svg/account-group-outline.svg?raw'
+import AccountPlusSvg from '@mdi/svg/svg/account-plus-outline.svg?raw'
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
import CircleSvg from '../../../../core/img/apps/circles.svg?raw'
diff --git a/apps/files_sharing/src/files_filters/AccountFilter.ts b/apps/files_sharing/src/files_filters/AccountFilter.ts
index 8da4d85d67c..4f185d9fd9c 100644
--- a/apps/files_sharing/src/files_filters/AccountFilter.ts
+++ b/apps/files_sharing/src/files_filters/AccountFilter.ts
@@ -4,27 +4,41 @@
*/
import type { IFileListFilterChip, INode } from '@nextcloud/files'
+import { subscribe } from '@nextcloud/event-bus'
import { FileListFilter, registerFileListFilter } from '@nextcloud/files'
+import { ShareType } from '@nextcloud/sharing'
import Vue from 'vue'
+
import FileListFilterAccount from '../components/FileListFilterAccount.vue'
+import { isPublicShare } from '@nextcloud/sharing/public'
export interface IAccountData {
uid: string
displayName: string
}
-type CurrentInstance = Vue & { resetFilter: () => void, toggleAccount: (account: string) => void }
+type CurrentInstance = Vue & {
+ resetFilter: () => void
+ setAvailableAccounts: (accounts: IAccountData[]) => void
+ toggleAccount: (account: string) => void
+}
/**
* File list filter to filter by owner / sharee
*/
class AccountFilter extends FileListFilter {
+ private availableAccounts: IAccountData[]
private currentInstance?: CurrentInstance
private filterAccounts?: IAccountData[]
constructor() {
super('files_sharing:account', 100)
+ this.availableAccounts = []
+
+ subscribe('files:list:updated', ({ contents }) => {
+ this.updateAvailableAccounts(contents)
+ })
}
public mount(el: HTMLElement) {
@@ -33,11 +47,11 @@ class AccountFilter extends FileListFilter {
}
const View = Vue.extend(FileListFilterAccount as never)
- this.currentInstance = new View({
- el,
- })
- .$on('update:accounts', this.setAccounts.bind(this))
+ this.currentInstance = new View({ el })
+ .$on('update:accounts', (accounts?: IAccountData[]) => this.setAccounts(accounts))
.$mount() as CurrentInstance
+ this.currentInstance
+ .setAvailableAccounts(this.availableAccounts)
}
public filter(nodes: INode[]): INode[] {
@@ -70,6 +84,11 @@ class AccountFilter extends FileListFilter {
this.currentInstance?.resetFilter()
}
+ /**
+ * Set accounts that should be filtered.
+ *
+ * @param accounts - Account to filter or undefined if inactive.
+ */
public setAccounts(accounts?: IAccountData[]) {
this.filterAccounts = accounts
let chips: IFileListFilterChip[] = []
@@ -85,11 +104,59 @@ class AccountFilter extends FileListFilter {
this.filterUpdated()
}
+ /**
+ * Update the accounts owning nodes or have nodes shared to them.
+ *
+ * @param nodes - The current content of the file list.
+ */
+ protected updateAvailableAccounts(nodes: INode[]): void {
+ const available = new Map<string, IAccountData>()
+
+ for (const node of nodes) {
+ const owner = node.owner
+ if (owner && !available.has(owner)) {
+ available.set(owner, {
+ uid: owner,
+ displayName: node.attributes['owner-display-name'] ?? node.owner,
+ })
+ }
+
+ // ensure sharees is an array (if only one share then it is just an object)
+ const sharees: { id: string, 'display-name': string, type: ShareType }[] = [node.attributes.sharees?.sharee].flat().filter(Boolean)
+ for (const sharee of [sharees].flat()) {
+ // Skip link shares and other without user
+ if (sharee.id === '') {
+ continue
+ }
+ if (sharee.type !== ShareType.User && sharee.type !== ShareType.Remote) {
+ continue
+ }
+ // Add if not already added
+ if (!available.has(sharee.id)) {
+ available.set(sharee.id, {
+ uid: sharee.id,
+ displayName: sharee['display-name'],
+ })
+ }
+ }
+ }
+
+ this.availableAccounts = [...available.values()]
+ if (this.currentInstance) {
+ this.currentInstance.setAvailableAccounts(this.availableAccounts)
+ }
+ }
+
}
/**
* Register the file list filter by owner or sharees
*/
export function registerAccountFilter() {
+ if (isPublicShare()) {
+ // We do not show the filter on public pages - it makes no sense
+ return
+ }
+
registerFileListFilter(new AccountFilter())
}
diff --git a/apps/files_sharing/src/files_newMenu/newFileRequest.ts b/apps/files_sharing/src/files_newMenu/newFileRequest.ts
index f7c5cc4057a..1d58e3552a2 100644
--- a/apps/files_sharing/src/files_newMenu/newFileRequest.ts
+++ b/apps/files_sharing/src/files_newMenu/newFileRequest.ts
@@ -7,7 +7,7 @@ import type { Entry, Folder, Node } from '@nextcloud/files'
import { defineAsyncComponent } from 'vue'
import { spawnDialog } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
-import FileUploadSvg from '@mdi/svg/svg/file-upload.svg?raw'
+import FileUploadSvg from '@mdi/svg/svg/file-upload-outline.svg?raw'
import Config from '../services/ConfigService'
import { isPublicShare } from '@nextcloud/sharing/public'
diff --git a/apps/files_sharing/src/files_views/publicFileDrop.ts b/apps/files_sharing/src/files_views/publicFileDrop.ts
index 0d782d48fc7..65756e83c74 100644
--- a/apps/files_sharing/src/files_views/publicFileDrop.ts
+++ b/apps/files_sharing/src/files_views/publicFileDrop.ts
@@ -4,7 +4,8 @@
*/
import type { VueConstructor } from 'vue'
-import { Folder, Permission, View, davRemoteURL, davRootPath, getNavigation } from '@nextcloud/files'
+import { Folder, Permission, View, getNavigation } from '@nextcloud/files'
+import { defaultRemoteURL, defaultRootPath } from '@nextcloud/files/dav'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw'
@@ -45,8 +46,8 @@ export default () => {
// Fake a writeonly folder as root
folder: new Folder({
id: 0,
- source: `${davRemoteURL}${davRootPath}`,
- root: davRootPath,
+ source: `${defaultRemoteURL}${defaultRootPath}`,
+ root: defaultRootPath,
owner: null,
permissions: Permission.CREATE,
}),
diff --git a/apps/files_sharing/src/files_views/shares.ts b/apps/files_sharing/src/files_views/shares.ts
index 297fd4796fd..fd5e908638c 100644
--- a/apps/files_sharing/src/files_views/shares.ts
+++ b/apps/files_sharing/src/files_views/shares.ts
@@ -6,11 +6,11 @@ import { translate as t } from '@nextcloud/l10n'
import { View, getNavigation } from '@nextcloud/files'
import { ShareType } from '@nextcloud/sharing'
import AccountClockSvg from '@mdi/svg/svg/account-clock.svg?raw'
-import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw'
-import AccountPlusSvg from '@mdi/svg/svg/account-plus.svg?raw'
+import AccountGroupSvg from '@mdi/svg/svg/account-group-outline.svg?raw'
+import AccountPlusSvg from '@mdi/svg/svg/account-plus-outline.svg?raw'
import AccountSvg from '@mdi/svg/svg/account.svg?raw'
import DeleteSvg from '@mdi/svg/svg/delete.svg?raw'
-import FileUploadSvg from '@mdi/svg/svg/file-upload.svg?raw'
+import FileUploadSvg from '@mdi/svg/svg/file-upload-outline.svg?raw'
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
import { getContents, isFileRequest } from '../services/SharingService'
diff --git a/apps/files_sharing/src/init.ts b/apps/files_sharing/src/init.ts
index 5a2b93c6ea3..f275f3beaf7 100644
--- a/apps/files_sharing/src/init.ts
+++ b/apps/files_sharing/src/init.ts
@@ -2,7 +2,8 @@
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { addNewFileMenuEntry, registerDavProperty } from '@nextcloud/files'
+import { addNewFileMenuEntry } from '@nextcloud/files'
+import { registerDavProperty } from '@nextcloud/files/dav'
import { registerAccountFilter } from './files_filters/AccountFilter'
import { entry as newFileRequest } from './files_newMenu/newFileRequest'
@@ -21,6 +22,7 @@ addNewFileMenuEntry(newFileRequest)
registerDavProperty('nc:note', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('nc:sharees', { nc: 'http://nextcloud.org/ns' })
+registerDavProperty('nc:hide-download', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('nc:share-attributes', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('oc:share-types', { oc: 'http://owncloud.org/ns' })
registerDavProperty('ocs:share-permissions', { ocs: 'http://open-collaboration-services.org/ns' })
diff --git a/apps/files_sharing/src/mixins/ShareDetails.js b/apps/files_sharing/src/mixins/ShareDetails.js
index 61cffab86f2..6ccdf8d63d0 100644
--- a/apps/files_sharing/src/mixins/ShareDetails.js
+++ b/apps/files_sharing/src/mixins/ShareDetails.js
@@ -5,6 +5,8 @@
import Share from '../models/Share.ts'
import Config from '../services/ConfigService.ts'
+import { ATOMIC_PERMISSIONS } from '../lib/SharePermissionsToolBox.js'
+import logger from '../services/logger.ts'
export default {
methods: {
@@ -26,6 +28,18 @@ export default {
share = this.mapShareRequestToShareObject(shareRequestObject)
}
+ if (this.fileInfo.type !== 'dir') {
+ const originalPermissions = share.permissions
+ const strippedPermissions = originalPermissions
+ & ~ATOMIC_PERMISSIONS.CREATE
+ & ~ATOMIC_PERMISSIONS.DELETE
+
+ if (originalPermissions !== strippedPermissions) {
+ logger.debug('Removed create/delete permissions from file share (only valid for folders)')
+ share.permissions = strippedPermissions
+ }
+ }
+
const shareDetails = {
fileInfo: this.fileInfo,
share,
diff --git a/apps/files_sharing/src/mixins/ShareRequests.js b/apps/files_sharing/src/mixins/ShareRequests.js
index 24b4b12061e..2c33fa3b0c7 100644
--- a/apps/files_sharing/src/mixins/ShareRequests.js
+++ b/apps/files_sharing/src/mixins/ShareRequests.js
@@ -6,10 +6,12 @@
// TODO: remove when ie not supported
import 'url-search-params-polyfill'
+import { emit } from '@nextcloud/event-bus'
+import { showError } from '@nextcloud/dialogs'
import { generateOcsUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
+
import Share from '../models/Share.ts'
-import { emit } from '@nextcloud/event-bus'
const shareUrl = generateOcsUrl('apps/files_sharing/api/v1/shares')
@@ -45,7 +47,7 @@ export default {
} catch (error) {
console.error('Error while creating share', error)
const errorMessage = error?.response?.data?.ocs?.meta?.message
- OC.Notification.showTemporary(
+ showError(
errorMessage ? t('files_sharing', 'Error creating the share: {errorMessage}', { errorMessage }) : t('files_sharing', 'Error creating the share'),
{ type: 'error' },
)
diff --git a/apps/files_sharing/src/mixins/SharesMixin.js b/apps/files_sharing/src/mixins/SharesMixin.js
index 588b0bf4bc6..a461da56d85 100644
--- a/apps/files_sharing/src/mixins/SharesMixin.js
+++ b/apps/files_sharing/src/mixins/SharesMixin.js
@@ -11,6 +11,7 @@ import { emit } from '@nextcloud/event-bus'
import PQueue from 'p-queue'
import debounce from 'debounce'
+import GeneratePassword from '../utils/GeneratePassword.ts'
import Share from '../models/Share.ts'
import SharesRequests from './ShareRequests.js'
import Config from '../services/ConfigService.ts'
@@ -156,6 +157,26 @@ export default {
}
return null
},
+ /**
+ * Is the current share password protected ?
+ *
+ * @return {boolean}
+ */
+ isPasswordProtected: {
+ get() {
+ return this.config.enforcePasswordForPublicLink
+ || this.share.password !== ''
+ || this.share.newPassword !== undefined
+ },
+ async set(enabled) {
+ if (enabled) {
+ this.$set(this.share, 'newPassword', await GeneratePassword(true))
+ } else {
+ this.share.password = ''
+ this.$delete(this.share, 'newPassword')
+ }
+ },
+ },
},
methods: {
@@ -213,8 +234,13 @@ export default {
* @param {Date} date
*/
onExpirationChange(date) {
- const formattedDate = date ? this.formatDateToString(new Date(date)) : ''
- this.share.expireDate = formattedDate
+ if (!date) {
+ this.share.expireDate = null
+ this.$set(this.share, 'expireDate', null)
+ return
+ }
+ const parsedDate = (date instanceof Date) ? date : new Date(date)
+ this.share.expireDate = this.formatDateToString(parsedDate)
},
/**
@@ -246,7 +272,7 @@ export default {
this.loading = true
this.open = false
await this.deleteShare(this.share.id)
- console.debug('Share deleted', this.share.id)
+ logger.debug('Share deleted', { shareId: this.share.id })
const message = this.share.itemType === 'file'
? t('files_sharing', 'File "{path}" has been unshared', { path: this.share.path })
: t('files_sharing', 'Folder "{path}" has been unshared', { path: this.share.path })
@@ -277,7 +303,12 @@ export default {
const properties = {}
// force value to string because that is what our
// share api controller accepts
- propertyNames.forEach(name => {
+ for (const name of propertyNames) {
+ if (name === 'password') {
+ properties[name] = this.share.newPassword ?? this.share.password
+ continue
+ }
+
if (this.share[name] === null || this.share[name] === undefined) {
properties[name] = ''
} else if ((typeof this.share[name]) === 'object') {
@@ -285,7 +316,7 @@ export default {
} else {
properties[name] = this.share[name].toString()
}
- })
+ }
return this.updateQueue.add(async () => {
this.saving = true
@@ -293,8 +324,9 @@ export default {
try {
const updatedShare = await this.updateShare(this.share.id, properties)
- if (propertyNames.indexOf('password') >= 0) {
+ if (propertyNames.includes('password')) {
// reset password state after sync
+ this.share.password = this.share.newPassword ?? ''
this.$delete(this.share, 'newPassword')
// updates password expiration time after sync
@@ -302,14 +334,18 @@ export default {
}
// clear any previous errors
- this.$delete(this.errors, propertyNames[0])
+ for (const property of propertyNames) {
+ this.$delete(this.errors, property)
+ }
showSuccess(this.updateSuccessMessage(propertyNames))
} catch (error) {
logger.error('Could not update share', { error, share: this.share, propertyNames })
const { message } = error
if (message && message !== '') {
- this.onSyncError(propertyNames[0], message)
+ for (const property of propertyNames) {
+ this.onSyncError(property, message)
+ }
showError(message)
} else {
// We do not have information what happened, but we should still inform the user
@@ -358,6 +394,13 @@ export default {
* @param {string} message the error message
*/
onSyncError(property, message) {
+ if (property === 'password' && this.share.newPassword) {
+ if (this.share.newPassword === this.share.password) {
+ this.share.password = ''
+ }
+ this.$delete(this.share, 'newPassword')
+ }
+
// re-open menu if closed
this.open = true
switch (property) {
diff --git a/apps/files_sharing/src/models/Share.ts b/apps/files_sharing/src/models/Share.ts
index 39907b03025..b0638b29448 100644
--- a/apps/files_sharing/src/models/Share.ts
+++ b/apps/files_sharing/src/models/Share.ts
@@ -252,6 +252,15 @@ export default class Share {
* Hide the download button on public page
*/
set hideDownload(state: boolean) {
+ // disabling hide-download also enables the download permission
+ // needed for regression in Nextcloud 31.0.0 until (incl.) 31.0.3
+ if (!state) {
+ const attribute = this.attributes.find(({ key, scope }) => key === 'download' && scope === 'permissions')
+ if (attribute) {
+ attribute.value = true
+ }
+ }
+
this._share.hide_download = state === true
}
@@ -477,4 +486,11 @@ export default class Share {
return this._share.status
}
+ /**
+ * Is the share from a trusted server
+ */
+ get isTrustedServer(): boolean {
+ return !!this._share.is_trusted_server
+ }
+
}
diff --git a/apps/files_sharing/src/public-file-request.ts b/apps/files_sharing/src/public-file-request.ts
deleted file mode 100644
index 1d640c5ea5e..00000000000
--- a/apps/files_sharing/src/public-file-request.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import { defineAsyncComponent } from 'vue'
-import { getBuilder } from '@nextcloud/browser-storage'
-import { getGuestNickname, setGuestNickname } from '@nextcloud/auth'
-import { getUploader } from '@nextcloud/upload'
-import { spawnDialog } from '@nextcloud/dialogs'
-
-import logger from './services/logger'
-
-const storage = getBuilder('files_sharing').build()
-
-/**
- * Setup file-request nickname header for the uploader
- * @param nickname The nickname
- */
-function registerFileRequestHeader(nickname: string) {
- const uploader = getUploader()
- uploader.setCustomHeader('X-NC-Nickname', encodeURIComponent(nickname))
- logger.debug('Nickname header registered for uploader', { headers: uploader.customHeaders })
-}
-
-/**
- * Callback when a nickname was chosen
- * @param nickname The chosen nickname
- */
-function onSetNickname(nickname: string): void {
- // Set the nickname
- setGuestNickname(nickname)
- // Set the dialog as shown
- storage.setItem('public-auth-prompt-shown', 'true')
- // Register header for uploader
- registerFileRequestHeader(nickname)
-}
-
-window.addEventListener('DOMContentLoaded', () => {
- const nickname = getGuestNickname() ?? ''
- const dialogShown = storage.getItem('public-auth-prompt-shown') !== null
-
- // If we don't have a nickname or the public auth prompt hasn't been shown yet, show it
- // We still show the prompt if the user has a nickname to double check
- if (!nickname || !dialogShown) {
- spawnDialog(
- defineAsyncComponent(() => import('./views/PublicAuthPrompt.vue')),
- {
- nickname,
- },
- onSetNickname as (...rest: unknown[]) => void,
- )
- } else {
- logger.debug('Public auth prompt already shown.', { nickname })
- registerFileRequestHeader(nickname)
- }
-})
diff --git a/apps/files_sharing/src/public-nickname-handler.ts b/apps/files_sharing/src/public-nickname-handler.ts
new file mode 100644
index 00000000000..02bdc641aaf
--- /dev/null
+++ b/apps/files_sharing/src/public-nickname-handler.ts
@@ -0,0 +1,86 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getBuilder } from '@nextcloud/browser-storage'
+import { getGuestNickname, type NextcloudUser } from '@nextcloud/auth'
+import { getUploader } from '@nextcloud/upload'
+import { loadState } from '@nextcloud/initial-state'
+import { showGuestUserPrompt } from '@nextcloud/dialogs'
+import { t } from '@nextcloud/l10n'
+
+import logger from './services/logger'
+import { subscribe } from '@nextcloud/event-bus'
+
+const storage = getBuilder('files_sharing').build()
+
+// Setup file-request nickname header for the uploader
+const registerFileRequestHeader = (nickname: string) => {
+ const uploader = getUploader()
+ uploader.setCustomHeader('X-NC-Nickname', encodeURIComponent(nickname))
+ logger.debug('Nickname header registered for uploader', { headers: uploader.customHeaders })
+}
+
+// Callback when a nickname was chosen
+const onUserInfoChanged = (guest: NextcloudUser) => {
+ logger.debug('User info changed', { guest })
+ registerFileRequestHeader(guest.displayName ?? '')
+}
+
+// Monitor nickname changes
+subscribe('user:info:changed', onUserInfoChanged)
+
+window.addEventListener('DOMContentLoaded', () => {
+ const nickname = getGuestNickname() ?? ''
+ const dialogShown = storage.getItem('public-auth-prompt-shown') !== null
+
+ // Check if a nickname is mandatory
+ const isFileRequest = loadState('files_sharing', 'isFileRequest', false)
+
+ const owner = loadState('files_sharing', 'owner', '')
+ const ownerDisplayName = loadState('files_sharing', 'ownerDisplayName', '')
+ const label = loadState('files_sharing', 'label', '')
+ const filename = loadState('files_sharing', 'filename', '')
+
+ // If the owner provided a custom label, use it instead of the filename
+ const folder = label || filename
+
+ const options = {
+ nickname,
+ notice: t('files_sharing', 'To upload files to {folder}, you need to provide your name first.', { folder }),
+ subtitle: undefined as string | undefined,
+ title: t('files_sharing', 'Upload files to {folder}', { folder }),
+ }
+
+ // If the guest already has a nickname, we just make them double check
+ if (nickname) {
+ options.notice = t('files_sharing', 'Please confirm your name to upload files to {folder}', { folder })
+ }
+
+ // If the account owner set their name as public,
+ // we show it in the subtitle
+ if (owner) {
+ options.subtitle = t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName })
+ }
+
+ // If this is a file request, then we need a nickname
+ if (isFileRequest) {
+ // If we don't have a nickname or the public auth prompt hasn't been shown yet, show it
+ // We still show the prompt if the user has a nickname to double check
+ if (!nickname || !dialogShown) {
+ logger.debug('Showing public auth prompt.', { nickname })
+ showGuestUserPrompt(options)
+ }
+ return
+ }
+
+ if (!dialogShown && !nickname) {
+ logger.debug('Public auth prompt not shown yet but nickname is not mandatory.', { nickname })
+ return
+ }
+
+ // Else, we just register the nickname header if any.
+ logger.debug('Public auth prompt already shown.', { nickname })
+ registerFileRequestHeader(nickname)
+})
diff --git a/apps/files_sharing/src/services/ConfigService.ts b/apps/files_sharing/src/services/ConfigService.ts
index 09fdca13598..f75f34c7936 100644
--- a/apps/files_sharing/src/services/ConfigService.ts
+++ b/apps/files_sharing/src/services/ConfigService.ts
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getCapabilities } from '@nextcloud/capabilities'
+import { loadState } from '@nextcloud/initial-state'
type PasswordPolicyCapabilities = {
enforceNonCommonPassword: boolean
@@ -306,4 +307,20 @@ export default class Config {
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/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.ts b/apps/files_sharing/src/services/SharingService.ts
index d8c6747f89c..41c20f9aa73 100644
--- a/apps/files_sharing/src/services/SharingService.ts
+++ b/apps/files_sharing/src/services/SharingService.ts
@@ -94,6 +94,7 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu
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,
diff --git a/apps/files_sharing/src/utils/GeneratePassword.ts b/apps/files_sharing/src/utils/GeneratePassword.ts
index 2f3f65c51d8..82efaaa69d4 100644
--- a/apps/files_sharing/src/utils/GeneratePassword.ts
+++ b/apps/files_sharing/src/utils/GeneratePassword.ts
@@ -38,10 +38,29 @@ export default async function(verbose = false): Promise<string> {
const array = new Uint8Array(10)
const ratio = passwordSet.length / 255
- self.crypto.getRandomValues(array)
+ getRandomValues(array)
let password = ''
for (let i = 0; i < array.length; i++) {
password += passwordSet.charAt(array[i] * ratio)
}
return password
}
+
+/**
+ * Fills the given array with cryptographically secure random values.
+ * If the crypto API is not available, it falls back to less secure Math.random().
+ * Crypto API is available in modern browsers on secure contexts (HTTPS).
+ *
+ * @param {Uint8Array} array - The array to fill with random values.
+ */
+function getRandomValues(array: Uint8Array): void {
+ if (self?.crypto?.getRandomValues) {
+ self.crypto.getRandomValues(array)
+ return
+ }
+
+ let len = array.length
+ while (len--) {
+ array[len] = Math.floor(Math.random() * 256)
+ }
+}
diff --git a/apps/files_sharing/src/views/CollaborationView.vue b/apps/files_sharing/src/views/CollaborationView.vue
deleted file mode 100644
index b75ad53e1b8..00000000000
--- a/apps/files_sharing/src/views/CollaborationView.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<!--
- - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
- - SPDX-License-Identifier: AGPL-3.0-or-later
--->
-
-<template>
- <CollectionList v-if="fileId"
- :id="fileId"
- type="file"
- :name="filename" />
-</template>
-
-<script>
-import { CollectionList } from 'nextcloud-vue-collections'
-
-export default {
- name: 'CollaborationView',
- components: {
- CollectionList,
- },
- computed: {
- fileId() {
- if (this.$root.model && this.$root.model.id) {
- return '' + this.$root.model.id
- }
- return null
- },
- filename() {
- if (this.$root.model && this.$root.model.name) {
- return '' + this.$root.model.name
- }
- return ''
- },
- },
-}
-</script>
diff --git a/apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue b/apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue
index 31b66741698..ec6348606fb 100644
--- a/apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue
+++ b/apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue
@@ -6,7 +6,7 @@
<NcNoteCard v-if="note.length > 0"
class="note-to-recipient"
type="info">
- <p v-if="user" class="note-to-recipient__heading">
+ <p v-if="displayName" class="note-to-recipient__heading">
{{ t('files_sharing', 'Note from') }}
<NcUserBubble :user="user.id" :display-name="user.displayName" />
</p>
@@ -28,13 +28,13 @@ import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'
const folder = ref<Folder>()
const note = computed<string>(() => folder.value?.attributes.note ?? '')
+const displayName = computed<string>(() => folder.value?.attributes['owner-display-name'] ?? '')
const user = computed(() => {
const id = folder.value?.owner
- const displayName = folder.value?.attributes?.['owner-display-name']
if (id !== getCurrentUser()?.uid) {
return {
id,
- displayName,
+ displayName: displayName.value,
}
}
return null
diff --git a/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue
index 5571e5e9f5d..dac22748d8a 100644
--- a/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue
+++ b/apps/files_sharing/src/views/FilesViewFileDropEmptyContent.vue
@@ -5,13 +5,29 @@
<template>
<NcEmptyContent class="file-drop-empty-content"
data-cy-files-sharing-file-drop
- :name="t('files_sharing', 'File drop')">
+ :name="name">
<template #icon>
<NcIconSvgWrapper :svg="svgCloudUpload" />
</template>
<template #description>
- {{ t('files_sharing', 'Upload files to {foldername}.', { foldername }) }}
- {{ disclaimer === '' ? '' : t('files_sharing', 'By uploading files, you agree to the terms of service.') }}
+ <p>
+ {{ shareNote || t('files_sharing', 'Upload files to {foldername}.', { foldername }) }}
+ </p>
+ <p v-if="disclaimer">
+ {{ t('files_sharing', 'By uploading files, you agree to the terms of service.') }}
+ </p>
+ <NcNoteCard v-if="getSortedUploads().length"
+ class="file-drop-empty-content__note-card"
+ type="success">
+ <h2 id="file-drop-empty-content__heading">
+ {{ t('files_sharing', 'Successfully uploaded files') }}
+ </h2>
+ <ul aria-labelledby="file-drop-empty-content__heading" class="file-drop-empty-content__list">
+ <li v-for="file in getSortedUploads()" :key="file">
+ {{ file }}
+ </li>
+ </ul>
+ </NcNoteCard>
</template>
<template #action>
<template v-if="disclaimer">
@@ -34,34 +50,87 @@
</NcEmptyContent>
</template>
+<script lang="ts">
+/* eslint-disable import/first */
+
+// We need this on module level rather than on the instance as view will be refreshed by the files app after uploading
+const uploads = new Set<string>()
+</script>
+
<script setup lang="ts">
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
-import { getUploader, UploadPicker } from '@nextcloud/upload'
+import { getUploader, UploadPicker, UploadStatus } from '@nextcloud/upload'
import { ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
-import svgCloudUpload from '@mdi/svg/svg/cloud-upload.svg?raw'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import svgCloudUpload from '@mdi/svg/svg/cloud-upload-outline.svg?raw'
defineProps<{
foldername: string
}>()
const disclaimer = loadState<string>('files_sharing', 'disclaimer', '')
+const shareLabel = loadState<string>('files_sharing', 'label', '')
+const shareNote = loadState<string>('files_sharing', 'note', '')
+
+const name = shareLabel || t('files_sharing', 'File drop')
+
const showDialog = ref(false)
const uploadDestination = getUploader().destination
-</script>
-<style scoped>
-:deep(.terms-of-service-dialog) {
- min-height: min(100px, 20vh);
+getUploader()
+ .addNotifier((upload) => {
+ if (upload.status === UploadStatus.FINISHED && upload.file.name) {
+ // if a upload is finished and is not a meta upload (name is set)
+ // then we add the upload to the list of finished uploads to be shown to the user
+ uploads.add(upload.file.name)
+ }
+ })
+
+/**
+ * Get the previous uploads as sorted list
+ */
+function getSortedUploads() {
+ return [...uploads].sort((a, b) => a.localeCompare(b))
}
-/* TODO fix in library */
-.file-drop-empty-content :deep(.empty-content__action) {
- display: flex;
- gap: var(--default-grid-baseline);
+</script>
+
+<style scoped lang="scss">
+.file-drop-empty-content {
+ margin: auto;
+ max-width: max(50vw, 300px);
+
+ .file-drop-empty-content__note-card {
+ width: fit-content;
+ margin-inline: auto;
+ }
+
+ #file-drop-empty-content__heading {
+ margin-block: 0 10px;
+ font-weight: bold;
+ font-size: 20px;
+ }
+
+ .file-drop-empty-content__list {
+ list-style: inside;
+ max-height: min(350px, 33vh);
+ overflow-y: scroll;
+ padding-inline-end: calc(2 * var(--default-grid-baseline));
+ }
+
+ :deep(.terms-of-service-dialog) {
+ min-height: min(100px, 20vh);
+ }
+
+ /* TODO fix in library */
+ :deep(.empty-content__action) {
+ display: flex;
+ gap: var(--default-grid-baseline);
+ }
}
</style>
diff --git a/apps/files_sharing/src/views/PublicAuthPrompt.vue b/apps/files_sharing/src/views/PublicAuthPrompt.vue
deleted file mode 100644
index afa1e10ac56..00000000000
--- a/apps/files_sharing/src/views/PublicAuthPrompt.vue
+++ /dev/null
@@ -1,123 +0,0 @@
-<!--
- - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- - SPDX-License-Identifier: AGPL-3.0-or-later
--->
-
-<template>
- <NcDialog :buttons="dialogButtons"
- class="public-auth-prompt"
- data-cy-public-auth-prompt-dialog
- is-form
- :can-close="false"
- :name="dialogName"
- @submit="$emit('close', name)">
- <p v-if="owner" class="public-auth-prompt__subtitle">
- {{ t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName }) }}
- </p>
-
- <!-- Header -->
- <NcNoteCard class="public-auth-prompt__header"
- :text="t('files_sharing', 'To upload files, you need to provide your name first.')"
- type="info" />
-
- <!-- Form -->
- <NcTextField ref="input"
- class="public-auth-prompt__input"
- data-cy-public-auth-prompt-dialog-name
- :label="t('files_sharing', 'Nickname')"
- :placeholder="t('files_sharing', 'Enter your nickname')"
- minlength="2"
- name="name"
- required
- :value.sync="name" />
- </NcDialog>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue'
-import { t } from '@nextcloud/l10n'
-
-import NcDialog from '@nextcloud/vue/components/NcDialog'
-import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
-import NcTextField from '@nextcloud/vue/components/NcTextField'
-import { loadState } from '@nextcloud/initial-state'
-
-export default defineComponent({
- name: 'PublicAuthPrompt',
-
- components: {
- NcDialog,
- NcNoteCard,
- NcTextField,
- },
-
- props: {
- /**
- * Preselected nickname
- * @default '' No name preselected by default
- */
- nickname: {
- type: String,
- default: '',
- },
- },
-
- setup() {
- return {
- t,
-
- owner: loadState('files_sharing', 'owner', ''),
- ownerDisplayName: loadState('files_sharing', 'ownerDisplayName', ''),
- label: loadState('files_sharing', 'label', ''),
- note: loadState('files_sharing', 'note', ''),
- filename: loadState('files_sharing', 'filename', ''),
- }
- },
-
- data() {
- return {
- name: '',
- }
- },
-
- computed: {
- dialogName() {
- return this.t('files_sharing', 'Upload files to {folder}', { folder: this.label || this.filename })
- },
- dialogButtons() {
- return [{
- label: t('files_sharing', 'Submit name'),
- type: 'primary',
- nativeType: 'submit',
- }]
- },
- },
-
- watch: {
- /** Reset name to pre-selected nickname (e.g. Talk / Collabora ) */
- nickname: {
- handler() {
- this.name = this.nickname
- },
- immediate: true,
- },
- },
-})
-</script>
-<style scoped lang="scss">
-.public-auth-prompt {
- &__subtitle {
- // Smaller than dialog title
- font-size: 1.25em;
- margin-block: 0 calc(3 * var(--default-grid-baseline));
- }
-
- &__header {
- margin-block: 0 calc(3 * var(--default-grid-baseline));
- }
-
- &__input {
- margin-block: calc(4 * var(--default-grid-baseline)) calc(2 * var(--default-grid-baseline));
- }
-}
-</style>
diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue
index 659d65cd70c..ee902a24c8a 100644
--- a/apps/files_sharing/src/views/SharingDetailsTab.vue
+++ b/apps/files_sharing/src/views/SharingDetailsTab.vue
@@ -38,7 +38,7 @@
<NcCheckboxRadioSwitch :button-variant="true"
data-cy-files-sharing-share-permissions-bundle="upload-edit"
:checked.sync="sharingPermission"
- :value="bundledPermissions.ALL.toString()"
+ :value="allPermissions"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@@ -128,7 +128,7 @@
</NcCheckboxRadioSwitch>
<NcPasswordField v-if="isPasswordProtected"
autocomplete="new-password"
- :value="hasUnsavedPassword ? share.newPassword : ''"
+ :value="share.newPassword ?? ''"
:error="passwordError"
:helper-text="errorPasswordLabel || passwordHint"
:required="isPasswordEnforced && isNewShare"
@@ -226,19 +226,6 @@
{{ t('files_sharing', 'Delete') }}
</NcCheckboxRadioSwitch>
</section>
- <div class="sharingTabDetailsView__delete">
- <NcButton v-if="!isNewShare"
- :aria-label="t('files_sharing', 'Delete share')"
- :disabled="false"
- :readonly="false"
- type="tertiary"
- @click.prevent="removeShare">
- <template #icon>
- <CloseIcon :size="16" />
- </template>
- {{ t('files_sharing', 'Delete share') }}
- </NcButton>
- </div>
</section>
</div>
</div>
@@ -249,8 +236,22 @@
@click="cancel">
{{ t('files_sharing', 'Cancel') }}
</NcButton>
+ <div class="sharingTabDetailsView__delete">
+ <NcButton v-if="!isNewShare"
+ :aria-label="t('files_sharing', 'Delete share')"
+ :disabled="false"
+ :readonly="false"
+ variant="tertiary"
+ @click.prevent="removeShare">
+ <template #icon>
+ <CloseIcon :size="20" />
+ </template>
+ {{ t('files_sharing', 'Delete share') }}
+ </NcButton>
+ </div>
<NcButton type="primary"
data-cy-files-sharing-share-editor-action="save"
+ :disabled="creating"
@click="saveShare">
{{ shareButtonText }}
<template v-if="creating" #icon>
@@ -280,7 +281,7 @@ import NcTextArea from '@nextcloud/vue/components/NcTextArea'
import CircleIcon from 'vue-material-design-icons/CircleOutline.vue'
import CloseIcon from 'vue-material-design-icons/Close.vue'
-import EditIcon from 'vue-material-design-icons/Pencil.vue'
+import EditIcon from 'vue-material-design-icons/PencilOutline.vue'
import EmailIcon from 'vue-material-design-icons/Email.vue'
import LinkIcon from 'vue-material-design-icons/Link.vue'
import GroupIcon from 'vue-material-design-icons/AccountGroup.vue'
@@ -399,6 +400,9 @@ export default {
}
}
},
+ allPermissions() {
+ return this.isFolder ? this.bundledPermissions.ALL.toString() : this.bundledPermissions.ALL_FILE.toString()
+ },
/**
* Can the sharee edit the shared file ?
*/
@@ -496,26 +500,6 @@ export default {
},
},
/**
- * Is the current share password protected ?
- *
- * @return {boolean}
- */
- isPasswordProtected: {
- get() {
- return this.config.enforcePasswordForPublicLink
- || !!this.share.password
- },
- async set(enabled) {
- if (enabled) {
- this.share.password = await GeneratePassword(true)
- this.$set(this.share, 'newPassword', this.share.password)
- } else {
- this.share.password = ''
- this.$delete(this.share, 'newPassword')
- }
- },
- },
- /**
* Is the current share a folder ?
*
* @return {boolean}
@@ -731,8 +715,15 @@ export default {
[ATOMIC_PERMISSIONS.DELETE]: this.t('files_sharing', 'Delete'),
}
- return [ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.CREATE, ATOMIC_PERMISSIONS.UPDATE, ...(this.resharingIsPossible ? [ATOMIC_PERMISSIONS.SHARE] : []), ATOMIC_PERMISSIONS.DELETE]
- .filter((permission) => hasPermissions(this.share.permissions, permission))
+ const permissionsList = [
+ ATOMIC_PERMISSIONS.READ,
+ ...(this.isFolder ? [ATOMIC_PERMISSIONS.CREATE] : []),
+ ATOMIC_PERMISSIONS.UPDATE,
+ ...(this.resharingIsPossible ? [ATOMIC_PERMISSIONS.SHARE] : []),
+ ...(this.isFolder ? [ATOMIC_PERMISSIONS.DELETE] : []),
+ ]
+
+ return permissionsList.filter((permission) => hasPermissions(this.share.permissions, permission))
.map((permission, index) => index === 0
? translatedPermissions[permission]
: translatedPermissions[permission].toLocaleLowerCase(getLanguage()))
@@ -850,6 +841,13 @@ export default {
isReshareChecked = this.canReshare,
} = {}) {
// calc permissions if checked
+
+ if (!this.isFolder && (isCreateChecked || isDeleteChecked)) {
+ logger.debug('Ignoring create/delete permissions for file share — only available for folders')
+ isCreateChecked = false
+ isDeleteChecked = false
+ }
+
const permissions = 0
| (isReadChecked ? ATOMIC_PERMISSIONS.READ : 0)
| (isCreateChecked ? ATOMIC_PERMISSIONS.CREATE : 0)
@@ -872,7 +870,7 @@ export default {
async initializeAttributes() {
if (this.isNewShare) {
- if (this.isPasswordEnforced && this.isPublicShare) {
+ if ((this.config.enableLinkPasswordByDefault || this.isPasswordEnforced) && this.isPublicShare) {
this.$set(this.share, 'newPassword', await GeneratePassword(true))
this.advancedSectionAccordionExpanded = true
}
@@ -906,8 +904,9 @@ export default {
this.advancedSectionAccordionExpanded = true
}
- if (this.share.note) {
+ if (this.isValidShareAttribute(this.share.note)) {
this.writeNoteToRecipientIsChecked = true
+ this.advancedSectionAccordionExpanded = true
}
},
@@ -973,10 +972,7 @@ export default {
this.share.note = ''
}
if (this.isPasswordProtected) {
- if (this.hasUnsavedPassword && this.isValidShareAttribute(this.share.newPassword)) {
- this.share.password = this.share.newPassword
- this.$delete(this.share, 'newPassword')
- } else if (this.isPasswordEnforced && this.isNewShare && !this.isValidShareAttribute(this.share.password)) {
+ if (this.isPasswordEnforced && this.isNewShare && !this.isValidShareAttribute(this.share.password)) {
this.passwordError = true
}
} else {
@@ -1000,11 +996,19 @@ export default {
incomingShare.expireDate = this.hasExpirationDate ? this.share.expireDate : ''
if (this.isPasswordProtected) {
- incomingShare.password = this.share.password
+ incomingShare.password = this.share.newPassword
+ }
+
+ let share
+ try {
+ this.creating = true
+ share = await this.addShare(incomingShare)
+ } catch (error) {
+ this.creating = false
+ // Error is already handled by ShareRequests mixin
+ return
}
- this.creating = true
- const share = await this.addShare(incomingShare)
// ugly hack to make code work - we need the id to be set but at the same time we need to keep values we want to update
this.share._share.id = share.id
await this.queueUpdate(...permissionsAndAttributes)
@@ -1018,14 +1022,14 @@ export default {
}
}
}
+
this.share = share
this.creating = false
this.$emit('add:share', this.share)
} else {
// Let's update after creation as some attrs are only available after creation
+ await this.queueUpdate(...permissionsAndAttributes)
this.$emit('update:share', this.share)
- emit('update:share', this.share)
- this.queueUpdate(...permissionsAndAttributes)
}
await this.getNode()
@@ -1102,10 +1106,6 @@ export default {
* "sendPasswordByTalk".
*/
onPasswordProtectedByTalkChange() {
- if (this.hasUnsavedPassword) {
- this.share.password = this.share.newPassword.trim()
- }
-
this.queueUpdate('sendPasswordByTalk', 'password')
},
isValidShareAttribute(value) {
diff --git a/apps/files_sharing/src/views/SharingLinkList.vue b/apps/files_sharing/src/views/SharingLinkList.vue
index 3dd6fdf317b..c3d9a7f83dc 100644
--- a/apps/files_sharing/src/views/SharingLinkList.vue
+++ b/apps/files_sharing/src/views/SharingLinkList.vue
@@ -7,12 +7,6 @@
<ul v-if="canLinkShare"
:aria-label="t('files_sharing', 'Link shares')"
class="sharing-link-list">
- <!-- If no link shares, show the add link default entry -->
- <SharingEntryLink v-if="!hasLinkShares && canReshare"
- :can-reshare="canReshare"
- :file-info="fileInfo"
- @add:share="addShare" />
-
<!-- Else we display the list -->
<template v-if="hasShares">
<!-- using shares[index] to work with .sync -->
@@ -27,6 +21,12 @@
@remove:share="removeShare"
@open-sharing-details="openSharingDetails(share)" />
</template>
+
+ <!-- If no link shares, show the add link default entry -->
+ <SharingEntryLink v-if="!hasLinkShares && canReshare"
+ :can-reshare="canReshare"
+ :file-info="fileInfo"
+ @add:share="addShare" />
</ul>
</template>
diff --git a/apps/files_sharing/src/views/SharingTab.vue b/apps/files_sharing/src/views/SharingTab.vue
index 9caa1a0973a..dc200c61df4 100644
--- a/apps/files_sharing/src/views/SharingTab.vue
+++ b/apps/files_sharing/src/views/SharingTab.vue
@@ -50,7 +50,7 @@
:link-shares="linkShares"
:reshare="reshare"
:shares="shares"
- :placeholder="t('files_sharing', 'Share with accounts and teams')"
+ :placeholder="internalShareInputPlaceholder"
@open-sharing-details="toggleShareDetailsView" />
<!-- other shares list -->
@@ -90,12 +90,17 @@
:file-info="fileInfo"
:link-shares="linkShares"
:is-external="true"
- :placeholder="t('files_sharing', 'Email, federated cloud id')"
+ :placeholder="externalShareInputPlaceholder"
:reshare="reshare"
:shares="shares"
@open-sharing-details="toggleShareDetailsView" />
+ <!-- Non link external shares list -->
+ <SharingList v-if="!loading"
+ :shares="externalShares"
+ :file-info="fileInfo"
+ @open-sharing-details="toggleShareDetailsView" />
<!-- link shares list -->
- <SharingLinkList v-if="!loading"
+ <SharingLinkList v-if="!loading && isLinkSharingAllowed"
ref="linkShareList"
:can-reshare="canReshare"
:file-info="fileInfo"
@@ -133,7 +138,7 @@
<div v-if="projectsEnabled"
v-show="!showSharingDetailsView && fileInfo"
class="sharingTab__additionalContent">
- <CollectionList :id="`${fileInfo.id}`"
+ <NcCollectionList :id="`${fileInfo.id}`"
type="file"
:name="fileInfo.name" />
</div>
@@ -152,19 +157,20 @@
<script>
import { getCurrentUser } from '@nextcloud/auth'
+import { getCapabilities } from '@nextcloud/capabilities'
import { orderBy } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { generateOcsUrl } from '@nextcloud/router'
-import { CollectionList } from 'nextcloud-vue-collections'
import { ShareType } from '@nextcloud/sharing'
-import InfoIcon from 'vue-material-design-icons/Information.vue'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCollectionList from '@nextcloud/vue/components/NcCollectionList'
import NcPopover from '@nextcloud/vue/components/NcPopover'
+import InfoIcon from 'vue-material-design-icons/InformationOutline.vue'
import axios from '@nextcloud/axios'
import moment from '@nextcloud/moment'
-import NcAvatar from '@nextcloud/vue/components/NcAvatar'
-import NcButton from '@nextcloud/vue/components/NcButton'
import { shareWithTitle } from '../utils/SharedWithMe.js'
@@ -180,15 +186,16 @@ import SharingList from './SharingList.vue'
import SharingDetailsTab from './SharingDetailsTab.vue'
import ShareDetails from '../mixins/ShareDetails.js'
+import logger from '../services/logger.ts'
export default {
name: 'SharingTab',
components: {
- CollectionList,
InfoIcon,
NcAvatar,
NcButton,
+ NcCollectionList,
NcPopover,
SharingEntryInternal,
SharingEntrySimple,
@@ -215,6 +222,7 @@ export default {
sharedWithMe: {},
shares: [],
linkShares: [],
+ externalShares: [],
sections: OCA.Sharing.ShareTabSections.getSections(),
projectsEnabled: loadState('core', 'projects_enabled', false),
@@ -235,13 +243,44 @@ export default {
* @return {boolean}
*/
isSharedWithMe() {
- return Object.keys(this.sharedWithMe).length > 0
+ return !!this.sharedWithMe?.user
+ },
+
+ /**
+ * Is link sharing allowed for the current user?
+ *
+ * @return {boolean}
+ */
+ isLinkSharingAllowed() {
+ const currentUser = getCurrentUser()
+ if (!currentUser) {
+ return false
+ }
+
+ const capabilities = getCapabilities()
+ const publicSharing = capabilities.files_sharing?.public || {}
+ return publicSharing.enabled === true
},
canReshare() {
return !!(this.fileInfo.permissions & OC.PERMISSION_SHARE)
|| !!(this.reshare && this.reshare.hasSharePermission && this.config.isResharingAllowed)
},
+
+ internalShareInputPlaceholder() {
+ return this.config.showFederatedSharesAsInternal
+ ? t('files_sharing', 'Share with accounts, teams, federated cloud IDs')
+ : t('files_sharing', 'Share with accounts and teams')
+ },
+
+ externalShareInputPlaceholder() {
+ if (!this.isLinkSharingAllowed) {
+ return t('files_sharing', 'Federated cloud ID')
+ }
+ return this.config.showFederatedSharesAsInternal
+ ? t('files_sharing', 'Email')
+ : t('files_sharing', 'Email, federated cloud ID')
+ },
},
methods: {
@@ -358,11 +397,29 @@ export default {
],
)
- this.linkShares = shares.filter(share => share.type === ShareType.Link || share.type === ShareType.Email)
- this.shares = shares.filter(share => share.type !== ShareType.Link && share.type !== ShareType.Email)
+ for (const share of shares) {
+ if ([ShareType.Link, ShareType.Email].includes(share.type)) {
+ this.linkShares.push(share)
+ } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) {
+ if (this.config.showFederatedSharesToTrustedServersAsInternal) {
+ if (share.isTrustedServer) {
+ this.shares.push(share)
+ } else {
+ this.externalShares.push(share)
+ }
+ } else if (this.config.showFederatedSharesAsInternal) {
+ this.shares.push(share)
+ } else {
+ this.externalShares.push(share)
+ }
+ } else {
+ this.shares.push(share)
+ }
+ }
- console.debug('Processed', this.linkShares.length, 'link share(s)')
- console.debug('Processed', this.shares.length, 'share(s)')
+ logger.debug(`Processed ${this.linkShares.length} link share(s)`)
+ logger.debug(`Processed ${this.shares.length} share(s)`)
+ logger.debug(`Processed ${this.externalShares.length} external share(s)`)
}
},
@@ -423,6 +480,16 @@ export default {
// meaning: not from the ShareInput
if (share.type === ShareType.Email) {
this.linkShares.unshift(share)
+ } else if ([ShareType.Remote, ShareType.RemoteGroup].includes(share.type)) {
+ if (this.config.showFederatedSharesAsInternal) {
+ this.shares.unshift(share)
+ } if (this.config.showFederatedSharesToTrustedServersAsInternal) {
+ if (share.isTrustedServer) {
+ this.shares.unshift(share)
+ }
+ } else {
+ this.externalShares.unshift(share)
+ }
} else {
this.shares.unshift(share)
}