diff --git a/customize.dist/loading.js b/customize.dist/loading.js index 600f3e8d4..09d2060dc 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -13,7 +13,8 @@ define([], function () { right: 0px; background: linear-gradient(to right, #326599 0%, #326599 50%, #4591c4 50%, #4591c4 100%); color: #fafafa; - font-size: 1.5em; + font-size: 1.3em; + line-height: 120%; opacity: 1; display: flex; flex-flow: column; @@ -77,13 +78,11 @@ define([], function () { background: #FFF; padding: 20px; width: 100%; - color: #000; - text-align: center; + color: #3F4141; + text-align: left; display: none; } -#cp-loading-password-prompt { - font-size: 18px; -} + #cp-loading-password-prompt .cp-password-error { color: white; background: #9e0000; @@ -94,24 +93,49 @@ define([], function () { text-align: left; margin-bottom: 15px; } +p.cp-password-info{ + text-align: left; +} #cp-loading-password-prompt .cp-password-form { display: flex; - justify-content: space-around; flex-wrap: wrap; } -#cp-loading-password-prompt .cp-password-form button, -#cp-loading-password-prompt .cp-password-form .cp-password-input { +#cp-loading-password-prompt .cp-password-form button{ background-color: #4591c4; color: white; border: 1px solid #4591c4; } + +.cp-password-input{ + font-size:16px; + border: 1px solid #4591c4; + background-color: white; + border-radius 0; +} + +.cp-password-form button{ + padding: 8px 12px; + font-weight: bold; + text-transform: uppercase; +} + +#cp-loading-password-prompt .cp-password-form{ + width: 100%; +} + #cp-loading-password-prompt .cp-password-form .cp-password-container { flex-shrink: 1; min-width: 0; } + +#cp-loading-password-prompt .cp-password-form .cp-password-container .cp-password-reveal{ + color: #4591c4; + padding: 0px 24px; +} + #cp-loading-password-prompt .cp-password-form input { flex: 1; - padding: 0 5px; + padding: 12px; min-width: 0; text-overflow: ellipsis; } @@ -119,7 +143,7 @@ define([], function () { background-color: #326599; } #cp-loading-password-prompt ::placeholder { - color: #d9d9d9; + color: #999999; opacity: 1; } #cp-loading-password-prompt :-ms-input-placeholder { @@ -154,7 +178,7 @@ define([], function () { background: #222; color: #fafafa; text-align: center; - font-size: 1.5em; + font-size: 1.3em; opacity: 0.7; font-family: 'Open Sans', 'Helvetica Neue', sans-serif; padding: 15px; @@ -201,6 +225,19 @@ define([], function () { animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85); } +button.primary{ + border: 1px solid #4591c4; + padding: 8px 12px; + text-transform: uppercase; + background-color: #4591c4; + color: white; + font-weight: bold; +} + +button.primary:hover{ + background-color: rgb(52, 118, 162); +} + */}).toString().slice(14, -3); var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; }); var elem = document.createElement('div'); diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 289cc4e69..cee774a98 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -14,6 +14,9 @@ .radio-group { display: flex; flex-direction: row; + &:not(:last-child){ + margin-bottom: 8px; + } .cp-radio { margin-right: 30px; } diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 0df5823a0..1d1ca0c47 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -97,6 +97,12 @@ .ckeditor_fix(); + .cp-burn-after-reading { + text-align: center; + font-size: @colortheme_app-font-size !important; + margin: 0 !important; + } + .cp-markdown-toolbar { height: @toolbar_line-height; background-color: @toolbar-bg-color-l20; diff --git a/www/assert/main.js b/www/assert/main.js index cdde441f0..c29b3bfa3 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -254,6 +254,40 @@ define([ !secret.hashData.present); }, "test support for trailing slashes in version 1 hash failed to parse"); + // test support for ownerKey + assert(function (cb) { + var secret = Hash.parsePadUrl('/pad/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/present/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/embed'); + return cb(secret.hashData.version === 1 && + secret.hashData.mode === "edit" && + secret.hashData.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && + secret.hashData.key === "usn4+9CqVja8Q7RZOGTfRgqI" && + secret.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" && + secret.hashData.embed && + secret.hashData.present); + }, "test support for owner key in version 1 hash failed to parse"); + assert(function (cb) { + var parsed = Hash.parsePadUrl('/pad/#/2/pad/edit/oRE0oLCtEXusRDyin7GyLGcS/p/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/embed'); + var secret = Hash.getSecrets('pad', parsed.hash); + return cb(parsed.hashData.version === 2 && + parsed.hashData.mode === "edit" && + parsed.hashData.type === "pad" && + parsed.hashData.key === "oRE0oLCtEXusRDyin7GyLGcS" && + secret.channel === "d8d51b4aea863f3f050f47f8ad261753" && + window.nacl.util.encodeBase64(secret.keys.cryptKey) === "0Ts1M6VVEozErV2Nx/LTv6Im5SCD7io2LlhasyyBPQo=" && + secret.keys.validateKey === "f5A1FM9Gp55tnOcM75RyHD1oxBG9ZPh9WDA7qe2Fvps=" && + parsed.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" && + parsed.hashData.embed && + parsed.hashData.password); + }, "test support for owner key in version 2 hash failed to parse"); + assert(function (cb) { + var secret = Hash.parsePadUrl('/file/#/1/TRplGM-WsVkXR+LkJ0tD3D45A1YFZ-Cy/eO4RJwh8yHEEDhl1aHfuwQ2IzosPBZx-HDaWc1lW+hY=/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/'); + return cb(secret.hashData.version === 1 && + secret.hashData.channel === "TRplGM/WsVkXR+LkJ0tD3D45A1YFZ/Cy" && + secret.hashData.key === "eO4RJwh8yHEEDhl1aHfuwQ2IzosPBZx/HDaWc1lW+hY=" && + secret.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" && + !secret.hashData.present); + }, "test support for owner key in version 1 file hash failed to parse"); + assert(function (cb) { var secret = Hash.parsePadUrl('/invite/#/2/invite/edit/oRE0oLCtEXusRDyin7GyLGcS/p/'); var hd = secret.hashData; diff --git a/www/code/inner.js b/www/code/inner.js index e2ed086de..e933bd4bd 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -98,6 +98,7 @@ define([ }; var mkHelpMenu = function (framework) { var $codeMirrorContainer = $('#cp-app-code-container'); + $codeMirrorContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning()); var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'code']); $codeMirrorContainer.prepend(helpMenu.menu); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 4ed1193e3..902a91793 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -15,6 +15,20 @@ var factory = function (Util, Crypto, Nacl) { .decodeUTF8(JSON.stringify(list)))); }; + // XXX move this code? + Hash.generateSignPair = function () { + var ed = Nacl.sign.keyPair(); + var makeSafe = function (key) { + return Crypto.b64RemoveSlashes(key).replace(/=+$/g, ''); + }; + return { + validateKey: Hash.encodeBase64(ed.publicKey), + signKey: Hash.encodeBase64(ed.secretKey), + safeValidateKey: makeSafe(Hash.encodeBase64(ed.publicKey)), + safeSignKey: makeSafe(Hash.encodeBase64(ed.secretKey)), + }; + }; + var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) { var version = secret.version; var data = secret.keys; @@ -134,6 +148,17 @@ Version 1 /code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI */ + var getOwnerKey = function (hashArr) { + var k; + // Check if we have a ownerKey for this pad + hashArr.some(function (data) { + if (data.length === 86) { // XXX 88 characters - 2 trailing "="... + k = data; + return true; + } + }); + return k; + }; var parseTypeHash = Hash.parseTypeHash = function (type, hash) { if (!hash) { return; } var options; @@ -158,9 +183,12 @@ Version 1 options = hashArr.slice(5); parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; + parsed.ownerKey = getOwnerKey(options); parsed.getHash = function (opts) { var hash = hashArr.slice(0, 5).join('/') + '/'; + var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey; + if (owner) { hash += owner + '/'; } if (opts.embed) { hash += 'embed/'; } if (opts.present) { hash += 'present/'; } return hash; @@ -177,9 +205,12 @@ Version 1 parsed.password = options.indexOf('p') !== -1; parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; + parsed.ownerKey = getOwnerKey(options); parsed.getHash = function (opts) { var hash = hashArr.slice(0, 5).join('/') + '/'; + var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey; + if (owner) { hash += owner + '/'; } if (parsed.password) { hash += 'p/'; } if (opts.embed) { hash += 'embed/'; } if (opts.present) { hash += 'present/'; } @@ -196,6 +227,8 @@ Version 1 parsed.version = 1; parsed.channel = hashArr[2].replace(/-/g, '/'); parsed.key = hashArr[3].replace(/-/g, '/'); + options = hashArr.slice(4); + parsed.ownerKey = getOwnerKey(options); return parsed; } if (hashArr[1] && hashArr[1] === '2') { // Version 2 @@ -207,9 +240,12 @@ Version 1 parsed.password = options.indexOf('p') !== -1; parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; + parsed.ownerKey = getOwnerKey(options); parsed.getHash = function (opts) { var hash = hashArr.slice(0, 4).join('/') + '/'; + var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey; + if (owner) { hash += owner + '/'; } if (parsed.password) { hash += 'p/'; } if (opts.embed) { hash += 'embed/'; } if (opts.present) { hash += 'present/'; } diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 4528b7a1b..55d7e6939 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -209,10 +209,16 @@ define([ $(title).prepend(' ').prepend(icon); } $(title).click(function () { + var old = tabs[active]; + if (old.onHide) { old.onHide(); } titles.forEach(function (t) { $(t).removeClass('alertify-tabs-active'); }); contents.forEach(function (c) { $(c).removeClass('alertify-tabs-content-active'); }); + if (tab.onShow) { + tab.onShow(); + } $(title).addClass('alertify-tabs-active'); $(content).addClass('alertify-tabs-content-active'); + active = i; }); titles.push(title); contents.push(content); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6323be780..a195a1ba9 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -917,60 +917,79 @@ define([ className: 'primary cp-share-with-friends', name: Messages.share_withFriends, onClick: function () { - var href = Hash.getRelativeHref(linkGetter()); - var $friends = $div.find('.cp-usergrid-user.cp-selected'); - $friends.each(function (i, el) { - var curve = $(el).attr('data-curve'); - // Check if the selected element is a friend or a team - if (curve) { // Friend - if (!curve || !friends[curve]) { return; } - var friend = friends[curve]; - if (!friend.notifications || !friend.curvePublic) { return; } - common.mailbox.sendTo("SHARE_PAD", { + var href; + NThen(function (waitFor) { + var w = waitFor(); + // linkGetter can be async if this is a burn after reading URL + var res = linkGetter({}, function (url) { + if (!url) { + waitFor.abort(); + return; + } + console.warn('BAR'); + href = url; + setTimeout(w); + }); + if (res && /^http/.test(res)) { + href = Hash.getRelativeHref(res); + setTimeout(w); + return; + } + }).nThen(function () { + var $friends = $div.find('.cp-usergrid-user.cp-selected'); + $friends.each(function (i, el) { + var curve = $(el).attr('data-curve'); + // Check if the selected element is a friend or a team + if (curve) { // Friend + if (!curve || !friends[curve]) { return; } + var friend = friends[curve]; + if (!friend.notifications || !friend.curvePublic) { return; } + common.mailbox.sendTo("SHARE_PAD", { + href: href, + password: config.password, + isTemplate: config.isTemplate, + name: myName, + title: title + }, { + channel: friend.notifications, + curvePublic: friend.curvePublic + }); + return; + } + // Team + var ed = $(el).attr('data-ed'); + var team = teams[ed]; + if (!team) { return; } + sframeChan.query('Q_STORE_IN_TEAM', { href: href, password: config.password, - isTemplate: config.isTemplate, - name: myName, - title: title - }, { - channel: friend.notifications, - curvePublic: friend.curvePublic + path: config.isTemplate ? ['template'] : undefined, + title: title, + teamId: team.id + }, function (err) { + if (err) { return void console.error(err); } }); - return; - } - // Team - var ed = $(el).attr('data-ed'); - var team = teams[ed]; - if (!team) { return; } - sframeChan.query('Q_STORE_IN_TEAM', { - href: href, - password: config.password, - path: config.isTemplate ? ['template'] : undefined, - title: title, - teamId: team.id - }, function (err) { - if (err) { return void console.error(err); } }); - }); - UI.findCancelButton().click(); - - // Update the "recently shared with" array: - // Get the selected curves - var curves = $friends.toArray().map(function (el) { - return ($(el).attr('data-curve') || '').slice(0,8); - }).filter(function (x) { return x; }); - // Prepend them to the "order" array - Array.prototype.unshift.apply(order, curves); - order = Util.deduplicateString(order); - // Make sure we don't have "old" friends and save - order = order.filter(function (curve) { - return smallCurves.indexOf(curve) !== -1; + UI.findCancelButton().click(); + + // Update the "recently shared with" array: + // Get the selected curves + var curves = $friends.toArray().map(function (el) { + return ($(el).attr('data-curve') || '').slice(0,8); + }).filter(function (x) { return x; }); + // Prepend them to the "order" array + Array.prototype.unshift.apply(order, curves); + order = Util.deduplicateString(order); + // Make sure we don't have "old" friends and save + order = order.filter(function (curve) { + return smallCurves.indexOf(curve) !== -1; + }); + common.setAttribute(['general', 'share-friends'], order); + if (onShare) { + onShare.fire(); + } }); - common.setAttribute(['general', 'share-friends'], order); - if (onShare) { - onShare.fire(); - } }, keys: [13] }; @@ -1049,6 +1068,29 @@ define([ } }; + var makeBurnAfterReadingUrl = function (common, href, channel, cb) { + var keyPair = Hash.generateSignPair(); + var parsed = Hash.parsePadUrl(href); + console.error(href, parsed); + var newHref = parsed.getUrl({ + ownerKey: keyPair.safeSignKey + }); + var sframeChan = common.getSframeChannel(); + NThen(function (waitFor) { + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_OWNERS', + value: [keyPair.validateKey] + }, waitFor(function (err) { + if (err) { + waitFor.abort(); + UI.warn(Messages.error); + } + })); + }).nThen(function () { + cb(newHref); + }); + }; UIElements.createShareModal = function (config) { var origin = config.origin; var pathname = config.pathname; @@ -1078,6 +1120,7 @@ define([ var parsed = Hash.parsePadUrl(pathname); var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1; + var burnAfterReading; var rights = h('div.msg.cp-inline-radio-group', [ h('label', Messages.share_linkAccess), h('div.radio-group',[ @@ -1086,9 +1129,33 @@ define([ canPresent ? UI.createRadio('accessRights', 'cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined, UI.createRadio('accessRights', 'cp-share-editable-true', - Messages.share_linkEdit, false, { mark: {tabindex:1} })]) + Messages.share_linkEdit, false, { mark: {tabindex:1} })]), + burnAfterReading = hashes.viewHash ? UI.createRadio('accessRights', 'cp-share-bar', Messages.burnAfterReading_linkBurnAfterReading || + 'View once and self-destruct', false, { mark: {tabindex:1}, label: {style: "display: none;"} }) : undefined // XXX temp KEY ]); + // Burn after reading + // Check if we are an owner of this pad. If we are, we can show the burn after reading option. + // When BAR is selected, display a red message indicating the consequence and add + // the options to generate the BAR url + var barAlert = h('div.alert.alert-danger.cp-alertify-bar-selected', { + style: 'display: none;' + }, Messages.burnAfterReading_warningLink || " You have set this pad to self-destruct. Once a recipient opens this pad, it will be permanently deleted from the server."); // XXX temp KEY + var channel = Hash.getSecrets('pad', hash, config.password).channel; + common.getPadMetadata({ + channel: channel + }, function (obj) { + if (!obj || obj.error) { return; } + var priv = common.getMetadataMgr().getPrivateData(); + // Not an owner: don't display the burn after reading option + if (!Array.isArray(obj.owners) || obj.owners.indexOf(priv.edPublic) === -1) { + $(burnAfterReading).remove(); + return; + } + // When the burn after reading option is selected, transform the modal buttons + $(burnAfterReading).show(); + }); + var $rights = $(rights); var saveValue = function () { @@ -1100,13 +1167,25 @@ define([ }); }; - var getLinkValue = function (initValue) { + var burnAfterReadingUrl; + + var getLinkValue = function (initValue, cb) { var val = initValue || {}; var edit = val.edit !== undefined ? val.edit : Util.isChecked($rights.find('#cp-share-editable-true')); var embed = val.embed; var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present')); + var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar')); + if (burnAfterReading && !burnAfterReadingUrl) { + if (cb) { // Called from the contacts tab, "share" button + var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash); + return makeBurnAfterReadingUrl(common, barHref, channel, function (url) { + cb(url); + }); + } + return Messages.burnAfterReading_generateLink || 'Click on the button below to generate a link'; // XXX temp KEY + } var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; - var href = origin + pathname + '#' + hash; + var href = burnAfterReading ? burnAfterReadingUrl : (origin + pathname + '#' + hash); var parsed = Hash.parsePadUrl(href); return origin + parsed.getUrl({embed: embed, present: present}); }; @@ -1160,8 +1239,8 @@ define([ }); }); - - + + linkContent.push($(barAlert).clone()[0]); // Burn after reading var link = h('div.cp-share-modal', linkContent); var $link = $(link); @@ -1169,7 +1248,7 @@ define([ var linkButtons = [ makeCancelButton(), !config.sharedFolder && { - className: 'secondary', + className: 'secondary cp-nobar', name: Messages.share_linkOpen, onClick: function () { saveValue(); @@ -1180,9 +1259,8 @@ define([ return true; }, keys: [[13, 'ctrl']] - }, - { - className: 'primary', + }, { + className: 'primary cp-nobar', name: Messages.share_linkCopy, onClick: function () { saveValue(); @@ -1193,26 +1271,26 @@ define([ if (success) { UI.log(Messages.shareSuccess); } }, keys: [13] + }, { + className: 'primary cp-bar', + name: 'GENERATE LINK', + onClick: function () { + var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash); + makeBurnAfterReadingUrl(common, barHref, channel, function (url) { + burnAfterReadingUrl = url; + $rights.find('input[type="radio"]').trigger('change'); + }); + return true; + }, + keys: [] } ]; - // update values for link preview when radio btns change - $link.find('#cp-share-link-preview').val(getLinkValue()); - $rights.find('input[type="radio"]').on('change', function () { - $link.find('#cp-share-link-preview').val(getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - })); - }); - $link.find('input[type="checkbox"]').on('change', function () { - $link.find('#cp-share-link-preview').val(getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - })); - }); - var frameLink = UI.dialog.customModal(link, { buttons: linkButtons, onClose: config.onClose, }); + $(frameLink).find('.cp-bar').hide(); // Share with contacts tab @@ -1240,10 +1318,17 @@ define([ ])); } + $(contactsContent).append($(barAlert).clone()); // Burn after reading var contactButtons = friendsObject.buttons; contactButtons.unshift(makeCancelButton()); - + + var onShowContacts = function () { + if (!hasFriends) { + $rights.hide(); + } + }; + var frameContacts = UI.dialog.customModal(contactsContent, { buttons: contactButtons, onClose: config.onClose, @@ -1282,26 +1367,60 @@ define([ keys: [13] }]; + var onShowEmbed = function () { + $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 embed = h('div.cp-share-modal', embedContent); var $embed = $(embed); - // update values for link preview when radio btns change + var frameEmbed = UI.dialog.customModal(embed, { + buttons: embedButtons, + onClose: config.onClose, + }); + + // update values for link and embed preview when radio btns change $embed.find('#cp-embed-link-preview').val(getEmbedValue()); + $link.find('#cp-share-link-preview').val(getLinkValue()); $rights.find('input[type="radio"]').on('change', function () { + $link.find('#cp-share-link-preview').val(getLinkValue({ + embed: Util.isChecked($link.find('#cp-share-embed')) + })); + // Hide or show the burn after reading alert + if (Util.isChecked($rights.find('#cp-share-bar')) && !burnAfterReadingUrl) { + $('.cp-alertify-bar-selected').show(); + // Show burn after reading button + $('.alertify').find('.cp-bar').show(); + $('.alertify').find('.cp-nobar').hide(); + return; + } $embed.find('#cp-embed-link-preview').val(getEmbedValue()); + // Hide burn after reading button + $('.alertify').find('.cp-nobar').show(); + $('.alertify').find('.cp-bar').hide(); + $('.cp-alertify-bar-selected').hide(); }); - - var frameEmbed = UI.dialog.customModal(embed, { - buttons: embedButtons, - onClose: config.onClose, + $link.find('input[type="checkbox"]').on('change', function () { + $link.find('#cp-share-link-preview').val(getLinkValue({ + embed: Util.isChecked($link.find('#cp-share-embed')) + })); }); + // Create modal + var resetTab = function () { + $rights.show(); + $rights.find('label.cp-radio').show(); + }; var tabs = [{ title: Messages.share_contactCategory, icon: "fa fa-address-book", content: frameContacts, - active: hasFriends + active: hasFriends, + onShow: onShowContacts, + onHide: resetTab }, { title: Messages.share_linkCategory, icon: "fa fa-link", @@ -1310,7 +1429,9 @@ define([ }, { title: Messages.share_embedCategory, icon: "fa fa-code", - content: frameEmbed + content: frameEmbed, + onShow: onShowEmbed, + onHide: resetTab }]; if (typeof(AppConfig.customizeShareOptions) === 'function') { AppConfig.customizeShareOptions(hashes, tabs, { @@ -3866,6 +3987,7 @@ define([ UIElements.onServerError = function (common, err, toolbar, cb) { if (["EDELETED", "EEXPIRED"].indexOf(err.type) === -1) { return; } + var priv = common.getMetadataMgr().getPrivateData(); var msg = err.type; if (err.type === 'EEXPIRED') { msg = Messages.expiredError; @@ -3873,11 +3995,14 @@ define([ msg += Messages.errorCopy; } } else if (err.type === 'EDELETED') { + if (priv.burnAfterReading) { return void cb(); } msg = Messages.deletedError; if (err.loaded) { msg += Messages.errorCopy; } } + var sframeChan = common.getSframeChannel(); + sframeChan.event('EV_SHARE_OPEN', {hidden: true}); if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } UI.errorLoadingScreen(msg, true, true); (cb || function () {})(); @@ -3922,6 +4047,26 @@ define([ $password.find('.cp-password-input').focus(); }; + UIElements.displayBurnAfterReadingPage = function (common, cb) { + var info = h('p.cp-password-info', Messages.burnAfterReading_warning || 'This document will self-destruct as soon as you open it. It will be removed form the server, once you close this window you will not be able to access it again. If you are not ready to proceed you can close this window and come back later. '); // XXX temp KEY + var button = h('button.primary', Messages.burnAfterReading_proceed || 'view and delete'); // XXX temp KEY + + $(button).on('click', function () { + cb(); + }); + + var block = h('div#cp-loading-burn-after-reading', [ + info, + button + ]); + UI.errorLoadingScreen(block); + }; + UIElements.getBurnAfterReadingWarning = function (common) { + var priv = common.getMetadataMgr().getPrivateData(); + if (!priv.burnAfterReading) { return; } + return h('div.alert.alert-danger.cp-burn-after-reading', Messages.burnAfterReading_warningDeleted || 'This pad has been deleted from the server, once you close this window you will not be able to access it again.'); // XXX temp KEY + }; + var crowdfundingState = false; UIElements.displayCrowdfunding = function (common) { if (crowdfundingState) { return; } @@ -3979,6 +4124,9 @@ define([ if (data && data.stored) { return; } // We won't display the popup for dropped files var priv = common.getMetadataMgr().getPrivateData(); + // This pad will be deleted automatically, it shouldn't be stored + if (priv.burnAfterReading) { return; } + var typeMsg = priv.pathname.indexOf('/file/') !== -1 ? Messages.autostore_file : priv.pathname.indexOf('/drive/') !== -1 ? Messages.autostore_sf : Messages.autostore_pad; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 57815b3ef..a0057c59a 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -847,6 +847,10 @@ define([ postMessage('GET_PAD_METADATA', data, cb); }; + common.burnPad = function (data) { + postMessage('BURN_PAD', data); + }; + common.changePadPassword = function (Crypt, Crypto, data, cb) { var href = data.href; var newPassword = data.password; diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 0958b024d..a40ccccae 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -926,6 +926,7 @@ define([ $rightside.append($forget); var helpMenu = common.createHelpMenu(['beta', 'oo']); + $('#cp-app-oo-editor').prepend(common.getBurnAfterReadingWarning()); $('#cp-app-oo-editor').prepend(helpMenu.menu); toolbar.$drawer.append(helpMenu.button); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 870a539e7..681f1d575 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -9,6 +9,7 @@ define([ '/common/common-feedback.js', '/common/common-realtime.js', '/common/common-messaging.js', + '/common/pinpad.js', '/common/outer/sharedfolder.js', '/common/outer/cursor.js', '/common/outer/onlyoffice.js', @@ -26,7 +27,7 @@ define([ '/bower_components/nthen/index.js', '/bower_components/saferphore/index.js', ], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, - Realtime, Messaging, + Realtime, Messaging, Pinpad, SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, NetConfig, AppConfig, Crypto, ChainPad, CpNetflux, Listmap, nThen, Saferphore) { @@ -409,19 +410,17 @@ define([ var initRpc = function (clientId, data, cb) { if (!store.loggedIn) { return cb(); } if (store.rpc) { return void cb(account); } - require(['/common/pinpad.js'], function (Pinpad) { - Pinpad.create(store.network, store.proxy, function (e, call) { - if (e) { return void cb({error: e}); } + Pinpad.create(store.network, store.proxy, function (e, call) { + if (e) { return void cb({error: e}); } - store.rpc = call; + store.rpc = call; - Store.getPinLimit(null, null, function (obj) { - if (obj.error) { console.error(obj.error); } - account.limit = obj.limit; - account.plan = obj.plan; - account.note = obj.note; - cb(obj); - }); + Store.getPinLimit(null, null, function (obj) { + if (obj.error) { console.error(obj.error); } + account.limit = obj.limit; + account.plan = obj.plan; + account.note = obj.note; + cb(obj); }); }); }; @@ -1653,6 +1652,73 @@ define([ cb(); }; + // Delete a pad received with a burn after reading URL + + var notifyOwnerPadRemoved = function (data, obj) { + var channel = data.channel; + var href = data.href; + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password); + if (obj && obj.error) { return; } + if (!obj.mailbox) { return; } + + // Decrypt the mailbox + var crypto = Crypto.createEncryptor(secret.keys); + var m = []; + try { + if (typeof (obj.mailbox) === "string") { + m.push(crypto.decrypt(obj.mailbox, true, true)); + } else { + Object.keys(obj.mailbox).forEach(function (k) { + m.push(crypto.decrypt(obj.mailbox[k], true, true)); + }); + } + } catch (e) { + console.error(e); + } + // Tell all the owners that the pad was deleted from the server + var curvePublic = store.proxy.curvePublic; + var myData = Messaging.createData(store.proxy, false); + m.forEach(function (obj) { + var mb = JSON.parse(obj); + if (mb.curvePublic === curvePublic) { return; } + store.mailbox.sendTo('OWNED_PAD_REMOVED', { + channel: channel, + user: myData + }, { + channel: mb.notifications, + curvePublic: mb.curvePublic + }, function () {}); + }); + }; + + Store.burnPad = function (clientId, data) { + var channel = data.channel; + var ownerKey = Crypto.b64AddSlashes(data.ownerKey || ''); + if (!channel || !ownerKey) { return void console.error("Can't delete BAR pad"); } + try { + var signKey = Hash.decodeBase64(ownerKey); + var pair = Crypto.Nacl.sign.keyPair.fromSecretKey(signKey); + Pinpad.create(store.network, { + edPublic: Hash.encodeBase64(pair.publicKey), + edPrivate: Hash.encodeBase64(pair.secretKey) + }, function (e, rpc) { + if (e) { return void console.error(e); } + Store.getPadMetadata(null, { + channel: channel + }, function (md) { + rpc.removeOwnedChannel(channel, function (err) { + if (err) { return void console.error(err); } + // Notify owners that the pad was removed + notifyOwnerPadRemoved(data, md); + }); + }); + }); + } catch (e) { + console.error(e); + } + }; + // Fetch the latest version of the metadata on the server and return it. // If the pad is stored in our drive, update the local values of "owners" and "expire" Store.getPadMetadata = function (clientId, data, cb) { @@ -2109,6 +2175,11 @@ define([ updateMetadata: function () { broadcast([], "UPDATE_METADATA"); }, + updateDrive: function () { + sendDriveEvent('DRIVE_CHANGE', { + path: ['drive', 'filesData'] + }); + }, pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, }, waitFor, function (ev, data, clients, _cb) { var cb = Util.once(_cb || function () {}); diff --git a/www/common/outer/invitation.js b/www/common/outer/invitation.js index 603cae3c1..d84d7a81f 100644 --- a/www/common/outer/invitation.js +++ b/www/common/outer/invitation.js @@ -17,6 +17,7 @@ var factory = function (Util, Cred, Nacl) { }; }; + // XXX move this function? Invite.generateSignPair = function () { var ed = Nacl.sign.keyPair(); return { diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 6e10cd5a2..253157361 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -482,6 +482,31 @@ define([ cb(true); }; + handlers['OWNED_PAD_REMOVED'] = function (ctx, box, data, cb) { + var msg = data.msg; + var content = msg.content; + + if (msg.author !== content.user.curvePublic) { return void cb(true); } + if (!content.channel) { + console.log('Remove invalid notification'); + return void cb(true); + } + + var channel = content.channel; + var res = ctx.store.manager.findChannel(channel); + + res.forEach(function (obj) { + var paths = ctx.store.manager.findFile(obj.id); + ctx.store.manager.delete({ + paths: paths + }, function () { + ctx.updateDrive(); + }); + }); + + cb(true); + }; + return { diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index 1def668e1..b84e98dd7 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -422,6 +422,7 @@ proxy.mailboxes = { store: store, pinPads: cfg.pinPads, updateMetadata: cfg.updateMetadata, + updateDrive: cfg.updateDrive, emit: emit, clients: [], boxes: {}, diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 49250582f..41963402b 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -80,6 +80,7 @@ define([ IS_NEW_CHANNEL: Store.isNewChannel, REQUEST_PAD_ACCESS: Store.requestPadAccess, GIVE_PAD_ACCESS: Store.givePadAccess, + BURN_PAD: Store.burnPad, GET_PAD_METADATA: Store.getPadMetadata, SET_PAD_METADATA: Store.setPadMetadata, CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin, diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 1bd88e90b..796c52898 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -119,6 +119,7 @@ define([ // If it's not a shared folder, check the pads if (!data) { data = Env.user.userObject.getFileData(id, editable); } ret.push({ + id: id, data: data, userObject: Env.user.userObject }); @@ -126,6 +127,7 @@ define([ Object.keys(Env.folders).forEach(function (fId) { Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) { ret.push({ + id: id, fId: fId, data: Env.folders[fId].userObject.getFileData(id, editable), userObject: Env.folders[fId].userObject @@ -1095,9 +1097,11 @@ define([ // Store getChannelsList: callWithEnv(getChannelsList), addPad: callWithEnv(addPad), + delete: callWithEnv(_delete), // Tools findChannel: callWithEnv(findChannel), findHref: callWithEnv(findHref), + findFile: callWithEnv(findFile), getEditHash: callWithEnv(getEditHash), user: Env.user, folders: Env.folders diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 05cafbdec..f53a6b496 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -81,8 +81,8 @@ define([ }); localStorage.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs; } - var cache = {}; - var localStore = {}; + var cache = window.cpCache = {}; + var localStore = window.localStore = {}; Object.keys(localStorage).forEach(function (k) { if (k.indexOf('CRYPTPAD_CACHE|') === 0) { cache[k.slice(('CRYPTPAD_CACHE|').length)] = localStorage[k]; @@ -323,6 +323,7 @@ define([ } Utils.crypto = Utils.Crypto.createEncryptor(Utils.secret.keys); var parsed = Utils.Hash.parsePadUrl(window.location.href); + var burnAfterReading = parsed && parsed.hashData && parsed.hashData.ownerKey; if (!parsed.type) { throw new Error(); } var defaultTitle = Utils.UserObject.getDefaultName(parsed); var edPublic, curvePublic, notifications, isTemplate; @@ -376,6 +377,7 @@ define([ fromFileData: Cryptpad.fromFileData ? { title: Cryptpad.fromFileData.title } : undefined, + burnAfterReading: burnAfterReading, storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined) }; if (window.CryptPad_newSharedFolder) { @@ -507,6 +509,17 @@ define([ } }); + sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) { + if (!data || !data.channel) { + data = { + channel: secret.channel + }; + } + Cryptpad.getPadMetadata(data, cb); + }); + sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) { + Cryptpad.setPadMetadata(data, cb); + }); }; addCommonRpc(sframeChan); @@ -1170,18 +1183,6 @@ define([ }); }); - sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) { - if (!data || !data.channel) { - data = { - channel: secret.channel - }; - } - Cryptpad.getPadMetadata(data, cb); - }); - sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) { - Cryptpad.setPadMetadata(data, cb); - }); - if (cfg.messaging) { Notifier.getPermission(); @@ -1236,6 +1237,16 @@ define([ window.location.hash = hash; }; + if (burnAfterReading) { + Cryptpad.padRpc.onReadyEvent.reg(function () { + Cryptpad.burnPad({ + password: password, + href: window.location.href, + channel: secret.channel, + ownerKey: burnAfterReading + }); + }); + } var cpNfCfg = { sframeChan: sframeChan, channel: secret.channel, @@ -1359,12 +1370,17 @@ define([ }); }); + sframeChan.on('EV_BURN_AFTER_READING', function () { + startRealtime(); + }); + sframeChan.ready(); Utils.Feedback.reportAppUsage(); if (!realtime && !Test.testing) { return; } if (isNewFile && cfg.useCreationScreen && !Test.testing) { return; } + if (burnAfterReading) { return; } //if (isNewFile && Utils.LocalStore.isLoggedIn() // && AppConfig.displayCreationScreen && cfg.useCreationScreen) { return; } diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index f8bbb205b..9e51d46de 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -96,6 +96,7 @@ define([ funcs.createMarkdownToolbar = callWithCommon(UIElements.createMarkdownToolbar); funcs.createHelpMenu = callWithCommon(UIElements.createHelpMenu); funcs.getPadCreationScreen = callWithCommon(UIElements.getPadCreationScreen); + funcs.getBurnAfterReadingWarning = callWithCommon(UIElements.getBurnAfterReadingWarning); funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal); funcs.onServerError = callWithCommon(UIElements.onServerError); funcs.importMediaTagMenu = callWithCommon(UIElements.importMediaTagMenu); @@ -300,6 +301,13 @@ define([ } // If we display the pad creation screen, it will handle deleted pads directly funcs.getPadCreationScreen(c, config, waitFor()); + return; + } + if (priv.burnAfterReading) { + UIElements.displayBurnAfterReadingPage(funcs, waitFor(function () { + UI.addLoadingScreen(); + ctx.sframeChan.event('EV_BURN_AFTER_READING'); + })); } }; funcs.createPad = function (cfg, cb) { diff --git a/www/filepicker/main.js b/www/filepicker/main.js index d6017c2dd..cada394ba 100644 --- a/www/filepicker/main.js +++ b/www/filepicker/main.js @@ -58,7 +58,7 @@ define([ // Remove the listener once we've received the READY message window.removeEventListener('message', whenReady); // Answer with the requested data - postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage() })); + postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage(), localStore: window.localStore, cache: window.cpCache })); // Then start the channel window.addEventListener('message', function (msg) { diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 5a9e10f47..621d56cae 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -350,6 +350,8 @@ define([ var mkHelpMenu = function (framework) { var $toolbarContainer = $('#cp-app-kanban-container'); + $toolbarContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning()); + var helpMenu = framework._.sfCommon.createHelpMenu(['kanban']); $toolbarContainer.prepend(helpMenu.menu); diff --git a/www/pad/inner.js b/www/pad/inner.js index 5db93837c..78f027182 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -190,6 +190,7 @@ define([ var mkHelpMenu = function (framework) { var $toolbarContainer = $('.cke_toolbox_main'); + $toolbarContainer.before(framework._.sfCommon.getBurnAfterReadingWarning()); var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']); $toolbarContainer.before(helpMenu.menu); diff --git a/www/poll/inner.js b/www/poll/inner.js index ba8fd7d65..7420f5c09 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -1187,6 +1187,7 @@ define([ $drawer.append($export); var helpMenu = common.createHelpMenu(['poll']); + $('#cp-app-poll-form').prepend(common.getBurnAfterReadingWarning()); $('#cp-app-poll-form').prepend(helpMenu.menu); $drawer.append(helpMenu.button); diff --git a/www/share/main.js b/www/share/main.js index 43e514f58..28cc1a882 100644 --- a/www/share/main.js +++ b/www/share/main.js @@ -60,7 +60,7 @@ define([ // Remove the listener once we've received the READY message window.removeEventListener('message', whenReady); // Answer with the requested data - postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage() })); + postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage(), localStore: window.localStore, cache: window.cpCache })); // Then start the channel window.addEventListener('message', function (msg) { @@ -105,6 +105,21 @@ define([ config.addCommonRpc(sframeChan); + sframeChan.on('EV_CACHE_PUT', function (x) { + Object.keys(x).forEach(function (k) { + localStorage['CRYPTPAD_CACHE|' + k] = x[k]; + }); + }); + sframeChan.on('EV_LOCALSTORE_PUT', function (x) { + Object.keys(x).forEach(function (k) { + if (typeof(x[k]) === "undefined") { + delete localStorage['CRYPTPAD_STORE|' + k]; + return; + } + localStorage['CRYPTPAD_STORE|' + k] = x[k]; + }); + }); + sframeChan.on('Q_GET_FILES_LIST', function (types, cb) { Cryptpad.getSecureFilesList(types, function (err, data) { cb({ diff --git a/www/slide/inner.js b/www/slide/inner.js index 6d97bf8e5..5736828d5 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -410,6 +410,7 @@ define([ var mkHelpMenu = function (framework) { var $codeMirrorContainer = $('#cp-app-slide-editor-container'); + $codeMirrorContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning()); var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'slide']); $codeMirrorContainer.prepend(helpMenu.menu); diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index a90dd4187..77057f505 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -270,6 +270,7 @@ define([ var mkHelpMenu = function (framework) { var $appContainer = $('#cp-app-whiteboard-container'); + $appContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning()); var helpMenu = framework._.sfCommon.createHelpMenu(['whiteboard']); $appContainer.prepend(helpMenu.menu); framework._.toolbar.$drawer.append(helpMenu.button);