From cf0d0e8e4bbdb98781643acb40ccd0d5ab4ab7c3 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 7 Oct 2019 14:35:11 +0200 Subject: [PATCH 01/50] Provide the secondaryKey to userObject --- www/common/common-hash.js | 34 +--------------------------- www/common/drive-ui.js | 5 +++- www/common/mergeDrive.js | 1 + www/common/outer/mailbox-handlers.js | 1 + www/common/outer/sharedfolder.js | 5 ++-- www/common/outer/team.js | 8 ++++++- www/common/outer/userObject.js | 2 +- www/common/proxy-manager.js | 3 ++- www/common/userObject.js | 27 ++++++++++++++++++---- www/drive/inner.js | 7 +++++- www/teams/inner.js | 15 ++++++++++-- 11 files changed, 62 insertions(+), 46 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index fa9feb7e1..f8b8d5f62 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -420,39 +420,6 @@ Version 1 }; // STORAGE - Hash.findWeaker = function (href, channel, recents) { - var parsed = parsePadUrl(href); - if (!parsed.hash) { return false; } - // We can't have a weaker hash if we're already in view mode - if (parsed.hashData && parsed.hashData.mode === 'view') { return; } - var weaker; - Object.keys(recents).some(function (id) { - var pad = recents[id]; - if (pad.href || !pad.roHref) { - // This pad has an edit link, so it can't be weaker - return; - } - var p = parsePadUrl(pad.roHref); - if (p.type !== parsed.type) { return; } // Not the same type - if (p.hash === parsed.hash) { return; } // Same hash, not stronger - if (channel !== pad.channel) { return; } // Not the same channel - - var pHash = p.hashData; - var parsedHash = parsed.hashData; - if (!parsedHash || !pHash) { return; } - - // We don't have stronger/weaker versions of files or users - if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; } - - if (pHash.version !== parsedHash.version) { return; } - if (pHash.mode === 'view' && parsedHash.mode === 'edit') { - weaker = pad; - return true; - } - return; - }); - return weaker; - }; Hash.findStronger = function (href, channel, recents) { var parsed = parsePadUrl(href); if (!parsed.hash) { return false; } @@ -472,6 +439,7 @@ Version 1 if (channel !== pad.channel) { return; } // If this pad doesn't have an edit link, it can't be stronger + // XXX encrypted href if (!pad.href || !pad.roHref) { return; } // This is a pad with an EDIT href and using the same channel as our target diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index d0ae3910f..5d6ac55ad 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -542,7 +542,10 @@ define([ Object.keys(folders).forEach(function (id) { var f = folders[id]; - manager.addProxy(id, f); + var sfData = files.sharedFolders[id] || {}; + var parsed = Hash.parsePadUrl(sfData.href); + var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); + manager.addProxy(id, f, null, secret.keys.secondaryKey); }); // UI containers diff --git a/www/common/mergeDrive.js b/www/common/mergeDrive.js index aa6e57b0a..4c63a7e0c 100644 --- a/www/common/mergeDrive.js +++ b/www/common/mergeDrive.js @@ -38,6 +38,7 @@ define([ var data = oldFo.getFileData(id); var channel = data.channel; + // XXX encrypted href: we need to be able to change the value here var datas = manager.findChannel(channel, true); // Do not migrate a pad if we already have it, it would create a duplicate // in the drive diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 913f094cf..945bcc284 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -249,6 +249,7 @@ define([ if (msg.author !== content.user.curvePublic) { return void cb(true); } var channel = content.channel; + // XXX encrypted href var res = ctx.store.manager.findChannel(channel); var title; diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index 6d48ae915..f9963e82f 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -27,13 +27,14 @@ define([ var parsed = Hash.parsePadUrl(data.href); var secret = Hash.getSecrets('drive', parsed.hash, data.password); + var secondaryKey = secret.keys.secondaryKey; var sf = allSharedFolders[secret.channel]; if (sf && sf.ready && sf.rt) { // The shared folder is already loaded, return its data setTimeout(function () { var leave = function () { SF.leave(secret.channel, teamId); }; - store.manager.addProxy(id, sf.rt.proxy, leave); + store.manager.addProxy(id, sf.rt.proxy, leave, secondaryKey); cb(sf.rt, sf.metadata); }); sf.team.push(teamId); @@ -84,7 +85,7 @@ define([ } sf.queue.forEach(function (obj) { var leave = function () { SF.leave(secret.channel, teamId); }; - obj.store.manager.addProxy(obj.id, rt.proxy, leave); + obj.store.manager.addProxy(obj.id, rt.proxy, leave, secondaryKey); obj.cb(rt, info.metadata); }); sf.leave = info.leave; diff --git a/www/common/outer/team.js b/www/common/outer/team.js index ef5960121..2399da4f7 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -215,6 +215,11 @@ define([ cb(id, rt); }); }; + var teamData = ctx.store.proxy.teams[team.id]; + var secret; + if (teamData) { + secret = Hash.getSecrets('team', teamData.hash, teamData.password); + } var manager = team.manager = ProxyManager.create(proxy.drive, { onSync: function (cb) { ctx.Store.onSync(id, cb); }, edPublic: keys.drive.edPublic, @@ -223,7 +228,8 @@ define([ loadSharedFolder: loadSharedFolder, settings: { drive: Util.find(ctx.store, ['proxy', 'settings', 'drive']) - } + }, + editKey: secret && secret.keys.secondaryKey }, { outer: true, removeOwnedChannel: function (channel, cb) { diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index d22d7d406..d28b9d84c 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -36,7 +36,7 @@ define([ var id = exp.getIdFromHref(href); if (!id) { return void cb("E_INVAL_HREF"); } if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } - var data = exp.getFileData(id); + var data = exp.getFileData(id, true); data[attr] = clone(value); cb(null); }; diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index ca914b57a..2c0741d01 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -14,10 +14,11 @@ define([ }; // Add a shared folder to the list - var addProxy = function (Env, id, proxy, leave) { + var addProxy = function (Env, id, proxy, leave, editKey) { var cfg = getConfig(Env); cfg.sharedFolder = true; cfg.id = id; + cfg.editKey = editKey; var userObject = UserObject.init(proxy, cfg); if (userObject.fixFiles) { // Only in outer diff --git a/www/common/userObject.js b/www/common/userObject.js index 7795f09b3..b847ce50d 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -5,8 +5,9 @@ define([ '/common/common-realtime.js', '/common/common-constants.js', '/common/outer/userObject.js', - '/customize/messages.js' -], function (AppConfig, Util, Hash, Realtime, Constants, OuterFO, Messages) { + '/customize/messages.js', + '/bower_components/chainpad-crypto/crypto.js', +], function (AppConfig, Util, Hash, Realtime, Constants, OuterFO, Messages, Crypto) { var module = {}; var ROOT = module.ROOT = "root"; @@ -31,6 +32,19 @@ define([ module.init = function (files, config) { var exp = {}; + + exp.cryptor = { + encrypt : function (x) { return x; }, + decrypt : function (x) { return x; }, + }; + if (config.editKey) { + try { + exp.cryptor = Crypto.createEncryptor(config.editKey); + } catch (e) { + console.error(e); + } + } + exp.getDefaultName = module.getDefaultName; var sframeChan = config.sframeChan; @@ -205,9 +219,14 @@ define([ }; // Get data from AllFiles (Cryptpad_RECENTPADS) - var getFileData = exp.getFileData = function (file) { + var getFileData = exp.getFileData = function (file, noCopy) { if (!file) { return; } - return files[FILES_DATA][file] || {}; + var data = files[FILES_DATA][file] || {}; + if (!noCopy) { + // XXX encrypted href: decrypt or remove "href" + data = JSON.parse(JSON.stringify(data)); + } + return data; }; exp.getFolderData = function (folder) { diff --git a/www/drive/inner.js b/www/drive/inner.js index 360944bbd..9907f6db5 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -3,6 +3,7 @@ define([ '/common/toolbar3.js', '/common/drive-ui.js', '/common/common-util.js', + '/common/common-hash.js', '/common/common-interface.js', '/common/common-feedback.js', '/bower_components/nthen/index.js', @@ -19,6 +20,7 @@ define([ Toolbar, DriveUI, Util, + Hash, UI, Feedback, nThen, @@ -41,13 +43,16 @@ define([ var oldIds = Object.keys(folders); nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { + var sfData = drive.sharedFolders[id] || {}; + var parsed = Hash.parsePadUrl(sfData.href); + var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId }, waitFor(function (err, newObj) { folders[fId] = folders[fId] || {}; copyObjectValue(folders[fId], newObj); if (manager && oldIds.indexOf(fId) === -1) { - manager.addProxy(fId, folders[fId]); + manager.addProxy(fId, folders[fId], null, secret.keys.secondaryKey); } })); }); diff --git a/www/teams/inner.js b/www/teams/inner.js index bb5422f3a..8d15e43c9 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -52,13 +52,16 @@ define([ var oldIds = Object.keys(folders); nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { + var sfData = drive.sharedFolders[id] || {}; + var parsed = Hash.parsePadUrl(sfData.href); + var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId }, waitFor(function (err, newObj) { folders[fId] = folders[fId] || {}; copyObjectValue(folders[fId], newObj); if (manager && oldIds.indexOf(fId) === -1) { - manager.addProxy(fId, folders[fId]); + manager.addProxy(fId, folders[fId], null, secret.keys.secondaryKey); } })); }); @@ -260,13 +263,21 @@ define([ $limitContainer.attr('title', Messages.team_quota); }, true); driveAPP.team = id; + + // Provide secondaryKey + var teamData = (proxy.teams || {})[id]; + var secret; + if (teamData) { + secret = Hash.getSecrets('team', teamData.hash, teamData.password); + } var drive = DriveUI.create(common, { proxy: proxy, folders: folders, updateObject: updateObject, updateSharedFolders: updateSharedFolders, APP: driveAPP, - edPublic: APP.teamEdPublic + edPublic: APP.teamEdPublic, + editKey: secret && secret.keys.secondaryKey }); APP.drive = drive; driveAPP.refresh = drive.refresh; From 5ab3f39fb87fff1dd0402fb2a4ef2b036ea2a737 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 7 Oct 2019 18:30:46 +0200 Subject: [PATCH 02/50] Encrypted href --- www/common/make-backup.js | 9 ++-- www/common/mergeDrive.js | 7 +-- www/common/migrate-user-object.js | 6 ++- www/common/outer/async-store.js | 8 ++-- www/common/outer/mailbox-handlers.js | 5 +-- www/common/outer/sharedfolder.js | 4 +- www/common/outer/userObject.js | 66 ++++++++++++++++++++++------ www/common/proxy-manager.js | 28 +++++++----- www/common/userObject.js | 39 +++++++++++++--- www/debug/inner.js | 4 +- www/drive/inner.js | 2 +- www/settings/inner.js | 4 +- www/teams/inner.js | 2 +- 13 files changed, 133 insertions(+), 51 deletions(-) diff --git a/www/common/make-backup.js b/www/common/make-backup.js index 382aee0b3..7db9b998f 100644 --- a/www/common/make-backup.js +++ b/www/common/make-backup.js @@ -52,7 +52,8 @@ define([ var cancel = function () { cancelled = true; }; - var parsed = Hash.parsePadUrl(fData.href || fData.roHref); + var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref; + var parsed = Hash.parsePadUrl(href); var hash = parsed.hash; var name = fData.filename || fData.title; var secret = Hash.getSecrets('file', hash, fData.password); @@ -88,7 +89,8 @@ define([ cancelled = true; }; - var parsed = Hash.parsePadUrl(pData.href || pData.roHref); + var href = (pData.href && pData.href.indexOf('#') !== -1) ? pData.href : pData.roHref; + var parsed = Hash.parsePadUrl(href); var name = pData.filename || pData.title; var opts = { password: pData.password @@ -137,7 +139,8 @@ define([ }); } - var parsed = Hash.parsePadUrl(fData.href || fData.roHref); + var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref; + var parsed = Hash.parsePadUrl(href); if (['pad', 'file'].indexOf(parsed.hashData.type) === -1) { return; } // waitFor is used to make sure all the pads and files are process before downloading the zip. diff --git a/www/common/mergeDrive.js b/www/common/mergeDrive.js index 4c63a7e0c..4aeb03dc5 100644 --- a/www/common/mergeDrive.js +++ b/www/common/mergeDrive.js @@ -38,8 +38,7 @@ define([ var data = oldFo.getFileData(id); var channel = data.channel; - // XXX encrypted href: we need to be able to change the value here - var datas = manager.findChannel(channel, true); + var datas = manager.findChannel(channel); // Do not migrate a pad if we already have it, it would create a duplicate // in the drive if (datas.length !== 0) { @@ -50,7 +49,9 @@ define([ // We want to merge an edit pad: check if we have the same channel // but read-only and upgrade it in that case datas.forEach(function (pad) { - if (pad.data && !pad.data.href) { pad.data.href = data.href; } + if (pad.data && !pad.data.href) { + pad.userObject.setHref(channel, null, data.href); + } }); return; } diff --git a/www/common/migrate-user-object.js b/www/common/migrate-user-object.js index 4684b200d..ff56065a8 100644 --- a/www/common/migrate-user-object.js +++ b/www/common/migrate-user-object.js @@ -145,10 +145,14 @@ define([ n = n.nThen(function (w) { setTimeout(w(function () { el = data[k]; - if (!el.href || (el.roHref && false)) { + if (!el.href) { // Already migrated return void progress(7, Math.round(100*i/padsLength)); } + if (el.href.indexOf('#') === -1) { + // Encrypted href: already migrated + return void progress(7, Math.round(100*i/padsLength)); + } parsed = Hash.parsePadUrl(el.href); if (parsed.hashData.type !== "pad") { // No read-only mode for files diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 9ff329335..2a19184c4 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1045,7 +1045,7 @@ define([ if (data.teamId && s.id !== data.teamId) { return; } if (storeLocally && s.id) { return; } - var res = s.manager.findChannel(channel); + var res = s.manager.findChannel(channel, true); if (res.length) { sendTo.push(s.id); } @@ -1081,7 +1081,7 @@ define([ // If all of the weaker ones were in the trash, add the stronger to ROOT obj.userObject.restoreHref(href); } - pad.href = href; + obj.userObject.setHref(channel, null, href); }); // Pads owned by us ("us" can be a user or a team) that are not in our "main" drive @@ -1474,7 +1474,7 @@ define([ onMetadataUpdate: function (metadata) { channel.data = metadata || {}; getAllStores().forEach(function (s) { - var allData = s.manager.findChannel(data.channel); + var allData = s.manager.findChannel(data.channel, true); allData.forEach(function (obj) { obj.data.owners = metadata.owners; obj.data.atime = +new Date(); @@ -1640,7 +1640,7 @@ define([ // Update owners and expire time in the drive getAllStores().forEach(function (s) { - var allData = s.manager.findChannel(data.channel); + var allData = s.manager.findChannel(data.channel, true); var changed = false; allData.forEach(function (obj) { if (Sortify(obj.data.owners) !== Sortify(metadata.owners)) { diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 945bcc284..87597f0b1 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -249,14 +249,13 @@ define([ if (msg.author !== content.user.curvePublic) { return void cb(true); } var channel = content.channel; - // XXX encrypted href - var res = ctx.store.manager.findChannel(channel); + var res = ctx.store.manager.findChannel(channel, true); var title; res.forEach(function (obj) { if (obj.data && !obj.data.href) { if (!title) { title = obj.data.filename || obj.data.title; } - obj.data.href = content.href; + obj.userObject.setHref(channel, null, content.href); } }); diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index f9963e82f..d9a7058a3 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -25,7 +25,9 @@ define([ var teamId = store.id || -1; var handler = store.handleSharedFolder; - var parsed = Hash.parsePadUrl(data.href); + var href = store.manager.user.userObject.getHref(data); + + var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets('drive', parsed.hash, data.password); var secondaryKey = secret.keys.secondaryKey; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index d28b9d84c..bf9c16fa2 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -31,13 +31,26 @@ define([ var debug = exp.debug; + exp.setHref = function (channel, id, href) { + if (!id && !channel) { return; } + var ids = id ? [id] : exp.findChannels([channel]); + ids.forEach(function (i) { + var data = exp.getFileData(i, true); + data.href = exp.cryptor.encrypt(href); + }); + }; + exp.setPadAttribute = function (href, attr, value, cb) { cb = cb || function () {}; var id = exp.getIdFromHref(href); if (!id) { return void cb("E_INVAL_HREF"); } if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } var data = exp.getFileData(id, true); - data[attr] = clone(value); + if (attr === "href") { + exp.setHref(null, id, value); + } else { + data[attr] = clone(value); + } cb(null); }; exp.getPadAttribute = function (href, attr, cb) { @@ -51,6 +64,8 @@ define([ exp.pushData = function (data, cb) { if (typeof cb !== "function") { cb = function () {}; } var id = Util.createRandomInteger(); + // If we were given an edit link, encrypt its value if needed + if (data.href) { data.href = exp.cryptor.encrypt(data.href); } files[FILES_DATA][id] = data; cb(null, id); }; @@ -70,6 +85,7 @@ define([ return void cb("EAUTH"); } var id = Util.createRandomInteger(); + if (data.href) { data.href = exp.cryptor.encrypt(data.href); } files[SHARED_FOLDERS][id] = data; cb(null, id); }; @@ -209,11 +225,15 @@ define([ id = Number(id); // Find and maybe update existing pads with the same channel id var d = data[id]; + // 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; for (var i in files[FILES_DATA]) { if (files[FILES_DATA][i].channel === d.channel) { // Update href? - if (!files[FILES_DATA][i].href) { files[FILES_DATA][i].href = d.href; } + if (!files[FILES_DATA][i].href) { + files[FILES_DATA][i].href = d.href; + } found = true; break; } @@ -222,7 +242,7 @@ define([ toRemove.push(id); return; } - files[FILES_DATA][id] = data[id]; + files[FILES_DATA][id] = d; }); // Remove existing pads from the "element" variable @@ -500,6 +520,7 @@ define([ if (silent) { debug = function () {}; } + var t0 = +new Date(); debug("Cleaning file system..."); var before = JSON.stringify(files); @@ -536,7 +557,10 @@ define([ // 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: element[el], filename: el}; + files[FILES_DATA][id] = { + href: exp.cryptor.encrypt(element[el]), + filename: el + }; element[key] = id; delete element[el]; } @@ -562,7 +586,10 @@ define([ if (typeof obj.element === "string") { // We have an old file (href) which is not in filesData: add it var id = Util.createRandomInteger(); - files[FILES_DATA][id] = {href: obj.element, filename: el}; + files[FILES_DATA][id] = { + href: exp.cryptor.encrypt(obj.element), + filename: el + }; obj.element = id; } if (exp.isFolder(obj.element)) { fixRoot(obj.element); } @@ -607,7 +634,9 @@ define([ if (typeof el === "string") { // We have an old file (href) which is not in filesData: add it var id = Util.createRandomInteger(); - files[FILES_DATA][id] = {href: el}; + files[FILES_DATA][id] = { + href: exp.cryptor.encrypt(el) + }; us[idx] = id; } if (typeof el === "number") { @@ -653,7 +682,12 @@ define([ continue; } - var parsed = Hash.parsePadUrl(el.href || el.roHref); + var href; + try { + href = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href)); + } catch (e) {} + + var parsed = Hash.parsePadUrl(href || el.roHref); var secret; // Clean invalid hash @@ -670,9 +704,9 @@ define([ } // If we have an edit link, check the view link - if (el.href && parsed.hashData.type === "pad" && parsed.hashData.version) { + if (href && parsed.hashData.type === "pad" && parsed.hashData.version) { if (parsed.hashData.mode === "view") { - el.roHref = el.href; + el.roHref = href; delete el.href; } else if (!el.roHref) { secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); @@ -691,7 +725,7 @@ define([ } // Fix href - if (el.href && /^https*:\/\//.test(el.href)) { el.href = Hash.getRelativeHref(el.href); } + if (href && href.slice(0,1) !== '/') { el.href = exp.cryptor.encrypt(Hash.getRelativeHref(el.href)); } // Fix creation time if (!el.ctime) { el.ctime = el.atime; } // Fix title @@ -732,8 +766,13 @@ define([ el = sf[id]; id = Number(id); + var href; + try { + href = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href)); + } catch (e) {} + // Fix undefined hash - parsed = Hash.parsePadUrl(el.href || el.roHref); + parsed = Hash.parsePadUrl(href || el.roHref); secret = Hash.getSecrets('drive', parsed.hash, el.password); if (!secret.keys) { delete sf[id]; @@ -762,11 +801,12 @@ define([ fixDrive(); fixSharedFolders(); + var ms = (+new Date() - t0) + 'ms'; if (JSON.stringify(files) !== before) { - debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely"); + debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely.", ms); return; } - debug("File system was clean"); + debug("File system was clean.", ms); }; return exp; diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 2c0741d01..77c466db7 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -81,11 +81,13 @@ define([ // Return files data objects associated to a channel for setPadTitle // All occurences are returned, in drive or shared folders - var findChannel = function (Env, channel) { + // If "editable" is true, the data returned is a proxy, otherwise + // it's a cloned object (NOTE: href should never be edited directly) + var findChannel = function (Env, channel, editable) { var ret = []; Env.user.userObject.findChannels([channel]).forEach(function (id) { ret.push({ - data: Env.user.userObject.getFileData(id), + data: Env.user.userObject.getFileData(id, editable), userObject: Env.user.userObject }); }); @@ -93,7 +95,7 @@ define([ Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) { ret.push({ fId: fId, - data: Env.folders[fId].userObject.getFileData(id), + data: Env.folders[fId].userObject.getFileData(id, editable), userObject: Env.folders[fId].userObject }); }); @@ -101,6 +103,8 @@ define([ return ret; }; // Return files data objects associated to a given href for setPadAttribute... + // If "editable" is true, the data returned is a proxy, otherwise + // it's a cloned object (NOTE: href should never be edited directly) var findHref = function (Env, href) { var ret = []; var id = Env.user.userObject.getIdFromHref(href); @@ -155,11 +159,11 @@ define([ return ret; }; - var _getFileData = function (Env, id) { + var _getFileData = function (Env, id, editable) { var userObjects = _getUserObjects(Env); var data = {}; userObjects.some(function (uo) { - data = uo.getFileData(id); + data = uo.getFileData(id, editable); if (Object.keys(data).length) { return true; } }); return data; @@ -278,11 +282,6 @@ define([ filesData[f] = userObject.getFileData(f); }); - // TODO RO - // Encrypt or decrypt edit link here - // filesData.forEach(function (d) { d.href = encrypt(d.href); }); - - data.push({ el: el, data: filesData, @@ -435,6 +434,7 @@ define([ Env.pinPads([folderData.channel], waitFor()); }).nThen(function (waitFor) { // 1. add the shared folder to our list of shared folders + // NOTE: pushSharedFolder will encrypt the href directly in the object if needed Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) { if (err) { waitFor.abort(); @@ -1128,7 +1128,13 @@ define([ if (!Env.folders[id]) { return {}; } var obj = Env.folders[id].proxy.metadata || {}; for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) { - obj[k] = Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]; + var data = JSON.parse(JSON.stringify(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k])); + if (data.href && data.href.indexOf('#') === -1) { + try { + data.href = Env.user.userObject.cryptor.decrypt(data.href); + } catch (e) {} + } + obj[k] = data; } return obj; }; diff --git a/www/common/userObject.js b/www/common/userObject.js index b847ce50d..909e0e654 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -39,7 +39,13 @@ define([ }; if (config.editKey) { try { - exp.cryptor = Crypto.createEncryptor(config.editKey); + var c = Crypto.createEncryptor(config.editKey); + exp.cryptor.encrypt = function (href) { + // Never encrypt blob href, they are always read-only + if (href.slice(0,7) === '/file/#') { return href; } + return c.encrypt(href); + }; + exp.cryptor.decrypt = c.decrypt; } catch (e) { console.error(e); } @@ -106,6 +112,16 @@ define([ return a; }; + var getHref = exp.getHref = function (pad) { + if (pad.href && pad.href.indexOf('#') !== -1) { + return pad.href; + } + if (pad.href) { + return exp.cryptor.decrypt(pad.href); + } + return pad.roHref; + }; + var type = function (dat) { return dat === null? 'null': Array.isArray(dat)?'array': typeof(dat); }; @@ -219,12 +235,23 @@ define([ }; // Get data from AllFiles (Cryptpad_RECENTPADS) - var getFileData = exp.getFileData = function (file, noCopy) { + var getFileData = exp.getFileData = function (file, editable) { if (!file) { return; } var data = files[FILES_DATA][file] || {}; - if (!noCopy) { - // XXX encrypted href: decrypt or remove "href" + if (!editable) { data = JSON.parse(JSON.stringify(data)); + if (data.href && data.href.indexOf('#') === -1) { + // Encrypted href: decrypt it if we can, otherwise remove it + if (config.editKey) { + try { + data.href = exp.cryptor.decrypt(data.href); + } catch (e) { + delete data.href; + } + } else { + delete data.href; + } + } } return data; }; @@ -401,7 +428,7 @@ define([ var getIdFromHref = exp.getIdFromHref = function (href) { var result; getFiles([FILES_DATA]).some(function (id) { - if (files[FILES_DATA][id].href === href || + if (getHref(files[FILES_DATA][id]) === href || files[FILES_DATA][id].roHref === href) { result = id; return true; @@ -413,7 +440,7 @@ define([ exp.getSFIdFromHref = function (href) { var result; getFiles([SHARED_FOLDERS]).some(function (id) { - if (files[SHARED_FOLDERS][id].href === href || + if (getHref(files[SHARED_FOLDERS][id]) === href || files[SHARED_FOLDERS][id].roHref === href) { result = id; return true; diff --git a/www/debug/inner.js b/www/debug/inner.js index 1a5c9e023..3373b4803 100644 --- a/www/debug/inner.js +++ b/www/debug/inner.js @@ -97,7 +97,7 @@ define([ for (var i = 0; i Date: Mon, 7 Oct 2019 18:38:49 +0200 Subject: [PATCH 03/50] Remove refactoring comment --- www/common/outer/async-store.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 2a19184c4..8d3b840ca 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -74,7 +74,6 @@ define([ }).nThen(function () { cb(); }); }; - // OKTEAM Store.get = function (clientId, data, cb) { var s = getStore(data.teamId); if (!s) { return void cb({ error: 'ENOTFOUND' }); } From 3015d7f22b346522854811b8b06f91bbc7e157b9 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 8 Oct 2019 11:10:03 +0200 Subject: [PATCH 04/50] Fix team deletion --- www/common/common-ui-elements.js | 10 ++++++++-- www/common/outer/async-store.js | 2 +- www/common/outer/team.js | 17 ++++++++++++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index b13353d0e..d1e5a87b3 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1343,7 +1343,6 @@ define([ }]; var content = h('div', [ - h('h4', 'Invite friends to your team: '+ team.name), list.div ]); @@ -4020,8 +4019,15 @@ define([ module.execCommand('JOIN_TEAM', { team: msg.content.team }, function (obj) { - if (obj && obj.error) { return void UI.warn(Messages.error); } + if (obj && obj.error) { + if (obj.error === 'ENOENT') { + common.mailbox.dismiss(data, function () {}); + return void UI.alert(Messages.deletedError); + } + return void UI.warn(Messages.error); + } answer(true); + if (priv.app !== 'teams') { common.openURL('/teams/'); } }); return; } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 8d3b840ca..f808c1205 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -678,7 +678,7 @@ define([ give(); return void _w.abort(); } - otherOwners = md.owners.some(function (ed) { return void ed !== edPublic; }); + otherOwners = md.owners.some(function (ed) { return ed !== edPublic; }); })); }).nThen(function (_w) { if (otherOwners) { diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 2399da4f7..5063d0257 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -568,6 +568,7 @@ define([ if (!me || me.role !== "OWNER") { return cb({ error: "EFORBIDDEN"}); } var edPublic = Util.find(ctx, ['store', 'proxy', 'edPublic']); + var teamEdPublic = Util.find(teamData, ['keys', 'drive', 'edPublic']); nThen(function (waitFor) { ctx.Store.anonRpcMsg(null, { @@ -603,18 +604,24 @@ define([ msg: 'GET_METADATA', data: c }, _w(function (obj) { - if (obj && obj.error) { return void _w.abort(); } + if (obj && obj.error) { + give(); + return void _w.abort(); + } var md = obj[0]; - var isOwner = md && Array.isArray(md.owners) && md.owners.indexOf(edPublic) !== -1; - if (!isOwner) { return void _w.abort(); } - otherOwners = md.owners.some(function (ed) { return void ed !== edPublic; }); + var isOwner = md && Array.isArray(md.owners) && md.owners.indexOf(teamEdPublic) !== -1; + if (!isOwner) { + give(); + return void _w.abort(); + } + otherOwners = md.owners.some(function (ed) { return ed !== teamEdPublic; }); })); }).nThen(function (_w) { if (otherOwners) { ctx.Store.setPadMetadata(null, { channel: c, command: 'RM_OWNERS', - value: [edPublic], + value: [teamEdPublic], }, _w()); return; } From 4e4d01a471280194d6d73564ee4a5f2a9942a430 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 8 Oct 2019 17:54:52 +0200 Subject: [PATCH 05/50] Improve getStrongerHash to also look inside shared folders --- www/common/common-hash.js | 30 ------------------------------ www/common/cryptpad-common.js | 9 ++++++--- www/common/outer/async-store.js | 16 ++++++---------- www/common/proxy-manager.js | 16 ++++++++++++++++ 4 files changed, 28 insertions(+), 43 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index f8b8d5f62..98330367f 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -420,36 +420,6 @@ Version 1 }; // STORAGE - Hash.findStronger = function (href, channel, recents) { - var parsed = parsePadUrl(href); - if (!parsed.hash) { return false; } - var parsedHash = parsed.hashData; - - // We can't have a stronger hash if we're already in edit mode - if (!parsedHash || parsedHash.mode === 'edit') { return; } - - // We don't have stronger/weaker versions of files or users - if (parsedHash.type !== 'pad') { return; } - - var stronger; - Object.keys(recents).some(function (id) { - var pad = recents[id]; - - // Not the same channel? reject - if (channel !== pad.channel) { return; } - - // If this pad doesn't have an edit link, it can't be stronger - // XXX encrypted href - if (!pad.href || !pad.roHref) { return; } - - // This is a pad with an EDIT href and using the same channel as our target - // ==> it is stronger - stronger = pad; - return true; - }); - return stronger; - }; - Hash.hrefToHexChannelId = function (href, password) { var parsed = Hash.parsePadUrl(href); if (!parsed || !parsed.hash) { return; } diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 7f16fa860..79dc781b3 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1200,6 +1200,11 @@ define([ if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); } hashes = Hash.getHashes(secret); + // If the current href is an edit one, return the existing hashes + var parsedHash = parsed.hashData; + if (!parsedHash || parsedHash.mode === 'edit') { return void cb(null, hashes); } + if (parsedHash.type !== 'pad') { return void cb(null, hashes); } + if (secret.version === 0) { // It means we're using an old hash hashes.editHash = window.location.hash.slice(1); @@ -1212,9 +1217,7 @@ define([ } postMessage("GET_STRONGER_HASH", { - href: window.location.href, - channel: secret.channel, - password: secret.password + channel: secret.channel }, function (hash) { if (hash) { hashes.editHash = hash; } cb(null, hashes); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index f808c1205..b88463989 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1265,21 +1265,17 @@ define([ // Get hashes for the share button // If we can find a stronger hash - Store.getStrongerHash = function (clientId, data, cb) { - var found = getAllStores().some(function (s) { - var allPads = Util.find(s.proxy, ['drive', 'filesData']) || {}; + Store.getStrongerHash = function (clientId, data, _cb) { + var cb = Util.once(_cb); - // If we have a stronger version in drive, add it and add a redirect button - var stronger = Hash.findStronger(data.href, data.channel, allPads); + var found = getAllStores().some(function (s) { + var stronger = s.manager.getEditHash(data.channel); if (stronger) { - var parsed2 = Hash.parsePadUrl(stronger.href); - cb(parsed2.hash); + cb(stronger); return true; } }); - if (!found) { - cb(); - } + if (!found) { cb(); } }; // Universal diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 77c466db7..d7cc0a0d4 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -297,6 +297,21 @@ define([ return data; }; + var getEditHash = function (Env, channel) { + var res = findChannel(Env, channel); + var stronger; + res.some(function (obj) { + if (!obj || !obj.data || !obj.data.href) { return; } + var parsed = Hash.parsePadUrl(obj.data.href); + var parsedHash = parsed.hashData; + if (!parsedHash || parsedHash.mode === 'view') { return; } + // We've found an edit hash! + stronger = parsed.hash; + return true; + }); + return stronger; + }; + /* Drive RPC */ @@ -961,6 +976,7 @@ define([ // Tools findChannel: callWithEnv(findChannel), findHref: callWithEnv(findHref), + getEditHash: callWithEnv(getEditHash), user: Env.user, folders: Env.folders }; From 9a0251f4ba6f5da2aa1dd2218fa1009e83a7f7d3 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 8 Oct 2019 18:47:54 +0200 Subject: [PATCH 06/50] Ability to open an anonymous read only shared folder --- www/common/cryptpad-common.js | 1 + www/common/drive-ui.js | 9 ++++++--- www/common/outer/sharedfolder.js | 2 +- www/common/outer/userObject.js | 6 ++++++ www/common/proxy-manager.js | 2 +- www/common/userObject.js | 8 +++++++- www/drive/inner.js | 7 ++++++- www/drive/main.js | 5 +++++ www/teams/inner.js | 7 ------- 9 files changed, 33 insertions(+), 14 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 79dc781b3..2b3dd4412 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1494,6 +1494,7 @@ define([ noWorker = localStorage.CryptPad_noWorkers === '1'; console.error('WebWorker/SharedWorker state forced to ' + !noWorker); } + noWorker = true; Nthen(function (waitFor2) { if (Worker) { var w = waitFor2(); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 197c31cd5..206ead590 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -1154,9 +1154,12 @@ define([ hide.push('openro'); // Remove open 'view' mode } // if it's not a plain text file - var metadata = manager.getFileData(manager.find(path)); - if (!metadata || !Util.isPlainTextFile(metadata.fileType, metadata.title)) { - hide.push('openincode'); + // XXX: there is a bug with this code in anon shared folder, so we disable it + if (APP.loggedIn || !APP.newSharedFolder) { + var metadata = manager.getFileData(manager.find(path)); + if (!metadata || !Util.isPlainTextFile(metadata.fileType, metadata.title)) { + hide.push('openincode'); + } } } else if ($element.is('.cp-app-drive-element-sharedf')) { if (containsFolder) { diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index d9a7058a3..8e6009a17 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -68,7 +68,7 @@ define([ var listmapConfig = { data: {}, channel: secret.channel, - readOnly: false, + readOnly: secret.keys && !secret.keys.editKeyStr, crypto: Crypto.createEncryptor(secret.keys), userName: 'sharedFolder', logLevel: 1, diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index bf9c16fa2..79047f4e3 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -687,6 +687,12 @@ define([ href = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href)); } catch (e) {} + if (href && href.indexOf('#') === -1) { + // If we can't decrypt the href, it means we don't have the correct secondaryKey and we're in readOnly mode: + // abort now, we won't be able to fix anything anyway + continue; + } + var parsed = Hash.parsePadUrl(href || el.roHref); var secret; diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index d7cc0a0d4..0e057f874 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -164,7 +164,7 @@ define([ var data = {}; userObjects.some(function (uo) { data = uo.getFileData(id, editable); - if (Object.keys(data).length) { return true; } + if (data && Object.keys(data).length) { return true; } }); return data; }; diff --git a/www/common/userObject.js b/www/common/userObject.js index 909e0e654..0dfaf9bee 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -114,10 +114,16 @@ define([ var getHref = exp.getHref = function (pad) { if (pad.href && pad.href.indexOf('#') !== -1) { + // Href exists and is not encrypted: return href return pad.href; } if (pad.href) { - return exp.cryptor.decrypt(pad.href); + // Href exists and is encrypted + var d = exp.cryptor.decrypt(pad.href); + // If we can decrypt, return the decrypted value, otherwise continue and return roHref + if (d.indexOf('#') !== -1) { + return d; + } } return pad.roHref; }; diff --git a/www/drive/inner.js b/www/drive/inner.js index 94847f661..6f5c3de72 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -65,7 +65,10 @@ define([ copyObjectValue(obj, newObj); if (!APP.loggedIn && APP.newSharedFolder) { obj.drive.sharedFolders = obj.drive.sharedFolders || {}; - obj.drive.sharedFolders[APP.newSharedFolder] = {}; + obj.drive.sharedFolders[APP.newSharedFolder] = { + href: APP.anonSFHref, + password: APP.anonSFPassword + }; } cb(); }); @@ -124,6 +127,8 @@ define([ var privateData = metadataMgr.getPrivateData(); if (privateData.newSharedFolder) { APP.newSharedFolder = privateData.newSharedFolder; + APP.anonSFHref = privateData.anonSFHref; + APP.anonSFPassword = privateData.password; } var sframeChan = common.getSframeChannel(); diff --git a/www/drive/main.js b/www/drive/main.js index 273f6b153..0cda8a9cc 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -107,12 +107,17 @@ define([ sframeChan.event('EV_DRIVE_REMOVE', data); }); }; + var addData = function (meta) { + if (!window.CryptPad_newSharedFolder) { return; } + meta.anonSFHref = window.location.href; + }; SFCommonO.start({ afterSecrets: afterSecrets, noHash: true, noRealtime: true, driveEvents: true, addRpc: addRpc, + addData: addData, isDrive: true, }); }); diff --git a/www/teams/inner.js b/www/teams/inner.js index d14cb53a8..7d855558c 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -72,10 +72,6 @@ define([ var updateObject = function (sframeChan, obj, cb) { sframeChan.query('Q_DRIVE_GETOBJECT', null, function (err, newObj) { copyObjectValue(obj, newObj); - if (!driveAPP.loggedIn && driveAPP.newSharedFolder) { - obj.drive.sharedFolders = obj.drive.sharedFolders || {}; - obj.drive.sharedFolders[driveAPP.newSharedFolder] = {}; - } cb(); }); }; @@ -922,9 +918,6 @@ define([ common.setTabTitle(Messages.type.teams); // Drive data - if (privateData.newSharedFolder) { - driveAPP.newSharedFolder = privateData.newSharedFolder; - } driveAPP.disableSF = !privateData.enableSF && AppConfig.disableSharedFolders; // Toolbar From d3b03f46eaa6bc98d42e086cebe16d7010767740 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 10 Oct 2019 12:35:01 +0200 Subject: [PATCH 07/50] Migration script --- www/common/drive-ui.js | 2 +- www/common/outer/async-store.js | 5 ++- www/common/outer/sharedfolder.js | 63 ++++++++++++++++++++++++++++---- www/common/outer/team.js | 8 ++-- www/common/outer/userObject.js | 47 ++++++++++++++++++++---- www/common/proxy-manager.js | 9 +++-- www/common/userObject.js | 3 +- www/drive/inner.js | 2 +- www/teams/inner.js | 2 +- 9 files changed, 113 insertions(+), 28 deletions(-) diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 206ead590..e8b9d7235 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -545,7 +545,7 @@ define([ var sfData = files.sharedFolders[id] || {}; var parsed = Hash.parsePadUrl(sfData.href); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); - manager.addProxy(id, f, null, secret.keys.secondaryKey); + manager.addProxy(id, {proxy: f}, null, secret.keys.secondaryKey); }); // UI containers diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index b88463989..a00ce6b9c 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -2034,7 +2034,7 @@ define([ pin: pin, unpin: unpin, loadSharedFolder: loadSharedFolder, - settings: proxy.settings + settings: proxy.settings, }, { outer: true, removeOwnedChannel: function (channel, cb) { Store.removeOwnedChannel('', channel, cb); }, @@ -2043,7 +2043,8 @@ define([ log: function (msg) { // broadcast to all drive apps sendDriveEvent("DRIVE_LOG", msg); - } + }, + rt: store.realtime }); var userObject = store.userObject = manager.user.userObject; addSharedFolderHandler(); diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index 8e6009a17..4e6d548a3 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -19,7 +19,52 @@ define([ var allSharedFolders = {}; - SF.load = function (config, id, data, cb) { + // No version: visible edit + // Version 2: encrypted edit links + SF.checkMigration = function (secondaryKey, proxy, uo, cb) { + if (true) { // XXX remove this block to enable migration at load time + // FIXME history + return void cb(); + } + var drive = proxy.drive || proxy; + // View access: can't migrate + if (!secondaryKey) { return void cb(); } + // Already migrated: nothing to do + if (drive.version >= 2) { return void cb(); } + // Not yet migrating: migrate + if (!drive.migrateRo) { return void uo.migrateReadOnly(cb); } + // Already migrating: wait for the end... + var done = false; + var to; + var it = setInterval(function () { + if (drive.version >= 2) { + done = true; + clearTimeout(to); + clearInterval(it); + return void cb(); + } + }, 100); + var to = setTimeout(function () { + clearInterval(it); + uo.migrateReadOnly(function () { + done = true; + cb(); + }); + }, 20000); + var path = proxy.drive ? ['drive', 'version'] : ['version']; + proxy.on('change', path, function () { + if (done) { return; } + if (drive.version >= 2) { + done = true; + clearTimeout(to); + clearInterval(it); + cb(); + } + }); + }; + + SF.load = function (config, id, data, _cb) { + var cb = Util.once(_cb); var network = config.network; var store = config.store; var teamId = store.id || -1; @@ -36,8 +81,10 @@ define([ // The shared folder is already loaded, return its data setTimeout(function () { var leave = function () { SF.leave(secret.channel, teamId); }; - store.manager.addProxy(id, sf.rt.proxy, leave, secondaryKey); - cb(sf.rt, sf.metadata); + var uo = store.manager.addProxy(id, sf.rt, leave, secondaryKey); + SF.checkMigration(secondaryKey, sf.rt.proxy, uo, function () { + cb(sf.rt, sf.metadata); + }); }); sf.team.push(teamId); if (handler) { handler(id, sf.rt); } @@ -85,13 +132,15 @@ define([ if (!sf.queue) { return; } + sf.leave = info.leave; + sf.metadata = info.metadata; sf.queue.forEach(function (obj) { var leave = function () { SF.leave(secret.channel, teamId); }; - obj.store.manager.addProxy(obj.id, rt.proxy, leave, secondaryKey); - obj.cb(rt, info.metadata); + var uo = obj.store.manager.addProxy(obj.id, rt, leave, secondaryKey); + SF.checkMigration(secondaryKey, rt.proxy, uo, function () { + obj.cb(sf.rt, sf.metadata); + }); }); - sf.leave = info.leave; - sf.metadata = info.metadata; sf.ready = true; delete sf.queue; }); diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 8fa3d65a6..428322d2f 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -181,6 +181,7 @@ define([ }; }; + var secret; team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; nThen(function (waitFor) { @@ -216,7 +217,6 @@ define([ }); }; var teamData = ctx.store.proxy.teams[team.id]; - var secret; if (teamData) { secret = Hash.getSecrets('team', teamData.hash, teamData.password); } @@ -229,7 +229,6 @@ define([ settings: { drive: Util.find(ctx.store, ['proxy', 'settings', 'drive']) }, - editKey: secret && secret.keys.secondaryKey }, { outer: true, removeOwnedChannel: function (channel, cb) { @@ -250,13 +249,16 @@ define([ log: function (msg) { // broadcast to all drive apps team.sendEvent("DRIVE_LOG", msg); - } + }, + rt: team.realtime, + editKey: secret && secret.keys.secondaryKey }); team.userObject = manager.user.userObject; team.userObject.fixFiles(); }).nThen(function (waitFor) { ctx.teams[id] = team; registerChangeEvents(ctx, team, proxy); + SF.checkMigration(secret && secret.keys.secondaryKey, proxy, team.userObject, waitFor()); SF.loadSharedFolders(ctx.Store, ctx.store.network, team, team.userObject, waitFor); }).nThen(function () { if (!team.rpc) { return; } diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index 79047f4e3..dcb588ac7 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -413,6 +413,45 @@ define([ * INTEGRITY CHECK */ + var onSync = function (next) { + if (exp.rt) { + exp.rt.sync(); + Realtime.whenRealtimeSyncs(exp.rt, next); + } else { + window.setTimeout(next, 1000); + } + }; + + exp.migrateReadOnly = function (cb) { + if (!config.editKey) { return void cb({error: 'EFORBIDDEN'}); } + if (files.version >= 2) { return void cb(); } // Already migrated, nothing to do + files.migrateRo = 1; + var next = function () { + var copy = JSON.parse(JSON.stringify(files)); + Object.keys(copy[FILES_DATA]).forEach(function (id) { + var data = copy[FILES_DATA][id] || {}; + // If this pad has a visible href, encrypt it + // "&& data.roHref" is here to make sure this is not a "file" + if (data.href && data.roHref && !data.fileType && data.href.indexOf('#') !== -1) { + data.href = exp.cryptor.encrypt(data.href); + } + }); + Object.keys(copy[SHARED_FOLDERS] || {}).forEach(function (id) { + var data = copy[SHARED_FOLDERS][id] || {}; + // If this folder has a visible href, encrypt it + if (data.href && data.roHref && !data.fileType && data.href.indexOf('#') !== -1) { + data.href = exp.cryptor.encrypt(data.href); + } + }); + copy.version = 2; + delete copy.migrateRo; + + files = copy; + onSync(cb); + }; + onSync(next); + }; + exp.migrate = function (cb) { // Make sure unsorted doesn't exist anymore // Note: Unsorted only works with the old structure where pads are href @@ -491,13 +530,7 @@ define([ delete files.migrate; todo(); }; - if (exp.rt) { - exp.rt.sync(); - // TODO - Realtime.whenRealtimeSyncs(exp.rt, next); - } else { - window.setTimeout(next, 1000); - } + onSync(next); } catch(e) { console.error(e); todo(); diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 0e057f874..00fceb15e 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -14,25 +14,26 @@ define([ }; // Add a shared folder to the list - var addProxy = function (Env, id, proxy, leave, editKey) { + var addProxy = function (Env, id, lm, leave, editKey) { var cfg = getConfig(Env); cfg.sharedFolder = true; cfg.id = id; cfg.editKey = editKey; - var userObject = UserObject.init(proxy, cfg); + cfg.rt = lm.realtime; + var userObject = UserObject.init(lm.proxy, cfg); if (userObject.fixFiles) { // Only in outer userObject.fixFiles(); } Env.folders[id] = { - proxy: proxy, + proxy: lm.proxy, userObject: userObject, leave: leave }; return userObject; }; - // TODO: Remove a shared folder from the list + // XXX: Remove a shared folder from the list var removeProxy = function (Env, id) { var f = Env.folders[id]; if (!f) { return; } diff --git a/www/common/userObject.js b/www/common/userObject.js index 0dfaf9bee..481c64545 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -2,12 +2,11 @@ define([ '/customize/application_config.js', '/common/common-util.js', '/common/common-hash.js', - '/common/common-realtime.js', '/common/common-constants.js', '/common/outer/userObject.js', '/customize/messages.js', '/bower_components/chainpad-crypto/crypto.js', -], function (AppConfig, Util, Hash, Realtime, Constants, OuterFO, Messages, Crypto) { +], function (AppConfig, Util, Hash, Constants, OuterFO, Messages, Crypto) { var module = {}; var ROOT = module.ROOT = "root"; diff --git a/www/drive/inner.js b/www/drive/inner.js index 6f5c3de72..ed2461cd0 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -52,7 +52,7 @@ define([ folders[fId] = folders[fId] || {}; copyObjectValue(folders[fId], newObj); if (manager && oldIds.indexOf(fId) === -1) { - manager.addProxy(fId, folders[fId], null, secret.keys.secondaryKey); + manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey); } })); }); diff --git a/www/teams/inner.js b/www/teams/inner.js index 7d855558c..b313e78e3 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -61,7 +61,7 @@ define([ folders[fId] = folders[fId] || {}; copyObjectValue(folders[fId], newObj); if (manager && oldIds.indexOf(fId) === -1) { - manager.addProxy(fId, folders[fId], null, secret.keys.secondaryKey); + manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey); } })); }); From 13df7e8d38ad473e21ef98114a51269ca3a12a16 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 10 Oct 2019 13:56:12 +0200 Subject: [PATCH 08/50] Fix team editor --- www/common/cryptpad-common.js | 3 ++- www/common/drive-ui.js | 1 + www/common/outer/async-store.js | 4 ++-- www/common/outer/team.js | 10 ++++++++-- www/teams/inner.js | 10 ++++------ 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 2b3dd4412..11c39179e 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -372,7 +372,8 @@ define([ common.getMetadata = function (cb) { - postMessage("GET_METADATA", null, function (obj) { + var parsed = Hash.parsePadUrl(window.location.href); + postMessage("GET_METADATA", parsed && parsed.type, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(null, obj); }); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index e8b9d7235..ec001e1af 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -526,6 +526,7 @@ define([ var files = proxy.drive; var history = driveConfig.history || {}; var edPublic = driveConfig.edPublic || priv.edPublic; + config.editKey = driveConfig.editKey; APP.origin = priv.origin; APP.hideDuplicateOwned = Util.find(priv, ['settings', 'drive', 'hideDuplicate']); APP.closed = false; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index a00ce6b9c..ee890579e 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -550,9 +550,9 @@ define([ }; // Get the metadata for sframe-common-outer - Store.getMetadata = function (clientId, data, cb) { + Store.getMetadata = function (clientId, app, cb) { var disableThumbnails = Util.find(store.proxy, ['settings', 'general', 'disableThumbnails']); - var teams = store.modules['team'] && store.modules['team'].getTeamsData(); + var teams = store.modules['team'] && store.modules['team'].getTeamsData(app); var metadata = { // "user" is shared with everybody via the userlist user: { diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 428322d2f..c57b010c2 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -253,12 +253,13 @@ define([ rt: team.realtime, editKey: secret && secret.keys.secondaryKey }); + team.secondaryKey = secret && secret.keys.secondaryKey; team.userObject = manager.user.userObject; team.userObject.fixFiles(); }).nThen(function (waitFor) { ctx.teams[id] = team; registerChangeEvents(ctx, team, proxy); - SF.checkMigration(secret && secret.keys.secondaryKey, proxy, team.userObject, waitFor()); + SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor()); SF.loadSharedFolders(ctx.Store, ctx.store.network, team, team.userObject, waitFor); }).nThen(function () { if (!team.rpc) { return; } @@ -1109,8 +1110,10 @@ define([ team.getTeam = function (id) { return ctx.teams[id]; }; - team.getTeamsData = function () { + team.getTeamsData = function (app) { var t = {}; + var safe = false; + if (['drive', 'teams', 'settings'].indexOf(app) !== -1) { safe = true; } Object.keys(teams).forEach(function (id) { t[id] = { owner: teams[id].owner, @@ -1118,6 +1121,9 @@ define([ edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), avatar: Util.find(teams[id], ['metadata', 'avatar']) }; + if (safe) { + t[id].secondaryKey = ctx.teams[id].secondaryKey; + } }); return t; }; diff --git a/www/teams/inner.js b/www/teams/inner.js index b313e78e3..8df4df44f 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -241,6 +241,8 @@ define([ // Team APP var loadTeam = function (common, id) { + var metadataMgr = common.getMetadataMgr(); + var privateData = metadataMgr.getPrivateData(); var sframeChan = common.getSframeChannel(); var proxy = {}; var folders = {}; @@ -261,11 +263,7 @@ define([ driveAPP.team = id; // Provide secondaryKey - var teamData = (proxy.teams || {})[id]; - var secret; - if (teamData) { - secret = Hash.getSecrets('team', teamData.hash, teamData.password); - } + var teamData = (privateData.teams || {})[id] || {}; var drive = DriveUI.create(common, { proxy: proxy, folders: folders, @@ -273,7 +271,7 @@ define([ updateSharedFolders: updateSharedFolders, APP: driveAPP, edPublic: APP.teamEdPublic, - editKey: secret && secret.keys.secondaryKey + editKey: teamData.secondaryKey }); APP.drive = drive; driveAPP.refresh = drive.refresh; From 8dcc2cad825a46a75585725621cbcc5b64fa5afd Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 10 Oct 2019 13:56:55 +0200 Subject: [PATCH 09/50] lint compliance --- www/common/outer/sharedfolder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index 4e6d548a3..bcee83472 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -44,7 +44,7 @@ define([ return void cb(); } }, 100); - var to = setTimeout(function () { + to = setTimeout(function () { clearInterval(it); uo.migrateReadOnly(function () { done = true; From 8ca7e11150723f8ad4bbcb7f5f9e447939152dee Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 10 Oct 2019 13:59:10 +0200 Subject: [PATCH 10/50] Add support for viewers in roster --- www/common/outer/roster.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index f2de26da7..be50c6e2c 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -14,7 +14,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { user0CurveKey: { notifications: "", // required displayName: "", // required - role: "OWNER|ADMIN|MEMBER", // MEMBER if not specified + role: "OWNER|ADMIN|MEMBER|VIEWER", // VIEWER if not specified profile: "", title: "" }, @@ -53,7 +53,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { }; var isValidRole = function (role) { - return ['OWNER', 'ADMIN', 'MEMBER'].indexOf(role) !== -1; + return ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1; }; var canAddRole = function (author, role, members) { @@ -65,8 +65,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { // owners can add any valid role they want if (authorRole === 'OWNER') { return true; } - // admins can add other admins or members - if (authorRole === "ADMIN") { return ['ADMIN', 'MEMBER'].indexOf(role) !== -1; } + // admins can add other admins or members or viewers + if (authorRole === "ADMIN") { return ['ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1; } // (MEMBER, other) can't add anyone of any role return false; }; @@ -105,7 +105,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { // owners can remove anyone they want if (authorRole === 'OWNER') { return true; } // admins can remove other admins or members - if (authorRole === "ADMIN") { return ["ADMIN", "MEMBER"].indexOf(role) !== -1; } + if (authorRole === "ADMIN") { return ["ADMIN", "MEMBER", "VIEWER"].indexOf(role) !== -1; } // MEMBERS and non-members cannot remove anyone of any role return false; }; @@ -168,8 +168,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { if (members[curve]) { throw new Error("ALREADY_PRESENT"); } var data = args[curve]; - // if no role was provided, assume MEMBER - if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; } + // if no role was provided, assume VIEWER + if (typeof(data.role) !== 'string') { data.role = 'VIEWER'; } if (!canAddRole(author, data.role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); From d443c93893d633c3a5f8254bba18628186d057f3 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 11 Oct 2019 18:15:48 +0200 Subject: [PATCH 11/50] Upgrade/downgrade shared folders access rights --- www/common/cryptpad-common.js | 3 +- www/common/drive-ui.js | 3 +- www/common/mergeDrive.js | 1 + www/common/messenger-ui.js | 2 +- www/common/outer/mailbox-handlers.js | 35 ++++++ www/common/outer/messenger.js | 1 + www/common/outer/roster.js | 4 +- www/common/outer/sharedfolder.js | 20 +++- www/common/outer/team.js | 168 ++++++++++++++++++++++++--- www/common/outer/userObject.js | 54 ++++++++- www/common/proxy-manager.js | 10 +- www/common/userObject.js | 26 +++-- www/drive/inner.js | 5 +- www/teams/inner.js | 38 +++--- 14 files changed, 315 insertions(+), 55 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 11c39179e..bea457833 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -158,11 +158,12 @@ define([ }); }; common.addSharedFolder = function (teamId, secret, cb) { + var href = secret.keys && secret.keys.editKeyStr ? '/drive/#' + Hash.getEditHashFromKeys(secret) : undefined; postMessage("ADD_SHARED_FOLDER", { teamId: teamId, path: ['root'], folderData: { - href: '/drive/#' + Hash.getEditHashFromKeys(secret), + href: href, roHref: '/drive/#' + Hash.getViewHashFromKeys(secret), channel: secret.channel, password: secret.password, diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index ec001e1af..94ea1cae5 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -544,7 +544,7 @@ define([ Object.keys(folders).forEach(function (id) { var f = folders[id]; var sfData = files.sharedFolders[id] || {}; - var parsed = Hash.parsePadUrl(sfData.href); + var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); manager.addProxy(id, {proxy: f}, null, secret.keys.secondaryKey); }); @@ -2509,6 +2509,7 @@ define([ $('').text(Messages.shareButton).appendTo($shareBlock); var data = manager.getSharedFolderData(id); var parsed = Hash.parsePadUrl(data.href); + // XXX share modal shared folder read only if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); } var friends = common.getFriends(); var teams = common.getMetadataMgr().getPrivateData().teams; diff --git a/www/common/mergeDrive.js b/www/common/mergeDrive.js index 4aeb03dc5..b8b87d0f0 100644 --- a/www/common/mergeDrive.js +++ b/www/common/mergeDrive.js @@ -27,6 +27,7 @@ define([ if (parsed) { var proxy = proxyData.proxy; var oldFo = FO.init(parsed.drive, { + readOnly: false, loggedIn: true, outer: true }); diff --git a/www/common/messenger-ui.js b/www/common/messenger-ui.js index 9d6ba71ad..4cbeaf56e 100644 --- a/www/common/messenger-ui.js +++ b/www/common/messenger-ui.js @@ -43,7 +43,7 @@ define([ MessengerUI.create = function ($container, common, toolbar) { var metadataMgr = common.getMetadataMgr(); var origin = metadataMgr.getPrivateData().origin; - var readOnly = metadataMgr.getPrivateData().readOnly; + var readOnly = metadataMgr.getPrivateData().readOnly || toolbar.readOnly; var isApp = typeof(toolbar) !== "undefined"; diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 87597f0b1..270018eed 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -415,6 +415,41 @@ define([ cb(false); }; + handlers['TEAM_EDIT_RIGHTS'] = 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.teamData) { + console.log('Remove invalid notification'); + return void cb(true); + } + + // Make sure we are a member of this team + var myTeams = Util.find(ctx, ['store', 'proxy', 'teams']) || {}; + var teamId; + var team; + Object.keys(myTeams).some(function (k) { + var _team = myTeams[k]; + if (_team.channel === content.teamChannel) { + teamId = k; + team = _team; + return true; + } + }); + if (!teamId) { return void cb(true); } + + var dismiss = false; + try { + var module = ctx.store.modules['team']; + // changeMyRights returns true if we can't change our rights + dismiss = module.changeMyRights(teamId, content.state, content.teamData); + } catch (e) { console.error(e); } + + cb(dismiss); + }; + + return { add: function (ctx, box, data, cb) { diff --git a/www/common/outer/messenger.js b/www/common/outer/messenger.js index 8a97b7dc0..d0e200deb 100644 --- a/www/common/outer/messenger.js +++ b/www/common/outer/messenger.js @@ -814,6 +814,7 @@ define([ var cb = Util.once(Util.mkAsync(function () { ctx.emit('TEAMCHAT_READY', chanId, [clientId]); _cb({ + readOnly: typeof(secret.keys) === "object" && !secret.keys.validateKey, channel: chanId }); })); diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index be50c6e2c..d5cfc570a 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -168,8 +168,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { if (members[curve]) { throw new Error("ALREADY_PRESENT"); } var data = args[curve]; - // if no role was provided, assume VIEWER - if (typeof(data.role) !== 'string') { data.role = 'VIEWER'; } + // if no role was provided, assume MEMBER + if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; } if (!canAddRole(author, data.role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index bcee83472..c12900c19 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -77,6 +77,10 @@ define([ var secondaryKey = secret.keys.secondaryKey; var sf = allSharedFolders[secret.channel]; + if (sf && sf.readOnly && secondaryKey) { + // We were in readOnly mode and now we know the edit keys! + SF.upgrade(secret.channel, secret); + } if (sf && sf.ready && sf.rt) { // The shared folder is already loaded, return its data setTimeout(function () { @@ -108,14 +112,15 @@ define([ store: store, id: id }], - team: [store.id || -1] + team: [store.id || -1], + readOnly: Boolean(secondaryKey) }; var owners = data.owners; var listmapConfig = { data: {}, channel: secret.channel, - readOnly: secret.keys && !secret.keys.editKeyStr, + readOnly: Boolean(secondaryKey), crypto: Crypto.createEncryptor(secret.keys), userName: 'sharedFolder', logLevel: 1, @@ -148,6 +153,17 @@ define([ return rt; }; + SF.upgrade = function (channel, secret) { + var sf = allSharedFolders[channel]; + if (!sf || !sf.readOnly) { return; } + if (!sf.rt.setReadOnly) { return; } + + if (!secret.keys || !secret.keys.editKeyStr) { return; } + var crypto = Crypto.createEncryptor(secret.keys); + sf.readOnly = false; + sf.rt.setReadOnly(false, crypto); + }; + SF.leave = function (channel, teamId) { var sf = allSharedFolders[channel]; if (!sf) { return; } diff --git a/www/common/outer/team.js b/www/common/outer/team.js index c57b010c2..86b06a186 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -81,6 +81,7 @@ define([ try { team.listmap.stop(); } catch (e) {} try { team.roster.stop(); } catch (e) {} team.proxy = {}; + team.stopped = true; delete ctx.teams[teamId]; delete ctx.store.proxy.teams[teamId]; ctx.emit('LEAVE_TEAM', teamId, team.clients); @@ -140,8 +141,10 @@ define([ roster: roster }; + // Subscribe to events if (cId) { team.clients.push(cId); } + // Listen for roster changes roster.on('change', function () { var state = roster.getState(); var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']); @@ -158,16 +161,19 @@ define([ rosterData.lastKnownHash = hash; }); + // Update metadata var state = roster.getState(); var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]); if (teamData) { teamData.metadata = state.metadata; } + // Broadcast an event to all the tabs displaying this team team.sendEvent = function (q, data, sender) { ctx.emit(q, data, team.clients.filter(function (cId) { return cId !== sender; })); }; + // Provide team chat keys to the messenger app team.getChatData = function () { var chatKeys = keys.chat || {}; var hash = chatKeys.edit || chatKeys.view; @@ -177,7 +183,7 @@ define([ teamId: id, channel: secret.channel, secret: secret, - validateKey: secret.keys.validateKey + validateKey: chatKeys.validateKey }; }; @@ -185,6 +191,7 @@ define([ team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; nThen(function (waitFor) { + // Init Team RPC if (!keys.drive.edPrivate) { return; } initRpc(ctx, team, keys.drive, waitFor(function (err) { if (err) { return; } @@ -208,6 +215,7 @@ define([ }; })); }).nThen(function () { + // Create the proxy manager var loadSharedFolder = function (id, data, cb) { SF.load({ network: ctx.store.network, @@ -217,9 +225,8 @@ define([ }); }; var teamData = ctx.store.proxy.teams[team.id]; - if (teamData) { - secret = Hash.getSecrets('team', teamData.hash, teamData.password); - } + var hash = teamData.hash || teamData.roHash; + secret = Hash.getSecrets('team', hash, teamData.password); var manager = team.manager = ProxyManager.create(proxy.drive, { onSync: function (cb) { ctx.Store.onSync(id, cb); }, edPublic: keys.drive.edPublic, @@ -251,12 +258,14 @@ define([ team.sendEvent("DRIVE_LOG", msg); }, rt: team.realtime, - editKey: secret && secret.keys.secondaryKey + editKey: secret.keys.secondaryKey, + readOnly: Boolean(!secret.keys.secondaryKey) }); team.secondaryKey = secret && secret.keys.secondaryKey; team.userObject = manager.user.userObject; team.userObject.fixFiles(); }).nThen(function (waitFor) { + // Load the shared folders ctx.teams[id] = team; registerChangeEvents(ctx, team, proxy); SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor()); @@ -296,10 +305,20 @@ define([ var openChannel = function (ctx, teamData, id, _cb) { var cb = Util.once(_cb); - var secret = Hash.getSecrets('team', teamData.hash, teamData.password); + var hash = teamData.hash || teamData.roHash; + var secret = Hash.getSecrets('team', hash, teamData.password); var crypto = Crypto.createEncryptor(secret.keys); + if (!teamData.roHash) { + teamData.roHash = Hash.getViewHashFromKeys(secret); + } + var keys = teamData.keys; + if (!keys.chat.validateKey && keys.chat.edit) { + var chatSecret = Hash.getSecrets('chat', keys.chat.edit); + keys.chat.validateKey = chatSecret.keys.validateKey; + } + var roster; var lm; @@ -373,6 +392,7 @@ define([ }, waitFor(function (err, _roster) { if (err) { waitFor.abort(); + console.error(err); return void cb({error: 'ROSTER_ERROR'}); } roster = _roster; @@ -421,6 +441,7 @@ define([ var password = Hash.createChannelId(); var hash = Hash.createRandomHash('team', password); var secret = Hash.getSecrets('team', hash, password); + var roHash = Hash.getViewHashFromKeys(secret); var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey var rosterSeed = Crypto.Team.createSeed(); @@ -520,6 +541,7 @@ define([ chat: { edit: chatHashes.editHash, view: chatHashes.viewHash, + validateKey: chatSecret.keys.validateKey, channel: chatSecret.channel }, roster: { @@ -532,6 +554,7 @@ define([ owner: true, channel: secret.channel, hash: hash, + roHash: roHash, password: password, keys: keys, //members: membersHashes.editHash, @@ -665,7 +688,7 @@ define([ var joinTeam = function (ctx, data, cId, cb) { var team = data.team; - if (!team.hash || !team.channel || !team.password + if (!(team.hash || team.roHash) || !team.channel || !team.password || !team.keys || !team.metadata) { return void cb({error: 'EINVAL'}); } var id = Util.createRandomInteger(); ctx.store.proxy.teams[id] = team; @@ -924,6 +947,92 @@ define([ }); }; + var getInviteData = function (ctx, teamId, edit) { + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return {}; } + var data = Util.clone(teamData); + if (!edit) { + // Delete edit keys + delete data.hash; + delete data.keys.drive.edPrivate; + delete data.keys.chat.edit; + } + // Delete owner key + delete data.owner; + return data; + }; + + // Update my edit rights in listmap (only upgrade) and userObject (upgrade and downgrade) + // We also need to propagate the changes to the shared folders + var updateMyRights = function (ctx, teamId, hash) { + if (!teamId) { return true; } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return true; } + var team = ctx.teams[teamId]; + + var secret = Hash.getSecrets('team', hash || teamData.roHash, teamData.password); + // Upgrade the listmap if we can + SF.upgrade(teamData.channel, secret); + // Set the new readOnly value in userObject + if (team.userObject) { + team.userObject.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey); + } + + // Upgrade the shared folders + var folders = Util.find(team, ['proxy', 'drive', 'sharedFolders']); + Object.keys(folders || {}).forEach(function (sfId) { + var data = team.manager.getSharedFolderData(sfId); + var parsed = Hash.parsePadUrl(data.href || data.roHref); + var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password); + SF.upgrade(secret.channel, secret); + var uo = Util.find(team, ['manager', 'folders', sfId, 'userObject']); + if (uo) { + uo.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey); + } + }); + ctx.emit('ROSTER_CHANGE_RIGHTS', teamId, team.clients); + }; + + var changeMyRights = function (ctx, teamId, state, data) { + if (!teamId) { return true; } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return true; } + var team = ctx.teams[teamId]; + if (!team) { return true; } + + if (teamData.channel !== data.channel || teamData.password !== data.password) { return true; } + + if (state) { + teamData.hash = data.hash; + teamData.keys.drive.edPrivate = data.keys.drive.edPrivate; + teamData.keys.chat.edit = data.keys.chat.edit; + } else { + delete teamData.hash; + delete teamData.keys.drive.edPrivate; + delete teamData.keys.chat.edit; + } + + updateMyRights(ctx, teamId, data.hash); + }; + var changeEditRights = function (ctx, teamId, user, state, cb) { + if (!teamId) { return void cb({error: 'EINVAL'}); } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return void cb ({error: 'ENOENT'}); } + var team = ctx.teams[teamId]; + if (!team) { return void cb ({error: 'ENOENT'}); } + + // Send mailbox to offer ownership + var myData = Messaging.createData(ctx.store.proxy, false); + ctx.store.mailbox.sendTo("TEAM_EDIT_RIGHTS", { + state: state, + teamData: getInviteData(ctx, teamId, state), + user: myData + }, { + channel: user.notifications, + curvePublic: user.curvePublic + }, cb); + }; + var describeUser = function (ctx, data, cId, cb) { var teamId = data.teamId; if (!teamId) { return void cb({error: 'EINVAL'}); } @@ -937,13 +1046,27 @@ define([ // It it is an ownership revocation, we have to set it in pad metadata first if (user.role === "OWNER" && data.data.role !== "OWNER") { revokeOwnership(ctx, teamId, user, function (err) { - if (!err) { return; } + if (!err) { return void cb(); } console.error(err); return void cb({error: err}); }); return; } + // Viewer to editor + if (user.role === "VIEWER" && data.data.role !== "VIEWER") { + return void changeEditRights(ctx, teamId, user, true, function (err) { + return void cb({error: err}); + }); + } + + // Editor to viewer + if (user.role !== "VIEWER" && data.data.role === "VIEWER") { + return void changeEditRights(ctx, teamId, user, false, function (err) { + return void cb({error: err}); + }); + } + var obj = {}; obj[data.curvePublic] = data.data; team.roster.describe(obj, function (err) { @@ -952,15 +1075,6 @@ define([ }); }; - // TODO send guest keys only in the future - var getInviteData = function (ctx, teamId) { - var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); - if (!teamData) { return {}; } - var data = Util.clone(teamData); - delete data.owner; - return data; - }; - var inviteToTeam = function (ctx, data, cId, cb) { var teamId = data.teamId; if (!teamId) { return void cb({error: 'EINVAL'}); } @@ -975,6 +1089,7 @@ define([ var obj = {}; obj[user.curvePublic] = user; + obj[user.curvePublic].role = 'VIEWER'; team.roster.add(obj, function (err) { if (err && err !== 'NO_CHANGE') { return void cb({error: err}); } ctx.store.mailbox.sendTo('INVITE_TO_TEAM', { @@ -1100,9 +1215,21 @@ define([ if (err) { return; } })); + // Listen for changes in our access rights (if another worker receives edit access) + ctx.store.proxy.on('change', ['teams'], function (o, n, p) { + if (p[2] !== 'hash') { return; } + updateMyRights(ctx, p[1], n); + }); + ctx.store.proxy.on('remove', ['teams'], function (o, p) { + if (p[2] !== 'hash') { return; } + updateMyRights(ctx, p[1]); + }); + + Object.keys(teams).forEach(function (id) { ctx.onReadyHandlers[id] = []; - openChannel(ctx, teams[id], id, waitFor(function () { + openChannel(ctx, teams[id], id, waitFor(function (err) { + if (err) { return void console.error(err); } console.debug('Team '+id+' ready'); })); }); @@ -1121,7 +1248,7 @@ define([ edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), avatar: Util.find(teams[id], ['metadata', 'avatar']) }; - if (safe) { + if (safe && ctx.teams[id]) { t[id].secondaryKey = ctx.teams[id].secondaryKey; } }); @@ -1147,6 +1274,9 @@ define([ }); }; + team.changeMyRights = function (id, edit, teamData) { + changeMyRights(ctx, id, edit, teamData); + }; team.updateMyData = function (data) { Object.keys(ctx.teams).forEach(function (id) { var team = ctx.teams[id]; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index dcb588ac7..267b654e7 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -21,6 +21,8 @@ define([ var sharedFolder = config.sharedFolder; var edPublic = config.edPublic; + var readOnly = config.readOnly; + var ROOT = exp.ROOT; var FILES_DATA = exp.FILES_DATA; var OLD_FILES_DATA = exp.OLD_FILES_DATA; @@ -31,8 +33,14 @@ define([ var debug = exp.debug; + exp._setReadOnly = function (state) { + readOnly = state; + if (!readOnly) { exp.fixFiles(); } + }; + exp.setHref = function (channel, id, href) { if (!id && !channel) { return; } + if (readOnly) { return; } var ids = id ? [id] : exp.findChannels([channel]); ids.forEach(function (i) { var data = exp.getFileData(i, true); @@ -42,6 +50,7 @@ define([ exp.setPadAttribute = function (href, attr, value, cb) { cb = cb || function () {}; + if (readOnly) { return void cb('EFORBIDDEN'); } var id = exp.getIdFromHref(href); if (!id) { return void cb("E_INVAL_HREF"); } if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } @@ -63,6 +72,7 @@ define([ exp.pushData = function (data, cb) { if (typeof cb !== "function") { cb = function () {}; } + if (readOnly) { return void cb('EFORBIDDEN'); } var id = Util.createRandomInteger(); // If we were given an edit link, encrypt its value if needed if (data.href) { data.href = exp.cryptor.encrypt(data.href); } @@ -72,12 +82,21 @@ define([ exp.pushSharedFolder = function (data, cb) { if (typeof cb !== "function") { cb = function () {}; } + if (readOnly) { return void cb('EFORBIDDEN'); } // Check if we already have this shared folder in our drive + var exists; if (Object.keys(files[SHARED_FOLDERS]).some(function (k) { - return files[SHARED_FOLDERS][k].channel === data.channel; + if (files[SHARED_FOLDERS][k].channel === data.channel) { + // We already know this shared folder. Check if we can get better access rights + if (data.href && !files[SHARED_FOLDERS][k].href) { + files[SHARED_FOLDERS][k].href = data.href; + } + exists = k; + return true; + } })) { - return void cb ('EEXISTS'); + return void cb ('EEXISTS', exists); } // Add the folder @@ -92,6 +111,7 @@ define([ // FILES DATA var spliceFileData = function (id) { + if (readOnly) { return; } delete files[FILES_DATA][id]; }; @@ -99,6 +119,7 @@ define([ // FILES_DATA. If there are owned pads, remove them from server too. exp.checkDeletedFiles = function (cb) { if (!loggedIn && !config.testMode) { return void cb(); } + if (readOnly) { return void cb('EFORBIDDEN'); } var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]); var toClean = []; @@ -144,21 +165,22 @@ define([ cb(null, toClean, ownedRemoved); }; var deleteHrefs = function (ids) { + if (readOnly) { return; } ids.forEach(function (obj) { var idx = files[obj.root].indexOf(obj.id); files[obj.root].splice(idx, 1); }); }; var deleteMultipleTrashRoot = function (roots) { + if (readOnly) { return; } roots.forEach(function (obj) { var idx = files[TRASH][obj.name].indexOf(obj.el); files[TRASH][obj.name].splice(idx, 1); }); }; exp.deleteMultiplePermanently = function (paths, nocheck, cb) { - var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); }); - var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); }); - var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); }); + if (readOnly) { return void cb('EFORBIDDEN'); } + var allFilesPaths = paths.filter(function(x) { return exp.isPathIn(x, [FILES_DATA]); }); if (!loggedIn && !config.testMode) { @@ -170,6 +192,10 @@ define([ return void cb(); } + var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); }); + var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); }); + var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); }); + var ids = []; hrefPaths.forEach(function (path) { var id = exp.find(path); @@ -216,6 +242,7 @@ define([ // From another drive exp.copyFromOtherDrive = function (path, element, data, key) { + if (readOnly) { return; } // Copy files data // We have to remove pads that are already in the current proxy to make sure // we won't create duplicates @@ -275,6 +302,8 @@ define([ // From the same drive var pushToTrash = function (name, element, path) { + if (readOnly) { return; } + var trash = files[TRASH]; if (typeof(trash[name]) === "undefined") { trash[name] = []; } var trashArray = trash[name]; @@ -285,6 +314,7 @@ define([ trashArray.push(trashElement); }; exp.copyElement = function (elementPath, newParentPath) { + if (readOnly) { return; } if (exp.comparePath(elementPath, newParentPath)) { return; } // Nothing to do... var element = exp.find(elementPath); var newParent = exp.find(newParentPath); @@ -332,6 +362,8 @@ define([ // FORGET (move with href not path) exp.forget = function (href) { + if (readOnly) { return; } + var id = exp.getIdFromHref(href); if (!id) { return; } if (!loggedIn && !config.testMode) { @@ -348,6 +380,8 @@ define([ // If all the occurences of an href are in the trash, remove them and add the file in root. // This is use with setPadTitle when we open a stronger version of a deleted pad exp.restoreHref = function (href) { + if (readOnly) { return; } + var idO = exp.getIdFromHref(href); if (!idO || !exp.isFile(idO)) { return; } @@ -370,6 +404,8 @@ define([ }; exp.add = function (id, path) { + if (readOnly) { return; } + if (!loggedIn && !config.testMode) { return; } id = Number(id); var data = files[FILES_DATA][id] || files[SHARED_FOLDERS][id]; @@ -397,6 +433,8 @@ define([ }; exp.setFolderData = function (path, key, value, cb) { + if (readOnly) { return; } + var folder = exp.find(path); if (!exp.isFolder(folder) || exp.isSharedFolder(folder)) { return; } if (!exp.hasFolderData(folder)) { @@ -423,7 +461,7 @@ define([ }; exp.migrateReadOnly = function (cb) { - if (!config.editKey) { return void cb({error: 'EFORBIDDEN'}); } + if (readOnly || !config.editKey) { return void cb({error: 'EFORBIDDEN'}); } if (files.version >= 2) { return void cb(); } // Already migrated, nothing to do files.migrateRo = 1; var next = function () { @@ -453,6 +491,7 @@ define([ }; exp.migrate = function (cb) { + if (readOnly) { return void cb(); } // Make sure unsorted doesn't exist anymore // Note: Unsorted only works with the old structure where pads are href // It should be called before the migration code @@ -551,6 +590,9 @@ define([ // - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' // * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT + // We can't fix anything in read-only mode: abort + if (readOnly) { return; } + if (silent) { debug = function () {}; } var t0 = +new Date(); diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 00fceb15e..3ceeab773 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -2,9 +2,10 @@ define([ '/common/userObject.js', '/common/common-util.js', '/common/common-hash.js', + '/common/outer/sharedfolder.js', '/customize/messages.js', '/bower_components/nthen/index.js', -], function (UserObject, Util, Hash, Messages, nThen) { +], function (UserObject, Util, Hash, SF, Messages, nThen) { var getConfig = function (Env) { @@ -20,6 +21,7 @@ define([ cfg.id = id; cfg.editKey = editKey; cfg.rt = lm.realtime; + cfg.readOnly = Boolean(!editKey); var userObject = UserObject.init(lm.proxy, cfg); if (userObject.fixFiles) { // Only in outer @@ -452,6 +454,12 @@ define([ // 1. add the shared folder to our list of shared folders // NOTE: pushSharedFolder will encrypt the href directly in the object if needed Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) { + if (err === "EEXISTS" && folderData.href && folderId) { + var parsed = Hash.parsePadUrl(folderData.href); + var secret = Hash.getSecrets('drive', parsed.hash, folderData.password); + SF.upgrade(secret.channel, secret); + Env.folders[folderId].userObject.setReadOnly(false, secret.keys.secondaryKey); + } if (err) { waitFor.abort(); return void cb(err); diff --git a/www/common/userObject.js b/www/common/userObject.js index 481c64545..3925b1b4f 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -32,13 +32,15 @@ define([ module.init = function (files, config) { var exp = {}; - exp.cryptor = { - encrypt : function (x) { return x; }, - decrypt : function (x) { return x; }, - }; - if (config.editKey) { + exp.cryptor = {}; + var createCryptor = function (key) { + if (!key) { + exp.cryptor.encrypt = function (x) { return x; }; + exp.cryptor.decrypt = function (x) { return x; }; + return; + } try { - var c = Crypto.createEncryptor(config.editKey); + var c = Crypto.createEncryptor(key); exp.cryptor.encrypt = function (href) { // Never encrypt blob href, they are always read-only if (href.slice(0,7) === '/file/#') { return href; } @@ -48,7 +50,17 @@ define([ } catch (e) { console.error(e); } - } + }; + createCryptor(config.editKey); + + exp.setReadOnly = function (state, key) { + config.editKey = key; + createCryptor(key); + if (exp._setReadOnly) { + // Change outer + exp._setReadOnly(state); + } + }; exp.getDefaultName = module.getDefaultName; diff --git a/www/drive/inner.js b/www/drive/inner.js index ed2461cd0..e0e695126 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -44,7 +44,7 @@ define([ nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { var sfData = drive.sharedFolders[fId] || {}; - var parsed = Hash.parsePadUrl(sfData.href); + var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId @@ -54,6 +54,9 @@ define([ if (manager && oldIds.indexOf(fId) === -1) { manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey); } + var readOnly = !secret.keys.editKeyStr; + if (!manager || !manager.folders[fId]) { return; } + manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey); })); }); }).nThen(function () { diff --git a/www/teams/inner.js b/www/teams/inner.js index 8df4df44f..f4448de5f 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -53,7 +53,7 @@ define([ nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { var sfData = drive.sharedFolders[fId] || {}; - var parsed = Hash.parsePadUrl(sfData.href); + var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId @@ -467,7 +467,7 @@ define([ }); }; - var ROLES = ['MEMBER', 'ADMIN', 'OWNER']; + var ROLES = ['VIEWER', 'MEMBER', 'ADMIN', 'OWNER']; var describeUser = function (common, curvePublic, data, icon) { APP.module.execCommand('DESCRIBE_USER', { teamId: APP.team, @@ -505,10 +505,11 @@ define([ var actions = h('span.cp-team-member-actions'); var $actions = $(actions); var isMe = me && me.curvePublic === data.curvePublic; - var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1; - var theirRole = ROLES.indexOf(data.role) || 0; + var myRole = me ? (ROLES.indexOf(me.role) || 1) : -1; + var theirRole = ROLES.indexOf(data.role) || 1; + var ADMIN = ROLES.indexOf('ADMIN'); // If they're an admin and I am an owner, I can promote them to owner - if (!isMe && myRole > theirRole && theirRole === 1 && !data.pending) { + if (!isMe && myRole > theirRole && theirRole === ADMIN && !data.pending) { var promoteOwner = h('span.fa.fa-angle-double-up', { title: Messages.team_rosterPromoteOwner }); @@ -530,28 +531,28 @@ define([ }); $actions.append(promoteOwner); } - // If they're a member and I have a higher role than them, I can promote them to admin - if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) { + // If they're a viewer/member and I have a higher role than them, I can promote them to admin + if (!isMe && myRole >= ADMIN && theirRole < ADMIN && !data.pending) { var promote = h('span.fa.fa-angle-double-up', { title: Messages.team_rosterPromote }); $(promote).click(function () { $(promote).hide(); describeUser(common, data.curvePublic, { - role: 'ADMIN' + role: ROLES[theirRole + 1] }, promote); }); $actions.append(promote); } // If I'm not a member and I have an equal or higher role than them, I can demote them // (if they're not already a MEMBER) - if (myRole >= theirRole && theirRole > 0 && !data.pending) { + if (myRole >= theirRole && myRole >= ADMIN && theirRole > 0 && !data.pending) { var demote = h('span.fa.fa-angle-double-down', { title: Messages.team_rosterDemote }); $(demote).click(function () { var todo = function () { - var role = ROLES[theirRole - 1] || 'MEMBER'; + var role = ROLES[theirRole - 1] || 'VIEWER'; $(demote).hide(); describeUser(common, data.curvePublic, { role: role @@ -569,9 +570,9 @@ define([ $actions.append(demote); } } - // If I'm not a member and I have an equal or higher role than them, I can remove them + // If I'm at least an admin and I have an equal or higher role than them, I can remove them // Note: we can't remove owners, we have to demote them first - if (!isMe && myRole > 0 && myRole >= theirRole && theirRole !== 2) { + if (!isMe && myRole >= ADMIN && myRole >= theirRole && theirRole !== ROLES.indexOf('OWNER')) { var remove = h('span.fa.fa-times', { title: Messages.team_rosterKick }); @@ -637,6 +638,12 @@ define([ }).map(function (k) { return makeMember(common, roster[k], me); }); + var viewers = Object.keys(roster).filter(function (k) { + if (roster[k].pending) { return; } + return roster[k].role === "VIEWER"; + }).map(function (k) { + return makeMember(common, roster[k], me); + }); var pending = Object.keys(roster).filter(function (k) { if (!roster[k].pending) { return; } return roster[k].role === "MEMBER" || !roster[k].role; @@ -671,7 +678,7 @@ define([ $header.append(invite); } - if (me && (me.role === 'ADMIN' || me.role === 'MEMBER')) { + if (me && (me.role !== 'OWNER')) { var leave = h('button.btn.btn-danger', Messages.team_leaveButton); $(leave).click(function () { UI.confirm(Messages.team_leaveConfirm, function (yes) { @@ -698,6 +705,8 @@ define([ h('div', admins), h('h3', Messages.team_members), h('div', members), + h('h3', Messages.team_viewers || 'VIEWERS'), // XXX + h('div', viewers), h('h3'+noPending, Messages.team_pending), h('div'+noPending, pending) ]; @@ -721,7 +730,8 @@ define([ common.setTeamChat(obj.channel); MessengerUI.create($(container), common, { chat: $('.cp-team-cat-chat'), - team: true + team: true, + readOnly: obj.readOnly }); cb(content); }); From a3d273ec478068d62cea3df6dc9b488a267a1923 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 14 Oct 2019 12:01:44 +0200 Subject: [PATCH 12/50] Update rights in inner --- www/common/drive-ui.js | 3 +- www/common/outer/mailbox-handlers.js | 2 +- www/common/outer/team.js | 9 ++- www/common/proxy-manager.js | 32 +++++---- www/common/sframe-common-outer.js | 8 +-- www/common/userObject.js | 71 +++++++++--------- www/drive/inner.js | 3 +- www/teams/inner.js | 103 ++++++++++++++++++--------- 8 files changed, 141 insertions(+), 90 deletions(-) diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 94ea1cae5..4bf2367e2 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -544,7 +544,8 @@ define([ Object.keys(folders).forEach(function (id) { var f = folders[id]; var sfData = files.sharedFolders[id] || {}; - var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); + var href = manager.user.userObject.getHref(sfData); + var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); manager.addProxy(id, {proxy: f}, null, secret.keys.secondaryKey); }); diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 270018eed..6fcdbc6cd 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -431,7 +431,7 @@ define([ var team; Object.keys(myTeams).some(function (k) { var _team = myTeams[k]; - if (_team.channel === content.teamChannel) { + if (_team.channel === content.teamData.channel) { teamId = k; team = _team; return true; diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 86b06a186..e297dee65 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -990,6 +990,7 @@ define([ uo.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey); } }); + ctx.updateMetadata(); ctx.emit('ROSTER_CHANGE_RIGHTS', teamId, team.clients); }; @@ -1006,10 +1007,14 @@ define([ teamData.hash = data.hash; teamData.keys.drive.edPrivate = data.keys.drive.edPrivate; teamData.keys.chat.edit = data.keys.chat.edit; + + var secret = Hash.getSecrets('team', data.hash, teamData.password); + team.secondaryKey = secret && secret.keys.secondaryKey; } else { delete teamData.hash; delete teamData.keys.drive.edPrivate; delete teamData.keys.chat.edit; + delete team.secondaryKey; } updateMyRights(ctx, teamId, data.hash); @@ -1055,14 +1060,14 @@ define([ // Viewer to editor if (user.role === "VIEWER" && data.data.role !== "VIEWER") { - return void changeEditRights(ctx, teamId, user, true, function (err) { + changeEditRights(ctx, teamId, user, true, function (err) { return void cb({error: err}); }); } // Editor to viewer if (user.role !== "VIEWER" && data.data.role === "VIEWER") { - return void changeEditRights(ctx, teamId, user, false, function (err) { + changeEditRights(ctx, teamId, user, false, function (err) { return void cb({error: err}); }); } diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 3ceeab773..ead731550 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -172,6 +172,22 @@ define([ return data; }; + var getSharedFolderData = function (Env, id) { + if (!Env.folders[id]) { return {}; } + var obj = Env.folders[id].proxy.metadata || {}; + for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) { + var data = JSON.parse(JSON.stringify(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k])); + if (data.href && data.href.indexOf('#') === -1) { + try { + data.href = Env.user.userObject.cryptor.decrypt(data.href); + } catch (e) {} + } + obj[k] = data; + } + return obj; + }; + + // Transform an absolute path into a path relative to the correct shared folder var _resolvePath = function (Env, path) { var res = { @@ -979,6 +995,7 @@ define([ setPadAttribute: callWithEnv(setPadAttribute), getTagsList: callWithEnv(getTagsList), getSecureFilesList: callWithEnv(getSecureFilesList), + getSharedFolderData: callWithEnv(getSharedFolderData), // Store getChannelsList: callWithEnv(getChannelsList), addPad: callWithEnv(addPad), @@ -1149,21 +1166,6 @@ define([ return Env.user.userObject.getOwnedPads(Env.edPublic); }; - var getSharedFolderData = function (Env, id) { - if (!Env.folders[id]) { return {}; } - var obj = Env.folders[id].proxy.metadata || {}; - for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) { - var data = JSON.parse(JSON.stringify(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k])); - if (data.href && data.href.indexOf('#') === -1) { - try { - data.href = Env.user.userObject.cryptor.decrypt(data.href); - } catch (e) {} - } - obj[k] = data; - } - return obj; - }; - var getFolderData = function (Env, path) { var resolved = _resolvePath(Env, path); if (!resolved || !resolved.userObject) { return {}; } diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index e919f1506..16210b743 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -433,6 +433,10 @@ define([ Cryptpad.mailbox.execCommand(data, cb); }); + sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) { + Cryptpad.storeInTeam(data, cb); + }); + }; addCommonRpc(sframeChan); @@ -465,10 +469,6 @@ define([ setDocumentTitle(); }); - sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) { - Cryptpad.storeInTeam(data, cb); - }); - sframeChan.on('EV_SET_HASH', function (hash) { window.location.hash = hash; }); diff --git a/www/common/userObject.js b/www/common/userObject.js index 3925b1b4f..c57bc0bb0 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -29,29 +29,46 @@ define([ return name; }; + var createCryptor = module.createCryptor = function (key) { + var cryptor = {}; + if (!key) { + cryptor.encrypt = function (x) { return x; }; + cryptor.decrypt = function (x) { return x; }; + return cryptor; + } + try { + var c = Crypto.createEncryptor(key); + cryptor.encrypt = function (href) { + // Never encrypt blob href, they are always read-only + if (href.slice(0,7) === '/file/#') { return href; } + return c.encrypt(href); + }; + cryptor.decrypt = c.decrypt; + } catch (e) { + console.error(e); + } + return cryptor; + }; + module.getHref = function (pad, cryptor) { + if (pad.href && pad.href.indexOf('#') !== -1) { + // Href exists and is not encrypted: return href + return pad.href; + } + if (pad.href) { + // Href exists and is encrypted + var d = cryptor.decrypt(pad.href); + // If we can decrypt, return the decrypted value, otherwise continue and return roHref + if (d.indexOf('#') !== -1) { + return d; + } + } + return pad.roHref; + }; + module.init = function (files, config) { var exp = {}; - exp.cryptor = {}; - var createCryptor = function (key) { - if (!key) { - exp.cryptor.encrypt = function (x) { return x; }; - exp.cryptor.decrypt = function (x) { return x; }; - return; - } - try { - var c = Crypto.createEncryptor(key); - exp.cryptor.encrypt = function (href) { - // Never encrypt blob href, they are always read-only - if (href.slice(0,7) === '/file/#') { return href; } - return c.encrypt(href); - }; - exp.cryptor.decrypt = c.decrypt; - } catch (e) { - console.error(e); - } - }; - createCryptor(config.editKey); + exp.cryptor = createCryptor(config.editKey); exp.setReadOnly = function (state, key) { config.editKey = key; @@ -124,19 +141,7 @@ define([ }; var getHref = exp.getHref = function (pad) { - if (pad.href && pad.href.indexOf('#') !== -1) { - // Href exists and is not encrypted: return href - return pad.href; - } - if (pad.href) { - // Href exists and is encrypted - var d = exp.cryptor.decrypt(pad.href); - // If we can decrypt, return the decrypted value, otherwise continue and return roHref - if (d.indexOf('#') !== -1) { - return d; - } - } - return pad.roHref; + return module.getHref(pad, exp.cryptor); }; var type = function (dat) { diff --git a/www/drive/inner.js b/www/drive/inner.js index e0e695126..f5416a861 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -44,7 +44,8 @@ define([ nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { var sfData = drive.sharedFolders[fId] || {}; - var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); + var href = (sfData.href && sfData.href.indexOf('#') !== -1) ? sfData.href : sfData.roHref; + var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId diff --git a/www/teams/inner.js b/www/teams/inner.js index f4448de5f..859887c36 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -11,6 +11,7 @@ define([ '/bower_components/nthen/index.js', '/common/sframe-common.js', '/common/proxy-manager.js', + '/common/userObject.js', '/common/hyperscript.js', '/customize/application_config.js', '/common/messenger-ui.js', @@ -32,6 +33,7 @@ define([ nThen, SFCommon, ProxyManager, + UserObject, h, AppConfig, MessengerUI, @@ -53,7 +55,8 @@ define([ nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { var sfData = drive.sharedFolders[fId] || {}; - var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); + var href = UserObject.getHref(sfData, APP.cryptor); + var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId @@ -78,6 +81,30 @@ define([ var setEditable = DriveUI.setEditable; + var closeTeam = function (common, cb) { + var sframeChan = common.getSframeChannel(); + APP.module.execCommand('SUBSCRIBE', null, function () { + sframeChan.query('Q_SET_TEAM', null, function (err) { + if (err) { return void console.error(err); } + if (APP.drive && APP.drive.close) { APP.drive.close(); } + $('.cp-toolbar-title-value').text(Messages.type.teams); + sframeChan.event('EV_SET_TAB_TITLE', Messages.type.teams); + APP.team = null; + APP.teamEdPublic = null; + APP.drive = null; + APP.cryptor = null; + APP.buildUI(common); + if (APP.usageBar) { + APP.usageBar.stop(); + APP.usageBar = null; + } + if (cb) { + cb(common); + } + }); + }); + }; + var mainCategories = { 'list': [ 'cp-team-list', @@ -92,23 +119,7 @@ define([ var teamCategories = { 'back': { onClick: function (common) { - var sframeChan = common.getSframeChannel(); - APP.module.execCommand('SUBSCRIBE', null, function () { - sframeChan.query('Q_SET_TEAM', null, function (err) { - if (err) { return void console.error(err); } - if (APP.drive && APP.drive.close) { APP.drive.close(); } - $('.cp-toolbar-title-value').text(Messages.type.teams); - sframeChan.event('EV_SET_TAB_TITLE', Messages.type.teams); - APP.team = null; - APP.teamEdPublic = null; - APP.drive = null; - APP.buildUI(common); - if (APP.usageBar) { - APP.usageBar.stop(); - APP.usageBar = null; - } - }); - }); + closeTeam(common); } }, 'drive': [ @@ -311,8 +322,26 @@ define([ }); var MAX_TEAMS_SLOTS = Constants.MAX_TEAMS_SLOTS; - var refreshList = function (common, cb) { + var openTeam = function (common, id, team) { var sframeChan = common.getSframeChannel(); + APP.module.execCommand('SUBSCRIBE', id, function () { + var t = Messages._getKey('team_title', [Util.fixHTML(team.metadata.name)]); + sframeChan.query('Q_SET_TEAM', id, function (err) { + if (err) { return void console.error(err); } + // Change title + $('.cp-toolbar-title-value').text(t); + sframeChan.event('EV_SET_TAB_TITLE', t); + // Get secondary key + var secret = Hash.getSecrets('team', team.hash || team.roHash, team.password); + APP.cryptor = UserObject.createCryptor(secret.keys.secondaryKey); + // Load data + APP.team = id; + APP.teamEdPublic = Util.find(team, ['keys', 'drive', 'edPublic']); + buildUI(common, true, team.owner); + }); + }); + }; + var refreshList = function (common, cb) { var content = []; APP.module.execCommand('LIST_TEAMS', null, function (obj) { if (!obj) { return; } @@ -348,19 +377,7 @@ define([ ])); common.displayAvatar($(avatar), team.metadata.avatar, team.metadata.name); $(btn).click(function () { - APP.module.execCommand('SUBSCRIBE', id, function () { - var t = Messages._getKey('team_title', [Util.fixHTML(team.metadata.name)]); - sframeChan.query('Q_SET_TEAM', id, function (err) { - if (err) { return void console.error(err); } - // Change title - $('.cp-toolbar-title-value').text(t); - sframeChan.event('EV_SET_TAB_TITLE', t); - // Load data - APP.team = id; - APP.teamEdPublic = Util.find(team, ['keys', 'drive', 'edPublic']); - buildUI(common, true, team.owner); - }); - }); + openTeam(common, id, team); }); }); content.push(h('div.cp-team-list-container', list)); @@ -506,7 +523,7 @@ define([ var $actions = $(actions); var isMe = me && me.curvePublic === data.curvePublic; var myRole = me ? (ROLES.indexOf(me.role) || 1) : -1; - var theirRole = ROLES.indexOf(data.role) || 1; + var theirRole = ROLES.indexOf(data.role); var ADMIN = ROLES.indexOf('ADMIN'); // If they're an admin and I am an owner, I can promote them to owner if (!isMe && myRole > theirRole && theirRole === ADMIN && !data.pending) { @@ -895,6 +912,22 @@ define([ ]); }, true); + var redrawTeam = function (common) { + if (!APP.team) { return; } + var teamId = APP.team; + var name = $('.cp-toolbar-title-value').text(); + APP.module.execCommand('LIST_TEAMS', null, function (obj) { + if (!obj) { return; } + if (obj.error) { return void console.error(obj.error); } + var team = obj[teamId]; + if (!team) { return; } + closeTeam(common, function () { + openTeam(common, teamId, team); + }); + }); + }; + + var main = function () { var common; var readOnly; @@ -961,6 +994,10 @@ define([ } return; } + if (ev === 'ROSTER_CHANGE_RIGHTS') { + redrawTeam(common); + return; + } }; APP.module = common.makeUniversal('team', { From 7c710e57b7d500fa7bd4008dbb991a05d11ca374 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 14 Oct 2019 16:02:21 +0200 Subject: [PATCH 13/50] Read-only shared folders UI --- customize.dist/src/less2/include/drive.less | 24 ++++++++ www/common/drive-ui.js | 67 +++++++++++++++++---- www/drive/inner.html | 1 + www/drive/inner.js | 2 + www/teams/inner.js | 5 +- 5 files changed, 85 insertions(+), 14 deletions(-) diff --git a/customize.dist/src/less2/include/drive.less b/customize.dist/src/less2/include/drive.less index 0691ffd9e..ee5472490 100644 --- a/customize.dist/src/less2/include/drive.less +++ b/customize.dist/src/less2/include/drive.less @@ -466,6 +466,7 @@ padding: 0.25em 0.75em; margin: 1em; background: @drive_info-box-bg; + cursor: default; span { cursor: pointer; float: right; @@ -976,5 +977,28 @@ flex: 1; } } + + #cp-app-drive-edition-state { + height: @variables_bar-height; + display: flex; + align-items: center; + justify-content: center; + background-color: lighten(@colortheme_drive-bg, 32%); + color: black; + font-weight: bold; + text-transform: uppercase; + cursor: default; + } + #cp-app-drive-connection-state { + height: @variables_bar-height; + display: flex; + align-items: center; + justify-content: center; + background-color: #eb675e; + color: white; + font-weight: bold; + text-transform: uppercase; + cursor: default; + } } diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 4bf2367e2..1badf239e 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -252,15 +252,18 @@ define([ return APP.store[LS_SEARCHCURSOR] || 0; }; + // Handle disconnect/reconnect var setEditable = function (state) { - APP.editable = state; - if (APP.closed || (APP.$content && !$.contains(document.documentElement, APP.$content[0]))) { return; } + if (APP.closed || !APP.$content || !$.contains(document.documentElement, APP.$content[0])) { return; } + APP.editable = !APP.readOnly && state; if (!state) { APP.$content.addClass('cp-app-drive-readonly'); + $('#cp-app-drive-connection-state').show(); $('[draggable="true"]').attr('draggable', false); } else { APP.$content.removeClass('cp-app-drive-readonly'); + $('#cp-app-drive-connection-state').hide(); $('[draggable="false"]').attr('draggable', true); } }; @@ -531,6 +534,8 @@ define([ APP.hideDuplicateOwned = Util.find(priv, ['settings', 'drive', 'hideDuplicate']); APP.closed = false; + var $readOnly = $('#cp-app-drive-edition-state'); + var updateObject = driveConfig.updateObject; var updateSharedFolders = driveConfig.updateSharedFolders; @@ -608,9 +613,7 @@ define([ } } - if (!APP.readOnly) { - setEditable(true); - } + APP.editable = !APP.readOnly; var appStatus = { isReady: true, _onReady: [], @@ -1091,8 +1094,10 @@ define([ var show = []; var filter; + var editable = true; if (type === "content") { + if (APP.$content.data('readOnlyFolder')) { editable = false; } // Return true in filter to hide filter = function ($el, className) { if (className === 'newfolder') { return; } @@ -1212,6 +1217,9 @@ define([ hide.push('removesf'); } } + if ($element.closest('[data-ro]').length) { + editable = false; + } }); if (paths.length > 1) { hide.push('restore'); @@ -1258,7 +1266,7 @@ define([ var filtered = []; show.forEach(function (className) { var $el = $contextMenu.find('.cp-app-drive-context-' + className); - if (!APP.editable && $el.is('.cp-app-drive-context-editable')) { return; } + if ((!APP.editable || !editable) && $el.is('.cp-app-drive-context-editable')) { return; } if (filter($el, className)) { return; } $el.parent('li').show(); filtered.push('.cp-app-drive-context-' + className); @@ -1665,6 +1673,13 @@ define([ $('.cp-app-drive-element-droppable').removeClass('cp-app-drive-element-droppable'); var data = ev.dataTransfer.getData("text"); + var newPath = findDropPath(ev.target); + if (!newPath) { return; } + var sfId = manager.isInSharedFolder(newPath); + if (sfId && folders[sfId] && folders[sfId].readOnly) { + return void UI.warn(Messages.fm_forbidden); + } + // Don't use the normal drop handler for file upload var fileDrop = ev.dataTransfer.files; if (fileDrop.length) { return void onFileDrop(fileDrop, ev); } @@ -1682,8 +1697,6 @@ define([ } }); - var newPath = findDropPath(ev.target); - if (!newPath) { return; } if (sharedF && manager.isPathIn(newPath, [TRASH])) { return void deletePaths(null, movedPaths); } @@ -1945,7 +1958,8 @@ define([ addFileData(element, $element); } $element.addClass(liClass); - addDragAndDropHandlers($element, newPath, isFolder, !isTrash); + var droppable = !isTrash && !APP.$content.data('readOnlyFolder'); + addDragAndDropHandlers($element, newPath, isFolder, droppable); $element.click(function(e) { e.stopPropagation(); onElementClick(e, $element); @@ -2786,6 +2800,7 @@ define([ return $container; }; var createGhostIcon = function ($list) { + if (APP.$content.data('readOnlyFolder')) { return; } var isInRoot = currentPath[0] === ROOT; var $element = $('
  • ', { 'class': 'cp-app-drive-element-row cp-app-drive-element-grid cp-app-drive-new-ghost' @@ -3209,6 +3224,7 @@ define([ if (!APP.editable) { debug("Read-only mode"); } if (!appStatus.isReady && !force) { return; } + // Fix path obvious issues if (!path || path.length === 0) { // Only Trash and Root are available in not-owned files manager if (!path || displayedCategories.indexOf(path[0]) === -1) { @@ -3226,7 +3242,7 @@ define([ path = [ROOT]; } - + // Get path data appStatus.ready(false); currentPath = path; var s = $content.scrollTop() || 0; @@ -3248,6 +3264,7 @@ define([ currentPath = path; } + // Make sure the path is valid var root = isVirtual ? undefined : manager.find(path); if (manager.isSharedFolder(root)) { // ANON_SHARED_FOLDER @@ -3270,6 +3287,7 @@ define([ } if (!isSearch) { delete APP.Search.oldLocation; } + // Display the tree and build the content APP.resetTree(); if (displayedCategories.indexOf(SEARCH) !== -1 && $tree.find('#cp-app-drive-tree-search-input').length) { // in history mode we want to focus the version number input @@ -3302,9 +3320,24 @@ define([ var $list = $('
      ').appendTo($dirContent); - // NewButton can be undefined if we're in read only mode - createNewButton(isInRoot, $toolbar.find('.cp-app-drive-toolbar-leftside')); var sfId = manager.isInSharedFolder(currentPath); + var readOnlyFolder = false; + if (APP.readOnly) { + // Read-only drive (team?) + $readOnly.show(); + } else if (folders[sfId] && folders[sfId].readOnly) { + // If readonly shared folder... + $readOnly.show(); + readOnlyFolder = true; + } else { + $readOnly.hide(); + } + $content.data('readOnlyFolder', readOnlyFolder); + + // NewButton can be undefined if we're in read only mode + if (!readOnlyFolder) { + createNewButton(isInRoot, $toolbar.find('.cp-app-drive-toolbar-leftside')); + } if (sfId) { var sfData = manager.getSharedFolderData(sfId); var parsed = Hash.parsePadUrl(sfData.href); @@ -3314,6 +3347,7 @@ define([ sframeChan.event('EV_DRIVE_SET_HASH', ''); } + createTitle($toolbar.find('.cp-app-drive-path'), path); if (APP.mobile()) { @@ -3524,6 +3558,7 @@ define([ var newPath = path.slice(); newPath.push(key); var isSharedFolder = manager.isSharedFolder(root[key]); + var sfId = manager.isInSharedFolder(newPath) || (isSharedFolder && root[key]); var $icon, isCurrentFolder, subfolder; if (isSharedFolder) { var fId = root[key]; @@ -3545,13 +3580,19 @@ define([ (isCurrentFolder ? $folderOpenedEmptyIcon : $folderEmptyIcon) : (isCurrentFolder ? $folderOpenedIcon : $folderIcon); } - var $element = createTreeElement(key, $icon.clone(), newPath, true, true, subfolder, isCurrentFolder, isSharedFolder); + var f = folders[sfId]; + var editable = !(f && f.readOnly); + var $element = createTreeElement(key, $icon.clone(), newPath, true, editable, + subfolder, isCurrentFolder, isSharedFolder); $element.appendTo($list); $element.find('>.cp-app-drive-element-row').contextmenu(openContextMenu('tree')); if (isSharedFolder) { $element.find('>.cp-app-drive-element-row') .addClass('cp-app-drive-element-sharedf'); } + if (sfId && !editable) { + $element.attr('data-ro', true); + } createTree($element, newPath); }); }; diff --git a/www/drive/inner.html b/www/drive/inner.html index 206744ea3..ce9e5afb4 100644 --- a/www/drive/inner.html +++ b/www/drive/inner.html @@ -16,6 +16,7 @@
      +
      diff --git a/www/drive/inner.js b/www/drive/inner.js index f5416a861..1f53baaf5 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -52,6 +52,7 @@ define([ }, waitFor(function (err, newObj) { folders[fId] = folders[fId] || {}; copyObjectValue(folders[fId], newObj); + folders[fId].readOnly = !secret.keys.secondaryKey; if (manager && oldIds.indexOf(fId) === -1) { manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey); } @@ -107,6 +108,7 @@ define([ })); SFCommon.create(waitFor(function (c) { common = c; })); }).nThen(function (waitFor) { + $('#cp-app-drive-connection-state').text(Messages.disconnected); var privReady = Util.once(waitFor()); var metadataMgr = common.getMetadataMgr(); if (JSON.stringify(metadataMgr.getPrivateData()) !== '{}') { diff --git a/www/teams/inner.js b/www/teams/inner.js index 859887c36..adc2b5014 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -63,6 +63,7 @@ define([ }, waitFor(function (err, newObj) { folders[fId] = folders[fId] || {}; copyObjectValue(folders[fId], newObj); + folders[fId].readOnly = !secret.keys.secondaryKey; if (manager && oldIds.indexOf(fId) === -1) { manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey); } @@ -275,6 +276,7 @@ define([ // Provide secondaryKey var teamData = (privateData.teams || {})[id] || {}; + driveAPP.readOnly = !teamData.secondaryKey; var drive = DriveUI.create(common, { proxy: proxy, folders: folders, @@ -461,6 +463,8 @@ define([ h('div#cp-app-drive-tree'), h('div#cp-app-drive-content-container', [ h('div#cp-app-drive-toolbar'), + h('div#cp-app-drive-connection-state', {style: "display: none;"}, Messages.disconnected), + h('div#cp-app-drive-edition-state', {style: "display: none;"}, Messages.readonly), h('div#cp-app-drive-content', {tabindex:2}) ]) ]) @@ -915,7 +919,6 @@ define([ var redrawTeam = function (common) { if (!APP.team) { return; } var teamId = APP.team; - var name = $('.cp-toolbar-title-value').text(); APP.module.execCommand('LIST_TEAMS', null, function (obj) { if (!obj) { return; } if (obj.error) { return void console.error(obj.error); } From 50829d3db17380eb90ea3b6270eb1bbd18e3b04f Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 14 Oct 2019 17:37:54 +0200 Subject: [PATCH 14/50] Share a folder with a read-only URL --- www/common/common-ui-elements.js | 106 +++++++++++++------------------ www/common/drive-ui.js | 44 +++++-------- www/common/outer/sharedfolder.js | 4 ++ www/common/outer/team.js | 1 + www/drive/inner.html | 3 +- www/drive/inner.js | 3 +- 6 files changed, 71 insertions(+), 90 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d1e5a87b3..41bcfd5f6 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -867,6 +867,7 @@ define([ // config.teamId only exists when we're trying to share a pad from a team drive // In this case, we don't want to share the pad with the current team if (config.teamId && config.teamId === id) { return; } + if (!teamsData[id].secondaryKey) { return; } var t = teamsData[id]; teams[t.edPublic] = { notifications: true, @@ -980,7 +981,7 @@ define([ var hashes = config.hashes; var common = config.common; - if (!hashes) { return; } + if (!hashes || (!hashes.editHash && !hashes.viewHash)) { return; } // Share link tab var hasFriends = Object.keys(config.friends || {}).length !== 0; @@ -988,7 +989,12 @@ define([ var friendsList = hasFriends ? createShareWithFriends(config, onFriendShare) : undefined; var friendsUIClass = hasFriends ? '.cp-share-columns' : ''; - var mainShareColumn = h('div.cp-share-column.contains-nav', [ + var content = []; + var sfContent = [ + h('label', Messages.sharedFolders_share), + h('br'), + ]; + var shareContent = [ h('label', Messages.share_linkAccess), h('br'), UI.createRadio('cp-share-editable', 'cp-share-editable-true', @@ -996,18 +1002,21 @@ define([ UI.createRadio('cp-share-editable', 'cp-share-editable-false', Messages.share_linkView, false, { mark: {tabindex:1} }), h('br'), + ]; + var padContent = [ h('label', Messages.share_linkOptions), h('br'), UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }), UI.createCheckbox('cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }), h('br'), - UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 }), - ]); + ]; + if (config.sharedFolder) { Array.prototype.push.apply(content, sfContent); } + Array.prototype.push.apply(content, shareContent); + if (!config.sharedFolder) { Array.prototype.push.apply(content, padContent); } + content.push(UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 })); + + var mainShareColumn = h('div.cp-share-column.contains-nav', content); var link = h('div.cp-share-modal' + friendsUIClass); - if (!hashes.editHash) { - $(link).find('#cp-share-editable-false').attr('checked', true); - $(link).find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true); - } var saveValue = function () { var edit = Util.isChecked($(link).find('#cp-share-editable-true')); var embed = Util.isChecked($(link).find('#cp-share-embed')); @@ -1046,21 +1055,32 @@ define([ if (success) { UI.log(Messages.shareSuccess); } }, keys: [13] - }, { - className: 'primary', - name: Messages.share_linkOpen, - onClick: function () { - saveValue(); - var v = getLinkValue(); - window.open(v); - }, - keys: [[13, 'ctrl']] }]; + if (!config.sharedFolder) { + shareButtons.push({ + className: 'primary', + name: Messages.share_linkOpen, + onClick: function () { + saveValue(); + var v = getLinkValue(); + window.open(v); + }, + keys: [[13, 'ctrl']] + }); + } var $link = $(link); $(mainShareColumn).append(UI.dialog.getButtons(shareButtons, config.onClose)).appendTo($link); $(friendsList).appendTo($link); + if (!hashes.editHash) { + $(link).find('#cp-share-editable-false').attr('checked', true); + $(link).find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true); + } else if (!hashes.viewHash) { + $(link).find('#cp-share-editable-false').removeAttr('checked').attr('disabled', true); + $(link).find('#cp-share-editable-true').attr('checked', true); + } + $(link).find('#cp-share-link-preview').val(getLinkValue()); $(link).find('input[type="radio"], input[type="checkbox"]').on('change', function () { $(link).find('#cp-share-link-preview').val(getLinkValue()); @@ -1122,7 +1142,7 @@ define([ } common.getAttribute(['general', 'share'], function (err, val) { val = val || {}; - if (val.edit === false || !hashes.editHash) { + if ((val.edit === false && hashes.viewHash) || !hashes.editHash) { $(link).find('#cp-share-editable-false').prop('checked', true); $(link).find('#cp-share-editable-true').prop('checked', false); } else { @@ -1131,12 +1151,17 @@ define([ } if (val.embed) { $(link).find('#cp-share-embed').prop('checked', true); } if (val.present) { $(link).find('#cp-share-present').prop('checked', true); } + if (config.sharedFolder) { + delete val.embed; + delete val.present; + } $(link).find('#cp-share-link-preview').val(getLinkValue(val)); }); common.getMetadataMgr().onChange(function () { // "hashes" is only available is the secure "share" app - hashes = common.getMetadataMgr().getPrivateData().hashes; - if (!hashes) { return; } + var _hashes = common.getMetadataMgr().getPrivateData().hashes; + if (!_hashes) { return; } + hashes = _hashes; $(link).find('#cp-share-link-preview').val(getLinkValue()); }); return tabs; @@ -1238,47 +1263,6 @@ define([ } return tabs; }; - UIElements.createSFShareModal = function (config) { - var origin = config.origin; - var pathname = config.pathname; - var hashes = config.hashes; - - if (!hashes.editHash) { throw new Error("You must provide a valid hash"); } - var url = origin + pathname + '#' + hashes.editHash; - - // Share link tab - var hasFriends = Object.keys(config.friends || {}).length !== 0; - var friendsList = hasFriends ? createShareWithFriends(config) : undefined; - var friendsUIClass = hasFriends ? '.cp-share-columns' : ''; - var mainShareColumn = h('div.cp-share-column.contains-nav', [ - h('div.cp-share-column', [ - h('label', Messages.sharedFolders_share), - h('br'), - hasFriends ? h('p', Messages.share_description) : undefined, - UI.dialog.selectable(url, { id: 'cp-share-link-preview', tabindex: 1 }) - ]) - ]); - var link = h('div.cp-share-modal' + friendsUIClass); - var linkButtons = [{ - className: 'cancel', - name: Messages.cancel, - onClick: function () {}, - keys: [27] - }]; - var shareButtons = [{ - className: 'primary', - name: Messages.share_linkCopy, - onClick: function () { - var success = Clipboard.copy(url); - if (success) { UI.log(Messages.shareSuccess); } - }, - keys: [13] - }]; - var $link = $(link); - $(mainShareColumn).append(UI.dialog.getButtons(shareButtons, config.onClose)).appendTo($link); - $(friendsList).appendTo($link); - return UI.dialog.customModal(link, {buttons: linkButtons}); - }; UIElements.createInviteTeamModal = function (config) { var common = config.common; diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 1badf239e..7e08a1a72 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -2523,24 +2523,28 @@ define([ $sharedIcon.clone().appendTo($shareBlock); $('').text(Messages.shareButton).appendTo($shareBlock); var data = manager.getSharedFolderData(id); - var parsed = Hash.parsePadUrl(data.href); - // XXX share modal shared folder read only - if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); } + var parsed = (data.href && data.href.indexOf('#') !== -1) ? Hash.parsePadUrl(data.href) : {}; + var roParsed = Hash.parsePadUrl(data.roHref) || {}; + if (!parsed.hash && !roParsed.hash) { return void console.error("Invalid href: "+(data.href || data.roHref)); } var friends = common.getFriends(); var teams = common.getMetadataMgr().getPrivateData().teams; var _wide = Object.keys(friends).length || Object.keys(teams).length; - var modal = UIElements.createSFShareModal({ + var ro = folders[id] && folders[id].version >= 2; + var modal = UIElements.createShareModal({ teamId: APP.team, origin: APP.origin, pathname: "/drive/", friends: friends, title: data.title, password: data.password, + sharedFolder: true, common: common, hashes: { - editHash: parsed.hash + editHash: parsed.hash, + viewHash: ro && roParsed.hash, } }); + modal = UI.dialog.tabs(modal); $shareBlock.click(function () { UI.openCustomModal(modal, { wide: _wide @@ -4018,25 +4022,7 @@ define([ var teams = common.getMetadataMgr().getPrivateData().teams; var _wide = Object.keys(friends).length || Object.keys(teams).length; - if (manager.isSharedFolder(el)) { - data = manager.getSharedFolderData(el); - parsed = Hash.parsePadUrl(data.href); - modal = UIElements.createSFShareModal({ - teamId: APP.team, - origin: APP.origin, - pathname: "/drive/", - friends: friends, - title: data.title, - common: common, - password: data.password, - hashes: { - editHash: parsed.hash - } - }); - return void UI.openCustomModal(modal, { - wide: _wide - }); - } else if (manager.isFolder(el)) { // Folder + if (manager.isFolder(el) && !manager.isSharedFolder(el)) { // Folder // if folder is inside SF return UI.warn('ERROR: Temporarily disabled'); // XXX CONVERT /*if (manager.isInSharedFolder(paths[0].path)) { @@ -4071,10 +4057,13 @@ define([ }); }*/ } else { // File - data = manager.getFileData(el); - parsed = Hash.parsePadUrl(data.href); + var sf = manager.isSharedFolder(el); + data = sf ? manager.getSharedFolderData(el) : manager.getFileData(el); + parsed = (data.href && data.href.indexOf('#') !== -1) ? Hash.parsePadUrl(data.href) : {}; var roParsed = Hash.parsePadUrl(data.roHref); var padType = parsed.type || roParsed.type; + var ro = !sf || (folders[el] && folders[el].version >= 2); + console.log(folders[el]); var padData = { teamId: APP.team, origin: APP.origin, @@ -4083,7 +4072,7 @@ define([ password: data.password, hashes: { editHash: parsed.hash, - viewHash: roParsed.hash, + viewHash: ro && roParsed.hash, fileHash: parsed.hash }, fileData: { @@ -4092,6 +4081,7 @@ define([ }, isTemplate: paths[0].path[0] === 'template', title: data.title, + sharedFolder: sf, common: common }; modal = padType === 'file' ? UIElements.createFileShareModal(padData) diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index c12900c19..d4883bd10 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -134,6 +134,10 @@ define([ }; var rt = sf.rt = Listmap.create(listmapConfig); rt.proxy.on('ready', function (info) { + if (!Object.keys(rt.proxy).length) { + // New Shared folder: no migration required + rt.proxy.version = 2; + } if (!sf.queue) { return; } diff --git a/www/common/outer/team.js b/www/common/outer/team.js index e297dee65..59b56c47e 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -531,6 +531,7 @@ define([ }; var lm = Listmap.create(config); var proxy = lm.proxy; + proxy.version = 2; // No migration needed proxy.on('ready', function () { // Store keys in our drive var keys = { diff --git a/www/drive/inner.html b/www/drive/inner.html index ce9e5afb4..abbb03414 100644 --- a/www/drive/inner.html +++ b/www/drive/inner.html @@ -16,7 +16,8 @@
      -
      + +
      diff --git a/www/drive/inner.js b/www/drive/inner.js index 1f53baaf5..5e2527600 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -109,6 +109,7 @@ define([ SFCommon.create(waitFor(function (c) { common = c; })); }).nThen(function (waitFor) { $('#cp-app-drive-connection-state').text(Messages.disconnected); + $('#cp-app-drive-edition-state').text(Messages.readonly); var privReady = Util.once(waitFor()); var metadataMgr = common.getMetadataMgr(); if (JSON.stringify(metadataMgr.getPrivateData()) !== '{}') { @@ -214,7 +215,7 @@ define([ }; // Add a "Burn this drive" button - if (!APP.loggedIn) { + if (!APP.loggedIn && !APP.readOnly) { APP.$burnThisDrive = common.createButton(null, true).click(function () { UI.confirm(Messages.fm_burnThisDrive, function (yes) { if (!yes) { return; } From 302030e1eed35ac07a6e95a5d7a1d986b86954eb Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 21 Oct 2019 15:22:59 +0200 Subject: [PATCH 15/50] Shared folder password change --- .../src/less2/include/alertify.less | 12 +- www/common/common-ui-elements.js | 6 +- www/common/cryptpad-common.js | 48 ++-- www/common/drive-ui.js | 80 ++++++- www/common/outer/async-store.js | 48 +++- www/common/outer/sharedfolder.js | 220 +++++++++++------- www/common/outer/store-rpc.js | 1 + www/common/outer/team.js | 34 ++- www/common/outer/userObject.js | 15 ++ www/common/proxy-manager.js | 77 +++++- www/common/sframe-common-outer.js | 2 +- www/common/userObject.js | 26 ++- www/drive/inner.js | 8 + 13 files changed, 446 insertions(+), 131 deletions(-) diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index bbc87f9c7..be4def5d7 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -116,7 +116,7 @@ }*/ } - .dialog, .alert { + .dialog { & > div { background-color: @alertify-dialog-bg; &.half { @@ -205,6 +205,16 @@ } } + ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: darken(@alertify-input-fg, 15%); + opacity: 1; /* Firefox */ + } + :-ms-input-placeholder { /* Internet Explorer 10-11 */ + color: darken(@alertify-input-fg, 15%); + } + ::-ms-input-placeholder { /* Microsoft Edge */ + color: darken(@alertify-input-fg, 15%); + } input:not(.form-control), textarea { background-color: @alertify-input-bg; color: @alertify-input-fg; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index cb270b3df..9d51f5500 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -548,6 +548,7 @@ define([ var sframeChan = common.getSframeChannel(); var changePwTitle = Messages.properties_changePassword; var changePwConfirm = Messages.properties_confirmChange; + var isSharedFolder = parsed.type === 'drive'; if (!hasPassword) { changePwTitle = Messages.properties_addPassword; changePwConfirm = Messages.properties_confirmNew; @@ -577,6 +578,7 @@ define([ password: newPass }, function (err, data) { if (err || data.error) { + console.error(err || data.error); return void UI.alert(Messages.properties_passwordError); } UI.findOKButton().click(); @@ -589,7 +591,9 @@ define([ }, {force: true}); } return void UI.alert(Messages.properties_passwordSuccess, function () { - common.gotoURL(hasPassword && newPass ? undefined : (data.href || data.roHref)); + if (!isSharedFolder) { + common.gotoURL(hasPassword && newPass ? undefined : (data.href || data.roHref)); + } }, {force: true}); }); }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 7f16fa860..5d65e92b6 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -867,13 +867,15 @@ define([ } var newHref = '/' + parsed.type + '/#' + newHash; + var isSharedFolder = parsed.type === 'drive'; + var optsGet = {}; var optsPut = { password: newPassword, - metadata: {} + metadata: {}, + initialState: isSharedFolder ? '{}' : undefined }; - Nthen(function (waitFor) { if (parsed.hashData && parsed.hashData.password) { common.getPadAttribute('password', waitFor(function (err, password) { @@ -933,7 +935,9 @@ define([ } var expire = oldMetadata.expire; - optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds + if (expire) { + optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds + } }).nThen(function (waitFor) { Crypt.get(parsed.hash, waitFor(function (err, val) { if (err) { @@ -948,23 +952,22 @@ define([ }), optsPut); }), optsGet); }).nThen(function (waitFor) { + if (isSharedFolder) { + postMessage("UPDATE_SHARED_FOLDER_PASSWORD", { + href: href, + oldChannel: oldChannel, + password: newPassword + }, waitFor(function (obj) { + console.error(obj); + })); + return; + } pad.leavePad({ channel: oldChannel }, waitFor()); pad.onDisconnectEvent.fire(true); }).nThen(function (waitFor) { - common.removeOwnedChannel({ - channel: oldChannel, - teamId: teamId - }, waitFor(function (obj) { - if (obj && obj.error) { - waitFor.abort(); - return void cb(obj); - } - })); - common.unpinPads([oldChannel], waitFor(), teamId); - common.pinPads([newSecret.channel], waitFor(), teamId); - }).nThen(function (waitFor) { + // Set the new password to our pad data common.setPadAttribute('password', newPassword, waitFor(function (err) { if (err) { warning = true; } }), href); @@ -981,6 +984,21 @@ define([ common.setPadAttribute('href', newHref, waitFor(function (err) { if (err) { warning = true; } }), href); + }).nThen(function (waitFor) { + // delete the old pad + common.removeOwnedChannel({ + channel: oldChannel, + teamId: teamId + }, waitFor(function (obj) { + if (obj && obj.error) { + waitFor.abort(); + return void cb(obj); + } + })); + if (!isSharedFolder) { + common.unpinPads([oldChannel], waitFor(), teamId); + common.pinPads([newSecret.channel], waitFor(), teamId); + } }).nThen(function () { cb({ warning: warning, diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 1561db904..551209fe3 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -3745,7 +3745,7 @@ define([ if (manager.isSharedFolder(el)) { delete data.roHref; //data.noPassword = true; - data.noEditPassword = true; + //data.noEditPassword = true; data.noExpiration = true; // this is here to allow users to check the channel id of a shared folder // we should remove it at some point @@ -4403,6 +4403,7 @@ define([ refresh(); UI.removeLoadingScreen(); + /* if (!APP.team) { sframeChan.query('Q_DRIVE_GETDELETED', null, function (err, data) { var ids = manager.findChannels(data); @@ -4417,6 +4418,83 @@ define([ UI.log(Messages._getKey('fm_deletedPads', [titles.join(', ')])); }); } + */ + var deprecated = files.sharedFoldersTemp; + var nt = nThen; + var passwordModal = function (fId, data, cb) { + var content = []; + var folderName = ''+ (data.lastTitlei || Messages.fm_newFolder) +''; + content.push(UI.setHTML(h('p'), Messages._getKey('drive_sfPassword', [folderName]))); + if (data.lastTitle) { + content.push(h('p', [ + Messages.fm_folderName, + ' ', + h('input', data.lastTitle) + ])); + } + var newPassword = UI.passwordInput({ + id: 'cp-app-prop-change-password', + placeholder: Messages.settings_changePasswordNew, + style: 'flex: 1;' + }); + var passwordOk = h('button', Messages.properties_changePasswordButton); + var changePass = h('span.cp-password-container', [ + newPassword, + passwordOk + ]); + content.push(changePass); + var div = h('div', content); + + var locked = false; + $(passwordOk).click(function () { + if (locked) { return; } + var pw = $(newPassword).find('.cp-password-input').val(); + locked = true; + $(div).find('.alert').remove(); + $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'})); + manager.restoreSharedFolder(fId, pw, function (err, obj) { + if (obj && obj.error) { + var wrong = h('div.alert.alert-danger', Messages.drive_sfPasswordError); + $(div).prepend(wrong); + $(passwordOk).text(Messages.properties_changePasswordButton); + locked = false; + return; + } + UI.findCancelButton($(div).closest('.alertify')).click(); + cb(); + }); + }); + var buttons = [{ + className: 'primary', + name: Messages.forgetButton, + onClick: function () { + manager.delete([['sharedFoldersTemp', fId]], function () { + }); + }, + keys: [] + }, { + className: 'cancel', + name: Messages.later, + onClick: function () {}, + keys: [27] + }]; + return UI.dialog.customModal(div, { + buttons: buttons, + onClose: cb + }); + }; + if (typeof (deprecated) === "object") { + Object.keys(deprecated).forEach(function (fId) { + nt = nt(function (waitFor) { + var data = deprecated[fId]; + UI.openCustomModal(passwordModal(fId, data, waitFor())); + }).nThen; + }); + nt(function () { + refresh(); + }); + } + return { refresh: refresh, diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 505a4cbc9..cf382caff 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -454,7 +454,7 @@ define([ Store.isNewChannel = function (clientId, data, cb) { if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); } - var channelId = Hash.hrefToHexChannelId(data.href, data.password); + var channelId = data.channel || Hash.hrefToHexChannelId(data.href, data.password); store.anon_rpc.send("IS_NEW_CHANNEL", channelId, function (e, response) { if (e) { return void cb({error: e}); } if (response && response.length && typeof(response[0]) === 'boolean') { @@ -1778,21 +1778,24 @@ define([ } }; }; - Store.loadSharedFolder = function (teamId, id, data, cb) { + Store.loadSharedFolder = function (teamId, id, data, cb, isNew) { var s = getStore(teamId); if (!s) { return void cb({ error: 'ENOTFOUND' }); } - var rt = SF.load({ + SF.load({ + isNew: isNew, network: store.network, - store: s + store: s, + isNewChannel: Store.isNewChannel }, id, data, cb); - return rt; }; - var loadSharedFolder = function (id, data, cb) { - Store.loadSharedFolder(null, id, data, cb); + var loadSharedFolder = function (id, data, cb, isNew) { + Store.loadSharedFolder(null, id, data, cb, isNew); }; Store.loadSharedFolderAnon = function (clientId, data, cb) { - Store.loadSharedFolder(null, data.id, data.data, function () { - cb(); + Store.loadSharedFolder(null, data.id, data.data, function (rt) { + cb({ + error: rt ? undefined : 'EDELETED' + }); }); }; Store.addSharedFolder = function (clientId, data, cb) { @@ -1805,6 +1808,9 @@ define([ cb(id); }); }; + Store.updateSharedFolderPassword = function (clientId, data, cb) { + SF.updatePassword(Store, data, store.network, cb); + }; // Drive Store.userObjectCommand = function (clientId, cmdData, cb) { @@ -1889,6 +1895,27 @@ define([ }); }; registerProxyEvents = function (proxy, fId) { + if (!fId) { + // Listen for shared folder password change + proxy.on('change', ['drive', UserObject.SHARED_FOLDERS], function (o, n, p) { + if (p.length > 3 && p[3] === 'password') { + var id = p[2]; + var data = proxy.drive[UserObject.SHARED_FOLDERS][id]; + var href = store.manager.user.userObject.getHref ? + store.manager.user.userObject.getHref(data) : data.href; + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets(parsed.type, parsed.hash, o); + SF.updatePassword({ + oldChannel: secret.channel, + password: n, + href: href + }, store.network, function () { + console.log('Shared folder password changed'); + }); + return false; + } + }); + } proxy.on('change', [], function (o, n, p) { if (fId) { // Pin the new pads @@ -2039,7 +2066,8 @@ define([ pin: pin, unpin: unpin, loadSharedFolder: loadSharedFolder, - settings: proxy.settings + settings: proxy.settings, + Store: Store }, { outer: true, removeOwnedChannel: function (channel, cb) { Store.removeOwnedChannel('', channel, cb); }, diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index 6d48ae915..1778db3f3 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -22,78 +22,104 @@ define([ SF.load = function (config, id, data, cb) { var network = config.network; var store = config.store; - var teamId = store.id || -1; + var isNew = config.isNew; + var isNewChannel = config.isNewChannel; + var teamId = store.id; var handler = store.handleSharedFolder; var parsed = Hash.parsePadUrl(data.href); var secret = Hash.getSecrets('drive', parsed.hash, data.password); - var sf = allSharedFolders[secret.channel]; - if (sf && sf.ready && sf.rt) { - // The shared folder is already loaded, return its data - setTimeout(function () { - var leave = function () { SF.leave(secret.channel, teamId); }; - store.manager.addProxy(id, sf.rt.proxy, leave); - cb(sf.rt, sf.metadata); - }); - sf.team.push(teamId); - if (handler) { handler(id, sf.rt); } - return sf.rt; - } - if (sf && sf.queue && sf.rt) { - // The shared folder is loading, add our callbacks to the queue - sf.queue.push({ - cb: cb, - store: store, - id: id - }); - sf.team.push(teamId); - if (handler) { handler(id, sf.rt); } - return sf.rt; - } - - sf = allSharedFolders[secret.channel] = { - queue: [{ - cb: cb, - store: store, - id: id - }], - team: [store.id || -1] - }; - - var owners = data.owners; - var listmapConfig = { - data: {}, - channel: secret.channel, - readOnly: false, - crypto: Crypto.createEncryptor(secret.keys), - userName: 'sharedFolder', - logLevel: 1, - ChainPad: ChainPad, - classic: true, - network: network, - metadata: { - validateKey: secret.keys.validateKey || undefined, - owners: owners + // If we try to load en existing shared folder (isNew === false) but this folder + // doesn't exist in the database, abort and cb + nThen(function (waitFor) { + isNewChannel(null, { channel: secret.channel }, waitFor(function (obj) { + if (obj.isNew && !isNew) { + store.manager.deprecateProxy(id, secret.channel); + waitFor.abort(); + return void cb(null); + } + })); + }).nThen(function () { + var sf = allSharedFolders[secret.channel]; + if (sf && sf.ready && sf.rt) { + // The shared folder is already loaded, return its data + setTimeout(function () { + var leave = function () { SF.leave(secret.channel, teamId); }; + store.manager.addProxy(id, sf.rt.proxy, leave); + cb(sf.rt, sf.metadata); + }); + sf.teams.push(store); + if (handler) { handler(id, sf.rt); } + return sf.rt; } - }; - var rt = sf.rt = Listmap.create(listmapConfig); - rt.proxy.on('ready', function (info) { - if (!sf.queue) { - return; + if (sf && sf.queue && sf.rt) { + // The shared folder is loading, add our callbacks to the queue + sf.queue.push({ + cb: cb, + store: store, + id: id + }); + sf.teams.push(store); + if (handler) { handler(id, sf.rt); } + return sf.rt; } - sf.queue.forEach(function (obj) { - var leave = function () { SF.leave(secret.channel, teamId); }; - obj.store.manager.addProxy(obj.id, rt.proxy, leave); - obj.cb(rt, info.metadata); + + sf = allSharedFolders[secret.channel] = { + queue: [{ + cb: cb, + store: store, + id: id + }], + teams: [store] + }; + + var owners = data.owners; + var listmapConfig = { + data: {}, + channel: secret.channel, + readOnly: false, + crypto: Crypto.createEncryptor(secret.keys), + userName: 'sharedFolder', + logLevel: 1, + ChainPad: ChainPad, + classic: true, + network: network, + metadata: { + validateKey: secret.keys.validateKey || undefined, + owners: owners + } + }; + var rt = sf.rt = Listmap.create(listmapConfig); + rt.proxy.on('ready', function (info) { + if (!sf.queue) { + return; + } + sf.queue.forEach(function (obj) { + var leave = function () { SF.leave(secret.channel, teamId); }; + obj.store.manager.addProxy(obj.id, rt.proxy, leave); + obj.cb(rt, info.metadata); + }); + sf.metadata = info.metadata; + sf.ready = true; + delete sf.queue; + }); + rt.proxy.on('error', function (info) { + if (info && info.error) { + if (info.error === "EDELETED" ) { + try { + // Deprecate the shared folder from each team + sf.teams.forEach(function (store) { + store.manager.deprecateProxy(id, secret.channel); + }); + } catch (e) {} + delete allSharedFolders[secret.channel]; + } + } }); - sf.leave = info.leave; - sf.metadata = info.metadata; - sf.ready = true; - delete sf.queue; + + if (handler) { handler(id, rt); } }); - if (handler) { handler(id, rt); } - return rt; }; SF.leave = function (channel, teamId) { @@ -101,8 +127,14 @@ define([ if (!sf) { return; } var clients = sf.teams; if (!Array.isArray(clients)) { return; } - var idx = clients.indexOf(teamId); - if (idx === -1) { return; } + var idx; + clients.some(function (store, i) { + if (store.id === teamId) { + idx = i; + return true; + } + }); + if (typeof (idx) === "undefined") { return; } // Remove the selected team clients.splice(idx, 1); @@ -113,6 +145,38 @@ define([ } }; + SF.updatePassword = function (Store, data, network, cb) { + var oldChannel = data.oldChannel; + var href = data.href; + var password = data.password; + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets(parsed.type, parsed.hash, password); + var sf = allSharedFolders[oldChannel]; + if (!sf) { return void cb({ error: 'ENOTFOUND' }); } + if (sf.rt && sf.rt.stop) { + sf.rt.stop(); + } + var nt = nThen; + sf.teams.forEach(function (s) { + nt = nt(function (waitFor) { + var sfId = s.manager.user.userObject.getSFIdFromHref(href); + var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; + if (!sfId || !shared[sfId]) { return; } + var sf = JSON.parse(JSON.stringify(shared[sfId])); + sf.password = password; + SF.load({ + network: network, + store: s, + isNewChannel: Store.isNewChannel + }, sfId, sf, waitFor()); + if (!s.rpc) { return; } + s.rpc.unpin([oldChannel], waitFor()); + s.rpc.pin([secret.channel], waitFor()); + }).nThen; + }); + nt(cb); + }; + /* loadSharedFolders load all shared folder stored in a given drive - store: user or team main store @@ -121,35 +185,13 @@ define([ */ SF.loadSharedFolders = function (Store, network, store, userObject, waitFor) { var shared = Util.find(store.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; - // Check if any of our shared folder is expired or deleted by its owner. - // If we don't check now, Listmap will create an empty proxy if it no longer exists on - // the server. nThen(function (waitFor) { - var checkExpired = Object.keys(shared).map(function (fId) { - return shared[fId].channel; - }); - Store.getDeletedPads(null, {list: checkExpired}, waitFor(function (chans) { - if (chans && chans.error) { return void console.error(chans.error); } - if (!Array.isArray(chans) || !chans.length) { return; } - var toDelete = []; - Object.keys(shared).forEach(function (fId) { - if (chans.indexOf(shared[fId].channel) !== -1 - && toDelete.indexOf(fId) === -1) { - toDelete.push(fId); - } - }); - toDelete.forEach(function (fId) { - var paths = userObject.findFile(Number(fId)); - userObject.delete(paths, waitFor(), true); - delete shared[fId]; - }); - })); - }).nThen(function (waitFor) { Object.keys(shared).forEach(function (id) { var sf = shared[id]; SF.load({ network: network, - store: store + store: store, + isNewChannel: Store.isNewChannel }, id, sf, waitFor()); }); }).nThen(waitFor()); diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index c75e69b70..9accc5f29 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -56,6 +56,7 @@ define([ ADD_SHARED_FOLDER: Store.addSharedFolder, LOAD_SHARED_FOLDER: Store.loadSharedFolderAnon, RESTORE_SHARED_FOLDER: Store.restoreSharedFolder, + UPDATE_SHARED_FOLDER_PASSWORD: Store.updateSharedFolderPassword, // Messaging ANSWER_FRIEND_REQUEST: Store.answerFriendRequest, SEND_FRIEND_REQUEST: Store.sendFriendRequest, diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 8769e5e7a..df367932d 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -31,6 +31,27 @@ define([ var registerChangeEvents = function (ctx, team, proxy, fId) { if (!team) { return; } + if (!fId) { + // Listen for shared folder password change + proxy.on('change', ['drive', UserObject.SHARED_FOLDERS], function (o, n, p) { + if (p.length > 3 && p[3] === 'password') { + var id = p[2]; + var data = proxy.drive[UserObject.SHARED_FOLDERS][id]; + var href = team.manager.user.userObject.getHref ? + team.manager.user.userObject.getHref(data) : data.href; + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets(parsed.type, parsed.hash, o); + SF.updatePassword(ctx.Store, { + oldChannel: secret.channel, + password: n, + href: href + }, ctx.store.network, function () { + console.log('Shared folder password changed'); + }); + return false; + } + }); + } proxy.on('change', [], function (o, n, p) { if (fId) { // Pin the new pads @@ -208,13 +229,13 @@ define([ }; })); }).nThen(function () { - var loadSharedFolder = function (id, data, cb) { + var loadSharedFolder = function (id, data, cb, isNew) { SF.load({ + isNew: isNew, network: ctx.store.network, - store: team - }, id, data, function (id, rt) { - cb(id, rt); - }); + store: team, + isNewChannel: ctx.Store.isNewChannel + }, id, data, cb); }; var manager = team.manager = ProxyManager.create(proxy.drive, { onSync: function (cb) { ctx.Store.onSync(id, cb); }, @@ -224,7 +245,8 @@ define([ loadSharedFolder: loadSharedFolder, settings: { drive: Util.find(ctx.store, ['proxy', 'settings', 'drive']) - } + }, + Store: ctx.Store }, { outer: true, removeOwnedChannel: function (channel, cb) { diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index d22d7d406..056beef28 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -28,6 +28,7 @@ define([ var TRASH = exp.TRASH; var TEMPLATE = exp.TEMPLATE; var SHARED_FOLDERS = exp.SHARED_FOLDERS; + var SHARED_FOLDERS_TEMP = exp.SHARED_FOLDERS_TEMP; var debug = exp.debug; @@ -74,6 +75,15 @@ define([ cb(null, id); }; + exp.deprecateSharedFolder = function (id) { + var data = files[SHARED_FOLDERS][id]; + if (!data) { return; } + files[SHARED_FOLDERS_TEMP][id] = JSON.parse(JSON.stringify(data)); + var paths = exp.findFile(Number(id)); + exp.delete(paths, null, true); + delete files[SHARED_FOLDERS][id]; + }; + // FILES DATA var spliceFileData = function (id) { delete files[FILES_DATA][id]; @@ -724,6 +734,10 @@ define([ var fixSharedFolders = function () { if (sharedFolder) { return; } if (typeof(files[SHARED_FOLDERS]) !== "object") { debug("SHARED_FOLDER was not an object"); files[SHARED_FOLDERS] = {}; } + if (typeof(files[SHARED_FOLDERS_TEMP]) !== "object") { + debug("SHARED_FOLDER_TEMP was not an object"); + files[SHARED_FOLDERS_TEMP] = {}; + } var sf = files[SHARED_FOLDERS]; var rootFiles = exp.getFiles([ROOT]); var root = exp.find([ROOT]); @@ -749,6 +763,7 @@ define([ } }; + var fixDrive = function () { Object.keys(files).forEach(function (key) { if (key.slice(0,1) === '/') { delete files[key]; } diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index ca914b57a..aae33e6d8 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -23,6 +23,12 @@ define([ // Only in outer userObject.fixFiles(); } + if (proxy.metadata && proxy.metadata.title) { + var sf = Env.user.proxy[UserObject.SHARED_FOLDERS][id]; + if (sf) { + sf.lastTitle = proxy.metadata.title; + } + } Env.folders[id] = { proxy: proxy, userObject: userObject, @@ -39,6 +45,12 @@ define([ delete Env.folders[id]; }; + // Password may have changed + var deprecateProxy = function (Env, id, channel) { + Env.unpinPads([channel], function () {}); + Env.user.userObject.deprecateSharedFolder(id); + }; + /* Tools */ @@ -447,6 +459,11 @@ define([ // 2b. load the proxy Env.loadSharedFolder(id, folderData, waitFor(function (rt, metadata) { + if (!rt) { + waitFor.abort(); + return void cb({ error: 'EDELETED' }); + } + if (!rt.proxy.metadata) { // Creating a new shared folder rt.proxy.metadata = { title: data.name || Messages.fm_newFolder }; } @@ -456,7 +473,7 @@ define([ if (metadata.owners) { fData.owners = metadata.owners; } if (metadata.expire) { fData.expire = +metadata.expire; } } - })); + }), !Boolean(data.folderData)); }).nThen(function () { Env.onSync(function () { cb(id); @@ -464,6 +481,42 @@ define([ }); }; + var _restoreSharedFolder = function (Env, _data, cb) { + var fId = _data.id; + var newPassword = _data.password; + var temp = Util.find(Env, ['user', 'proxy', UserObject.SHARED_FOLDERS_TEMP]); + var data = temp && temp[fId]; + if (!data) { return void cb({ error: 'EINVAL' }); } + if (!Env.Store) { return void cb({ error: 'ESTORE' }); } + var href = Env.user.userObject.getHref ? Env.user.userObject.getHref(data) : data.href; + var isNew = false; + nThen(function (waitFor) { + Env.Store.isNewChannel(null, { + href: href, + password: newPassword + }, waitFor(function (obj) { + if (!obj || obj.error) { + isNew = false; + return; + } + isNew = obj.isNew; + })); + }).nThen(function () { + if (isNew) { + return void cb({ error: 'ENOTFOUND' }); + } + data.password = newPassword; + _addSharedFolder(Env, { + path: ['root'], + folderData: data, + }, function () { + delete temp[fId]; + Env.onSync(cb); + }); + }); + + }; + // convert a folder to a Shared Folder var _convertFolderToSharedFolder = function (Env, data, cb) { return void cb({ @@ -570,6 +623,13 @@ define([ return void cb({error: 'E_NOTFOUND'}); } + // Deleted or password changed for a shared folder + if (data.paths.length === 1 && data.paths[0][0] === UserObject.SHARED_FOLDERS_TEMP) { + var temp = Util.find(Env, ['user', 'proxy', UserObject.SHARED_FOLDERS_TEMP]); + delete temp[data.paths[0][1]]; + return void Env.onSync(cb); + } + var toUnpin = []; var ownedRemoved; nThen(function (waitFor)  { @@ -666,6 +726,7 @@ define([ var el = Env.user.userObject.find(resolved.path); if (Env.user.userObject.isSharedFolder(el) && Env.folders[el]) { Env.folders[el].proxy.metadata.title = data.newName; + Env.user.proxy[UserObject.SHARED_FOLDERS][el].lastTitle = data.value; return void cb(); } } @@ -699,6 +760,8 @@ define([ _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': @@ -925,6 +988,7 @@ define([ pinPads: data.pin, unpinPads: data.unpin, onSync: data.onSync, + Store: data.Store, loadSharedFolder: data.loadSharedFolder, cfg: uoConfig, edPublic: data.edPublic, @@ -947,6 +1011,7 @@ define([ // Manager addProxy: callWithEnv(addProxy), removeProxy: callWithEnv(removeProxy), + deprecateProxy: callWithEnv(deprecateProxy), addSharedFolder: callWithEnv(_addSharedFolder), // Drive command: callWithEnv(onCommand), @@ -1016,6 +1081,15 @@ define([ } }, cb); }; + var restoreSharedFolderInner = function (Env, fId, password, cb) { + return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { + cmd: "restoreSharedFolder", + data: { + id: fId, + password: password + } + }, cb); + }; var convertFolderToSharedFolderInner = function (Env, path, owned, password, cb) { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "convertFolderToSharedFolder", @@ -1228,6 +1302,7 @@ define([ emptyTrash: callWithEnv(emptyTrashInner), addFolder: callWithEnv(addFolderInner), addSharedFolder: callWithEnv(addSharedFolderInner), + restoreSharedFolder: callWithEnv(restoreSharedFolderInner), convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner), delete: callWithEnv(deleteInner), restore: callWithEnv(restoreInner), diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index e919f1506..d14e89456 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -118,7 +118,7 @@ define([ msgEv.fire(msg); }); SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) { - sframeChan = sfc; + Utils.sframeChan = sframeChan = sfc; })); }); window.addEventListener('message', whenReady); diff --git a/www/common/userObject.js b/www/common/userObject.js index 7795f09b3..398ebe301 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -14,6 +14,7 @@ define([ var TRASH = module.TRASH = "trash"; var TEMPLATE = module.TEMPLATE = "template"; var SHARED_FOLDERS = module.SHARED_FOLDERS = "sharedFolders"; + var SHARED_FOLDERS_TEMP = module.SHARED_FOLDERS_TEMP = "sharedFoldersTemp"; // Maybe deleted or new password // Create untitled documents when no name is given var getLocaleDate = function () { @@ -45,6 +46,7 @@ define([ exp.TRASH = TRASH; exp.TEMPLATE = TEMPLATE; exp.SHARED_FOLDERS = SHARED_FOLDERS; + exp.SHARED_FOLDERS_TEMP = SHARED_FOLDERS_TEMP; var sharedFolder = exp.sharedFolder = config.sharedFolder; exp.id = config.id; @@ -379,11 +381,17 @@ define([ return Util.deduplicateString(ret); }; - var getIdFromHref = exp.getIdFromHref = function (href) { + var getIdFromHref = exp.getIdFromHref = function (_href) { var result; + var noPassword = function (str) { + if (!str) { return; } + var value = str.replace(/\/p\/?/, '/'); + return Hash.getRelativeHref(value); + }; + var href = noPassword(_href); getFiles([FILES_DATA]).some(function (id) { - if (files[FILES_DATA][id].href === href || - files[FILES_DATA][id].roHref === href) { + if (noPassword(files[FILES_DATA][id].href) === href || + noPassword(files[FILES_DATA][id].roHref) === href) { result = id; return true; } @@ -391,11 +399,17 @@ define([ return result; }; - exp.getSFIdFromHref = function (href) { + exp.getSFIdFromHref = function (_href) { var result; + var noPassword = function (str) { + if (!str) { return; } + var value = str.replace(/\/p\/?/, '/'); + return Hash.getRelativeHref(value); + }; + var href = noPassword(_href); getFiles([SHARED_FOLDERS]).some(function (id) { - if (files[SHARED_FOLDERS][id].href === href || - files[SHARED_FOLDERS][id].roHref === href) { + if (noPassword(files[SHARED_FOLDERS][id].href) === href || + noPassword(files[SHARED_FOLDERS][id].roHref) === href) { result = id; return true; } diff --git a/www/drive/inner.js b/www/drive/inner.js index 360944bbd..e75b09edd 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -44,6 +44,14 @@ define([ sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId }, waitFor(function (err, newObj) { + if (!APP.loggedIn && APP.newSharedFolder) { + if (!newObj || !Object.keys(newObj).length) { + // Empty anon drive: deleted + var msg = Messages.deletedError + '
      ' + Messages.errorRedirectToHome; + setTimeout(function () { UI.errorLoadingScreen(msg, false, function () {}); }); + APP.newSharedFolder = null; + } + } folders[fId] = folders[fId] || {}; copyObjectValue(folders[fId], newObj); if (manager && oldIds.indexOf(fId) === -1) { From 7d7a6923830ffaa9d500f14713a59e33e3315648 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 21 Oct 2019 17:12:36 +0200 Subject: [PATCH 16/50] Display a password prompt when an unprotected pad is not found on the server --- customize.dist/loading.js | 10 +++ www/common/notifications.js | 4 +- www/common/sframe-common-outer.js | 141 ++++++++++++++++-------------- www/common/sframe-common.js | 4 + 4 files changed, 92 insertions(+), 67 deletions(-) diff --git a/customize.dist/loading.js b/customize.dist/loading.js index 11a82f770..600f3e8d4 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -118,6 +118,16 @@ define([], function () { #cp-loading-password-prompt .cp-password-form button:hover { background-color: #326599; } +#cp-loading-password-prompt ::placeholder { + color: #d9d9d9; + opacity: 1; +} +#cp-loading-password-prompt :-ms-input-placeholder { + color: #d9d9d9; +} +#cp-loading-password-prompt ::-ms-input-placeholder { + color: #d9d9d9; +} #cp-loading .cp-loading-spinner-container { position: relative; height: 100px; diff --git a/www/common/notifications.js b/www/common/notifications.js index 1222e8602..ed44c2917 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -95,9 +95,7 @@ define([ if (msg.content.isTemplate) { common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor()); } - if (msg.content.password) { - common.sessionStorage.put('newPadPassword', msg.content.password, waitFor()); - } + common.sessionStorage.put('newPadPassword', msg.content.password || '', waitFor()); }).nThen(function () { todo(); }); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index d14e89456..87b477d5c 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -177,75 +177,88 @@ define([ Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; })); }; - // Prompt the password here if we have a hash containing /p/ - // or get it from the pad attributes - var needPassword = parsed.hashData && parsed.hashData.password; - if (needPassword) { - // Check if we have a password, and check if it is correct (file exists). - // It we don't have a correct password, display the password prompt. - // Maybe the file has been deleted from the server or the password has been changed. - Cryptpad.getPadAttribute('password', waitFor(function (err, val) { - var askPassword = function (wrongPasswordStored) { - // Ask for the password and check if the pad exists - // If the pad doesn't exist, it means the password isn't correct - // or the pad has been deleted - var correctPassword = waitFor(); - sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) { - password = data; - var next = function (e, isNew) { - if (Boolean(isNew)) { - // Ask again in the inner iframe - // We should receive a new Q_PAD_PASSWORD_VALUE - cb(false); + if (!parsed.hashData) { // No hash, no need to check for a password + return void todo(); + } + + // We now need to check if there is a password and if we know the correct password. + // We'll use getFileSize and isNewChannel to detect incorrect passwords. + + // First we'll get the password value from our drive (getPadAttribute), and we'll check + // if the channel is valid. If the pad is not stored in our drive, we'll test with an + // empty password instead. + + // If this initial check returns a valid channel, open the pad. + // If the channel is invalid: + // Option 1: this is a password-protected pad not stored in our drive --> password prompt + // Option 2: this is a pad stored in our drive + // 2a: 'edit' pad or file --> password-prompt + // 2b: 'view' pad no '/p/' --> the seed is incorrect + // 2c: 'view' pad and '/p/' and a wrong password stored --> the seed is incorrect + // 2d: 'view' pad and '/p/' and password never stored (security feature) --> password-prompt + + Cryptpad.getPadAttribute('password', waitFor(function (err, val) { + var askPassword = function (wrongPasswordStored) { + // Ask for the password and check if the pad exists + // If the pad doesn't exist, it means the password isn't correct + // or the pad has been deleted + var correctPassword = waitFor(); + sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) { + password = data; + var next = function (e, isNew) { + if (Boolean(isNew)) { + // Ask again in the inner iframe + // We should receive a new Q_PAD_PASSWORD_VALUE + cb(false); + } else { + todo(); + if (wrongPasswordStored) { + // Store the correct password + nThen(function (w) { + Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl()); + }).nThen(correctPassword); } else { - todo(); - if (wrongPasswordStored) { - // Store the correct password - Cryptpad.setPadAttribute('password', password, function () { - correctPassword(); - }, parsed.getUrl()); - } else { - correctPassword(); - } - cb(true); + correctPassword(); } - }; - if (parsed.type === "file") { - // `isNewChannel` doesn't work for files (not a channel) - // `getFileSize` is not adapted to channels because of metadata - Cryptpad.getFileSize(window.location.href, password, function (e, size) { - next(e, size === 0); - }); - return; + cb(true); } - // Not a file, so we can use `isNewChannel` - Cryptpad.isNewChannel(window.location.href, password, next); - }); - sframeChan.event("EV_PAD_PASSWORD"); - }; - - if (!val && sessionStorage.newPadPassword) { - val = sessionStorage.newPadPassword; - delete sessionStorage.newPadPassword; - } + }; + if (parsed.type === "file") { + // `isNewChannel` doesn't work for files (not a channel) + // `getFileSize` is not adapted to channels because of metadata + Cryptpad.getFileSize(window.location.href, password, function (e, size) { + next(e, size === 0); + }); + return; + } + // Not a file, so we can use `isNewChannel` + Cryptpad.isNewChannel(window.location.href, password, next); + }); + sframeChan.event("EV_PAD_PASSWORD"); + }; - if (val) { - password = val; - Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) { - if (size !== 0) { - return void todo(); - } - // Wrong password or deleted file? - askPassword(true); - })); - } else { - askPassword(); + if (!val && sessionStorage.newPadPassword) { + val = sessionStorage.newPadPassword; + delete sessionStorage.newPadPassword; + } + + password = val; + Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) { + if (size !== 0) { + return void todo(); } - }), parsed.getUrl()); - return; - } - // If no password, continue... - todo(); + if (parsed.hashData.mode === 'view' && (val || !parsed.hashData.password)) { + // Error, wrong password stored, the view seed has changed with the password + // password will never work + sframeChan.event("EV_PAD_PASSWORD_ERROR"); + waitFor.abort(); + return; + } + // Wrong password or deleted file? + askPassword(true); + })); + }), parsed.getUrl()); } }).nThen(function (waitFor) { if (cfg.afterSecrets) { diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 2f22a2b57..c73e43996 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -599,6 +599,10 @@ define([ UIElements.displayPasswordPrompt(funcs); }); + ctx.sframeChan.on("EV_PAD_PASSWORD_ERROR", function () { + UI.errorLoadingScreen(Messages.password_error_seed); + }); + ctx.sframeChan.on('EV_LOADING_INFO', function (data) { UI.updateLoadingProgress(data, 'drive'); }); From 51b1c19e77bbf97848e18e9042a26fe06d673fc1 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 21 Oct 2019 17:13:46 +0200 Subject: [PATCH 17/50] Fix shared folder password prompt UI --- www/common/drive-ui.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 551209fe3..298209284 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -4423,15 +4423,8 @@ define([ var nt = nThen; var passwordModal = function (fId, data, cb) { var content = []; - var folderName = ''+ (data.lastTitlei || Messages.fm_newFolder) +''; + var folderName = ''+ (data.lastTitle || Messages.fm_newFolder) +''; content.push(UI.setHTML(h('p'), Messages._getKey('drive_sfPassword', [folderName]))); - if (data.lastTitle) { - content.push(h('p', [ - Messages.fm_folderName, - ' ', - h('input', data.lastTitle) - ])); - } var newPassword = UI.passwordInput({ id: 'cp-app-prop-change-password', placeholder: Messages.settings_changePasswordNew, From efded1063f1a10c50c0b7344fc222893278f6231 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 21 Oct 2019 17:26:52 +0200 Subject: [PATCH 18/50] Fix shared folder duplication bug --- www/common/drive-ui.js | 9 ++++++--- www/common/outer/userObject.js | 20 ++++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 298209284..ec7245885 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -4461,8 +4461,7 @@ define([ className: 'primary', name: Messages.forgetButton, onClick: function () { - manager.delete([['sharedFoldersTemp', fId]], function () { - }); + manager.delete([['sharedFoldersTemp', fId]], function () { }); }, keys: [] }, { @@ -4478,8 +4477,12 @@ define([ }; if (typeof (deprecated) === "object") { Object.keys(deprecated).forEach(function (fId) { + var data = deprecated[fId]; + var sfId = manager.user.userObject.getSFIdFromHref(data.href); + if (folders[fId] || sfId) { // This shared folder is already stored in the drive... + return void manager.delete([['sharedFoldersTemp', fId]], function () { }); + } nt = nt(function (waitFor) { - var data = deprecated[fId]; UI.openCustomModal(passwordModal(fId, data, waitFor())); }).nThen; }); diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index 056beef28..1a60e867e 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -734,10 +734,6 @@ define([ var fixSharedFolders = function () { if (sharedFolder) { return; } if (typeof(files[SHARED_FOLDERS]) !== "object") { debug("SHARED_FOLDER was not an object"); files[SHARED_FOLDERS] = {}; } - if (typeof(files[SHARED_FOLDERS_TEMP]) !== "object") { - debug("SHARED_FOLDER_TEMP was not an object"); - files[SHARED_FOLDERS_TEMP] = {}; - } var sf = files[SHARED_FOLDERS]; var rootFiles = exp.getFiles([ROOT]); var root = exp.find([ROOT]); @@ -762,6 +758,21 @@ define([ } } }; + var fixSharedFoldersTemp = function () { + if (sharedFolder) { return; } + if (typeof(files[SHARED_FOLDERS_TEMP]) !== "object") { + debug("SHARED_FOLDER_TEMP was not an object"); + files[SHARED_FOLDERS_TEMP] = {}; + } + // Remove deprecated shared folder if they were already added back + var sft = files[SHARED_FOLDERS_TEMP]; + var sf = files[SHARED_FOLDERS]; + for (var id in sft) { + if (sf[id]) { + delete sft[id]; + } + } + }; var fixDrive = function () { @@ -776,6 +787,7 @@ define([ fixFilesData(); fixDrive(); fixSharedFolders(); + fixSharedFoldersTemp(); if (JSON.stringify(files) !== before) { debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely"); From 66908d15ccf5df9b92d22b2793175ead2a2bfade Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 21 Oct 2019 18:01:44 +0200 Subject: [PATCH 19/50] Make sure shared folders href are always encrypted when required --- www/common/proxy-manager.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 3ce2d59e9..76651cf2f 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -825,6 +825,9 @@ define([ if (!data.attr || !data.attr.trim()) { return void cb("E_INVAL_ATTR"); } var sfId = Env.user.userObject.getSFIdFromHref(data.href); if (sfId) { + if (data.attr === "href") { + data.value = Env.user.userObject.cryptor.encrypt(data.value); + } Env.user.proxy[UserObject.SHARED_FOLDERS][sfId][data.attr] = data.value; } var datas = findHref(Env, data.href); From c894351a3042aaac36be9b541c3362c99a6deb4e Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 11:21:41 +0200 Subject: [PATCH 20/50] Fix pinning issue causing unnecessary reset --- www/common/cryptpad-common.js | 6 ++++-- www/common/outer/async-store.js | 14 ++++++++++++++ www/common/outer/store-rpc.js | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 5d65e92b6..f0def2f9c 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -996,8 +996,10 @@ define([ } })); if (!isSharedFolder) { - common.unpinPads([oldChannel], waitFor(), teamId); - common.pinPads([newSecret.channel], waitFor(), teamId); + postMessage("CHANGE_PAD_PASSWORD_PIN", { + oldChannel: oldChannel, + channel: newSecret.channel + }, waitFor()); } }).nThen(function () { cb({ diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index cf382caff..1e419548a 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1547,6 +1547,20 @@ define([ channel.sendMessage(msg, clientId, cb); }; + // Unpin and pin the new channel in all team when changing a pad password + Store.changePadPasswordPin = function (clientId, data, cb) { + var oldChannel = data.oldChannel; + var channel = data.channel; + nThen(function (waitFor) { + getAllStores().forEach(function (s) { + var allData = s.manager.findChannel(channel); + if (!allData.length) { return; } + s.rpc.unpin([oldChannel], waitFor()); + s.rpc.pin([channel], waitFor()); + }); + }).nThen(cb); + }; + // requestPadAccess is used to check if we have a way to contact the owner // of the pad AND to send the request if we want // data.send === false ==> check if we can contact them diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 9accc5f29..2ef490876 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -79,6 +79,7 @@ define([ GIVE_PAD_ACCESS: Store.givePadAccess, GET_PAD_METADATA: Store.getPadMetadata, SET_PAD_METADATA: Store.setPadMetadata, + CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin, // Drive DRIVE_USEROBJECT: Store.userObjectCommand, // Settings, From 427cf836b11fe86333226a6980457f17865b91a5 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 12:04:30 +0200 Subject: [PATCH 21/50] Improve password change workflow --- www/common/drive-ui.js | 6 ++++++ www/common/outer/async-store.js | 11 ++++++++++- www/common/proxy-manager.js | 3 +++ www/common/sframe-common-outer.js | 24 ++++++++++++++++++++---- www/common/userObject.js | 8 ++++---- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index ec7245885..072f144fa 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -1857,6 +1857,12 @@ define([ $span.addClass('cp-app-drive-element-sharedf'); _addOwnership($span, $state, data); + var hrefData = Hash.parsePadUrl(data.href || data.roHref); + if (hrefData.hashData && hrefData.hashData.password) { + var $password = $passwordIcon.clone().appendTo($state); + $password.attr('title', Messages.fm_passwordProtected || ''); + } + var $shared = $sharedIcon.clone().appendTo($state); $shared.attr('title', Messages.fm_canBeShared); } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 1e419548a..eaa019384 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1919,7 +1919,7 @@ define([ store.manager.user.userObject.getHref(data) : data.href; var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets(parsed.type, parsed.hash, o); - SF.updatePassword({ + SF.updatePassword(Store, { oldChannel: secret.channel, password: n, href: href @@ -2063,6 +2063,15 @@ define([ /////////////////////// Init ///////////////////////////////////// ////////////////////////////////////////////////////////////////// + Store.refreshDriveUI = function () { + getAllStores().forEach(function (_s) { + var send = _s.id ? _s.sendEvent : sendDriveEvent; + send('DRIVE_CHANGE', { + path: ['drive', UserObject.FILES_DATA] + }); + }); + }; + var onReady = function (clientId, returned, cb) { var proxy = store.proxy; var unpin = function (data, cb) { diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index aae33e6d8..cf7f7d78b 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -49,6 +49,9 @@ define([ var deprecateProxy = function (Env, id, channel) { Env.unpinPads([channel], function () {}); Env.user.userObject.deprecateSharedFolder(id); + if (Env.Store && Env.Store.refreshDriveUI) { + Env.Store.refreshDriveUI(); + } }; /* diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 87b477d5c..84c08416a 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -215,8 +215,15 @@ define([ if (wrongPasswordStored) { // Store the correct password nThen(function (w) { + // XXX noPasswordStored: return; ? Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl()); + if (parsed.hashData.mode === 'edit') { + var href = window.location.pathname + '#' + Utils.Hash.getEditHashFromKeys(secret); + Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl()); + var roHref = window.location.pathname + '#' + Utils.Hash.getViewHashFromKeys(secret); + Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl()); + } }).nThen(correctPassword); } else { correctPassword(); @@ -244,10 +251,19 @@ define([ } password = val; - Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) { - if (size !== 0) { - return void todo(); - } + if (parsed.type === "file") { + // `isNewChannel` doesn't work for files (not a channel) + // `getFileSize` is not adapted to channels because of metadata + Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) { + if (size !== 0) { return void todo(); } + // Wrong password or deleted file? + askPassword(true); + })); + return; + } + // Not a file, so we can use `isNewChannel` + Cryptpad.isNewChannel(window.location.href, password, waitFor(function(e, isNew) { + if (!isNew) { return void todo(); } if (parsed.hashData.mode === 'view' && (val || !parsed.hashData.password)) { // Error, wrong password stored, the view seed has changed with the password // password will never work diff --git a/www/common/userObject.js b/www/common/userObject.js index 398ebe301..7a81e220f 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -385,8 +385,8 @@ define([ var result; var noPassword = function (str) { if (!str) { return; } - var value = str.replace(/\/p\/?/, '/'); - return Hash.getRelativeHref(value); + var parsed = Hash.parsePadUrl(str); + return parsed.getUrl().replace(/\/p\/?/, '/'); }; var href = noPassword(_href); getFiles([FILES_DATA]).some(function (id) { @@ -403,8 +403,8 @@ define([ var result; var noPassword = function (str) { if (!str) { return; } - var value = str.replace(/\/p\/?/, '/'); - return Hash.getRelativeHref(value); + var parsed = Hash.parsePadUrl(str); + return parsed.getUrl().replace(/\/p\/?/, '/'); }; var href = noPassword(_href); getFiles([SHARED_FOLDERS]).some(function (id) { From fa2ad4e476107fdd8e215ff859370bef0188aad0 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 13:06:21 +0200 Subject: [PATCH 22/50] Add /p/ to the hash when adding a password --- www/common/outer/async-store.js | 5 ++--- www/common/sframe-common-outer.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index eaa019384..cc94e9d25 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1531,8 +1531,7 @@ define([ Store.leavePad = function (clientId, data, cb) { var channel = channels[data.channel]; if (!channel || !channel.cpNf) { return void cb ({error: 'EINVAL'}); } - channel.cpNf.stop(); - delete channels[data.channel]; + Store.dropChannel(data.channel); cb(); }; Store.sendPadMsg = function (clientId, data, cb) { @@ -1850,7 +1849,7 @@ define([ // Clients management var driveEventClients = []; - var dropChannel = function (chanId) { + var dropChannel = Store.dropChannel = function (chanId) { try { store.messenger.leavePad(chanId); } catch (e) { console.error(e); } diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 84c08416a..431e274bb 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -174,7 +174,16 @@ define([ var parsed = Utils.Hash.parsePadUrl(window.location.href); var todo = function () { secret = Utils.secret = Utils.Hash.getSecrets(parsed.type, void 0, password); - Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; })); + Cryptpad.getShareHashes(secret, waitFor(function (err, h) { + hashes = h; + if (password && !parsed.hashData.password) { + var ohc = window.onhashchange; + window.onhashchange = function () {}; + window.location.hash = h.fileHash || h.editHash || h.viewHash || window.location.hash; + window.onhashchange = ohc; + ohc({reset: true}); + } + })); }; if (!parsed.hashData) { // No hash, no need to check for a password From e6267a89ff2cdb02bf85107a780e504fbb92d7ed Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 13:10:57 +0200 Subject: [PATCH 23/50] Remove XXX --- www/teams/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/teams/inner.js b/www/teams/inner.js index 6344653eb..bf5732340 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -726,7 +726,7 @@ define([ h('div', admins), h('h3', Messages.team_members), h('div', members), - h('h3', Messages.team_viewers || 'VIEWERS'), // XXX + h('h3', Messages.team_viewers || 'VIEWERS'), h('div', viewers), h('h3'+noPending, Messages.team_pending), h('div'+noPending, pending) From 550965b1c8367a5aaac49254087bf79f23f95262 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 15:22:43 +0200 Subject: [PATCH 24/50] Password change UI --- www/common/common-ui-elements.js | 8 ++- www/common/sframe-common-outer.js | 114 +++++++++++++++++------------- 2 files changed, 70 insertions(+), 52 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 9d51f5500..2f7261326 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -564,14 +564,18 @@ define([ newPassword, passwordOk ]); + var pLocked = false; $(passwordOk).click(function () { var newPass = $(newPassword).find('input').val(); if (data.password === newPass || (!data.password && !newPass)) { return void UI.alert(Messages.properties_passwordSame); } + if (pLocked) { return; } + pLocked = true; UI.confirm(changePwConfirm, function (yes) { - if (!yes) { return; } + if (!yes) { pLocked = false; return; } + $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'})); sframeChan.query("Q_PAD_PASSWORD_CHANGE", { teamId: typeof(owned) !== "boolean" ? owned : undefined, href: data.href || data.roHref, @@ -579,6 +583,8 @@ define([ }, function (err, data) { if (err || data.error) { console.error(err || data.error); + pLocked = false; + $(passwordOk).text(Messages.properties_changePasswordButton); return void UI.alert(Messages.properties_passwordError); } UI.findOKButton().click(); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 431e274bb..542b1b69b 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -206,64 +206,72 @@ define([ // 2c: 'view' pad and '/p/' and a wrong password stored --> the seed is incorrect // 2d: 'view' pad and '/p/' and password never stored (security feature) --> password-prompt - Cryptpad.getPadAttribute('password', waitFor(function (err, val) { - var askPassword = function (wrongPasswordStored) { - // Ask for the password and check if the pad exists - // If the pad doesn't exist, it means the password isn't correct - // or the pad has been deleted - var correctPassword = waitFor(); - sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) { - password = data; - var next = function (e, isNew) { - if (Boolean(isNew)) { - // Ask again in the inner iframe - // We should receive a new Q_PAD_PASSWORD_VALUE - cb(false); + var askPassword = function (wrongPasswordStored) { + // Ask for the password and check if the pad exists + // If the pad doesn't exist, it means the password isn't correct + // or the pad has been deleted + var correctPassword = waitFor(); + sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) { + password = data; + var next = function (e, isNew) { + if (Boolean(isNew)) { + // Ask again in the inner iframe + // We should receive a new Q_PAD_PASSWORD_VALUE + cb(false); + } else { + todo(); + if (wrongPasswordStored) { + // Store the correct password + nThen(function (w) { + // XXX noPasswordStored: return; ? + Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl()); + if (parsed.hashData.mode === 'edit') { + var href = window.location.pathname + '#' + Utils.Hash.getEditHashFromKeys(secret); + Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl()); + var roHref = window.location.pathname + '#' + Utils.Hash.getViewHashFromKeys(secret); + Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl()); + } + }).nThen(correctPassword); } else { - todo(); - if (wrongPasswordStored) { - // Store the correct password - nThen(function (w) { - // XXX noPasswordStored: return; ? - Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); - Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl()); - if (parsed.hashData.mode === 'edit') { - var href = window.location.pathname + '#' + Utils.Hash.getEditHashFromKeys(secret); - Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl()); - var roHref = window.location.pathname + '#' + Utils.Hash.getViewHashFromKeys(secret); - Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl()); - } - }).nThen(correctPassword); - } else { - correctPassword(); - } - cb(true); + correctPassword(); } - }; - if (parsed.type === "file") { - // `isNewChannel` doesn't work for files (not a channel) - // `getFileSize` is not adapted to channels because of metadata - Cryptpad.getFileSize(window.location.href, password, function (e, size) { - next(e, size === 0); - }); - return; + cb(true); } - // Not a file, so we can use `isNewChannel` - Cryptpad.isNewChannel(window.location.href, password, next); - }); - sframeChan.event("EV_PAD_PASSWORD"); - }; + }; + if (parsed.type === "file") { + // `isNewChannel` doesn't work for files (not a channel) + // `getFileSize` is not adapted to channels because of metadata + Cryptpad.getFileSize(window.location.href, password, function (e, size) { + next(e, size === 0); + }); + return; + } + // Not a file, so we can use `isNewChannel` + Cryptpad.isNewChannel(window.location.href, password, next); + }); + sframeChan.event("EV_PAD_PASSWORD"); + }; - if (!val && sessionStorage.newPadPassword) { - val = sessionStorage.newPadPassword; + var done = waitFor(); + var stored = false; + nThen(function (w) { + Cryptpad.getPadAttribute('title', w(function (err, data) { + stored = (!err && typeof (data) === "string"); + })); + Cryptpad.getPadAttribute('password', w(function (err, val) { + password = val; + }), parsed.getUrl()); + }).nThen(function (w) { + if (!password && sessionStorage.newPadPassword) { + password = sessionStorage.newPadPassword; delete sessionStorage.newPadPassword; } - password = val; if (parsed.type === "file") { // `isNewChannel` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata - Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) { + Cryptpad.getFileSize(window.location.href, password, w(function (e, size) { if (size !== 0) { return void todo(); } // Wrong password or deleted file? askPassword(true); @@ -271,19 +279,23 @@ define([ return; } // Not a file, so we can use `isNewChannel` - Cryptpad.isNewChannel(window.location.href, password, waitFor(function(e, isNew) { + Cryptpad.isNewChannel(window.location.href, password, w(function(e, isNew) { if (!isNew) { return void todo(); } - if (parsed.hashData.mode === 'view' && (val || !parsed.hashData.password)) { + if (parsed.hashData.mode === 'view' && (password || !parsed.hashData.password)) { // Error, wrong password stored, the view seed has changed with the password // password will never work sframeChan.event("EV_PAD_PASSWORD_ERROR"); waitFor.abort(); return; } + if (!stored && !parsed.hashData.password) { + // We've received a link without /p/ and it doesn't work without a password: abort + return void todo(); + } // Wrong password or deleted file? askPassword(true); })); - }), parsed.getUrl()); + }).nThen(done); } }).nThen(function (waitFor) { if (cfg.afterSecrets) { From e75e22fb10a5c71ce50c217e1e258f848f295525 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 15:45:31 +0200 Subject: [PATCH 25/50] Fix sessionStorage bug with pad password change --- www/common/sframe-common-outer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 542b1b69b..722de0607 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -263,7 +263,7 @@ define([ password = val; }), parsed.getUrl()); }).nThen(function (w) { - if (!password && sessionStorage.newPadPassword) { + if (!password && !stored && sessionStorage.newPadPassword) { password = sessionStorage.newPadPassword; delete sessionStorage.newPadPassword; } From a511fccdc2c8be9bc691fd819b1f542486d6f1ef Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 16:15:13 +0200 Subject: [PATCH 26/50] Don't log error when a friend's avatar has been deleted --- www/common/common-ui-elements.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 2f7261326..6bce00d8f 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2087,10 +2087,7 @@ define([ var cryptKey = Hash.encodeBase64(secret.keys && secret.keys.cryptKey); var src = origin + Hash.getBlobPathFromHex(hexFileName); common.getFileSize(hexFileName, function (e, data) { - if (e || !data) { - displayDefault(); - return void console.error(e || "404 avatar"); - } + if (e || !data) { return void displayDefault(); } if (typeof data !== "number") { return void displayDefault(); } if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); } var $img = $('').appendTo($container); From 126f10b782a8bd3a31637476d25ce95a22ecff4e Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 17:23:31 +0200 Subject: [PATCH 27/50] Fix shared folder password change issues --- www/common/cryptpad-common.js | 4 +-- www/common/drive-ui.js | 2 +- www/common/outer/sharedfolder.js | 44 ++++++++++++++++++++------------ www/common/proxy-manager.js | 5 ++++ www/teams/inner.js | 2 +- 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index f0def2f9c..827884db6 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -957,9 +957,7 @@ define([ href: href, oldChannel: oldChannel, password: newPassword - }, waitFor(function (obj) { - console.error(obj); - })); + }, waitFor()); return; } pad.leavePad({ diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 072f144fa..a48f922ee 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -4481,7 +4481,7 @@ define([ onClose: cb }); }; - if (typeof (deprecated) === "object") { + if (typeof (deprecated) === "object" && APP.editable) { Object.keys(deprecated).forEach(function (fId) { var data = deprecated[fId]; var sfId = manager.user.userObject.getSFIdFromHref(data.href); diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index 1778db3f3..7816029c3 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -49,29 +49,31 @@ define([ store.manager.addProxy(id, sf.rt.proxy, leave); cb(sf.rt, sf.metadata); }); - sf.teams.push(store); + sf.teams.push({ + cb: cb, + store: store, + id: id + }); if (handler) { handler(id, sf.rt); } - return sf.rt; + return; } - if (sf && sf.queue && sf.rt) { + if (sf && !sf.ready && sf.rt) { // The shared folder is loading, add our callbacks to the queue - sf.queue.push({ + sf.teams.push({ cb: cb, store: store, id: id }); - sf.teams.push(store); if (handler) { handler(id, sf.rt); } - return sf.rt; + return; } sf = allSharedFolders[secret.channel] = { - queue: [{ + teams: [{ cb: cb, store: store, id: id }], - teams: [store] }; var owners = data.owners; @@ -92,25 +94,27 @@ define([ }; var rt = sf.rt = Listmap.create(listmapConfig); rt.proxy.on('ready', function (info) { - if (!sf.queue) { + if (!sf.teams) { return; } - sf.queue.forEach(function (obj) { + sf.teams.forEach(function (obj) { var leave = function () { SF.leave(secret.channel, teamId); }; obj.store.manager.addProxy(obj.id, rt.proxy, leave); obj.cb(rt, info.metadata); }); sf.metadata = info.metadata; sf.ready = true; - delete sf.queue; }); rt.proxy.on('error', function (info) { if (info && info.error) { if (info.error === "EDELETED" ) { try { // Deprecate the shared folder from each team - sf.teams.forEach(function (store) { - store.manager.deprecateProxy(id, secret.channel); + // XXX We can't deprecate a read-only proxy: the read-only seed will change... + // We can only remove it + sf.teams.forEach(function (obj) { + console.log(obj.store.id, obj.store, obj.id); + obj.store.manager.deprecateProxy(obj.id, secret.channel); }); } catch (e) {} delete allSharedFolders[secret.channel]; @@ -128,8 +132,8 @@ define([ var clients = sf.teams; if (!Array.isArray(clients)) { return; } var idx; - clients.some(function (store, i) { - if (store.id === teamId) { + clients.some(function (obj, i) { + if (obj.store.id === teamId) { idx = i; return true; } @@ -145,6 +149,7 @@ define([ } }; + // Update the password locally SF.updatePassword = function (Store, data, network, cb) { var oldChannel = data.oldChannel; var href = data.href; @@ -157,13 +162,18 @@ define([ sf.rt.stop(); } var nt = nThen; - sf.teams.forEach(function (s) { + sf.teams.forEach(function (obj) { + // XXX if we're a viewer in this team, we can't update the keys nt = nt(function (waitFor) { - var sfId = s.manager.user.userObject.getSFIdFromHref(href); + var s = obj.store; + var sfId = obj.id; var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; if (!sfId || !shared[sfId]) { return; } var sf = JSON.parse(JSON.stringify(shared[sfId])); sf.password = password; + sf.channel = secret.channel; + sf.href = '/drive/#'+Hash.getEditHashFromKeys(secret); // XXX encrypt + sf.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); SF.load({ network: network, store: s, diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index cf7f7d78b..13515be5c 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -508,7 +508,12 @@ define([ if (isNew) { return void cb({ error: 'ENOTFOUND' }); } + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets(parsed.type, parsed.hash, newPassword); data.password = newPassword; + data.channel = secret.channel; + data.href = '/drive/#'+Hash.getEditHashFromKeys(secret); // XXX encrypt + data.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); _addSharedFolder(Env, { path: ['root'], folderData: data, diff --git a/www/teams/inner.js b/www/teams/inner.js index 40b410954..6c42429ca 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -374,7 +374,7 @@ define([ var isOwner = Object.keys(privateData.teams || {}).filter(function (id) { return privateData.teams[id].owner; - }).length >= Constants.MAX_TEAMS_OWNED; // && !privateData.devMode; + }).length >= Constants.MAX_TEAMS_OWNED && !privateData.devMode; var getWarningBox = function () { return h('div.alert.alert-warning', { From 0583ed3c8bc844668540d12a47aae48eeede3fca Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 22 Oct 2019 17:33:24 +0200 Subject: [PATCH 28/50] Owner can't demote themselves if there are no other owners --- www/teams/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/teams/inner.js b/www/teams/inner.js index 34358a70e..ebe06e248 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -587,7 +587,7 @@ define([ } todo(); }); - if (!(isMe && myRole === 2 && !otherOwners)) { + if (!(isMe && myRole === 3 && !otherOwners)) { $actions.append(demote); } } From f9723a6183647b3453600751bf91eb51b0788c04 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 24 Oct 2019 16:06:33 +0200 Subject: [PATCH 29/50] Fix read-only shared folders with password change --- www/common/cryptpad-common.js | 29 ++++++++++++++++------- www/common/outer/sharedfolder.js | 29 ++++++++++++++--------- www/common/outer/userObject.js | 11 ++++++--- www/common/proxy-manager.js | 10 +++++++- www/common/userObject.js | 40 ++++++++++++++++++++++++++++++-- 5 files changed, 93 insertions(+), 26 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index dab5fc89d..9ed46ce04 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -6,6 +6,7 @@ define([ '/common/common-messaging.js', '/common/common-constants.js', '/common/common-feedback.js', + '/common/userObject.js', '/common/outer/local-store.js', '/common/outer/worker-channel.js', '/common/outer/login-block.js', @@ -13,7 +14,7 @@ define([ '/customize/application_config.js', '/bower_components/nthen/index.js', ], function (Config, Messages, Util, Hash, - Messaging, Constants, Feedback, LocalStore, Channel, Block, + Messaging, Constants, Feedback, UserObject, LocalStore, Channel, Block, AppConfig, Nthen) { /* This file exposes functionality which is specific to Cryptpad, but not to @@ -158,7 +159,7 @@ define([ }); }; common.addSharedFolder = function (teamId, secret, cb) { - var href = secret.keys && secret.keys.editKeyStr ? '/drive/#' + Hash.getEditHashFromKeys(secret) : undefined; + var href = (secret.keys && secret.keys.editKeyStr) ? '/drive/#' + Hash.getEditHashFromKeys(secret) : undefined; postMessage("ADD_SHARED_FOLDER", { teamId: teamId, path: ['root'], @@ -878,6 +879,8 @@ define([ initialState: isSharedFolder ? '{}' : undefined }; + var cryptgetVal; + Nthen(function (waitFor) { if (parsed.hashData && parsed.hashData.password) { common.getPadAttribute('password', waitFor(function (err, password) { @@ -946,13 +949,22 @@ define([ waitFor.abort(); return void cb({ error: err }); } - Crypt.put(newHash, val, waitFor(function (err) { - if (err) { - waitFor.abort(); - return void cb({ error: err }); - } - }), optsPut); + cryptgetVal = val; + if (isSharedFolder) { + var parsed = JSON.parse(val || '{}'); + var oldKey = parsed.version === 2 && oldSecret.keys.secondaryKey; + var newKey = newSecret.keys.secondaryKey; + UserObject.reencrypt(oldKey, newKey, parsed); + cryptgetVal = JSON.stringify(parsed); + } }), optsGet); + }).nThen(function (waitFor) { + Crypt.put(newHash, cryptgetVal, waitFor(function (err) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + }), optsPut); }).nThen(function (waitFor) { if (isSharedFolder) { postMessage("UPDATE_SHARED_FOLDER_PASSWORD", { @@ -1514,7 +1526,6 @@ define([ noWorker = localStorage.CryptPad_noWorkers === '1'; console.error('WebWorker/SharedWorker state forced to ' + !noWorker); } - noWorker = true; Nthen(function (waitFor2) { if (Worker) { var w = waitFor2(); diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index 2ebb718ec..a118e04e3 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -116,6 +116,7 @@ define([ sf.teams.push({ cb: cb, store: store, + secondaryKey: secondaryKey, id: id }); if (handler) { handler(id, sf.rt); } @@ -126,16 +127,17 @@ define([ teams: [{ cb: cb, store: store, + secondaryKey: secondaryKey, id: id }], - readOnly: Boolean(secondaryKey) + readOnly: !Boolean(secondaryKey) }; var owners = data.owners; var listmapConfig = { data: {}, channel: secret.channel, - readOnly: Boolean(secondaryKey), + readOnly: !Boolean(secondaryKey), crypto: Crypto.createEncryptor(secret.keys), userName: 'sharedFolder', logLevel: 1, @@ -158,7 +160,7 @@ define([ } sf.teams.forEach(function (obj) { var leave = function () { SF.leave(secret.channel, teamId); }; - var uo = obj.store.manager.addProxy(obj.id, rt, leave, secondaryKey); + var uo = obj.store.manager.addProxy(obj.id, rt, leave, obj.secondaryKey); SF.checkMigration(secondaryKey, rt.proxy, uo, function () { obj.cb(sf.rt, info.metadata); }); @@ -171,10 +173,8 @@ define([ if (info.error === "EDELETED" ) { try { // Deprecate the shared folder from each team - // XXX We can't deprecate a read-only proxy: the read-only seed will change... - // We can only remove it + // We can only hide it sf.teams.forEach(function (obj) { - console.log(obj.store.id, obj.store, obj.id); obj.store.manager.deprecateProxy(obj.id, secret.channel); }); } catch (e) {} @@ -187,6 +187,7 @@ define([ }); }; + SF.upgrade = function (channel, secret) { var sf = allSharedFolders[channel]; if (!sf || !sf.readOnly) { return; } @@ -235,17 +236,21 @@ define([ } var nt = nThen; sf.teams.forEach(function (obj) { - // XXX if we're a viewer in this team, we can't update the keys nt = nt(function (waitFor) { var s = obj.store; var sfId = obj.id; + // We can't update the password of a shared folder in a read-only team + if (s.manager.user.userObject.readOnly) { + // Just deprecate the folder so that inner can stop displaying a folder no longer available + if (s.manager.folders[sfId]) { + s.manager.folders[sfId].proxy = { deprecated: true }; + } + return; + } var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; if (!sfId || !shared[sfId]) { return; } var sf = JSON.parse(JSON.stringify(shared[sfId])); sf.password = password; - sf.channel = secret.channel; - sf.href = '/drive/#'+Hash.getEditHashFromKeys(secret); // XXX encrypt - sf.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); SF.load({ network: network, store: s, @@ -256,7 +261,9 @@ define([ s.rpc.pin([secret.channel], waitFor()); }).nThen; }); - nt(cb); + nt(function () { + cb(); + }); }; /* loadSharedFolders diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index 8cdbae7b4..55fe8014d 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -71,19 +71,21 @@ define([ cb(null, clone(data[attr])); }; - exp.pushData = function (data, cb) { + exp.pushData = function (_data, cb) { if (typeof cb !== "function") { cb = function () {}; } if (readOnly) { return void cb('EFORBIDDEN'); } var id = Util.createRandomInteger(); + var data = clone(_data); // If we were given an edit link, encrypt its value if needed if (data.href) { data.href = exp.cryptor.encrypt(data.href); } files[FILES_DATA][id] = data; cb(null, id); }; - exp.pushSharedFolder = function (data, cb) { + exp.pushSharedFolder = function (_data, cb) { if (typeof cb !== "function") { cb = function () {}; } if (readOnly) { return void cb('EFORBIDDEN'); } + var data = clone(_data); // Check if we already have this shared folder in our drive var exists; @@ -476,6 +478,9 @@ define([ files.migrateRo = 1; var next = function () { var copy = JSON.parse(JSON.stringify(files)); + exp.reencrypt(null, config.editKey, copy); + // XXX test migration again + /* Object.keys(copy[FILES_DATA]).forEach(function (id) { var data = copy[FILES_DATA][id] || {}; // If this pad has a visible href, encrypt it @@ -490,7 +495,7 @@ define([ if (data.href && data.roHref && !data.fileType && data.href.indexOf('#') !== -1) { data.href = exp.cryptor.encrypt(data.href); } - }); + });*/ copy.version = 2; delete copy.migrateRo; diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 788c59fae..bcbd240fb 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -52,6 +52,13 @@ define([ // Password may have changed var deprecateProxy = function (Env, id, channel) { + if (Env.user.userObject.readOnly) { + // In a read-only team, we can't deprecate a shared folder + if (Env.folders[id]) { + Env.folders[id].proxy = { deprecated: true }; + } + return void Env.Store.refreshDriveUI(); + } Env.unpinPads([channel], function () {}); Env.user.userObject.deprecateSharedFolder(id); if (Env.Store && Env.Store.refreshDriveUI) { @@ -554,7 +561,8 @@ define([ var secret = Hash.getSecrets(parsed.type, parsed.hash, newPassword); data.password = newPassword; data.channel = secret.channel; - data.href = '/drive/#'+Hash.getEditHashFromKeys(secret); // XXX encrypt + var _href = '/drive/#'+Hash.getEditHashFromKeys(secret); + data.href = Env.user.userObject.cryptor.encrypt(_href); data.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); _addSharedFolder(Env, { path: ['root'], diff --git a/www/common/userObject.js b/www/common/userObject.js index bdfff2caa..74ed702d7 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -15,6 +15,8 @@ define([ var TEMPLATE = module.TEMPLATE = "template"; var SHARED_FOLDERS = module.SHARED_FOLDERS = "sharedFolders"; 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; // Create untitled documents when no name is given var getLocaleDate = function () { @@ -66,6 +68,37 @@ define([ return pad.roHref; }; + module.reencrypt = function (oldKey, newKey, obj) { + obj = obj || {}; + var oldCryptor = createCryptor(oldKey); + var newCryptor = createCryptor(newKey); + Object.keys(obj[FILES_DATA]).forEach(function (id) { + var data = obj[FILES_DATA][id] || {}; + // If this pad has a visible href, encrypt it + // "&& data.roHref" is here to make sure this is not a "file" + if (data.href && data.roHref && !data.fileType) { + var _href = oldCryptor.decrypt(data.href); + data.href = newCryptor.encrypt(_href); + } + }); + Object.keys(obj[SHARED_FOLDERS] || {}).forEach(function (id) { + var data = obj[SHARED_FOLDERS][id] || {}; + // If this folder has a visible href, encrypt it + if (data.href) { + var _href = oldCryptor.decrypt(data.href); + data.href = newCryptor.encrypt(_href); + } + }); + Object.keys(obj[SHARED_FOLDERS_TEMP] || {}).forEach(function (id) { + var data = obj[SHARED_FOLDERS_TEMP][id] || {}; + // If this folder has a visible href, encrypt it + if (data.href) { + var _href = oldCryptor.decrypt(data.href); + data.href = newCryptor.encrypt(_href); + } + }); + }; + module.init = function (files, config) { var exp = {}; @@ -74,18 +107,19 @@ define([ exp.setReadOnly = function (state, key) { config.editKey = key; createCryptor(key); + exp.readOnly = state; if (exp._setReadOnly) { // Change outer exp._setReadOnly(state); } }; + exp.readOnly = config.readOnly; + exp.reencrypt = module.reencrypt; exp.getDefaultName = module.getDefaultName; var sframeChan = config.sframeChan; - var FILES_DATA = module.FILES_DATA = exp.FILES_DATA = Constants.storageKey; - var OLD_FILES_DATA = module.OLD_FILES_DATA = exp.OLD_FILES_DATA = Constants.oldStorageKey; var NEW_FOLDER_NAME = Messages.fm_newFolder || 'New folder'; var NEW_FILE_NAME = Messages.fm_newFile || 'New file'; @@ -95,6 +129,8 @@ define([ exp.TEMPLATE = TEMPLATE; exp.SHARED_FOLDERS = SHARED_FOLDERS; exp.SHARED_FOLDERS_TEMP = SHARED_FOLDERS_TEMP; + exp.FILES_DATA = FILES_DATA; + exp.OLD_FILES_DATA = OLD_FILES_DATA; var sharedFolder = exp.sharedFolder = config.sharedFolder; exp.id = config.id; From 37a72d2f87db844463ee2e127ff410a47486fd10 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 25 Oct 2019 16:36:00 +0200 Subject: [PATCH 30/50] Fix more issues when changing password of a readonly folder --- www/common/common-ui-elements.js | 2 +- www/common/cryptpad-common.js | 1 + www/common/drive-ui.js | 3 ++- www/common/outer/sharedfolder.js | 16 +++++++--------- www/common/outer/team.js | 18 ++++++++++++------ www/common/proxy-manager.js | 12 +++++++----- www/common/userObject.js | 3 ++- www/teams/inner.js | 3 +++ 8 files changed, 35 insertions(+), 23 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6df68a3f9..d097994ca 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -629,7 +629,7 @@ define([ }; var getPadProperties = function (common, data, cb) { var $d = $('
      '); - if (!data || (!data.href && !data.roHref)) { return void cb(void 0, $d); } + if (!data) { return void cb(void 0, $d); } if (data.href) { $('