Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sharing link & mail parity #24364

Merged
merged 8 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 41 additions & 20 deletions apps/files_sharing/js/dist/files_sharing_tab.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/files_sharing/js/dist/files_sharing_tab.js.map

Large diffs are not rendered by default.

16 changes: 7 additions & 9 deletions apps/files_sharing/lib/Controller/ShareAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -556,13 +556,13 @@ public function createShare(
}

// Only share by mail have a recipient
if ($shareType === IShare::TYPE_EMAIL) {
if (is_string($shareWith) && $shareType === IShare::TYPE_EMAIL) {
$share->setSharedWith($shareWith);
} else {
// Only link share have a label
if (!empty($label)) {
$share->setLabel($label);
}
}

// If we have a label, use it
if (!empty($label)) {
$share->setLabel($label);
}

if ($sendPasswordByTalk === 'true') {
Expand Down Expand Up @@ -1127,8 +1127,7 @@ public function updateShare(
$share->setPassword($password);
}

// only link shares have labels
if ($share->getShareType() === IShare::TYPE_LINK && $label !== null) {
if ($label !== null) {
if (strlen($label) > 255) {
throw new OCSBadRequestException("Maxmimum label length is 255");
}
Expand Down Expand Up @@ -1592,7 +1591,6 @@ private function getSharesFromNode(string $viewer, $node, bool $reShares): array
IShare::TYPE_GROUP,
IShare::TYPE_LINK,
IShare::TYPE_EMAIL,
IShare::TYPE_EMAIL,
skjnldsv marked this conversation as resolved.
Show resolved Hide resolved
IShare::TYPE_CIRCLE,
IShare::TYPE_ROOM,
IShare::TYPE_DECK
Expand Down
63 changes: 27 additions & 36 deletions apps/files_sharing/src/components/SharingEntryLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
<h5 :title="title">
{{ title }}
</h5>
<p v-if="subtitle">
{{ subtitle }}
</p>
</div>

<!-- clipboard -->
Expand Down Expand Up @@ -321,7 +324,6 @@

<script>
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import Vue from 'vue'

import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
Expand All @@ -335,11 +337,10 @@ 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 GeneratePassword from '../utils/GeneratePassword'
import Share from '../models/Share'
import SharesMixin from '../mixins/SharesMixin'

const passwordSet = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789'

export default {
name: 'SharingEntryLink',

Expand Down Expand Up @@ -406,7 +407,6 @@ export default {

/**
* Link share label
* TODO: allow editing
* @returns {string}
*/
title() {
Expand All @@ -424,6 +424,11 @@ export default {
})
}
if (this.share.label && this.share.label.trim() !== '') {
if (this.isEmailShareType) {
return t('files_sharing', 'Mail share ({label})', {
MorrisJobke marked this conversation as resolved.
Show resolved Hide resolved
label: this.share.label.trim(),
})
}
return t('files_sharing', 'Share link ({label})', {
label: this.share.label.trim(),
})
Expand All @@ -435,6 +440,18 @@ export default {
return t('files_sharing', 'Share link')
},

/**
* Show the email on a second line if a label is set for mail shares
* @returns {string}
*/
subtitle() {
if (this.isEmailShareType
&& this.title !== this.share.shareWith) {
return this.share.shareWith
}
return null
},

/**
* Does the current share have an expiration date
* @returns {boolean}
Expand Down Expand Up @@ -472,7 +489,7 @@ export default {
},
async set(enabled) {
// TODO: directly save after generation to make sure the share is always protected
Vue.set(this.share, 'password', enabled ? await this.generatePassword() : '')
Vue.set(this.share, 'password', enabled ? await GeneratePassword() : '')
Vue.set(this.share, 'newPassword', this.share.password)
},
},
Expand Down Expand Up @@ -635,7 +652,7 @@ export default {
shareDefaults.expiration = this.config.defaultExpirationDateString
}
if (this.config.enableLinkPasswordByDefault) {
shareDefaults.password = await this.generatePassword()
shareDefaults.password = await GeneratePassword()
}

// do not push yet if we need a password or an expiration date: show pending menu
Expand All @@ -658,7 +675,7 @@ export default {
// ELSE, show the pending popovermenu
// if password enforced, pre-fill with random one
if (this.config.enforcePasswordForPublicLink) {
shareDefaults.password = await this.generatePassword()
shareDefaults.password = await GeneratePassword()
}

// create share & close menu
Expand Down Expand Up @@ -781,35 +798,6 @@ export default {
this.queueUpdate('label')
}
},

/**
* 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)
Expand Down Expand Up @@ -933,6 +921,9 @@ export default {
overflow: hidden;
white-space: nowrap;
}
p {
color: var(--color-text-maxcontrast);
}
}

&:not(.sharing-entry--share) &__actions {
Expand Down
40 changes: 34 additions & 6 deletions apps/files_sharing/src/components/SharingInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import debounce from 'debounce'
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'

import Config from '../services/ConfigService'
import GeneratePassword from '../utils/GeneratePassword'
import Share from '../models/Share'
import ShareRequests from '../mixins/ShareRequests'
import ShareTypes from '../mixins/ShareTypes'
Expand Down Expand Up @@ -448,9 +449,6 @@ export default {
return true
}

// TODO: reset the search string when done
// https://github.com/shentao/vue-multiselect/issues/633

// handle externalResults from OCA.Sharing.ShareSearch
if (value.handler) {
const share = await value.handler(this)
Expand All @@ -459,25 +457,55 @@ export default {
}

this.loading = true
console.debug('Adding a new share from the input for', value)
try {
let password = null

if (this.config.enforcePasswordForPublicLink
&& value.shareType === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
password = await GeneratePassword()
}

const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
const share = await this.createShare({
path,
shareType: value.shareType,
shareWith: value.shareWith,
password,
permissions: this.fileInfo.sharePermissions & OC.getCapabilities().files_sharing.default_permissions,
})
this.$emit('add:share', share)

this.getRecommendations()
// If we had a password, we need to show it to the user as it was generated
if (password) {
share.newPassword = password
// Wait for the newly added share
const component = await new Promise(resolve => {
this.$emit('add:share', share, resolve)
})

// open the menu on the
// freshly created share component
component.open = true
} else {
// Else we just add it normally
this.$emit('add:share', share)
}

// reset the search string when done
// FIXME: https://github.com/shentao/vue-multiselect/issues/633
if (this.$refs.multiselect?.$refs?.VueMultiselect?.search) {
this.$refs.multiselect.$refs.VueMultiselect.search = ''
}

} catch (response) {
await this.getRecommendations()
} catch (error) {
// focus back if any error
const input = this.$refs.multiselect.$el.querySelector('input')
if (input) {
input.focus()
}
this.query = value.shareWith
console.error('Error while adding new share', error)
} finally {
this.loading = false
}
Expand Down
56 changes: 56 additions & 0 deletions apps/files_sharing/src/utils/GeneratePassword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

/**
* @copyright Copyright (c) 2020 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/>.
*
*/

import axios from '@nextcloud/axios'
import Config from '../services/ConfigService'

const config = new Config()
const passwordSet = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789'

/**
* Generate a valid policy password or
* request a valid password if password_policy
* is enabled
*
* @returns {string} a valid password
*/
export default async function() {
// password policy is enabled, let's request a pass
if (config.passwordPolicy.api && config.passwordPolicy.api.generate) {
try {
const request = await axios.get(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
}, '')
}
35 changes: 32 additions & 3 deletions apps/files_sharing/src/views/SharingTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@

<!-- link shares list -->
<SharingLinkList v-if="!loading"
ref="linkShareList"
:can-reshare="canReshare"
:file-info="fileInfo"
:shares="linkShares" />

<!-- other shares list -->
<SharingList v-if="!loading"
ref="shareList"
:shares="shares"
:file-info="fileInfo" />

Expand Down Expand Up @@ -295,18 +297,45 @@ export default {
},

/**
* Insert share at top of arrays
* Add a new share into the shares list
* and return the newly created share component
*
* @param {Share} share the share to insert
* @param {Share} share the share to add to the array
* @param {Function} resolve a function to run after the share is added and its component initialized
*/
addShare(share) {
addShare(share, resolve) {
// only catching share type MAIL as link shares are added differently
// meaning: not from the ShareInput
if (share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
this.linkShares.unshift(share)
} else {
this.shares.unshift(share)
}
this.awaitForShare(share, resolve)
},

/**
* Await for next tick and render after the list updated
* Then resolve with the matched vue component of the
* provided share object
*
* @param {Share} share newly created share
* @param {Function} resolve a function to execute after
*/
awaitForShare(share, resolve) {
let listComponent = this.$refs.shareList
// Only mail shares comes from the input, link shares
// are managed internally in the SharingLinkList component
if (share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
listComponent = this.$refs.linkShareList
}

this.$nextTick(() => {
skjnldsv marked this conversation as resolved.
Show resolved Hide resolved
const newShare = listComponent.$children.find(component => component.share === share)
if (newShare) {
resolve(newShare)
}
})
},
},
}
Expand Down
4 changes: 2 additions & 2 deletions apps/settings/templates/settings/admin/sharing.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
value="1" <?php if ($_['allowLinks'] === 'yes') {
print_unescaped('checked="checked"');
} ?> />
<label for="allowLinks"><?php p($l->t('Allow users to share via link'));?></label><br/>
<label for="allowLinks"><?php p($l->t('Allow users to share via link and emails'));?></label><br/>
</p>

<p id="publicLinkSettings" class="indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareAPIEnabled'] === 'no') {
Expand All @@ -96,7 +96,7 @@
value="1" <?php if ($_['shareDefaultExpireDateSet'] === 'yes') {
print_unescaped('checked="checked"');
} ?> />
<label for="shareapiDefaultExpireDate"><?php p($l->t('Set default expiration date for link shares'));?></label><br/>
<label for="shareapiDefaultExpireDate"><?php p($l->t('Set default expiration date'));?></label><br/>

</p>
<p id="setDefaultExpireDate" class="double-indent <?php if ($_['allowLinks'] !== 'yes' || $_['shareDefaultExpireDateSet'] === 'no' || $_['shareAPIEnabled'] === 'no') {
Expand Down
Loading