From 600771682a36e8ac1ea289765bf89c0f09a64d01 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 8 Dec 2022 16:53:29 +0100 Subject: [PATCH] Allow edit/delete/multiple answers without a drive and fix race condition --- lib/commands/channel.js | 5 +-- lib/rpc.js | 2 +- www/common/cryptpad-common.js | 63 ++++++++++++++++++++++++++++----- www/common/outer/async-store.js | 7 ++-- www/common/pinpad.js | 10 ------ www/form/inner.js | 12 +++++-- www/form/main.js | 7 +--- 7 files changed, 73 insertions(+), 33 deletions(-) diff --git a/lib/commands/channel.js b/lib/commands/channel.js index 0481c5870..9c6cefcac 100644 --- a/lib/commands/channel.js +++ b/lib/commands/channel.js @@ -210,7 +210,7 @@ Channel.trimHistory = function (Env, safeKey, data, cb) { // Delete a signed mailbox message. This is used when users want // to delete their form reponses. -Channel.deleteMailboxMessage = function (Env, safeKey, data, cb) { +Channel.deleteMailboxMessage = function (Env, data, cb) { const channelId = data.channel; const hash = data.hash; const proof = data.proof; @@ -355,10 +355,11 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) { Server.getChannelUserList(channelId).forEach(function (userId) { Server.send(userId, fullMessage); }); + + cb(); }); - cb(); }); }; diff --git a/lib/rpc.js b/lib/rpc.js index 0abcaac62..1f84a1582 100644 --- a/lib/rpc.js +++ b/lib/rpc.js @@ -20,6 +20,7 @@ const UNAUTHENTICATED_CALLS = { IS_CHANNEL_PINNED: Pinning.isChannelPinned, // FIXME drop this RPC IS_NEW_CHANNEL: Channel.isNewChannel, WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage, + DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage, GET_METADATA: Metadata.getMetadata, }; @@ -47,7 +48,6 @@ const AUTHENTICATED_USER_TARGETED = { CLEAR_OWNED_CHANNEL: Channel.clearOwnedChannel, REMOVE_OWNED_CHANNEL: Channel.removeOwnedChannel, TRIM_HISTORY: Channel.trimHistory, - DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage, UPLOAD_STATUS: Upload.status, UPLOAD: Upload.upload, UPLOAD_COMPLETE: Upload.complete, diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 2127e75e0..e9f5e0cee 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -125,6 +125,12 @@ define([ formSeed = obj; })); }).nThen(function () { + if (!formSeed) { // no drive mode + formSeed = localStorage.CP_formSeed || Hash.createChannelId(); + localStorage.CP_formSeed = formSeed; + } else { + delete localStorage.CP_formSeed; + } cb({ curvePrivate: curvePrivate, curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate), @@ -132,16 +138,39 @@ define([ }); }); }; - common.getFormAnswer = function (data, onlyLast, cb) { + common.getFormAnswer = function (data, cb) { postMessage("GET", { key: ['forms', data.channel], }, function (obj) { - if (!obj || obj.error) { return void cb(obj); } - if (!Array.isArray(obj)) { obj = [obj]; } + if (obj && obj.error === "ENODRIVE") { + var all = Util.tryParse(localStorage.CP_formAnswers || "{}"); + return void cb(all[data.channel]); + } + if (obj && obj.error) { return void cb(obj); } - var last = obj[obj.length - 1]; - if (onlyLast) { return void cb(last); } - return void cb(obj); + if (obj) { + if (!Array.isArray(obj)) { obj = [obj]; } + return void cb(obj); + } + + // We have a drive and no answer but maybe we had + // previous "nodrive" answers: migrate + var old = Util.tryParse(localStorage.CP_formAnswers || "{}"); + if (Array.isArray(old[data.channel])) { + var d = old[data.channel]; + return void postMessage("SET", { + key: ['forms', data.channel], + value: d + }, function (obj) { + // Delete old data if it was correctly stored in the drive + if (obj && obj.error) { return void cb(d); } + delete old[data.channel]; + localStorage.CP_formAnswers = JSON.stringify(old); + cb(d); + }); + } + + cb(); }); }; common.storeFormAnswer = function (data, cb) { @@ -153,7 +182,7 @@ define([ }; var answers = []; Nthen(function (waitFor) { - common.getFormAnswer(data, false, waitFor(function (obj) { + common.getFormAnswer(data, waitFor(function (obj) { if (!obj || obj.error) { return; } answers = obj; })); @@ -165,9 +194,15 @@ define([ }, function (obj) { if (obj && obj.error) { if (obj.error === "ENODRIVE") { + var all = Util.tryParse(localStorage.CP_formAnswers || "{}"); + all[data.channel] = answers; + localStorage.CP_formAnswers = JSON.stringify(all); +/* + var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); } localStorage.CP_formAnswered = JSON.stringify(answered); +*/ return void cb(); } console.error(obj.error); @@ -178,7 +213,7 @@ define([ }; common.deleteFormAnswers = function (data, _cb) { var cb = Util.once(_cb); - common.getFormAnswer(data, false, function (obj) { + common.getFormAnswer(data, function (obj) { if (!obj || obj.error) { return void cb(); } if (!obj.length) { return void cb(); } var n = Nthen; @@ -224,7 +259,16 @@ define([ postMessage("SET", { key: ['forms', data.channel], value: obj - }, cb); + }, function (_obj) { + if (_obj && _obj.error === "ENODRIVE") { + var all = Util.tryParse(localStorage.CP_formAnswers || "{}"); + if (obj) { all[data.channel] = obj; } + else { delete all[data.channel]; } + localStorage.CP_formAnswers = JSON.stringify(all); + return void cb(); + } + return void cb(_obj); + }); }); }); }; @@ -2480,6 +2524,7 @@ define([ anonHash: LocalStore.getFSHash(), localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ? language: common.getLanguage(), + form_seed: localStorage.CP_formSeed, cache: rdyCfg.cache, noDrive: rdyCfg.noDrive, disableCache: localStorage['CRYPTPAD_STORE|disableCache'], diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 724828d83..62eeb8a29 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -2162,8 +2162,8 @@ define([ }; Store.deleteMailboxMessage = function (clientId, data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.deleteMailboxMessage(data, function (e) { + if (!store.anon_rpc) { return void cb({error: 'RPC_NOT_READY'}); } + store.anon_rpc.send('DELETE_MAILBOX_MESSAGE', data, function (e) { cb({error:e}); }); }; @@ -2962,6 +2962,9 @@ define([ if (!rt.proxy.uid && store.noDriveUid) { rt.proxy.uid = store.noDriveUid; } + if (!rt.proxy.form_seed && data.form_seed) { + rt.proxy.form_seed = data.form_seed; + } /* // deprecating localStorage migration as of 4.2.0 var drive = rt.proxy.drive; diff --git a/www/common/pinpad.js b/www/common/pinpad.js index e91ebdbf3..7102be669 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -249,16 +249,6 @@ var factory = function (Util, Rpc) { }, cb); }; - exp.deleteMailboxMessage = function (obj, cb) { - rpc.send('DELETE_MAILBOX_MESSAGE', { - channel: obj.channel, - hash: obj.hash, - proof: obj.proof - }, cb); - }; - - - cb(e, exp); }); }; diff --git a/www/form/inner.js b/www/form/inner.js index 629334aef..7cd350077 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3218,7 +3218,8 @@ define([ $(anonOffContent).append(h('span.cp-form-anon-answer-input', [ Messages.form_answerAs, h('input', { - value: user.name || '', + value: (typeof(APP.cantAnon) === "string" && APP.cantAnon) + || user.name || '', placeholder: Messages.form_anonName }) ])); @@ -3227,7 +3228,8 @@ define([ $anonName.on('click input', function () { if (!Util.isChecked($off)) { $off.prop('checked', true); } }); - } else if (APP.cantAnon) { + } + if (APP.cantAnon) { // You've already answered with your credentials $(anonRadioOn).find('input').attr('disabled', 'disabled'); $(anonRadioOff).find('input[type="radio"]').prop('checked', true); @@ -4143,13 +4145,17 @@ define([ }); } + var loggedIn = framework._.sfCommon.isLoggedIn(); + // In view mode, add "Submit" and "reset" buttons APP.cantAnon = Object.keys(answers || {}).length && !answers._isAnon; + if (!loggedIn && answers && answers._userdata && answers._userdata.name) { + APP.cantAnon = answers._userdata.name; + } $container.append(makeFormControls(framework, content, evOnChange)); // In view mode, tell the user if answers are forced to be anonymous or authenticated var infoTxt; - var loggedIn = framework._.sfCommon.isLoggedIn(); if (content.answers.makeAnonymous) { infoTxt = Messages.form_anonAnswer; } else if (!content.answers.anonymous && loggedIn && !content.answers.cantEdit) { diff --git a/www/form/main.js b/www/form/main.js index 81065cbe7..b39ec3858 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -265,7 +265,7 @@ define([ Cryptpad.getFormKeys(w(function (keys) { myKeys = keys; })); - Cryptpad.getFormAnswer({channel: data.channel}, false, w(function (obj) { + Cryptpad.getFormAnswer({channel: data.channel}, w(function (obj) { if (!obj || obj.error) { if (obj && obj.error === "ENODRIVE") { var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); @@ -354,11 +354,6 @@ define([ // We can create a seed in localStorage. if (!keys.formSeed) { // No drive mode - var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); - if(answered.indexOf(data.channel) !== -1) { - // Already answered: abort - return void cb({ error: "EANSWERED" }); - } keys = { formSeed: noDriveSeed }; } myKeys = keys;