diff --git a/lib/archive-account.js b/lib/archive-account.js index 085b4878c..f3ce033e8 100644 --- a/lib/archive-account.js +++ b/lib/archive-account.js @@ -94,8 +94,13 @@ const init = (cb) => { }).nThen(cb); }; -COMMANDS.start = (edPublic, blockId) => { +COMMANDS.start = (edPublic, blockId, reason) => { const safeKey = Util.escapeKeyCharacters(edPublic); + const archiveReason = { + code: 'MODERATION_ACCOUNT', + txt: reason + }; + let ref = {}; let blobsToArchive = []; let channelsToArchive = []; @@ -142,7 +147,7 @@ COMMANDS.start = (edPublic, blockId) => { // Archive the pads channelsToArchive.forEach((chanId) => { n = n((w) => { - Env.store.archiveChannel(chanId, w(function (err) { + Env.store.archiveChannel(chanId, archiveReason, w(function (err) { if (err) { return Log.error('MODERATION_CHANNEL_ARCHIVAL_ERROR', { error: err, @@ -157,7 +162,7 @@ COMMANDS.start = (edPublic, blockId) => { // Archive the blobs blobsToArchive.forEach((blobId) => { n = n((w) => { - Env.blobStore.archive.blob(blobId, w(function (err) { + Env.blobStore.archive.blob(blobId, archiveReason, w(function (err) { if (err) { return Log.error('MODERATION_BLOB_ARCHIVAL_ERROR', { error: err, @@ -171,7 +176,7 @@ COMMANDS.start = (edPublic, blockId) => { }); n(waitFor(() => { // Archive the pin log - Env.pinStore.archiveChannel(safeKey, waitFor(function (err) { + Env.pinStore.archiveChannel(safeKey, undefined, waitFor(function (err) { if (err) { return Log.error('MODERATION_ACCOUNT_PIN_LOG', err, waitFor()); } diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index 0daf2c38d..765257975 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -175,20 +175,26 @@ var archiveDocument = function (Env, Server, cb, data) { if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } + const archiveReason = { + code: 'MODERATION_PAD', + txt: reason + }; + const reasonStr = `MODERATION_PAD:${reason}`; + switch (id.length) { case 32: - return void Env.msgStore.archiveChannel(id, Util.both(cb, function (err) { + return void Env.msgStore.archiveChannel(id, archiveReason, Util.both(cb, function (err) { Env.Log.info("ARCHIVAL_CHANNEL_BY_ADMIN_RPC", { channelId: id, reason: reason, status: err? String(err): "SUCCESS", }); - Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', err => { + Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', reasonStr, err => { if (err) { } // TODO }); })); case 48: - return void Env.blobStore.archive.blob(id, Util.both(cb, function (err) { + return void Env.blobStore.archive.blob(id, archiveReason, Util.both(cb, function (err) { Env.Log.info("ARCHIVAL_BLOB_BY_ADMIN_RPC", { id: id, reason: reason, @@ -213,7 +219,7 @@ var removeDocument = function (Env, Server, cb, data) { id = args; } else if (args && typeof(args) === 'object') { id = args.id; - reason = args.reason; + reason = `MODERATION_DESTROY:${args.reason}`; } if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } @@ -226,7 +232,7 @@ var removeDocument = function (Env, Server, cb, data) { reason: reason, status: err? String(err): "SUCCESS", }); - Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', err => { + Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', reason, err => { if (err) { } // TODO }); })); @@ -291,17 +297,20 @@ var archiveAccount = function (Env, Server, _cb, data) { return worker.postMessage({ command: 'start', content: data && data[1], - block: data && data[2] + block: data && data[2], + reason: data && data[3] }); } + // DONE: disconnect all users from these channels + const reason = `MODERATION_ACCOUNT:${data && data[3]}`; var deletedChannels = Util.tryParse(message); if (Array.isArray(deletedChannels)) { let n = nThen; deletedChannels.forEach((chanId) => { n = n((w) => { setTimeout(w(() => { - Channel.disconnectChannelMembers(Env, Server, chanId, 'EDELETED', () => {}); + Channel.disconnectChannelMembers(Env, Server, chanId, 'EDELETED', reason, () => {}); }), 10); }).nThen; }); @@ -645,7 +654,7 @@ var archivePinLog = function (Env, Server, cb, data) { if (!isValidKey(key)) { return void cb("EINVAL"); } var safeKey = Util.escapeKeyCharacters(key); - Env.pinStore.archiveChannel(safeKey, function (err) { + Env.pinStore.archiveChannel(safeKey, undefined, function (err) { Core.expireSession(Env.Sessions, safeKey); if (err) { Env.Log.error('ARCHIVE_PIN_LOG_BY_ADMIN', { diff --git a/lib/commands/channel.js b/lib/commands/channel.js index 55ef88995..c3d354881 100644 --- a/lib/commands/channel.js +++ b/lib/commands/channel.js @@ -8,7 +8,7 @@ const Metadata = require("./metadata"); const HK = require("../hk-util"); const Nacl = require("tweetnacl/nacl-fast"); -Channel.disconnectChannelMembers = function (Env, Server, channelId, code, cb) { +Channel.disconnectChannelMembers = function (Env, Server, channelId, code, reason, cb) { var done = Util.once(Util.mkAsync(cb)); if (!Core.isValidId(channelId)) { return done('INVALID_ID'); } @@ -39,6 +39,7 @@ Channel.disconnectChannelMembers = function (Env, Server, channelId, code, cb) { userId, JSON.stringify({ error: code, //'EDELETED', + message: reason, channel: channelId, }) ], w()); @@ -52,6 +53,7 @@ Channel.disconnectChannelMembers = function (Env, Server, channelId, code, cb) { Env.Log.warn('DISCONNECT_CHANNEL_MEMBERS_TIMEOUT', { channelId, code, + reason }); clear(); done(); @@ -105,9 +107,10 @@ Channel.clearOwnedChannel = function (Env, safeKey, channelId, cb, Server) { }); }; -var archiveOwnedChannel = function (Env, safeKey, channelId, __cb, Server) { +var archiveOwnedChannel = function (Env, safeKey, channelId, reason, __cb, Server) { var _cb = Util.once(Util.mkAsync(__cb)); var unsafeKey = Util.unescapeKeyCharacters(safeKey); + reason = reason || 'ARCHIVE_OWNED'; nThen(function (w) { // confirm that the channel exists before worrying about whether // we have permission to delete it. @@ -130,7 +133,7 @@ var archiveOwnedChannel = function (Env, safeKey, channelId, __cb, Server) { }).nThen(function () { var cb = _cb; // temporarily archive the file - return void Env.msgStore.archiveChannel(channelId, function (e) { + return void Env.msgStore.archiveChannel(channelId, reason, function (e) { Env.Log.info('ARCHIVAL_CHANNEL_BY_OWNER_RPC', { unsafeKey: unsafeKey, channelId: channelId, @@ -141,16 +144,19 @@ var archiveOwnedChannel = function (Env, safeKey, channelId, __cb, Server) { } cb(void 0, 'OK'); - Channel.disconnectChannelMembers(Env, Server, channelId, 'EDELETED', err => { + Channel.disconnectChannelMembers(Env, Server, channelId, 'EDELETED', reason, err => { if (err) { } // TODO }); }); }); }; -Channel.removeOwnedChannel = function (Env, safeKey, channelId, __cb, Server) { +Channel.removeOwnedChannel = function (Env, safeKey, obj, __cb, Server) { var _cb = Util.once(Util.mkAsync(__cb)); + var channelId = obj.channel; + var reason = obj.reason; + if (typeof(channelId) !== 'string' || !Core.isValidId(channelId)) { return _cb('INVALID_ARGUMENTS'); } @@ -160,9 +166,9 @@ Channel.removeOwnedChannel = function (Env, safeKey, channelId, __cb, Server) { Env.queueDeletes(safeKey, function (next) { var cb = Util.both(_cb, next); if (Env.blobStore.isFileId(channelId)) { - return void Env.removeOwnedBlob(channelId, safeKey, cb); + return void Env.removeOwnedBlob(channelId, safeKey, reason, cb); } - archiveOwnedChannel(Env, safeKey, channelId, cb, Server); + archiveOwnedChannel(Env, safeKey, channelId, reason, cb, Server); }); }; @@ -258,15 +264,18 @@ Channel.isNewChannel = function (Env, channel, cb) { var msg = msgObj.buff.toString('utf8'); if (typeof(msg) === 'string' && ARRAY_LINE.test(msg)) { abort(); - return void cb(void 0, false); + return void cb(void 0, {isNew: false}); } } catch (e) { Env.WARN('invalid message read from store', e); } readMore(); - }, function () { + }, function (err, reason) { // no more messages... - cb(void 0, true); + cb(void 0, { + isNew: true, + reason: reason + }); }); }; diff --git a/lib/commands/pin-rpc.js b/lib/commands/pin-rpc.js index cad040e4c..1540c9843 100644 --- a/lib/commands/pin-rpc.js +++ b/lib/commands/pin-rpc.js @@ -115,7 +115,7 @@ Pinning.getTotalSize = function (Env, safeKey, cb) { */ Pinning.removePins = function (Env, safeKey, cb) { // FIXME respect the queue - Env.pinStore.archiveChannel(safeKey, function (err) { + Env.pinStore.archiveChannel(safeKey, undefined, function (err) { Core.expireSession(Env.Sessions, safeKey); Env.Log.info('ARCHIVAL_PIN_BY_OWNER_RPC', { safeKey: safeKey, diff --git a/lib/eviction.js b/lib/eviction.js index 9cf6a5558..16b456078 100644 --- a/lib/eviction.js +++ b/lib/eviction.js @@ -540,7 +540,7 @@ module.exports = function (Env, cb) { } // remove the pin logs of inactive accounts if inactive account removal is configured - pinStore.archiveChannel(id, function (err) { + pinStore.archiveChannel(id, undefined, function (err) { if (err) { return Log.error('EVICT_INACTIVE_ACCOUNT_PIN_LOG', err, next); } @@ -599,7 +599,7 @@ module.exports = function (Env, cb) { if (item.mtime > inactiveTime) { return void next(); } removed++; - blobs.archive.blob(item.blobId, function (err) { + blobs.archive.blob(item.blobId, 'INACTIVE', function (err) { if (err) { return Log.error("EVICT_ARCHIVE_BLOB_ERROR", { error: err, @@ -724,7 +724,7 @@ module.exports = function (Env, cb) { // else fall through to the archival })); }).nThen(function (w) { - return void store.archiveChannel(item.channel, w(function (err) { + return void store.archiveChannel(item.channel, 'INACTIVE', w(function (err) { if (err) { Log.error('EVICT_CHANNEL_ARCHIVAL_ERROR', { error: err, diff --git a/lib/hk-util.js b/lib/hk-util.js index 455964ccb..81fc4c16e 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -124,7 +124,7 @@ var CHECKPOINT_PATTERN = /^cp\|(([A-Za-z0-9+\/=]+)\|)?/; but for some reason are still present */ const expireChannel = HK.expireChannel = function (Env, channel) { - return void Env.store.archiveChannel(channel, function (err) { + return void Env.store.archiveChannel(channel, 'EXPIRED', function (err) { Env.Log.info("ARCHIVAL_CHANNEL_BY_HISTORY_KEEPER_EXPIRATION", { channelId: channel, status: err? String(err): "SUCCESS", @@ -526,7 +526,12 @@ const getHistoryAsync = (Env, channelName, lastKnownHash, beforeHash, handler, c getHistoryOffset(Env, channelName, lastKnownHash, waitFor((err, os) => { if (err) { waitFor.abort(); - return void cb(err); + var reason; + if (err && err.reason) { + reason = err.reason; + err = err.error; + } + return void cb(err, reason); } offset = os; })); @@ -540,8 +545,8 @@ const getHistoryAsync = (Env, channelName, lastKnownHash, beforeHash, handler, c const parsed = tryParse(Env, msgObj.buff.toString('utf8')); if (!parsed) { return void readMore(); } handler(parsed, readMore); - }, waitFor(function (err) { - return void cb(err); + }, waitFor(function (err, reason) { + return void cb(err, reason); })); }); }; @@ -682,7 +687,7 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) { if (isMetadataMessage(msg) && metadata_cache[channelName]) { return readMore(); } if (txid) { msg[0] = txid; } Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(msg)], readMore); - }, (err) => { + }, (err, reason) => { // Any error but ENOENT: abort // ENOENT is allowed in case we want to create a new pad if (err && err.code !== 'ENOENT') { @@ -700,10 +705,15 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) { stack: err && err.stack, }); } // FIXME err.message isn't useful for users - const parsedMsg = {error:err.message, channel: channelName, txid: txid}; + const parsedMsg = {error:err.message, test:'a', channel: channelName, txid: txid}; Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg)]); return; } + if (err && err.code === 'ENOENT' && reason) { + const parsedMsg2 = {error:'EDELETED', message: reason, channel: channelName, txid: txid}; + Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg2)]); + return; + } // If we're asking for a specific version (lastKnownHash) but we receive an // ENOENT, this is not a pad creation so we need to abort. @@ -716,7 +726,7 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) { is no longer on the server so they don't abuse the data and so that they don't unintentionally continue to edit it in a broken state. */ - const parsedMsg2 = {error:'EDELETED', channel: channelName, txid: txid}; + const parsedMsg2 = {error:'EDELETED', test:'b',channel: channelName, txid: txid}; Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg2)]); return; } diff --git a/lib/storage/blob.js b/lib/storage/blob.js index d2c0a7420..98c6a9266 100644 --- a/lib/storage/blob.js +++ b/lib/storage/blob.js @@ -47,6 +47,10 @@ var makeProofPath = function (Env, safeKey, blobId) { return Path.join(Env.blobPath, safeKey.slice(0, 3), safeKey, blobId.slice(0, 2), blobId); }; +var mkPlaceholderPath = function (Env, blobId) { + return makeBlobPath(Env, blobId) + '.placeholder'; +}; + var parseProofPath = function (path) { var parts = path.split('/'); return { @@ -55,6 +59,26 @@ var parseProofPath = function (path) { }; }; +// Placeholder for deleted files +var addPlaceholder = function (Env, blobId, reason, cb) { + if (!reason) { return cb(); } + var path = mkPlaceholderPath(Env, blobId); + var s_data = typeof(reason) === "string" ? reason : `${reason.code}:${reason.txt}`; + Fs.writeFile(path, s_data, cb); +}; +var clearPlaceholder = function (Env, blobId, cb) { + var path = mkPlaceholderPath(Env, blobId); + Fs.unlink(path, cb); +}; +var readPlaceholder = function (Env, blobId, cb) { + var path = mkPlaceholderPath(Env, blobId); + Fs.readFile(path, function (err, content) { + if (err) { return void cb(); } + cb(content.toString('utf8')); + }); +}; + + // getUploadSize: used by // getFileSize var getUploadSize = function (Env, blobId, cb) { @@ -63,7 +87,16 @@ var getUploadSize = function (Env, blobId, cb) { Fs.stat(path, function (err, stats) { if (err) { // if a file was deleted, its size is 0 bytes - if (err.code === 'ENOENT') { return cb(void 0, 0); } + if (err.code === 'ENOENT') { + return readPlaceholder(Env, blobId, (content) => { + if (!content) { return cb(void 0, 0); } + cb({ + code: err.code, + reason: content + }); + + }); + } return void cb(err.code); } cb(void 0, stats.size); @@ -345,6 +378,7 @@ var owned_upload_complete = function (Env, safeKey, id, cb) { }); }; + // removeBlob var remove = function (Env, blobId, cb) { var blobPath = makeBlobPath(Env, blobId); @@ -366,11 +400,12 @@ var isOwnedBy = function (Env, safeKey, blobId, cb) { // archiveBlob -var archiveBlob = function (Env, blobId, cb) { +var archiveBlob = function (Env, blobId, reason, cb) { var blobPath = makeBlobPath(Env, blobId); var archivePath = prependArchive(Env, blobPath); Fse.move(blobPath, archivePath, { overwrite: true }, cb); archiveActivity(Env, blobId, () => {}); + addPlaceholder(Env, blobId, reason, () => {}); }; var removeArchivedBlob = function (Env, blobId, cb) { @@ -385,6 +420,7 @@ var restoreBlob = function (Env, blobId, cb) { var archivePath = prependArchive(Env, blobPath); Fse.move(archivePath, blobPath, cb); restoreActivity(Env, blobId, () => {}); + clearPlaceholder(Env, blobId, () => {}); }; // archiveProof @@ -614,10 +650,10 @@ BlobStore.create = function (config, _cb) { }, archive: { - blob: function (blobId, _cb) { + blob: function (blobId, reason, _cb) { var cb = Util.once(Util.mkAsync(_cb)); if (!isValidId(blobId)) { return void cb("INVALID_ID"); } - archiveBlob(Env, blobId, cb); + archiveBlob(Env, blobId, reason, cb); }, proof: function (safeKey, blobId, _cb) { var cb = Util.once(Util.mkAsync(_cb)); diff --git a/lib/storage/file.js b/lib/storage/file.js index 5f915f5c8..60ff9dfc1 100644 --- a/lib/storage/file.js +++ b/lib/storage/file.js @@ -70,6 +70,10 @@ var mkOffsetPath = function (env, channelId) { return mkPath(env, channelId) + '.offset'; }; +var mkPlaceholderPath = function (env, channelId) { + return mkPath(env, channelId) + '.placeholder'; +}; + // pass in the path so we can reuse the same function for archived files var channelExists = function (filepath, cb) { Fs.stat(filepath, function (err, stat) { @@ -141,6 +145,25 @@ var isChannelArchived = function (env, channelName, cb) { }); }; +var addPlaceholder = function (env, channelId, reason, cb) { + if (!reason) { return cb(); } + var path = mkPlaceholderPath(env, channelId); + var s_data = typeof(reason) === "string" ? reason : `${reason.code}:${reason.txt}`; + Fs.writeFile(path, s_data, cb); +}; +var clearPlaceholder = function (env, channelId, cb) { + var path = mkPlaceholderPath(env, channelId); + Fs.unlink(path, cb); +}; +var readPlaceholder = function (env, channelId, cb) { + var path = mkPlaceholderPath(env, channelId); + Fs.readFile(path, function (err, content) { + if (err) { return void cb(); } + cb(content.toString('utf8')); + }); +}; + + const destroyStream = function (stream) { if (!stream) { return; } try { @@ -190,7 +213,16 @@ const readMessagesBin = (env, id, start, msgHandler, cb) => { const stream = Fs.createReadStream(mkPath(env, id), { start: start }); const collector = createIdleStreamCollector(stream); const handleMessageAndKeepStreamAlive = Util.both(msgHandler, collector.keepAlive); - const done = Util.both(cb, collector); + const done = Util.both((err) => { + if (err && err.code === 'ENOENT') { + // If the channel doesn't exists, look for a placeholder. + // If a placeholder exists, call back with its content in addition to the original error + return readPlaceholder(env, id, (content) => { + cb(err, content); + }); + } + cb(err); + }, collector); return void readFileBin(stream, handleMessageAndKeepStreamAlive, done, { offset: start, }); @@ -670,7 +702,7 @@ var listChannels = function (root, handler, cb, fast) { // move a channel's log file from its current location // to an equivalent location in the cold storage directory -var archiveChannel = function (env, channelName, cb) { +var archiveChannel = function (env, channelName, reason, cb) { // TODO close channels before archiving them? // ctime is the most reliable indicator of when a file was archived @@ -700,6 +732,9 @@ var archiveChannel = function (env, channelName, cb) { })); }).nThen(function (w) { clearOffset(env, channelName, w()); + }).nThen(function (w) { + if (!reason) { return; } + addPlaceholder(env, channelName, reason, w()); }).nThen(function (w) { // archive the dedicated metadata channel var metadataPath = mkMetadataPath(env, channelName); @@ -775,6 +810,8 @@ var unarchiveChannel = function (env, channelName, cb) { return void CB(err); } })); + }).nThen(function (w) { + clearPlaceholder(env, channelName, w()); }).nThen(function (w) { var archiveMetadataPath = mkArchiveMetadataPath(env, channelName); // TODO validate that it's ok to move metadata non-atomically @@ -1290,12 +1327,12 @@ module.exports.create = function (conf, _cb) { isChannelArchived(env, channelName, cb); }, // move a channel from the database to the archive, along with its metadata - archiveChannel: function (channelName, cb) { + archiveChannel: function (channelName, reason, cb) { if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } // again, the semantics around archiving and appending are really muddy. // so I'm calling this 'unordered' again schedule.unordered(channelName, function (next) { - archiveChannel(env, channelName, Util.both(cb, next)); + archiveChannel(env, channelName, reason, Util.both(cb, next)); }); }, // restore a channel from the archive to the database, along with its metadata diff --git a/lib/workers/db-worker.js b/lib/workers/db-worker.js index d12e62365..76f03421d 100644 --- a/lib/workers/db-worker.js +++ b/lib/workers/db-worker.js @@ -499,8 +499,13 @@ const getHashOffset = function (data, cb) { } offset = msgObj.offset; abort(); - }, function (err) { - if (err) { return void cb(err); } + }, function (err, reason) { + if (err) { + return void cb({ + error: err, + reason: reason + }); + } cb(void 0, offset); }); }; @@ -510,6 +515,8 @@ const removeOwnedBlob = function (data, cb) { const blobId = data.blobId; const safeKey = Util.escapeKeyCharacters(data.safeKey); + const reason = data.reason || 'ARCHIVE_OWNED'; + nThen(function (w) { // check if you have permissions blobStore.isOwnedBy(safeKey, blobId, w(function (err, owned) { @@ -520,7 +527,7 @@ const removeOwnedBlob = function (data, cb) { })); }).nThen(function (w) { // remove the blob - blobStore.archive.blob(blobId, w(function (err) { + blobStore.archive.blob(blobId, reason, w(function (err) { Env.Log.info('ARCHIVAL_OWNED_FILE_BY_OWNER_RPC', { safeKey: safeKey, blobId: blobId, diff --git a/lib/workers/index.js b/lib/workers/index.js index 7b11e7d62..deddd8afb 100644 --- a/lib/workers/index.js +++ b/lib/workers/index.js @@ -382,11 +382,12 @@ Workers.initialize = function (Env, config, _cb) { }); }; - Env.removeOwnedBlob = function (blobId, safeKey, cb) { + Env.removeOwnedBlob = function (blobId, safeKey, reason, cb) { sendCommand({ command: 'REMOVE_OWNED_BLOB', blobId: blobId, safeKey: safeKey, + reason: reason }, cb); }; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index a8992250c..9971ccab5 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2876,6 +2876,15 @@ define([ delete autoStoreModal[priv.channel]; } + if (err.message && err.message !== "PASSWORD_CHANGE") { + UI.errorLoadingScreen(h('div', [ + h('p', Messages.deletedError), + h('p', err.message) // XXX custom message for each reason + // XXX reasons: ARCHIVE_OWNED, INACTIVE, MODERATION_ACCOUNT:{reason}, MODERATION_PAD:{reason} + ]), Boolean(err.loaded), Boolean(err.loaded)); + return; + } + if (err.ownDeletion) { if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } (cb || function () {})(); @@ -2921,6 +2930,12 @@ define([ var error; if (isError) { error = setHTML(h('p.cp-password-error'), Messages.password_error); } +// XXX +if (cfg.legacy) { + // XXX Legacy mode: we don't know if pad destroyed or password changed +} + + var info = h('p.cp-password-info', Messages.password_info); var info_loaded = setHTML(h('p.cp-password-info'), Messages.errorCopy); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index aaf722c8a..5e9c9fab3 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -649,15 +649,23 @@ define([ }).nThen(function (waitFor) { // If it's not in the cache or it's not a blob, try to get the value from the server - postMessage("GET_FILE_SIZE", {channel:channel}, waitFor(function (obj) { - if (obj && obj.error) { - // If disconnected, try to get the value from the channel cache (next nThen) - error = obj.error; - return; - } - waitFor.abort(); - cb(undefined, obj.size); - })); + var getSize = () => { + postMessage("GET_FILE_SIZE", {channel:channel}, waitFor(function (obj) { + if (obj && obj.error === "ANON_RPC_NOT_READY") { return void setTimeout(waitFor(getSize), 100); } + + if (obj && obj.error && obj.error.code === 'ENOENT' && obj.error.reason) { + waitFor.abort(); + cb(obj.error.reason); + } else if (obj && obj.error) { + // If disconnected, try to get the value from the channel cache (next nThen) + error = obj.error; + return; + } + waitFor.abort(); + cb(undefined, obj.size); + })); + }; + getSize(); }).nThen(function () { Cache.getChannelCache(channel, function(err, data) { if (err) { return void cb(error); } @@ -681,7 +689,7 @@ define([ var error = obj && obj.error; if (error) { return void cb(error); } if (!obj) { return void cb('ERROR'); } - cb (null, obj.isNew); + cb (null, obj.isNew, obj.reason); }, {timeout: -1}); }; // This function is used when we want to open a pad. We first need @@ -711,7 +719,7 @@ define([ } else if (error) { return void cb(error); } - cb(undefined, obj.isNew); + cb(undefined, obj.isNew, obj.reason); }, {timeout: -1}); }; isNew(); @@ -1484,7 +1492,8 @@ define([ // delete the old pad common.removeOwnedChannel({ channel: oldChannel, - teamId: teamId + teamId: teamId, + reason: 'PASSWORD_CHANGE', }, waitFor(function (obj) { if (obj && obj.error) { waitFor.abort(); @@ -1620,7 +1629,8 @@ define([ // delete the old pad common.removeOwnedChannel({ channel: oldChannel, - teamId: teamId + teamId: teamId, + reason: 'PASSWORD_CHANGE' }, waitFor(function (obj) { if (obj && obj.error) { waitFor.abort(); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 11908cf38..b574345ce 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -1152,10 +1152,36 @@ define([ var FILTER_BY = "filterBy"; + var refreshDeprecated = function () { + if (!APP.passwordModal) { return; } + var deprecated = files.sharedFoldersTemp; + if (JSONSortify(deprecated) === APP.deprecatedSF) { return; } + APP.deprecatedSF = JSONSortify(deprecated); + if (typeof (deprecated) === "object" && Object.keys(deprecated).length) { + var nt = nThen; + 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) { + UI.openCustomModal(APP.passwordModal(fId, data, waitFor())); + }).nThen; + }); + nt(function () { + APP.refresh(); + }); + } + + }; var refresh = APP.refresh = function (cb) { var type = APP.store[FILTER_BY]; var path = type ? [FILTER, type, currentPath] : currentPath; - APP.displayDirectory(path, undefined, cb); + APP.displayDirectory(path, undefined, () => { + refreshDeprecated(); + if (typeof(cb) === "function") { cb(); } + }); }; // `app`: true (force open wiht the app), false (force open in preview), @@ -5370,9 +5396,13 @@ define([ }); } */ - var nt = nThen; - var passwordModal = function (fId, data, cb) { + APP.passwordModal = function (fId, data, cb) { var content = []; + + var legacy = data.legacy; // XXX in legacy mode, it's either deletion or password change + legacy = legacy; + // XXX when legacy is false, we know it's a password change + var folderName = ''+ (data.lastTitle || Messages.fm_newFolder) +''; content.push(UI.setHTML(h('p'), Messages._getKey('drive_sfPassword', [folderName]))); var newPassword = UI.passwordInput({ @@ -5425,24 +5455,7 @@ define([ onClose: cb }); }; - onConnectEvt.reg(function () { - var deprecated = files.sharedFoldersTemp; - if (typeof (deprecated) === "object" && Object.keys(deprecated).length) { - 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) { - UI.openCustomModal(passwordModal(fId, data, waitFor())); - }).nThen; - }); - nt(function () { - refresh(); - }); - } - }); + onConnectEvt.reg(refreshDeprecated); return { refresh: refresh, diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 85e2186ab..e81738c04 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -365,10 +365,12 @@ define([ var channel = data; var force = false; var teamId; + var reason; if (data && typeof(data) === "object") { channel = data.channel; force = data.force; teamId = data.teamId; + reason = data.reason; } if (channel === store.driveChannel && !force) { @@ -385,7 +387,7 @@ define([ s.rpc.removeOwnedChannel(channel, function (err) { if (err) { delete myDeletions[channel]; } cb({error:err}); - }); + }, reason); }; var arePinsSynced = function (cb) { @@ -512,11 +514,9 @@ define([ 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') { - if (response[0]) { Cache.clearChannel(channelId); } - return void cb({ - isNew: response[0] - }); + if (response && response.length && typeof(response[0]) === 'object') { + if (response[0].isNew) { Cache.clearChannel(channelId); } + return void cb(response[0]); } else { cb({error: 'INVALID_RESPONSE'}); } diff --git a/www/common/outer/onlyoffice.js b/www/common/outer/onlyoffice.js index dbde5e199..60a3e412a 100644 --- a/www/common/outer/onlyoffice.js +++ b/www/common/outer/onlyoffice.js @@ -251,8 +251,8 @@ define([ ctx.store.anon_rpc.send("IS_NEW_CHANNEL", channel, function (e, response) { if (e) { return void cb({error: e}); } var isNew; - if (response && response.length && typeof(response[0]) === 'boolean') { - isNew = response[0]; + if (response && response.length && typeof(response[0]) === 'object') { + isNew = response[0].isNew; } else { cb({error: 'INVALID_RESPONSE'}); } diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index e86682746..c121b5c24 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -120,7 +120,7 @@ define([ }).nThen(function (waitFor) { isNewChannel(null, { channel: secret.channel }, waitFor(function (obj) { if (obj.isNew && !isNew) { - store.manager.deprecateProxy(id, secret.channel); + store.manager.deprecateProxy(id, secret.channel, obj.reason); waitFor.abort(); return void cb(null); } @@ -245,7 +245,7 @@ define([ // Deprecate the shared folder from each team // We can only hide it sf.teams.forEach(function (obj) { - obj.store.manager.deprecateProxy(obj.id, secret.channel); + obj.store.manager.deprecateProxy(obj.id, secret.channel, info.message); if (obj.store.handleSharedFolder) { obj.store.handleSharedFolder(obj.id, null); } diff --git a/www/common/outer/team.js b/www/common/outer/team.js index c7a78170f..641ec59ff 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -423,7 +423,7 @@ define([ }; if (channel) { ctx.store.anon_rpc.send("IS_NEW_CHANNEL", channel, waitFor(function (e, res) { - if (res && res.length && typeof(res[0]) === 'boolean' && res[0]) { + if (res && res.length && typeof(res[0]) === 'object' && res[0].isNew) { // Channel is empty: remove this team close(); } @@ -431,7 +431,7 @@ define([ } if (roster) { ctx.store.anon_rpc.send("IS_NEW_CHANNEL", roster, waitFor(function (e, res) { - if (res && res.length && typeof(res[0]) === 'boolean' && res[0]) { + if (res && res.length && typeof(res[0]) === 'object' && res[0].isNew) { // Channel is empty: remove this team close(); } diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index 84d64401e..66163d06d 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -118,13 +118,14 @@ define([ cb(null, id); }; - exp.deprecateSharedFolder = function (id) { + exp.deprecateSharedFolder = function (id, reason) { if (readOnly) { return; } var data = files[SHARED_FOLDERS][id]; if (!data) { return; } var ro = !data.href || exp.cryptor.decrypt(data.href).indexOf('#') === -1; if (!ro) { - files[SHARED_FOLDERS_TEMP][id] = JSON.parse(JSON.stringify(data)); + var obj = files[SHARED_FOLDERS_TEMP][id] = JSON.parse(JSON.stringify(data)); + obj.legacy = reason !== "PASSWORD_CHANGE"; } var paths = exp.findFile(Number(id)); exp.delete(paths, null, true); diff --git a/www/common/pinpad.js b/www/common/pinpad.js index ab1a8c8c6..084b2d482 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -131,12 +131,15 @@ var factory = function (Util, Rpc) { }); }; - exp.removeOwnedChannel = function (channel, cb) { + exp.removeOwnedChannel = function (channel, cb, reason) { if (typeof(channel) !== 'string' || [32,48].indexOf(channel.length) === -1) { console.error('invalid channel to remove', channel); return void cb('INVALID_ARGUMENTS'); } - rpc.send('REMOVE_OWNED_CHANNEL', channel, function (e, response) { + rpc.send('REMOVE_OWNED_CHANNEL', { + channel: channel, + reason: reason + }, function (e, response) { if (e) { return void cb(e); } if (response && response.length && response[0] === "OK") { cb(); diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 79f19bef9..71097864d 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -71,7 +71,7 @@ define([ }; // Password may have changed - var deprecateProxy = function (Env, id, channel) { + var deprecateProxy = function (Env, id, channel, reason) { if (Env.folders[id] && Env.folders[id].deleting) { // Folder is being deleted by its owner, don't deprecate it return; @@ -85,11 +85,19 @@ define([ return void Env.Store.refreshDriveUI(); } if (channel) { Env.unpinPads([channel], function () {}); } - Env.user.userObject.deprecateSharedFolder(id); - removeProxy(Env, id); - if (Env.Store && Env.Store.refreshDriveUI) { - Env.Store.refreshDriveUI(); + + // If it's explicitely a deletion, no need to deprecate, just delete + if (reason && reason !== "PASSWORD_CHANGE") { + var temp = Util.find(Env, ['user', 'proxy', UserObject.SHARED_FOLDERS]); + delete temp[id]; + if (Env.Store && Env.Store.refreshDriveUI) { Env.Store.refreshDriveUI(); } + return; } + + // It's explicitely a password change, better message in drive: provide the "reason" to the UI + Env.user.userObject.deprecateSharedFolder(id, reason); + removeProxy(Env, id); + if (Env.Store && Env.Store.refreshDriveUI) { Env.Store.refreshDriveUI(); } }; var restrictedProxy = function (Env, id) { @@ -632,17 +640,19 @@ define([ if (isNew) { return void cb({ error: 'ENOTFOUND' }); } + var newData = Util.clone(data); var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets(parsed.type, parsed.hash, newPassword); - data.password = newPassword; - data.channel = secret.channel; + newData.password = newPassword; + newData.channel = secret.channel; if (secret.keys.editKeyStr) { - data.href = '/drive/#'+Hash.getEditHashFromKeys(secret); + newData.href = '/drive/#'+Hash.getEditHashFromKeys(secret); } - data.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); + newData.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); + delete newData.legacy; _addSharedFolder(Env, { path: ['root'], - folderData: data, + folderData: newData, }, function () { delete temp[fId]; Env.onSync(cb); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 8fb767788..921e447ed 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -469,12 +469,20 @@ define([ // `hasChannelHistory` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata Cryptpad.getFileSize(currentPad.href, password, function (e, size) { + if (e && e !== "PASSWORD_CHANGE") { + return sframeChan.event("EV_DELETED_ERROR", e); + } next(e, size === 0); }); return; } // Not a file, so we can use `hasChannelHistory` - Cryptpad.hasChannelHistory(currentPad.href, password, next); + Cryptpad.hasChannelHistory(currentPad.href, password, (e, isNew, reason) => { + if (isNew && reason && reason !== "PASSWORD_CHANGE") { + return sframeChan.event("EV_DELETED_ERROR", reason); + } + next(); + }); }); sframeChan.event("EV_PAD_PASSWORD", cfg); }; @@ -564,20 +572,32 @@ define([ // `hasChannelHistory` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata Cryptpad.getFileSize(currentPad.href, password, w(function (e, size) { - if (size !== 0) { return void todo(); } + if (e && e !== "PASSWORD_CHANGE") { + sframeChan.event("EV_DELETED_ERROR", e); + waitFor.abort(); + return; + } + if (!e && size !== 0) { return void todo(); } // Wrong password or deleted file? + passwordCfg.legacy = !e; // Legacy means we don't know if it's a deletion or pw change askPassword(true, passwordCfg); })); return; } // Not a file, so we can use `hasChannelHistory` - Cryptpad.hasChannelHistory(currentPad.href, password, w(function(e, isNew) { + Cryptpad.hasChannelHistory(currentPad.href, password, w(function(e, isNew, reason) { if (isNew && expire && expire < (+new Date())) { sframeChan.event("EV_EXPIRED_ERROR"); waitFor.abort(); return; } if (!e && !isNew) { return void todo(); } + // NOTE: Legacy mode ==> no reason may indicate a password change + if (isNew && reason && reason !== "PASSWORD_CHANGE") { + sframeChan.event("EV_DELETED_ERROR", reason); + waitFor.abort(); + return; + } 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 @@ -586,6 +606,7 @@ define([ return; } // Wrong password or deleted file? + passwordCfg.legacy = !reason; // Legacy means we don't know if it's a deletion or pw change askPassword(true, passwordCfg); })); }).nThen(done); @@ -1915,6 +1936,8 @@ define([ // redirect window.location.href = url; document.location.reload(); + // XXX XXX XXX TEST TEST TEST + // XXX XXX XXX TEST TEST TEST }); return; diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 35f51ef03..8a698d5f9 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -844,6 +844,13 @@ define([ UI.errorLoadingScreen(Messages.restrictedError); }); + ctx.sframeChan.on("EV_DELETED_ERROR", function (reason) { + funcs.onServerError({ + type: 'EDELETED', + message: reason + }); + }); + ctx.sframeChan.on("EV_PAD_PASSWORD_ERROR", function () { UI.errorLoadingScreen(Messages.password_error_seed); }); diff --git a/www/file/main.js b/www/file/main.js index ba2497a93..0329b65a6 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -20,6 +20,7 @@ define([ }; SFCommonO.start({ cache: true, + noDrive: true, hash: hash, href: href, noRealtime: true,