Merge pull request #41936 from nextcloud/41885-27-manual-backport

[stable27] Improve share logic for enforced password & expiry date
This commit is contained in:
F. E Noel Nfebe 2023-11-30 21:46:24 +01:00 committed by GitHub
commit eb8907323e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 221 additions and 197 deletions

View file

@ -97,13 +97,13 @@
</NcActionText>
<NcActionInput v-if="pendingExpirationDate"
class="share-link-expire-date"
:disabled="saving || isExpiryDateEnforced"
:disabled="saving"
:is-native-picker="true"
:hide-label="true"
:value="new Date(share.expireDate)"
type="date"
:min="dateTomorrow"
:max="dateMaxEnforced"
:max="maxExpirationDateEnforced"
@input="onExpirationChange">
<!-- let's not submit when picked, the user
might want to still edit or copy the password -->
@ -302,12 +302,6 @@ export default {
}
return null
},
dateMaxEnforced() {
if (this.config.isDefaultExpireDateEnforced) {
return new Date(new Date().setDate(new Date().getDate() + this.config.defaultExpireDate))
}
return null
},
/**
* Is the current share password protected ?
*

View file

@ -132,6 +132,9 @@ export default {
const shareType = this.share.shareType ?? this.share.type
return [this.SHARE_TYPES.SHARE_TYPE_LINK, this.SHARE_TYPES.SHARE_TYPE_EMAIL].includes(shareType)
},
isRemoteShare() {
return this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP || this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE
},
isShareOwner() {
return this.share && this.share.owner === getCurrentUser().uid
},
@ -152,6 +155,19 @@ export default {
]
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
},
},
methods: {

View file

@ -15,169 +15,179 @@
<h1>{{ title }}</h1>
</span>
</div>
<div class="sharingTabDetailsView__quick-permissions">
<div>
<NcCheckboxRadioSwitch :button-variant="true"
:checked.sync="sharingPermission"
:value="bundledPermissions.READ_ONLY.toString()"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@update:checked="toggleCustomPermissions">
<ViewIcon :size="20" />
<span>{{ t('files_sharing', 'View only') }}</span>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :button-variant="true"
:checked.sync="sharingPermission"
:value="bundledPermissions.ALL.toString()"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@update:checked="toggleCustomPermissions">
<EditIcon :size="20" />
<template v-if="allowsFileDrop">
<span>{{ t('files_sharing', 'Allow upload and editing') }}</span>
</template>
<template v-else>
<span>{{ t('files_sharing', 'Allow editing') }}</span>
</template>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="allowsFileDrop"
:button-variant="true"
:checked.sync="sharingPermission"
:value="bundledPermissions.FILE_DROP.toString()"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@update:checked="toggleCustomPermissions">
<UploadIcon :size="20" />
<span>{{ t('files_sharing', 'File drop') }}</span>
<small>{{ t('files_sharing', 'Upload only') }}</small>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :button-variant="true"
:checked.sync="sharingPermission"
:value="'custom'"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@update:checked="expandCustomPermissions">
<DotsHorizontalIcon :size="20" />
<span>{{ t('files_sharing', 'Custom permissions') }}</span>
<small>{{ customPermissionsList }}</small>
</NcCheckboxRadioSwitch>
<div class="sharingTabDetailsView__wrapper">
<div ref="quickPermissions"
class="sharingTabDetailsView__quick-permissions">
<div>
<NcCheckboxRadioSwitch :button-variant="true"
:checked.sync="sharingPermission"
:value="bundledPermissions.READ_ONLY.toString()"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@update:checked="toggleCustomPermissions">
<ViewIcon :size="20" />
<span>{{ t('files_sharing', 'View only') }}</span>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :button-variant="true"
:checked.sync="sharingPermission"
:value="bundledPermissions.ALL.toString()"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@update:checked="toggleCustomPermissions">
<EditIcon :size="20" />
<template v-if="allowsFileDrop">
<span>{{ t('files_sharing', 'Allow upload and editing') }}</span>
</template>
<template v-else>
<span>{{ t('files_sharing', 'Allow editing') }}</span>
</template>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="allowsFileDrop"
:button-variant="true"
:checked.sync="sharingPermission"
:value="bundledPermissions.FILE_DROP.toString()"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@update:checked="toggleCustomPermissions">
<UploadIcon :size="20" />
<span>{{ t('files_sharing', 'File drop') }}</span>
<small>{{ t('files_sharing', 'Upload only') }}</small>
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :button-variant="true"
:checked.sync="sharingPermission"
:value="'custom'"
name="sharing_permission_radio"
type="radio"
button-variant-grouped="vertical"
@update:checked="expandCustomPermissions">
<DotsHorizontalIcon :size="20" />
<span>{{ t('files_sharing', 'Custom permissions') }}</span>
<small>{{ customPermissionsList }}</small>
</NcCheckboxRadioSwitch>
</div>
</div>
</div>
<div class="sharingTabDetailsView__advanced-control">
<NcButton type="tertiary"
alignment="end-reverse"
@click="advancedSectionAccordionExpanded = !advancedSectionAccordionExpanded">
{{ t('files_sharing', 'Advanced settings') }}
<template #icon>
<MenuDownIcon />
</template>
</NcButton>
</div>
<div v-if="advancedSectionAccordionExpanded" class="sharingTabDetailsView__advanced">
<section>
<NcInputField v-if="isPublicShare"
:value.sync="share.label"
type="text"
:label="t('files_sharing', 'Share label')" />
<template v-if="isPublicShare">
<NcCheckboxRadioSwitch :checked.sync="isPasswordProtected" :disabled="isPasswordEnforced">
{{ t('files_sharing', 'Set password') }}
</NcCheckboxRadioSwitch>
<NcInputField v-if="isPasswordProtected"
:type="hasUnsavedPassword ? 'text' : 'password'"
:value="hasUnsavedPassword ? share.newPassword : '***************'"
:error="passwordError"
:required="isPasswordEnforced"
:label="t('files_sharing', 'Password')"
@update:value="onPasswordChange" />
<div class="sharingTabDetailsView__advanced-control">
<NcButton id="advancedSectionAccordionAdvancedControl"
type="tertiary"
alignment="end-reverse"
@click="advancedSectionAccordionExpanded = !advancedSectionAccordionExpanded">
{{ t('files_sharing', 'Advanced settings') }}
<template #icon>
<MenuDownIcon />
</template>
</NcButton>
</div>
<div v-if="advancedSectionAccordionExpanded"
id="advancedSectionAccordionAdvanced"
class="sharingTabDetailsView__advanced"
aria-labelledby="advancedSectionAccordionAdvancedControl"
role="region">
<section>
<NcInputField v-if="isPublicShare"
:value.sync="share.label"
type="text"
:label="t('files_sharing', 'Share label')" />
<template v-if="isPublicShare">
<NcCheckboxRadioSwitch :checked.sync="isPasswordProtected" :disabled="isPasswordEnforced">
{{ t('files_sharing', 'Set password') }}
</NcCheckboxRadioSwitch>
<NcInputField v-if="isPasswordProtected"
:type="hasUnsavedPassword ? 'text' : 'password'"
:value="hasUnsavedPassword ? share.newPassword : '***************'"
:error="passwordError"
:required="isPasswordEnforced"
:label="t('files_sharing', 'Password')"
@update:value="onPasswordChange" />
<!-- Migrate icons and remote -> icon="icon-info"-->
<span v-if="isEmailShareType && passwordExpirationTime" icon="icon-info">
{{ t('files_sharing', 'Password expires {passwordExpirationTime}', { passwordExpirationTime }) }}
</span>
<span v-else-if="isEmailShareType && passwordExpirationTime !== null" icon="icon-error">
{{ t('files_sharing', 'Password expired') }}
</span>
</template>
<NcCheckboxRadioSwitch :checked.sync="hasExpirationDate" :disabled="isExpiryDateEnforced">
{{ isExpiryDateEnforced
? t('files_sharing', 'Expiration date (enforced)')
: t('files_sharing', 'Set expiration date') }}
</NcCheckboxRadioSwitch>
<NcDateTimePickerNative v-if="hasExpirationDate"
id="share-date-picker"
:value="new Date(share.expireDate ?? dateTomorrow)"
:min="dateTomorrow"
:max="maxExpirationDateEnforced"
:hide-label="true"
:placeholder="t('files_sharing', 'Expiration date')"
type="date"
@input="onExpirationChange" />
<NcCheckboxRadioSwitch v-if="isPublicShare"
:disabled="canChangeHideDownload"
:checked.sync="share.hideDownload"
@update:checked="queueUpdate('hideDownload')">
{{ t('files_sharing', 'Hide download') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="canTogglePasswordProtectedByTalkAvailable"
:checked.sync="isPasswordProtectedByTalk"
@update:checked="onPasswordProtectedByTalkChange">
{{ t('files_sharing', 'Video verification') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="!isPublicShare" :disabled="!canSetDownload" :checked.sync="canDownload">
{{ t('files_sharing', 'Allow download') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="writeNoteToRecipientIsChecked">
{{ t('files_sharing', 'Note to recipient') }}
</NcCheckboxRadioSwitch>
<template v-if="writeNoteToRecipientIsChecked">
<label for="share-note-textarea">
{{ t('files_sharing', 'Enter a note for the share recipient') }}
</label>
<textarea id="share-note-textarea" :value="share.note" @input="share.note = $event.target.value" />
</template>
<NcCheckboxRadioSwitch :checked.sync="setCustomPermissions">
{{ t('files_sharing', 'Custom permissions') }}
</NcCheckboxRadioSwitch>
<section v-if="setCustomPermissions" class="custom-permissions-group">
<NcCheckboxRadioSwitch :disabled="!allowsFileDrop && share.type === SHARE_TYPES.SHARE_TYPE_LINK"
:checked.sync="hasRead">
{{ t('files_sharing', 'Read') }}
<!-- Migrate icons and remote -> icon="icon-info"-->
<span v-if="isEmailShareType && passwordExpirationTime" icon="icon-info">
{{ t('files_sharing', 'Password expires {passwordExpirationTime}', { passwordExpirationTime }) }}
</span>
<span v-else-if="isEmailShareType && passwordExpirationTime !== null" icon="icon-error">
{{ t('files_sharing', 'Password expired') }}
</span>
</template>
<NcCheckboxRadioSwitch :checked.sync="hasExpirationDate" :disabled="isExpiryDateEnforced">
{{ isExpiryDateEnforced
? t('files_sharing', 'Expiration date (enforced)')
: t('files_sharing', 'Set expiration date') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="isFolder" :disabled="!canSetCreate" :checked.sync="canCreate">
{{ t('files_sharing', 'Create') }}
<NcDateTimePickerNative v-if="hasExpirationDate"
id="share-date-picker"
:value="new Date(share.expireDate ?? dateTomorrow)"
:min="dateTomorrow"
:max="maxExpirationDateEnforced"
:hide-label="true"
:placeholder="t('files_sharing', 'Expiration date')"
type="date"
@input="onExpirationChange" />
<NcCheckboxRadioSwitch v-if="isPublicShare"
:disabled="canChangeHideDownload"
:checked.sync="share.hideDownload"
@update:checked="queueUpdate('hideDownload')">
{{ t('files_sharing', 'Hide download') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :disabled="!canSetEdit" :checked.sync="canEdit">
{{ t('files_sharing', 'Edit') }}
<NcCheckboxRadioSwitch v-if="canTogglePasswordProtectedByTalkAvailable"
:checked.sync="isPasswordProtectedByTalk"
@update:checked="onPasswordProtectedByTalkChange">
{{ t('files_sharing', 'Video verification') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="config.isResharingAllowed && share.type !== SHARE_TYPES.SHARE_TYPE_LINK"
:disabled="!canSetReshare"
:checked.sync="canReshare">
{{ t('files_sharing', 'Share') }}
<NcCheckboxRadioSwitch v-if="!isPublicShare" :disabled="!canSetDownload" :checked.sync="canDownload">
{{ t('files_sharing', 'Allow download') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :disabled="!canSetDelete" :checked.sync="canDelete">
{{ t('files_sharing', 'Delete') }}
<NcCheckboxRadioSwitch :checked.sync="writeNoteToRecipientIsChecked">
{{ t('files_sharing', 'Note to recipient') }}
</NcCheckboxRadioSwitch>
<template v-if="writeNoteToRecipientIsChecked">
<label for="share-note-textarea">
{{ t('files_sharing', 'Enter a note for the share recipient') }}
</label>
<textarea id="share-note-textarea" :value="share.note" @input="share.note = $event.target.value" />
</template>
<NcCheckboxRadioSwitch :checked.sync="setCustomPermissions">
{{ t('files_sharing', 'Custom permissions') }}
</NcCheckboxRadioSwitch>
<section v-if="setCustomPermissions" class="custom-permissions-group">
<NcCheckboxRadioSwitch :disabled="!allowsFileDrop && share.type === SHARE_TYPES.SHARE_TYPE_LINK"
:checked.sync="hasRead">
{{ t('files_sharing', 'Read') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="isFolder" :disabled="!canSetCreate" :checked.sync="canCreate">
{{ t('files_sharing', 'Create') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :disabled="!canSetEdit" :checked.sync="canEdit">
{{ t('files_sharing', 'Update') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch v-if="config.isResharingAllowed && share.type !== SHARE_TYPES.SHARE_TYPE_LINK"
:disabled="!canSetReshare"
:checked.sync="canReshare">
{{ t('files_sharing', 'Share') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :disabled="!canSetDelete" :checked.sync="canDelete">
{{ t('files_sharing', 'Delete') }}
</NcCheckboxRadioSwitch>
</section>
</section>
</section>
</div>
</div>
<div class="sharingTabDetailsView__footer">
<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 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>
<div class="button-group">
<NcButton @click="$emit('close-sharing-details')">
{{ t('files_sharing', 'Cancel') }}
@ -401,19 +411,6 @@ export default {
isFolder() {
return this.fileInfo.type === 'dir'
},
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
},
/**
* @return {boolean}
*/
@ -452,9 +449,6 @@ export default {
isGroupShare() {
return this.share.type === this.SHARE_TYPES.SHARE_TYPE_GROUP
},
isRemoteShare() {
return this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP || this.share.type === this.SHARE_TYPES.SHARE_TYPE_REMOTE
},
isNewShare() {
return this.share.id === null || this.share.id === undefined
},
@ -626,6 +620,9 @@ export default {
: translatedPermissions[permission].toLocaleLowerCase(getLanguage()))
.join(', ')
},
advancedControlExpandedValue() {
return this.advancedSectionAccordionExpanded ? 'true' : 'false'
},
},
watch: {
setCustomPermissions(isChecked) {
@ -643,6 +640,10 @@ export default {
console.debug('config', this.config)
},
mounted() {
this.$refs.quickPermissions?.querySelector('input:checked')?.focus()
},
methods: {
updateAtomicPermissions({
isReadChecked = this.hasRead,
@ -698,6 +699,12 @@ export default {
return
}
// If there is an enforced expiry date, then existing shares created before enforcement
// have no expiry date, hence we set it here.
if (!this.isValidShareAttribute(this.share.expireDate) && this.isExpiryDateEnforced) {
this.hasExpirationDate = true
}
if (
this.isValidShareAttribute(this.share.password)
|| this.isValidShareAttribute(this.share.expireDate)
@ -753,16 +760,12 @@ export default {
if (!this.writeNoteToRecipientIsChecked) {
this.share.note = ''
}
if (this.isPasswordProtected) {
if (this.isValidShareAttribute(this.share.newPassword)) {
if (this.hasUnsavedPassword && this.isValidShareAttribute(this.share.newPassword)) {
this.share.password = this.share.newPassword
this.$delete(this.share, 'newPassword')
} else {
if (this.isPasswordEnforced) {
this.passwordError = true
return
}
} else if (this.isPasswordEnforced && !this.isValidShareAttribute(this.share.password)) {
this.passwordError = true
}
} else {
this.share.password = ''
@ -901,9 +904,11 @@ export default {
.sharingTabDetailsView {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 96%;
width: 100%;
margin: 0 auto;
position: relative;
height: 100%;
overflow: hidden;
&__header {
display: flex;
@ -923,10 +928,16 @@ export default {
}
}
&__wrapper {
overflow: scroll;
flex-shrink: 1;
padding: 4px;
padding-right: 12px;
}
&__quick-permissions {
display: flex;
justify-content: center;
margin-bottom: 0.2em;
width: 100%;
margin: 0 auto;
border-radius: 0;
@ -995,6 +1006,7 @@ export default {
textarea {
height: 80px;
margin: 0;
}
/*

View file

@ -21,7 +21,7 @@
-->
<template>
<div :class="{ 'icon-loading': loading }">
<div class="sharingTab" :class="{ 'icon-loading': loading }">
<!-- error message -->
<div v-if="error" class="emptycontent" :class="{ emptyContentWithSections: sections.length > 0 }">
<div class="icon icon-error" />
@ -87,13 +87,12 @@
</template>
<!-- share details -->
<div v-else>
<SharingDetailsTab :file-info="shareDetailsData.fileInfo"
:share="shareDetailsData.share"
@close-sharing-details="toggleShareDetailsView"
@add:share="addShare"
@remove:share="removeShare" />
</div>
<SharingDetailsTab v-else
:file-info="shareDetailsData.fileInfo"
:share="shareDetailsData.share"
@close-sharing-details="toggleShareDetailsView"
@add:share="addShare"
@remove:share="removeShare" />
</div>
</template>
@ -406,6 +405,9 @@ export default {
}
.sharingTab {
position: relative;
height: 100%;
&__content {
padding: 0 6px;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long