mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-12 19:49:59 +05:00
Merge branch 'formanswers_download' into 2025.3-test
This commit is contained in:
commit
e8a09080c7
@ -21,6 +21,7 @@ define([
|
||||
|
||||
'/customize/application_config.js',
|
||||
'/components/nthen/index.js',
|
||||
'/components/tweetnacl/nacl-fast.min.js'
|
||||
], function (Config, Messages, Util, Hash, Cache,
|
||||
Messaging, Constants, Feedback, Visible, UserObject, LocalStore, Channel, Block,
|
||||
Cred, Login, AppConfig, Nthen) {
|
||||
@ -227,7 +228,6 @@ define([
|
||||
n = n(function (waitFor) {
|
||||
require([
|
||||
'/api/broadcast?'+ (+new Date()),
|
||||
'/components/tweetnacl/nacl-fast.min.js'
|
||||
], waitFor(function (Broadcast) {
|
||||
nacl = window.nacl;
|
||||
theirs = nacl.util.decodeBase64(Broadcast.curvePublic);
|
||||
@ -1580,7 +1580,6 @@ define([
|
||||
require([
|
||||
'/common/media-tag.js',
|
||||
'/common/outer/upload.js',
|
||||
'/components/tweetnacl/nacl-fast.min.js'
|
||||
], waitFor(function (_MT, _Upload) {
|
||||
MediaTag = _MT;
|
||||
Upload = _Upload;
|
||||
@ -2451,6 +2450,17 @@ define([
|
||||
window.RTCPeerConnection);
|
||||
};
|
||||
|
||||
common.getAnonymousKeys = function (formSeed, channel) {
|
||||
var array = window.nacl.util.decodeBase64(formSeed + channel);
|
||||
var hash = window.nacl.hash(array);
|
||||
var secretKey = window.nacl.util.encodeBase64(hash.subarray(32));
|
||||
var publicKey = Hash.getCurvePublicFromPrivate(secretKey);
|
||||
return {
|
||||
curvePrivate: secretKey,
|
||||
curvePublic: publicKey,
|
||||
};
|
||||
};
|
||||
|
||||
common.ready = (function () {
|
||||
var env = {};
|
||||
var initialized = false;
|
||||
|
||||
@ -10,12 +10,13 @@ define([
|
||||
'/common/common-interface.js',
|
||||
'/common/hyperscript.js',
|
||||
'/common/common-feedback.js',
|
||||
'/common/userObject.js',
|
||||
'/common/inner/cache.js',
|
||||
'/customize/messages.js',
|
||||
'/components/nthen/index.js',
|
||||
'/components/saferphore/index.js',
|
||||
'/components/jszip/dist/jszip.min.js',
|
||||
], function ($, FileCrypto, Hash, Util, UI, h, Feedback,
|
||||
], function ($, FileCrypto, Hash, Util, UI, h, Feedback, UO,
|
||||
Cache, Messages, nThen, Saferphore, JsZip) {
|
||||
var saveAs = window.saveAs;
|
||||
|
||||
@ -169,6 +170,7 @@ define([
|
||||
data: fData
|
||||
});
|
||||
}
|
||||
|
||||
var href;
|
||||
var parsed;
|
||||
if (!fData.channel) {
|
||||
@ -176,7 +178,7 @@ define([
|
||||
parsed = {};
|
||||
parsed['hashData'] = {type: 'link'};
|
||||
} else {
|
||||
href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref;
|
||||
href = UO.getHref(fData, ctx.currentCryptor);
|
||||
parsed = Hash.parsePadUrl(href);
|
||||
}
|
||||
if (['pad', 'file', 'link'].indexOf(parsed.hashData.type) === -1) { return; }
|
||||
@ -314,20 +316,27 @@ define([
|
||||
if (typeof el === "object" && el.metadata !== true) { // if folder
|
||||
var fName = getUnique(sanitize(k), '', existingNames);
|
||||
existingNames.push(fName.toLowerCase());
|
||||
ctx.currentCryptor = undefined;
|
||||
return void makeFolder(ctx, el, zip.folder(fName), fd, sd);
|
||||
}
|
||||
if (ctx.data.sharedFolders[el]) { // if shared folder
|
||||
let staticData = ctx.sf[el].static;
|
||||
let obj = ctx.data.sharedFolders[el];
|
||||
let parsed = Hash.parsePadUrl(obj.href || obj.roHref);
|
||||
var secret = Hash.getSecrets('drive', parsed.hash, obj.password);
|
||||
let cryptor = secret.keys?.secondaryKey ? UO.createCryptor(secret.keys?.secondaryKey)
|
||||
: undefined;
|
||||
ctx.currentCryptor = cryptor;
|
||||
var sfData = ctx.sf[el].metadata;
|
||||
var sfName = getUnique(sanitize((sfData && sfData.title) || 'Folder'), '', existingNames);
|
||||
existingNames.push(sfName.toLowerCase());
|
||||
let staticData = ctx.sf[el].static;
|
||||
return void makeFolder(ctx, ctx.sf[el].root, zip.folder(sfName), ctx.sf[el].filesData, staticData);
|
||||
}
|
||||
var fData = fd[el] || (sd && sd[el]);
|
||||
if (fData) {
|
||||
addFile(ctx, zip, fData, existingNames);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -123,6 +123,7 @@ define([
|
||||
var password, newPadPassword, newPadPasswordForce;
|
||||
var initialPathInDrive;
|
||||
var burnAfterReading;
|
||||
var Handler;
|
||||
|
||||
var currentPad = window.CryptPad_location = {
|
||||
app: '',
|
||||
@ -156,10 +157,11 @@ define([
|
||||
'/common/userObject.js',
|
||||
'optional!/api/instance',
|
||||
'/common/pad-types.js',
|
||||
'/form/command-handler.js'
|
||||
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel,
|
||||
_SecureIframe, _UnsafeIframe, _OOIframe, _Notifier, _Hash, _Util, _Realtime, _Notify,
|
||||
_Constants, _Feedback, _LocalStore, _Block, _Cache, _AppConfig, /* _Test,*/ _UserObject,
|
||||
_Instance, _PadTypes) {
|
||||
_Instance, _PadTypes, _Handler) {
|
||||
CpNfOuter = _CpNfOuter;
|
||||
Cryptpad = _Cryptpad;
|
||||
Crypto = Utils.Crypto = _Crypto;
|
||||
@ -183,6 +185,7 @@ define([
|
||||
Utils.Block = _Block;
|
||||
Utils.PadTypes = _PadTypes;
|
||||
AppConfig = _AppConfig;
|
||||
Handler = _Handler;
|
||||
//Test = _Test;
|
||||
|
||||
if (localStorage.CRYPTPAD_URLARGS !== ApiConfig.requireConf.urlArgs) {
|
||||
@ -2072,6 +2075,8 @@ define([
|
||||
}
|
||||
});
|
||||
|
||||
Handler.formCommandHandlers(sframeChan, Utils, nThen, Cryptpad);
|
||||
|
||||
var integrationSave = function () {};
|
||||
if (cfg.integration) {
|
||||
sframeChan.on('Q_INTEGRATION_SAVE', function (obj, cb) {
|
||||
|
||||
@ -72,7 +72,7 @@ define([
|
||||
// Href exists and is not encrypted: return href
|
||||
return pad.href;
|
||||
}
|
||||
if (pad.href) {
|
||||
if (pad.href && cryptor) {
|
||||
// Href exists and is encrypted
|
||||
var d = cryptor.decrypt(pad.href);
|
||||
// If we can decrypt, return the decrypted value, otherwise continue and return roHref
|
||||
|
||||
386
www/form/command-handler.js
Normal file
386
www/form/command-handler.js
Normal file
@ -0,0 +1,386 @@
|
||||
// SPDX-FileCopyrightText: 2025 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
define([
|
||||
'/components/tweetnacl/nacl-fast.min.js'
|
||||
], function () {
|
||||
|
||||
var Handler = {};
|
||||
|
||||
var Nacl = window.nacl;
|
||||
Handler.formCommandHandlers = function(sframeChan, Utils, nThen, Cryptpad) {
|
||||
sframeChan.on('EV_EXPORT_SHEET', function (data) {
|
||||
if (!data || !Array.isArray(data.content)) { return; }
|
||||
sessionStorage.CP_formExportSheet = JSON.stringify(data);
|
||||
var href = Utils.Hash.hashToHref('', 'sheet');
|
||||
var a = window.open(href);
|
||||
if (!a) { sframeChan.event('EV_POPUP_BLOCKED'); }
|
||||
delete sessionStorage.CP_formExportSheet;
|
||||
});
|
||||
var u8_concat = function (A) {
|
||||
var length = 0;
|
||||
A.forEach(function (a) { length += a.length; });
|
||||
var total = new Uint8Array(length);
|
||||
|
||||
var offset = 0;
|
||||
A.forEach(function (a) {
|
||||
total.set(a, offset);
|
||||
offset += a.length;
|
||||
});
|
||||
return total;
|
||||
};
|
||||
var anonProof = function (channel, theirPub, anonKeys) {
|
||||
var u8_plain = Nacl.util.decodeUTF8(channel);
|
||||
var u8_nonce = Nacl.randomBytes(Nacl.box.nonceLength);
|
||||
var u8_cipher = Nacl.box(
|
||||
u8_plain,
|
||||
u8_nonce,
|
||||
Nacl.util.decodeBase64(theirPub),
|
||||
Nacl.util.decodeBase64(anonKeys.curvePrivate)
|
||||
);
|
||||
var u8_bundle = u8_concat([
|
||||
u8_nonce, // 24 uint8s
|
||||
u8_cipher, // arbitrary length
|
||||
]);
|
||||
return {
|
||||
key: anonKeys.curvePublic,
|
||||
proof: Nacl.util.encodeBase64(u8_bundle)
|
||||
};
|
||||
};
|
||||
var deleteLines = false; // "false" to support old forms
|
||||
sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) {
|
||||
var answers = [];
|
||||
var myKeys;
|
||||
nThen(function (w) {
|
||||
Cryptpad.getFormKeys(w(function (keys) {
|
||||
myKeys = keys;
|
||||
}));
|
||||
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);
|
||||
}
|
||||
// Get the latest edit per uid
|
||||
var temp = {};
|
||||
obj.forEach(function (ans) {
|
||||
var uid = ans.uid || '000';
|
||||
temp[uid] = ans;
|
||||
});
|
||||
answers = Object.values(temp);
|
||||
}));
|
||||
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
|
||||
if (md && md.deleteLines) { deleteLines = true; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
var n = nThen;
|
||||
var err;
|
||||
var all = {};
|
||||
answers.forEach(function (answer) {
|
||||
n = n(function(waitFor) {
|
||||
var finalKeys = myKeys;
|
||||
if (answer.anonymous) {
|
||||
if (!myKeys.formSeed) {
|
||||
err = 'ANONYMOUS_ERROR';
|
||||
console.error('ANONYMOUS_ERROR', answer);
|
||||
return;
|
||||
}
|
||||
finalKeys = Cryptpad.getAnonymousKeys(myKeys.formSeed, data.channel, Utils);
|
||||
}
|
||||
Cryptpad.getHistoryRange({
|
||||
channel: data.channel,
|
||||
lastKnownHash: answer.hash,
|
||||
toHash: answer.hash,
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) { err = obj.error; return; }
|
||||
var messages = obj.messages;
|
||||
if (!messages.length) {
|
||||
// TODO delete from drive.forms?
|
||||
return;
|
||||
}
|
||||
if (obj.lastKnownHash !== answer.hash) { return; }
|
||||
try {
|
||||
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
|
||||
validateKey: data.validateKey,
|
||||
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
|
||||
my_private: Nacl.util.decodeBase64(finalKeys.curvePrivate),
|
||||
their_public: Nacl.util.decodeBase64(data.publicKey)
|
||||
});
|
||||
var parsed = JSON.parse(res.content);
|
||||
parsed._isAnon = answer.anonymous;
|
||||
parsed._time = messages[0].time;
|
||||
if (deleteLines) { parsed._hash = answer.hash; }
|
||||
var uid = parsed._uid || '000';
|
||||
if (all[uid] && !all[uid]._isAnon) { parsed._isAnon = false; }
|
||||
all[uid] = parsed;
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
}));
|
||||
}).nThen;
|
||||
});
|
||||
n(function () {
|
||||
if (err) { return void cb({error: err}); }
|
||||
cb(all);
|
||||
});
|
||||
});
|
||||
});
|
||||
var u8_slice = function (A, start, end) {
|
||||
return new Uint8Array(Array.prototype.slice.call(A, start, end));
|
||||
};
|
||||
var checkAnonProof = function (proofObj, channel, curvePrivate) {
|
||||
var pub = proofObj.key;
|
||||
var proofTxt = proofObj.proof;
|
||||
try {
|
||||
var u8_bundle = Nacl.util.decodeBase64(proofTxt);
|
||||
var u8_nonce = u8_slice(u8_bundle, 0, Nacl.box.nonceLength);
|
||||
var u8_cipher = u8_slice(u8_bundle, Nacl.box.nonceLength);
|
||||
var u8_plain = Nacl.box.open(
|
||||
u8_cipher,
|
||||
u8_nonce,
|
||||
Nacl.util.decodeBase64(pub),
|
||||
Nacl.util.decodeBase64(curvePrivate)
|
||||
);
|
||||
return channel === Nacl.util.encodeUTF8(u8_plain);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
|
||||
var formHref = data.href;
|
||||
var cb = Utils.Util.once(_cb);
|
||||
var myKeys = {};
|
||||
var myFormKeys;
|
||||
var accessKeys;
|
||||
var CPNetflux, Pinpad;
|
||||
var network;
|
||||
var noDriveAnswered = false;
|
||||
nThen(function (w) {
|
||||
require([
|
||||
'chainpad-netflux',
|
||||
'/common/pinpad.js',
|
||||
], w(function (_CPNetflux, _Pinpad) {
|
||||
CPNetflux = _CPNetflux;
|
||||
Pinpad = _Pinpad;
|
||||
}));
|
||||
var personalDrive = !Cryptpad.initialTeam || Cryptpad.initialTeam === -1;
|
||||
Cryptpad.getAccessKeys(w(function (_keys) {
|
||||
if (!Array.isArray(_keys)) { return; }
|
||||
accessKeys = _keys;
|
||||
|
||||
_keys.some(function (_k) {
|
||||
if ((personalDrive && !_k.id) || Cryptpad.initialTeam === Number(_k.id)) {
|
||||
myKeys = _k;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}));
|
||||
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) {
|
||||
network = nw;
|
||||
}));
|
||||
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
|
||||
if (md && md.deleteLines) { deleteLines = true; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (!network) { return void cb({error: "E_CONNECT"}); }
|
||||
if (myFormKeys.formSeed) {
|
||||
myFormKeys = Cryptpad.getAnonymousKeys(myFormKeys.formSeed, data.channel, Utils);
|
||||
}
|
||||
var keys;
|
||||
var privateKey, publicKey;
|
||||
var formData;
|
||||
if (data.drive) {
|
||||
var secret = Utils.Hash.getSecrets('form', formHref, data.password);
|
||||
keys = secret && secret.keys;
|
||||
formData = Utils.Hash.getFormData(secret);
|
||||
} else {
|
||||
formData = Utils.Hash.getFormData(Utils.secret);
|
||||
keys = Utils.secret && Utils.secret.keys;
|
||||
}
|
||||
privateKey = formData.form_private;
|
||||
publicKey = formData.form_public;
|
||||
var curvePrivate = privateKey || data.privateKey;
|
||||
if (!curvePrivate) { return void cb({error: 'EFORBIDDEN'}); }
|
||||
var crypto = Utils.Crypto.Mailbox.createEncryptor({
|
||||
curvePrivate: curvePrivate,
|
||||
curvePublic: publicKey || data.publicKey,
|
||||
validateKey: data.validateKey
|
||||
});
|
||||
|
||||
var config = {
|
||||
network: network,
|
||||
channel: data.channel,
|
||||
noChainPad: true,
|
||||
validateKey: keys.secondaryValidateKey,
|
||||
owners: [myKeys.edPublic],
|
||||
crypto: crypto,
|
||||
metadata: {
|
||||
deleteLines: true
|
||||
}
|
||||
//Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers
|
||||
};
|
||||
var results = {};
|
||||
config.onError = function (info) {
|
||||
cb({ error: info.type });
|
||||
};
|
||||
config.onRejected = function (data, cb) {
|
||||
if (!Array.isArray(data) || !data.length || data[0].length !== 16) {
|
||||
return void cb(true);
|
||||
}
|
||||
if (!Array.isArray(accessKeys)) { return void cb(true); }
|
||||
network.historyKeeper = data[0];
|
||||
nThen(function (waitFor) {
|
||||
accessKeys.forEach(function (obj) {
|
||||
Pinpad.create(network, obj, waitFor(function (e) {
|
||||
if (e) { console.error(e); }
|
||||
}));
|
||||
});
|
||||
}).nThen(function () {
|
||||
cb();
|
||||
});
|
||||
};
|
||||
config.onReady = function () {
|
||||
var myKey;
|
||||
// If we have submitted an anonymous answer, retrieve it
|
||||
if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) {
|
||||
myKey = myFormKeys.curvePublic;
|
||||
}
|
||||
cb({
|
||||
noDriveAnswered: noDriveAnswered,
|
||||
myKey: myKey,
|
||||
results: results
|
||||
});
|
||||
network.disconnect();
|
||||
};
|
||||
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
|
||||
var parsed = Utils.Util.tryParse(msg);
|
||||
if (!parsed) { return; }
|
||||
var uid = parsed._uid || '000';
|
||||
|
||||
// If we have a "non-anonymous" answer, it may be the edition of a
|
||||
// previous anonymous answer. Check if a previous anonymous answer exists
|
||||
// with the same uid and delete it.
|
||||
if (parsed._proof) {
|
||||
var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
|
||||
var theirAnonKey = parsed._proof.key;
|
||||
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
|
||||
delete results[theirAnonKey][uid];
|
||||
}
|
||||
}
|
||||
|
||||
parsed._time = cfg && cfg.time;
|
||||
if (deleteLines) { parsed._hash = hash; }
|
||||
|
||||
if (data.cantEdit && results[senderCurve]
|
||||
&& results[senderCurve][uid]) { return; }
|
||||
results[senderCurve] = results[senderCurve] || {};
|
||||
results[senderCurve][uid] = {
|
||||
msg: parsed,
|
||||
hash: hash,
|
||||
time: cfg && cfg.time
|
||||
};
|
||||
};
|
||||
CPNetflux.start(config);
|
||||
});
|
||||
});
|
||||
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
|
||||
keys = { formSeed: noDriveSeed };
|
||||
}
|
||||
myKeys = keys;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
var myAnonymousKeys;
|
||||
if (data.anonymous) {
|
||||
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
|
||||
myKeys = Cryptpad.getAnonymousKeys(myKeys.formSeed, box.channel, Utils);
|
||||
} else {
|
||||
myAnonymousKeys = Cryptpad.getAnonymousKeys(myKeys.formSeed, box.channel, Utils);
|
||||
}
|
||||
var keys = Utils.secret && Utils.secret.keys;
|
||||
myKeys.signingKey = keys.secondarySignKey;
|
||||
|
||||
var ephemeral_keypair = Nacl.box.keyPair();
|
||||
var ephemeral_private = Nacl.util.encodeBase64(ephemeral_keypair.secretKey);
|
||||
myKeys.ephemeral_keypair = ephemeral_keypair;
|
||||
|
||||
if (myAnonymousKeys) {
|
||||
var proof = anonProof(box.channel, box.publicKey, myAnonymousKeys);
|
||||
data.results._proof = proof;
|
||||
}
|
||||
|
||||
var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys);
|
||||
var uid = data.results._uid || Utils.Util.uid();
|
||||
data.results._uid = uid;
|
||||
var text = JSON.stringify(data.results);
|
||||
var ciphertext = crypto.encrypt(text, box.publicKey);
|
||||
|
||||
var hash = ciphertext.slice(0,64);
|
||||
Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [
|
||||
box.channel,
|
||||
ciphertext
|
||||
], function (err, response) {
|
||||
Cryptpad.storeFormAnswer({
|
||||
uid: uid,
|
||||
channel: box.channel,
|
||||
hash: hash,
|
||||
curvePrivate: ephemeral_private,
|
||||
anonymous: Boolean(data.anonymous)
|
||||
}, function () {
|
||||
var res = data.results;
|
||||
res._isAnon = data.anonymous;
|
||||
res._time = +new Date();
|
||||
if (deleteLines) { res._hash = hash; }
|
||||
cb({
|
||||
error: err,
|
||||
response: response,
|
||||
results: res
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ALL_ANSWERS", function (data, cb) {
|
||||
if (!data || !data.channel) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.clearOwnedChannel(data, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ANSWER", function (data, cb) {
|
||||
if (!deleteLines) {
|
||||
return void cb({error: 'EFORBIDDEN'});
|
||||
}
|
||||
Cryptpad.deleteFormAnswers(data, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_MUTE", function (data, cb) {
|
||||
if (!Utils.secret) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.muteChannel(Utils.secret.channel, data.muted, cb);
|
||||
});
|
||||
};
|
||||
|
||||
return Handler;
|
||||
});
|
||||
@ -93,7 +93,11 @@ define([
|
||||
data[id] = TYPES[type].exportCSV(msg[key], form[key]);
|
||||
return;
|
||||
}
|
||||
data[id] = msg[key];
|
||||
if (type === 'date') {
|
||||
data[id] = new Date(msg[key]).toISOString();
|
||||
} else {
|
||||
data[id] = msg[key];
|
||||
}
|
||||
});
|
||||
r.push(data);
|
||||
});
|
||||
@ -115,7 +119,6 @@ define([
|
||||
var form = content.form;
|
||||
|
||||
var questions = [Messages.form_poll_time, Messages.share_formView];
|
||||
|
||||
order.forEach(function (key) {
|
||||
var obj = form[key];
|
||||
if (!obj) { return; }
|
||||
@ -162,12 +165,49 @@ define([
|
||||
return csv;
|
||||
};
|
||||
|
||||
Export.main = function (content, cb) {
|
||||
var json = Util.clone(content || {});
|
||||
delete json.answers;
|
||||
cb(new Blob([JSON.stringify(json, 0, 2)], {
|
||||
type: 'application/json;charset=utf-8'
|
||||
}));
|
||||
var getFullOrder = function (content) {
|
||||
var order = content.order.slice();
|
||||
var getSections = function (content) {
|
||||
var uids = Object.keys(content.form).filter(function (uid) {
|
||||
return content.form[uid].type === 'section';
|
||||
});
|
||||
return uids;
|
||||
};
|
||||
getSections(content).forEach(function (uid) {
|
||||
var block = content.form[uid];
|
||||
if (!block.opts || !Array.isArray(block.opts.questions)) { return; }
|
||||
var idx = order.indexOf(uid);
|
||||
if (idx === -1) { return; }
|
||||
idx++;
|
||||
block.opts.questions.forEach(function (el, i) {
|
||||
order.splice(idx+i, 0, el);
|
||||
});
|
||||
});
|
||||
return order;
|
||||
};
|
||||
|
||||
Export.main = function (content, cb, ext, sframeChan, parsed) {
|
||||
if (sframeChan && content.form) {
|
||||
var _answers = content["answers"];
|
||||
_answers['href'] = parsed.hash;
|
||||
_answers['password'] = parsed.password;
|
||||
_answers['drive'] = true;
|
||||
var answers;
|
||||
sframeChan.query("Q_FORM_FETCH_ANSWERS", _answers, function (err, obj) {
|
||||
answers = obj && obj.results;
|
||||
var types = {input: {}, textarea: {}, radio: {}, multiradio: {}, date: {}, checkbox: {}, multicheck: {}, sort: {}, poll: {}};
|
||||
var arr = Export.results(content, answers, types, getFullOrder(content), "json");
|
||||
cb(new Blob([arr], {
|
||||
type: 'application/json;charset=utf-8'
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
var json = Util.clone(content || {});
|
||||
delete json.answers;
|
||||
cb(new Blob([JSON.stringify(json, 0, 2)], {
|
||||
type: 'application/json;charset=utf-8'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return Export;
|
||||
|
||||
380
www/form/main.js
380
www/form/main.js
@ -8,9 +8,7 @@ define([
|
||||
'/api/config',
|
||||
'/common/dom-ready.js',
|
||||
'/common/sframe-common-outer.js',
|
||||
'/components/tweetnacl/nacl-fast.min.js',
|
||||
], function (nThen, ApiConfig, DomReady, SFCommonO) {
|
||||
var Nacl = window.nacl;
|
||||
|
||||
var href, hash;
|
||||
// Loaded in load #2
|
||||
@ -21,7 +19,6 @@ define([
|
||||
href = obj.href;
|
||||
hash = obj.hash;
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
var privateKey, publicKey;
|
||||
var channels = {};
|
||||
var getPropChannels = function () {
|
||||
return channels;
|
||||
@ -41,11 +38,11 @@ define([
|
||||
var validateKey = keys.secondaryValidateKey;
|
||||
meta.form_answerValidateKey = validateKey;
|
||||
|
||||
publicKey = meta.form_public = formData.form_public;
|
||||
privateKey = meta.form_private = formData.form_private;
|
||||
meta.form_public = formData.form_public;
|
||||
meta.form_private = formData.form_private;
|
||||
meta.form_auditorHash = formData.form_auditorHash;
|
||||
};
|
||||
var addRpc = function (sframeChan, Cryptpad, Utils) {
|
||||
var addRpc = function (sframeChan, Cryptpad) {
|
||||
sframeChan.on('EV_FORM_PIN', function (data) {
|
||||
channels.answersChannel = data.channel;
|
||||
Cryptpad.changeMetadata();
|
||||
@ -57,377 +54,6 @@ define([
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on('EV_EXPORT_SHEET', function (data) {
|
||||
if (!data || !Array.isArray(data.content)) { return; }
|
||||
sessionStorage.CP_formExportSheet = JSON.stringify(data);
|
||||
var href = Utils.Hash.hashToHref('', 'sheet');
|
||||
var a = window.open(href);
|
||||
if (!a) { sframeChan.event('EV_POPUP_BLOCKED'); }
|
||||
delete sessionStorage.CP_formExportSheet;
|
||||
});
|
||||
var getAnonymousKeys = function (formSeed, channel) {
|
||||
var array = Nacl.util.decodeBase64(formSeed + channel);
|
||||
var hash = Nacl.hash(array);
|
||||
var secretKey = Nacl.util.encodeBase64(hash.subarray(32));
|
||||
var publicKey = Utils.Hash.getCurvePublicFromPrivate(secretKey);
|
||||
return {
|
||||
curvePrivate: secretKey,
|
||||
curvePublic: publicKey,
|
||||
};
|
||||
};
|
||||
var u8_slice = function (A, start, end) {
|
||||
return new Uint8Array(Array.prototype.slice.call(A, start, end));
|
||||
};
|
||||
var u8_concat = function (A) {
|
||||
var length = 0;
|
||||
A.forEach(function (a) { length += a.length; });
|
||||
var total = new Uint8Array(length);
|
||||
|
||||
var offset = 0;
|
||||
A.forEach(function (a) {
|
||||
total.set(a, offset);
|
||||
offset += a.length;
|
||||
});
|
||||
return total;
|
||||
};
|
||||
var anonProof = function (channel, theirPub, anonKeys) {
|
||||
var u8_plain = Nacl.util.decodeUTF8(channel);
|
||||
var u8_nonce = Nacl.randomBytes(Nacl.box.nonceLength);
|
||||
var u8_cipher = Nacl.box(
|
||||
u8_plain,
|
||||
u8_nonce,
|
||||
Nacl.util.decodeBase64(theirPub),
|
||||
Nacl.util.decodeBase64(anonKeys.curvePrivate)
|
||||
);
|
||||
var u8_bundle = u8_concat([
|
||||
u8_nonce, // 24 uint8s
|
||||
u8_cipher, // arbitrary length
|
||||
]);
|
||||
return {
|
||||
key: anonKeys.curvePublic,
|
||||
proof: Nacl.util.encodeBase64(u8_bundle)
|
||||
};
|
||||
};
|
||||
var checkAnonProof = function (proofObj, channel, curvePrivate) {
|
||||
var pub = proofObj.key;
|
||||
var proofTxt = proofObj.proof;
|
||||
try {
|
||||
var u8_bundle = Nacl.util.decodeBase64(proofTxt);
|
||||
var u8_nonce = u8_slice(u8_bundle, 0, Nacl.box.nonceLength);
|
||||
var u8_cipher = u8_slice(u8_bundle, Nacl.box.nonceLength);
|
||||
var u8_plain = Nacl.box.open(
|
||||
u8_cipher,
|
||||
u8_nonce,
|
||||
Nacl.util.decodeBase64(pub),
|
||||
Nacl.util.decodeBase64(curvePrivate)
|
||||
);
|
||||
return channel === Nacl.util.encodeUTF8(u8_plain);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
var deleteLines = false; // "false" to support old forms
|
||||
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
|
||||
var cb = Utils.Util.once(_cb);
|
||||
var myKeys = {};
|
||||
var myFormKeys;
|
||||
var accessKeys;
|
||||
var CPNetflux, Pinpad;
|
||||
var network;
|
||||
var noDriveAnswered = false;
|
||||
nThen(function (w) {
|
||||
require([
|
||||
'chainpad-netflux',
|
||||
'/common/pinpad.js',
|
||||
], w(function (_CPNetflux, _Pinpad) {
|
||||
CPNetflux = _CPNetflux;
|
||||
Pinpad = _Pinpad;
|
||||
}));
|
||||
var personalDrive = !Cryptpad.initialTeam || Cryptpad.initialTeam === -1;
|
||||
Cryptpad.getAccessKeys(w(function (_keys) {
|
||||
if (!Array.isArray(_keys)) { return; }
|
||||
accessKeys = _keys;
|
||||
|
||||
_keys.some(function (_k) {
|
||||
if ((personalDrive && !_k.id) || Cryptpad.initialTeam === Number(_k.id)) {
|
||||
myKeys = _k;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}));
|
||||
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) {
|
||||
network = nw;
|
||||
}));
|
||||
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
|
||||
if (md && md.deleteLines) { deleteLines = true; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (!network) { return void cb({error: "E_CONNECT"}); }
|
||||
|
||||
if (myFormKeys.formSeed) {
|
||||
myFormKeys = getAnonymousKeys(myFormKeys.formSeed, data.channel);
|
||||
}
|
||||
|
||||
var keys = Utils.secret && Utils.secret.keys;
|
||||
|
||||
var curvePrivate = privateKey || data.privateKey;
|
||||
if (!curvePrivate) { return void cb({error: 'EFORBIDDEN'}); }
|
||||
var crypto = Utils.Crypto.Mailbox.createEncryptor({
|
||||
curvePrivate: curvePrivate,
|
||||
curvePublic: publicKey || data.publicKey,
|
||||
validateKey: data.validateKey
|
||||
});
|
||||
var config = {
|
||||
network: network,
|
||||
channel: data.channel,
|
||||
noChainPad: true,
|
||||
validateKey: keys.secondaryValidateKey,
|
||||
owners: [myKeys.edPublic],
|
||||
crypto: crypto,
|
||||
metadata: {
|
||||
deleteLines: true
|
||||
}
|
||||
//Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers
|
||||
};
|
||||
var results = {};
|
||||
config.onError = function (info) {
|
||||
cb({ error: info.type });
|
||||
};
|
||||
config.onRejected = function (data, cb) {
|
||||
if (!Array.isArray(data) || !data.length || data[0].length !== 16) {
|
||||
return void cb(true);
|
||||
}
|
||||
if (!Array.isArray(accessKeys)) { return void cb(true); }
|
||||
network.historyKeeper = data[0];
|
||||
nThen(function (waitFor) {
|
||||
accessKeys.forEach(function (obj) {
|
||||
Pinpad.create(network, obj, waitFor(function (e) {
|
||||
if (e) { console.error(e); }
|
||||
}));
|
||||
});
|
||||
}).nThen(function () {
|
||||
cb();
|
||||
});
|
||||
};
|
||||
config.onReady = function () {
|
||||
var myKey;
|
||||
// If we have submitted an anonymous answer, retrieve it
|
||||
if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) {
|
||||
myKey = myFormKeys.curvePublic;
|
||||
}
|
||||
cb({
|
||||
noDriveAnswered: noDriveAnswered,
|
||||
myKey: myKey,
|
||||
results: results
|
||||
});
|
||||
network.disconnect();
|
||||
};
|
||||
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
|
||||
var parsed = Utils.Util.tryParse(msg);
|
||||
if (!parsed) { return; }
|
||||
var uid = parsed._uid || '000';
|
||||
|
||||
// If we have a "non-anonymous" answer, it may be the edition of a
|
||||
// previous anonymous answer. Check if a previous anonymous answer exists
|
||||
// with the same uid and delete it.
|
||||
if (parsed._proof) {
|
||||
var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
|
||||
var theirAnonKey = parsed._proof.key;
|
||||
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
|
||||
delete results[theirAnonKey][uid];
|
||||
}
|
||||
}
|
||||
|
||||
parsed._time = cfg && cfg.time;
|
||||
if (deleteLines) { parsed._hash = hash; }
|
||||
|
||||
if (data.cantEdit && results[senderCurve]
|
||||
&& results[senderCurve][uid]) { return; }
|
||||
results[senderCurve] = results[senderCurve] || {};
|
||||
results[senderCurve][uid] = {
|
||||
msg: parsed,
|
||||
hash: hash,
|
||||
time: cfg && cfg.time
|
||||
};
|
||||
};
|
||||
CPNetflux.start(config);
|
||||
});
|
||||
});
|
||||
sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) {
|
||||
var answers = [];
|
||||
var myKeys;
|
||||
nThen(function (w) {
|
||||
Cryptpad.getFormKeys(w(function (keys) {
|
||||
myKeys = keys;
|
||||
}));
|
||||
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);
|
||||
}
|
||||
// Get the latest edit per uid
|
||||
var temp = {};
|
||||
obj.forEach(function (ans) {
|
||||
var uid = ans.uid || '000';
|
||||
temp[uid] = ans;
|
||||
});
|
||||
answers = Object.values(temp);
|
||||
}));
|
||||
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
|
||||
if (md && md.deleteLines) { deleteLines = true; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
var n = nThen;
|
||||
var err;
|
||||
var all = {};
|
||||
answers.forEach(function (answer) {
|
||||
n = n(function(waitFor) {
|
||||
var finalKeys = myKeys;
|
||||
if (answer.anonymous) {
|
||||
if (!myKeys.formSeed) {
|
||||
err = 'ANONYMOUS_ERROR';
|
||||
console.error('ANONYMOUS_ERROR', answer);
|
||||
return;
|
||||
}
|
||||
finalKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
|
||||
}
|
||||
Cryptpad.getHistoryRange({
|
||||
channel: data.channel,
|
||||
lastKnownHash: answer.hash,
|
||||
toHash: answer.hash,
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) { err = obj.error; return; }
|
||||
var messages = obj.messages;
|
||||
if (!messages.length) {
|
||||
// TODO delete from drive.forms?
|
||||
return;
|
||||
}
|
||||
if (obj.lastKnownHash !== answer.hash) { return; }
|
||||
try {
|
||||
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
|
||||
validateKey: data.validateKey,
|
||||
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
|
||||
my_private: Nacl.util.decodeBase64(finalKeys.curvePrivate),
|
||||
their_public: Nacl.util.decodeBase64(data.publicKey)
|
||||
});
|
||||
var parsed = JSON.parse(res.content);
|
||||
parsed._isAnon = answer.anonymous;
|
||||
parsed._time = messages[0].time;
|
||||
if (deleteLines) { parsed._hash = answer.hash; }
|
||||
var uid = parsed._uid || '000';
|
||||
if (all[uid] && !all[uid]._isAnon) { parsed._isAnon = false; }
|
||||
all[uid] = parsed;
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
}));
|
||||
}).nThen;
|
||||
});
|
||||
n(function () {
|
||||
if (err) { return void cb({error: err}); }
|
||||
cb(all);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
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
|
||||
keys = { formSeed: noDriveSeed };
|
||||
}
|
||||
myKeys = keys;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
var myAnonymousKeys;
|
||||
if (data.anonymous) {
|
||||
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
|
||||
myKeys = getAnonymousKeys(myKeys.formSeed, box.channel);
|
||||
} else {
|
||||
myAnonymousKeys = getAnonymousKeys(myKeys.formSeed, box.channel);
|
||||
}
|
||||
var keys = Utils.secret && Utils.secret.keys;
|
||||
myKeys.signingKey = keys.secondarySignKey;
|
||||
|
||||
var ephemeral_keypair = Nacl.box.keyPair();
|
||||
var ephemeral_private = Nacl.util.encodeBase64(ephemeral_keypair.secretKey);
|
||||
myKeys.ephemeral_keypair = ephemeral_keypair;
|
||||
|
||||
if (myAnonymousKeys) {
|
||||
var proof = anonProof(box.channel, box.publicKey, myAnonymousKeys);
|
||||
data.results._proof = proof;
|
||||
}
|
||||
|
||||
var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys);
|
||||
var uid = data.results._uid || Utils.Util.uid();
|
||||
data.results._uid = uid;
|
||||
var text = JSON.stringify(data.results);
|
||||
var ciphertext = crypto.encrypt(text, box.publicKey);
|
||||
|
||||
var hash = ciphertext.slice(0,64);
|
||||
Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [
|
||||
box.channel,
|
||||
ciphertext
|
||||
], function (err, response) {
|
||||
Cryptpad.storeFormAnswer({
|
||||
uid: uid,
|
||||
channel: box.channel,
|
||||
hash: hash,
|
||||
curvePrivate: ephemeral_private,
|
||||
anonymous: Boolean(data.anonymous)
|
||||
}, function () {
|
||||
var res = data.results;
|
||||
res._isAnon = data.anonymous;
|
||||
res._time = +new Date();
|
||||
if (deleteLines) { res._hash = hash; }
|
||||
cb({
|
||||
error: err,
|
||||
response: response,
|
||||
results: res
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ALL_ANSWERS", function (data, cb) {
|
||||
if (!data || !data.channel) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.clearOwnedChannel(data, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ANSWER", function (data, cb) {
|
||||
if (!deleteLines) {
|
||||
return void cb({error: 'EFORBIDDEN'});
|
||||
}
|
||||
Cryptpad.deleteFormAnswers(data, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_MUTE", function (data, cb) {
|
||||
if (!Utils.secret) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.muteChannel(Utils.secret.channel, data.muted, cb);
|
||||
});
|
||||
};
|
||||
SFCommonO.start({
|
||||
addData: addData,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user