diff --git a/www/code/inner.js b/www/code/inner.js index 785e82788..4f6c0308e 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -368,7 +368,7 @@ define([ var mkFilePicker = function (framework, editor, evModeChange) { evModeChange.reg(function (mode) { if (MEDIA_TAG_MODES.indexOf(mode) !== -1) { - // Embedding is endabled + // Embedding is enabled framework.setMediaTagEmbedder(function (mt) { editor.focus(); editor.replaceSelection($(mt)[0].outerHTML); diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 15e5b5e34..3be979f17 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -119,6 +119,7 @@ define(function() { file: 'cptools-file', fileupload: 'cptools-file-upload', folderupload: 'cptools-folder-upload', + link: 'fa-link', pad: 'cptools-richtext', code: 'cptools-code', slide: 'cptools-slide', diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 4d3520426..5aea35c78 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -1050,6 +1050,7 @@ define([ var font = icon.indexOf('cptools') === 0 ? 'cptools' : 'fa'; if (type === 'fileupload') { type = 'file'; } if (type === 'folderupload') { type = 'file'; } + if (type === 'link') { type = 'drive'; } var appClass = ' cp-icon cp-icon-color-'+type; $icon = $('', {'class': font + ' ' + icon + appClass}); } @@ -1061,6 +1062,7 @@ define([ if (!data) { return $icon; } var href = data.href || data.roHref; var type = data.type; + if (data.static) { type = 'link'; } if (!href && !type) { return $icon; } if (!type) { type = Hash.parsePadUrl(href).type; } diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 0ab010cb5..6114884b2 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1032,10 +1032,19 @@ define([ icon: 'fa-picture-o', action: function () { var _cfg = { - types: ['file'], + types: ['file', 'link'], where: ['root'] }; common.openFilePicker(_cfg, function (data) { + // Embed links + if (data.static) { + var a = h('a', { + href: data.href + }, data.name); + cfg.embed(a, data); + return; + } + // Embed files if (data.type !== 'file') { console.log("Unexpected data type picked " + data.type); return; @@ -3021,6 +3030,63 @@ define([ UI.proposal(content, todo); }; + UIElements.displayOpenLinkModal = function (common, data, dismiss) { + var name = Util.fixHTML(data.title); + var url = data.href; + var user = data.name; + Messages.notification_openLink = "You've received a link {0} from {1}:"; // XXX + Messages.link_open = "Open URL"; + Messages.link_store = "Store link in drive"; + + var content = h('div', [ + UI.setHTML(h('p'), Messages._getKey('notification_openLink', [name, user])), + h('pre', url), + UIElements.getVerifiedFriend(common, data.curve, user) + ]); + var clicked = false; + var modal; + var buttons = [{ + name: Messages.friendRequest_later, + onClick: function () { + if (clicked) { return true; } + clicked = true; + }, + keys: [27] + }, { + className: 'primary', + name: Messages.link_open, + onClick: function () { + if (clicked) { return true; } + clicked = true; + common.openUnsafeURL(url); + }, + keys: [13] + }, { + className: 'primary', + name: Messages.link_store, + onClick: function () { + if (clicked) { return; } + clicked = true; + common.getSframeChannel().query("Q_DRIVE_USEROBJECT", { + cmd: "addLink", + data: { + name: name, + href: url, + path: ['root'] + } + }, function () { + modal.closeModal(); + dismiss(); + }); + return true; + }, + keys: [[13, 'ctrl']] + }]; + var _modal = UI.dialog.customModal(content, {buttons: buttons}); + modal = UI.openCustomModal(_modal); + return modal; + }; + UIElements.displayAddOwnerModal = function (common, data) { var priv = common.getMetadataMgr().getPrivateData(); var sframeChan = common.getSframeChannel(); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index b2489c22b..cd456fd41 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -42,6 +42,9 @@ define([ Pages) { + Messages.fm_link = "Link"; // XXX "New Link" ? + // XXX check for all occurrences of `fm_link` before changing it + var APP = window.APP = { editable: false, online: false, @@ -443,6 +446,11 @@ define([ 'data-icon': AppConfig.applicationsIcon.poll, 'data-type': 'poll' }, Messages.button_newpoll)), + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.link, + 'data-type': 'link' + }, Messages.fm_link)), ]), ]), $separator.clone()[0], @@ -1106,11 +1114,24 @@ define([ common.getMediaTagPreview(mts, idx); }; + var refresh = APP.refresh = function () { + APP.displayDirectory(currentPath); + }; + // `app`: true (force open wiht the app), false (force open in preview), // falsy (open in preview if default is not using the app) var defaultInApp = ['application/pdf']; var openFile = function (el, isRo, app) { var data = manager.getFileData(el); + + if (data.static) { + if (data.href) { + common.openUnsafeURL(data.href); + manager.updateStaticAccess(el, refresh); + } + return; + } + if (!data || (!data.href && !data.roHref)) { return void logError("Missing data for the file", el, data); } @@ -1147,10 +1168,6 @@ define([ common.openURL(Hash.getNewPadURL(href, obj)); }; - var refresh = APP.refresh = function () { - APP.displayDirectory(currentPath); - }; - var pickFolderColor = function ($element, currentColor, cb) { var colors = ["", "#f23c38", "#ff0073", "#da0eba", "#9d00ac", "#6c19b3", "#4a42b1", "#3d8af0", "#30a0f1", "#1fb9d1", "#009686", "#45b354", "#84c750", "#c6e144", "#faf147", "#fbc423", "#fc9819", "#fd5227", "#775549", "#9c9c9c", "#607a89"]; @@ -1263,6 +1280,9 @@ define([ if ($element.is('.cp-border-color-sheet')) { hide.push('download'); } + if ($element.is('.cp-app-drive-static')) { + hide.push('access', 'hashtag', 'properties', 'download'); + } if ($element.is('.cp-app-drive-element-file')) { // No folder in files hide.push('color'); @@ -1899,6 +1919,27 @@ define([ // In list mode, display metadata from the filesData object + var addStaticData = function (element, $element, data) { + $element.addClass('cp-border-color-drive'); + var name = data.name; + var $name = $('', {'class': 'cp-app-drive-element-name'}).text(name); + $element.append($name); + if (getViewMode() === 'grid') { + $element.attr('title', name); // XXX Util.fixHTML + } + + var type = Messages.fm_link; // XXX new translation key ("Link") + var $type = $('', { + 'class': 'cp-app-drive-element-type cp-app-drive-element-list' + }).text(type); + var $adate = $('', { + 'class': 'cp-app-drive-element-atime cp-app-drive-element-list' + }).text(getDate(data.atime)); + var $cdate = $('', { + 'class': 'cp-app-drive-element-ctime cp-app-drive-element-list' + }).text(getDate(data.ctime)); + $element.append($type).append($adate).append($cdate); + }; var _addOwnership = function ($span, $state, data) { if (data && Array.isArray(data.owners) && data.owners.indexOf(edPublic) !== -1) { var $owned = $ownedIcon.clone().appendTo($state); @@ -1914,6 +1955,9 @@ define([ if (!manager.isFile(element)) { return; } var data = manager.getFileData(element); + if (data.static) { + return addStaticData(element, $element, data); + } if (!Object.keys(data).length) { return true; @@ -2124,7 +2168,9 @@ define([ $icon = manager.isFolderEmpty(root[key]) ? $folderEmptyIcon.clone() : $folderIcon.clone(); $icon.css("color", getFolderColor(path.concat(elPath))); } - var classes = restrictedClass + roClass + liClass; + + var staticClass = manager.isStaticFile(element) ? '.cp-app-drive-static' : ''; + var classes = restrictedClass + roClass + liClass + staticClass; var $element = $(h('li.cp-app-drive-element.cp-app-drive-element-row' + classes, { draggable: true })); @@ -2459,8 +2505,7 @@ define([ setViewMode(viewMode || 'grid'); showMode(viewMode); - $button.click(function (e) { - console.error(e); + $button.click(function () { var viewMode = getViewMode(); var newViewMode = getOppositeViewMode(viewMode); setViewMode(newViewMode); @@ -2686,6 +2731,63 @@ define([ }); $input.click(); }; + var showLinkModal = function () { + Messages.fm_link_name = "Link name"; // XXX + Messages.fm_link_url = "URL"; + Messages.fm_link_warning = "Warning: URL size..."; + var name, url; + var warning = h('div.alert.alert-warning', [ + h('i.fa.fa-exclamation-triangle'), + h('span', Messages.fm_link_warning) + ]); + var content = h('p', [ + warning, + h('label', {for: 'cp-app-drive-link-name'}, Messages.fm_link_name), + name = h('input#cp-app-drive-link-name', { autocomplete: 'off' }), + h('label', {for: 'cp-app-drive-link-url'}, Messages.fm_link_url), + url = h('input#cp-app-drive-link-url', { type: 'url', autocomplete: 'off' }) + ]); + var $warning = $(warning).hide(); + var $url = $(url).on('change keypress keyup keydown', function () { + var v = $url.val().trim(); + if (v.length > 200) { + $warning.show(); + return; + } + $warning.hide(); + }); + var buttons = [{ + className: 'cancel', + name: Messages.cancelButton, + onClick: function () {}, + keys: [27] + }]; + buttons.push({ + className: 'primary', + // We may want to use a new key here + iconClass: '.fa.fa-plus', + name: Messages.tag_add, + onClick: function () { + var n = $(name).val().trim(); + var u = $url.val().trim(); + if (!n || !u) { return true; } + if (!Util.isValidURL(u)) { + // XXX add style for invalid input? input:invalid + UI.warn(Messages.error); + return true; + } + manager.addLink(currentPath, { + name: n, + url: u + }, refresh); + }, + keys: [13] + }); + var m = UI.dialog.customModal(content, { + buttons: buttons + }); + UI.openCustomModal(m); + }; var addNewPadHandlers = function ($block, isInRoot) { // Handlers if (isInRoot) { @@ -2714,6 +2816,7 @@ define([ } $block.find('a.cp-app-drive-new-fileupload, li.cp-app-drive-new-fileupload').click(showUploadFilesModal); $block.find('a.cp-app-drive-new-folderupload, li.cp-app-drive-new-folderupload').click(showUploadFolderModal); + $block.find('a.cp-app-drive-new-link, li.cp-app-drive-new-link').click(showLinkModal); } $block.find('a.cp-app-drive-new-doc, li.cp-app-drive-new-doc') .click(function () { @@ -2757,6 +2860,12 @@ define([ }); } options.push({tag: 'hr'}); + options.push({ + tag: 'a', + attributes: {'class': 'cp-app-drive-new-link'}, + content: $('
').append(getIcon('link')).html() + Messages.fm_link + }); + options.push({tag: 'hr'}); } getNewPadTypes().forEach(function (type) { var attributes = { @@ -3073,6 +3182,13 @@ define([ $elementFolderUpload.append($('', {'class': 'cp-app-drive-new-name'}) .text(Messages.uploadFolderButton)); } + // Link + var $elementLink = $('
  • ', { + 'class': 'cp-app-drive-new-link cp-app-drive-element-row ' + + 'cp-app-drive-element-grid' + }).prepend(getIcon('link')).appendTo($container); + $elementLink.append($('', {'class': 'cp-app-drive-new-name'}) + .text(Messages.fm_link)); } // Pads getNewPadTypes().forEach(function (type) { @@ -3479,6 +3595,7 @@ define([ var path = paths[0]; if (manager.isPathIn(path, [TRASH])) { return; } + if (!file.channel) { file.channel = id; } if (channels.indexOf(file.channel) !== -1) { return; } channels.push(file.channel); @@ -4482,8 +4599,9 @@ define([ password: data.password }, isTemplate: paths[0].path[0] === 'template', - title: data.title, + title: data.title || data.name, sharedFolder: sf, + static: data.static ? data.href : undefined, common: common }; if (padType === 'file') { @@ -4501,6 +4619,20 @@ define([ data = manager.getSharedFolderData(el); } if (!data) { return; } + if (data.static) { + sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "addLink", + teamId: -1, + data: { + name: data.name, + href: data.href, + path: ['root'] + } + }, function () { + UI.log(Messages.saved); + }); + return; + } sframeChan.query('Q_STORE_IN_TEAM', { href: data.href || data.rohref, password: data.password, @@ -4539,6 +4671,9 @@ define([ } else if ($this.hasClass("cp-app-drive-context-newdoc")) { var ntype = $this.data('type') || 'pad'; + if (ntype === 'link') { + return void showLinkModal(); + } var path2 = manager.isPathIn(currentPath, [TRASH]) ? '' : currentPath; openIn(ntype, path2, APP.team); } diff --git a/www/common/inner/share.js b/www/common/inner/share.js index 9ba75f867..5225ea818 100644 --- a/www/common/inner/share.js +++ b/www/common/inner/share.js @@ -90,7 +90,11 @@ define([ setTimeout(w); }); if (res && /^http/.test(res)) { - href = Hash.getRelativeHref(res); + var _href = Hash.getRelativeHref(res); + if (_href) { href = _href; } + else { + href = res; + } setTimeout(w); return; } @@ -109,6 +113,7 @@ define([ if (mailbox.notifications && mailbox.curvePublic) { common.mailbox.sendTo("SHARE_PAD", { href: href, + isStatic: Boolean(config.static), password: config.password, isTemplate: config.isTemplate, name: myName, @@ -137,6 +142,20 @@ define([ }); return; } + if (config.static) { + common.getSframeChannel().query("Q_DRIVE_USEROBJECT", { + cmd: "addLink", + teamId: team.id, + data: { + name: title, + href: href, + path: ['root'] + } + }, function () { + UI.log(Messages.saved); + }); + return; + } sframeChan.query('Q_STORE_IN_TEAM', { href: href, password: config.password, @@ -346,6 +365,9 @@ define([ ] : [ UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }), ]; + + if (opts.static) { linkContent = []; } + linkContent.push(h('div.cp-spacer')); linkContent.push(UI.dialog.selectableArea('', { id: 'cp-share-link-preview', tabindex: 1, rows:3})); @@ -361,7 +383,7 @@ define([ // warning about sharing links // when sharing a version hash, there is a similar warning and we want // to avoid alert fatigue - if (!opts.versionHash) { + if (!opts.versionHash && !opts.static) { var localStore = window.cryptpadStore; var dismissButton = h('span.fa.fa-times'); var shareLinkWarning = h('div.alert.alert-warning.dismissable', @@ -405,6 +427,10 @@ define([ var v = opts.getLinkValue({ embed: Util.isChecked($link.find('#cp-share-embed')) }); + if (opts.static) { + common.openUnsafeURL(v); + return true; + } window.open(v); return true; }, @@ -562,6 +588,7 @@ define([ }); }; opts.getLinkValue = function (initValue, cb) { + if (opts.static) { return opts.static; } var val = initValue || {}; var edit = val.edit !== undefined ? val.edit : Util.isChecked($rights.find('#cp-share-editable-true')); var embed = val.embed; @@ -686,7 +713,7 @@ define([ opts.access = true; // Allow the use of the modal even if the pad is not stored var hashes = opts.hashes; - if (!hashes || (!hashes.editHash && !hashes.viewHash)) { return; } + if (!hashes || (!hashes.editHash && !hashes.viewHash && !opts.static)) { return; } var teams = getEditableTeams(common, opts); opts.teams = teams; @@ -705,19 +732,23 @@ define([ var $rights = opts.$rights = getRightsHeader(common, opts); var resetTab = function () { + if (opts.static) { return; } $rights.show(); $rights.find('label.cp-radio').show(); }; var onShowEmbed = function () { + if (opts.static) { return; } $rights.find('#cp-share-bar').closest('label').hide(); $rights.find('input[type="radio"]:enabled').first().prop('checked', 'checked'); $rights.find('input[type="radio"]').trigger('change'); }; var onShowContacts = function () { + if (opts.static) { return; } if (!hasFriends || priv.offline) { $rights.hide(); } }; + if (opts.static) { $rights.hide(); } var contactsActive = hasFriends && !priv.offline; var tabs = [{ @@ -732,13 +763,16 @@ define([ title: Messages.share_linkCategory, icon: "fa fa-link", active: !contactsActive, - }, { - getTab: getEmbedTab, - title: Messages.share_embedCategory, - icon: "fa fa-code", - onShow: onShowEmbed, - onHide: resetTab }]; + if (!opts.static) { + tabs.push({ + getTab: getEmbedTab, + title: Messages.share_embedCategory, + icon: "fa fa-code", + onShow: onShowEmbed, + onHide: resetTab + }); + } Modal.getModal(common, opts, tabs, function (err, modal) { // Hide the burn-after-reading option by default var $modal = $(modal); diff --git a/www/common/notifications.js b/www/common/notifications.js index 20738e237..24458420e 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -92,6 +92,11 @@ define([ (type === 'file' ? 'notification_fileShared' : // Msg.notification_fileSharedTeam 'notification_padShared'); // Msg.notification_padSharedTeam + Messages.notification_linkShared = "{0} has shared a link with you: {1}"; // XXX + if (msg.content.isStatic) { + key = 'notification_linkShared'; // Msg.notification_linkShared; + } + var teamNotification = /^team-/.test(data.type) && Number(data.type.slice(5)); var teamName = ''; if (teamNotification) { @@ -109,6 +114,15 @@ define([ return Messages._getKey(key, [name, title, teamName]); }; content.handler = function() { + if (msg.content.isStatic) { + UIElements.displayOpenLinkModal(common, { + curve: msg.author, + href: msg.content.href, + name: name, + title: title + }, defaultDismiss(common, data)); + return; + } var obj = { p: msg.content.isTemplate ? ['template'] : undefined, t: teamNotification || undefined, diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 9f779a0a8..6aacd4a1b 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1306,9 +1306,14 @@ define([ getAllStores().forEach(function (s) { s.manager.getSecureFilesList(where).forEach(function (obj) { var data = obj.data; - if (channels.indexOf(data.channel) !== -1) { return; } + if (channels.indexOf(data.channel || data.id) !== -1) { return; } var id = obj.id; - if (data.channel) { channels.push(data.channel); } + if (data.channel) { channels.push(data.channel || data.id); } + // Only include static links if "link" is requested + if (data.static) { + if (types.indexOf('link') !== -1) { list[id] = data; } + return; + } var parsed = Hash.parsePadUrl(data.href || data.roHref); if ((!types || types.length === 0 || types.indexOf(parsed.type) !== -1) && !isFiltered(parsed.type, data)) { diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 7f7e20d2a..88e43352b 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -238,8 +238,9 @@ define([ // content.name, content.title, content.href, content.password if (isMuted(ctx, data)) { return void cb(true); } - - var channel = Hash.hrefToHexChannelId(content.href, content.password); + // if the shared content is a 'link' then we can't use the channel to deduplicate notifications + // use href instead. + var channel = content.isStatic ? content.href : Hash.hrefToHexChannelId(content.href, content.password); var parsed = Hash.parsePadUrl(content.href); var mode = parsed.hashData && parsed.hashData.mode || 'n/a'; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index e7c55da67..4051c79c1 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -20,6 +20,7 @@ define([ var ROOT = exp.ROOT; var FILES_DATA = exp.FILES_DATA; + var STATIC_DATA = exp.STATIC_DATA; var OLD_FILES_DATA = exp.OLD_FILES_DATA; var UNSORTED = exp.UNSORTED; var TRASH = exp.TRASH; @@ -78,6 +79,14 @@ define([ files[FILES_DATA][id] = data; cb(null, id); }; + exp.pushLink = function (_data, cb) { + if (typeof cb !== "function") { cb = function () {}; } + if (readOnly) { return void cb('EFORBIDDEN'); } + var id = Util.createRandomInteger(); + var data = clone(_data); + files[STATIC_DATA][id] = data; + cb(null, id); + }; exp.pushSharedFolder = function (_data, cb) { if (typeof cb !== "function") { cb = function () {}; } @@ -136,7 +145,7 @@ define([ var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]); var toClean = []; - exp.getFiles([FILES_DATA, SHARED_FOLDERS]).forEach(function (id) { + exp.getFiles([FILES_DATA, SHARED_FOLDERS, STATIC_DATA]).forEach(function (id) { if (filesList.indexOf(id) === -1) { var fd = exp.isSharedFolder(id) ? files[SHARED_FOLDERS][id] : exp.getFileData(id); var channelId = fd.channel; @@ -146,6 +155,8 @@ define([ if (exp.isSharedFolder(id)) { delete files[SHARED_FOLDERS][id]; if (config.removeProxy) { config.removeProxy(id); } + } else if (files[STATIC_DATA][id]) { + delete files[STATIC_DATA][id]; } else { spliceFileData(id); } @@ -242,6 +253,12 @@ define([ id = Number(id); // Find and maybe update existing pads with the same channel id var d = data[id]; + // If we were given a static link, copy to STATIC_DATA + if (d.static) { + delete d.static; + files[STATIC_DATA][id] = d; + return; + } // If we were given an edit link, encrypt its value if needed if (d.href) { d.href = exp.cryptor.encrypt(d.href); } var found = false; @@ -398,7 +415,7 @@ define([ if (!loggedIn && !config.testMode) { return; } id = Number(id); - var data = files[FILES_DATA][id] || files[SHARED_FOLDERS][id]; + var data = files[FILES_DATA][id] || files[STATIC_DATA][id] || files[SHARED_FOLDERS][id]; if (!data || typeof(data) !== "object") { return; } var newPath = path, parentEl; if (path && !Array.isArray(path)) { @@ -599,13 +616,18 @@ define([ var element = elem || files[ROOT]; if (!element) { return console.error("Invalid element in root"); } var nbMetadataFolders = 0; + // caching this variables saves a lot of hashmap lookups in this loop + var static_data = files[STATIC_DATA]; + var files_data = files[FILES_DATA]; + var element_el; for (var el in element) { - if (element[el] === null) { + element_el = element[el]; + if (element_el === null) { console.error('element[%s] is null', el); delete element[el]; continue; } - if (exp.isFolderData(element[el])) { + if (exp.isFolderData(element_el)) { if (nbMetadataFolders !== 0) { debug("Multiple metadata files in folder"); delete element[el]; @@ -613,30 +635,30 @@ define([ nbMetadataFolders++; continue; } - if (!exp.isFile(element[el], true) && !exp.isFolder(element[el])) { - debug("An element in ROOT was not a folder nor a file. ", element[el]); + if (!exp.isFile(element_el, true) && !exp.isFolder(element_el)) { + debug("An element in ROOT was not a folder nor a file. ", element_el); delete element[el]; continue; } - if (exp.isFolder(element[el])) { - fixRoot(element[el]); + if (exp.isFolder(element_el)) { + fixRoot(element_el); continue; } - if (typeof element[el] === "string") { + if (typeof element_el === "string") { // We have an old file (href) which is not in filesData: add it var id = Util.createRandomInteger(); var key = Hash.createChannelId(); - files[FILES_DATA][id] = { - href: exp.cryptor.encrypt(element[el]), + files_data[id] = { + href: exp.cryptor.encrypt(element_el), filename: el }; element[key] = id; delete element[el]; } - if (typeof element[el] === "number") { - var data = files[FILES_DATA][element[el]]; + if (typeof element_el === "number") { + var data = files_data[element_el] || static_data[element_el]; if (!data) { - debug("An element in ROOT doesn't have associated data", element[el], el); + debug("An element in ROOT doesn't have associated data", element_el, el); delete element[el]; } } @@ -845,6 +867,26 @@ define([ toClean.forEach(function (id) { spliceFileData(id); }); + // make sure that links are displayed at least once in your drive if you are going to keep them + var sd = files[STATIC_DATA]; + var toCleanSD = []; + for (var id2 in sd) { + id2 = Number(id2); + var el2 = sd[id2]; + if (!el2 || typeof(el2) !== "object" || !el2.href) { + toCleanSD.push(id2); + continue; + } + if ((loggedIn || config.testMode) && rootFiles.indexOf(id2) === -1) { + toCleanSD.push(id2); + continue; + } + } + var spliceSD = function (id) { + if (readOnly) { return; } + delete files[STATIC_DATA][id]; + }; + toCleanSD.forEach(spliceSD); }; var fixSharedFolders = function () { if (sharedFolder) { return; } @@ -892,6 +934,12 @@ define([ } } }; + var fixStaticData = function () { + if (Util.isObject(files[STATIC_DATA])) { + debug("STATIC_DATA was not an object"); + files[STATIC_DATA] = {}; + } + }; var fixDrive = function () { @@ -900,6 +948,7 @@ define([ }); }; + fixStaticData(); fixRoot(); fixTrashRoot(); fixTemplate(); diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 0e799de56..01c22812c 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -22,11 +22,11 @@ define([ // a cached version if (Env.folders[id].offline && !lm.cache) { Env.folders[id].offline = false; + if (Env.folders[id].userObject.fixFiles) { Env.folders[id].userObject.fixFiles(); } Env.Store.refreshDriveUI(); } return; } - if (Env.folders[id]) { console.warn(Env.folders[id]); } var cfg = getConfig(Env); cfg.sharedFolder = true; cfg.id = id; @@ -584,6 +584,24 @@ define([ }); }); }; + // Add a link + var _addLink = function (Env, data, cb) { + data = data || {}; + var resolved = _resolvePath(Env, data.path); + if (!resolved || !resolved.userObject) { return void cb({error: 'E_NOTFOUND'}); } + var uo = resolved.userObject; + var now = +new Date(); + uo.pushLink({ + name: data.name, + href: data.href, + atime: now, + ctime: now + }, function (e, id) { + if (e) { return void cb({error: e}); } + uo.add(id, resolved.path); + Env.onSync(cb); + }); + }; var _restoreSharedFolder = function (Env, _data, cb) { var fId = _data.id; @@ -1019,6 +1037,14 @@ define([ }); }; + + var _updateStaticAccess = function (Env, id, cb) { + var uo = _getUserObjectFromId(Env, id); + var sd = uo.getFileData(id, true); + sd.atime = +new Date(); + Env.onSync(cb); + }; + var onCommand = function (Env, cmdData, cb) { var cmd = cmdData.cmd; var data = cmdData.data || {}; @@ -1031,6 +1057,8 @@ define([ _addFolder(Env, data, cb); break; case 'addSharedFolder': _addSharedFolder(Env, data, cb); break; + case 'addLink': + _addLink(Env, data, cb); break; case 'restoreSharedFolder': _restoreSharedFolder(Env, data, cb); break; case 'convertFolderToSharedFolder': @@ -1045,6 +1073,8 @@ define([ _rename(Env, data, cb); break; case 'setFolderData': _setFolderData(Env, data, cb); break; + case 'updateStaticAccess': + _updateStaticAccess(Env, data, cb); break; default: cb(); } @@ -1129,8 +1159,8 @@ define([ data: uo.getFileData(id) }; }).filter(function (d) { - if (channels.indexOf(d.data.channel) === -1) { - channels.push(d.data.channel); + if (channels.indexOf(d.data.channel || d.id) === -1) { + channels.push(d.data.channel || d.id); return true; } }); @@ -1383,6 +1413,16 @@ define([ } }, cb); }; + var addLinkInner = function (Env, path, data, cb) { + return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "addLink", + data: { + path: path, + name: data.name, + href: data.url + } + }, cb); + }; var restoreSharedFolderInner = function (Env, fId, password, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "restoreSharedFolder", @@ -1433,6 +1473,14 @@ define([ }, cb); }; + var updateStaticAccessInner = function (Env, id, cb) { + return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "updateStaticAccess", + data: id + }, cb); + + }; + /* Tools */ var findChannels = _findChannels; @@ -1450,6 +1498,11 @@ define([ return String(uo.getTitle(id, type)); }; + var isStaticFile = function (Env, id) { + var uo = _getUserObjectFromId(Env, id); + return uo.isStaticFile(id); + }; + var isReadOnlyFile = function (Env, id) { var uo = _getUserObjectFromId(Env, id); return uo.isReadOnlyFile(id); @@ -1491,7 +1544,7 @@ define([ var files = []; var userObjects = _getUserObjects(Env); userObjects.forEach(function (uo) { - var data = uo.getFiles([UserObject.FILES_DATA]).map(function (id) { + var data = uo.getFiles([UserObject.FILES_DATA, UserObject.STATIC_DATA]).map(function (id) { return [Number(id), uo.getFileData(id)]; }); Array.prototype.push.apply(files, data); @@ -1608,17 +1661,20 @@ define([ emptyTrash: callWithEnv(emptyTrashInner), addFolder: callWithEnv(addFolderInner), addSharedFolder: callWithEnv(addSharedFolderInner), + addLink: callWithEnv(addLinkInner), restoreSharedFolder: callWithEnv(restoreSharedFolderInner), convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner), delete: callWithEnv(deleteInner), deleteOwned: callWithEnv(deleteOwnedInner), restore: callWithEnv(restoreInner), setFolderData: callWithEnv(setFolderDataInner), + updateStaticAccess: callWithEnv(updateStaticAccessInner), // Tools getFileData: callWithEnv(getFileData), find: callWithEnv(find), getTitle: callWithEnv(getTitle), isReadOnlyFile: callWithEnv(isReadOnlyFile), + isStaticFile: callWithEnv(isStaticFile), getFiles: callWithEnv(getFiles), search: callWithEnv(search), getRecentPads: callWithEnv(getRecentPads), diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 0fd250f80..96997cd06 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -733,11 +733,20 @@ define([ if (!common.isLoggedIn()) { return; } $embedButton = common.createButton('mediatag', true).click(function () { var cfg = { - types: ['file'], + types: ['file', 'link'], where: ['root'] }; if ($embedButton.data('filter')) { cfg.filter = $embedButton.data('filter'); } common.openFilePicker(cfg, function (data) { + // Embed links + if (data.static) { + var a = h('a', { + href: data.href + }, data.name); + mediaTagEmbedder($(a), data); + return; + } + // Embed files if (data.type !== 'file') { console.log("Unexpected data type picked " + data.type); return; diff --git a/www/common/userObject.js b/www/common/userObject.js index ca716f25f..9de3ea918 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -17,6 +17,7 @@ define([ var SHARED_FOLDERS_TEMP = module.SHARED_FOLDERS_TEMP = "sharedFoldersTemp"; // Maybe deleted or new password var FILES_DATA = module.FILES_DATA = Constants.storageKey; var OLD_FILES_DATA = module.OLD_FILES_DATA = Constants.oldStorageKey; + var STATIC_DATA = module.STATIC_DATA = 'static'; // Create untitled documents when no name is given var getLocaleDate = function () { @@ -138,6 +139,7 @@ define([ var NEW_FILE_NAME = Messages.fm_newFile || 'New file'; exp.ROOT = ROOT; + exp.STATIC_DATA = STATIC_DATA; exp.UNSORTED = UNSORTED; exp.TRASH = TRASH; exp.TEMPLATE = TEMPLATE; @@ -236,6 +238,10 @@ define([ return Boolean(data.roHref && !data.href); }; + exp.isStaticFile = function (element) { + return Boolean(files[STATIC_DATA] && files[STATIC_DATA][element]); + }; + var isFolder = exp.isFolder = function (element) { if (isFolderData(element)) { return false; } return typeof(element) === "object" || isSharedFolder(element); @@ -310,6 +316,12 @@ define([ // Get data from AllFiles (Cryptpad_RECENTPADS) var getFileData = exp.getFileData = function (file, editable) { if (!file) { return; } + var link = (files[STATIC_DATA] || {})[file]; + if (link) { + var _link = editable ? link : Util.clone(link); + if (!editable) { _link.static = true; } + return _link; + } var data = files[FILES_DATA][file] || {}; if (!editable) { data = JSON.parse(JSON.stringify(data)); @@ -344,6 +356,7 @@ define([ return '??'; } var data = getFileData(file); + if (data.static) { return data.name; } if (!file || !data || !(data.href || data.roHref)) { error("getTitle called with a non-existing file id: ", file, data); return; @@ -475,6 +488,11 @@ define([ }); return ret; }; + _getFiles[STATIC_DATA] = function () { + var ret = []; + if (!files[STATIC_DATA]) { return ret; } + return Object.keys(files[STATIC_DATA]).map(Number).filter(Boolean); + }; _getFiles[FILES_DATA] = function () { var ret = []; if (!files[FILES_DATA]) { return ret; } @@ -854,6 +872,7 @@ define([ // RENAME exp.rename = function (path, newName, cb) { + cb = cb || function () {}; if (sframeChan) { return void sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "rename", @@ -891,9 +910,15 @@ define([ if (isSharedFolder(element)) { data = files[SHARED_FOLDERS][element]; } else { - data = files[FILES_DATA][element]; + data = files[FILES_DATA][element] || files[STATIC_DATA][element]; } if (!data) { return; } + if (files[STATIC_DATA][element]) { + if (!newName || !newName.trim()) { return void cb(); } + data.name = newName; + cb(); + return; + } if (!newName || newName.trim() === "") { delete data.filename; if (typeof cb === "function") { cb(); } diff --git a/www/secureiframe/inner.js b/www/secureiframe/inner.js index 4226c7f82..18970c47b 100644 --- a/www/secureiframe/inner.js +++ b/www/secureiframe/inner.js @@ -127,6 +127,7 @@ define([ sframeChan.event("EV_SECURE_ACTION", { type: parsed.type, password: data.password, + static: data.static, href: data.url, name: data.name }); @@ -214,20 +215,21 @@ define([ $container.html(''); Object.keys(list).forEach(function (id) { var data = list[id]; - var name = data.filename || data.title || '?'; + var name = data.filename || data.title || data.name || '?'; if (filter && name.toLowerCase().indexOf(filter.toLowerCase()) === -1) { return; } var $span = $('', { 'class': 'cp-filepicker-content-element', - 'title': name, + 'title': Util.fixHTML(name), }).appendTo($container); $span.append(UI.getFileIcon(data)); $('', {'class': 'cp-filepicker-content-element-name'}).text(name) .appendTo($span); + if (data.static) { $span.attr('title', Util.fixHTML(data.href)); } $span.click(function () { if (typeof onFilePicked === "function") { - onFilePicked({url: data.href, name: name, password: data.password}); + onFilePicked({url: data.href, name: name, static: data.static, password: data.password}); } }); diff --git a/www/teams/main.js b/www/teams/main.js index ea48f72a1..7d12126eb 100644 --- a/www/teams/main.js +++ b/www/teams/main.js @@ -24,7 +24,10 @@ define([ sframeChan.on('Q_DRIVE_USEROBJECT', function (data, cb) { if (!teamId) { return void cb({error: 'EINVAL'}); } - data.teamId = teamId; + // a teamId of -1 bypasses guards against modifying your drive + // from the team app + if (data.teamId !== -1) { data.teamId = teamId; } + else { delete data.teamId; } Cryptpad.userObjectCommand(data, cb); }); sframeChan.on('Q_DRIVE_GETOBJECT', function (data, cb) {