share.js 17 KB

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