462 lines
12 KiB
JavaScript
462 lines
12 KiB
JavaScript
/**
|
|
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
import { getCurrentUser } from '@nextcloud/auth'
|
|
import { showError, showSuccess } from '@nextcloud/dialogs'
|
|
import { emit } from '@nextcloud/event-bus'
|
|
import { ShareType } from '@nextcloud/sharing'
|
|
import debounce from 'debounce'
|
|
import PQueue from 'p-queue'
|
|
import { fetchNode } from '../../../files/src/services/WebdavClient.ts'
|
|
import {
|
|
BUNDLED_PERMISSIONS,
|
|
} from '../lib/SharePermissionsToolBox.js'
|
|
import Share from '../models/Share.ts'
|
|
import Config from '../services/ConfigService.ts'
|
|
import logger from '../services/logger.ts'
|
|
import GeneratePassword from '../utils/GeneratePassword.ts'
|
|
import SharesRequests from './ShareRequests.js'
|
|
|
|
export default {
|
|
mixins: [SharesRequests],
|
|
|
|
props: {
|
|
fileInfo: {
|
|
type: Object,
|
|
default: () => { },
|
|
required: true,
|
|
},
|
|
share: {
|
|
type: Share,
|
|
default: null,
|
|
},
|
|
isUnique: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
config: new Config(),
|
|
node: null,
|
|
ShareType,
|
|
|
|
// errors helpers
|
|
errors: {},
|
|
|
|
// component status toggles
|
|
loading: false,
|
|
saving: false,
|
|
open: false,
|
|
|
|
/** @type {boolean | undefined} */
|
|
passwordProtectedState: undefined,
|
|
|
|
// concurrency management queue
|
|
// we want one queue per share
|
|
updateQueue: new PQueue({ concurrency: 1 }),
|
|
|
|
/**
|
|
* ! This allow vue to make the Share class state reactive
|
|
* ! do not remove it ot you'll lose all reactivity here
|
|
*/
|
|
reactiveState: this.share?.state,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
path() {
|
|
return (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
|
|
},
|
|
/**
|
|
* Does the current share have a note
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
hasNote: {
|
|
get() {
|
|
return this.share.note !== ''
|
|
},
|
|
set(enabled) {
|
|
this.share.note = enabled
|
|
? null // enabled but user did not changed the content yet
|
|
: '' // empty = no note = disabled
|
|
},
|
|
},
|
|
|
|
dateTomorrow() {
|
|
return new Date(new Date().setDate(new Date().getDate() + 1))
|
|
},
|
|
|
|
// Datepicker language
|
|
lang() {
|
|
const weekdaysShort = window.dayNamesShort
|
|
? window.dayNamesShort // provided by Nextcloud
|
|
: ['Sun.', 'Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fri.', 'Sat.']
|
|
const monthsShort = window.monthNamesShort
|
|
? window.monthNamesShort // provided by Nextcloud
|
|
: ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.']
|
|
const firstDayOfWeek = window.firstDay ? window.firstDay : 0
|
|
|
|
return {
|
|
formatLocale: {
|
|
firstDayOfWeek,
|
|
monthsShort,
|
|
weekdaysMin: weekdaysShort,
|
|
weekdaysShort,
|
|
},
|
|
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() {
|
|
if (this.config.enforcePasswordForPublicLink) {
|
|
return true
|
|
}
|
|
if (this.passwordProtectedState !== undefined) {
|
|
return this.passwordProtectedState
|
|
}
|
|
return this.share.newPassword !== undefined
|
|
|| this.share.password !== undefined
|
|
},
|
|
async set(enabled) {
|
|
if (enabled) {
|
|
this.passwordProtectedState = true
|
|
this.$set(this.share, 'newPassword', await GeneratePassword(true))
|
|
} else {
|
|
this.passwordProtectedState = false
|
|
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
|
|
*
|
|
* @param {Share} share the share to check
|
|
* @return {boolean}
|
|
*/
|
|
checkShare(share) {
|
|
if (share.password) {
|
|
if (typeof share.password !== 'string' || share.password.trim() === '') {
|
|
return false
|
|
}
|
|
}
|
|
if (share.newPassword) {
|
|
if (typeof share.newPassword !== 'string') {
|
|
return false
|
|
}
|
|
}
|
|
if (share.expirationDate) {
|
|
const date = share.expirationDate
|
|
if (!date.isValid()) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
},
|
|
|
|
/**
|
|
* @param {Date} date the date to format
|
|
* @return {string} date a date with YYYY-MM-DD format
|
|
*/
|
|
formatDateToString(date) {
|
|
// Force utc time. Drop time information to be timezone-less
|
|
const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
|
// Format to YYYY-MM-DD
|
|
return utcDate.toISOString().split('T')[0]
|
|
},
|
|
|
|
/**
|
|
* Save given value to expireDate and trigger queueUpdate
|
|
*
|
|
* @param {Date} date
|
|
*/
|
|
onExpirationChange(date) {
|
|
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)
|
|
},
|
|
|
|
/**
|
|
* Note changed, let's save it to a different key
|
|
*
|
|
* @param {string} note the share note
|
|
*/
|
|
onNoteChange(note) {
|
|
this.$set(this.share, 'newNote', note.trim())
|
|
},
|
|
|
|
/**
|
|
* When the note change, we trim, save and dispatch
|
|
*
|
|
*/
|
|
onNoteSubmit() {
|
|
if (this.share.newNote) {
|
|
this.share.note = this.share.newNote
|
|
this.$delete(this.share, 'newNote')
|
|
this.queueUpdate('note')
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete share button handler
|
|
*/
|
|
async onDelete() {
|
|
try {
|
|
this.loading = true
|
|
this.open = false
|
|
await this.deleteShare(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 {
|
|
// re-open menu if error
|
|
this.open = true
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Send an update of the share to the queue
|
|
*
|
|
* @param {Array<string>} propertyNames the properties to sync
|
|
*/
|
|
queueUpdate(...propertyNames) {
|
|
if (propertyNames.length === 0) {
|
|
// Nothing to update
|
|
return
|
|
}
|
|
|
|
if (this.share.id) {
|
|
const properties = {}
|
|
// force value to string because that is what our
|
|
// share api controller accepts
|
|
for (const name of propertyNames) {
|
|
if (name === 'password') {
|
|
if (this.share.newPassword !== undefined) {
|
|
properties[name] = this.share.newPassword
|
|
}
|
|
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()
|
|
}
|
|
}
|
|
|
|
return this.updateQueue.add(async () => {
|
|
this.saving = true
|
|
this.errors = {}
|
|
try {
|
|
const updatedShare = await this.updateShare(this.share.id, properties)
|
|
|
|
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
|
|
this.share.passwordExpirationTime = updatedShare.password_expiration_time
|
|
}
|
|
|
|
// clear any previous errors
|
|
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 !== '') {
|
|
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
|
|
}
|
|
})
|
|
}
|
|
|
|
// This share does not exists on the server yet
|
|
logger.debug('Updated local share', { share: this.share })
|
|
},
|
|
|
|
/**
|
|
* @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 !== undefined) {
|
|
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) {
|
|
case 'password':
|
|
case 'pending':
|
|
case 'expireDate':
|
|
case 'label':
|
|
case 'note': {
|
|
// show error
|
|
this.$set(this.errors, property, message)
|
|
|
|
let propertyEl = this.$refs[property]
|
|
if (propertyEl) {
|
|
if (propertyEl.$el) {
|
|
propertyEl = propertyEl.$el
|
|
}
|
|
// focus if there is a focusable action element
|
|
const focusable = propertyEl.querySelector('.focusable')
|
|
if (focusable) {
|
|
focusable.focus()
|
|
}
|
|
}
|
|
break
|
|
}
|
|
case 'sendPasswordByTalk': {
|
|
// show error
|
|
this.$set(this.errors, property, message)
|
|
|
|
// Restore previous state
|
|
this.share.sendPasswordByTalk = !this.share.sendPasswordByTalk
|
|
break
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Debounce queueUpdate to avoid requests spamming
|
|
* more importantly for text data
|
|
*
|
|
* @param {string} property the property to sync
|
|
*/
|
|
debounceQueueUpdate: debounce(function(property) {
|
|
this.queueUpdate(property)
|
|
}, 500),
|
|
},
|
|
}
|