public.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. /**
  2. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  3. * SPDX-FileCopyrightText: 2012-2016 ownCloud, Inc.
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. /* global FileActions, Files, FileList */
  7. /* global dragOptions, folderDropOptions */
  8. if (!OCA.Sharing) {
  9. OCA.Sharing = {};
  10. }
  11. if (!OCA.Files) {
  12. OCA.Files = {};
  13. }
  14. /**
  15. * @namespace
  16. */
  17. OCA.Sharing.PublicApp = {
  18. _initialized: false,
  19. /**
  20. * Initializes the public share app.
  21. *
  22. * @param $el container
  23. */
  24. initialize: function ($el) {
  25. var self = this;
  26. var fileActions;
  27. if (this._initialized) {
  28. return;
  29. }
  30. fileActions = new OCA.Files.FileActions();
  31. // default actions
  32. fileActions.registerDefaultActions();
  33. // regular actions
  34. fileActions.merge(OCA.Files.fileActions);
  35. // in case apps would decide to register file actions later,
  36. // replace the global object with this one
  37. OCA.Files.fileActions = fileActions;
  38. this._initialized = true;
  39. var urlParams = OC.Util.History.parseUrlQuery();
  40. this.initialDir = urlParams.path || '/';
  41. var token = $('#sharingToken').val();
  42. var hideDownload = $('#hideDownload').val();
  43. // Prevent all right-click options if hideDownload is enabled
  44. if (hideDownload === 'true') {
  45. window.oncontextmenu = function(event) {
  46. event.preventDefault();
  47. event.stopPropagation();
  48. return false;
  49. };
  50. }
  51. // file list mode ?
  52. if ($el.find('.files-filestable').length) {
  53. // Toggle for grid view
  54. this.$showGridView = $('input#showgridview');
  55. this.$showGridView.on('change', _.bind(this._onGridviewChange, this));
  56. var filesClient = new OC.Files.Client({
  57. host: OC.getHost(),
  58. port: OC.getPort(),
  59. // note: password not be required, the endpoint
  60. // will recognize previous validation from the session
  61. root: OC.getRootPath() + '/public.php/dav/files/' + token + '/',
  62. useHTTPS: OC.getProtocol() === 'https'
  63. });
  64. this.fileList = new OCA.Files.FileList(
  65. $el,
  66. {
  67. id: 'files.public',
  68. dragOptions: dragOptions,
  69. folderDropOptions: folderDropOptions,
  70. fileActions: fileActions,
  71. detailsViewEnabled: false,
  72. filesClient: filesClient,
  73. enableUpload: true,
  74. multiSelectMenu: [
  75. {
  76. name: 'copyMove',
  77. displayName: t('files', 'Move or copy'),
  78. iconClass: 'icon-external',
  79. },
  80. {
  81. name: 'download',
  82. displayName: t('files', 'Download'),
  83. iconClass: 'icon-download',
  84. },
  85. {
  86. name: 'delete',
  87. displayName: t('files', 'Delete'),
  88. iconClass: 'icon-delete',
  89. }
  90. ]
  91. }
  92. );
  93. if (hideDownload === 'true') {
  94. this.fileList._allowSelection = false;
  95. }
  96. this.files = OCA.Files.Files;
  97. this.files.initialize();
  98. // TODO: move to PublicFileList.initialize() once
  99. // the code was split into a separate class
  100. OC.Plugins.attach('OCA.Sharing.PublicFileList', this.fileList);
  101. }
  102. var mimetype = $('#mimetype').val();
  103. var mimetypeIcon = $('#mimetypeIcon').val();
  104. mimetypeIcon = mimetypeIcon.substring(0, mimetypeIcon.length - 3);
  105. mimetypeIcon = mimetypeIcon + 'svg';
  106. var previewSupported = $('#previewSupported').val();
  107. if (typeof FileActions !== 'undefined') {
  108. // Show file preview if previewer is available, images are already handled by the template
  109. if (mimetype.substr(0, mimetype.indexOf('/')) !== 'image' && $('.publicpreview').length === 0) {
  110. // Trigger default action if not download TODO
  111. var spec = FileActions.getDefaultFileAction(mimetype, 'file', OC.PERMISSION_READ);
  112. if (spec && spec.action) {
  113. spec.action($('#filename').val());
  114. }
  115. }
  116. }
  117. // dynamically load image previews
  118. var bottomMargin = 350;
  119. var previewWidth = $(window).width();
  120. var previewHeight = $(window).height() - bottomMargin;
  121. previewHeight = Math.max(200, previewHeight);
  122. var params = {
  123. x: Math.ceil(previewWidth * window.devicePixelRatio),
  124. y: Math.ceil(previewHeight * window.devicePixelRatio),
  125. a: 'true',
  126. file: encodeURIComponent(this.initialDir + $('#filename').val()),
  127. scalingup: 0
  128. };
  129. var imgcontainer = $('<img class="publicpreview" alt="">');
  130. if (hideDownload === 'false') {
  131. imgcontainer = $('<a href="' + $('#previewURL').val() + '" target="_blank"></a>').append(imgcontainer);
  132. }
  133. var img = imgcontainer.hasClass('publicpreview')? imgcontainer: imgcontainer.find('.publicpreview');
  134. img.css({
  135. 'max-width': previewWidth,
  136. 'max-height': previewHeight
  137. });
  138. if (OCA.Viewer && OCA.Viewer.mimetypes.includes(mimetype)
  139. && (mimetype.startsWith('image/') || mimetype.startsWith('video/') || mimetype.startsWith('audio'))) {
  140. OCA.Viewer.setRootElement('#imgframe')
  141. OCA.Viewer.open({ path: '/' })
  142. } else if (mimetype.substr(0, mimetype.indexOf('/')) === 'text' && window.btoa) {
  143. if (OC.appswebroots['files_texteditor'] !== undefined ||
  144. OC.appswebroots['text'] !== undefined) {
  145. // the text editor handles the previewing
  146. return;
  147. }
  148. // Undocumented Url to public WebDAV endpoint
  149. var url = parent.location.protocol + '//' + location.host + OC.linkTo('', 'public.php/dav/files/'+ token);
  150. $.ajax({
  151. url: url,
  152. headers: {
  153. Range: 'bytes=0-10000'
  154. }
  155. }).then(function (data) {
  156. self._showTextPreview(data, previewHeight);
  157. });
  158. } else if ((previewSupported === 'true' && mimetype.substr(0, mimetype.indexOf('/')) !== 'video') ||
  159. mimetype.substr(0, mimetype.indexOf('/')) === 'image' &&
  160. mimetype !== 'image/svg+xml') {
  161. img.attr('src', OC.generateUrl('/apps/files_sharing/publicpreview/' + token + '?' + OC.buildQueryString(params)));
  162. imgcontainer.appendTo('#imgframe');
  163. } else if (mimetype.substr(0, mimetype.indexOf('/')) !== 'video') {
  164. img.attr('src', mimetypeIcon);
  165. img.attr('width', 128);
  166. // "#imgframe" is either empty or it contains an audio preview that
  167. // the icon should appear before, so the container should be
  168. // prepended to the frame.
  169. imgcontainer.prependTo('#imgframe');
  170. } else if (previewSupported === 'true') {
  171. $('#imgframe > video').attr('poster', OC.generateUrl('/apps/files_sharing/publicpreview/' + token + '?' + OC.buildQueryString(params)));
  172. }
  173. if (this.fileList) {
  174. // TODO: move this to a separate PublicFileList class that extends OCA.Files.FileList (+ unit tests)
  175. this.fileList.getDownloadUrl = function (filename, dir, isDir) {
  176. var path = dir || this.getCurrentDirectory();
  177. if (_.isArray(filename)) {
  178. filename = JSON.stringify(filename);
  179. }
  180. var params = {
  181. path: path
  182. };
  183. if (filename) {
  184. params.files = filename;
  185. }
  186. return OC.generateUrl('/s/' + token + '/download') + '?' + OC.buildQueryString(params);
  187. };
  188. this.fileList._createRow = function(fileData) {
  189. var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments);
  190. if (hideDownload === 'true') {
  191. this.fileActions.currentFile = $tr.find('td');
  192. // Remove the link. This means that files without a default action fail hard
  193. $tr.find('a.name').attr('href', '#');
  194. delete this.fileActions.actions.all.Download;
  195. }
  196. return $tr;
  197. };
  198. this.fileList.isSelectedDownloadable = function () {
  199. return hideDownload !== 'true';
  200. };
  201. this.fileList.getUploadUrl = function(fileName, dir) {
  202. if (_.isUndefined(dir)) {
  203. dir = this.getCurrentDirectory();
  204. }
  205. var pathSections = dir.split('/');
  206. if (!_.isUndefined(fileName)) {
  207. pathSections.push(fileName);
  208. }
  209. var encodedPath = '';
  210. _.each(pathSections, function(section) {
  211. if (section !== '') {
  212. encodedPath += '/' + encodeURIComponent(section);
  213. }
  214. });
  215. var base = '';
  216. if (!this._uploader.isXHRUpload()) {
  217. // also add auth in URL due to POST workaround
  218. base = OC.getProtocol() + '://' + token + '@' + OC.getHost() + (OC.getPort() ? ':' + OC.getPort() : '');
  219. }
  220. // encodedPath starts with a leading slash
  221. return base + OC.getRootPath() + '/public.php/dav/files/' + token + encodedPath;
  222. };
  223. this.fileList.getAjaxUrl = function (action, params) {
  224. params = params || {};
  225. params.t = token;
  226. return OC.filePath('files_sharing', 'ajax', action + '.php') + '?' + OC.buildQueryString(params);
  227. };
  228. this.fileList.linkTo = function (dir) {
  229. return OC.generateUrl('/s/' + token + '') + '?' + OC.buildQueryString({path: dir});
  230. };
  231. this.fileList.generatePreviewUrl = function (urlSpec) {
  232. urlSpec = urlSpec || {};
  233. if (!urlSpec.x) {
  234. urlSpec.x = this.$table.data('preview-x') || 250;
  235. }
  236. if (!urlSpec.y) {
  237. urlSpec.y = this.$table.data('preview-y') || 250;
  238. }
  239. urlSpec.x *= window.devicePixelRatio;
  240. urlSpec.y *= window.devicePixelRatio;
  241. urlSpec.x = Math.ceil(urlSpec.x);
  242. urlSpec.y = Math.ceil(urlSpec.y);
  243. var token = $('#dirToken').val();
  244. return OC.generateUrl('/apps/files_sharing/publicpreview/' + token + '?' + OC.buildQueryString(urlSpec));
  245. };
  246. this.fileList.updateEmptyContent = function() {
  247. this.$el.find('.emptycontent .uploadmessage').text(
  248. t('files_sharing', 'You can upload into this folder')
  249. );
  250. OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
  251. };
  252. this.fileList._uploader.on('fileuploadadd', function(e, data) {
  253. if (!data.headers) {
  254. data.headers = {};
  255. }
  256. data.headers.Authorization = 'Basic ' + btoa(token + ':');
  257. });
  258. // do not allow sharing from the public page
  259. delete this.fileList.fileActions.actions.all.Share;
  260. this.fileList.changeDirectory(this.initialDir || '/', false, true);
  261. // URL history handling
  262. this.fileList.$el.on('changeDirectory', _.bind(this._onDirectoryChanged, this));
  263. OC.Util.History.addOnPopStateHandler(_.bind(this._onUrlChanged, this));
  264. $('#download').click(function (e) {
  265. e.preventDefault();
  266. OC.redirect(FileList.getDownloadUrl());
  267. });
  268. if (hideDownload === 'true') {
  269. this.fileList.$el.find('.summary').find('td:first-child').remove();
  270. }
  271. }
  272. $(document).on('click', '#directLink', function () {
  273. $(this).focus();
  274. $(this).select();
  275. });
  276. $('.save-form').submit(function (event) {
  277. event.preventDefault();
  278. var remote = $(this).find('#remote_address').val();
  279. var token = $('#sharingToken').val();
  280. var owner = $('#save-external-share').data('owner');
  281. var ownerDisplayName = $('#save-external-share').data('owner-display-name');
  282. var name = $('#save-external-share').data('name');
  283. var isProtected = $('#save-external-share').data('protected') ? 1 : 0;
  284. OCA.Sharing.PublicApp._createFederatedShare(remote, token, owner, ownerDisplayName, name, isProtected);
  285. });
  286. $('#remote_address').on("keyup paste", function() {
  287. if ($(this).val() === '' || $('#save-external-share > .icon.icon-loading-small').length > 0) {
  288. $('#save-button-confirm').prop('disabled', true);
  289. } else {
  290. $('#save-button-confirm').prop('disabled', false);
  291. }
  292. });
  293. self._bindShowTermsAction();
  294. // legacy
  295. window.FileList = this.fileList;
  296. },
  297. /**
  298. * Binds the click action for the "terms of service" action.
  299. * Shows an OC info dialog on click.
  300. *
  301. * @private
  302. */
  303. _bindShowTermsAction: function() {
  304. $('#show-terms-dialog').on('click', function() {
  305. OC.dialogs.info($('#disclaimerText').val(), t('files_sharing', 'Terms of service'));
  306. });
  307. },
  308. _showTextPreview: function (data, previewHeight) {
  309. var textDiv = $('<div></div>').addClass('text-preview');
  310. textDiv.text(data);
  311. textDiv.appendTo('#imgframe');
  312. var divHeight = textDiv.height();
  313. if (data.length > 999) {
  314. var ellipsis = $('<div></div>').addClass('ellipsis');
  315. ellipsis.html('(&#133;)');
  316. ellipsis.appendTo('#imgframe');
  317. }
  318. if (divHeight > previewHeight) {
  319. textDiv.height(previewHeight);
  320. }
  321. },
  322. /**
  323. * Toggle showing gridview by default or not
  324. *
  325. * @returns {undefined}
  326. */
  327. _onGridviewChange: function() {
  328. const isGridView = this.$showGridView.is(':checked');
  329. this.$showGridView.next('#view-toggle')
  330. .removeClass('icon-toggle-filelist icon-toggle-pictures')
  331. .addClass(isGridView ? 'icon-toggle-filelist' : 'icon-toggle-pictures')
  332. this.$showGridView.next('#view-toggle').attr(
  333. 'title',
  334. isGridView ? t('files', 'Show list view') : t('files', 'Show grid view'),
  335. )
  336. if (this.fileList) {
  337. this.fileList.setGridView(isGridView);
  338. }
  339. },
  340. _onDirectoryChanged: function (e) {
  341. OC.Util.History.pushState({
  342. // arghhhh, why is this not called "dir" !?
  343. path: e.dir
  344. });
  345. },
  346. _onUrlChanged: function (params) {
  347. this.fileList.changeDirectory(params.path || params.dir, false, true);
  348. },
  349. /**
  350. * fall back to old behaviour where we redirect the user to his server to mount
  351. * the public link instead of creating a dedicated federated share
  352. *
  353. * @param {any} remote -
  354. * @param {any} token -
  355. * @param {any} owner -
  356. * @param {any} ownerDisplayName -
  357. * @param {any} name -
  358. * @param {any} isProtected -
  359. * @private
  360. */
  361. _legacyCreateFederatedShare: function (remote, token, owner, ownerDisplayName, name, isProtected) {
  362. var self = this;
  363. var location = window.location.protocol + '//' + window.location.host + OC.getRootPath();
  364. if(remote.substr(-1) !== '/') {
  365. remote += '/'
  366. }
  367. var url = remote + 'index.php/apps/files#' + 'remote=' + encodeURIComponent(location) // our location is the remote for the other server
  368. + "&token=" + encodeURIComponent(token) + "&owner=" + encodeURIComponent(owner) +"&ownerDisplayName=" + encodeURIComponent(ownerDisplayName) + "&name=" + encodeURIComponent(name) + "&protected=" + isProtected;
  369. if (remote.indexOf('://') > 0) {
  370. OC.redirect(url);
  371. } else {
  372. // if no protocol is specified, we automatically detect it by testing https and http
  373. // this check needs to happen on the server due to the Content Security Policy directive
  374. $.get(OC.generateUrl('apps/files_sharing/testremote'), {remote: remote}).then(function (protocol) {
  375. if (protocol !== 'http' && protocol !== 'https') {
  376. self._toggleLoading();
  377. OC.dialogs.alert(t('files_sharing', 'No compatible server found at {remote}', {remote: remote}),
  378. t('files_sharing', 'Invalid server URL'));
  379. } else {
  380. OC.redirect(protocol + '://' + url);
  381. }
  382. });
  383. }
  384. },
  385. _toggleLoading: function() {
  386. var loading = $('#save-external-share > .icon.icon-loading-small').length === 0;
  387. if (loading) {
  388. $('#save-external-share > .icon-external')
  389. .removeClass("icon-external")
  390. .addClass("icon-loading-small");
  391. $('#save-external-share #save-button-confirm').prop("disabled", true);
  392. } else {
  393. $('#save-external-share > .icon-loading-small')
  394. .addClass("icon-external")
  395. .removeClass("icon-loading-small");
  396. $('#save-external-share #save-button-confirm').prop("disabled", false);
  397. }
  398. },
  399. _createFederatedShare: function (remote, token, owner, ownerDisplayName, name, isProtected) {
  400. var self = this;
  401. this._toggleLoading();
  402. if (remote.indexOf('@') === -1) {
  403. this._legacyCreateFederatedShare(remote, token, owner, ownerDisplayName, name, isProtected);
  404. return;
  405. }
  406. $.post(
  407. OC.generateUrl('/apps/federatedfilesharing/createFederatedShare'),
  408. {
  409. 'shareWith': remote,
  410. 'token': token
  411. }
  412. ).done(
  413. function (data) {
  414. var url = data.remoteUrl;
  415. if (url.indexOf('://') > 0) {
  416. OC.redirect(url);
  417. } else {
  418. OC.redirect('http://' + url);
  419. }
  420. }
  421. ).fail(
  422. function (jqXHR) {
  423. OC.dialogs.alert(JSON.parse(jqXHR.responseText).message,
  424. t('files_sharing', 'Failed to add the public link to your Nextcloud'));
  425. self._toggleLoading();
  426. }
  427. );
  428. }
  429. };
  430. window.addEventListener('DOMContentLoaded', function () {
  431. // FIXME: replace with OC.Plugins.register()
  432. if (window.TESTING) {
  433. return;
  434. }
  435. var App = OCA.Sharing.PublicApp;
  436. // defer app init, to give a chance to plugins to register file actions
  437. _.defer(function () {
  438. App.initialize($('#preview'));
  439. });
  440. if (window.Files) {
  441. // HACK: for oc-dialogs previews that depends on Files:
  442. Files.generatePreviewUrl = function (urlSpec) {
  443. return App.fileList.generatePreviewUrl(urlSpec);
  444. };
  445. }
  446. });