|
@@ -0,0 +1,769 @@
|
|
|
+<!--
|
|
|
+ - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
|
|
+ -
|
|
|
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
|
|
|
+ -
|
|
|
+ - @license GNU AGPL version 3 or any later version
|
|
|
+ -
|
|
|
+ - 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/>.
|
|
|
+ -
|
|
|
+ -->
|
|
|
+
|
|
|
+<template>
|
|
|
+ <li :class="{'sharing-entry--share': share}" class="sharing-entry sharing-entry__link">
|
|
|
+ <Avatar :is-no-user="true"
|
|
|
+ :class="isEmailShareType ? 'icon-mail-white' : 'icon-public-white'"
|
|
|
+ class="sharing-entry__avatar" />
|
|
|
+ <div class="sharing-entry__desc">
|
|
|
+ <h5>{{ title }}</h5>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- clipboard -->
|
|
|
+ <Actions v-if="share && !isEmailShareType && share.token"
|
|
|
+ ref="copyButton"
|
|
|
+ class="sharing-entry__copy">
|
|
|
+ <ActionLink :href="shareLink"
|
|
|
+ target="_blank"
|
|
|
+ :icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
|
|
|
+ @click.stop.prevent="copyLink">
|
|
|
+ {{ clipboardTooltip }}
|
|
|
+ </ActionLink>
|
|
|
+ </Actions>
|
|
|
+
|
|
|
+ <!-- pending actions -->
|
|
|
+ <Actions v-if="!loading && (pendingPassword || pendingExpirationDate)"
|
|
|
+ class="sharing-entry__actions"
|
|
|
+ menu-align="right"
|
|
|
+ :open.sync="open"
|
|
|
+ @close="onNewLinkShare">
|
|
|
+ <!-- pending data menu -->
|
|
|
+ <ActionText v-if="errors.pending"
|
|
|
+ icon="icon-error"
|
|
|
+ :class="{ error: errors.pending}">
|
|
|
+ {{ errors.pending }}
|
|
|
+ </ActionText>
|
|
|
+ <ActionText v-else icon="icon-info">
|
|
|
+ {{ t('files_sharing', 'Please enter the following required information before creating the share') }}
|
|
|
+ </ActionText>
|
|
|
+
|
|
|
+ <!-- password -->
|
|
|
+ <ActionText v-if="pendingPassword" icon="icon-password">
|
|
|
+ {{ t('files_sharing', 'Password protection (enforced)') }}
|
|
|
+ </ActionText>
|
|
|
+ <ActionCheckbox v-else-if="config.enableLinkPasswordByDefault"
|
|
|
+ :checked.sync="isPasswordProtected"
|
|
|
+ :disabled="config.enforcePasswordForPublicLink || saving"
|
|
|
+ class="share-link-password-checkbox"
|
|
|
+ @uncheck="onPasswordDisable">
|
|
|
+ {{ t('files_sharing', 'Password protection') }}
|
|
|
+ </ActionCheckbox>
|
|
|
+ <ActionInput v-if="pendingPassword || share.password"
|
|
|
+ v-tooltip.auto="{
|
|
|
+ content: errors.password,
|
|
|
+ show: errors.password,
|
|
|
+ trigger: 'manual',
|
|
|
+ defaultContainer: '#app-sidebar'
|
|
|
+ }"
|
|
|
+ class="share-link-password"
|
|
|
+ :value.sync="share.password"
|
|
|
+ :disabled="saving"
|
|
|
+ :required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink"
|
|
|
+ :minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength"
|
|
|
+ icon=""
|
|
|
+ autocomplete="new-password"
|
|
|
+ @submit="onNewLinkShare">
|
|
|
+ {{ t('files_sharing', 'Enter a password') }}
|
|
|
+ </ActionInput>
|
|
|
+
|
|
|
+ <!-- expiration date -->
|
|
|
+ <ActionText v-if="pendingExpirationDate" icon="icon-calendar-dark">
|
|
|
+ {{ t('files_sharing', 'Expiration date (enforced)') }}
|
|
|
+ </ActionText>
|
|
|
+ <ActionInput v-if="pendingExpirationDate"
|
|
|
+ v-model="share.expireDate"
|
|
|
+ v-tooltip.auto="{
|
|
|
+ content: errors.expireDate,
|
|
|
+ show: errors.expireDate,
|
|
|
+ trigger: 'manual',
|
|
|
+ defaultContainer: '#app-sidebar'
|
|
|
+ }"
|
|
|
+ class="share-link-expire-date"
|
|
|
+ :disabled="saving"
|
|
|
+ :first-day-of-week="firstDay"
|
|
|
+ :lang="lang"
|
|
|
+ icon=""
|
|
|
+ type="date"
|
|
|
+ :not-before="dateTomorrow"
|
|
|
+ :not-after="dateMaxEnforced">
|
|
|
+ <!-- let's not submit when picked, the user
|
|
|
+ might want to still edit or copy the password -->
|
|
|
+ {{ t('files_sharing', 'Enter a date') }}
|
|
|
+ </ActionInput>
|
|
|
+
|
|
|
+ <ActionButton icon="icon-close" @click.prevent.stop="onCancel">
|
|
|
+ {{ t('files_sharing', 'Cancel') }}
|
|
|
+ </ActionButton>
|
|
|
+ </Actions>
|
|
|
+
|
|
|
+ <!-- actions -->
|
|
|
+ <Actions v-else-if="!loading"
|
|
|
+ class="sharing-entry__actions"
|
|
|
+ menu-align="right"
|
|
|
+ :open.sync="open"
|
|
|
+ @close="onPasswordSubmit">
|
|
|
+ <template v-if="share">
|
|
|
+ <template v-if="isShareOwner">
|
|
|
+ <!-- folder -->
|
|
|
+ <template v-if="isFolder && fileHasCreatePermission && config.isPublicUploadEnabled">
|
|
|
+ <ActionRadio :checked="share.permissions === publicUploadRValue"
|
|
|
+ :value="publicUploadRValue"
|
|
|
+ :name="randomId"
|
|
|
+ :disabled="saving"
|
|
|
+ @change="togglePermissions">
|
|
|
+ {{ t('files_sharing', 'Read only') }}
|
|
|
+ </ActionRadio>
|
|
|
+ <ActionRadio :checked="share.permissions === publicUploadRWValue"
|
|
|
+ :value="publicUploadRWValue"
|
|
|
+ :disabled="saving"
|
|
|
+ :name="randomId"
|
|
|
+ @change="togglePermissions">
|
|
|
+ {{ t('files_sharing', 'Allow upload and editing') }}
|
|
|
+ </ActionRadio>
|
|
|
+ <ActionRadio :checked="share.permissions === publicUploadWValue"
|
|
|
+ :value="publicUploadWValue"
|
|
|
+ :disabled="saving"
|
|
|
+ :name="randomId"
|
|
|
+ class="sharing-entry__action--public-upload"
|
|
|
+ @change="togglePermissions">
|
|
|
+ {{ t('files_sharing', 'File drop (upload only)') }}
|
|
|
+ </ActionRadio>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- file -->
|
|
|
+ <ActionCheckbox v-else
|
|
|
+ :checked.sync="canUpdate"
|
|
|
+ :disabled="saving"
|
|
|
+ @change="queueUpdate('permissions')">
|
|
|
+ {{ t('files_sharing', 'Allow editing') }}
|
|
|
+ </ActionCheckbox>
|
|
|
+
|
|
|
+ <ActionCheckbox
|
|
|
+ :checked.sync="share.hideDownload"
|
|
|
+ :disabled="saving"
|
|
|
+ @change="queueUpdate('hideDownload')">
|
|
|
+ {{ t('files_sharing', 'Hide download') }}
|
|
|
+ </ActionCheckbox>
|
|
|
+
|
|
|
+ <!-- password -->
|
|
|
+ <ActionCheckbox :checked.sync="isPasswordProtected"
|
|
|
+ :disabled="config.enforcePasswordForPublicLink || saving"
|
|
|
+ class="share-link-password-checkbox"
|
|
|
+ @uncheck="onPasswordDisable">
|
|
|
+ {{ config.enforcePasswordForPublicLink
|
|
|
+ ? t('files_sharing', 'Password protection (enforced)')
|
|
|
+ : t('files_sharing', 'Password protect') }}
|
|
|
+ </ActionCheckbox>
|
|
|
+ <ActionInput v-if="isPasswordProtected"
|
|
|
+ ref="password"
|
|
|
+ v-tooltip.auto="{
|
|
|
+ content: errors.password,
|
|
|
+ show: errors.password,
|
|
|
+ trigger: 'manual',
|
|
|
+ defaultContainer: '#app-sidebar'
|
|
|
+ }"
|
|
|
+ class="share-link-password"
|
|
|
+ :class="{ error: errors.password}"
|
|
|
+ :disabled="saving"
|
|
|
+ :required="config.enforcePasswordForPublicLink"
|
|
|
+ :value="hasUnsavedPassword ? share.newPassword : '***************'"
|
|
|
+ icon="icon-password"
|
|
|
+ autocomplete="new-password"
|
|
|
+ :type="hasUnsavedPassword ? 'text': 'password'"
|
|
|
+ @update:value="onPasswordChange"
|
|
|
+ @submit="onPasswordSubmit">
|
|
|
+ {{ t('files_sharing', 'Enter a password') }}
|
|
|
+ </ActionInput>
|
|
|
+
|
|
|
+ <!-- expiration date -->
|
|
|
+ <ActionCheckbox :checked.sync="hasExpirationDate"
|
|
|
+ :disabled="config.isDefaultExpireDateEnforced || saving"
|
|
|
+ class="share-link-expire-date-checkbox"
|
|
|
+ @uncheck="onExpirationDisable">
|
|
|
+ {{ config.isDefaultExpireDateEnforced
|
|
|
+ ? t('files_sharing', 'Expiration date (enforced)')
|
|
|
+ : t('files_sharing', 'Set expiration date') }}
|
|
|
+ </ActionCheckbox>
|
|
|
+ <ActionInput v-if="hasExpirationDate"
|
|
|
+ ref="expireDate"
|
|
|
+ v-tooltip.auto="{
|
|
|
+ content: errors.expireDate,
|
|
|
+ show: errors.expireDate,
|
|
|
+ trigger: 'manual',
|
|
|
+ defaultContainer: '#app-sidebar'
|
|
|
+ }"
|
|
|
+ class="share-link-expire-date"
|
|
|
+ :class="{ error: errors.expireDate}"
|
|
|
+ :disabled="saving"
|
|
|
+ :first-day-of-week="firstDay"
|
|
|
+ :lang="lang"
|
|
|
+ :value="share.expireDate"
|
|
|
+ icon="icon-calendar-dark"
|
|
|
+ type="date"
|
|
|
+ :not-before="dateTomorrow"
|
|
|
+ :not-after="dateMaxEnforced"
|
|
|
+ @update:value="onExpirationChange">
|
|
|
+ {{ t('files_sharing', 'Enter a date') }}
|
|
|
+ </ActionInput>
|
|
|
+
|
|
|
+ <!-- note -->
|
|
|
+ <ActionCheckbox :checked.sync="hasNote"
|
|
|
+ :disabled="saving"
|
|
|
+ @uncheck="queueUpdate('note')">
|
|
|
+ {{ t('files_sharing', 'Note to recipient') }}
|
|
|
+ </ActionCheckbox>
|
|
|
+ <ActionTextEditable v-if="hasNote"
|
|
|
+ ref="note"
|
|
|
+ v-tooltip.auto="{
|
|
|
+ content: errors.note,
|
|
|
+ show: errors.note,
|
|
|
+ trigger: 'manual',
|
|
|
+ defaultContainer: '#app-sidebar'
|
|
|
+ }"
|
|
|
+ :class="{ error: errors.note}"
|
|
|
+ :disabled="saving"
|
|
|
+ :value.sync="share.note"
|
|
|
+ icon="icon-edit"
|
|
|
+ @update:value="debounceQueueUpdate('note')" />
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <components :is="action" v-for="(action, index) in externalActions" :key="index" />
|
|
|
+
|
|
|
+ <ActionButton icon="icon-delete" :disabled="saving" @click.prevent="onDelete">
|
|
|
+ {{ t('files_sharing', 'Delete share') }}
|
|
|
+ </ActionButton>
|
|
|
+ <ActionButton v-if="!isEmailShareType && canReshare"
|
|
|
+ class="new-share-link"
|
|
|
+ icon="icon-add"
|
|
|
+ @click.prevent.stop="onNewLinkShare">
|
|
|
+ {{ t('files_sharing', 'Add another link') }}
|
|
|
+ </ActionButton>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- Create new share -->
|
|
|
+ <ActionButton v-else-if="canReshare"
|
|
|
+ class="new-share-link"
|
|
|
+ icon="icon-add"
|
|
|
+ @click.prevent.stop="onNewLinkShare">
|
|
|
+ {{ t('files_sharing', 'Create a new share link') }}
|
|
|
+ </ActionButton>
|
|
|
+ </Actions>
|
|
|
+
|
|
|
+ <!-- loading indicator to replace the menu -->
|
|
|
+ <div v-else class="icon-loading-small sharing-entry__loading" />
|
|
|
+ </li>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { generateUrl } from '@nextcloud/router'
|
|
|
+import axios from '@nextcloud/axios'
|
|
|
+
|
|
|
+import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
|
|
|
+import ActionCheckbox from 'nextcloud-vue/dist/Components/ActionCheckbox'
|
|
|
+import ActionRadio from 'nextcloud-vue/dist/Components/ActionRadio'
|
|
|
+import ActionInput from 'nextcloud-vue/dist/Components/ActionInput'
|
|
|
+import ActionText from 'nextcloud-vue/dist/Components/ActionText'
|
|
|
+import ActionTextEditable from 'nextcloud-vue/dist/Components/ActionTextEditable'
|
|
|
+import ActionLink from 'nextcloud-vue/dist/Components/ActionLink'
|
|
|
+import Actions from 'nextcloud-vue/dist/Components/Actions'
|
|
|
+import Avatar from 'nextcloud-vue/dist/Components/Avatar'
|
|
|
+import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
|
|
|
+
|
|
|
+import Share from '../models/Share'
|
|
|
+import SharesMixin from '../mixins/SharesMixin'
|
|
|
+
|
|
|
+const passwordSet = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'SharingEntryLink',
|
|
|
+
|
|
|
+ components: {
|
|
|
+ Actions,
|
|
|
+ ActionButton,
|
|
|
+ ActionCheckbox,
|
|
|
+ ActionRadio,
|
|
|
+ ActionInput,
|
|
|
+ ActionLink,
|
|
|
+ ActionText,
|
|
|
+ ActionTextEditable,
|
|
|
+ Avatar
|
|
|
+ },
|
|
|
+
|
|
|
+ directives: {
|
|
|
+ Tooltip
|
|
|
+ },
|
|
|
+
|
|
|
+ mixins: [SharesMixin],
|
|
|
+
|
|
|
+ props: {
|
|
|
+ canReshare: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ copySuccess: true,
|
|
|
+ copied: false,
|
|
|
+
|
|
|
+ publicUploadRWValue: OC.PERMISSION_UPDATE | OC.PERMISSION_CREATE | OC.PERMISSION_READ | OC.PERMISSION_DELETE,
|
|
|
+ publicUploadRValue: OC.PERMISSION_READ,
|
|
|
+ publicUploadWValue: OC.PERMISSION_CREATE,
|
|
|
+
|
|
|
+ ExternalLinkActions: OCA.Sharing.ExternalLinkActions.state
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ computed: {
|
|
|
+ /**
|
|
|
+ * Generate a unique random id for this SharingEntryLink only
|
|
|
+ * This allows ActionRadios to have the same name prop
|
|
|
+ * but not to impact others SharingEntryLink
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+ randomId() {
|
|
|
+ return Math.random().toString(27).substr(2)
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Link share label
|
|
|
+ * TODO: allow editing
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+ title() {
|
|
|
+ // if we have a valid existing share (not pending)
|
|
|
+ if (this.share && this.share.id) {
|
|
|
+ if (!this.isShareOwner && this.share.ownerDisplayName) {
|
|
|
+ return t('files_sharing', 'Shared via link by {initiator}', {
|
|
|
+ initiator: this.share.ownerDisplayName
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (this.share.label && this.share.label.trim() !== '') {
|
|
|
+ return this.share.label
|
|
|
+ }
|
|
|
+ if (this.isEmailShareType) {
|
|
|
+ return this.share.shareWith
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return t('files_sharing', 'Share link')
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the current share password protected ?
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+ isPasswordProtected: {
|
|
|
+ get: function() {
|
|
|
+ return this.config.enforcePasswordForPublicLink
|
|
|
+ || !!this.share.password
|
|
|
+ },
|
|
|
+ set: async function(enabled) {
|
|
|
+ // TODO: directly save after generation to make sure the share is always protected
|
|
|
+ this.share.password = enabled ? await this.generatePassword() : ''
|
|
|
+ this.share.newPassword = this.share.password
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the current share an email share ?
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+ isEmailShareType() {
|
|
|
+ return this.share
|
|
|
+ ? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
|
|
|
+ : false
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Pending data.
|
|
|
+ * If the share still doesn't have an id, it is not synced
|
|
|
+ * Therefore this is still not valid and requires user input
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+ pendingPassword() {
|
|
|
+ return this.config.enforcePasswordForPublicLink && this.share && !this.share.id
|
|
|
+ },
|
|
|
+ pendingExpirationDate() {
|
|
|
+ return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Can the recipient edit the file ?
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+ canUpdate: {
|
|
|
+ get: function() {
|
|
|
+ return this.share.hasUpdatePermission
|
|
|
+ },
|
|
|
+ set: function(enabled) {
|
|
|
+ this.share.permissions = enabled
|
|
|
+ ? OC.PERMISSION_READ | OC.PERMISSION_UPDATE
|
|
|
+ : OC.PERMISSION_READ
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // if newPassword exists, but is empty, it means
|
|
|
+ // the user deleted the original password
|
|
|
+ hasUnsavedPassword() {
|
|
|
+ return this.share.newPassword !== undefined
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the current share a folder ?
|
|
|
+ * TODO: move to a proper FileInfo model?
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+ isFolder() {
|
|
|
+ return this.fileInfo.type === 'dir'
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Does the current file/folder have create permissions
|
|
|
+ * TODO: move to a proper FileInfo model?
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+ fileHasCreatePermission() {
|
|
|
+ return !!(this.fileInfo.permissions & OC.PERMISSION_CREATE)
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Return the public share link
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+ shareLink() {
|
|
|
+ return window.location.protocol + '//' + window.location.host + generateUrl('/s/') + this.share.token
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Clipboard v-tooltip message
|
|
|
+ * @returns {string}
|
|
|
+ */
|
|
|
+ clipboardTooltip() {
|
|
|
+ if (this.copied) {
|
|
|
+ return this.copySuccess
|
|
|
+ ? t('files_sharing', 'Link copied')
|
|
|
+ : t('files_sharing', 'Cannot copy, please copy the link manually')
|
|
|
+ }
|
|
|
+ return t('files_sharing', 'Copy to clipboard')
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * External aditionnal actions for the menu
|
|
|
+ * @returns {Array}
|
|
|
+ */
|
|
|
+ externalActions() {
|
|
|
+ return this.ExternalLinkActions.actions
|
|
|
+ },
|
|
|
+
|
|
|
+ isPasswordPolicyEnabled() {
|
|
|
+ return typeof this.config.passwordPolicy === 'object'
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * Create a new share link and append it to the list
|
|
|
+ */
|
|
|
+ async onNewLinkShare() {
|
|
|
+ const shareDefaults = {
|
|
|
+ share_type: OC.Share.SHARE_TYPE_LINK
|
|
|
+ }
|
|
|
+ if (this.config.isDefaultExpireDateEnforced) {
|
|
|
+ // default is empty string if not set
|
|
|
+ // expiration is the share object key, not expireDate
|
|
|
+ shareDefaults.expiration = this.config.defaultExpirationDateString
|
|
|
+ }
|
|
|
+ if (this.config.enableLinkPasswordByDefault) {
|
|
|
+ shareDefaults.password = await this.generatePassword()
|
|
|
+ }
|
|
|
+
|
|
|
+ // do not push yet if we need a password or an expiration date
|
|
|
+ if (this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced) {
|
|
|
+ this.loading = true
|
|
|
+ // if a share already exists, pushing it
|
|
|
+ if (this.share && !this.share.id) {
|
|
|
+ if (this.checkShare(this.share)) {
|
|
|
+ await this.pushNewLinkShare(this.share, true)
|
|
|
+ return true
|
|
|
+ } else {
|
|
|
+ this.open = true
|
|
|
+ OC.Notification.showTemporary(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ELSE, show the pending popovermenu
|
|
|
+ // if password enforced, pre-fill with random one
|
|
|
+ if (this.config.enforcePasswordForPublicLink) {
|
|
|
+ shareDefaults.password = await this.generatePassword()
|
|
|
+ }
|
|
|
+
|
|
|
+ // create share & close menu
|
|
|
+ const share = new Share(shareDefaults)
|
|
|
+ const component = await new Promise(resolve => {
|
|
|
+ this.$emit('add:share', share, resolve)
|
|
|
+ })
|
|
|
+
|
|
|
+ // open the menu on the
|
|
|
+ // freshly created share component
|
|
|
+ this.open = false
|
|
|
+ this.loading = false
|
|
|
+ component.open = true
|
|
|
+
|
|
|
+ // Nothing enforced, creating share directly
|
|
|
+ } else {
|
|
|
+ const share = new Share(shareDefaults)
|
|
|
+ await this.pushNewLinkShare(share)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Push a new link share to the server
|
|
|
+ * And update or append to the list
|
|
|
+ * accordingly
|
|
|
+ *
|
|
|
+ * @param {Share} share the new share
|
|
|
+ * @param {boolean} [update=false] do we update the current share ?
|
|
|
+ */
|
|
|
+ async pushNewLinkShare(share, update) {
|
|
|
+ try {
|
|
|
+ this.loading = true
|
|
|
+ this.errors = {}
|
|
|
+
|
|
|
+ const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
|
|
|
+ const newShare = await this.createShare({
|
|
|
+ path,
|
|
|
+ shareType: OC.Share.SHARE_TYPE_LINK,
|
|
|
+ password: share.password,
|
|
|
+ expireDate: share.expireDate
|
|
|
+ // we do not allow setting the publicUpload
|
|
|
+ // before the share creation.
|
|
|
+ // Todo: We also need to fix the createShare method in
|
|
|
+ // lib/Controller/ShareAPIController.php to allow file drop
|
|
|
+ // (currently not supported on create, only update)
|
|
|
+ })
|
|
|
+
|
|
|
+ this.open = false
|
|
|
+
|
|
|
+ console.debug('Link share created', newShare)
|
|
|
+
|
|
|
+ // if share already exists, copy link directly on next tick
|
|
|
+ let component
|
|
|
+ if (update) {
|
|
|
+ component = await new Promise(resolve => {
|
|
|
+ this.$emit('update:share', newShare, resolve)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // adding new share to the array and copying link to clipboard
|
|
|
+ // using promise so that we can copy link in the same click function
|
|
|
+ // and avoid firefox copy permissions issue
|
|
|
+ component = await new Promise(resolve => {
|
|
|
+ this.$emit('add:share', newShare, resolve)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // Execute the copy link method
|
|
|
+ // freshly created share component
|
|
|
+ // ! somehow does not works on firefox !
|
|
|
+ component.copyLink()
|
|
|
+
|
|
|
+ } catch ({ response }) {
|
|
|
+ const message = response.data.ocs.meta.message
|
|
|
+ if (message.match(/password/i)) {
|
|
|
+ this.onSyncError('password', message)
|
|
|
+ } else if (message.match(/date/i)) {
|
|
|
+ this.onSyncError('expireDate', message)
|
|
|
+ } else {
|
|
|
+ this.onSyncError('pending', message)
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * On permissions change
|
|
|
+ * @param {Event} event js event
|
|
|
+ */
|
|
|
+ togglePermissions(event) {
|
|
|
+ const permissions = parseInt(event.target.value, 10)
|
|
|
+ this.share.permissions = permissions
|
|
|
+ this.queueUpdate('permissions')
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Generate a valid policy password or
|
|
|
+ * request a valid password if password_policy
|
|
|
+ * is enabled
|
|
|
+ *
|
|
|
+ * @returns {string} a valid password
|
|
|
+ */
|
|
|
+ async generatePassword() {
|
|
|
+ // password policy is enabled, let's request a pass
|
|
|
+ if (this.config.passwordPolicy.api && this.config.passwordPolicy.api.generate) {
|
|
|
+ try {
|
|
|
+ const request = await axios.get(this.config.passwordPolicy.api.generate)
|
|
|
+ if (request.data.ocs.data.password) {
|
|
|
+ return request.data.ocs.data.password
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.info('Error generating password from password_policy', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // generate password of 10 length based on passwordSet
|
|
|
+ return Array(10).fill(0)
|
|
|
+ .reduce((prev, curr) => {
|
|
|
+ prev += passwordSet.charAt(Math.floor(Math.random() * passwordSet.length))
|
|
|
+ return prev
|
|
|
+ }, '')
|
|
|
+ },
|
|
|
+
|
|
|
+ async copyLink() {
|
|
|
+ try {
|
|
|
+ await this.$copyText(this.shareLink)
|
|
|
+ // focus and show the tooltip
|
|
|
+ this.$refs.copyButton.$el.focus()
|
|
|
+ this.copySuccess = true
|
|
|
+ this.copied = true
|
|
|
+ } catch (error) {
|
|
|
+ this.copySuccess = false
|
|
|
+ this.copied = true
|
|
|
+ console.error(error)
|
|
|
+ } finally {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.copySuccess = false
|
|
|
+ this.copied = false
|
|
|
+ }, 4000)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Update newPassword values
|
|
|
+ * of share. If password is set but not newPassword
|
|
|
+ * then the user did not changed the password
|
|
|
+ * If both co-exists, the password have changed and
|
|
|
+ * we show it in plain text.
|
|
|
+ * Then on submit (or menu close), we sync it.
|
|
|
+ * @param {string} password the changed password
|
|
|
+ */
|
|
|
+ onPasswordChange(password) {
|
|
|
+ this.$set(this.share, 'newPassword', password)
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Uncheck password protection
|
|
|
+ * We need this method because @update:checked
|
|
|
+ * is ran simultaneously as @uncheck, so
|
|
|
+ * so we cannot ensure data is up-to-date
|
|
|
+ */
|
|
|
+ onPasswordDisable() {
|
|
|
+ this.share.password = ''
|
|
|
+
|
|
|
+ // reset password state after sync
|
|
|
+ this.$delete(this.share, 'newPassword')
|
|
|
+
|
|
|
+ // only update if valid share.
|
|
|
+ if (this.share.id) {
|
|
|
+ this.queueUpdate('password')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Menu have been closed or password has been submited.
|
|
|
+ * The only property that does not get
|
|
|
+ * synced automatically is the password
|
|
|
+ * So let's check if we have an unsaved
|
|
|
+ * password.
|
|
|
+ * expireDate is saved on datepicker pick
|
|
|
+ * or close.
|
|
|
+ */
|
|
|
+ onPasswordSubmit() {
|
|
|
+ if (this.hasUnsavedPassword) {
|
|
|
+ this.share.password = this.share.newPassword
|
|
|
+ this.queueUpdate('password')
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Cancel the share creation
|
|
|
+ * Used in the pending popover
|
|
|
+ */
|
|
|
+ onCancel() {
|
|
|
+ // this.share already exists at this point,
|
|
|
+ // but is incomplete as not pushed to server
|
|
|
+ // YET. We can safely delete the share :)
|
|
|
+ this.$emit('remove:share', this.share)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.sharing-entry {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ height: 44px;
|
|
|
+ &__desc {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px;
|
|
|
+ line-height: 1.2em;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:not(.sharing-entry--share) &__actions {
|
|
|
+ .new-share-link {
|
|
|
+ border-top: 1px solid var(--color-border);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .sharing-entry__action--public-upload {
|
|
|
+ border-bottom: 1px solid var(--color-border);
|
|
|
+ }
|
|
|
+
|
|
|
+ &__loading {
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ margin: 0;
|
|
|
+ padding: 14px;
|
|
|
+ margin-left: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ // put menus to the left
|
|
|
+ // but only the first one
|
|
|
+ .action-item {
|
|
|
+ margin-left: auto;
|
|
|
+ ~ .action-item,
|
|
|
+ ~ .sharing-entry__loading {
|
|
|
+ margin-left: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .icon-checkmark-color {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|