diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index ddd2f2595..30ca9adae 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -47,6 +47,9 @@ display: flex; max-width: 100%; align-items: center; + & > input { + margin-bottom: 0; + } &:not(:last-child) { margin-bottom: 10px; } diff --git a/lib/metadata.js b/lib/metadata.js index 6221c7af0..23bb73477 100644 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -457,7 +457,7 @@ revocationCommands.UPDATE_ACCESS = function (meta, args, myKey) { } else if (!oldValue) { // Create newValue.from = myKey; access[user] = newValue; - return; + return true; } else { // Update oldValue.rights = newValue.rights; oldValue.from = myKey; // XXX to decide: when updating an access, put it under your tree diff --git a/www/common/common-hash.js b/www/common/common-hash.js index d27c1d628..8d4fd0c21 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -726,7 +726,8 @@ Version 5: Revocable mailbox Hash.hrefToHexChannelId = function (href, password) { var parsed = Hash.parsePadUrl(href); if (!parsed || !parsed.hash) { return; } - var secret = Hash.getSecrets(parsed.type, parsed.hash, password); + var secret = parsed.revocable ? Hash.getRevocable(parsed.type, parsed.hashData.key) + : Hash.getSecrets(parsed.type, parsed.hash, password); return secret.channel; }; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 9a26505d6..ed3055a98 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -222,6 +222,10 @@ define([ } onSelect(); }); + } else if (config.picker) { + $div.on('click', '.cp-usergrid-user', function () { + onSelect($(this)); + }); } return { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 51af0afec..45a7631d6 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -368,6 +368,11 @@ define([ (function () { var bypassHashChange = function (key) { return function (value) { + if (currentPad.type && currentPad.type !== 'link' && /^\/5\//.test(value)) { + // XXX REVOCATION decide if we store user access before opening it + console.error('Preserve safe link for revocable access', key); + return; + } var ohc = window.onhashchange; window.onhashchange = function () {}; window.location[key] = value; diff --git a/www/common/inner/share.js b/www/common/inner/share.js index ba72f0a6b..b0566f76d 100644 --- a/www/common/inner/share.js +++ b/www/common/inner/share.js @@ -321,6 +321,8 @@ define([ var hasFriends = opts.hasFriends; var onFriendShare = Util.mkEvent(); + var myName = common.getMetadataMgr().getUserData().name; + var title = opts.title; var metadataMgr = common.getMetadataMgr(); var priv = metadataMgr.getPrivateData(); @@ -344,15 +346,21 @@ define([ var viewAs = h('div.cp-share-access-as'); var list = h('div.cp-share-access-list'); - var content = h('div.cp-share-access-list-container', [viewAs, list]); - var $content = $(list); + var buttonContainer = h('div'); + var content = h('div.cp-share-access-list-container', [viewAs, list, buttonContainer]); + var $list = $(list); var $viewAs = $(viewAs); + var usergrid = h('div.cp-share-access-usergrid'); + var $usergrid = $(usergrid); + var $content = $(content); + var channel = rev.channel; var revocation = common.makeUniversal('revocation'); var updateAccess = function () {}; var addAccess = function () {}; + var redraw = function () {}; var TYPES = { user: { icon: '.fa.fa-user', order: 1 }, @@ -363,16 +371,18 @@ define([ var makeDD = function (current, editable, maxR) { var value; - if (current.includes('m')) { value = 'moderate'; } - else if (current.includes('w')) { value = 'write'; } + current = current.replace(/d+$/, ''); + + if (current.includes('rwm')) { value = 'moderate'; } + else if (current.includes('rw')) { value = 'write'; } else if (current.includes('r')) { value = 'read'; } if (!editable) { return [h('button.btn.btn-secondary', {disabled:'disabled'}, value)]; // XXX } - var options = ['r', 'w', 'm'].map(function (r, i) { + var options = ['r', 'rw', 'rwm'].map(function (r, i) { if (maxR === 'r' && i) { return; } - if (maxR === 'w' && i > 1) { return; } + if (maxR === 'rw' && i > 1) { return; } return { tag: 'a', attributes: { @@ -397,9 +407,9 @@ define([ var getRights = function (dd, $d) { var r = dd.getValue() || 'r'; var d = Util.isChecked($d); - var rights = r === 'm' ? 'rwm' : (r === 'w' ? 'rw' : 'r'); - if (d) { rights += 'd'; } - return rights; + //var rights = r === 'm' ? 'rwm' : (r === 'w' ? 'rw' : 'r'); + if (d) { r += 'd'; } + return r; }; var renderAccess = function (edPublic, accessData, editable, maxR, renderedAs) { var type = accessData.notes.type @@ -451,9 +461,11 @@ define([ ]); }; - var addAccessButton = function (maxR, renderedAs) { + var addAccessForm = function (maxR, renderedAs, userData, onCancel) { // new form - var input = h('input', {placeholder:'Note'}); // XXX + var input = h('input', { + placeholder: (userData && userData.displayName) || 'Note' // XXX + }); var dd = makeDD('r', true, maxR); var canDestroy = UI.createCheckbox('cp-share-can-destroy', h('i.fa.fa-trash'), false, {}); var $d = $(canDestroy).find('input'); @@ -465,8 +477,8 @@ define([ h('i.fa.fa-times') ]); - var temp = h('div.cp-share-access', {order:100, style: 'display:none;'}, [ - h('i.fa.fa-plus'), + var temp = h('div.cp-share-access', {order:100}, [ + userData ? h('i.fa.fa-user-plus') : h('i.fa.fa-plus'), input, dd[0], canDestroy, @@ -475,6 +487,93 @@ define([ ]); var $temp = $(temp); + $(cancelBtn).click(function () { + onCancel(); + $temp.remove(); + }); + $(saveBtn).click(function () { + var access = getRights(dd, $d); + var note = { + type: 'link', + note: $(input).val() + }; + if (userData && userData.edPublic) { + // XXX REVOCATION teams and sf? + note.type = 'user'; + note.edPublic = userData.edPublic; + } + // XXX REVOCATION add "share with team when I'm a viewer" (send to mailbox) + addAccess(access, note, renderedAs.key, function (obj) { + redraw(); + if (obj && obj.error) { return UI.warn(Messages.error); } + var box = Hash.getRevocable(priv.app, obj.seed); + var hash = Hash.getRevocableHashFromKeys(priv.app, box); + var href = Hash.hashToHref(hash, priv.app); + common.mailbox.sendTo("SHARE_PAD", { + href: href, + isStatic: Boolean(opts.static), + password: opts.password, + isTemplate: opts.isTemplate, + name: myName, + isCalendar: Boolean(opts.calendar), + title: title, + revocable: { + channel: opts.channel, + } + }, { + channel: userData.notifications, + curvePublic: userData.curvePublic + }); + }); + }); + + return $temp; + }; + + var refreshUsergrid = function (cb) { + $usergrid.empty().show(); + $content.hide(); + var friendsList = UIElements.getUserGrid(Messages.share_linkFriends, { + common: common, + data: opts.friends, + noFilter: true, // XXX + noSelect: true, + picker: true, + large: true + }, function ($friend) { + var curve = $friend.data('curve'); + var data = opts.friends[curve]; + if (!data || !data.notifications || !data.curvePublic) { + // XXX REVOCATION show error? can't share with this contact + return void redraw(); + } + $usergrid.hide(); + $content.show(); + cb(data); + }); + $usergrid.append(friendsList.div); + }; + var addUserAccessButton = function (maxR, renderedAs) { + var button = h('button.btn.btn-primary', [ + h('i.fa.fa-plus'), + h('span', 'ADD USER') // XXX + ]); + var $b = $(button); + + var onCancel = function () { $b.show(); }; + + $b.click(function () { + $b.hide(); + refreshUsergrid(function (userData) { + var $form = addAccessForm(maxR, renderedAs, userData, onCancel); + $list.append($form); + }); + }); + + $(buttonContainer).append($b); + return button; + }; + var addAccessButton = function (maxR, renderedAs) { // show form btn var button = h('button.btn.btn-primary', [ h('i.fa.fa-plus'), @@ -482,34 +581,21 @@ define([ ]); var $b = $(button); - $(saveBtn).click(function () { - var access = getRights(dd, $d); - var note = { - type: 'link', - note: $(input).val() - }; - if (false) { note.edPublic = "edpublic"; } // XXX user access - addAccess(access, note, renderedAs.key); - }); - $(cancelBtn).click(function () { - $b.show(); - $temp.hide(); - }); + var onCancel = function () { $b.show(); }; + $b.click(function () { $b.hide(); - $temp.css('display', 'flex'); - + var $form = addAccessForm(maxR, renderedAs, null, onCancel); + $list.append($form); }); - $content.append(temp); - var container = h('div', button); - $(content).append(container); - return container; + $(buttonContainer).append($b); + return button; }; - var addButton; + var addButton, addUserButton; var renderAll = function (obj, renderedAs) { - $content.empty(); + $list.empty(); var list = obj.list; var myAccess = list[renderedAs.key]; var maxRights = myAccess.rights.includes('m') ? 'm' : @@ -517,10 +603,12 @@ define([ Object.keys(list || {}).forEach(function (ed) { var editable = renderedAs.moderator || renderedAs.key === list[ed].from; var a = renderAccess(ed, list[ed], editable, maxRights, renderedAs); - $content.append(a); + $list.append(a); }); if (addButton) { $(addButton).remove(); } addButton = addAccessButton(maxRights, renderedAs); + if (addUserButton) { $(addUserButton).remove(); } + addUserButton = addUserAccessButton(maxRights, renderedAs); }; var renderAs = function (obj) { $viewAs.empty(); @@ -554,7 +642,22 @@ define([ renderAll(obj, myKeys[0]); }; - addAccess = function (rights, note, updateAs) { + redraw = function () { + revocation.execCommand('LIST_ACCESS', { + channel: channel + }, function (obj) { + if (obj && obj.error) { + return void UI.warn(Messages.error); + } + if (!obj.myKeys || !obj.myKeys.length) { + console.error('Not a member!'); + return void UI.warn(Messages.error); + } + renderAs(obj); + }); + }; + + addAccess = function (rights, note, updateAs, cb) { revocation.execCommand('ADD_ACCESS', { type: priv.app, channel: channel, @@ -562,8 +665,10 @@ define([ note: note, from: updateAs }, function (obj) { - console.warn(obj); - // XXX refresh view + if (cb) { return void cb(obj); } + + redraw(); + if (obj && obj.error) { return UI.warn(Messages.error); } }); }; updateAccess = function (user, rights, updateAs) { @@ -578,25 +683,15 @@ define([ }, from: updateAs }, function () { + redraw(); // XXX refresh view }); }; - revocation.execCommand('LIST_ACCESS', { - channel: channel - }, function (obj) { - if (obj && obj.error) { - return void UI.warn(Messages.error); - } - if (!obj.myKeys || !obj.myKeys.length) { - console.error('Not a member!'); - return void UI.warn(Messages.error); - } - renderAs(obj); - }); + redraw(); cb(void 0, { - content: content, + content: [content, usergrid], buttons: [{ className: 'cancel', name: Messages.filePicker_close, @@ -1071,7 +1166,7 @@ define([ opts.hasPassword = parsedHref.hashData.password; var $rights; - if (!parsedHref.revocable) { + if (!hashes.revocableData) { // XXX this function adds opts.channel which breaks data // but we may not need it for revocable pads $rights = opts.$rights = getRightsHeader(common, opts); diff --git a/www/common/notifications.js b/www/common/notifications.js index 6d921929d..7e3f75817 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -122,12 +122,29 @@ define([ }, defaultDismiss(common, data)); return; } + var href = msg.content.href; var obj = { p: msg.content.isTemplate ? ['template'] : undefined, t: teamNotification || undefined, pw: msg.content.password || '' }; - common.openURL(Hash.getNewPadURL(msg.content.href, obj)); + if (msg.content.revocable) { + // XXX TODO + // We don't want to leak the user personal access for this pad + // The URL should not be visible in the address bar otherwise the user may try + // to copy it and send it to others. + // Option 1: [ ] Ask the user to store in drive before opening + // Option 2: [x] Hide mailbox data in pad options (instantly removed in sco) + var channel = msg.content.revocable.channel; + var parsed = Hash.parsePadUrl(href); + var hash = Hash.getHiddenHashFromKeys(parsed.type, {channel:channel}); + href = Hash.hashToHref(hash, parsed.type); + obj.revocable = { + type: 'user', + seed: parsed.hashData && parsed.hashData.key + }; + } + common.openURL(Hash.getNewPadURL(href, obj)); defaultDismiss(common, data)(); }; if (!content.archived) { diff --git a/www/common/outer/revocation.js b/www/common/outer/revocation.js index e9a0690bb..d9facb6b9 100644 --- a/www/common/outer/revocation.js +++ b/www/common/outer/revocation.js @@ -661,7 +661,7 @@ updateAccess({ var keys = box.parse.getContent(); getPadMetadata(ctx, obj.channel, function (md) { - if (md && md.error) { return; } + if (md && md.error) { return void cb(md); } var edPrivate = keys.edPrivate; var log = Revocable.getSanitizedLog(md); var last = log[log.length-1]; @@ -724,14 +724,15 @@ updateAccess({ moderatorSeedStr: keys.doc.moderator }, keys.password); var crypto = Crypto.createEncryptor(doc.keys); - var cryptoSym = crypto.encrypt; + var cryptoSym = crypto.encrypt; // XXX WRONG ==> DONT SIGN THE NOTES? ONLY ENCRYPT var cryptoAsym = crypto.encrypt; // XXX var mailboxData = Revocable.createMailbox(type, box.secret, keys.doc, rights); var access = Revocable.createAccess(type, mailboxData, note, cryptoSym, cryptoAsym); console.error('NEW HASH', Hash.getRevocableHashFromKeys(type, mailboxData.mailbox)); // XXX - sendInitMsg(ctx, mailboxData.initMsg, function () { + sendInitMsg(ctx, mailboxData.initMsg, function (sendObj) { + if (sendObj && sendObj.error) { return void cb(sendObj); } updateAccess(ctx, { add: true, channel: keys.channel, @@ -740,7 +741,15 @@ console.error('NEW HASH', Hash.getRevocableHashFromKeys(type, mailboxData.mailbo access: access }, from: obj.from, - }, clientId, cb); + }, clientId, function (accessObj) { + if (accessObj && accessObj.error) { + // XXX delete mailbox? or let it be deleted after 3 months + return void cb(accessObj); + } + cb({ + seed: mailboxData.mailbox.keys.seed + }); + }); }); }; @@ -839,6 +848,7 @@ console.error(keyHashStr); nThen(function (waitFor) { sendInitMsg(ctx, moderator.initMsg, waitFor()); sendInitMsg(ctx, editor.initMsg, waitFor()); + // XXX handle errors }).nThen(function (waitFor) { cb(data); }); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 358ef1003..6c5b6bcb7 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -360,6 +360,7 @@ define([ // New pad options var options = parsed.getOptions(); if (options.newPadOpts) { + var rev = false; try { var newPad = Utils.Hash.decodeDataOptions(options.newPadOpts); Cryptpad.initialTeam = newPad.t; @@ -381,15 +382,26 @@ define([ delete Cryptpad.fromFileData; } } - + if (newPad.revocable) { + // XXX REVOCATION fake safe link user access, might change + var seed = newPad.revocable.seed; + var box = Utils.Hash.getRevocable(parsed.type, seed); + currentPad.hash = Utils.Hash.getRevocableHashFromKeys(parsed.type, box); + currentPad.type = newPad.revocable.type; + currentPad.href = Utils.Hash.hashToHref(currentPad.hash, parsed.type); + rev = true; + Cryptpad.setTabHash(parsed.hashData.getHash({})); + } } catch (e) { console.error(e, parsed.hashData.newPadOpts); } delete options.newPadOpts; - currentPad.href = parsed.getUrl(options); - currentPad.hash = parsed.hashData.getHash ? parsed.hashData.getHash(options) - : ''; + if (!rev) { + currentPad.href = parsed.getUrl(options); + currentPad.hash = parsed.hashData.getHash ? parsed.hashData.getHash(options) + : ''; + } var version = parsed.hashData.version; parsed = Utils.Hash.parsePadUrl(currentPad.href); Cryptpad.setTabHash(currentPad.hash); @@ -479,6 +491,7 @@ define([ value: '' }; + // Handle pads with revocable access (safe & unsafe) var getRevocable = function (w) { var correctPassword = waitFor(); @@ -545,7 +558,6 @@ define([ } }; })); - }; // Hidden hash: can't find the channel in our drives: abort @@ -557,10 +569,6 @@ define([ var expire; var revocable; nThen(function (w) { - return; - // XXX REVOCATION - if (parsed.hashData.version !== 5) { return; } - }).nThen(function (w) { // If we're using an unsafe link, get pad attribute if (parsed.hashData.key || !parsed.hashData.channel) { Cryptpad.getPadAttribute('expire', w(function (err, data) { @@ -611,18 +619,14 @@ define([ var chan = parsed.hashData.version === 3 && parsed.hashData.channel; var url = revocable ? null : parsed.getUrl(); Cryptpad.getPadAttribute('channel', w(function (err, data) { - console.error(err, data); stored = (!err && typeof (data) === "string"); }), null, chan); Cryptpad.getPadAttribute('password', w(function (err, val) { - console.error(err, val); password = val; }), url, chan); }).nThen(function (w) { - console.error(expire, password); // Revocable pad, safe (revocable) or unsafe (parsed.revocable) link if (parsed.revocable || revocable) { - console.error(parsed.revocable, revocable); return void getRevocable(w); }