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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941
  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. <Avatar :is-no-user="true"
  25. :class="isEmailShareType ? 'icon-mail-white' : 'icon-public-white'"
  26. class="sharing-entry__avatar" />
  27. <div class="sharing-entry__desc">
  28. <h5 :title="title">
  29. {{ title }}
  30. </h5>
  31. </div>
  32. <!-- clipboard -->
  33. <Actions v-if="share && !isEmailShareType && share.token"
  34. ref="copyButton"
  35. class="sharing-entry__copy">
  36. <ActionLink :href="shareLink"
  37. target="_blank"
  38. :icon="copied && copySuccess ? 'icon-checkmark-color' : 'icon-clippy'"
  39. @click.stop.prevent="copyLink">
  40. {{ clipboardTooltip }}
  41. </ActionLink>
  42. </Actions>
  43. <!-- pending actions -->
  44. <Actions v-if="!loading && (pendingPassword || pendingExpirationDate)"
  45. class="sharing-entry__actions"
  46. menu-align="right"
  47. :open.sync="open"
  48. @close="onNewLinkShare">
  49. <!-- pending data menu -->
  50. <ActionText v-if="errors.pending"
  51. icon="icon-error"
  52. :class="{ error: errors.pending}">
  53. {{ errors.pending }}
  54. </ActionText>
  55. <ActionText v-else icon="icon-info">
  56. {{ t('files_sharing', 'Please enter the following required information before creating the share') }}
  57. </ActionText>
  58. <!-- password -->
  59. <ActionText v-if="pendingPassword" icon="icon-password">
  60. {{ t('files_sharing', 'Password protection (enforced)') }}
  61. </ActionText>
  62. <ActionCheckbox v-else-if="config.enableLinkPasswordByDefault"
  63. :checked.sync="isPasswordProtected"
  64. :disabled="config.enforcePasswordForPublicLink || saving"
  65. class="share-link-password-checkbox"
  66. @uncheck="onPasswordDisable">
  67. {{ t('files_sharing', 'Password protection') }}
  68. </ActionCheckbox>
  69. <ActionInput v-if="pendingPassword || share.password"
  70. v-tooltip.auto="{
  71. content: errors.password,
  72. show: errors.password,
  73. trigger: 'manual',
  74. defaultContainer: '#app-sidebar'
  75. }"
  76. class="share-link-password"
  77. :value.sync="share.password"
  78. :disabled="saving"
  79. :required="config.enableLinkPasswordByDefault || config.enforcePasswordForPublicLink"
  80. :minlength="isPasswordPolicyEnabled && config.passwordPolicy.minLength"
  81. icon=""
  82. autocomplete="new-password"
  83. @submit="onNewLinkShare">
  84. {{ t('files_sharing', 'Enter a password') }}
  85. </ActionInput>
  86. <!-- expiration date -->
  87. <ActionText v-if="pendingExpirationDate" icon="icon-calendar-dark">
  88. {{ t('files_sharing', 'Expiration date (enforced)') }}
  89. </ActionText>
  90. <ActionInput v-if="pendingExpirationDate"
  91. v-model="share.expireDate"
  92. v-tooltip.auto="{
  93. content: errors.expireDate,
  94. show: errors.expireDate,
  95. trigger: 'manual',
  96. defaultContainer: '#app-sidebar'
  97. }"
  98. class="share-link-expire-date"
  99. :disabled="saving"
  100. :first-day-of-week="firstDay"
  101. :lang="lang"
  102. icon=""
  103. type="date"
  104. value-type="format"
  105. :disabled-date="disabledDate">
  106. <!-- let's not submit when picked, the user
  107. might want to still edit or copy the password -->
  108. {{ t('files_sharing', 'Enter a date') }}
  109. </ActionInput>
  110. <ActionButton icon="icon-close" @click.prevent.stop="onCancel">
  111. {{ t('files_sharing', 'Cancel') }}
  112. </ActionButton>
  113. </Actions>
  114. <!-- actions -->
  115. <Actions v-else-if="!loading"
  116. class="sharing-entry__actions"
  117. menu-align="right"
  118. :open.sync="open"
  119. @close="onMenuClose">
  120. <template v-if="share">
  121. <template v-if="share.canEdit">
  122. <!-- Custom Label -->
  123. <ActionInput
  124. ref="label"
  125. v-tooltip.auto="{
  126. content: errors.label,
  127. show: errors.label,
  128. trigger: 'manual',
  129. defaultContainer: '.app-sidebar'
  130. }"
  131. :class="{ error: errors.label }"
  132. :disabled="saving"
  133. :placeholder="t('files_sharing', 'Share label')"
  134. :aria-label="t('files_sharing', 'Share label')"
  135. :value="share.newLabel || share.label"
  136. icon="icon-edit"
  137. maxlength="255"
  138. @update:value="onLabelChange"
  139. @submit="onLabelSubmit" />
  140. <!-- folder -->
  141. <template v-if="isFolder && fileHasCreatePermission && config.isPublicUploadEnabled">
  142. <ActionRadio :checked="sharePermissions === publicUploadRValue"
  143. :value="publicUploadRValue"
  144. :name="randomId"
  145. :disabled="saving"
  146. @change="togglePermissions">
  147. {{ t('files_sharing', 'Read only') }}
  148. </ActionRadio>
  149. <ActionRadio :checked="sharePermissions === publicUploadRWValue"
  150. :value="publicUploadRWValue"
  151. :disabled="saving"
  152. :name="randomId"
  153. @change="togglePermissions">
  154. {{ t('files_sharing', 'Allow upload and editing') }}
  155. </ActionRadio>
  156. <ActionRadio :checked="sharePermissions === publicUploadWValue"
  157. :value="publicUploadWValue"
  158. :disabled="saving"
  159. :name="randomId"
  160. class="sharing-entry__action--public-upload"
  161. @change="togglePermissions">
  162. {{ t('files_sharing', 'File drop (upload only)') }}
  163. </ActionRadio>
  164. </template>
  165. <!-- file -->
  166. <ActionCheckbox v-else
  167. :checked.sync="canUpdate"
  168. :disabled="saving"
  169. @change="queueUpdate('permissions')">
  170. {{ t('files_sharing', 'Allow editing') }}
  171. </ActionCheckbox>
  172. <ActionCheckbox
  173. :checked.sync="share.hideDownload"
  174. :disabled="saving"
  175. @change="queueUpdate('hideDownload')">
  176. {{ t('files_sharing', 'Hide download') }}
  177. </ActionCheckbox>
  178. <!-- password -->
  179. <ActionCheckbox :checked.sync="isPasswordProtected"
  180. :disabled="config.enforcePasswordForPublicLink || saving"
  181. class="share-link-password-checkbox"
  182. @uncheck="onPasswordDisable">
  183. {{ config.enforcePasswordForPublicLink
  184. ? t('files_sharing', 'Password protection (enforced)')
  185. : t('files_sharing', 'Password protect') }}
  186. </ActionCheckbox>
  187. <ActionInput v-if="isPasswordProtected"
  188. ref="password"
  189. v-tooltip.auto="{
  190. content: errors.password,
  191. show: errors.password,
  192. trigger: 'manual',
  193. defaultContainer: '#app-sidebar'
  194. }"
  195. class="share-link-password"
  196. :class="{ error: errors.password}"
  197. :disabled="saving"
  198. :required="config.enforcePasswordForPublicLink"
  199. :value="hasUnsavedPassword ? share.newPassword : '***************'"
  200. icon="icon-password"
  201. autocomplete="new-password"
  202. :type="hasUnsavedPassword ? 'text': 'password'"
  203. @update:value="onPasswordChange"
  204. @submit="onPasswordSubmit">
  205. {{ t('files_sharing', 'Enter a password') }}
  206. </ActionInput>
  207. <!-- password protected by Talk -->
  208. <ActionCheckbox v-if="isPasswordProtectedByTalkAvailable"
  209. :checked.sync="isPasswordProtectedByTalk"
  210. :disabled="!canTogglePasswordProtectedByTalkAvailable || saving"
  211. class="share-link-password-talk-checkbox"
  212. @change="onPasswordProtectedByTalkChange">
  213. {{ t('files_sharing', 'Video verification') }}
  214. </ActionCheckbox>
  215. <!-- expiration date -->
  216. <ActionCheckbox :checked.sync="hasExpirationDate"
  217. :disabled="config.isDefaultExpireDateEnforced || saving"
  218. class="share-link-expire-date-checkbox"
  219. @uncheck="onExpirationDisable">
  220. {{ config.isDefaultExpireDateEnforced
  221. ? t('files_sharing', 'Expiration date (enforced)')
  222. : t('files_sharing', 'Set expiration date') }}
  223. </ActionCheckbox>
  224. <ActionInput v-if="hasExpirationDate"
  225. ref="expireDate"
  226. v-tooltip.auto="{
  227. content: errors.expireDate,
  228. show: errors.expireDate,
  229. trigger: 'manual',
  230. defaultContainer: '#app-sidebar'
  231. }"
  232. class="share-link-expire-date"
  233. :class="{ error: errors.expireDate}"
  234. :disabled="saving"
  235. :first-day-of-week="firstDay"
  236. :lang="lang"
  237. :value="share.expireDate"
  238. value-type="format"
  239. icon="icon-calendar-dark"
  240. type="date"
  241. :disabled-date="disabledDate"
  242. @update:value="onExpirationChange">
  243. {{ t('files_sharing', 'Enter a date') }}
  244. </ActionInput>
  245. <!-- note -->
  246. <ActionCheckbox :checked.sync="hasNote"
  247. :disabled="saving"
  248. @uncheck="queueUpdate('note')">
  249. {{ t('files_sharing', 'Note to recipient') }}
  250. </ActionCheckbox>
  251. <ActionTextEditable v-if="hasNote"
  252. ref="note"
  253. v-tooltip.auto="{
  254. content: errors.note,
  255. show: errors.note,
  256. trigger: 'manual',
  257. defaultContainer: '#app-sidebar'
  258. }"
  259. :class="{ error: errors.note}"
  260. :disabled="saving"
  261. :placeholder="t('files_sharing', 'Enter a note for the share recipient')"
  262. :value="share.newNote || share.note"
  263. icon="icon-edit"
  264. @update:value="onNoteChange"
  265. @submit="onNoteSubmit" />
  266. </template>
  267. <!-- external sharing via url (social...) -->
  268. <ActionLink v-for="({icon, url, name}, index) in externalActions"
  269. :key="index"
  270. :href="url(shareLink)"
  271. :icon="icon"
  272. target="_blank">
  273. {{ name }}
  274. </ActionLink>
  275. <ActionButton v-if="share.canDelete"
  276. icon="icon-close"
  277. :disabled="saving"
  278. @click.prevent="onDelete">
  279. {{ t('files_sharing', 'Unshare') }}
  280. </ActionButton>
  281. <ActionButton v-if="!isEmailShareType && canReshare"
  282. class="new-share-link"
  283. icon="icon-add"
  284. @click.prevent.stop="onNewLinkShare">
  285. {{ t('files_sharing', 'Add another link') }}
  286. </ActionButton>
  287. </template>
  288. <!-- Create new share -->
  289. <ActionButton v-else-if="canReshare"
  290. class="new-share-link"
  291. icon="icon-add"
  292. @click.prevent.stop="onNewLinkShare">
  293. {{ t('files_sharing', 'Create a new share link') }}
  294. </ActionButton>
  295. </Actions>
  296. <!-- loading indicator to replace the menu -->
  297. <div v-else class="icon-loading-small sharing-entry__loading" />
  298. </li>
  299. </template>
  300. <script>
  301. import { generateUrl } from '@nextcloud/router'
  302. import axios from '@nextcloud/axios'
  303. import Vue from 'vue'
  304. import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
  305. import ActionCheckbox from '@nextcloud/vue/dist/Components/ActionCheckbox'
  306. import ActionRadio from '@nextcloud/vue/dist/Components/ActionRadio'
  307. import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
  308. import ActionText from '@nextcloud/vue/dist/Components/ActionText'
  309. import ActionTextEditable from '@nextcloud/vue/dist/Components/ActionTextEditable'
  310. import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
  311. import Actions from '@nextcloud/vue/dist/Components/Actions'
  312. import Avatar from '@nextcloud/vue/dist/Components/Avatar'
  313. import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
  314. import Share from '../models/Share'
  315. import SharesMixin from '../mixins/SharesMixin'
  316. const passwordSet = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789'
  317. export default {
  318. name: 'SharingEntryLink',
  319. components: {
  320. Actions,
  321. ActionButton,
  322. ActionCheckbox,
  323. ActionRadio,
  324. ActionInput,
  325. ActionLink,
  326. ActionText,
  327. ActionTextEditable,
  328. Avatar,
  329. },
  330. directives: {
  331. Tooltip,
  332. },
  333. mixins: [SharesMixin],
  334. props: {
  335. canReshare: {
  336. type: Boolean,
  337. default: true,
  338. },
  339. },
  340. data() {
  341. return {
  342. copySuccess: true,
  343. copied: false,
  344. publicUploadRWValue: OC.PERMISSION_UPDATE | OC.PERMISSION_CREATE | OC.PERMISSION_READ | OC.PERMISSION_DELETE,
  345. publicUploadRValue: OC.PERMISSION_READ,
  346. publicUploadWValue: OC.PERMISSION_CREATE,
  347. ExternalLinkActions: OCA.Sharing.ExternalLinkActions.state,
  348. }
  349. },
  350. computed: {
  351. /**
  352. * Return the current share permissions
  353. * We always ignore the SHARE permission as this is used for the
  354. * federated sharing.
  355. * @returns {number}
  356. */
  357. sharePermissions() {
  358. return this.share.permissions & ~OC.PERMISSION_SHARE
  359. },
  360. /**
  361. * Generate a unique random id for this SharingEntryLink only
  362. * This allows ActionRadios to have the same name prop
  363. * but not to impact others SharingEntryLink
  364. * @returns {string}
  365. */
  366. randomId() {
  367. return Math.random().toString(27).substr(2)
  368. },
  369. /**
  370. * Link share label
  371. * TODO: allow editing
  372. * @returns {string}
  373. */
  374. title() {
  375. // if we have a valid existing share (not pending)
  376. if (this.share && this.share.id) {
  377. if (!this.isShareOwner && this.share.ownerDisplayName) {
  378. return t('files_sharing', 'Shared via link by {initiator}', {
  379. initiator: this.share.ownerDisplayName,
  380. })
  381. }
  382. if (this.share.label && this.share.label.trim() !== '') {
  383. return t('files_sharing', 'Share link ({label})', {
  384. label: this.share.label.trim(),
  385. })
  386. }
  387. if (this.isEmailShareType) {
  388. return this.share.shareWith
  389. }
  390. }
  391. return t('files_sharing', 'Share link')
  392. },
  393. /**
  394. * Does the current share have an expiration date
  395. * @returns {boolean}
  396. */
  397. hasExpirationDate: {
  398. get() {
  399. return this.config.isDefaultExpireDateEnforced
  400. || !!this.share.expireDate
  401. },
  402. set(enabled) {
  403. let dateString = moment(this.config.defaultExpirationDateString)
  404. if (!dateString.isValid()) {
  405. dateString = moment()
  406. }
  407. this.share.state.expiration = enabled
  408. ? dateString.format('YYYY-MM-DD')
  409. : ''
  410. console.debug('Expiration date status', enabled, this.share.expireDate)
  411. },
  412. },
  413. dateMaxEnforced() {
  414. return this.config.isDefaultExpireDateEnforced
  415. && moment().add(1 + this.config.defaultExpireDate, 'days')
  416. },
  417. /**
  418. * Is the current share password protected ?
  419. * @returns {boolean}
  420. */
  421. isPasswordProtected: {
  422. get() {
  423. return this.config.enforcePasswordForPublicLink
  424. || !!this.share.password
  425. },
  426. async set(enabled) {
  427. // TODO: directly save after generation to make sure the share is always protected
  428. Vue.set(this.share, 'password', enabled ? await this.generatePassword() : '')
  429. Vue.set(this.share, 'newPassword', this.share.password)
  430. },
  431. },
  432. /**
  433. * Is Talk enabled?
  434. * @returns {boolean}
  435. */
  436. isTalkEnabled() {
  437. return OC.appswebroots['spreed'] !== undefined
  438. },
  439. /**
  440. * Is it possible to protect the password by Talk?
  441. * @returns {boolean}
  442. */
  443. isPasswordProtectedByTalkAvailable() {
  444. return this.isPasswordProtected && this.isTalkEnabled
  445. },
  446. /**
  447. * Is the current share password protected by Talk?
  448. * @returns {boolean}
  449. */
  450. isPasswordProtectedByTalk: {
  451. get: function() {
  452. return this.share.sendPasswordByTalk
  453. },
  454. set: async function(enabled) {
  455. this.share.sendPasswordByTalk = enabled
  456. },
  457. },
  458. /**
  459. * Is the current share an email share ?
  460. * @returns {boolean}
  461. */
  462. isEmailShareType() {
  463. return this.share
  464. ? this.share.type === this.SHARE_TYPES.SHARE_TYPE_EMAIL
  465. : false
  466. },
  467. canTogglePasswordProtectedByTalkAvailable() {
  468. if (!this.isPasswordProtected) {
  469. // Makes no sense
  470. return false
  471. } else if (this.isEmailShareType && !this.hasUnsavedPassword) {
  472. // For email shares we need a new password in order to enable or
  473. // disable
  474. return false
  475. }
  476. // Anything else should be fine
  477. return true
  478. },
  479. /**
  480. * Pending data.
  481. * If the share still doesn't have an id, it is not synced
  482. * Therefore this is still not valid and requires user input
  483. * @returns {boolean}
  484. */
  485. pendingPassword() {
  486. return this.config.enforcePasswordForPublicLink && this.share && !this.share.id
  487. },
  488. pendingExpirationDate() {
  489. return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id
  490. },
  491. /**
  492. * Can the recipient edit the file ?
  493. * @returns {boolean}
  494. */
  495. canUpdate: {
  496. get: function() {
  497. return this.share.hasUpdatePermission
  498. },
  499. set: function(enabled) {
  500. this.share.permissions = enabled
  501. ? OC.PERMISSION_READ | OC.PERMISSION_UPDATE
  502. : OC.PERMISSION_READ
  503. },
  504. },
  505. // if newPassword exists, but is empty, it means
  506. // the user deleted the original password
  507. hasUnsavedPassword() {
  508. return this.share.newPassword !== undefined
  509. },
  510. /**
  511. * Is the current share a folder ?
  512. * TODO: move to a proper FileInfo model?
  513. * @returns {boolean}
  514. */
  515. isFolder() {
  516. return this.fileInfo.type === 'dir'
  517. },
  518. /**
  519. * Does the current file/folder have create permissions
  520. * TODO: move to a proper FileInfo model?
  521. * @returns {boolean}
  522. */
  523. fileHasCreatePermission() {
  524. return !!(this.fileInfo.permissions & OC.PERMISSION_CREATE)
  525. },
  526. /**
  527. * Return the public share link
  528. * @returns {string}
  529. */
  530. shareLink() {
  531. return window.location.protocol + '//' + window.location.host + generateUrl('/s/') + this.share.token
  532. },
  533. /**
  534. * Clipboard v-tooltip message
  535. * @returns {string}
  536. */
  537. clipboardTooltip() {
  538. if (this.copied) {
  539. return this.copySuccess
  540. ? t('files_sharing', 'Link copied')
  541. : t('files_sharing', 'Cannot copy, please copy the link manually')
  542. }
  543. return t('files_sharing', 'Copy to clipboard')
  544. },
  545. /**
  546. * External aditionnal actions for the menu
  547. * @returns {Array}
  548. */
  549. externalActions() {
  550. return this.ExternalLinkActions.actions
  551. },
  552. isPasswordPolicyEnabled() {
  553. return typeof this.config.passwordPolicy === 'object'
  554. },
  555. },
  556. methods: {
  557. /**
  558. * Create a new share link and append it to the list
  559. */
  560. async onNewLinkShare() {
  561. const shareDefaults = {
  562. share_type: OC.Share.SHARE_TYPE_LINK,
  563. }
  564. if (this.config.isDefaultExpireDateEnforced) {
  565. // default is empty string if not set
  566. // expiration is the share object key, not expireDate
  567. shareDefaults.expiration = this.config.defaultExpirationDateString
  568. }
  569. if (this.config.enableLinkPasswordByDefault) {
  570. shareDefaults.password = await this.generatePassword()
  571. }
  572. // do not push yet if we need a password or an expiration date
  573. if (this.config.enforcePasswordForPublicLink || this.config.isDefaultExpireDateEnforced) {
  574. this.loading = true
  575. // if a share already exists, pushing it
  576. if (this.share && !this.share.id) {
  577. if (this.checkShare(this.share)) {
  578. await this.pushNewLinkShare(this.share, true)
  579. return true
  580. } else {
  581. this.open = true
  582. OC.Notification.showTemporary(t('files_sharing', 'Error, please enter proper password and/or expiration date'))
  583. return false
  584. }
  585. }
  586. // ELSE, show the pending popovermenu
  587. // if password enforced, pre-fill with random one
  588. if (this.config.enforcePasswordForPublicLink) {
  589. shareDefaults.password = await this.generatePassword()
  590. }
  591. // create share & close menu
  592. const share = new Share(shareDefaults)
  593. const component = await new Promise(resolve => {
  594. this.$emit('add:share', share, resolve)
  595. })
  596. // open the menu on the
  597. // freshly created share component
  598. this.open = false
  599. this.loading = false
  600. component.open = true
  601. // Nothing enforced, creating share directly
  602. } else {
  603. const share = new Share(shareDefaults)
  604. await this.pushNewLinkShare(share)
  605. }
  606. },
  607. /**
  608. * Push a new link share to the server
  609. * And update or append to the list
  610. * accordingly
  611. *
  612. * @param {Share} share the new share
  613. * @param {boolean} [update=false] do we update the current share ?
  614. */
  615. async pushNewLinkShare(share, update) {
  616. try {
  617. this.loading = true
  618. this.errors = {}
  619. const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
  620. const newShare = await this.createShare({
  621. path,
  622. shareType: OC.Share.SHARE_TYPE_LINK,
  623. password: share.password,
  624. expireDate: share.expireDate,
  625. // we do not allow setting the publicUpload
  626. // before the share creation.
  627. // Todo: We also need to fix the createShare method in
  628. // lib/Controller/ShareAPIController.php to allow file drop
  629. // (currently not supported on create, only update)
  630. })
  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. component.copyLink()
  651. } catch ({ response }) {
  652. const message = response.data.ocs.meta.message
  653. if (message.match(/password/i)) {
  654. this.onSyncError('password', message)
  655. } else if (message.match(/date/i)) {
  656. this.onSyncError('expireDate', message)
  657. } else {
  658. this.onSyncError('pending', message)
  659. }
  660. } finally {
  661. this.loading = false
  662. }
  663. },
  664. /**
  665. * On permissions change
  666. * @param {Event} event js event
  667. */
  668. togglePermissions(event) {
  669. const permissions = parseInt(event.target.value, 10)
  670. this.share.permissions = permissions
  671. this.queueUpdate('permissions')
  672. },
  673. /**
  674. * Label changed, let's save it to a different key
  675. * @param {String} label the share label
  676. */
  677. onLabelChange(label) {
  678. this.$set(this.share, 'newLabel', label.trim())
  679. },
  680. /**
  681. * When the note change, we trim, save and dispatch
  682. */
  683. onLabelSubmit() {
  684. if (typeof this.share.newLabel === 'string') {
  685. this.share.label = this.share.newLabel
  686. this.$delete(this.share, 'newLabel')
  687. this.queueUpdate('label')
  688. }
  689. },
  690. /**
  691. * Generate a valid policy password or
  692. * request a valid password if password_policy
  693. * is enabled
  694. *
  695. * @returns {string} a valid password
  696. */
  697. async generatePassword() {
  698. // password policy is enabled, let's request a pass
  699. if (this.config.passwordPolicy.api && this.config.passwordPolicy.api.generate) {
  700. try {
  701. const request = await axios.get(this.config.passwordPolicy.api.generate)
  702. if (request.data.ocs.data.password) {
  703. return request.data.ocs.data.password
  704. }
  705. } catch (error) {
  706. console.info('Error generating password from password_policy', error)
  707. }
  708. }
  709. // generate password of 10 length based on passwordSet
  710. return Array(10).fill(0)
  711. .reduce((prev, curr) => {
  712. prev += passwordSet.charAt(Math.floor(Math.random() * passwordSet.length))
  713. return prev
  714. }, '')
  715. },
  716. async copyLink() {
  717. try {
  718. await this.$copyText(this.shareLink)
  719. // focus and show the tooltip
  720. this.$refs.copyButton.$el.focus()
  721. this.copySuccess = true
  722. this.copied = true
  723. } catch (error) {
  724. this.copySuccess = false
  725. this.copied = true
  726. console.error(error)
  727. } finally {
  728. setTimeout(() => {
  729. this.copySuccess = false
  730. this.copied = false
  731. }, 4000)
  732. }
  733. },
  734. /**
  735. * Update newPassword values
  736. * of share. If password is set but not newPassword
  737. * then the user did not changed the password
  738. * If both co-exists, the password have changed and
  739. * we show it in plain text.
  740. * Then on submit (or menu close), we sync it.
  741. * @param {string} password the changed password
  742. */
  743. onPasswordChange(password) {
  744. this.$set(this.share, 'newPassword', password)
  745. },
  746. /**
  747. * Uncheck password protection
  748. * We need this method because @update:checked
  749. * is ran simultaneously as @uncheck, so
  750. * so we cannot ensure data is up-to-date
  751. */
  752. onPasswordDisable() {
  753. this.share.password = ''
  754. // reset password state after sync
  755. this.$delete(this.share, 'newPassword')
  756. // only update if valid share.
  757. if (this.share.id) {
  758. this.queueUpdate('password')
  759. }
  760. },
  761. /**
  762. * Menu have been closed or password has been submited.
  763. * The only property that does not get
  764. * synced automatically is the password
  765. * So let's check if we have an unsaved
  766. * password.
  767. * expireDate is saved on datepicker pick
  768. * or close.
  769. */
  770. onPasswordSubmit() {
  771. if (this.hasUnsavedPassword) {
  772. this.share.password = this.share.newPassword.trim()
  773. this.queueUpdate('password')
  774. }
  775. },
  776. /**
  777. * Update the password along with "sendPasswordByTalk".
  778. *
  779. * If the password was modified the new password is sent; otherwise
  780. * updating a mail share would fail, as in that case it is required that
  781. * a new password is set when enabling or disabling
  782. * "sendPasswordByTalk".
  783. */
  784. onPasswordProtectedByTalkChange() {
  785. if (this.hasUnsavedPassword) {
  786. this.share.password = this.share.newPassword.trim()
  787. }
  788. this.queueUpdate('sendPasswordByTalk', 'password')
  789. },
  790. /**
  791. * Save potential changed data on menu close
  792. */
  793. onMenuClose() {
  794. this.onPasswordSubmit()
  795. this.onNoteSubmit()
  796. },
  797. /**
  798. * Cancel the share creation
  799. * Used in the pending popover
  800. */
  801. onCancel() {
  802. // this.share already exists at this point,
  803. // but is incomplete as not pushed to server
  804. // YET. We can safely delete the share :)
  805. this.$emit('remove:share', this.share)
  806. },
  807. },
  808. }
  809. </script>
  810. <style lang="scss" scoped>
  811. .sharing-entry {
  812. display: flex;
  813. align-items: center;
  814. min-height: 44px;
  815. &__desc {
  816. display: flex;
  817. flex-direction: column;
  818. justify-content: space-between;
  819. padding: 8px;
  820. line-height: 1.2em;
  821. overflow: hidden;
  822. h5 {
  823. text-overflow: ellipsis;
  824. overflow: hidden;
  825. white-space: nowrap;
  826. }
  827. }
  828. &:not(.sharing-entry--share) &__actions {
  829. .new-share-link {
  830. border-top: 1px solid var(--color-border);
  831. }
  832. }
  833. .sharing-entry__action--public-upload {
  834. border-bottom: 1px solid var(--color-border);
  835. }
  836. &__loading {
  837. width: 44px;
  838. height: 44px;
  839. margin: 0;
  840. padding: 14px;
  841. margin-left: auto;
  842. }
  843. // put menus to the left
  844. // but only the first one
  845. .action-item {
  846. margin-left: auto;
  847. ~ .action-item,
  848. ~ .sharing-entry__loading {
  849. margin-left: 0;
  850. }
  851. }
  852. .icon-checkmark-color {
  853. opacity: 1;
  854. }
  855. }
  856. </style>