You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SharingEntryLink.vue 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935
  1. <!--
  2. - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
  3. -
  4. - @author John Molakvoæ <skjnldsv@protonmail.com>
  5. -
  6. - @license GNU AGPL version 3 or any later version
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <li :class="{'sharing-entry--share': share}" class="sharing-entry sharing-entry__link">
  24. <NcAvatar :is-no-user="true"
  25. :icon-class="isEmailShareType ? 'avatar-link-share icon-mail-white' : 'avatar-link-share icon-public-white'"
  26. class="sharing-entry__avatar" />
  27. <div class="sharing-entry__desc">
  28. <span class="sharing-entry__title" :title="title">
  29. {{ title }}
  30. </span>
  31. <p v-if="subtitle">
  32. {{ subtitle }}
  33. </p>
  34. </div>
  35. <!-- clipboard -->
  36. <NcActions v-if="share && !isEmailShareType && share.token"
  37. ref="copyButton"
  38. class="sharing-entry__copy">
  39. <NcActionLink :href="shareLink"
  40. target="_blank"
  41. :title="copyLinkTooltip"
  42. :aria-label="copyLinkTooltip"
  43. :icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
  44. @click.stop.prevent="copyLink" />
  45. </NcActions>
  46. <!-- pending actions -->
  47. <NcActions v-if="!pending && (pendingPassword || pendingExpirationDate)"
  48. class="sharing-entry__actions"
  49. :aria-label="actionsTooltip"
  50. menu-align="right"
  51. :open.sync="open"
  52. @close="onNewLinkShare">
  53. <!-- pending data menu -->
  54. <NcActionText v-if="errors.pending"
  55. icon="icon-error"
  56. :class="{ error: errors.pending}">
  57. {{ errors.pending }}
  58. </NcActionText>
  59. <NcActionText v-else icon="icon-info">
  60. {{ t('files_sharing', 'Please enter the following required information before creating the share') }}
  61. </NcActionText>
  62. <!-- password -->
  63. <NcActionText v-if="pendingPassword" icon="icon-password">
  64. {{ t('files_sharing', 'Password protection (enforced)') }}
  65. </NcActionText>
  66. <NcActionCheckbox v-else-if="config.enableLinkPasswordByDefault"
  67. :checked.sync="isPasswordProtected"
  68. :disabled="config.enforcePasswordForPublicLink || saving"
  69. class="share-link-password-checkbox"
  70. @uncheck="onPasswordDisable">
  71. {{ t('files_sharing', 'Password protection') }}
  72. </NcActionCheckbox>
  73. <NcActionInput v-if="pendingPassword || share.password"
  74. class="share-link-password"
  75. :value.sync="share.password"
  76. :disabled="saving"
  77. :required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink"
  78. :minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength"
  79. icon=""
  80. autocomplete="new-password"
  81. @submit="onNewLinkShare">
  82. {{ t('files_sharing', 'Enter a password') }}
  83. </NcActionInput>
  84. <!-- expiration date -->
  85. <NcActionText v-if="pendingExpirationDate" icon="icon-calendar-dark">
  86. {{ t('files_sharing', 'Expiration date (enforced)') }}
  87. </NcActionText>
  88. <NcActionInput v-if="pendingExpirationDate"
  89. class="share-link-expire-date"
  90. :disabled="saving"
  91. :is-native-picker="true"
  92. :hide-label="true"
  93. :value="new Date(share.expireDate)"
  94. type="date"
  95. :min="dateTomorrow"
  96. :max="dateMaxEnforced"
  97. @input="onExpirationChange">
  98. <!-- let's not submit when picked, the user
  99. might want to still edit or copy the password -->
  100. {{ t('files_sharing', 'Enter a date') }}
  101. </NcActionInput>
  102. <NcActionButton icon="icon-checkmark" @click.prevent.stop="onNewLinkShare">
  103. {{ t('files_sharing', 'Create share') }}
  104. </NcActionButton>
  105. <NcActionButton icon="icon-close" @click.prevent.stop="onCancel">
  106. {{ t('files_sharing', 'Cancel') }}
  107. </NcActionButton>
  108. </NcActions>
  109. <!-- actions -->
  110. <NcActions v-else-if="!loading"
  111. class="sharing-entry__actions"
  112. :aria-label="actionsTooltip"
  113. menu-align="right"
  114. :open.sync="open"
  115. @close="onMenuClose">
  116. <template v-if="share">
  117. <template v-if="share.canEdit && canReshare">
  118. <!-- Custom Label -->
  119. <NcActionInput ref="label"
  120. :class="{ error: errors.label }"
  121. :disabled="saving"
  122. :label="t('files_sharing', 'Share label')"
  123. :value="share.newLabel !== undefined ? share.newLabel : share.label"
  124. icon="icon-edit"
  125. maxlength="255"
  126. @update:value="onLabelChange"
  127. @submit="onLabelSubmit" />
  128. <SharePermissionsEditor :can-reshare="canReshare"
  129. :share.sync="share"
  130. :file-info="fileInfo" />
  131. <NcActionSeparator />
  132. <NcActionCheckbox :checked.sync="share.hideDownload"
  133. :disabled="saving || canChangeHideDownload"
  134. @change="queueUpdate('hideDownload')">
  135. {{ t('files_sharing', 'Hide download') }}
  136. </NcActionCheckbox>
  137. <!-- password -->
  138. <NcActionCheckbox :checked.sync="isPasswordProtected"
  139. :disabled="config.enforcePasswordForPublicLink || saving"
  140. class="share-link-password-checkbox"
  141. @uncheck="onPasswordDisable">
  142. {{ config.enforcePasswordForPublicLink
  143. ? t('files_sharing', 'Password protection (enforced)')
  144. : t('files_sharing', 'Password protect') }}
  145. </NcActionCheckbox>
  146. <NcActionInput v-if="isPasswordProtected"
  147. ref="password"
  148. class="share-link-password"
  149. :class="{ error: errors.password}"
  150. :disabled="saving"
  151. :required="config.enforcePasswordForPublicLink"
  152. :value="hasUnsavedPassword ? share.newPassword : '***************'"
  153. icon="icon-password"
  154. autocomplete="new-password"
  155. :type="hasUnsavedPassword ? 'text': 'password'"
  156. @update:value="onPasswordChange"
  157. @submit="onPasswordSubmit">
  158. {{ t('files_sharing', 'Enter a password') }}
  159. </NcActionInput>
  160. <NcActionText v-if="isEmailShareType && passwordExpirationTime" icon="icon-info">
  161. {{ t('files_sharing', 'Password expires {passwordExpirationTime}', {passwordExpirationTime}) }}
  162. </NcActionText>
  163. <NcActionText v-else-if="isEmailShareType && passwordExpirationTime !== null" icon="icon-error">
  164. {{ t('files_sharing', 'Password expired') }}
  165. </NcActionText>
  166. <!-- password protected by Talk -->
  167. <NcActionCheckbox v-if="isPasswordProtectedByTalkAvailable"
  168. :checked.sync="isPasswordProtectedByTalk"
  169. :disabled="!canTogglePasswordProtectedByTalkAvailable || saving"
  170. class="share-link-password-talk-checkbox"
  171. @change="onPasswordProtectedByTalkChange">
  172. {{ t('files_sharing', 'Video verification') }}
  173. </NcActionCheckbox>
  174. <!-- expiration date -->
  175. <NcActionCheckbox :checked.sync="hasExpirationDate"
  176. :disabled="config.isDefaultExpireDateEnforced || saving"
  177. class="share-link-expire-date-checkbox"
  178. @uncheck="onExpirationDisable">
  179. {{ config.isDefaultExpireDateEnforced
  180. ? t('files_sharing', 'Expiration date (enforced)')
  181. : t('files_sharing', 'Set expiration date') }}
  182. </NcActionCheckbox>
  183. <NcActionInput v-if="hasExpirationDate"
  184. ref="expireDate"
  185. :is-native-picker="true"
  186. :hide-label="true"
  187. class="share-link-expire-date"
  188. :class="{ error: errors.expireDate}"
  189. :disabled="saving"
  190. :value="new Date(share.expireDate)"
  191. type="date"
  192. :min="dateTomorrow"
  193. :max="dateMaxEnforced"
  194. @input="onExpirationChange">
  195. {{ t('files_sharing', 'Enter a date') }}
  196. </NcActionInput>
  197. <!-- note -->
  198. <NcActionCheckbox :checked.sync="hasNote"
  199. :disabled="saving"
  200. @uncheck="queueUpdate('note')">
  201. {{ t('files_sharing', 'Note to recipient') }}
  202. </NcActionCheckbox>
  203. <NcActionTextEditable v-if="hasNote"
  204. ref="note"
  205. :class="{ error: errors.note}"
  206. :disabled="saving"
  207. :placeholder="t('files_sharing', 'Enter a note for the share recipient')"
  208. :value="share.newNote || share.note"
  209. icon="icon-edit"
  210. @update:value="onNoteChange"
  211. @submit="onNoteSubmit" />
  212. </template>
  213. <NcActionSeparator />
  214. <!-- external actions -->
  215. <ExternalShareAction v-for="action in externalLinkActions"
  216. :id="action.id"
  217. :key="action.id"
  218. :action="action"
  219. :file-info="fileInfo"
  220. :share="share" />
  221. <!-- external legacy sharing via url (social...) -->
  222. <NcActionLink v-for="({icon, url, name}, index) in externalLegacyLinkActions"
  223. :key="index"
  224. :href="url(shareLink)"
  225. :icon="icon"
  226. target="_blank">
  227. {{ name }}
  228. </NcActionLink>
  229. <NcActionButton v-if="share.canDelete"
  230. icon="icon-close"
  231. :disabled="saving"
  232. @click.prevent="onDelete">
  233. {{ t('files_sharing', 'Unshare') }}
  234. </NcActionButton>
  235. <NcActionButton v-if="!isEmailShareType && canReshare"
  236. class="new-share-link"
  237. icon="icon-add"
  238. @click.prevent.stop="onNewLinkShare">
  239. {{ t('files_sharing', 'Add another link') }}
  240. </NcActionButton>
  241. </template>
  242. <!-- Create new share -->
  243. <NcActionButton v-else-if="canReshare"
  244. class="new-share-link"
  245. :title="t('files_sharing', 'Create a new share link')"
  246. :aria-label="t('files_sharing', 'Create a new share link')"
  247. :icon="loading ? 'icon-loading-small' : 'icon-add'"
  248. @click.prevent.stop="onNewLinkShare" />
  249. </NcActions>
  250. <!-- loading indicator to replace the menu -->
  251. <div v-else class="icon-loading-small sharing-entry__loading" />
  252. </li>
  253. </template>
  254. <script>
  255. import { generateUrl } from '@nextcloud/router'
  256. import { showError, showSuccess } from '@nextcloud/dialogs'
  257. import { Type as ShareTypes } from '@nextcloud/sharing'
  258. import Vue from 'vue'
  259. import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton'
  260. import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox'
  261. import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput'
  262. import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink'
  263. import NcActionText from '@nextcloud/vue/dist/Components/NcActionText'
  264. import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator'
  265. import NcActionTextEditable from '@nextcloud/vue/dist/Components/NcActionTextEditable'
  266. import NcActions from '@nextcloud/vue/dist/Components/NcActions'
  267. import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar'
  268. import ExternalShareAction from './ExternalShareAction.vue'
  269. import SharePermissionsEditor from './SharePermissionsEditor.vue'
  270. import GeneratePassword from '../utils/GeneratePassword.js'
  271. import Share from '../models/Share.js'
  272. import SharesMixin from '../mixins/SharesMixin.js'
  273. export default {
  274. name: 'SharingEntryLink',
  275. components: {
  276. NcActions,
  277. NcActionButton,
  278. NcActionCheckbox,
  279. NcActionInput,
  280. NcActionLink,
  281. NcActionText,
  282. NcActionTextEditable,
  283. NcActionSeparator,
  284. NcAvatar,
  285. ExternalShareAction,
  286. SharePermissionsEditor,
  287. },
  288. mixins: [SharesMixin],
  289. props: {
  290. canReshare: {
  291. type: Boolean,
  292. default: true,
  293. },
  294. index: {
  295. type: Number,
  296. default: null,
  297. },
  298. },
  299. data() {
  300. return {
  301. copySuccess: true,
  302. copied: false,
  303. // Are we waiting for password/expiration date
  304. pending: false,
  305. ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state,
  306. ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
  307. }
  308. },
  309. computed: {
  310. /**
  311. * Link share label
  312. *
  313. * @return {string}
  314. */
  315. title() {
  316. // if we have a valid existing share (not pending)
  317. if (this.share && this.share.id) {
  318. if (!this.isShareOwner && this.share.ownerDisplayName) {
  319. if (this.isEmailShareType) {
  320. return t('files_sharing', '{shareWith} by {initiator}', {
  321. shareWith: this.share.shareWith,
  322. initiator: this.share.ownerDisplayName,
  323. })
  324. }
  325. return t('files_sharing', 'Shared via link by {initiator}', {
  326. initiator: this.share.ownerDisplayName,
  327. })
  328. }
  329. if (this.share.label && this.share.label.trim() !== '') {
  330. if (this.isEmailShareType) {
  331. return t('files_sharing', 'Mail share ({label})', {
  332. label: this.share.label.trim(),
  333. })
  334. }
  335. return t('files_sharing', 'Share link ({label})', {
  336. label: this.share.label.trim(),
  337. })
  338. }
  339. if (this.isEmailShareType) {
  340. return this.share.shareWith
  341. }
  342. }
  343. if (this.index > 1) {
  344. return t('files_sharing', 'Share link ({index})', { index: this.index })
  345. }
  346. return t('files_sharing', 'Share link')
  347. },
  348. /**
  349. * Show the email on a second line if a label is set for mail shares
  350. *
  351. * @return {string}
  352. */
  353. subtitle() {
  354. if (this.isEmailShareType
  355. && this.title !== this.share.shareWith) {
  356. return this.share.shareWith
  357. }
  358. return null
  359. },
  360. /**
  361. * Does the current share have an expiration date
  362. *
  363. * @return {boolean}
  364. */
  365. hasExpirationDate: {
  366. get() {
  367. return this.config.isDefaultExpireDateEnforced
  368. || !!this.share.expireDate
  369. },
  370. set(enabled) {
  371. const defaultExpirationDate = this.config.defaultExpirationDate
  372. || new Date(new Date().setDate(new Date().getDate() + 1))
  373. this.share.expireDate = enabled
  374. ? this.formatDateToString(defaultExpirationDate)
  375. : ''
  376. console.debug('Expiration date status', enabled, this.share.expireDate)
  377. },
  378. },
  379. dateMaxEnforced() {
  380. if (this.config.isDefaultExpireDateEnforced) {
  381. return new Date(new Date().setDate(new Date().getDate() + this.config.defaultExpireDate))
  382. }
  383. return null
  384. },
  385. /**
  386. * Is the current share password protected ?
  387. *
  388. * @return {boolean}
  389. */
  390. isPasswordProtected: {
  391. get() {
  392. return this.config.enforcePasswordForPublicLink
  393. || !!this.share.password
  394. },
  395. async set(enabled) {
  396. // TODO: directly save after generation to make sure the share is always protected
  397. Vue.set(this.share, 'password', enabled ? await GeneratePassword() : '')
  398. Vue.set(this.share, 'newPassword', this.share.password)
  399. },
  400. },
  401. passwordExpirationTime() {
  402. if (this.share.passwordExpirationTime === null) {
  403. return null
  404. }
  405. const expirationTime = moment(this.share.passwordExpirationTime)
  406. if (expirationTime.diff(moment()) < 0) {
  407. return false
  408. }
  409. return expirationTime.fromNow()
  410. },
  411. /**
  412. * Is Talk enabled?
  413. *
  414. * @return {boolean}
  415. */
  416. isTalkEnabled() {
  417. return OC.appswebroots.spreed !== undefined
  418. },
  419. /**
  420. * Is it possible to protect the password by Talk?
  421. *
  422. * @return {boolean}
  423. */
  424. isPasswordProtectedByTalkAvailable() {
  425. return this.isPasswordProtected && this.isTalkEnabled
  426. },
  427. /**
  428. * Is the current share password protected by Talk?
  429. *
  430. * @return {boolean}
  431. */
  432. isPasswordProtectedByTalk: {
  433. get() {
  434. return this.share.sendPasswordByTalk
  435. },
  436. async set(enabled) {
  437. this.share.sendPasswordByTalk = enabled
  438. },
  439. },
  440. /**
  441. * Is the current share an email share ?
  442. *
  443. * @return {boolean}
  444. */
  445. isEmailShareType() {
  446. return this.share
  447. ? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
  448. : false
  449. },
  450. canTogglePasswordProtectedByTalkAvailable() {
  451. if (!this.isPasswordProtected) {
  452. // Makes no sense
  453. return false
  454. } else if (this.isEmailShareType && !this.hasUnsavedPassword) {
  455. // For email shares we need a new password in order to enable or
  456. // disable
  457. return false
  458. }
  459. // Anything else should be fine
  460. return true
  461. },
  462. /**
  463. * Pending data.
  464. * If the share still doesn't have an id, it is not synced
  465. * Therefore this is still not valid and requires user input
  466. *
  467. * @return {boolean}
  468. */
  469. pendingPassword() {
  470. return this.config.enforcePasswordForPublicLink && this.share && !this.share.id
  471. },
  472. pendingExpirationDate() {
  473. return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id
  474. },
  475. // if newPassword exists, but is empty, it means
  476. // the user deleted the original password
  477. hasUnsavedPassword() {
  478. return this.share.newPassword !== undefined
  479. },
  480. /**
  481. * Return the public share link
  482. *
  483. * @return {string}
  484. */
  485. shareLink() {
  486. return window.location.protocol + '//' + window.location.host + generateUrl('/s/') + this.share.token
  487. },
  488. /**
  489. * Tooltip message for actions button
  490. *
  491. * @return {string}
  492. */
  493. actionsTooltip() {
  494. return t('files_sharing', 'Actions for "{title}"', { title: this.title })
  495. },
  496. /**
  497. * Tooltip message for copy button
  498. *
  499. * @return {string}
  500. */
  501. copyLinkTooltip() {
  502. if (this.copied) {
  503. if (this.copySuccess) {
  504. return ''
  505. }
  506. return t('files_sharing', 'Cannot copy, please copy the link manually')
  507. }
  508. return t('files_sharing', 'Copy public link of "{title}" to clipboard', { title: this.title })
  509. },
  510. /**
  511. * External additionnai actions for the menu
  512. *
  513. * @deprecated use OCA.Sharing.ExternalShareActions
  514. * @return {Array}
  515. */
  516. externalLegacyLinkActions() {
  517. return this.ExternalLegacyLinkActions.actions
  518. },
  519. /**
  520. * Additional actions for the menu
  521. *
  522. * @return {Array}
  523. */
  524. externalLinkActions() {
  525. // filter only the registered actions for said link
  526. return this.ExternalShareActions.actions
  527. .filter(action => action.shareType.includes(ShareTypes.SHARE_TYPE_LINK)
  528. || action.shareType.includes(ShareTypes.SHARE_TYPE_EMAIL))
  529. },
  530. isPasswordPolicyEnabled() {
  531. return typeof this.config.passwordPolicy === 'object'
  532. },
  533. canChangeHideDownload() {
  534. const hasDisabledDownload = (shareAttribute) => shareAttribute.key === 'download' && shareAttribute.scope === 'permissions' && shareAttribute.enabled === false
  535. return this.fileInfo.shareAttributes.some(hasDisabledDownload)
  536. },
  537. },
  538. methods: {
  539. /**
  540. * Create a new share link and append it to the list
  541. */
  542. async onNewLinkShare() {
  543. // do not run again if already loading
  544. if (this.loading) {
  545. return
  546. }
  547. const shareDefaults = {
  548. share_type: ShareTypes.SHARE_TYPE_LINK,
  549. }
  550. if (this.config.isDefaultExpireDateEnforced) {
  551. // default is empty string if not set
  552. // expiration is the share object key, not expireDate
  553. shareDefaults.expiration = this.formatDateToString(this.config.defaultExpirationDate)
  554. }
  555. if (this.config.enableLinkPasswordByDefault) {
  556. shareDefaults.password = await GeneratePassword()
  557. }
  558. // do not push yet if we need a password or an expiration date: show pending menu
  559. if (this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced) {
  560. this.pending = true
  561. // if a share already exists, pushing it
  562. if (this.share && !this.share.id) {
  563. // if the share is valid, create it on the server
  564. if (this.checkShare(this.share)) {
  565. try {
  566. await this.pushNewLinkShare(this.share, true)
  567. } catch (e) {
  568. this.pending = false
  569. console.error(e)
  570. return false
  571. }
  572. return true
  573. } else {
  574. this.open = true
  575. OC.Notification.showTemporary(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
  576. return false
  577. }
  578. }
  579. // ELSE, show the pending popovermenu
  580. // if password enforced, pre-fill with random one
  581. if (this.config.enforcePasswordForPublicLink) {
  582. shareDefaults.password = await GeneratePassword()
  583. }
  584. // create share & close menu
  585. const share = new Share(shareDefaults)
  586. const component = await new Promise(resolve => {
  587. this.$emit('add:share', share, resolve)
  588. })
  589. // open the menu on the
  590. // freshly created share component
  591. this.open = false
  592. this.pending = false
  593. component.open = true
  594. // Nothing is enforced, creating share directly
  595. } else {
  596. const share = new Share(shareDefaults)
  597. await this.pushNewLinkShare(share)
  598. }
  599. },
  600. /**
  601. * Push a new link share to the server
  602. * And update or append to the list
  603. * accordingly
  604. *
  605. * @param {Share} share the new share
  606. * @param {boolean} [update=false] do we update the current share ?
  607. */
  608. async pushNewLinkShare(share, update) {
  609. try {
  610. // do nothing if we're already pending creation
  611. if (this.loading) {
  612. return true
  613. }
  614. this.loading = true
  615. this.errors = {}
  616. const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
  617. const options = {
  618. path,
  619. shareType: ShareTypes.SHARE_TYPE_LINK,
  620. password: share.password,
  621. expireDate: share.expireDate,
  622. attributes: JSON.stringify(this.fileInfo.shareAttributes),
  623. // we do not allow setting the publicUpload
  624. // before the share creation.
  625. // Todo: We also need to fix the createShare method in
  626. // lib/Controller/ShareAPIController.php to allow file drop
  627. // (currently not supported on create, only update)
  628. }
  629. console.debug('Creating link share with options', options)
  630. const newShare = await this.createShare(options)
  631. this.open = false
  632. console.debug('Link share created', newShare)
  633. // if share already exists, copy link directly on next tick
  634. let component
  635. if (update) {
  636. component = await new Promise(resolve => {
  637. this.$emit('update:share', newShare, resolve)
  638. })
  639. } else {
  640. // adding new share to the array and copying link to clipboard
  641. // using promise so that we can copy link in the same click function
  642. // and avoid firefox copy permissions issue
  643. component = await new Promise(resolve => {
  644. this.$emit('add:share', newShare, resolve)
  645. })
  646. }
  647. // Execute the copy link method
  648. // freshly created share component
  649. // ! somehow does not works on firefox !
  650. if (!this.config.enforcePasswordForPublicLink) {
  651. // Only copy the link when the password was not forced,
  652. // otherwise the user needs to copy/paste the password before finishing the share.
  653. component.copyLink()
  654. }
  655. showSuccess(t('sharing', 'Link share created'))
  656. } catch (data) {
  657. const message = data?.response?.data?.ocs?.meta?.message
  658. if (!message) {
  659. showError(t('sharing', 'Error while creating the share'))
  660. console.error(data)
  661. return
  662. }
  663. if (message.match(/password/i)) {
  664. this.onSyncError('password', message)
  665. } else if (message.match(/date/i)) {
  666. this.onSyncError('expireDate', message)
  667. } else {
  668. this.onSyncError('pending', message)
  669. }
  670. throw data
  671. } finally {
  672. this.loading = false
  673. }
  674. },
  675. /**
  676. * Label changed, let's save it to a different key
  677. *
  678. * @param {string} label the share label
  679. */
  680. onLabelChange(label) {
  681. this.$set(this.share, 'newLabel', label.trim())
  682. },
  683. /**
  684. * When the note change, we trim, save and dispatch
  685. */
  686. onLabelSubmit() {
  687. if (typeof this.share.newLabel === 'string') {
  688. this.share.label = this.share.newLabel
  689. this.$delete(this.share, 'newLabel')
  690. this.queueUpdate('label')
  691. }
  692. },
  693. async copyLink() {
  694. try {
  695. await navigator.clipboard.writeText(this.shareLink)
  696. showSuccess(t('files_sharing', 'Link copied'))
  697. // focus and show the tooltip
  698. this.$refs.copyButton.$el.focus()
  699. this.copySuccess = true
  700. this.copied = true
  701. } catch (error) {
  702. this.copySuccess = false
  703. this.copied = true
  704. console.error(error)
  705. } finally {
  706. setTimeout(() => {
  707. this.copySuccess = false
  708. this.copied = false
  709. }, 4000)
  710. }
  711. },
  712. /**
  713. * Update newPassword values
  714. * of share. If password is set but not newPassword
  715. * then the user did not changed the password
  716. * If both co-exists, the password have changed and
  717. * we show it in plain text.
  718. * Then on submit (or menu close), we sync it.
  719. *
  720. * @param {string} password the changed password
  721. */
  722. onPasswordChange(password) {
  723. this.$set(this.share, 'newPassword', password)
  724. },
  725. /**
  726. * Uncheck password protection
  727. * We need this method because @update:checked
  728. * is ran simultaneously as @uncheck, so we
  729. * cannot ensure data is up-to-date
  730. */
  731. onPasswordDisable() {
  732. this.share.password = ''
  733. // reset password state after sync
  734. this.$delete(this.share, 'newPassword')
  735. // only update if valid share.
  736. if (this.share.id) {
  737. this.queueUpdate('password')
  738. }
  739. },
  740. /**
  741. * Menu have been closed or password has been submitted.
  742. * The only property that does not get
  743. * synced automatically is the password
  744. * So let's check if we have an unsaved
  745. * password.
  746. * expireDate is saved on datepicker pick
  747. * or close.
  748. */
  749. onPasswordSubmit() {
  750. if (this.hasUnsavedPassword) {
  751. this.share.password = this.share.newPassword.trim()
  752. this.queueUpdate('password')
  753. }
  754. },
  755. /**
  756. * Update the password along with "sendPasswordByTalk".
  757. *
  758. * If the password was modified the new password is sent; otherwise
  759. * updating a mail share would fail, as in that case it is required that
  760. * a new password is set when enabling or disabling
  761. * "sendPasswordByTalk".
  762. */
  763. onPasswordProtectedByTalkChange() {
  764. if (this.hasUnsavedPassword) {
  765. this.share.password = this.share.newPassword.trim()
  766. }
  767. this.queueUpdate('sendPasswordByTalk', 'password')
  768. },
  769. /**
  770. * Save potential changed data on menu close
  771. */
  772. onMenuClose() {
  773. this.onPasswordSubmit()
  774. this.onNoteSubmit()
  775. },
  776. /**
  777. * Cancel the share creation
  778. * Used in the pending popover
  779. */
  780. onCancel() {
  781. // this.share already exists at this point,
  782. // but is incomplete as not pushed to server
  783. // YET. We can safely delete the share :)
  784. this.$emit('remove:share', this.share)
  785. },
  786. },
  787. }
  788. </script>
  789. <style lang="scss" scoped>
  790. .sharing-entry {
  791. display: flex;
  792. align-items: center;
  793. min-height: 44px;
  794. &__desc {
  795. display: flex;
  796. flex-direction: column;
  797. justify-content: space-between;
  798. padding: 8px;
  799. line-height: 1.2em;
  800. overflow: hidden;
  801. p {
  802. color: var(--color-text-maxcontrast);
  803. }
  804. }
  805. &__title {
  806. text-overflow: ellipsis;
  807. overflow: hidden;
  808. white-space: nowrap;
  809. }
  810. &:not(.sharing-entry--share) &__actions {
  811. .new-share-link {
  812. border-top: 1px solid var(--color-border);
  813. }
  814. }
  815. ::v-deep .avatar-link-share {
  816. background-color: var(--color-primary);
  817. }
  818. .sharing-entry__action--public-upload {
  819. border-bottom: 1px solid var(--color-border);
  820. }
  821. &__loading {
  822. width: 44px;
  823. height: 44px;
  824. margin: 0;
  825. padding: 14px;
  826. margin-left: auto;
  827. }
  828. // put menus to the left
  829. // but only the first one
  830. .action-item {
  831. margin-left: auto;
  832. ~ .action-item,
  833. ~ .sharing-entry__loading {
  834. margin-left: 0;
  835. }
  836. }
  837. .icon-checkmark-color {
  838. opacity: 1;
  839. }
  840. }
  841. </style>