aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/mixins/SharesMixin.js
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src/mixins/SharesMixin.js')
-rw-r--r--apps/files_sharing/src/mixins/SharesMixin.js249
1 files changed, 174 insertions, 75 deletions
diff --git a/apps/files_sharing/src/mixins/SharesMixin.js b/apps/files_sharing/src/mixins/SharesMixin.js
index 2a4637be121..a461da56d85 100644
--- a/apps/files_sharing/src/mixins/SharesMixin.js
+++ b/apps/files_sharing/src/mixins/SharesMixin.js
@@ -1,48 +1,34 @@
/**
- * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Gary Kim <gary@garykim.dev>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { showError, showSuccess } from '@nextcloud/dialogs'
import { getCurrentUser } from '@nextcloud/auth'
-// eslint-disable-next-line import/no-unresolved, node/no-missing-import
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { ShareType } from '@nextcloud/sharing'
+import { emit } from '@nextcloud/event-bus'
+
import PQueue from 'p-queue'
import debounce from 'debounce'
-import Share from '../models/Share.js'
+import GeneratePassword from '../utils/GeneratePassword.ts'
+import Share from '../models/Share.ts'
import SharesRequests from './ShareRequests.js'
-import ShareTypes from './ShareTypes.js'
-import Config from '../services/ConfigService.js'
+import Config from '../services/ConfigService.ts'
+import logger from '../services/logger.ts'
+
+import {
+ BUNDLED_PERMISSIONS,
+} from '../lib/SharePermissionsToolBox.js'
+import { fetchNode } from '../../../files/src/services/WebdavClient.ts'
export default {
- mixins: [SharesRequests, ShareTypes],
+ mixins: [SharesRequests],
props: {
fileInfo: {
type: Object,
- default: () => {},
+ default: () => { },
required: true,
},
share: {
@@ -58,6 +44,8 @@ export default {
data() {
return {
config: new Config(),
+ node: null,
+ ShareType,
// errors helpers
errors: {},
@@ -80,7 +68,9 @@ export default {
},
computed: {
-
+ path() {
+ return (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
+ },
/**
* Does the current share have a note
*
@@ -104,10 +94,10 @@ export default {
// Datepicker language
lang() {
const weekdaysShort = window.dayNamesShort
- ? window.dayNamesShort // provided by nextcloud
+ ? window.dayNamesShort // provided by Nextcloud
: ['Sun.', 'Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fri.', 'Sat.']
const monthsShort = window.monthNamesShort
- ? window.monthNamesShort // provided by nextcloud
+ ? window.monthNamesShort // provided by Nextcloud
: ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.']
const firstDayOfWeek = window.firstDay ? window.firstDay : 0
@@ -121,15 +111,91 @@ export default {
monthFormat: 'MMM',
}
},
-
+ isNewShare() {
+ return !this.share.id
+ },
+ isFolder() {
+ return this.fileInfo.type === 'dir'
+ },
+ isPublicShare() {
+ const shareType = this.share.shareType ?? this.share.type
+ return [ShareType.Link, ShareType.Email].includes(shareType)
+ },
+ isRemoteShare() {
+ return this.share.type === ShareType.RemoteGroup || this.share.type === ShareType.Remote
+ },
isShareOwner() {
return this.share && this.share.owner === getCurrentUser().uid
},
-
+ isExpiryDateEnforced() {
+ if (this.isPublicShare) {
+ return this.config.isDefaultExpireDateEnforced
+ }
+ if (this.isRemoteShare) {
+ return this.config.isDefaultRemoteExpireDateEnforced
+ }
+ return this.config.isDefaultInternalExpireDateEnforced
+ },
+ hasCustomPermissions() {
+ const bundledPermissions = [
+ BUNDLED_PERMISSIONS.ALL,
+ BUNDLED_PERMISSIONS.READ_ONLY,
+ BUNDLED_PERMISSIONS.FILE_DROP,
+ ]
+ return !bundledPermissions.includes(this.share.permissions)
+ },
+ maxExpirationDateEnforced() {
+ if (this.isExpiryDateEnforced) {
+ if (this.isPublicShare) {
+ return this.config.defaultExpirationDate
+ }
+ if (this.isRemoteShare) {
+ return this.config.defaultRemoteExpirationDateString
+ }
+ // If it get's here then it must be an internal share
+ return this.config.defaultInternalExpirationDate
+ }
+ 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: {
/**
+ * Fetch WebDAV node
+ *
+ * @return {Node}
+ */
+ async getNode() {
+ const node = { path: this.path }
+ try {
+ this.node = await fetchNode(node.path)
+ logger.info('Fetched node:', { node: this.node })
+ } catch (error) {
+ logger.error('Error:', error)
+ }
+ },
+
+ /**
* Check if a share is valid before
* firing the request
*
@@ -152,19 +218,7 @@ export default {
},
/**
- * @param {string} date a date with YYYY-MM-DD format
- * @return {Date} date
- */
- parseDateString(date) {
- if (!date) {
- return
- }
- const regex = /([0-9]{4}-[0-9]{2}-[0-9]{2})/i
- return new Date(date.match(regex)?.pop())
- },
-
- /**
- * @param {Date} date
+ * @param {Date} date the date to format
* @return {string} date a date with YYYY-MM-DD format
*/
formatDateToString(date) {
@@ -180,19 +234,13 @@ export default {
* @param {Date} date
*/
onExpirationChange(date) {
- this.share.expireDate = this.formatDateToString(date)
- this.queueUpdate('expireDate')
- },
-
- /**
- * Uncheck expire date
- * We need this method because @update:checked
- * is ran simultaneously as @uncheck, so
- * so we cannot ensure data is up-to-date
- */
- onExpirationDisable() {
- this.share.expireDate = ''
- this.queueUpdate('expireDate')
+ 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)
},
/**
@@ -224,12 +272,14 @@ 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 })
showSuccess(message)
this.$emit('remove:share', this.share)
+ await this.getNode()
+ emit('files:node:updated', this.node)
} catch (error) {
// re-open menu if error
this.open = true
@@ -253,22 +303,30 @@ export default {
const properties = {}
// force value to string because that is what our
// share api controller accepts
- propertyNames.forEach(name => {
- if ((typeof this.share[name]) === 'object') {
+ 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') {
properties[name] = JSON.stringify(this.share[name])
} else {
properties[name] = this.share[name].toString()
}
- })
+ }
- this.updateQueue.add(async () => {
+ return this.updateQueue.add(async () => {
this.saving = true
this.errors = {}
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
@@ -276,18 +334,27 @@ export default {
}
// clear any previous errors
- this.$delete(this.errors, propertyNames[0])
- showSuccess(t('files_sharing', 'Share {propertyName} saved', { propertyName: propertyNames[0] }))
- } catch ({ message }) {
+ 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)
- showError(t('files_sharing', 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
+ showError(t('files_sharing', 'Could not update share'))
}
} finally {
this.saving = false
}
})
- return
}
// This share does not exists on the server yet
@@ -295,12 +362,45 @@ export default {
},
/**
+ * @param {string[]} names Properties changed
+ */
+ updateSuccessMessage(names) {
+ if (names.length !== 1) {
+ return t('files_sharing', 'Share saved')
+ }
+
+ switch (names[0]) {
+ case 'expireDate':
+ return t('files_sharing', 'Share expiry date saved')
+ case 'hideDownload':
+ return t('files_sharing', 'Share hide-download state saved')
+ case 'label':
+ return t('files_sharing', 'Share label saved')
+ case 'note':
+ return t('files_sharing', 'Share note for recipient saved')
+ case 'password':
+ return t('files_sharing', 'Share password saved')
+ case 'permissions':
+ return t('files_sharing', 'Share permissions saved')
+ default:
+ return t('files_sharing', 'Share saved')
+ }
+ },
+
+ /**
* Manage sync errors
*
* @param {string} property the errored property, e.g. 'password'
* @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) {
@@ -335,7 +435,6 @@ export default {
}
}
},
-
/**
* Debounce queueUpdate to avoid requests spamming
* more importantly for text data