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

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