diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index dbcbcf8eb..2b12a8855 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -141,7 +141,15 @@ define([ anonymous: data.anonymous } }, function (obj) { - if (obj && obj.error) { console.error(obj.error); } + if (obj && obj.error) { + if (obj.error === "ENODRIVE") { + var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); + if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); } + localStorage.CP_formAnswered = JSON.stringify(answered); + return; + } + console.error(obj.error); + } }); }; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 1af15a9ed..0158d5c51 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -112,6 +112,7 @@ define([ Store.set = function (clientId, data, cb) { var s = getStore(data.teamId); if (!s) { return void cb({ error: 'ENOTFOUND' }); } + if (!s.proxy) { return void cb({ error: 'ENODRIVE' }); } var path = data.key.slice(); var key = path.pop(); var obj = Util.find(s.proxy, path); diff --git a/www/form/app-form.less b/www/form/app-form.less index 6c63d193c..18431ead1 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -64,6 +64,10 @@ flex: 1; overflow: auto; + .cp-form-page + .cp-form-send-container { + margin-top: 10px; + } + .cp-form-page-container { display: flex; justify-content: center; diff --git a/www/form/inner.js b/www/form/inner.js index 8e96341ae..8a3baa470 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -108,6 +108,7 @@ define([ Messages.form_delete = "Delete block"; Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; + Messages.form_answered = "You already answered this form"; Messages.form_viewResults = "Go to responses"; Messages.form_viewCreator = "Go to form creator"; @@ -1274,7 +1275,7 @@ define([ var form = content.form; var switchMode = h('button.btn.btn-primary', Messages.form_showIndividual); - $controls.append(switchMode); + $controls.hide().append(switchMode); var show = function (answers, header) { var elements = content.order.map(function (uid) { @@ -1299,6 +1300,8 @@ define([ }; show(answers); + if (APP.isEditor || APP.isAuditor) { $controls.show(); } + var $s = $(switchMode).click(function () { $results.empty(); if (!summary) { @@ -1436,6 +1439,9 @@ define([ }, function (err, data) { $send.attr('disabled', 'disabled'); if (err || (data && data.error)) { + if (data.error === "EANSWERED") { + return void UI.warn(Messages.form_answered); + } console.error(err || data.error); return void UI.warn(Messages.error); } @@ -1663,34 +1669,36 @@ define([ }); _content.push(div); - var pageContainer = h('div.cp-form-page-container'); - var $page = $(pageContainer); - _content.push(pageContainer); - var refreshPage = function (current) { - $page.empty(); - if (!current || current < 1) { current = 1; } - if (current > pages) { current = pages; } - var left = h('button.btn.btn-secondary.small.cp-prev', [ - h('i.fa.fa-chevron-left'), - h('span', Messages.form_page_prev) - ]); - var state = h('span', Messages._getKey('form_page', [current, pages])); - var right = h('button.btn.btn-secondary.small.cp-next', [ - h('span', Messages.form_page_next), - h('i.fa.fa-chevron-right'), - ]); - $(left).click(function () { refreshPage(current - 1); }); - $(right).click(function () { refreshPage(current + 1); }); - $page.append([left, state, right]); - $container.find('.cp-form-page').hide(); - $($container.find('.cp-form-page').get(current-1)).show(); - if (current !== pages) { - $container.find('.cp-form-send-container').hide(); - } else { - $container.find('.cp-form-send-container').show(); - } - }; - setTimeout(refreshPage); + if (pages > 1) { + var pageContainer = h('div.cp-form-page-container'); + var $page = $(pageContainer); + _content.push(pageContainer); + var refreshPage = function (current) { + $page.empty(); + if (!current || current < 1) { current = 1; } + if (current > pages) { current = pages; } + var left = h('button.btn.btn-secondary.small.cp-prev', [ + h('i.fa.fa-chevron-left'), + h('span', Messages.form_page_prev) + ]); + var state = h('span', Messages._getKey('form_page', [current, pages])); + var right = h('button.btn.btn-secondary.small.cp-next', [ + h('span', Messages.form_page_next), + h('i.fa.fa-chevron-right'), + ]); + $(left).click(function () { refreshPage(current - 1); }); + $(right).click(function () { refreshPage(current + 1); }); + $page.append([left, state, right]); + $container.find('.cp-form-page').hide(); + $($container.find('.cp-form-page').get(current-1)).show(); + if (current !== pages) { + $container.find('.cp-form-send-container').hide(); + } else { + $container.find('.cp-form-send-container').show(); + } + }; + setTimeout(refreshPage); + } } $container.empty().append(_content); @@ -1745,7 +1753,6 @@ define([ $toolbarContainer.after(helpMenu.menu); - // XXX refresh form settings on remote change var makeFormSettings = function () { // Private / public status var resultsType = h('div.cp-form-results-type-container'); @@ -1761,6 +1768,7 @@ define([ UI.confirm(Messages.form_makePublicWarning, function (yes) { if (!yes) { return; } $makePublic.attr('disabled', 'disabled'); + var priv = metadataMgr.getPrivateData(); content.answers.privateKey = priv.form_private; framework.localChange(); framework._.cpNfInner.chainpad.onSettle(function () { @@ -1889,11 +1897,6 @@ define([ resultsType, viewResults, ]; - - // XXX - // Button to set results as public - // Checkbox to allow anonymous answers - // Button to clear all answers? }; var checkIntegrity = function (getter) { @@ -2025,18 +2028,22 @@ define([ // * viewers ==> check if you've already answered and show form (new or edit) // * editors ==> show schema and warn users if existing questions already have answers - if (priv.form_auditorKey) { + var getResults = function (key) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, validateKey: content.answers.validateKey, publicKey: content.answers.publicKey, - privateKey: priv.form_auditorKey + privateKey: key }, function (err, obj) { var answers = obj && obj.results; if (answers) { APP.answers = answers; } $body.addClass('cp-app-form-results'); renderResults(content, answers); }); + }; + if (priv.form_auditorKey) { + APP.isAuditor = true; + getResults(priv.form_auditorKey); return; } @@ -2073,6 +2080,17 @@ define([ }, function (err, obj) { var answers = obj && obj.results; if (answers) { APP.answers = answers; } + + if (obj && obj.noDriveAnswered) { + // No drive mode already answered: can't answer again + if (answers) { + $body.addClass('cp-app-form-results'); + renderResults(content, answers); + } else { + return void UI.errorLoadingScreen(Messages.form_answered); + } + return; + } checkIntegrity(false); var myAnswers; var curve1 = user.curvePublic; @@ -2096,6 +2114,14 @@ define([ publicKey: content.answers.publicKey }, function (err, obj) { if (obj && obj.error) { + if (obj.error === "EANSWERED") { + // No drive mode already answered: can't answer again + if (content.answers.privateKey) { + return void getResults(content.answers.privateKey); + } + // Here, we know results are private so we can use an error screen + return void UI.errorLoadingScreen(Messages.form_answered); + } UI.warn(Messages.form_cantFindAnswers); } var answers; diff --git a/www/form/main.js b/www/form/main.js index 94b79165e..0807d5d8f 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -47,7 +47,6 @@ define([ }); var _parsed = Utils.Hash.parseTypeHash('pad', auditorHash); meta.form_auditorHash = _parsed.getHash({auditorKey: privateKey}); - }; var addRpc = function (sframeChan, Cryptpad, Utils) { sframeChan.on('EV_FORM_PIN', function (data) { @@ -127,6 +126,7 @@ define([ var accessKeys; var CPNetflux, Pinpad; var network; + var noDriveAnswered = false; nThen(function (w) { require([ '/bower_components/chainpad-netflux/chainpad-netflux.js', @@ -147,6 +147,11 @@ define([ }); })); Cryptpad.getFormKeys(w(function (keys) { + if (!keys.curvePublic && !keys.formSeed) { + // No drive mode + var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); + noDriveAnswered = answered.indexOf(data.channel) !== -1; + } myFormKeys = keys; })); Cryptpad.makeNetwork(w(function (err, nw) { @@ -204,6 +209,7 @@ define([ myKey = myFormKeys.curvePublic; } cb({ + noDriveAnswered: noDriveAnswered, myKey: myKey, results: results }); @@ -236,6 +242,15 @@ define([ })); Cryptpad.getFormAnswer({channel: data.channel}, w(function (obj) { if (!obj || obj.error) { + if (obj && obj.error === "ENODRIVE") { + var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); + if (answered.indexOf(data.channel) !== -1) { + cb({error:'EANSWERED'}); + } else { + cb(); + } + return void w.abort(); + } w.abort(); return void cb(obj); } @@ -266,19 +281,26 @@ define([ }); }); + var noDriveSeed = Utils.Hash.createChannelId(); sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { var box = data.mailbox; var myKeys; nThen(function (w) { Cryptpad.getFormKeys(w(function (keys) { + // If formSeed doesn't exists, it means we're probably in noDrive mode. + // 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; })); }).nThen(function () { - // XXX if we are a registered user (myKeys.curvePrivate exists), we may - // have already answered anonymously. We should send a "proof" to show - // that the existing anonymous answer are ours (using myKeys.formSeed). - // Even if we never answered anonymously, the keyPair would be unique to - // the current channel so it wouldn't leak anything. var myAnonymousKeys; if (data.anonymous) { if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }