diff --git a/CHANGELOG.md b/CHANGELOG.md index f98c1ea68..26dc534d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +# 4.9.0 + +## Goals and announcements + +We allocated most of this release cycle towards a schedule of one-on-one user interviews and some broad usage studies leveraging our new Form app. The remainder of our time was spent on some minor improvements. We'll continue at a slightly slower pace of implementation for the coming weeks while we complete our scheduled interviews and take some much-needed vacations. + +## Update notes + +It appears our promotion of the checkup page through our recent release notes and the inclusion of a link to it from the instance admin have been moderately successful. We've observed that more instance admins are noticing and fixing some common configuration issues. + +This release features some minor changes to one instance configuration test which incorrectly provided an exemption for the use of `http://localhost:3000` as an `httpUnsafeOrigin` value. This exemption was provided because it's valid for local development, however, it suppressed errors when this configuration was used for production instances where it could cause a variety of problems. As usual, we recommend checking your instance's admin page after updating to confirm that you are passing the latest tests. Information about the checkup page is included in [our documentation](https://docs.cryptpad.fr/en/admin_guide/admin_panel.html#network). + +To update from 4.8.0 to 4.9.0: + +1. Stop your server +2. Get the latest code with git +3. Install the latest dependencies with `bower update` and `npm i` +4. Restart your server +5. Confirm that your instance is passing all the tests included on the `/checkup/` page + +## Features + +* We've added the ability to store URLs in user and team drives as requested in a private support ticket and [this issue](https://github.com/xwiki-labs/cryptpad/issues/732). Links can be shared directly with contacts. Unlike pads, links are not collaborative objects, so updating a link's name will not update the entry in another user's drive if you've already shared it with them. Links are integrated into our apps' _insert_ menu to facilitate quick insertion of links you've stored into your documents. We're interested in measuring how this functionality is used in practice so we can decide whether it's worth spending more time on it, so we've added some telemetry to measure (in aggregate) how often its components are used. We anonymize IP addresses in the logs for CryptPad.fr, but as always, you can disable telemetry via your settings panel. +* Our rich text editor now supports indentation with the tab key, as per [this ticket](https://github.com/xwiki-labs/cryptpad/issues/634). +* Forms received another round of improvements to styles, workflows, and some basic survey functionality to yield more accurate results. + * Ordered lists are now shuffled for each survey participant so that their initial order has less effect on the final results. + * CSV export now uses a layout that makes poll options easier to read. + * Unregistered users can now add a name to their response. + * Form results are displayed automatically (when available) to those who have answered. + * Authors and auditors can now click on some types of answers to jump directly to other answers from the same user. +* Users with very large drives might notice that their account loads slightly faster now, due to some minor optimizations in an integrity check that the client performs when loading accounts. + +## Bugs + +* We've added a guard against a type error that could be triggered when loading teams under certain rare conditions. +* Unregistered users' drives now show the "bread-crumb" UI for navigating between folders when viewing a shared folder in read-only mode. We've also suppressed the "Files" button for displaying the tree view which was non-functional for such users. +* A change in the format of support tickets caused tickets recently created by premium users to not be recognized as such. We've fixed the categorization in the admin panel's support ticket view. +* We've fixed a number of minor issues with forms: + * The maximum number of selectable choices for checkbox questions can no longer exceed the number of available choices. + * We guard against a type error that could occur when parsing dates. + * Forms imported from templates now have their initial title corrected. + * We've disabled the use of our indexedDB caching system for form results, since it was quietly dropping older responses when more than 100 responses had been submitted. We plan to re-enable caching for results once we've updated the eviction metric to better handle the response format. + # 4.8.0 ## Goals diff --git a/customize.dist/images/YannFlory.jpg b/customize.dist/images/YannFlory.jpg deleted file mode 100644 index 9c358d1a6..000000000 Binary files a/customize.dist/images/YannFlory.jpg and /dev/null differ diff --git a/customize.dist/images/logo_white.png b/customize.dist/images/logo_white.png deleted file mode 100644 index 8219cc2b0..000000000 Binary files a/customize.dist/images/logo_white.png and /dev/null differ diff --git a/customize.dist/images/logo_white.svg b/customize.dist/images/logo_white.svg deleted file mode 100644 index d1cd2bc73..000000000 --- a/customize.dist/images/logo_white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/customize.dist/old_logos/CryptPad-white-logo.svg b/customize.dist/old_logos/CryptPad-white-logo.svg deleted file mode 100644 index 26131b502..000000000 --- a/customize.dist/old_logos/CryptPad-white-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/customize.dist/old_logos/CryptPad_logo_color.svg b/customize.dist/old_logos/CryptPad_logo_color.svg deleted file mode 100644 index 64fc86682..000000000 --- a/customize.dist/old_logos/CryptPad_logo_color.svg +++ /dev/null @@ -1 +0,0 @@ -CryptPad_logo_color \ No newline at end of file diff --git a/customize.dist/old_logos/CryptPadlogo_op5.svg b/customize.dist/old_logos/CryptPadlogo_op5.svg deleted file mode 100644 index aa5acb201..000000000 --- a/customize.dist/old_logos/CryptPadlogo_op5.svg +++ /dev/null @@ -1 +0,0 @@ -CryptPadlogo \ No newline at end of file diff --git a/customize.dist/old_logos/cryptofist_mini.png b/customize.dist/old_logos/cryptofist_mini.png deleted file mode 100644 index 73845e455..000000000 Binary files a/customize.dist/old_logos/cryptofist_mini.png and /dev/null differ diff --git a/customize.dist/old_logos/cryptofist_small.png b/customize.dist/old_logos/cryptofist_small.png deleted file mode 100644 index 9c818eb45..000000000 Binary files a/customize.dist/old_logos/cryptofist_small.png and /dev/null differ diff --git a/customize.dist/old_logos/cryptpad-new-logo-colors-logoonly.png b/customize.dist/old_logos/cryptpad-new-logo-colors-logoonly.png deleted file mode 100644 index 722438d54..000000000 Binary files a/customize.dist/old_logos/cryptpad-new-logo-colors-logoonly.png and /dev/null differ diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 18632f6ea..18d65c680 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -105,7 +105,7 @@ define([ var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? '/imprint.html' : AppConfig.imprint); - Pages.versionString = "v4.8.0"; + Pages.versionString = "v4.9.0"; // used for the about menu diff --git a/lib/env.js b/lib/env.js index 3d58f11f7..4db282d77 100644 --- a/lib/env.js +++ b/lib/env.js @@ -123,7 +123,7 @@ module.exports.create = function (config) { maxWorkers: config.maxWorkers, disableIntegratedTasks: config.disableIntegratedTasks || false, - disableIntegratedEviction: typeof(config.disableIntegratedEviction) === 'undefined'? true: config.disableIntegratedEviction, // XXX false, + disableIntegratedEviction: typeof(config.disableIntegratedEviction) === 'undefined'? true: config.disableIntegratedEviction, // XXX 4.10.0 false, lastEviction: +new Date(), evictionReport: {}, commandTimers: {}, diff --git a/package-lock.json b/package-lock.json index 4d7992f82..42136e238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "4.8.0", + "version": "4.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9e18f05f0..68a315891 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "4.8.0", + "version": "4.9.0", "license": "AGPL-3.0+", "repository": { "type": "git", diff --git a/www/admin/inner.js b/www/admin/inner.js index 61cf05896..b35059df0 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -833,7 +833,8 @@ define([ var premium = t.some(function (msg) { var _ed = Util.find(msg, ['content', 'msg', 'content', 'sender', 'edPublic']); if (ed !== _ed) { return; } - return Util.find(msg, ['content', 'msg', 'content', 'sender', 'plan']); + return Util.find(msg, ['content', 'msg', 'content', 'sender', 'plan']) || + Util.find(msg, ['content', 'msg', 'content', 'sender', 'quota', 'plan']); }); var lastMsg = t[t.length - 1]; var lastMsgEd = Util.find(lastMsg, ['content', 'msg', 'content', 'sender', 'edPublic']); diff --git a/www/checkup/main.js b/www/checkup/main.js index f4a40d11e..90c16a29a 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -705,17 +705,19 @@ define([ var isOnion = function (host) { return /\.onion$/.test(host); }; + var isLocalhost = function (host) { + return /^http:\/\/localhost/.test(host); + }; + assert(function (cb, msg) { // provide an exception for development instances - if (/http:\/\/localhost/.test(trimmedUnsafe)) { return void cb(true); } + if (isLocalhost(trimmedUnsafe) && isLocalhost(window.location.href)) { return void cb(true); } // if both the main and sandbox domains are onion addresses // then the HTTPS requirement is unnecessary if (isOnion(trimmedUnsafe) && isOnion(trimmedSafe)) { return void cb(true); } // otherwise expect that both inner and outer domains use HTTPS - setWarningClass(msg); - msg.appendChild(h('span', [ "Both ", code('httpUnsafeOrigin'), 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..99d506347 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,75 @@ 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.link_open = "Open URL"; + // openLinkInNewTab ("Open Link in New Tab") + // fc_open ("Open") + // share_linkOpen ("Preview") + // resources_openInNewTab ("Open it in a new tab") + Messages.link_open = Messages.fc_open; // XXX 4.10.0 + + //Messages.link_store = "Store link in drive"; + // toolbar_storeInDrive ? ("Store in CryptDrive") + // autostore_store ? ("Store") + Messages.link_store = Messages.toolbar_storeInDrive; // XXX 4.10.0 + + + 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; + Feedback.send('LINK_RECEIVED_LATER'); + }, + keys: [27] + }, { + className: 'primary', + name: Messages.link_open, + onClick: function () { + if (clicked) { return true; } + clicked = true; + common.openUnsafeURL(url); + Feedback.send("LINK_RECEIVED_OPEN"); + }, + 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(); + Feedback.send("LINK_RECEIVED_STORE"); + }); + 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/common-util.js b/www/common/common-util.js index 51540a660..ac50a1f24 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -9,6 +9,15 @@ return Array.prototype.slice.call(A, start, end); }; + Util.shuffleArray = function (a) { + for (var i = a.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var tmp = a[i]; + a[i] = a[j]; + a[j] = tmp; + } + }; + Util.bake = function (f, args) { if (typeof(args) === 'undefined') { args = []; } if (!Array.isArray(args)) { args = [args]; } @@ -152,6 +161,10 @@ }; }; + Util.inc = function (map, key, val) { + map[key] = (map[key] || 0) + (typeof(val) === 'number'? val: 1); + }; + Util.find = function (map, path) { var l = path.length; for (var i = 0; i < l; i++) { diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index b2489c22b..15558cf18 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -443,6 +443,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_new)), ]), ]), $separator.clone()[0], @@ -1106,11 +1111,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 +1165,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 +1277,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 +1916,31 @@ 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') { + //console.error(name, Util.fixHTML(name)); + // this is only safe because our build of tippy sets titles as + // 'textContent' instead of innerHTML, otherwise + // we would need to use Util.fixHTML + $element.attr('title', name); + } + + var type = Messages.fm_link_type; + 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 +1956,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 +2169,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 +2506,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 +2732,74 @@ define([ }); $input.click(); }; + var showLinkModal = function () { + 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', [ + h('label', {for: 'cp-app-drive-link-name'}, Messages.fm_link_name), + name = h('input#cp-app-drive-link-name', { autocomplete: 'off', placeholder: Messages.fm_link_name_placeholder }), + h('label', {for: 'cp-app-drive-link-url'}, Messages.fm_link_url), + url = h('input#cp-app-drive-link-url', { type: 'url', autocomplete: 'off', placeholder: Messages.form_input_ph_url }), + warning, + ]); + + var protocolPattern = /https*:\/\//; + var fragmentPattern = /#.*$/; + var setNamePlaceholder = function (val) { + var temp = val.replace(protocolPattern, '').replace(fragmentPattern, '').trim().slice(0, 48); + if (!protocolPattern.test(val) || !temp) { + temp = Messages.fm_link_name_placeholder; + } + name.setAttribute('placeholder', temp); + }; + + 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; + } + setNamePlaceholder(v); + $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 $name = $(name); + var n = $name.val().trim() || $name.attr('placeholder'); + var u = $url.val().trim(); + if (!n || !u) { return true; } + if (!Util.isValidURL(u)) { + // XXX 4.10.0 add style for invalid input? input:invalid + UI.warn(Messages.fm_link_invalid); + return true; + } + manager.addLink(currentPath, { + name: n, + url: u + }, refresh); + Feedback.send("LINK_CREATED"); + }, + keys: [13] + }); + var m = UI.dialog.customModal(content, { + buttons: buttons + }); + UI.openCustomModal(m); + }; var addNewPadHandlers = function ($block, isInRoot) { // Handlers if (isInRoot) { @@ -2714,6 +2828,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 +2872,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_new + }); + options.push({tag: 'hr'}); } getNewPadTypes().forEach(function (type) { var attributes = { @@ -3073,6 +3194,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_type)); } // Pads getNewPadTypes().forEach(function (type) { @@ -3479,6 +3607,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 +4611,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 +4631,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 +4683,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..4bcf0d621 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, @@ -119,6 +124,9 @@ define([ channel: mailbox.notifications, curvePublic: mailbox.curvePublic }); + if (config.static) { + Feedback.send("LINK_SHARED_WITH_CONTACT"); + } return; } } @@ -137,6 +145,21 @@ 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); + }); + Feedback.send("LINK_ADDED_TO_DRIVE"); + return; + } sframeChan.query('Q_STORE_IN_TEAM', { href: href, password: config.password, @@ -346,6 +369,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 +387,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 +431,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 +592,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 +717,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 +736,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 +767,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..83052ff68 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -92,6 +92,10 @@ define([ (type === 'file' ? 'notification_fileShared' : // Msg.notification_fileSharedTeam 'notification_padShared'); // Msg.notification_padSharedTeam + 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 +113,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..d8e558dad 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)) { @@ -2053,8 +2058,17 @@ define([ } catch (e) { console.error(e); } + // Tell all the owners that the pad was deleted from the server - var curvePublic = store.proxy.curvePublic; + var curvePublic; + try { + // users in noDrive mode don't have a proxy and + // unregistered users don't have a curvePublic + curvePublic = store.proxy.curvePublic; + } catch (err) { + console.error(err); + return; + } m.forEach(function (obj) { var mb = JSON.parse(obj); if (mb.curvePublic === curvePublic) { return; } diff --git a/www/common/outer/cache-store.js b/www/common/outer/cache-store.js index 514e18ab7..93bc0d0c6 100644 --- a/www/common/outer/cache-store.js +++ b/www/common/outer/cache-store.js @@ -97,7 +97,7 @@ define([ var checkCheckpoints = function (array) { if (!Array.isArray(array)) { return; } // Keep the last 100 messages - if (array.length > 100) { // XXX + if (array.length > 100) { // XXX 4.10.0 array.splice(0, array.length - 100); } // Remove every message before the first checkpoint 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/team.js b/www/common/outer/team.js index 4184b8482..94f91c807 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -195,7 +195,7 @@ define([ Pinpad.create(ctx.store.network, data, function (e, call) { if (e) { return void cb(e); } team.rpc = call; - team.onRpcReadyEvt.fire(); + if (team && team.onRpcReadyEvt) { team.onRpcReadyEvt.fire(); } cb(); }, Cache); }); diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index e7c55da67..7108da936 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..d6492c68c 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,35 +1037,40 @@ 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 COMMANDS = { + move: _move, + restore: _restore, + addFolder: _addFolder, + addSharedFolder: _addSharedFolder, + addLink: _addLink, + restoreSharedFolder: _restoreSharedFolder, + convertFolderToSharedFolder: _convertFolderToSharedFolder, + delete: _delete, + deleteOwned: _deleteOwned, + emptyTrash: _emptyTrash, + rename: _rename, + setFolderData: _setFolderData, + updateStaticAccess: _updateStaticAccess, + }; + var onCommand = function (Env, cmdData, cb) { var cmd = cmdData.cmd; var data = cmdData.data || {}; - switch (cmd) { - case 'move': - _move(Env, data, cb); break; - case 'restore': - _restore(Env, data, cb); break; - case 'addFolder': - _addFolder(Env, data, cb); break; - case 'addSharedFolder': - _addSharedFolder(Env, data, cb); break; - case 'restoreSharedFolder': - _restoreSharedFolder(Env, data, cb); break; - case 'convertFolderToSharedFolder': - _convertFolderToSharedFolder(Env, data, cb); break; - case 'delete': - _delete(Env, data, cb); break; - case 'deleteOwned': - _deleteOwned(Env, data, cb); break; - case 'emptyTrash': - _emptyTrash(Env, data, cb); break; - case 'rename': - _rename(Env, data, cb); break; - case 'setFolderData': - _setFolderData(Env, data, cb); break; - default: - cb(); + var method = COMMANDS[cmd]; + + if (typeof(method) === 'function') { + return void method(Env, data, cb); } + // if the command was not handled then call back + cb(); }; // Set the value everywhere the given pad is stored (main and shared folders) @@ -1129,8 +1152,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; } }); @@ -1233,7 +1256,10 @@ define([ Array.prototype.push.apply(result, sfChannels); } - return result; + return result.filter(function (channel) { + if (typeof(channel) !== 'string') { return; } + return [32, 48].indexOf(channel.length) !== -1; + }); }; var addPad = function (Env, path, pad, cb) { @@ -1383,6 +1409,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 +1469,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 +1494,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 +1540,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 +1657,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/sframe-common-outer.js b/www/common/sframe-common-outer.js index b0759b5ec..575971368 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1925,7 +1925,6 @@ define([ var cryptputCfg = $.extend(true, {}, rtConfig, {password: password}); if (data.templateContent) { Cryptget.put(currentPad.hash, JSON.stringify(data.templateContent), function () { - console.error(arguments); startRealtime(); cb(); }, cryptputCfg); @@ -2004,6 +2003,8 @@ define([ sframeChan.on('EV_BURN_AFTER_READING', function () { startRealtime(); + // feedback fails for users in noDrive mode + Utils.Feedback.send("BURN_AFTER_READING", Boolean(cfg.noDrive)); }); sframeChan.ready(); diff --git a/www/common/sframe-common-title.js b/www/common/sframe-common-title.js index 2539d1542..d1b06df73 100644 --- a/www/common/sframe-common-title.js +++ b/www/common/sframe-common-title.js @@ -23,6 +23,12 @@ define([ var $title; exp.setToolbar = function (toolbar) { $title = toolbar && (toolbar.title || toolbar.pageTitle); + if ($title && exp.defaultTitle) { + var md = metadataMgr.getMetadata(); + $title.find('input').prop('placeholder', md.defaultTitle); + $title.find('span.cp-toolbar-title-value').text(md.title || md.defaultTitle); + $title.find('input').val(md.title || md.defaultTitle); + } }; exp.getTitle = function () { return exp.title; }; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 785d1bc7e..5b53aa9a1 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -867,10 +867,6 @@ MessengerUI, Messages, Pages) { 'class': "cp-toolbar-link-logo" }).append(UIElements.getSvgLogo()); - /*.append($('', { - //src: '/customize/images/logo_white.png?' + ApiConfig.requireConf.urlArgs - src: '/customize/favicon/main-favicon.png?' + ApiConfig.requireConf.urlArgs - }));*/ var onClick = function (e) { e.preventDefault(); if (e.ctrlKey) { 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/convert/inner.js b/www/convert/inner.js index cdcf745ca..ba9870ccf 100644 --- a/www/convert/inner.js +++ b/www/convert/inner.js @@ -192,8 +192,8 @@ define([ }, }; - Messages.convertPage = "Convert"; // XXX - Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX + Messages.convertPage = "Convert"; // XXX 4.10.0 + Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX 4.10.0 var createToolbar = function () { var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications']; diff --git a/www/form/app-form.less b/www/form/app-form.less index 95802e18b..60da1daff 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -247,6 +247,15 @@ .cp-form-anon-answer { text-align: center; margin: 20px 0 30px 0; + .cp-form-anon-answer-input { + margin-top: 20px; + display: flex; + white-space: nowrap; + align-items: center; + input { + margin-left: 10px; + } + } } } @@ -464,6 +473,16 @@ display: flex; flex-flow: column; position: relative; + + #cp-form-response-msg { + background: @cp_form-bg1; + margin-bottom: 20px; + padding: 10px; + p:last-child { + margin-bottom: 0; + } + } + .cp-form-creator-results-controls { margin-bottom: 20px; //background: @cp_form-bg1; @@ -529,6 +548,7 @@ .cp-form-individual { background: @cp_form-bg1; padding: 10px; + margin-bottom: 20px; & > *:not(:last-child) { margin-right: 10px; } @@ -659,6 +679,16 @@ .avatar_main(30px); margin-right: 10px; } + &.cp-clickable { + cursor: pointer; + &:hover { + color: @cryptpad_color_link; + &::after { + font-family: FontAwesome; + content: "\00a0\f06e"; + } + } + } } .cp-poll-time-day { flex-basis: 100px; @@ -677,6 +707,16 @@ -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } + .cp-form-poll-option, .cp-poll-time-day { + flex-flow: column; + text-align: center; + .cp-form-weekday-separator { + display: none; + } + } + .cp-poll-time-day { + height: 50px; + } } &.cp-form-poll-switch { display: flex; @@ -706,12 +746,21 @@ } .cp-form-poll-option, .cp-poll-switch { width: 200px; + .cp-form-weekday-separator { + margin-right: 5px; + margin-left: 5px; + } } .cp-poll-time-day { flex-basis: 40px; border-right: none; border-right: 1px solid @cryptpad_text_col; border-bottom: 0px; + flex-flow: column; + text-align: center; + .cp-form-weekday-separator { + display: none; + } } } .cp-form-poll-choice, .cp-form-poll-answer { diff --git a/www/form/inner.js b/www/form/inner.js index 2a1501ecb..1b555be5d 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -298,6 +298,7 @@ define([ del ]); $(del).click(function () { + var $block = $(el).closest('.cp-form-edit-block'); $(el).remove(); // We've just deleted an item/option so we should be under the MAX limit and // we can show the "add" button again @@ -306,6 +307,13 @@ define([ $add.show(); if (v.type === "time") { $(addMultiple).show(); } } + // decrement the max choices input when there are fewer options than the current maximum + if (maxInput) { + var inputs = $block.find('input').length; + var $maxInput = $(maxInput); + var currentMax = Number($maxInput.val()); + $maxInput.val(Math.min(inputs, currentMax)); + } }); return el; }; @@ -575,7 +583,22 @@ define([ ]; }; - var makePollTable = function (answers, opts) { + var getWeekDays = function (large) { + var baseDate = new Date(2017, 0, 1); // just a Sunday + var weekDays = []; + for(var i = 0; i < 7; i++) { + weekDays.push(baseDate.toLocaleDateString(undefined, { weekday: 'long' })); + baseDate.setDate(baseDate.getDate() + 1); + } + if (!large) { + weekDays = weekDays.map(function (day) { return day.slice(0,3); }); + } + return weekDays.map(function (day) { return day.replace(/^./, function (str) { return str.toUpperCase(); }); }); + }; + + // "resultsPageObj" is an object with "content" and "answers" + // only available when viewing the Responses page + var makePollTable = function (answers, opts, resultsPageObj) { // Sort date values if (opts.type !== "text") { opts.values.sort(function (a, b) { @@ -583,18 +606,25 @@ define([ }); } // Create first line with options + var allDays = getWeekDays(true); var els = opts.values.map(function (data) { + var _date; if (opts.type === "day") { - var _date = new Date(data); + _date = new Date(data); data = _date.toLocaleDateString(); } if (opts.type === "time") { - var _dateT = new Date(data); - data = Flatpickr.formatDate(_dateT, timeFormat); + _date = new Date(data); + data = Flatpickr.formatDate(_date, timeFormat); } + var day = _date && allDays[_date.getDay()]; return h('div.cp-poll-cell.cp-form-poll-option', { title: Util.fixHTML(data) - }, data); + }, [ + opts.type === 'day' ? h('span.cp-form-weekday', day) : undefined, + opts.type === 'day' ? h('span.cp-form-weekday-separator', ' - ') : undefined, + h('span', data) + ]); }); // Insert axis switch button var switchAxis = h('button.btn.btn-default', [ @@ -611,13 +641,20 @@ define([ opts.values.forEach(function (d) { var date = new Date(d); var day = date.toLocaleDateString(); - _days[day] = _days[day] || 0; - _days[day]++; + _days[day] = { + n: (_days[day] && _days[day].n) || 0, + name: allDays[date.getDay()] + }; + _days[day].n++; }); Object.keys(_days).forEach(function (day) { days.push(h('div.cp-poll-cell.cp-poll-time-day', { - style: 'flex-grow:'+(_days[day]-1)+';' - }, day)); + style: 'flex-grow:'+(_days[day].n - 1)+';' + }, [ + h('span.cp-form-weekday', _days[day].name), + h('span.cp-form-weekday-separator', ' - '), + h('span', day) + ])); }); lines.unshift(h('div', days)); } @@ -640,13 +677,19 @@ define([ }, v); return cell; }); - els.unshift(h('div.cp-poll-cell.cp-poll-answer-name', { + var nameCell; + els.unshift(nameCell = h('div.cp-poll-cell.cp-poll-answer-name', { title: Util.fixHTML(name) }, [ avatar, h('span', name) ])); bodyEls.push(h('div', els)); + if (resultsPageObj && (APP.isEditor || APP.isAuditor)) { + $(nameCell).addClass('cp-clickable').click(function () { + APP.renderResults(resultsPageObj.content, resultsPageObj.answers, answerObj.curve); + }); + } }); } var body = h('div.cp-form-poll-body', bodyEls); @@ -788,6 +831,7 @@ define([ if (filterCurve && user === filterCurve) { return; } try { return { + curve: user, user: answers[user].msg._userdata, results: answers[user].msg[uid] }; @@ -950,7 +994,7 @@ define([ printResults: function (answers, uid) { var results = []; var empty = 0; - Object.keys(answers).forEach(function (author) { + Object.keys(answers).forEach(function (author) { // TODO deduplicate these? var obj = answers[author]; var answer = obj.msg[uid]; if (!answer || !answer.trim()) { return empty++; } @@ -1016,7 +1060,7 @@ define([ printResults: function (answers, uid) { var results = []; var empty = 0; - Object.keys(answers).forEach(function (author) { + Object.keys(answers).forEach(function (author) { // TODO deduplicate these var obj = answers[author]; var answer = obj.msg[uid]; if (!answer || !answer.trim()) { return empty++; } @@ -1090,8 +1134,7 @@ define([ var obj = answers[author]; var answer = obj.msg[uid]; if (!answer || !answer.trim()) { return empty++; } - count[answer] = count[answer] || 0; - count[answer]++; + Util.inc(count, answer); }); Object.keys(count).forEach(function (value) { results.push(h('div.cp-form-results-type-radio-data', [ @@ -1193,8 +1236,7 @@ define([ var c = count[q_uid] = count[q_uid] || {}; var res = answer[q_uid]; if (!res || !res.trim()) { return; } - c[res] = c[res] || 0; - c[res]++; + Util.inc(c, res); }); }); Object.keys(count).forEach(function (q_uid) { @@ -1304,8 +1346,7 @@ define([ var answer = obj.msg[uid]; if (!Array.isArray(answer) || !answer.length) { return empty++; } answer.forEach(function (val) { - count[val] = count[val] || 0; - count[val]++; + Util.inc(count, val); }); }); Object.keys(count).forEach(function (value) { @@ -1420,8 +1461,7 @@ define([ var res = answer[q_uid]; if (!Array.isArray(res) || !res.length) { return; } res.forEach(function (v) { - c[v] = c[v] || 0; - c[v]++; + Util.inc(c, v); }); }); }); @@ -1470,7 +1510,9 @@ define([ if (!Array.isArray(opts.values)) { return; } var map = {}; var invMap = {}; - var els = opts.values.map(function (data, i) { + var sorted = false; + Util.shuffleArray(opts.values); + var els = opts.values.map(function (data) { var uid = Util.uid(); map[uid] = data; invMap[data] = uid; @@ -1479,7 +1521,7 @@ define([ h('i.fa.fa-ellipsis-v'), h('i.fa.fa-ellipsis-v'), ]), - h('span.cp-form-sort-order', (i+1)), + h('span.cp-form-sort-order', '?'), h('span', data) ]); $(div).data('val', data); @@ -1490,10 +1532,11 @@ define([ els ]); var $tag = $(tag); - var reorder = function () { + var reorder = function (reset) { $tag.find('.cp-form-type-sort').each(function (i, el) { - $(el).find('.cp-form-sort-order').text(i+1); + $(el).find('.cp-form-sort-order').text(reset ? '?' : i+1); }); + sorted = !reset; }; var cursorGetter; var setCursorGetter = function (f) { cursorGetter = f; }; @@ -1516,16 +1559,18 @@ define([ return { tag: tag, getValue: function () { + if (!sorted) { return; } return sortable.toArray().map(function (id) { return map[id]; }); }, reset: function () { + Util.shuffleArray(opts.values); var toSort = (opts.values).map(function (val) { return invMap[val]; }); sortable.sort(toSort); - reorder(); + reorder(true); }, edit: function (cb, tmp) { var v = Util.clone(opts); @@ -1554,7 +1599,7 @@ define([ if (!Array.isArray(answer) || !answer.length) { return empty++; } answer.forEach(function (el, i) { var score = l - i; - count[el] = (count[el] || 0) + score; + Util.inc(count, el, score); }); }); var sorted = Object.keys(count).sort(function (a, b) { @@ -1583,7 +1628,7 @@ define([ if (!opts) { opts = TYPES.poll.defaultOpts; } if (!Array.isArray(opts.values)) { return; } - var lines = makePollTable(answers, opts); + var lines = makePollTable(answers, opts, false); // Add form var addLine = opts.values.map(function (data) { @@ -1667,31 +1712,53 @@ define([ }; }, - printResults: function (answers, uid, form) { + printResults: function (answers, uid, form, content) { var opts = form[uid].opts || TYPES.poll.defaultOpts; var _answers = getBlockAnswers(answers, uid); - var lines = makePollTable(_answers, opts); + + // If content is defined, we'll be able to click on a row to display + // all the answers of this user + var lines = makePollTable(_answers, opts, content && { + content: content, + answers: answers + }); var total = makePollTotal(_answers, opts); if (total) { lines.push(h('div', total)); } return h('div.cp-form-type-poll', lines); }, - exportCSV: function (answer) { - if (answer === false) { return; } - if (!answer || !answer.values) { return ['']; } + exportCSV: function (answer, form) { + var opts = form.opts || TYPES.poll.defaultOpts; + var q = form.q || Messages.form_default; + if (answer === false) { + var cols = opts.values.map(function (key) { + return q + ' | ' + key; + }); + cols.unshift(q); + return cols; + } + if (!answer || !answer.values) { + var empty = opts.values.map(function () { return ''; }); + empty.unshift(''); + return empty; + } var str = ''; Object.keys(answer.values).sort().forEach(function (k, i) { if (i !== 0) { str += ';'; } str += k.replace(';', '').replace(':', '') + ':' + answer.values[k]; }); - return [str]; + var res = opts.values.map(function (key) { + return answer.values[key] || ''; + }); + res.unshift(str); + return res; }, icon: h('i.cptools.cptools-form-poll') }, }; - var renderResults = function (content, answers) { + var renderResults = APP.renderResults = function (content, answers, showUser) { var $container = $('div.cp-form-creator-results').empty(); if (!Object.keys(answers || {}).length) { @@ -1699,9 +1766,15 @@ define([ return; } + if (content.answers.msg) { + var description = h('div.cp-form-creator-results-description#cp-form-response-msg'); + var $desc = $(description).appendTo($container); + DiffMd.apply(DiffMd.render(content.answers.msg), $desc, APP.common); + } + var controls = h('div.cp-form-creator-results-controls'); var $controls = $(controls).appendTo($container); - var exportButton = h('button.btn.btn-secondary', Messages.exportButton); // XXX form_exportCSV; + var exportButton = h('button.btn.btn-secondary', Messages.form_exportCSV); var exportCSV = h('div.cp-form-creator-results-export', exportButton); $(exportCSV).appendTo($container); var results = h('div.cp-form-creator-results-content'); @@ -1729,7 +1802,9 @@ define([ var type = block.type; var model = TYPES[type]; if (!model || !model.printResults) { return; } - var print = model.printResults(answers, uid, form); + + // Only use content if we're not viewing individual answers + var print = model.printResults(answers, uid, form, !header && content); var q = h('div.cp-form-block-question', block.q || Messages.form_default); @@ -1823,14 +1898,23 @@ define([ e.preventDefault(); APP.common.openURL(Hash.hashToHref(ud.profile, 'profile')); }); + if (showUser === curve) { + setTimeout(function () { + showUser = undefined; + $(viewButton).click(); + }); + } return div; }); $results.append(els); }); + if (showUser) { + $s.click(); + } }; var addResultsButton = function (framework, content) { - var $res = $(h('button.cp-toolbar-appmenu', [ + var $res = $(h('button.cp-toolbar-appmenu.cp-toolbar-form-button', [ h('i.fa.fa-bar-chart'), h('span.cp-button-name', Messages.form_results) ])); @@ -1872,25 +1956,42 @@ define([ var makeFormControls = function (framework, content, update, evOnChange) { var loggedIn = framework._.sfCommon.isLoggedIn(); var metadataMgr = framework._.cpNfInner.metadataMgr; + var user = metadataMgr.getUserData(); if (!loggedIn && !content.answers.anonymous) { return; } var cbox; + var anonName, $anonName; cbox = UI.createCheckbox('cp-form-anonymous', Messages.form_anonymousBox, true, { mark: { tabindex:1 } }); + var $anonBox = $(cbox).find('input'); if (loggedIn) { if (!content.answers.anonymous || APP.cantAnon) { $(cbox).hide().find('input').attr('disabled', 'disabled').prop('checked', false); } + } else { + anonName = h('div.cp-form-anon-answer-input', [ + Messages.form_answerAs, + h('input', { + value: user.name || '', + placeholder: Messages.form_anonName + }) + ]); + $anonName = $(anonName).hide(); + $anonBox.on('change', function () { + if (Util.isChecked($anonBox)) { $anonName.hide(); } + else { $anonName.show(); } + }); } var send = h('button.cp-open.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); - var reset = h('button.cp-open.btn.btn-danger-alt', Messages.form_reset); + var reset = h('button.cp-open.cp-reset-button.btn.btn-danger-alt', Messages.form_reset); $(reset).click(function () { if (!Array.isArray(APP.formBlocks)) { return; } APP.formBlocks.forEach(function (data) { if (typeof(data.reset) === "function") { data.reset(); } }); + $(reset).attr('disabled', 'disabled'); }); var $send = $(send).click(function () { $send.attr('disabled', 'disabled'); @@ -1898,14 +1999,16 @@ define([ if (!results) { return; } var user = metadataMgr.getUserData(); - if (!Util.isChecked($(cbox).find('input'))) { + if (!Util.isChecked($anonBox)) { results._userdata = loggedIn ? { avatar: user.avatar, name: user.name, notifications: user.notifications, curvePublic: user.curvePublic, profile: user.profile - } : { name: user.name }; + } : { + name: $anonName ? $anonName.find('input').val() : user.name + }; } var sframeChan = framework._.sfCommon.getSframeChannel(); @@ -1985,7 +2088,10 @@ define([ return h('div.cp-form-send-container', [ invalid, - cbox ? h('div.cp-form-anon-answer', cbox) : undefined, + cbox ? h('div.cp-form-anon-answer', [ + cbox, + anonName + ]) : undefined, reset, send ]); }; @@ -1997,6 +2103,18 @@ define([ APP.formBlocks = []; + if (APP.isClosed && content.answers.privateKey && !APP.isEditor) { + var sframeChan = framework._.sfCommon.getSframeChannel(); + sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, obj) { + var answers = obj && obj.results; + if (answers) { APP.answers = answers; } + $('body').addClass('cp-app-form-results'); + $('.cp-toolbar-form-button').remove(); + renderResults(content, answers); + }); + return; + } + var evOnChange = Util.mkEvent(); if (!APP.isEditor) { var _answers = Util.clone(answers || {}); @@ -2004,6 +2122,7 @@ define([ delete _answers._userdata; evOnChange.reg(function (noBeforeUnload, isSave) { if (noBeforeUnload) { return; } + $container.find('.cp-reset-button').removeAttr('disabled'); var results = getFormResults(); if (isSave) { answers = Util.clone(results || {}); @@ -2356,6 +2475,9 @@ define([ // In view mode, add "Submit" and "reset" buttons $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); + if (!answers) { + $container.find('.cp-reset-button').attr('disabled', 'disabled'); + } }; var getTempFields = function () { @@ -2443,6 +2565,72 @@ define([ }; refreshPublic(); + var responseMsg = h('div.cp-form-response-msg-container'); + var $responseMsg = $(responseMsg); + var refreshResponse = function () { + if (true) { return; } // XXX 4.10.0 + $responseMsg.empty(); + Messages.form_updateMsg = "Update response message"; // XXX 4.10.0 + Messages.form_addMsg = "Add response message"; // XXX 4.10.0 + Messages.form_responseMsg = "Add a message that will be displayed in the response page."; // XXX 4.10.0 + var text = content.answers.msg ? Messages.form_updateMsg : Messages.form_addMsg; + var btn = h('button.btn.btn-secondary', text); + $(btn).click(function () { + var editor; + if (!APP.responseModal) { + var t = h('textarea'); + var div = h('div', [ + h('p', Messages.form_responseMsg), + t + ]); + var cm = SFCodeMirror.create("gfm", CMeditor, t); + editor = APP.responseEditor = cm.editor; + editor.setOption('lineNumbers', true); + editor.setOption('lineWrapping', true); + editor.setOption('styleActiveLine', true); + editor.setOption('readOnly', false); + setTimeout(function () { + editor.setValue(content.answers.msg || ''); + editor.refresh(); + editor.save(); + editor.focus(); + }); + + var buttons = [{ + className: 'primary', + name: Messages.settings_save, + onClick: function () { + var v = editor.getValue(); + content.answers.msg = v.trim(0, 2000); // XXX 4.10.0 max length? + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + refreshResponse(); + }); + }, + //keys: [] + }, { + className: 'cancel', + name: Messages.cancel, + onClick: function () {}, + keys: [27] + }]; + APP.responseModal = UI.dialog.customModal(div, { buttons: buttons }); + } else { + editor = APP.responseEditor; + setTimeout(function () { + editor.setValue(content.answers.msg || ''); + editor.refresh(); + editor.save(); + editor.focus(); + }); + } + UI.openCustomModal(APP.responseModal); + }); + // $responseMsg.append(btn); // XXX 4.10.0 + }; + //refreshResponse(); + // Allow anonymous answers var privacyContainer = h('div.cp-form-privacy-container'); var $privacy = $(privacyContainer); @@ -2538,11 +2726,13 @@ define([ evOnChange.reg(refreshPublic); evOnChange.reg(refreshPrivacy); evOnChange.reg(refreshEndDate); + //evOnChange.reg(refreshResponse); return [ endDateContainer, privacyContainer, resultsType, + responseMsg ]; }; diff --git a/www/form/main.js b/www/form/main.js index 3b2c160a4..de2c311ef 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -176,7 +176,7 @@ define([ validateKey: keys.secondaryValidateKey, owners: [myKeys.edPublic], crypto: crypto, - //Cache: Utils.Cache // XXX + //Cache: Utils.Cache // XXX 4.10.0 }; var results = {}; config.onError = function (info) { @@ -265,6 +265,7 @@ define([ }, function (obj) { if (obj && obj.error) { return void cb(obj); } var messages = obj.messages; + if (!messages.length) { return void cb(); } var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, { validateKey: data.validateKey, ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate), diff --git a/www/pad/inner.js b/www/pad/inner.js index 304e9abe4..95e7c1bff 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -1326,6 +1326,11 @@ define([ })); $(waitFor()); }).nThen(function(waitFor) { + // TODO this breaks users' ability to tab out of the editor + // but that's a problem in other editors and nobody has complained so far + // so we'll include this as-is for now while we search for a good pattern + // addresses this issue more generally + Ckeditor.config.tabSpaces = 4; Ckeditor.config.toolbarCanCollapse = true; Ckeditor.config.language = Messages._getLanguage(); if (screen.height < 800) { 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/support/ui.js b/www/support/ui.js index 90fd43fa5..f61ddac4b 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -345,7 +345,7 @@ define([ var senderKey = content.sender && content.sender.edPublic; var fromMe = senderKey === privateData.edPublic; var fromAdmin = ctx.adminKeys.indexOf(senderKey) !== -1; - var fromPremium = Boolean(content.sender.plan); + var fromPremium = Boolean(content.sender.plan || Util.find(content, ['sender', 'quota', 'plan'])); var userData = h('div.cp-support-showdata', [ Messages.support_showData, 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) {