share.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /**
  2. * Copyright (c) 2014
  3. *
  4. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author John Molakvoæ <skjnldsv@protonmail.com>
  9. * @author Julius Härtl <jus@bitgrid.net>
  10. * @author Maxence Lange <maxence@nextcloud.com>
  11. * @author Michael Jobst <mjobst+github@tecratech.de>
  12. * @author Michael Jobst <mjobst@necls.com>
  13. * @author Morris Jobke <hey@morrisjobke.de>
  14. * @author Roeland Jago Douma <roeland@famdouma.nl>
  15. * @author Samuel <faust64@gmail.com>
  16. * @author Vincent Petry <vincent@nextcloud.com>
  17. *
  18. * @license AGPL-3.0-or-later
  19. *
  20. * This program is free software: you can redistribute it and/or modify
  21. * it under the terms of the GNU Affero General Public License as
  22. * published by the Free Software Foundation, either version 3 of the
  23. * License, or (at your option) any later version.
  24. *
  25. * This program is distributed in the hope that it will be useful,
  26. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  27. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  28. * GNU Affero General Public License for more details.
  29. *
  30. * You should have received a copy of the GNU Affero General Public License
  31. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  32. *
  33. */
  34. /* eslint-disable */
  35. import escapeHTML from 'escape-html'
  36. import { Type as ShareTypes } from '@nextcloud/sharing'
  37. import { getCapabilities } from '@nextcloud/capabilities'
  38. (function() {
  39. _.extend(OC.Files.Client, {
  40. PROPERTY_SHARE_TYPES: '{' + OC.Files.Client.NS_OWNCLOUD + '}share-types',
  41. PROPERTY_OWNER_ID: '{' + OC.Files.Client.NS_OWNCLOUD + '}owner-id',
  42. PROPERTY_OWNER_DISPLAY_NAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}owner-display-name'
  43. })
  44. if (!OCA.Sharing) {
  45. OCA.Sharing = {}
  46. }
  47. /**
  48. * @namespace
  49. */
  50. OCA.Sharing.Util = {
  51. /**
  52. * Regular expression for splitting parts of remote share owners:
  53. * "user@example.com/"
  54. * "user@example.com/path/to/owncloud"
  55. * "user@anotherexample.com@example.com/path/to/owncloud
  56. */
  57. _REMOTE_OWNER_REGEXP: new RegExp('^(([^@]*)@(([^@^/\\s]*)@)?)((https://)?[^[\\s/]*)([/](.*))?$'),
  58. /**
  59. * Initialize the sharing plugin.
  60. *
  61. * Registers the "Share" file action and adds additional
  62. * DOM attributes for the sharing file info.
  63. *
  64. * @param {OCA.Files.FileList} fileList file list to be extended
  65. */
  66. attach: function(fileList) {
  67. // core sharing is disabled/not loaded
  68. if (!getCapabilities().files_sharing?.api_enabled) {
  69. return
  70. }
  71. if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
  72. return
  73. }
  74. var fileActions = fileList.fileActions
  75. var oldCreateRow = fileList._createRow
  76. fileList._createRow = function(fileData) {
  77. var tr = oldCreateRow.apply(this, arguments)
  78. var sharePermissions = OCA.Sharing.Util.getSharePermissions(fileData)
  79. if (fileData.permissions === 0) {
  80. // no permission, disabling sidebar
  81. delete fileActions.actions.all.Comment
  82. delete fileActions.actions.all.Details
  83. delete fileActions.actions.all.Goto
  84. }
  85. if (_.isFunction(fileData.canDownload) && !fileData.canDownload()) {
  86. delete fileActions.actions.all.Download
  87. if (fileData.permissions & OC.PERMISSION_UPDATE === 0) {
  88. // neither move nor copy is allowed, remove the action completely
  89. delete fileActions.actions.all.MoveCopy
  90. }
  91. }
  92. tr.attr('data-share-permissions', sharePermissions)
  93. tr.attr('data-share-attributes', JSON.stringify(fileData.shareAttributes))
  94. if (fileData.shareOwner) {
  95. tr.attr('data-share-owner', fileData.shareOwner)
  96. tr.attr('data-share-owner-id', fileData.shareOwnerId)
  97. // user should always be able to rename a mount point
  98. if (fileData.mountType === 'shared-root') {
  99. tr.attr('data-permissions', fileData.permissions | OC.PERMISSION_UPDATE)
  100. }
  101. }
  102. if (fileData.recipientData && !_.isEmpty(fileData.recipientData)) {
  103. tr.attr('data-share-recipient-data', JSON.stringify(fileData.recipientData))
  104. }
  105. if (fileData.shareTypes) {
  106. tr.attr('data-share-types', fileData.shareTypes.join(','))
  107. }
  108. return tr
  109. }
  110. var oldElementToFile = fileList.elementToFile
  111. fileList.elementToFile = function($el) {
  112. var fileInfo = oldElementToFile.apply(this, arguments)
  113. fileInfo.shareAttributes = JSON.parse($el.attr('data-share-attributes') || '[]')
  114. fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined
  115. fileInfo.shareOwner = $el.attr('data-share-owner') || undefined
  116. fileInfo.shareOwnerId = $el.attr('data-share-owner-id') || undefined
  117. if ($el.attr('data-share-types')) {
  118. fileInfo.shareTypes = $el.attr('data-share-types').split(',')
  119. }
  120. if ($el.attr('data-expiration')) {
  121. var expirationTimestamp = parseInt($el.attr('data-expiration'))
  122. fileInfo.shares = []
  123. fileInfo.shares.push({ expiration: expirationTimestamp })
  124. }
  125. return fileInfo
  126. }
  127. var oldGetWebdavProperties = fileList._getWebdavProperties
  128. fileList._getWebdavProperties = function() {
  129. var props = oldGetWebdavProperties.apply(this, arguments)
  130. props.push(OC.Files.Client.PROPERTY_OWNER_ID)
  131. props.push(OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME)
  132. props.push(OC.Files.Client.PROPERTY_SHARE_TYPES)
  133. return props
  134. }
  135. fileList.filesClient.addFileInfoParser(function(response) {
  136. var data = {}
  137. var props = response.propStat[0].properties
  138. var permissionsProp = props[OC.Files.Client.PROPERTY_PERMISSIONS]
  139. if (permissionsProp && permissionsProp.indexOf('S') >= 0) {
  140. data.shareOwner = props[OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME]
  141. data.shareOwnerId = props[OC.Files.Client.PROPERTY_OWNER_ID]
  142. }
  143. var shareTypesProp = props[OC.Files.Client.PROPERTY_SHARE_TYPES]
  144. if (shareTypesProp) {
  145. data.shareTypes = _.chain(shareTypesProp).filter(function(xmlvalue) {
  146. return (xmlvalue.namespaceURI === OC.Files.Client.NS_OWNCLOUD && xmlvalue.nodeName.split(':')[1] === 'share-type')
  147. }).map(function(xmlvalue) {
  148. return parseInt(xmlvalue.textContent || xmlvalue.text, 10)
  149. }).value()
  150. }
  151. return data
  152. })
  153. // use delegate to catch the case with multiple file lists
  154. fileList.$el.on('fileActionsReady', function(ev) {
  155. var $files = ev.$files
  156. _.each($files, function(file) {
  157. var $tr = $(file)
  158. var shareTypesStr = $tr.attr('data-share-types') || ''
  159. var shareOwner = $tr.attr('data-share-owner')
  160. if (shareTypesStr || shareOwner) {
  161. var hasLink = false
  162. var hasShares = false
  163. _.each(shareTypesStr.split(',') || [], function(shareTypeStr) {
  164. let shareType = parseInt(shareTypeStr, 10)
  165. if (shareType === ShareTypes.SHARE_TYPE_LINK) {
  166. hasLink = true
  167. } else if (shareType === ShareTypes.SHARE_TYPE_EMAIL) {
  168. hasLink = true
  169. } else if (shareType === ShareTypes.SHARE_TYPE_USER) {
  170. hasShares = true
  171. } else if (shareType === ShareTypes.SHARE_TYPE_GROUP) {
  172. hasShares = true
  173. } else if (shareType === ShareTypes.SHARE_TYPE_REMOTE) {
  174. hasShares = true
  175. } else if (shareType === ShareTypes.SHARE_TYPE_REMOTE_GROUP) {
  176. hasShares = true
  177. } else if (shareType === ShareTypes.SHARE_TYPE_CIRCLE) {
  178. hasShares = true
  179. } else if (shareType === ShareTypes.SHARE_TYPE_ROOM) {
  180. hasShares = true
  181. } else if (shareType === ShareTypes.SHARE_TYPE_DECK) {
  182. hasShares = true
  183. }
  184. })
  185. OCA.Sharing.Util._updateFileActionIcon($tr, hasShares, hasLink)
  186. }
  187. })
  188. })
  189. fileList.$el.on('changeDirectory', function() {
  190. OCA.Sharing.sharesLoaded = false
  191. })
  192. fileActions.registerAction({
  193. name: 'Share',
  194. displayName: function(context) {
  195. if (context && context.$file) {
  196. var shareType = parseInt(context.$file.data('share-types'), 10)
  197. var shareOwner = context.$file.data('share-owner-id')
  198. if (shareType >= 0 || shareOwner) {
  199. return t('files_sharing', 'Shared')
  200. }
  201. }
  202. return t('files_sharing', 'Share')
  203. },
  204. altText: t('files_sharing', 'Share'),
  205. mime: 'all',
  206. order: -150,
  207. permissions: OC.PERMISSION_ALL,
  208. iconClass: function(fileName, context) {
  209. var shareType = parseInt(context.$file.data('share-types'), 10)
  210. if (shareType === ShareTypes.SHARE_TYPE_EMAIL
  211. || shareType === ShareTypes.SHARE_TYPE_LINK) {
  212. return 'icon-public'
  213. }
  214. return 'icon-shared'
  215. },
  216. icon: function(fileName, context) {
  217. var shareOwner = context.$file.data('share-owner-id')
  218. if (shareOwner) {
  219. return OC.generateUrl(`/avatar/${shareOwner}/32`)
  220. }
  221. },
  222. type: OCA.Files.FileActions.TYPE_INLINE,
  223. actionHandler: function(fileName, context) {
  224. // details view disabled in some share lists
  225. if (!fileList._detailsView) {
  226. return
  227. }
  228. // do not open sidebar if permission is set and equal to 0
  229. var permissions = parseInt(context.$file.data('share-permissions'), 10)
  230. if (isNaN(permissions) || permissions > 0) {
  231. fileList.showDetailsView(fileName, 'sharing')
  232. }
  233. },
  234. render: function(actionSpec, isDefault, context) {
  235. var permissions = parseInt(context.$file.data('permissions'), 10)
  236. // if no share permissions but share owner exists, still show the link
  237. if ((permissions & OC.PERMISSION_SHARE) !== 0 || context.$file.attr('data-share-owner')) {
  238. return fileActions._defaultRenderAction.call(fileActions, actionSpec, isDefault, context)
  239. }
  240. // don't render anything
  241. return null
  242. }
  243. })
  244. // register share breadcrumbs component
  245. var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView()
  246. fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView)
  247. },
  248. /**
  249. * Update file list data attributes
  250. */
  251. _updateFileListDataAttributes: function(fileList, $tr, shareModel) {
  252. // files app current cannot show recipients on load, so we don't update the
  253. // icon when changed for consistency
  254. if (fileList.id === 'files') {
  255. return
  256. }
  257. var recipients = _.pluck(shareModel.get('shares'), 'share_with_displayname')
  258. // note: we only update the data attribute because updateIcon()
  259. if (recipients.length) {
  260. var recipientData = _.mapObject(shareModel.get('shares'), function(share) {
  261. return { shareWith: share.share_with, shareWithDisplayName: share.share_with_displayname }
  262. })
  263. $tr.attr('data-share-recipient-data', JSON.stringify(recipientData))
  264. } else {
  265. $tr.removeAttr('data-share-recipient-data')
  266. }
  267. },
  268. /**
  269. * Update the file action share icon for the given file
  270. *
  271. * @param $tr file element of the file to update
  272. * @param {boolean} hasUserShares true if a user share exists
  273. * @param {boolean} hasLinkShares true if a link share exists
  274. *
  275. * @returns {boolean} true if the icon was set, false otherwise
  276. */
  277. _updateFileActionIcon: function($tr, hasUserShares, hasLinkShares) {
  278. // if the statuses are loaded already, use them for the icon
  279. // (needed when scrolling to the next page)
  280. if (hasUserShares || hasLinkShares || $tr.attr('data-share-recipient-data') || $tr.attr('data-share-owner')) {
  281. OCA.Sharing.Util._markFileAsShared($tr, true, hasLinkShares)
  282. return true
  283. }
  284. return false
  285. },
  286. /**
  287. * Marks/unmarks a given file as shared by changing its action icon
  288. * and folder icon.
  289. *
  290. * @param $tr file element to mark as shared
  291. * @param hasShares whether shares are available
  292. * @param hasLink whether link share is available
  293. */
  294. _markFileAsShared: function($tr, hasShares, hasLink) {
  295. var action = $tr.find('.fileactions .action[data-action="Share"]')
  296. var type = $tr.data('type')
  297. var icon = action.find('.icon')
  298. var message, recipients, avatars
  299. var ownerId = $tr.attr('data-share-owner-id')
  300. var owner = $tr.attr('data-share-owner')
  301. var mountType = $tr.attr('data-mounttype')
  302. var shareFolderIcon
  303. var iconClass = 'icon-shared'
  304. action.removeClass('shared-style')
  305. // update folder icon
  306. if (type === 'dir' && (hasShares || hasLink || ownerId)) {
  307. if (typeof mountType !== 'undefined' && mountType !== 'shared-root' && mountType !== 'shared') {
  308. shareFolderIcon = OC.MimeType.getIconUrl('dir-' + mountType)
  309. } else if (hasLink) {
  310. shareFolderIcon = OC.MimeType.getIconUrl('dir-public')
  311. } else {
  312. shareFolderIcon = OC.MimeType.getIconUrl('dir-shared')
  313. }
  314. $tr.find('.filename .thumbnail').css('background-image', 'url(' + shareFolderIcon + ')')
  315. $tr.attr('data-icon', shareFolderIcon)
  316. } else if (type === 'dir') {
  317. var isEncrypted = $tr.attr('data-e2eencrypted')
  318. // FIXME: duplicate of FileList._createRow logic for external folder,
  319. // need to refactor the icon logic into a single code path eventually
  320. if (isEncrypted === 'true') {
  321. shareFolderIcon = OC.MimeType.getIconUrl('dir-encrypted')
  322. $tr.attr('data-icon', shareFolderIcon)
  323. } else if (mountType && mountType.indexOf('external') === 0) {
  324. shareFolderIcon = OC.MimeType.getIconUrl('dir-external')
  325. $tr.attr('data-icon', shareFolderIcon)
  326. } else {
  327. shareFolderIcon = OC.MimeType.getIconUrl('dir')
  328. // back to default
  329. $tr.removeAttr('data-icon')
  330. }
  331. $tr.find('.filename .thumbnail').css('background-image', 'url(' + shareFolderIcon + ')')
  332. }
  333. // update share action text / icon
  334. if (hasShares || ownerId) {
  335. recipients = $tr.data('share-recipient-data')
  336. action.addClass('shared-style')
  337. avatars = '<span>' + t('files_sharing', 'Shared') + '</span>'
  338. // even if reshared, only show "Shared by"
  339. if (ownerId) {
  340. message = t('files_sharing', 'Shared by')
  341. avatars = OCA.Sharing.Util._formatRemoteShare(ownerId, owner, message)
  342. } else if (recipients) {
  343. avatars = OCA.Sharing.Util._formatShareList(recipients)
  344. }
  345. action.html(avatars).prepend(icon)
  346. if (ownerId || recipients) {
  347. var avatarElement = action.find('.avatar')
  348. avatarElement.each(function() {
  349. $(this).avatar($(this).data('username'), 32)
  350. })
  351. action.find('span[title]').tooltip({ placement: 'top' })
  352. }
  353. } else {
  354. action.html('<span class="hidden-visually">' + t('files_sharing', 'Shared') + '</span>').prepend(icon)
  355. }
  356. if (hasLink) {
  357. iconClass = 'icon-public'
  358. }
  359. icon.removeClass('icon-shared icon-public').addClass(iconClass)
  360. },
  361. /**
  362. * Format a remote address
  363. *
  364. * @param {String} shareWith userid, full remote share, or whatever
  365. * @param {String} shareWithDisplayName
  366. * @param {String} message
  367. * @returns {String} HTML code to display
  368. */
  369. _formatRemoteShare: function(shareWith, shareWithDisplayName, message) {
  370. var parts = OCA.Sharing.Util._REMOTE_OWNER_REGEXP.exec(shareWith)
  371. if (!parts || !parts[7]) {
  372. // display avatar of the user
  373. var avatar = '<span class="avatar" data-username="' + escapeHTML(shareWith) + '" title="' + message + ' ' + escapeHTML(shareWithDisplayName) + '"></span>'
  374. var hidden = '<span class="hidden-visually">' + message + ' ' + escapeHTML(shareWithDisplayName) + '</span> '
  375. return avatar + hidden
  376. }
  377. var userName = parts[2]
  378. var userDomain = parts[4]
  379. var server = parts[5]
  380. var protocol = parts[6]
  381. var serverPath = parts[8] ? parts[7] : ''; // no trailing slash on root
  382. var tooltip = message + ' ' + userName
  383. if (userDomain) {
  384. tooltip += '@' + userDomain
  385. }
  386. if (server) {
  387. tooltip += '@' + server.replace(protocol, '') + serverPath
  388. }
  389. var html = '<span class="remoteAddress" title="' + escapeHTML(tooltip) + '">'
  390. html += '<span class="username">' + escapeHTML(userName) + '</span>'
  391. if (userDomain) {
  392. html += '<span class="userDomain">@' + escapeHTML(userDomain) + '</span>'
  393. }
  394. html += '</span> '
  395. return html
  396. },
  397. /**
  398. * Loop over all recipients in the list and format them using
  399. * all kind of fancy magic.
  400. *
  401. * @param {Object} recipients array of all the recipients
  402. * @returns {String[]} modified list of recipients
  403. */
  404. _formatShareList: function(recipients) {
  405. var _parent = this
  406. recipients = _.toArray(recipients)
  407. recipients.sort(function(a, b) {
  408. return a.shareWithDisplayName.localeCompare(b.shareWithDisplayName)
  409. })
  410. return $.map(recipients, function(recipient) {
  411. return _parent._formatRemoteShare(recipient.shareWith, recipient.shareWithDisplayName, t('files_sharing', 'Shared with'))
  412. })
  413. },
  414. /**
  415. * Marks/unmarks a given file as shared by changing its action icon
  416. * and folder icon.
  417. *
  418. * @param $tr file element to mark as shared
  419. * @param hasShares whether shares are available
  420. * @param hasLink whether link share is available
  421. */
  422. markFileAsShared: function($tr, hasShares, hasLink) {
  423. var action = $tr.find('.fileactions .action[data-action="Share"]')
  424. var type = $tr.data('type')
  425. var icon = action.find('.icon')
  426. var message, recipients, avatars
  427. var ownerId = $tr.attr('data-share-owner-id')
  428. var owner = $tr.attr('data-share-owner')
  429. var mountType = $tr.attr('data-mounttype')
  430. var shareFolderIcon
  431. var iconClass = 'icon-shared'
  432. action.removeClass('shared-style')
  433. // update folder icon
  434. if (type === 'dir' && (hasShares || hasLink || ownerId)) {
  435. if (typeof mountType !== 'undefined' && mountType !== 'shared-root' && mountType !== 'shared') {
  436. shareFolderIcon = OC.MimeType.getIconUrl('dir-' + mountType)
  437. } else if (hasLink) {
  438. shareFolderIcon = OC.MimeType.getIconUrl('dir-public')
  439. } else {
  440. shareFolderIcon = OC.MimeType.getIconUrl('dir-shared')
  441. }
  442. $tr.find('.filename .thumbnail').css('background-image', 'url(' + shareFolderIcon + ')')
  443. $tr.attr('data-icon', shareFolderIcon)
  444. } else if (type === 'dir') {
  445. var isEncrypted = $tr.attr('data-e2eencrypted')
  446. // FIXME: duplicate of FileList._createRow logic for external folder,
  447. // need to refactor the icon logic into a single code path eventually
  448. if (isEncrypted === 'true') {
  449. shareFolderIcon = OC.MimeType.getIconUrl('dir-encrypted')
  450. $tr.attr('data-icon', shareFolderIcon)
  451. } else if (mountType && mountType.indexOf('external') === 0) {
  452. shareFolderIcon = OC.MimeType.getIconUrl('dir-external')
  453. $tr.attr('data-icon', shareFolderIcon)
  454. } else {
  455. shareFolderIcon = OC.MimeType.getIconUrl('dir')
  456. // back to default
  457. $tr.removeAttr('data-icon')
  458. }
  459. $tr.find('.filename .thumbnail').css('background-image', 'url(' + shareFolderIcon + ')')
  460. }
  461. // update share action text / icon
  462. if (hasShares || ownerId) {
  463. recipients = $tr.data('share-recipient-data')
  464. action.addClass('shared-style')
  465. avatars = '<span>' + t('files_sharing', 'Shared') + '</span>'
  466. // even if reshared, only show "Shared by"
  467. if (ownerId) {
  468. message = t('files_sharing', 'Shared by')
  469. avatars = this._formatRemoteShare(ownerId, owner, message)
  470. } else if (recipients) {
  471. avatars = this._formatShareList(recipients)
  472. }
  473. action.html(avatars).prepend(icon)
  474. if (ownerId || recipients) {
  475. var avatarElement = action.find('.avatar')
  476. avatarElement.each(function() {
  477. $(this).avatar($(this).data('username'), 32)
  478. })
  479. action.find('span[title]').tooltip({ placement: 'top' })
  480. }
  481. } else {
  482. action.html('<span class="hidden-visually">' + t('files_sharing', 'Shared') + '</span>').prepend(icon)
  483. }
  484. if (hasLink) {
  485. iconClass = 'icon-public'
  486. }
  487. icon.removeClass('icon-shared icon-public').addClass(iconClass)
  488. },
  489. /**
  490. * @param {Array} fileData
  491. * @returns {String}
  492. */
  493. getSharePermissions: function(fileData) {
  494. return fileData.sharePermissions
  495. }
  496. }
  497. })()
  498. OC.Plugins.register('OCA.Files.FileList', OCA.Sharing.Util)