Convert missing modules

This commit is contained in:
yflory 2024-10-14 16:49:35 +02:00
parent 503a7a08b5
commit df789f8284
47 changed files with 20042 additions and 17 deletions

View File

@ -9,7 +9,7 @@ import json from '@rollup/plugin-json';
export default {
//input: "./_src/worker/index.ts",
input: "./_src/worker/store.ts",
input: "./src/worker/store.ts",
output: {
name: 'cryptpad-worker',
file: "./_build/worker.bundle.js",

View File

@ -2,6 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
module.exports = require("../../www/common/outer/roster.js");
module.exports = require("../../src/worker/components/roster.js");

210
src/common/cache-store.js Normal file
View File

@ -0,0 +1,210 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Util, localForage) => {
let window = globalThis;
let self = globalThis;
var S = window.CryptPad_Cache = {};
var onReady = Util.mkEvent(true);
// Check if indexedDB is allowed
var allowed = false;
var disabled = false;
var supported = false;
try {
var request = window.indexedDB.open('test_db', 1);
request.onsuccess = function () {
supported = true;
allowed = supported && !disabled;
onReady.fire();
};
request.onerror = function () {
onReady.fire();
};
} catch (e) {
onReady.fire();
}
S.enable = function () {
disabled = false;
allowed = supported && !disabled;
};
S.disable = function () {
disabled = true;
allowed = supported && !disabled;
};
var cache = localForage.createInstance({
driver: localForage.INDEXEDDB,
name: "cp_cache"
});
S.getBlobCache = function (id, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.getItem(id, function (err, obj) {
if (err || !obj || !obj.c) {
return void cb(Util.serializeError(err || 'EINVAL'));
}
cb(null, obj.c);
obj.t = +new Date();
cache.setItem(id, obj, function (err) {
if (!err) { return; }
console.error(err);
});
});
});
};
S.setBlobCache = function (id, u8, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
if (!u8) { return void cb('EINVAL'); }
cache.setItem(id, {
c: u8,
t: (+new Date()) // 't' represent the "lastAccess" of this cache (get or set)
}, function (err) {
cb(Util.serializeError(err));
});
});
};
// id: channel ID or blob ID
// returns array of messages
S.getChannelCache = function (id, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.getItem(id, function (err, obj) {
if (err || !obj || !Array.isArray(obj.c)) {
return void cb(Util.serializeError(err || 'EINVAL'));
}
cb(null, obj);
obj.t = +new Date();
cache.setItem(id, obj, function (err) {
if (!err) { return; }
console.error(err);
});
});
});
};
// Keep the last two checkpoint + any checkpoint that may exist in the last 100 messages
// FIXME: duplicate system with sliceCpIndex from lib/hk-util.js
var checkCheckpoints = function (array) {
if (!Array.isArray(array)) { return; }
// Keep the last 100 messages
if (array.length > 100) { // FIXME this behaviour is only valid for chainpad-style documents
array.splice(0, array.length - 100);
}
// Remove every message before the first checkpoint
var firstCpIdx;
array.some(function (el, i) {
if (!el.isCheckpoint) { return; }
firstCpIdx = i;
return true;
});
array.splice(0, firstCpIdx);
};
var t = {};
S.storeCache = function (id, validateKey, val, onError) {
onError = Util.once(Util.mkAsync(onError || function () {}));
onReady.reg(function () {
// Make a throttle or use the existing one to avoid calling
// storeCache with the same array multiple times
t[id] = t[id] || Util.throttle(function (validateKey, val, onError) {
if (!allowed) { return void onError('NOCACHE'); }
if (!Array.isArray(val) || !validateKey) { return void onError('EINVAL'); }
checkCheckpoints(val);
cache.setItem(id, {
k: validateKey,
c: val,
t: (+new Date()) // 't' represent the "lastAccess" of this cache (get or set)
}, function (err) {
if (err) { onError(Util.serializeError(err)); }
});
}, 50);
t[id](validateKey, val, onError);
});
};
S.leaveChannel = function (id) {
delete t[id];
};
S.clearChannel = function (id, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.removeItem(id, function () {
cb();
});
});
};
S.clear = function (cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.clear(cb);
});
};
S.getKeys = function (cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.keys().then(function (keys) {
cb(null, keys);
}).catch(function (err) {
cb(err);
});
});
};
S.getTime = function (id, cb) {
cb = Util.once(Util.mkAsync(cb || function () {}));
onReady.reg(function () {
if (!allowed) { return void cb('NOCACHE'); }
cache.getItem(id, function (err, obj) {
if (err || !obj || !obj.c) {
return void cb(Util.serializeError(err || 'EINVAL'));
}
cb(null, obj.t);
});
});
};
self.CryptPad_clearIndexedDB = S.clear;
return S;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require('./common-util'),
require('localforage'),
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/components/localforage/dist/localforage.min.js',
], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = function (AppConfig = {}) {
return {
setCustomize: data => {
AppConfig = data.AppConfig;
},
// localStorage
userHashKey: 'User_hash',
userNameKey: 'User_name',
blockHashKey: 'Block_hash',
fileHashKey: 'FS_hash',
sessionJWT: 'Session_JWT',
ssoSeed: 'SSO_seed',
// Store
displayNameKey: 'cryptpad.username',
oldStorageKey: 'CryptPad_RECENTPADS',
storageKey: 'filesData',
tokenKey: 'loginToken',
prefersDriveRedirectKey: 'prefersDriveRedirect',
isPremiumKey: 'isPremiumUser',
displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated',
MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 5,
MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 5,
MAX_PREMIUM_TEAMS_SLOTS: Math.max(AppConfig.maxTeamsSlots || 0, AppConfig.maxPremiumTeamsSlots || 0) || 5,
MAX_PREMIUM_TEAMS_OWNED: Math.max(AppConfig.maxTeamsOwned || 0, AppConfig.maxPremiumTeamsOwned || 0) || 5,
// Apps
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications', 'calendar', 'moderation', 'oldadmin'], // XXX oldadmin
earlyAccessApps: ['doc', 'presentation']
};
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(undefined);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define(['/customize/application_config.js'], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(function () {
var factory = function (AppConfig = {}, Scrypt) {
var Cred = {};
Cred.setCustomize = data => {
AppConfig = data.AppConfig;
};
Cred.MINIMUM_PASSWORD_LENGTH = typeof(AppConfig.minimumPasswordLength) === 'number'?
AppConfig.minimumPasswordLength: 8; // TODO 14 or higher is a decent default for 2023
Cred.MINIMUM_NAME_LENGTH = 1;
Cred.MAXIMUM_NAME_LENGTH = 64;
// https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
Cred.isEmail = function (email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
};
Cred.isLongEnoughPassword = function (passwd) {
return passwd.length >= Cred.MINIMUM_PASSWORD_LENGTH;
};
var isString = Cred.isString = function (x) {
return typeof(x) === 'string';
};
// Maximum username length is enforced at registration time
// rather than in this function
// in order to maintain backwards compatibility with accounts
// that might have already registered with a longer name.
Cred.isValidUsername = function (name) {
return !!(isString(name) && name.length >= Cred.MINIMUM_NAME_LENGTH);
};
Cred.isValidPassword = function (passwd) {
return !!(passwd && isString(passwd));
};
Cred.passwordsMatch = function (a, b) {
return isString(a) && isString(b) && a === b;
};
Cred.customSalt = function () {
return typeof(AppConfig.loginSalt) === 'string'?
AppConfig.loginSalt: '';
};
Cred.deriveFromPassphrase = function (username, password, len, cb) {
Scrypt(password,
username + Cred.customSalt(), // salt
8, // memoryCost (n)
1024, // block size parameter (r)
len || 128, // dkLen
200, // interruptStep
cb,
undefined); // format, could be 'base64'
};
Cred.dispenser = function (bytes) {
var entropy = {
used: 0,
};
// crypto hygeine
var consume = function (n) {
// explode if you run out of bytes
if (entropy.used + n > bytes.length) {
throw new Error('exceeded available entropy');
}
if (typeof(n) !== 'number') { throw new Error('expected a number'); }
if (n <= 0) {
throw new Error('expected to consume a positive number of bytes');
}
// grab an unused slice of the entropy
// Note: Internet Explorer doesn't support .slice on Uint8Array
var A;
if (bytes.slice) {
A = bytes.slice(entropy.used, entropy.used + n);
} else {
A = bytes.subarray(entropy.used, entropy.used + n);
}
// account for the bytes you used so you don't reuse bytes
entropy.used += n;
//console.info("%s bytes of entropy remaining", bytes.length - entropy.used);
return A;
};
return consume;
};
return Cred;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
undefined, //require("../../customize.dist/application_config.js"),
require("scrypt-async")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/customize/application_config.js',
'/components/scrypt-async/scrypt-async.min.js',
], function (AppConfig) {
return factory(AppConfig, window.scrypt);
});
}
}());

View File

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (AppConfig = {}, Messages= {}) => {
var Feedback = {};
Feedback.setCustomize = data => {
Messages = data.Messages;
AppConfig = data.AppConfig;
};
Feedback.init = function (state) {
Feedback.state = state;
};
var randomToken = function () {
return Math.random().toString(16).replace(/0./, '');
};
var ajax = function (url, cb) {
var http = new XMLHttpRequest();
http.open('HEAD', url);
http.onreadystatechange = function() {
if (this.readyState === this.DONE) {
if (cb) { cb(); }
}
};
http.send();
};
Feedback.send = function (action, force, cb) {
if (typeof(cb) !== 'function') { cb = function () {}; }
if (AppConfig.disableFeedback) { return void cb(); }
if (!action) { return void cb(); }
if (force !== true) {
if (!Feedback.state) { return void cb(); }
}
var href = '/common/feedback.html?' + action + '=' + randomToken();
ajax(href, cb);
};
Feedback.reportAppUsage = function () {
var pattern = window.location.pathname.split('/')
.filter(function (x) { return x; }).join('.');
if (/^#\/1\/view\//.test(window.location.hash)) {
Feedback.send(pattern + '_VIEW');
} else {
Feedback.send(pattern);
}
};
Feedback.reportScreenDimensions = function () {
var h = window.innerHeight;
var w = window.innerWidth;
Feedback.send('DIMENSIONS:' + h + 'x' + w);
};
Feedback.reportLanguage = function () {
if (!Messages) { return; }
Feedback.send('LANG_' + Messages._languageUsed);
};
return Feedback;
};
if (typeof(module) !== 'undefined' && module.exports) {
// Code from customize can't be laoded directly in the build
module.exports = factory(undefined, undefined);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/customize/application_config.js',
'/customize/messages.js'
], factory);
} else {
// unsupported initialization
}
})();

766
src/common/common-hash.js Normal file
View File

@ -0,0 +1,766 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(function (window) {
var factory = function (Util, Crypto, Keys, Nacl) {
var Hash = window.CryptPad_Hash = {};
var uint8ArrayToHex = Util.uint8ArrayToHex;
var hexToBase64 = Util.hexToBase64;
var base64ToHex = Util.base64ToHex;
Hash.encodeBase64 = Nacl.util.encodeBase64;
Hash.decodeBase64 = Nacl.util.decodeBase64;
// This implementation must match that on the server
// it's used for a checksum
Hash.hashChannelList = function (list) {
return Nacl.util.encodeBase64(Nacl.hash(Nacl.util
.decodeUTF8(JSON.stringify(list))));
};
Hash.generateSignPair = function () {
var ed = Nacl.sign.keyPair();
var makeSafe = function (key) {
return Crypto.b64RemoveSlashes(key).replace(/=+$/g, '');
};
return {
validateKey: Hash.encodeBase64(ed.publicKey),
signKey: Hash.encodeBase64(ed.secretKey),
safeValidateKey: makeSafe(Hash.encodeBase64(ed.publicKey)),
safeSignKey: makeSafe(Hash.encodeBase64(ed.secretKey)),
};
};
Hash.getSignPublicFromPrivate = function (edPrivateSafeStr) {
var edPrivateStr = Crypto.b64AddSlashes(edPrivateSafeStr);
var privateKey = Nacl.util.decodeBase64(edPrivateStr);
var keyPair = Nacl.sign.keyPair.fromSecretKey(privateKey);
return Nacl.util.encodeBase64(keyPair.publicKey);
};
Hash.getCurvePublicFromPrivate = function (curvePrivateSafeStr) {
var curvePrivateStr = Crypto.b64AddSlashes(curvePrivateSafeStr);
var privateKey = Nacl.util.decodeBase64(curvePrivateStr);
var keyPair = Nacl.box.keyPair.fromSecretKey(privateKey);
return Nacl.util.encodeBase64(keyPair.publicKey);
};
var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) {
var version = secret.version;
var data = secret.keys;
if (version === 0) {
return secret.channel + secret.key;
}
if (version === 1) {
if (!data.editKeyStr) { return; }
return '/1/edit/' + hexToBase64(secret.channel) +
'/' + Crypto.b64RemoveSlashes(data.editKeyStr) + '/';
}
if (version === 2) {
if (!data.editKeyStr) { return; }
var pass = secret.password ? 'p/' : '';
return '/2/' + secret.type + '/edit/' + Crypto.b64RemoveSlashes(data.editKeyStr) + '/' + pass;
}
};
var getViewHashFromKeys = Hash.getViewHashFromKeys = function (secret) {
var version = secret.version;
var data = secret.keys;
if (version === 0) { return; }
if (version === 1) {
if (!data.viewKeyStr) { return; }
return '/1/view/' + hexToBase64(secret.channel) +
'/'+Crypto.b64RemoveSlashes(data.viewKeyStr)+'/';
}
if (version === 2) {
if (!data.viewKeyStr) { return; }
var pass = secret.password ? 'p/' : '';
return '/2/' + secret.type + '/view/' + Crypto.b64RemoveSlashes(data.viewKeyStr) + '/' + pass;
}
};
Hash.getHiddenHashFromKeys = function (type, secret, opts) {
opts = opts || {};
var canEdit = (secret.keys && secret.keys.editKeyStr) || secret.key;
var mode = (!opts.view && canEdit) ? 'edit/' : 'view/';
var pass = secret.password ? 'p/' : '';
if (secret.keys && secret.keys.fileKeyStr) { mode = ''; }
var hash = '/3/' + type + '/' + mode + secret.channel + '/' + pass;
var hashData = Hash.parseTypeHash(type, hash);
if (hashData && hashData.getHash) {
return hashData.getHash(opts || {});
}
return hash;
};
var getFileHashFromKeys = Hash.getFileHashFromKeys = function (secret) {
var version = secret.version;
var data = secret.keys;
if (version === 0) { return; }
if (version === 1) {
return '/1/' + hexToBase64(secret.channel) + '/' +
Crypto.b64RemoveSlashes(data.fileKeyStr) + '/';
}
if (version === 2) {
if (!data.fileKeyStr) { return; }
var pass = secret.password ? 'p/' : '';
return '/2/' + secret.type + '/' + Crypto.b64RemoveSlashes(data.fileKeyStr) + '/' + pass;
}
};
Hash.getPublicSigningKeyString = Keys.serialize;
var fixDuplicateSlashes = function (s) {
return s.replace(/\/+/g, '/');
};
Hash.ephemeralChannelLength = 34;
Hash.createChannelId = function (ephemeral) {
var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(ephemeral? 17: 16));
if ([32, 34].indexOf(id.length) === -1 || /[^a-f0-9]/.test(id)) {
throw new Error('channel ids must consist of 32 hex characters');
}
return id;
};
/* Given a base64-encoded public key, deterministically derive a channel id
Used for support mailboxes
*/
Hash.getChannelIdFromKey = function (publicKey) {
if (!publicKey) { return; }
return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16));
};
/* Given a base64-encoded asymmetric private key
derive the corresponding public key
*/
Hash.getBoxPublicFromSecret = function (priv) {
if (!priv) { return; }
var u8_priv = Hash.decodeBase64(priv);
var pair = Nacl.box.keyPair.fromSecretKey(u8_priv);
return Hash.encodeBase64(pair.publicKey);
};
/* Given a base64-encoded private key and public key
check that the keys are part of a valid keypair
*/
Hash.checkBoxKeyPair = function (priv, pub) {
if (!pub || !priv) { return false; }
var u8_priv = Hash.decodeBase64(priv);
var pair = Nacl.box.keyPair.fromSecretKey(u8_priv);
return pub === Hash.encodeBase64(pair.publicKey);
};
Hash.createRandomHash = function (type, password) {
var cryptor;
if (type === 'file') {
cryptor = Crypto.createFileCryptor2(void 0, password);
return getFileHashFromKeys({
password: Boolean(password),
version: 2,
type: type,
keys: cryptor
});
}
cryptor = Crypto.createEditCryptor2(void 0, void 0, password);
return getEditHashFromKeys({
password: Boolean(password),
version: 2,
type: type,
keys: cryptor
});
};
/*
Version 0
/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy
Version 1: Add support for read-only access
/code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI
Version 2: Add support for password-protection
/code/#/2/code/edit/u5ACvxAYmhvG0FtrNn9FJQcf/p/
Version 3: Safe links
/code/#/3/code/edit/f0d8055aa640a97e7fd25020ca4e93b3/
Version 4: Data URL when not a realtime link yet (new pad or "static" app)
/login/#/4/login/newpad=eyJocmVmIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL2NvZGUvIy8yL2NvZGUvZWRpdC91NUFDdnhBWW1odkcwRnRyTm45RklRY2YvIn0%3D/
/drive/#/4/drive/login=e30%3D/
*/
var getLoginOpts = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
hashArr.some(function (data) {
if (/^login=/.test(data)) {
k = data.slice(6);
return true;
}
});
return k || '';
};
var getNewPadOpts = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
hashArr.some(function (data) {
if (/^newpad=/.test(data)) {
k = data.slice(7);
return true;
}
});
return k || '';
};
var getVersionHash = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
hashArr.some(function (data) {
if (/^hash=/.test(data)) {
k = data.slice(5);
return true;
}
});
return k ? Crypto.b64AddSlashes(k) : '';
};
var getAuditorKey = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
hashArr.some(function (data) {
if (/^auditor=/.test(data)) {
k = data.slice(8);
return true;
}
});
return k ? Crypto.b64AddSlashes(k) : '';
};
var getOwnerKey = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
hashArr.some(function (data) {
if (data.length === 86) {
k = data;
return true;
}
});
return k;
};
var parseTypeHash = Hash.parseTypeHash = function (type, hash) {
if (!hash) { return; }
var options = [];
var parsed = {};
var hashArr = fixDuplicateSlashes(hash).split('/');
var addOptions = function () {
parsed.password = options.indexOf('p') !== -1;
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.versionHash = getVersionHash(options);
parsed.auditorKey = getAuditorKey(options);
parsed.newPadOpts = getNewPadOpts(options);
parsed.loginOpts = getLoginOpts(options);
parsed.ownerKey = getOwnerKey(options);
};
// Version 4: only login or newpad options, same for all the apps
if (hashArr[1] && hashArr[1] === '4') {
parsed.getHash = function (opts) {
if (!opts || !Object.keys(opts).length) { return ''; }
var hash = '/4/' + type + '/';
if (opts.newPadOpts) { hash += 'newpad=' + opts.newPadOpts + '/'; }
if (opts.loginOpts) { hash += 'login=' + opts.loginOpts + '/'; }
return hash;
};
parsed.getOptions = function () {
var options = {};
if (parsed.newPadOpts) { options.newPadOpts = parsed.newPadOpts; }
if (parsed.loginOpts) { options.loginOpts = parsed.loginOpts; }
return options;
};
parsed.version = 4;
parsed.app = hashArr[2];
options = hashArr.slice(3);
addOptions();
return parsed;
}
// The other versions depends on the type
if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) {
parsed.type = 'pad';
parsed.getHash = function () {
return hash;
};
parsed.getOptions = function () {
return {
embed: parsed.embed,
present: parsed.present,
ownerKey: parsed.ownerKey,
versionHash: parsed.versionHash,
auditorKey: parsed.auditorKey,
newPadOpts: parsed.newPadOpts,
loginOpts: parsed.loginOpts,
password: parsed.password
};
};
if (hash.slice(0,1) !== '/' && hash.length >= 56) { // Version 0
// Old hash
parsed.channel = hash.slice(0, 32);
parsed.key = hash.slice(32, 56);
parsed.version = 0;
return parsed;
}
// Version >= 1: more hash options
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 5).join('/') + '/';
var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey;
if (owner) { hash += owner + '/'; }
if (parsed.password || opts.password) { hash += 'p/'; }
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }
var versionHash = typeof(opts.versionHash) !== "undefined" ? opts.versionHash : parsed.versionHash;
if (versionHash) {
hash += 'hash=' + Crypto.b64RemoveSlashes(versionHash) + '/';
}
var auditorKey = typeof(opts.auditorKey) !== "undefined" ? opts.auditorKey : parsed.auditorKey;
if (auditorKey) {
hash += 'auditor=' + Crypto.b64RemoveSlashes(auditorKey) + '/';
}
if (opts.newPadOpts) { hash += 'newpad=' + opts.newPadOpts + '/'; }
if (opts.loginOpts) { hash += 'login=' + opts.loginOpts + '/'; }
return hash;
};
if (hashArr[1] && hashArr[1] === '1') { // Version 1
parsed.version = 1;
parsed.mode = hashArr[2];
parsed.channel = hashArr[3];
parsed.key = Crypto.b64AddSlashes(hashArr[4]);
options = hashArr.slice(5);
addOptions();
return parsed;
}
if (hashArr[1] && hashArr[1] === '2') { // Version 2
parsed.version = 2;
parsed.app = hashArr[2];
parsed.mode = hashArr[3];
parsed.key = hashArr[4];
options = hashArr.slice(5);
addOptions();
return parsed;
}
if (hashArr[1] && hashArr[1] === '3') { // Version 3: hidden hash
parsed.version = 3;
parsed.app = hashArr[2];
parsed.mode = hashArr[3];
parsed.channel = hashArr[4];
options = hashArr.slice(5);
addOptions();
return parsed;
}
return parsed;
}
parsed.getHash = function () { return hashArr.join('/'); };
if (['media', 'file'].indexOf(type) !== -1) {
parsed.type = 'file';
parsed.getOptions = function () {
return {
embed: parsed.embed,
present: parsed.present,
ownerKey: parsed.ownerKey,
newPadOpts: parsed.newPadOpts,
loginOpts: parsed.loginOpts,
password: parsed.password
};
};
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 4).join('/') + '/';
var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey;
if (owner) { hash += owner + '/'; }
if (parsed.password || opts.password) { hash += 'p/'; }
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }
if (opts.newPadOpts) { hash += 'newpad=' + opts.newPadOpts + '/'; }
if (opts.loginOpts) { hash += 'login=' + opts.loginOpts + '/'; }
return hash;
};
if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1;
parsed.channel = hashArr[2].replace(/-/g, '/');
parsed.key = hashArr[3].replace(/-/g, '/');
options = hashArr.slice(4);
addOptions();
return parsed;
}
if (hashArr[1] && hashArr[1] === '2') { // Version 2
parsed.version = 2;
parsed.app = hashArr[2];
parsed.key = hashArr[3];
options = hashArr.slice(4);
addOptions();
return parsed;
}
if (hashArr[1] && hashArr[1] === '3') { // Version 3: hidden hash
parsed.version = 3;
parsed.app = hashArr[2];
parsed.channel = hashArr[3];
options = hashArr.slice(4);
addOptions();
return parsed;
}
return parsed;
}
if (['user'].indexOf(type) !== -1) {
parsed.type = 'user';
if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1;
parsed.user = hashArr[2];
parsed.pubkey = hashArr[3].replace(/-/g, '/');
return parsed;
}
return parsed;
}
if (['invite'].indexOf(type) !== -1) {
parsed.type = 'invite';
if (hashArr[1] && hashArr[1] === '2') {
parsed.version = 2;
parsed.app = hashArr[2];
parsed.mode = hashArr[3];
parsed.key = hashArr[4];
options = hashArr.slice(5);
parsed.password = options.indexOf('p') !== -1;
return parsed;
}
return parsed;
}
return;
};
var parsePadUrl = Hash.parsePadUrl = function (href) {
var patt = /^https*:\/\/([^\/]*)\/(.*?)\//i;
var ret = {};
if (!href) { return ret; }
if (href.slice(-1) !== '/' && href.slice(-1) !== '#') { href += '/'; }
href = href.replace(/\/\?[^#]+#/, '/#');
var idx;
// When we start without a hash, use version 4 links to add login or newpad options
var getHash = function (opts) {
if (!opts || !Object.keys(opts).length) { return ''; }
var hash = '/4/' + ret.type + '/';
if (opts.newPadOpts) { hash += 'newpad=' + opts.newPadOpts + '/'; }
if (opts.loginOpts) { hash += 'login=' + opts.loginOpts + '/'; }
return hash;
};
ret.getUrl = function (options) {
options = options || {};
var url = '/';
if (!ret.type) { return url; }
url += ret.type + '/';
// New pad with options: append the options to the hash
if (!ret.hashData && options && Object.keys(options).length) {
return url + '#' + getHash(options);
}
if (!ret.hashData) { return url; }
//if (ret.hashData.version === 0) { return url + '#' + ret.hash; }
//if (ret.hashData.type !== 'pad') { return url + '#' + ret.hash; }
var hash = ret.hashData.getHash(options);
url += '#' + hash;
return url;
};
ret.getOptions = function () {
if (!ret.hashData || !ret.hashData.getOptions) { return {}; }
return ret.hashData.getOptions();
};
if (!/^https*:\/\//.test(href)) {
// If it doesn't start with http(s), it should be a relative href
if (!/^\/($|[^\/])/.test(href)) { return ret; }
idx = href.indexOf('/#');
ret.type = href.slice(1, idx);
if (idx === -1) { return ret; }
ret.hash = href.slice(idx + 2);
ret.hashData = parseTypeHash(ret.type, ret.hash);
return ret;
}
href.replace(patt, function (a, domain, type) {
ret.domain = domain;
ret.type = type;
return '';
});
idx = href.indexOf('/#');
if (idx === -1) { return ret; }
ret.hash = href.slice(idx + 2);
ret.hashData = parseTypeHash(ret.type, ret.hash);
return ret;
};
Hash.hashToHref = function (hash, type) {
return '/' + type + '/#' + hash;
};
Hash.hrefToHash = function (href) {
var parsed = Hash.parsePadUrl(href);
return parsed.hash;
};
Hash.getRelativeHref = function (href) {
if (!href) { return; }
if (href.indexOf('#') === -1) { return; }
var parsed = parsePadUrl(href);
return '/' + parsed.type + '/#' + parsed.hash;
};
/*
* Returns all needed keys for a realtime channel
* - no argument: use the URL hash or create one if it doesn't exist
* - secretHash provided: use secretHash to find the keys
*/
Hash.getSecrets = function (type, secretHash, password) {
var secret = {};
var generate = function () {
secret.keys = Crypto.createEditCryptor2(void 0, void 0, password);
secret.channel = base64ToHex(secret.keys.chanId);
secret.version = 2;
secret.type = type;
};
if (!secretHash) {
generate();
return secret;
} else {
var parsed;
var hash;
if (secretHash) {
if (!type) { throw new Error("getSecrets with a hash requires a type parameter"); }
parsed = parseTypeHash(type, secretHash);
hash = secretHash;
}
if (hash.length === 0) {
generate();
return secret;
}
// old hash system : #{hexChanKey}{cryptKey}
// new hash system : #/{hashVersion}/{b64ChanKey}/{cryptKey}
if (parsed.version === 0) {
// Old hash
secret.channel = parsed.channel;
secret.key = parsed.key;
secret.version = 0;
} else if (parsed.version === 1) {
// New hash
secret.version = 1;
if (parsed.type === "pad") {
secret.channel = base64ToHex(parsed.channel);
if (parsed.mode === 'edit') {
secret.keys = Crypto.createEditCryptor(parsed.key);
secret.key = secret.keys.editKeyStr;
if (secret.channel.length !== 32 || secret.key.length !== 24) {
throw new Error("The channel key and/or the encryption key is invalid");
}
}
else if (parsed.mode === 'view') {
secret.keys = Crypto.createViewCryptor(parsed.key);
if (secret.channel.length !== 32) {
throw new Error("The channel key is invalid");
}
}
} else if (parsed.type === "file") {
secret.channel = base64ToHex(parsed.channel);
secret.keys = {
fileKeyStr: parsed.key,
cryptKey: Nacl.util.decodeBase64(parsed.key)
};
} else if (parsed.type === "user") {
throw new Error("User hashes can't be opened (yet)");
}
} else if (parsed.version === 2) {
// New hash
secret.version = 2;
secret.type = type;
secret.password = password;
if (parsed.type === "pad") {
if (parsed.mode === 'edit') {
secret.keys = Crypto.createEditCryptor2(parsed.key, void 0, password);
secret.channel = base64ToHex(secret.keys.chanId);
secret.key = secret.keys.editKeyStr;
if (secret.channel.length !== 32 || secret.key.length !== 24) {
throw new Error("The channel key and/or the encryption key is invalid");
}
}
else if (parsed.mode === 'view') {
secret.keys = Crypto.createViewCryptor2(parsed.key, password);
secret.channel = base64ToHex(secret.keys.chanId);
if (secret.channel.length !== 32) {
throw new Error("The channel key is invalid");
}
}
} else if (parsed.type === "file") {
secret.keys = Crypto.createFileCryptor2(parsed.key, password);
secret.channel = base64ToHex(secret.keys.chanId);
secret.key = secret.keys.fileKeyStr;
if (secret.channel.length !== 48 || secret.key.length !== 24) {
throw new Error("The channel key and/or the encryption key is invalid");
}
} else if (parsed.type === "user") {
throw new Error("User hashes can't be opened (yet)");
}
}
}
return secret;
};
Hash.getHashes = function (secret) {
var hashes = {};
secret = JSON.parse(JSON.stringify(secret));
if (!secret.keys && !secret.key) {
return hashes;
} else if (!secret.keys) {
secret.keys = {};
}
if (secret.keys.editKeyStr || (secret.version === 0 && secret.key)) {
hashes.editHash = getEditHashFromKeys(secret);
}
if (secret.keys.viewKeyStr) {
hashes.viewHash = getViewHashFromKeys(secret);
}
if (secret.keys.fileKeyStr) {
hashes.fileHash = getFileHashFromKeys(secret);
}
return hashes;
};
Hash.getFormData = function (secret, hash, password) {
secret = secret || Hash.getSecrets('form', hash, password);
var keys = secret && secret.keys;
var secondary = keys && keys.secondaryKey;
if (!secondary) { return; }
var curvePair = Nacl.box.keyPair.fromSecretKey(Nacl.util.decodeUTF8(secondary).slice(0,32));
var ret = {};
ret.form_public = Nacl.util.encodeBase64(curvePair.publicKey);
var privateKey = ret.form_private = Nacl.util.encodeBase64(curvePair.secretKey);
var auditorHash = Hash.getViewHashFromKeys({
version: 1,
channel: secret.channel,
keys: { viewKeyStr: Nacl.util.encodeBase64(keys.cryptKey) }
});
var _parsed = Hash.parseTypeHash('pad', auditorHash);
ret.form_auditorHash = _parsed.getHash({auditorKey: privateKey});
return ret;
};
// STORAGE
Hash.hrefToHexChannelId = function (href, password) {
var parsed = Hash.parsePadUrl(href);
if (!parsed || !parsed.hash) { return; }
var secret = Hash.getSecrets(parsed.type, parsed.hash, password);
return secret.channel;
};
Hash.getBlobPathFromHex = function (id) {
return '/blob/' + id.slice(0,2) + '/' + id;
};
Hash.serializeHash = function (hash) {
if (hash && hash.slice(-1) !== "/") { hash += "/"; }
return hash;
};
Hash.createInviteUrl = function (curvePublic, channel) {
channel = channel || Hash.createChannelId();
return window.location.origin + '/invite/#/1/' + channel +
'/' + curvePublic.replace(/\//g, '-') + '/';
};
Hash.isValidChannel = function (channelId) {
return /^[a-zA-Z0-9]{32,48}$/.test(channelId);
};
Hash.isValidHref = function (href) {
// Non-empty href?
if (!href) { return; }
var parsed = Hash.parsePadUrl(href);
// Can be parsed?
if (!parsed) { return; }
// Link to a CryptPad app?
if (!parsed.type) { return; }
// Valid hash?
if (parsed.hash) {
if (!parsed.hashData) { return; }
// Version should be a number
if (typeof(parsed.hashData.version) === "undefined") { return; }
// pads and files should have a base64 (or hex) key
if (parsed.hashData.type === 'pad' || parsed.hashData.type === 'file') {
if (!parsed.hashData.key && !parsed.hashData.channel) { return; }
if (parsed.hashData.key && !/^[a-zA-Z0-9+-/=]+$/.test(parsed.hashData.key)) { return; }
}
}
return parsed;
};
Hash.decodeDataOptions = function (opts) {
var b64 = decodeURIComponent(opts);
var str = Nacl.util.encodeUTF8(Nacl.util.decodeBase64(b64));
return Util.tryParse(str) || {};
};
Hash.encodeDataOptions = function (opts) {
var str = JSON.stringify(opts);
var b64 = Nacl.util.encodeBase64(Nacl.util.decodeUTF8(str));
return encodeURIComponent(b64);
};
Hash.getNewPadURL = function (href, opts) {
var parsed = Hash.parsePadUrl(href);
var options = parsed.getOptions();
options.newPadOpts = Hash.encodeDataOptions(opts);
return parsed.getUrl(options);
};
Hash.getLoginURL = function (href, opts) {
var parsed = Hash.parsePadUrl(href);
var options = parsed.getOptions();
options.loginOpts = Hash.encodeDataOptions(opts);
return parsed.getUrl(options);
};
return Hash;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require("./common-util"),
require("chainpad-crypto"),
require("./common-signing-keys"),
require("tweetnacl/nacl-fast")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/components/chainpad-crypto/crypto.js',
'/common/common-signing-keys.js',
'/components/tweetnacl/nacl-fast.min.js'
], function (Util, Crypto, Keys) {
return factory(Util, Crypto, Keys, window.nacl);
});
} else {
// unsupported initialization
}
}(typeof(window) !== 'undefined'? window : {}));

View File

@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Crypto, Hash, Util, Constants, Realtime) => {
var Msg = {};
var createData = Msg.createData = function (proxy, hash) {
var data = {
channel: hash || Hash.createChannelId(),
displayName: proxy['cryptpad.username'],
profile: proxy.profile && proxy.profile.view,
edPublic: proxy.edPublic,
curvePublic: proxy.curvePublic,
notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']),
avatar: proxy.profile && proxy.profile.avatar,
uid: proxy.uid,
};
if (hash === false) { delete data.channel; }
return data;
};
var getFriend = Msg.getFriend = function (proxy, pubkey) {
if (!pubkey) { return; }
if (pubkey === proxy.curvePublic) {
var data = createData(proxy);
delete data.channel;
return data;
}
return proxy.friends ? proxy.friends[pubkey] : undefined;
};
var getFriendList = Msg.getFriendList = function (proxy) {
if (!proxy.friends) { proxy.friends = {}; }
return proxy.friends;
};
var eachFriend = function (friends, cb) {
Object.keys(friends).forEach(function (id) {
if (id === 'me') { return; }
cb(friends[id], id, friends);
});
};
Msg.getFriendChannelsList = function (proxy) {
var list = [];
eachFriend(proxy.friends, function (friend) {
list.push(friend.channel);
});
return list;
};
Msg.declineFriendRequest = function (store, data, cb) {
store.mailbox.sendTo('DECLINE_FRIEND_REQUEST', {}, {
channel: data.notifications,
curvePublic: data.curvePublic
}, function (obj) {
cb(obj);
});
};
Msg.acceptFriendRequest = function (store, data, cb) {
var friend = getFriend(store.proxy, data.curvePublic) || {};
var myData = createData(store.proxy, friend.channel || data.channel);
store.mailbox.sendTo('ACCEPT_FRIEND_REQUEST', { user: myData }, {
channel: data.notifications,
curvePublic: data.curvePublic
}, function (obj) {
cb(obj);
});
};
Msg.addToFriendList = function (cfg, data, cb) {
var proxy = cfg.proxy;
var friends = getFriendList(proxy);
var pubKey = data.curvePublic; // todo validata data
if (pubKey === proxy.curvePublic) { return void cb("E_MYKEY"); }
friends[pubKey] = data;
Realtime.whenRealtimeSyncs(cfg.realtime, function () {
cb();
cfg.pinPads([data.channel], function (res) {
if (res.error) { console.error(res.error); }
});
});
};
Msg.updateMyData = function (store, curve) {
var myData = createData(store.proxy, false);
if (store.proxy.friends) {
store.proxy.friends.me = Util.clone(myData);
delete store.proxy.friends.me.channel;
}
if (store.modules['team']) {
store.modules['team'].updateMyData(myData);
}
var todo = function (friend) {
if (!friend || !friend.notifications) { return; }
delete friend.user;
myData.channel = friend.channel;
store.mailbox.sendTo('UPDATE_DATA', myData, {
channel: friend.notifications,
curvePublic: friend.curvePublic
}, function (obj) {
if (obj && obj.error) { console.error(obj); }
});
};
if (curve) {
var friend = getFriend(store.proxy, curve);
return void todo(friend);
}
eachFriend(store.proxy.friends || {}, todo);
};
Msg.removeFriend = function (store, curvePublic, cb) {
var proxy = store.proxy;
var friend = proxy.friends[curvePublic];
if (!friend) { return void cb({error: 'ENOENT'}); }
if (!friend.notifications) { return void cb({error: 'EINVAL'}); }
store.mailbox.sendTo('UNFRIEND', {
curvePublic: proxy.curvePublic
}, {
channel: friend.notifications,
curvePublic: friend.curvePublic
}, function (obj) {
if (obj && obj.error) {
return void cb(obj);
}
store.messenger.onFriendRemoved(curvePublic, friend.channel);
delete proxy.friends[curvePublic];
Realtime.whenRealtimeSyncs(store.realtime, function () {
cb(obj);
});
});
};
return Msg;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require('chainpad-crypto'),
require('./common-hash'),
require('./common-util'),
require('./common-constants'),
require('./common-realtime')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/components/chainpad-crypto/crypto.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-constants.js',
'/common/common-realtime.js',
], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = () => {
var common = {};
/*
TODO make this not blow up when disconnected or lagging...
*/
common.whenRealtimeSyncs = function (realtime, cb) {
if (typeof(realtime.getAuthDoc) !== 'function') {
return void console.error('improper use of this function');
}
setTimeout(function () {
if (realtime.getAuthDoc() === realtime.getUserDoc()) {
return void cb();
} else {
realtime.onSettle(cb);
}
}, 0);
};
return common;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(function () {
var factory = function () {
var Keys = {};
var unescape = function (s) {
return s.replace(/-/g, '/');
};
/* Parse the new format of "Signing Public Keys".
If anything about the input is found to be invalid, return;
this will fall back to the old parsing method
*/
var parseNewUser = function (userString) {
if (!/^\[.*?@.*\]$/.test(userString)) { return; }
var temp = userString.slice(1, -1);
var domain, username, pubkey;
temp = temp
.replace(/\/([a-zA-Z0-9+-]{43}=)$/, function (all, k) {
pubkey = unescape(k);
return '';
});
if (!pubkey) { return; }
var index = temp.lastIndexOf('@');
if (index < 1) { return; }
domain = temp.slice(index + 1);
username = temp.slice(0, index);
return {
domain: domain,
user: username,
pubkey: pubkey
};
};
var isValidUser = function (parsed) {
if (!parsed) { return; }
if (!(parsed.domain && parsed.user && parsed.pubkey)) { return; }
return true;
};
Keys.parseUser = function (user) {
var parsed = parseNewUser(user);
if (isValidUser(parsed)) { return parsed; }
var domain, username, pubkey;
user.replace(/^https*:\/\/([^\/]+)\/user\/#\/1\/([^\/]+)\/([a-zA-Z0-9+-]{43}=)$/,
function (a, d, u, k) {
domain = d;
username = u;
pubkey = unescape(k);
return '';
});
if (!domain) { throw new Error("Could not parse user id [" + user + "]"); }
return {
domain: domain,
user: username,
pubkey: pubkey
};
};
/*
0. usernames may contain spaces or many other wacky characters, so enclose the whole thing in square braces so we know its boundaries. If the formatted string does not include these we know it is either a _v1 public key string_ or _an incomplete string_. Start parsing by removing them.
1. public keys should have a fixed length, so slice them off of the end of the string.
2. domains cannot include `@`, so find the last occurence of it in the signing key and slice everything thereafter.
3. the username is everything before the `@`.
*/
Keys.serialize = function (origin, username, pubkey) {
return '[' +
username +
'@' +
origin.replace(/https*:\/\//, '') +
'/' +
pubkey.replace(/\//g, '-') +
']';
// return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-');
};
Keys.canonicalize = function (input) {
if (typeof(input) !== 'string') { return; }
// key is already in simple form. ensure that it is an 'unsafeKey'
if (input.length === 44) {
return unescape(input);
}
try {
return Keys.parseUser(input).pubkey;
} catch (err) {
return;
}
};
return Keys;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([], factory);
}
}());

775
src/common/common-util.js Normal file
View File

@ -0,0 +1,775 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(function (window) {
var Util = {};
// polyfill for atob in case you're using this from node...
window.atob = window.atob || function (str) { return Buffer.from(str, 'base64').toString('binary'); };
window.btoa = window.btoa || function (str) { return Buffer.from(str, 'binary').toString('base64'); };
Util.slice = function (A, start, end) {
return Array.prototype.slice.call(A, start, end);
};
Util.shuffleArray = function (a) {
for (var i = a.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
};
Util.bake = function (f, args) {
if (typeof(args) === 'undefined') { args = []; }
if (!Array.isArray(args)) { args = [args]; }
return function () {
return f.apply(null, args);
};
};
Util.both = function (pre, post) {
if (typeof(pre) !== 'function') { throw new Error('INVALID_USAGE'); }
if (typeof(post) !== 'function') { post = function (x) { return x; }; }
return function () {
pre.apply(null, arguments);
return post.apply(null, arguments);
};
};
Util.clone = function (o) {
if (o === undefined || o === null) { return o; }
return JSON.parse(JSON.stringify(o));
};
Util.serializeError = function (err) {
if (!(err instanceof Error)) { return err; }
var ser = {};
Object.getOwnPropertyNames(err).forEach(function (key) {
ser[key] = err[key];
});
return ser;
};
Util.tryParse = function (s) {
try { return JSON.parse(s); } catch (e) { return;}
};
Util.mkAsync = function (f, ms) {
if (typeof(f) !== 'function') {
throw new Error('EXPECTED_FUNCTION');
}
return function () {
var args = Array.prototype.slice.call(arguments);
setTimeout(function () {
f.apply(null, args);
}, ms);
};
};
// If once is true, after the event has been fired, any further handlers which are
// registered will fire immediately, and this type of event cannot be fired twice.
Util.mkEvent = function (once) {
var handlers = [];
var fired = false;
return {
reg: function (cb) {
if (once && fired) { return void setTimeout(cb); }
handlers.push(cb);
},
unreg: function (cb) {
if (handlers.indexOf(cb) === -1) {
return void console.log("event handler was already unregistered");
}
handlers.splice(handlers.indexOf(cb), 1);
},
fire: function () {
if (once && fired) { return; }
fired = true;
var args = Array.prototype.slice.call(arguments);
handlers.forEach(function (h) { h.apply(null, args); });
}
};
};
Util.mkTimeout = function (_f, ms) {
ms = ms || 0;
var f = Util.once(_f);
var timeout = setTimeout(function () {
f('TIMEOUT');
}, ms);
return Util.both(f, function () {
clearTimeout(timeout);
});
};
Util.onClickEnter = function ($element, handler, cfg) {
$element.on('click keydown', function (e) {
var isClick = e.type === 'click';
var isEnter = e.type === 'keydown' && e.which === 13;
var isSpace = e.type === 'keydown' && e.which === 32 && cfg && cfg.space;
if (!isClick && !isEnter && !isSpace) { return; }
// "enter" on a button triggers a click, disable it
if (e.type === 'keydown') { e.preventDefault(); }
handler(e);
});
};
Util.response = function (errorHandler) {
var pending = {};
var timeouts = {};
if (typeof(errorHandler) !== 'function') {
errorHandler = function (label) {
throw new Error(label);
};
}
var clear = function (id) {
clearTimeout(timeouts[id]);
delete timeouts[id];
delete pending[id];
};
var expect = function (id, fn, ms) {
if (typeof(id) !== 'string') { errorHandler('EXPECTED_STRING'); }
if (typeof(fn) !== 'function') { errorHandler('EXPECTED_CALLBACK'); }
pending[id] = fn;
if (typeof(ms) === 'number' && ms) {
timeouts[id] = setTimeout(function () {
if (typeof(pending[id]) === 'function') { pending[id]('TIMEOUT'); }
clear(id);
}, ms);
}
};
var handle = function (id, args) {
var fn = pending[id];
if (typeof(fn) !== 'function') {
return void errorHandler("MISSING_CALLBACK", {
id: id,
args: args,
});
}
try {
fn.apply(null, Array.isArray(args)? args : [args]);
} catch (err) {
errorHandler('HANDLER_ERROR', {
error: err,
id: id,
args: args,
});
}
clear(id);
};
return {
clear: clear,
expected: function (id) {
return Boolean(pending[id]);
},
expectation: function (id) {
return pending[id];
},
expect: expect,
handle: handle,
_pending: pending,
};
};
Util.inc = function (map, key, val) {
map[key] = (map[key] || 0) + (typeof(val) === 'number'? val: 1);
};
Util.values = function (obj) {
return Object.keys(obj).map(function (k) {
return obj[k];
});
};
Util.find = function (map, path) {
var l = path.length;
for (var i = 0; i < l; i++) {
if (typeof(map[path[i]]) === 'undefined') { return; }
map = map[path[i]];
}
return map;
};
Util.uid = function () {
return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
.toString(32).replace(/\./g, '');
};
Util.guid = function (map) {
var id = Util.uid();
// the guid (globally unique id) is valid if it does already exist in the map
if (typeof(map[id]) === 'undefined') { return id; }
// otherwise try again
return Util.guid(map);
};
Util.fixHTML = function (str) {
if (!str) { return ''; }
return str.replace(/[<>&"']/g, function (x) {
return ({ "<": "&lt;", ">": "&gt", "&": "&amp;", '"': "&#34;", "'": "&#39;" })[x];
});
};
Util.hexToBase64 = function (hex) {
var hexArray = hex
.replace(/\r|\n/g, "")
.replace(/([\da-fA-F]{2}) ?/g, "0x$1 ")
.replace(/ +$/, "")
.split(" ");
var byteString = String.fromCharCode.apply(null, hexArray);
return window.btoa(byteString).replace(/\//g, '-').replace(/=+$/, '');
};
Util.base64ToHex = function (b64String) {
var hexArray = [];
window.atob(b64String.replace(/-/g, '/')).split("").forEach(function(e){
var h = e.charCodeAt(0).toString(16);
if (h.length === 1) { h = "0"+h; }
hexArray.push(h);
});
return hexArray.join("");
};
Util.uint8ArrayToHex = function (bytes) {
var hexString = '';
for (var i = 0; i < bytes.length; i++) {
if (bytes[i] < 16) { hexString += '0'; }
hexString += bytes[i].toString(16);
}
return hexString;
};
Util.hexToUint8Array = function (hexString) {
var bytes = new Uint8Array(Math.ceil(hexString.length / 2));
for (var i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
}
return bytes;
};
// given an array of Uint8Arrays, return a new Array with all their values
Util.uint8ArrayJoin = function (AA) {
var l = 0;
var i = 0;
for (; i < AA.length; i++) { l += AA[i].length; }
var C = new Uint8Array(l);
i = 0;
for (var offset = 0; i < AA.length; i++) {
C.set(AA[i], offset);
offset += AA[i].length;
}
return C;
};
Util.escapeKeyCharacters = function (key) {
return key && key.replace && key.replace(/\//g, '-');
};
Util.unescapeKeyCharacters = function (key) {
return key.replace(/\-/g, '/');
};
Util.deduplicateString = function (array) {
var a = array.slice();
for(var i=0; i<a.length; i++) {
for(var j=i+1; j<a.length; j++) {
if(a[i] === a[j]) { a.splice(j--, 1); }
}
}
return a;
};
/*
* Saving files
*/
Util.fixFileName = function (filename) {
return filename.replace(/ /g, '-').replace(/[\/\?]/g, '_')
.replace(/_+/g, '_');
};
var oneKilobyte = 1024;
var oneMegabyte = 1024 * oneKilobyte;
var oneGigabyte = 1024 * oneMegabyte;
Util.bytesToGigabytes = function (bytes) {
return Math.ceil(bytes / oneGigabyte * 100) / 100;
};
Util.bytesToMegabytes = function (bytes) {
return Math.ceil(bytes / oneMegabyte * 100) / 100;
};
Util.bytesToKilobytes = function (bytes) {
return Math.ceil(bytes / oneKilobyte * 100) / 100;
};
Util.magnitudeOfBytes = function (bytes) {
if (bytes >= oneGigabyte) { return 'GB'; }
else if (bytes >= oneMegabyte) { return 'MB'; }
else { return 'KB'; }
};
// given a path, asynchronously return an arraybuffer
var getCacheKey = function (src) {
var _src = src.replace(/(\/)*$/, ''); // Remove trailing slashes
var idx = _src.lastIndexOf('/');
var cacheKey = _src.slice(idx+1);
if (!/^[a-f0-9]{48}$/.test(cacheKey)) { cacheKey = undefined; }
return cacheKey;
};
Util.getBlock = function (src, opt, cb) {
var CB = Util.once(Util.mkAsync(cb));
var headers = {};
if (typeof(opt.bearer) === 'string' && opt.bearer) {
headers.authorization = `Bearer ${opt.bearer}`;
}
fetch(src, {
method: 'GET',
credentials: 'include',
headers: headers,
}).then(response => {
if (response.ok) {
// TODO this should probably be returned as an arraybuffer or something rather than a promise
// this is resulting in some code duplication
return void CB(void 0, response);
}
if (response.status === 401 || response.status === 404) {
response.json().then((data) => {
CB(response.status, data);
}).catch(() => {
CB(response.status);
});
return;
}
CB(response.status, response);
}).catch(error => {
CB(error);
});
};
Util.fetch = function (src, cb, progress, cache) {
var CB = Util.once(Util.mkAsync(cb));
var cacheKey = getCacheKey(src);
var getBlobCache = function (id, cb) {
if (!cache || typeof(cache.getBlobCache) !== "function") { return void cb('EINVAL'); }
cache.getBlobCache(id, cb);
};
var setBlobCache = function (id, u8, cb) {
if (!cache || typeof(cache.setBlobCache) !== "function") { return void cb('EINVAL'); }
cache.setBlobCache(id, u8, cb);
};
var xhr;
var fetch = function () {
xhr = new XMLHttpRequest();
xhr.open("GET", src, true);
if (progress) {
xhr.addEventListener("progress", function (evt) {
if (evt.lengthComputable) {
var percentComplete = evt.loaded / evt.total;
progress(percentComplete);
}
}, false);
}
xhr.responseType = "arraybuffer";
xhr.onerror = function (err) { CB(err); };
xhr.onload = function () {
if (/^4/.test(''+this.status)) {
return CB('XHR_ERROR');
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
if (cacheKey) {
return void setBlobCache(cacheKey, u8, function () {
CB(null, u8);
});
}
return void CB(void 0, u8);
}
CB('ENOENT');
};
xhr.send(null);
};
if (!cacheKey) { return void fetch(); }
getBlobCache(cacheKey, function (err, u8) {
if (err || !u8) { return void fetch(); }
CB(void 0, u8);
});
return {
cancel: function () {
if (xhr && xhr.abort) { xhr.abort(); }
}
};
};
Util.dataURIToBlob = function (dataURI) {
var byteString = atob(dataURI.split(',')[1]);
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
// write the bytes of the string to an ArrayBuffer
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
// write the ArrayBuffer to a blob, and you're done
var bb = new Blob([ab], {type: mimeString});
return bb;
};
Util.throttle = function (f, ms) {
var last = 0;
var to;
var args;
var defer = function (delay) {
// no timeout: run function `f` in `ms` milliseconds
// unless `g` is called again in the meantime
to = setTimeout(function () {
// wipe the current timeout handler
to = undefined;
// take the current time
var now = +new Date();
// compute time passed since `last`
var diff = now - last;
if (diff < ms) {
// don't run `f` if `g` was called since this timeout was set
// instead calculate how much further in the future your next
// timeout should be scheduled
return void defer(ms - diff);
}
// else run `f` with the most recently supplied arguments
f.apply(null, args);
}, delay);
};
var g = function () {
// every time you call this function store the time
last = +new Date();
// remember what arguments were passed
args = Util.slice(arguments);
// if there is a pending timeout then do nothing
if (to) { return; }
defer(ms);
};
g.clear = function () {
clearTimeout(to);
to = undefined;
};
return g;
};
/* takes a function (f) and a time (t) in ms. returns a function wrapper
which prevents the internal function from being called more than once
every t ms. if the function is prevented, returns time til next valid
execution, else null.
*/
Util.notAgainForAnother = function (f, t) {
if (typeof(f) !== 'function' || typeof(t) !== 'number') {
throw new Error("invalid inputs");
}
var last = null;
return function () {
var now = +new Date();
if (last && now <= last + t) { return t - (now - last); }
last = now;
f.apply(null, Util.slice(arguments));
return null;
};
};
Util.createRandomInteger = function () {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
};
Util.noop = function () {};
/* for wrapping async functions such that they can only be called once */
Util.once = function (f, g) {
return function () {
if (!f) { return; }
f.apply(this, Array.prototype.slice.call(arguments));
f = g;
};
};
Util.blobToImage = function (blob, cb) {
var reader = new FileReader();
reader.onloadend = function() {
cb(reader.result);
};
reader.readAsDataURL(blob);
};
Util.blobURLToImage = function (url, cb) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var reader = new FileReader();
reader.onloadend = function() {
cb(reader.result);
};
reader.readAsDataURL(xhr.response);
};
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.send();
};
// Check if an element is a plain object
Util.isObject = function (o) {
return typeof (o) === "object" &&
Object.prototype.toString.call(o) === '[object Object]';
};
Util.isCircular = function (o) {
try {
JSON.stringify(o);
return false;
} catch (e) { return true; }
};
/* recursively adds the properties of an object 'b' to 'a'
arrays are only shallow copies, so references to the original
might still be present. Be mindful if you will modify 'a' in the future */
Util.extend = function (a, b) {
if (!Util.isObject(a) || !Util.isObject(b)) {
return void console.log("Extend only works with 2 objects");
}
if (Util.isCircular(b)) {
return void console.log("Extend doesn't accept circular objects");
}
for (var k in b) {
if (Util.isObject(b[k])) {
a[k] = Util.isObject(a[k]) ? a[k] : {};
Util.extend(a[k], b[k]);
continue;
}
if (Array.isArray(b[k])) {
a[k] = b[k].slice();
continue;
}
a[k] = b[k];
}
};
Util.isChecked = function (el) {
// could be nothing...
if (!el) { return false; }
// check if it's a dom element
if (typeof(el.tagName) !== 'undefined') {
return Boolean(el.checked);
}
// sketchy test to see if it's jquery
if (typeof(el.prop) === 'function') {
return Boolean(el.prop('checked'));
}
// else just say it's not checked
return false;
};
Util.hexToRGB = function (hex) {
var h = hex.replace(/^#/, '');
return [
parseInt(h.slice(0,2), 16),
parseInt(h.slice(2,4), 16),
parseInt(h.slice(4,6), 16),
];
};
Util.rgbToHex = function (rgb) {
return `#${rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/).slice(1).map(n => parseInt(n, 10).toString(16).padStart(2, '0')).join('')}`;
};
Util.isSmallScreen = function () {
return window.innerHeight < 800 || window.innerWidth < 800;
};
Util.stripTags = function (text) {
var div = document.createElement("div");
div.innerHTML = text;
return div.innerText;
};
// return an object containing {name, ext}
// or {} if the name could not be parsed
Util.parseFilename = function (filename) {
if (!filename || !filename.trim()) { return {}; }
var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(filename) || [];
return {
name: parsedName[1],
ext: parsedName[2],
};
};
// Tell if a file is plain text from its metadata={title, fileType}
Util.isPlainTextFile = function (type, name) {
// does its type begins with "text/"
if (type && type.indexOf("text/") === 0) { return true; }
// no type and no file extension -> let's guess it's plain text
var parsedName = Util.parseFilename(name);
if (!type && name && !parsedName.ext) { return true; }
// other exceptions
if (type === 'application/x-javascript') { return true; }
if (type === 'application/xml') { return true; }
return false;
};
// Tell if a file is spreadsheet from its metadata={title, fileType}
Util.isSpreadsheet = function (type, name) {
return (type &&
(type === 'application/vnd.oasis.opendocument.spreadsheet' ||
type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'))
|| (name && (name.endsWith('.xlsx') || name.endsWith('.ods')));
};
Util.isOfficeDoc = function (type, name) {
return (type &&
(type === 'application/vnd.oasis.opendocument.text' ||
type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'))
|| (name && (name.endsWith('.docx') || name.endsWith('.odt')));
};
Util.isPresentation = function (type, name) {
return (type &&
(type === 'application/vnd.oasis.opendocument.presentation' ||
type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation'))
|| (name && (name.endsWith('.pptx') || name.endsWith('.odp')));
};
Util.isValidURL = function (str) {
var pattern = new RegExp('^(https?:\\/\\/)'+ // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?'); // query string
//'(\\#[-a-z\\d_]*)?$','i'); // fragment locator
return !!pattern.test(str);
};
var emoji_patt = /([\uD800-\uDBFF][\uDC00-\uDFFF])/;
var isEmoji = function (str) {
return emoji_patt.test(str);
};
var emojiStringToArray = function (str) {
var split = str.split(emoji_patt);
var arr = [];
for (var i=0; i<split.length; i++) {
var char = split[i];
if (char !== "") {
arr.push(char);
}
}
return arr;
};
Util.getFirstCharacter = function (str) {
if (!str || !str.trim()) { return '?'; }
var emojis = emojiStringToArray(str);
return isEmoji(emojis[0])? emojis[0]: str[0];
};
Util.getRandomColor = function (light) {
var getColor = function () {
if (light) {
return Math.floor(Math.random() * 156) + 70;
}
return Math.floor(Math.random() * 200) + 25;
};
return '#' + getColor().toString(16) +
getColor().toString(16) +
getColor().toString(16);
};
Util.checkRestrictedApp = function (app, AppConfig, earlyTypes, plan, loggedIn) {
// If this is an early access app, make sure this instance allows them
if (Array.isArray(earlyTypes) && earlyTypes.includes(app) && !AppConfig.enableEarlyAccess) {
return -2;
}
var premiumTypes = AppConfig.premiumTypes;
// If this is not a premium app, don't disable it
if (!Array.isArray(premiumTypes) || !premiumTypes.includes(app)) { return 2; }
// This is a premium app
// if you're not logged in, disable it
if (!loggedIn) { return -1; }
// if you're logged in, enable it only if you're a premium user
return plan ? 1 : 0;
};
/* Chrome 92 dropped support for SharedArrayBuffer in cross-origin contexts
where window.crossOriginIsolated is false.
Their blog (https://blog.chromium.org/2021/02/restriction-on-sharedarraybuffers.html)
isn't clear about why they're doing this, but since it's related to site-isolation
it seems they're trying to do vague security things.
In any case, there seems to be a workaround where you can still create them
by using `new WebAssembly.Memory({shared: true, ...})` instead of `new SharedArrayBuffer`.
This seems unreliable, but it's better than not being able to export, since
we actively rely on postMessage between iframes and therefore can't afford
to opt for full isolation.
*/
var supportsSharedArrayBuffers = function () {
try {
return Object.prototype.toString.call(new window.WebAssembly.Memory({shared: true, initial: 0, maximum: 0}).buffer) === '[object SharedArrayBuffer]';
} catch (err) {
console.error(err);
}
return false;
};
Util.supportsWasm = function () {
return !(typeof(Atomics) === "undefined" || !supportsSharedArrayBuffers() || typeof(WebAssembly) === 'undefined');
};
//Returns an array of integers in range 0 to (length-1)
Util.getKeysArray = function (length) {
return [...Array(length).keys()];
};
Util.getVersionFromUrlArgs = urlArgs => {
let arr = /ver=([0-9.]+)(-[0-9]*)?/.exec(urlArgs);
let ver = Array.isArray(arr) && arr[1];
return ver || undefined;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = Util;
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([], function () {
window.CryptPad_Util = Util;
return Util;
});
} else {
window.CryptPad_Util = Util;
}
}(typeof(self) !== 'undefined'? self: this));

238
src/common/cryptget.js Normal file
View File

@ -0,0 +1,238 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Crypto, CPNetflux, Netflux, Util,
Hash, Realtime, NetConfig, Cache, Pinpad, nThen) => {
var finish = function (S, err, doc) {
if (S.done) { return; }
S.cb((err && err.error), doc, err);
S.done = true;
if (!S.hasNetwork) {
var disconnect = Util.find(S, ['network', 'disconnect']);
if (typeof(disconnect) === 'function') { disconnect(); }
}
if (S.realtime && S.realtime.stop) {
try {
S.realtime.stop();
} catch (e) { console.error(e); }
}
var abort = Util.find(S, ['session', 'realtime', 'abort']);
if (typeof(abort) === 'function') {
S.session.realtime.sync();
abort();
}
};
var makeNetwork = function (cb) {
var wsUrl = NetConfig.getWebsocketURL();
Netflux.connect(wsUrl).then(function (network) {
cb(null, network);
}, function (err) {
cb(err);
});
};
var start = function (Session, config) {
// Create a network and authenticate with all our keys if necessary,
// then start chainpad-netflux
nThen(function (waitFor) {
if (Session.hasNetwork) { return; }
makeNetwork(waitFor(function (err, network) {
if (err) { return; }
config.network = network;
}));
}).nThen(function () {
Session.realtime = CPNetflux.start(config);
});
};
var onRejected = function (config, Session, data, cb) {
// Check if we can authenticate
if (!Array.isArray(data) || !data.length || data[0].length !== 16) {
return void cb(true);
}
if (!Array.isArray(Session.accessKeys)) { return void cb(true); }
// Authenticate
config.network.historyKeeper = data[0];
nThen(function (waitFor) {
Session.accessKeys.forEach(function (obj) {
Pinpad.create(config.network, obj, waitFor(function (e) {
console.log('done', obj);
if (e) { console.error(e); }
}));
});
}).nThen(function () {
cb();
});
};
var makeConfig = function (hash, opt) {
var secret;
if (typeof(hash) === 'string') {
// We can't use cryptget with a file or a user so we can use 'pad' as hash type
secret = Hash.getSecrets('pad', hash, opt.password);
} else if (typeof(hash) === 'object') {
// we may want to just supply options directly
// and this is the easiest place to do it
secret = hash;
}
if (!secret.keys) { secret.keys = secret.key; } // support old hashses
var config = {
websocketURL: NetConfig.getWebsocketURL(opt.origin),
channel: secret.channel,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
logLevel: 0,
initialState: opt.initialState,
Cache: Cache
};
return config;
};
var isObject = function (o) {
return typeof(o) === 'object';
};
var overwrite = function (a, b) {
if (!(isObject(a) && isObject(b))) { return; }
Object.keys(b).forEach(function (k) { a[k] = b[k]; });
};
var get = function (hash, cb, opt, progress) {
if (typeof(cb) !== 'function') {
throw new Error('Cryptget expects a callback');
}
opt = opt || {};
progress = progress || function () {};
var config = makeConfig(hash, opt);
var Session = {
cb: cb,
accessKeys: opt.accessKeys,
hasNetwork: Boolean(opt.network)
};
config.onRejected = function (data, cb) {
onRejected(config, Session, data, cb);
};
config.onReady = function (info) {
var rt = Session.session = info.realtime;
Session.network = info.network;
progress(1);
finish(Session, void 0, rt.getUserDoc());
};
config.onError = function (info) {
console.warn(info);
finish(Session, info);
};
config.onChannelError = function (info) {
console.error(info);
finish(Session, info);
};
config.onCacheReady = opt.onCacheReady;
// We use the new onMessage handler to compute the progress:
// we should receive 2 checkpoints max, so 100 messages max
// We're going to consider that 1 message = 1%, and we'll send 100%
// at the end
var i = 0;
config.onMessage = function () {
i++;
progress(Math.min(0.99, i/100));
};
overwrite(config, opt);
start(Session, config);
};
var put = function (hash, doc, cb, opt) {
if (typeof(cb) !== 'function') {
throw new Error('Cryptput expects a callback');
}
opt = opt || {};
var config = makeConfig(hash, opt);
var Session = {
cb: cb,
accessKeys: opt.accessKeys,
hasNetwork: Boolean(opt.network)
};
config.onRejected = function (data, cb) {
onRejected(config, Session, data, cb);
};
config.onReady = function (info) {
var realtime = Session.session = info.realtime;
Session.network = info.network;
realtime.contentUpdate(doc);
var to = setTimeout(function () {
cb(new Error("Timeout"));
}, 15000);
Realtime.whenRealtimeSyncs(realtime, function () {
clearTimeout(to);
var doc = realtime.getAuthDoc();
realtime.abort();
finish(Session, void 0, doc);
});
};
config.onChannelError = function (info) {
finish(Session, info);
};
overwrite(config, opt);
start(Session, config);
};
return {
get: get,
put: put,
};
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require('chainpad-crypto'),
require('chainpad-netflux'),
require('netflux-websocket'),
require('./common-util'),
require('./common-hash'),
require('./common-realtime'),
require('./network-config'),
require('./cache-store'),
require('./pinpad'),
require('nthen'),
require('chainpad')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/components/chainpad-crypto/crypto.js',
'chainpad-netflux',
'netflux-client',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-realtime.js',
'/common/outer/network-config.js',
'/common/outer/cache-store.js',
'/common/pinpad.js',
'/components/nthen/index.js',
'/components/chainpad/chainpad.dist.js',
], factory);
} else {
// unsupported initialization
}
})();

130
src/common/http-command.js Normal file
View File

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (nThen, Util, ApiConfig = {}, Nacl) => {
const getApiOrigin = function () {
if (!Object.keys(ApiConfig).length) { return; }
var url;
var unsafeOriginURL = new URL(ApiConfig.httpUnsafeOrigin);
try {
url = new URL(ApiConfig.websocketPath, ApiConfig.httpUnsafeOrigin);
url.protocol = unsafeOriginURL.protocol;
return url.origin;
} catch (err) {
console.error(err);
return ApiConfig.httpUnsafeOrigin;
}
};
var API_ORIGIN = getApiOrigin();
const setCustomize = data => {
ApiConfig = data.ApiConfig;
API_ORIGIN = getApiOrigin();
};
var clone = o => JSON.parse(JSON.stringify(o));
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24));
var postData = function (url, data, cb) {
var CB = Util.once(Util.mkAsync(cb));
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then(response => {
if (response.ok) {
return void response.text().then(result => { CB(void 0, Util.tryParse(result)); }); // checkup error when using .json()
//return void response.json().then(result => { CB(void 0, result); });
}
response.json().then().then(result => {
CB(response.status, result);
});
//CB(response.status, response);
}).catch(error => {
CB(error);
});
};
var serverCommand = function (keypair, my_data, cb) {
var obj = clone(my_data);
obj.publicKey = Nacl.util.encodeBase64(keypair.publicKey);
obj.nonce = randomToken();
var href = new URL('/api/auth/', API_ORIGIN);
var txid, date;
nThen(function (w) {
// Tell the server we want to do some action
postData(href, obj, w((err, data) => {
if (err) {
w.abort();
console.error(err);
// there might be more info here
if (data) { console.error(data); }
return void cb(err);
}
// if the requested action is valid, it responds with a txid and a nonce
// bundle all that up into an object, stringify it, and sign it.
// respond with an object: {sig, txid}
if (!data.date || !data.txid) {
w.abort();
return void cb('REQUEST_REJECTED');
}
txid = data.txid;
date = data.date;
}));
}).nThen(function (w) {
var copy = clone(obj);
copy.txid = txid;
copy.date = date;
var toSign = Nacl.util.decodeUTF8(JSON.stringify(copy));
var sig = Nacl.sign.detached(toSign, keypair.secretKey);
var encoded = Nacl.util.encodeBase64(sig);
var obj2 = {
sig: encoded,
txid: txid,
};
postData(href, obj2, w((err, data) => {
if (err) {
w.abort();
console.error(err);
// there might be more info here
if (data) { console.error(data); }
return void cb("RESPONSE_REJECTED", data);
}
cb(void 0, data);
}));
});
};
serverCommand.setCustomize = setCustomize;
return serverCommand;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require('nthen'),
require('./common-util'),
undefined,
require('tweetnacl/nacl-fast')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/components/nthen/index.js',
'/common/common-util.js',
'/api/config',
'/components/tweetnacl/nacl-fast.min.js',
], (nThen, Util, ApiConfig) => {
factory(nThen, Util, ApiConfig, window.nacl);
});
} else {
// unsupported initialization
}
})();

245
src/common/login-block.js Normal file
View File

@ -0,0 +1,245 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Util, ApiConfig = {}, ServerCommand, Nacl) => {
var Block = {};
Block.setCustomize = data => {
ApiConfig = data.ApiConfig;
ServerCommand.setCustomize(data);
};
Block.join = Util.uint8ArrayJoin;
// publickey <base64 string>
// signature <base64 string>
// block <base64 string>
// [b64_public, b64_sig, b64_block [version, nonce, content]]
Block.seed = function () {
return Nacl.hash(Nacl.util.decodeUTF8('pewpewpew'));
};
// should be deterministic from a seed...
Block.genkeys = function (seed) {
if (!(seed instanceof Uint8Array)) {
throw new Error('INVALID_SEED_FORMAT');
}
if (!seed || typeof(seed.length) !== 'number' || seed.length < 64) {
throw new Error('INVALID_SEED_LENGTH');
}
var signSeed = seed.subarray(0, Nacl.sign.seedLength);
var symmetric = seed.subarray(Nacl.sign.seedLength,
Nacl.sign.seedLength + Nacl.secretbox.keyLength);
return {
sign: Nacl.sign.keyPair.fromSeed(signSeed), // 32 bytes
symmetric: symmetric, // 32 bytes ...
};
};
Block.keysToRPCFormat = function (keys) {
try {
var sign = keys.sign;
return {
edPrivate: Nacl.util.encodeBase64(sign.secretKey),
edPublic: Nacl.util.encodeBase64(sign.publicKey),
};
} catch (err) {
console.error(err);
return;
}
};
// (UTF8 content, keys object) => Uint8Array block
Block.encrypt = function (version, content, keys) {
var u8 = Nacl.util.decodeUTF8(content);
var nonce = Nacl.randomBytes(Nacl.secretbox.nonceLength);
return Block.join([
[0],
nonce,
Nacl.secretbox(u8, nonce, keys.symmetric)
]);
};
// (uint8Array block) => payload object
Block.decrypt = function (u8_content, keys) {
// version is currently ignored since there is only one
var nonce = u8_content.subarray(1, 1 + Nacl.secretbox.nonceLength);
var box = u8_content.subarray(1 + Nacl.secretbox.nonceLength);
var plaintext = Nacl.secretbox.open(box, nonce, keys.symmetric);
try {
return JSON.parse(Nacl.util.encodeUTF8(plaintext));
} catch (e) {
console.error(e);
return;
}
};
// (Uint8Array block) => signature
Block.sign = function (ciphertext, keys) {
return Nacl.sign.detached(Nacl.hash(ciphertext), keys.sign.secretKey);
};
Block.serialize = function (content, keys) {
// encrypt the content
var ciphertext = Block.encrypt(0, content, keys);
// generate a detached signature
var sig = Block.sign(ciphertext, keys);
// serialize {publickey, sig, ciphertext}
return {
publicKey: Nacl.util.encodeBase64(keys.sign.publicKey),
signature: Nacl.util.encodeBase64(sig),
ciphertext: Nacl.util.encodeBase64(ciphertext),
};
};
Block.proveAncestor = function (O /* oldBlockKeys, N, newBlockKeys */) {
var u8_pub = Util.find(O, ['sign', 'publicKey']);
var u8_secret = Util.find(O, ['sign', 'secretKey']);
try {
// sign your old publicKey with your old privateKey
var u8_sig = Nacl.sign.detached(u8_pub, u8_secret);
// return an array with the sig and the pubkey
return JSON.stringify([u8_pub, u8_sig].map(Nacl.util.encodeBase64));
} catch (err) {
return void console.error(err);
}
};
var urlSafeB64 = function (u8) {
return Nacl.util.encodeBase64(u8).replace(/\//g, '-');
};
Block.getBlockUrl = function (keys) {
var publicKey = urlSafeB64(keys.sign.publicKey);
// 'block/' here is hardcoded because it's hardcoded on the server
// if we want to make CryptPad work in server subfolders, we'll need
// to update this path derivation
return (ApiConfig.fileHost || ApiConfig.httpUnsafeOrigin || window.location.origin)
+ '/block/' + publicKey.slice(0, 2) + '/' + publicKey;
};
Block.getBlockHash = function (keys) {
var absolute = Block.getBlockUrl(keys);
var symmetric = urlSafeB64(keys.symmetric);
return absolute + '#' + symmetric;
};
var decodeSafeB64 = function (b64) {
try {
return Nacl.util.decodeBase64(b64.replace(/\-/g, '/'));
} catch (e) {
console.error(e);
return;
}
};
Block.parseBlockHash = function (hash) {
if (typeof(hash) !== 'string') { return; }
var parts = hash.split('#');
if (parts.length !== 2) { return; }
try {
return {
href: parts[0],
keys: {
symmetric: decodeSafeB64(parts[1]),
}
};
} catch (e) {
console.error(e);
return;
}
};
Block.checkRights = function (data, _cb) {
const cb = Util.mkAsync(_cb);
const { blockKeys, auth } = data;
var command = 'MFA_CHECK';
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
ServerCommand(blockKeys.sign, {
command: command,
auth: auth && auth.data
}, cb);
};
Block.writeLoginBlock = function (data, cb) {
const { content, blockKeys, oldBlockKeys, auth, pw, session, token, userData } = data;
var command = 'WRITE_BLOCK';
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
var block = Block.serialize(JSON.stringify(content), blockKeys);
block.auth = auth && auth.data;
block.hasPassword = pw;
block.registrationProof = oldBlockKeys && Block.proveAncestor(oldBlockKeys);
if (token) { block.inviteToken = token; }
if (userData) { block.userData = userData; }
ServerCommand(blockKeys.sign, {
command: command,
content: block,
session: session // sso session
}, cb);
};
Block.removeLoginBlock = function (data, cb) {
const { reason, blockKeys, auth, edPublic } = data;
var command = 'REMOVE_BLOCK';
if (auth && auth.type) { command = `${auth.type.toUpperCase()}_` + command; }
ServerCommand(blockKeys.sign, {
command: command,
auth: auth && auth.data,
edPublic: edPublic,
reason: reason
}, cb);
};
Block.updateSSOBlock = function (data, cb) {
const { blockKeys, oldBlockKeys } = data;
var oldProof = oldBlockKeys && Block.proveAncestor(oldBlockKeys);
ServerCommand(blockKeys.sign, {
command: 'SSO_UPDATE_BLOCK',
ancestorProof: oldProof
}, cb);
};
return Block;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require('./common-util'),
undefined,
require('./http-command'),
require('tweetnacl/nacl-fast')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/api/config',
'/common/outer/http-command.js',
'/components/tweetnacl/nacl-fast.min.js',
], (Util, ApiConfig, ServerCommand) => {
factory(Util, ApiConfig, ServerCommand, window.nacl);
});
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (ApiConfig = {}) => {
var Config = {};
Config.setCustomize = data => {
ApiConfig = data.ApiConfig;
};
Config.getWebsocketURL = function (origin) {
var path = ApiConfig.websocketPath || '/cryptpad_websocket';
if (/^ws{1,2}:\/\//.test(path)) { return path; }
var l = window.location;
if (origin && window && window.document) {
l = document.createElement("a");
l.href = origin;
}
var protocol = l.protocol.replace(/http/, 'ws');
var host = l.host;
var url = protocol + '//' + host + path;
return url;
};
return Config;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/api/config'
], factory);
} else {
// unsupported initialization
}
})();

201
src/common/notify.js Normal file
View File

@ -0,0 +1,201 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* eslint compat/compat: "off" */
(() => {
const factory = (ApiConfig = {}) => {
let window = globalThis;
var Module = {};
ApiConfig.requireConf = ApiConfig.requireConf || {};
Module.setCustomize = data => {
ApiConfig = data.ApiConfig;
};
var apps = ['code', 'slide', 'pad', 'kanban', 'whiteboard', 'diagram', 'sheet', 'poll', 'teams', 'form', 'doc', 'presentation'];
var app = window.location && window.location.pathname.slice(1, -1); // remove "/" at the beginnin and the end
var suffix = apps.indexOf(app) !== -1 ? '-'+app : '';
var DEFAULT_MAIN = '/customize/favicon/main-favicon' + suffix + '.png?' + ApiConfig.requireConf.urlArgs;
var DEFAULT_ALT = '/customize/favicon/alt-favicon' + suffix + '.png?' + ApiConfig.requireConf.urlArgs;
var DEFAULT_MAIN_ICO = '/customize/favicon/main-favicon' + suffix + '.ico?' + ApiConfig.requireConf.urlArgs;
var DEFAULT_ALT_ICO = '/customize/favicon/alt-favicon' + suffix + '.ico?' + ApiConfig.requireConf.urlArgs;
var document = window.document;
var isSupported = Module.isSupported = function () {
return typeof(window.Notification) === 'function' && window.isSecureContext;
};
var hasPermission = Module.hasPermission = function () {
return Notification.permission === 'granted';
};
var getPermission = Module.getPermission = function (f) {
f = f || function () {};
// "Notification.requestPermission is not a function" on Firefox 68.11.0esr
if (!Notification || typeof(Notification.requestPermission) !== 'function') { return void f(false); }
Notification.requestPermission(function (permission) {
if (permission === "granted") { f(true); }
else { f(false); }
});
};
var create = Module.create = function (msg, title, icon) {
if (document && !icon) {
var favicon = document.getElementById('favicon');
icon = favicon.getAttribute('data-main-favicon') || DEFAULT_ALT;
} else if (!icon) {
icon = DEFAULT_ALT;
}
var n = new Notification(title,{
icon: icon,
body: msg,
});
n.onclick = function () {
if (!document) { return; }
try {
parent.focus();
window.focus(); //just in case, older browsers
this.close();
} catch (e) {}
};
return n;
};
Module.system = function (msg, title, icon) {
// Let's check if the browser supports notifications
if (!isSupported()) { return; /*console.log("Notifications are not supported");*/ }
// Let's check whether notification permissions have already been granted
else if (hasPermission()) {
// If it's okay let's create a notification
return create(msg, title, icon);
}
// Otherwise, we need to ask the user for permission
else if (Notification.permission !== 'denied') {
getPermission(function (state) {
if (state) { create(msg, title, icon); }
});
}
};
var createFavicon = function () {
if (!document) {
return void console.error('document is not available in this context');
}
console.debug("creating favicon");
var attrs = {
id: 'favicon',
type: 'image/png',
rel: 'icon',
'data-main-favicon': DEFAULT_MAIN,
'data-alt-favicon': DEFAULT_ALT,
href: DEFAULT_MAIN,
};
if(!document.getElementById("favicon")) {
var fav = document.createElement('link');
Object.keys(attrs).forEach(function (k) {
fav.setAttribute(k, attrs[k]);
});
document.head.appendChild(fav);
}
if(!document.getElementById("favicon-ico")) {
var faviconLink = document.createElement('link');
attrs.href = attrs.href.replace(/\.png/g, ".ico");
attrs.id = 'favicon-ico';
attrs.type = 'image/x-icon';
Object.keys(attrs).forEach(function (k) {
faviconLink.setAttribute(k, attrs[k]);
});
document.head.appendChild(faviconLink);
}
};
if (document && !document.getElementById('favicon')) { createFavicon(); }
Module.tab = function (frequency, count) {
if (!document) {
return void console.error('document is not available in this context');
}
var key = '_pendingTabNotification';
var favicon = document.getElementById('favicon');
var faviconIco = document.getElementById('favicon-ico');
var main = DEFAULT_MAIN;
var alt = DEFAULT_ALT;
var mainIco = DEFAULT_MAIN_ICO;
var altIco = DEFAULT_ALT_ICO;
if (favicon) {
main = favicon.getAttribute('data-main-favicon') || DEFAULT_MAIN;
alt = favicon.getAttribute('data-alt-favicon') || DEFAULT_ALT;
favicon.setAttribute('href', main);
}
if (faviconIco) {
mainIco = faviconIco.getAttribute('data-main-favicon') || DEFAULT_MAIN_ICO;
altIco = faviconIco.getAttribute('data-alt-favicon') || DEFAULT_ALT_ICO;
faviconIco.setAttribute('href', mainIco);
}
var cancel = function (pending) {
// only run one tab notification at a time
if (Module[key]) {
window.clearInterval(Module[key]);
if (favicon) {
favicon.setAttribute('href', pending? alt : main);
}
if (faviconIco) {
faviconIco.setAttribute('href', pending? altIco : mainIco);
}
return true;
}
return false;
};
cancel();
var step = function () {
if (favicon) {
favicon.setAttribute('href', favicon.getAttribute('href') === main? alt : main);
}
if (faviconIco) {
faviconIco.setAttribute('href', faviconIco.getAttribute('href') === mainIco? altIco : mainIco);
}
--count;
};
Module[key] = window.setInterval(function () {
if (count > 0) { return step(); }
cancel(true);
}, frequency);
step();
return {
cancel: cancel,
};
};
return Module;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(undefined);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define(['/api/config'], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = () => {
return {
currentVersion: 'v7'
};
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([], factory);
}
})();

64
src/common/pad-types.js Normal file
View File

@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (AppConfig = {}, ApiConfig = {},
OOCurrentVersion) => {
let availablePadTypes = [];
const OO_APPS = ["sheet", "doc", "presentation"];
const setCustomize = data => {
AppConfig = data.AppConfig;
ApiConfig = data.ApiConfig;
const ooEnabled = ApiConfig.onlyOffice &&
ApiConfig.onlyOffice.availableVersions.includes(
OOCurrentVersion.currentVersion
);
availablePadTypes = AppConfig.availablePadTypes.filter(
(t) => ooEnabled || !OO_APPS.includes(t)
);
};
const Types = { setCustomize };
Types.__defineGetter__("availableTypes", function () {
if (ApiConfig.appsToDisable) {
return availablePadTypes.filter(value => {
return !ApiConfig.appsToDisable.includes(value);
});
}
return availablePadTypes;
});
Types.__defineGetter__("appsToSelect", function () {
return availablePadTypes.filter(value => !['drive', 'teams', 'file', 'contacts', 'convert'].includes(value));
});
Types.isAvailable = type => {
return Array.isArray(Types.availableTypes) &&
Types.availableTypes.includes(type);
};
return Types;
};
if (typeof(module) !== 'undefined' && module.exports) {
// Code from customize can't be laoded directly in the build
module.exports = factory(
undefined,
undefined,
require('./onlyoffice/current-version')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/customize/application_config.js',
"/api/config",
"/common/onlyoffice/current-version.js",
], factory);
} else {
// unsupported initialization
}
})();

235
src/common/pinpad.js Normal file
View File

@ -0,0 +1,235 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(function () {
var factory = function (Util, Rpc) {
var create = function (network, proxy, _cb, Cache) {
if (typeof(_cb) !== 'function') { throw new Error("Expected callback"); }
var cb = Util.once(Util.mkAsync(_cb));
if (!network) { return void cb('INVALID_NETWORK'); }
if (!proxy) { return void cb('INVALID_PROXY'); }
var edPrivate = proxy.edPrivate;
var edPublic = proxy.edPublic;
if (!(edPrivate && edPublic)) { return void cb('INVALID_KEYS'); }
Rpc.create(network, edPrivate, edPublic, function (e, rpc) {
if (e) { return void cb(e); }
var exp = {};
exp.destroy = rpc.destroy;
// expose the supplied publicKey as an identifier
exp.publicKey = edPublic;
// expose the RPC module's raw 'send' command
exp.send = rpc.send;
// you can ask the server to pin a particular channel for you
exp.pin = function (channels, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Array.isArray(channels)) {
return void cb('[TypeError] pin expects an array');
}
rpc.send('PIN', channels, cb);
};
// you can also ask to unpin a particular channel
exp.unpin = function (channels, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Array.isArray(channels)) {
return void cb('[TypeError] pin expects an array');
}
rpc.send('UNPIN', channels, cb);
};
// Get data for the admin panel
exp.adminRpc = function (obj, cb) {
if (!obj.cmd) {
setTimeout(function () {
cb('[TypeError] admin rpc expects a command');
});
return;
}
var params = [obj.cmd, obj.data];
rpc.send('ADMIN', params, cb);
};
// ask the server what it thinks your hash is
exp.getServerHash = function (cb) {
rpc.send('GET_HASH', edPublic, function (e, hash) {
if (!(hash && hash[0])) {
return void cb('NO_HASH_RETURNED');
}
cb(e, Array.isArray(hash) && hash[0] || undefined);
});
};
// if local and remote hashes don't match, send a reset
exp.reset = function (channels, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Array.isArray(channels)) {
return void cb('[TypeError] pin expects an array');
}
rpc.send('RESET', channels, cb);
};
// get the combined size of all channels (in bytes) for all the
// channels which the server has pinned for your publicKey
exp.getFileListSize = function (cb) {
rpc.send('GET_TOTAL_SIZE', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && typeof(response[0]) === 'number') {
cb(void 0, response[0]);
} else {
cb('INVALID_RESPONSE');
}
});
};
// Update the limit value for all the users and return the limit for your publicKey
exp.updatePinLimits = function (cb) {
rpc.send('UPDATE_LIMITS', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && typeof(response[0]) === "number") {
cb (void 0, response[0], response[1], response[2]);
} else {
cb('INVALID_RESPONSE');
}
});
};
// Get the storage limit associated with your publicKey
exp.getLimit = function (cb) {
rpc.send('GET_LIMIT', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && typeof(response[0]) === "number") {
cb (void 0, response[0], response[1], response[2]);
} else {
cb('INVALID_RESPONSE');
}
});
};
exp.trimHistory = function (data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (typeof(data) !== 'object' || !data.channel || !data.hash) {
return void cb('INVALID_ARGUMENTS');
}
rpc.send('TRIM_HISTORY', data, function (e) {
if (e) { return cb(e); }
cb();
});
};
exp.clearOwnedChannel = function (channel, cb) {
if (typeof(channel) !== 'string' || channel.length !== 32) {
return void cb('INVALID_ARGUMENTS');
}
rpc.send('CLEAR_OWNED_CHANNEL', channel, function (e) {
if (e) { return cb(e); }
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: channel,
reason: reason
}, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && response[0] === "OK") {
cb();
if (Cache && Cache.clearChannel) {
Cache.clearChannel(channel);
}
} else {
cb('INVALID_RESPONSE');
}
});
};
exp.removePins = function (cb) {
rpc.send('REMOVE_PINS', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && response[0] === "OK") {
cb();
} else {
cb('INVALID_RESPONSE');
}
});
};
exp.uploadComplete = function (id, cb) {
rpc.send('UPLOAD_COMPLETE', id, function (e, res) {
if (e) { return void cb(e); }
var id = res[0];
if (typeof(id) !== 'string') {
return void cb('INVALID_ID');
}
cb(void 0, id);
});
};
exp.ownedUploadComplete = function (id, cb) {
rpc.send('OWNED_UPLOAD_COMPLETE', id, function (e, res) {
if (e) { return void cb(e); }
var id = res[0];
if (typeof(id) !== 'string') {
return void cb('INVALID_ID');
}
cb(void 0, id);
});
};
exp.uploadStatus = function (size, cb) {
if (typeof(size) !== 'number') {
return void setTimeout(function () {
cb('INVALID_SIZE');
});
}
rpc.send('UPLOAD_STATUS', size, function (e, res) {
if (e) { return void cb(e); }
var pending = res[0];
if (typeof(pending) !== 'boolean') {
return void cb('INVALID_RESPONSE');
}
cb(void 0, pending);
});
};
exp.uploadCancel = function (size, cb) {
rpc.send('UPLOAD_CANCEL', size, function (e) {
if (e) { return void cb(e); }
cb();
});
};
// Get data for the admin panel
exp.setMetadata = function (obj, cb) {
rpc.send('SET_METADATA', {
channel: obj.channel,
command: obj.command,
value: obj.value
}, cb);
};
cb(e, exp);
});
};
return { create: create };
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(require('./common-util'), require("./rpc"));
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([ '/common/common-util.js', '/common/rpc.js', ], function (Util, Rpc) { return factory(Util, Rpc); });
}
}());

1813
src/common/proxy-manager.js Normal file

File diff suppressed because it is too large Load Diff

422
src/common/rpc.js Normal file
View File

@ -0,0 +1,422 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(function () {
var factory = function (Util, Nacl) {
// we will send messages with a unique id for each RPC
// that id is returned with each response, indicating which call it was in response to
var uid = Util.uid;
// safely parse json messages, because they might cause parse errors
var tryParse = Util.tryParse;
// we will sign various message with our edPrivate keys
// this handles that in a generic way
var signMsg = function (data, signKey) {
var buffer = Nacl.util.decodeUTF8(JSON.stringify(data));
return Nacl.util.encodeBase64(Nacl.sign.detached(buffer, signKey));
};
// sendMsg takes a pre-formed message, does a little validation
// adds a transaction id to the message and stores its callback
// and finally sends it off to the historyKeeper, which delegates its
// processing to the RPC submodule
var sendMsg = function (ctx, data, cb) {
if (typeof(cb) !== 'function') { throw new Error('expected callback'); }
var network = ctx.network;
var hkn = network.historyKeeper;
if (typeof(hkn) !== 'string') { return void cb("NO_HISTORY_KEEPER"); }
var txid = uid();
var pending = ctx.pending[txid] = function (err, response) {
cb(err, response);
};
pending.data = data;
pending.called = 0;
return network.sendto(hkn, JSON.stringify([txid, data]));
};
var matchesAnon = function (ctx, txid) {
if (!ctx.anon) { return false; }
if (typeof(ctx.anon.pending[txid]) !== 'function') { return false; }
return true;
};
var handleAnon = function (ctx /* anon_ctx */, txid, body /* parsed messages without txid */) {
// if anon is handling it we know there's a pending callback
var pending = ctx.pending[txid];
if (body[0] === 'ERROR') { pending(body[1]); }
else { pending(void 0, body.slice(1)); }
delete ctx.pending[txid];
};
var onMsg = function (ctx /* network context */, msg /* string message */) {
if (typeof(msg) !== 'string') {
console.error("received non-string message [%s]", msg);
}
var parsed = tryParse(msg);
if (!parsed) {
return void console.error(new Error('could not parse message: %s', msg));
}
// RPC messages are always arrays.
if (!Array.isArray(parsed)) { return; }
// ignore FULL_HISTORY messages
if (/(FULL_HISTORY|HISTORY_RANGE)/.test(parsed[0])) { return; }
var txid = parsed[0];
// txid must be a string, or this message is not meant for us
if (typeof(txid) !== 'string') { return; }
if (matchesAnon(ctx, txid)) {
return void handleAnon(ctx.anon, txid, parsed.slice(1));
}
// iterate over authenticated rpc contexts and check if they are expecting
// a message with this txid
if (ctx.authenticated.some(function (rpc_ctx) {
var pending = rpc_ctx.pending[txid];
// not meant for you
if (typeof(pending) !== 'function') { return false; }
// if you're here, the message is for you...
if (parsed[1] !== 'ERROR') {
// if the server sent you a new cookie, replace the old one
if (/\|/.test(parsed[1]) && rpc_ctx.cookie !== parsed[1]) {
rpc_ctx.cookie = parsed[1];
}
pending(void 0, parsed.slice(2));
// if successful, delete the callback...
delete rpc_ctx.pending[txid];
// prevent further iteration
return true;
}
// NO_COOKIE errors mean you failed to authenticate.
// request a new cookie and resend the query
if (parsed[2] === 'NO_COOKIE') {
rpc_ctx.send('COOKIE', "", function (e) {
if (e) {
console.error(e);
return void pending(e);
}
// resend the same command again
// give up if you've already tried resending
if (rpc_ctx.resend(txid)) { delete rpc_ctx.pending[txid]; }
});
// prevent further iteration
return true;
}
// if you're here then your RPC passed authentication but had some other error
// call back with the error message
pending(parsed[2]);
// and delete the pending callback
delete rpc_ctx.pending[txid];
// prevent further iteration
return true;
})) {
// the message was handled, so stop here
return;
}
console.error("UNHANDLED RPC MESSAGE", msg);
};
var networks = [];
var contexts = [];
var initNetworkContext = function (network) {
var ctx = {
network: network,
connected: true,
anon: undefined,
authenticated: [],
};
networks.push(network);
contexts.push(ctx);
// add listeners...
network.on('message', function (msg, sender) {
if (sender !== network.historyKeeper) { return; }
onMsg(ctx, msg);
});
network.on('disconnect', function () {
ctx.connected = false;
if (ctx.anon) { ctx.anon.connected = false; }
ctx.authenticated.forEach(function (ctx) {
ctx.connected = false;
});
});
network.on('reconnect', function () {
if (ctx.anon) { ctx.anon.connected = true; }
ctx.authenticated.forEach(function (ctx) {
ctx.connected = true;
});
});
return ctx;
};
var getNetworkContext = function (network) {
var i;
networks.some(function (current, j) {
if (network !== current) { return false; }
i = j;
return true;
});
if (contexts[i]) { return contexts[i]; }
return initNetworkContext(network);
};
var initAuthenticatedRpc = function (networkContext, keys) {
var ctx = {
network: networkContext.network,
publicKey: keys.publicKeyString,
timeouts: {},
pending: {},
cookie: null,
connected: true,
};
var send = ctx.send = function (type, msg, _cb) {
var cb = Util.mkAsync(_cb);
if (!ctx.connected && type !== 'COOKIE') {
return void cb("DISCONNECTED");
}
// construct a signed message...
var data = [type, msg];
if (ctx.cookie && ctx.cookie.join) {
data.unshift(ctx.cookie.join('|'));
} else {
data.unshift(ctx.cookie);
}
var sig = signMsg(data, keys.signKey);
data.unshift(keys.publicKeyString);
data.unshift(sig);
// [sig, edPublicKey, cookie, type, msg]
return sendMsg(ctx, data, cb);
};
ctx.resend = function (txid) {
var pending = ctx.pending[txid];
if (pending.called) {
console.error("[%s] called too many times", txid);
return true;
}
pending.called++;
// update the cookie and signature...
pending.data[2] = ctx.cookie;
pending.data[0] = signMsg(pending.data.slice(2), keys.signKey);
// store the callback with a new txid
var new_txid = uid();
ctx.pending[new_txid] = pending;
// and delete the old one
delete ctx.pending[txid];
try {
return ctx.network.sendto(ctx.network.historyKeeper,
JSON.stringify([new_txid, pending.data]));
} catch (e) {
console.log("failed to resend");
console.error(e);
}
};
send.unauthenticated = function (type, msg, _cb) {
var cb = Util.mkAsync(_cb);
if (!ctx.connected) { return void cb('DISCONNECTED'); }
// construct an unsigned message
var data = [null, keys.publicKeyString, null, type, msg];
if (ctx.cookie && ctx.cookie.join) {
data[2] = ctx.cookie.join('|');
} else {
data[2] = ctx.cookie;
}
return sendMsg(ctx, data, cb);
};
ctx.destroy = function () {
// clear all pending timeouts
Object.keys(ctx.timeouts).forEach(function (to) {
clearTimeout(to);
});
// remove the ctx from the network's stack
var idx = networkContext.authenticated.indexOf(ctx);
if (idx === -1) { return; }
networkContext.authenticated.splice(idx, 1);
};
networkContext.authenticated.push(ctx);
return ctx;
};
var getAuthenticatedContext = function (networkContext, keys) {
if (!networkContext) { throw new Error('expected network context'); }
var publicKey = keys.publicKeyString;
var i;
networkContext.authenticated.some(function (ctx, j) {
if (ctx.publicKey !== publicKey) { return false; }
i = j;
return true;
});
if (networkContext.authenticated[i]) { return networkContext.authenticated[i]; }
return initAuthenticatedRpc(networkContext, keys);
};
var create = function (network, edPrivateKey, edPublicKey, _cb) {
if (typeof(_cb) !== 'function') { throw new Error("expected callback"); }
var cb = Util.mkAsync(_cb);
var signKey;
try {
signKey = Nacl.util.decodeBase64(edPrivateKey);
if (signKey.length !== 64) {
throw new Error('private key did not match expected length of 64');
}
} catch (err) {
return void cb(err);
}
try {
if (Nacl.util.decodeBase64(edPublicKey).length !== 32) {
return void cb('expected public key to be 32 uint');
}
} catch (err) { return void cb(err); }
if (!network) { return void cb('NO_NETWORK'); }
// get or create a context for the provided network
var net_ctx = getNetworkContext(network);
var rpc_ctx = getAuthenticatedContext(net_ctx, {
publicKeyString: edPublicKey,
signKey: signKey,
});
rpc_ctx.send('COOKIE', "", function (e) {
if (e) { return void cb(e); }
// callback to provide 'send' method to whatever needs it
cb(void 0, {
send: rpc_ctx.send,
destroy: rpc_ctx.destroy,
});
});
};
var initAnonRpc = function (networkContext) {
var ctx = {
network: networkContext.network,
timeouts: {},
pending: {},
connected: true,
};
// any particular network will only ever need one anonymous rpc
networkContext.anon = ctx;
ctx.send = function (type, msg, _cb) {
var cb = Util.mkAsync(_cb);
if (!ctx.connected) { return void cb('DISCONNECTED'); }
// construct an unsigned message...
var data = [type, msg];
// [type, msg]
return sendMsg(ctx, data, cb);
};
ctx.resend = function (txid) {
var pending = ctx.pending[txid];
if (pending.called) {
console.error("[%s] called too many times", txid);
return true;
}
pending.called++;
try {
return ctx.network.sendto(ctx.network.historyKeeper,
JSON.stringify([txid, pending.data]));
} catch (e) {
console.log("failed to resend");
console.error(e);
}
};
ctx.destroy = function () {
// clear all pending timeouts
Object.keys(ctx.timeouts).forEach(function (to) {
clearTimeout(to);
});
networkContext.anon = undefined;
};
return ctx;
};
var getAnonContext = function (networkContext) {
return networkContext.anon || initAnonRpc(networkContext);
};
var createAnonymous = function (network, _cb) {
// enforce asynchrony
var cb = Util.mkAsync(_cb);
if (typeof(cb) !== 'function') { throw new Error("expected callback"); }
if (!network) { return void cb('NO_NETWORK'); }
// get or create a context for the provided network
var ctx = getAnonContext(getNetworkContext(network));
cb(void 0, {
send: ctx.send,
destroy: ctx.destroy,
});
};
return { create: create, createAnonymous: createAnonymous };
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(require("./common-util"), require("tweetnacl/nacl-fast"));
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/components/tweetnacl/nacl-fast.min.js',
], function (Util) {
return factory(Util, window.nacl);
});
} else {
// I'm not gonna bother supporting any other kind of instanciation
}
}());

File diff suppressed because it is too large Load Diff

997
src/common/user-object.js Normal file
View File

@ -0,0 +1,997 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (AppConfig = {}, Util, Hash,
Constants, UOSetter, Crypto, Messages = {}) => {
let window = globalThis;
var module = {};
module.setCustomize = (data) => {
Messages = data.Messages;
AppConfig = data.AppConfig;
UOSetter.setCustomize(data);
};
var ROOT = module.ROOT = "root";
var UNSORTED = module.UNSORTED = "unsorted";
var TRASH = module.TRASH = "trash";
var TEMPLATE = module.TEMPLATE = "template";
var SHARED_FOLDERS = module.SHARED_FOLDERS = "sharedFolders";
var SHARED_FOLDERS_TEMP = module.SHARED_FOLDERS_TEMP = "sharedFoldersTemp"; // Maybe deleted or new password
var FILES_DATA = module.FILES_DATA = Constants.storageKey;
var OLD_FILES_DATA = module.OLD_FILES_DATA = Constants.oldStorageKey;
var STATIC_DATA = module.STATIC_DATA = 'static';
// Create untitled documents when no name is given
var getLocaleDate = function () {
if (window.Intl && window.Intl.DateTimeFormat) {
var options = {weekday: "short", year: "numeric", month: "long", day: "numeric"};
return new window.Intl.DateTimeFormat(undefined, options).format(new Date());
}
return new Date().toString().split(' ').slice(0,4).join(' ');
};
module.getDefaultName = function (parsed) {
var type = parsed.type;
var name = (Messages.type)[type] + ' - ' + getLocaleDate();
return name;
};
var createCryptor = module.createCryptor = function (key) {
var cryptor = {};
if (!key) {
cryptor.encrypt = function (x) { return x; };
cryptor.decrypt = function (x) { return x; };
return cryptor;
}
try {
var c = Crypto.createEncryptor(key);
cryptor.encrypt = function (href) {
// Never encrypt blob href, they are always read-only
try {
if (href.slice(0,7) === '/file/#') { return href; }
return c.encrypt(href);
} catch (e) {
return;
}
};
cryptor.decrypt = function (msg) {
try {
return c.decrypt(msg);
} catch (e) {
return;
}
};
} catch (e) {
console.error(e);
}
return cryptor;
};
module.getHref = function (pad, cryptor) {
if (pad.href && pad.href.indexOf('#') !== -1) {
// Href exists and is not encrypted: return href
return pad.href;
}
if (pad.href) {
// Href exists and is encrypted
var d = cryptor.decrypt(pad.href);
// If we can decrypt, return the decrypted value, otherwise continue and return roHref
if (d && d.indexOf('#') !== -1) {
return d;
}
}
return pad.roHref;
};
module.reencrypt = function (oldKey, newKey, obj) {
if (!obj) { return void console.error("Nothing to reencrypt"); }
var oldCryptor = createCryptor(oldKey);
var newCryptor = createCryptor(newKey);
Object.keys(obj[FILES_DATA]).forEach(function (id) {
var data = obj[FILES_DATA][id] || {};
// If this pad has a visible href, encrypt it
// "&& data.roHref" is here to make sure this is not a "file"
if (data.href && data.roHref && !data.fileType) {
var _href = (data.href && data.href.indexOf('#') === -1) ? oldCryptor.decrypt(data.href) : data.href;
if (!_href) { return; }
data.href = newCryptor.encrypt(_href);
}
});
Object.keys(obj[SHARED_FOLDERS] || {}).forEach(function (id) {
var data = obj[SHARED_FOLDERS][id] || {};
// If this folder has a visible href, encrypt it
if (data.href) {
var _href = (data.href && data.href.indexOf('#') === -1) ? oldCryptor.decrypt(data.href) : data.href;
if (!_href) { return; }
data.href = newCryptor.encrypt(_href);
}
});
Object.keys(obj[SHARED_FOLDERS_TEMP] || {}).forEach(function (id) {
var data = obj[SHARED_FOLDERS_TEMP][id] || {};
// If this folder has a visible href, encrypt it
if (data.href) {
var _href = (data.href && data.href.indexOf('#') === -1) ? oldCryptor.decrypt(data.href) : data.href;
if (!_href) { return; }
data.href = newCryptor.encrypt(_href);
}
});
};
module.init = function (files, config) {
var exp = {};
exp.cryptor = createCryptor(config.editKey);
exp.setReadOnly = function (state, key) {
config.editKey = key;
exp.cryptor = createCryptor(key);
exp.cryptor.k = Math.random();
exp.readOnly = state;
if (exp._setReadOnly) {
// Change outer
exp._setReadOnly(state);
}
};
exp.readOnly = config.readOnly;
exp.reencrypt = module.reencrypt;
exp.getDefaultName = module.getDefaultName;
var sframeChan = config.sframeChan;
var NEW_FOLDER_NAME = Messages.fm_newFolder || 'New folder';
var NEW_FILE_NAME = Messages.fm_newFile || 'New file';
exp.ROOT = ROOT;
exp.STATIC_DATA = STATIC_DATA;
exp.UNSORTED = UNSORTED;
exp.TRASH = TRASH;
exp.TEMPLATE = TEMPLATE;
exp.SHARED_FOLDERS = SHARED_FOLDERS;
exp.SHARED_FOLDERS_TEMP = SHARED_FOLDERS_TEMP;
exp.FILES_DATA = FILES_DATA;
exp.OLD_FILES_DATA = OLD_FILES_DATA;
var sharedFolder = exp.sharedFolder = config.sharedFolder;
exp.id = config.id;
// Logging
var logging = function () {
console.debug.apply(console, arguments);
};
var log = exp.log = config.log || logging;
var logError = config.logError || logging;
var debug = exp.debug = config.debug || logging;
exp.fixFiles = function () {}; // Overriden by UOSetter
var error = exp.error = function() {
if (sframeChan) {
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "fixFiles",
data: {}
}, function () {});
} else if (typeof (exp.fixFiles) === "function") {
exp.fixFiles();
}
console.error.apply(console, arguments);
exp.fixFiles();
};
if (config.outer) {
// Extend "exp" with methods used only outside of the iframe (requires access to store)
UOSetter.init(config, exp, files);
}
/*
* UTILS
*/
exp.getStructure = function () {
var a = {};
a[ROOT] = {};
a[TRASH] = {};
a[FILES_DATA] = {};
a[TEMPLATE] = [];
a[SHARED_FOLDERS] = {};
return a;
};
var getHref = exp.getHref = function (pad) {
return module.getHref(pad, exp.cryptor);
};
var type = function (dat) {
return dat === null? 'null': Array.isArray(dat)?'array': typeof(dat);
};
exp.isValidDrive = function (obj) {
var base = exp.getStructure();
return typeof (obj) === "object" &&
Object.keys(base).every(function (key) {
return obj[key] && type(base[key]) === type(obj[key]);
});
};
var getHrefArray = function () {
return [TEMPLATE];
};
var compareFiles = function (fileA, fileB) { return fileA === fileB; };
var isSharedFolder = exp.isSharedFolder = function (element) {
if (sharedFolder) { return false; } // No recursive shared folders
return Boolean(files[SHARED_FOLDERS] && files[SHARED_FOLDERS][element]);
};
var isFile = exp.isFile = function (element, allowStr) {
if (isSharedFolder(element)) { return false; }
return typeof(element) === "number" ||
((typeof(files[OLD_FILES_DATA]) !== "undefined" || allowStr)
&& typeof(element) === "string");
};
var isFolderData = exp.isFolderData = function (element) {
return typeof(element) === "object" && element.metadata === true;
};
exp.isReadOnlyFile = function (element) {
if (!isFile(element)) { return false; }
var data = exp.getFileData(element);
// undefined means this pad doesn't support read-only
if (!data.roHref) { return; }
return Boolean(data.roHref && !data.href);
};
exp.isStaticFile = function (element) {
return Boolean(files[STATIC_DATA] && files[STATIC_DATA][element]);
};
var isFolder = exp.isFolder = function (element) {
if (isFolderData(element)) { return false; }
return typeof(element) === "object" || isSharedFolder(element);
};
exp.isFolderEmpty = function (element) {
if (!isFolder(element)) { return false; }
// if the folder contains nothing, it's empty
if (Object.keys(element).length === 0) { return true; }
// or if it contains one thing and that thing is metadata
if (Object.keys(element).length === 1 && isFolderData(element[Object.keys(element)[0]])) { return true; }
return false;
};
exp.hasSubfolder = function (element, trashRoot) {
if (!isFolder(element)) { return false; }
var subfolder = 0;
var addSubfolder = function (el) {
subfolder += isFolder(el.element) ? 1 : 0;
};
for (var f in element) {
if (trashRoot) {
if (Array.isArray(element[f])) {
element[f].forEach(addSubfolder);
}
} else {
subfolder += isFolder(element[f]) ? 1 : 0;
}
}
return subfolder;
};
exp.hasFile = function (element, trashRoot) {
if (!isFolder(element)) { return false; }
var file = 0;
var addFile = function (el) {
file += isFile(el.element) ? 1 : 0;
};
for (var f in element) {
if (trashRoot) {
if (Array.isArray(element[f])) {
element[f].forEach(addFile);
}
} else {
file += isFile(element[f]) ? 1 : 0;
}
}
return file;
};
exp.hasFolderData = function (folder) {
for (var el in folder) {
if(isFolderData(folder[el])) {
return true;
}
}
};
var hasSubSharedFolder = exp.hasSubSharedFolder = function (folder) {
for (var el in folder) {
if (isSharedFolder(folder[el])) {
return true;
}
else if (isFolder(folder[el])) {
if (hasSubSharedFolder(folder[el])) {
return true;
}
}
}
return false;
};
// Get data from AllFiles (Cryptpad_RECENTPADS)
var getFileData = exp.getFileData = function (file, editable) {
if (!file) { return; }
var link;
try {
link = (files[STATIC_DATA] || {})[file];
} catch (err) {
console.error(err);
}
if (link) {
var _link = editable ? link : Util.clone(link);
if (!editable) { _link.static = true; }
return _link;
}
var data;
try {
data = files[FILES_DATA][file] || {};
} catch (err) {
console.error(err);
data = {};
}
if (!editable) {
data = JSON.parse(JSON.stringify(data));
if (data.href && data.href.indexOf('#') === -1) {
// Encrypted href: decrypt it if we can, otherwise remove it
if (config.editKey) {
try {
data.href = exp.cryptor.decrypt(data.href);
} catch (e) {
delete data.href;
}
} else {
delete data.href;
}
}
}
return data;
};
exp.getFolderData = function (folder) {
for (var el in folder) {
if(isFolderData(folder[el])) {
return folder[el];
}
}
return {};
};
// Data from filesData
var getTitle = exp.getTitle = function (file, type) {
if (isSharedFolder(file)) {
return '??';
}
var data = getFileData(file);
if (!data) {
error("unable to retrieve data about the requested file: ", file, data);
return;
}
// handle links
if (data.static) { return data.name; }
if (!file || !(data.href || data.roHref)) {
error("getTitle called with a non-existing file id: ", file, data);
return;
}
if (type === 'title') { return data.title; }
if (type === 'name') { return data.filename; }
return data.filename || data.title || NEW_FILE_NAME;
};
// PATHS
var comparePath = exp.comparePath = function (a, b) {
if (!a || !b || !Array.isArray(a) || !Array.isArray(b)) { return false; }
if (a.length !== b.length) { return false; }
var result = true;
var i = a.length - 1;
while (result && i >= 0) {
result = a[i] === b[i];
i--;
}
return result;
};
var isSubpath = exp.isSubpath = function (path, parentPath) {
var pathA = parentPath.slice();
var pathB = path.slice(0, pathA.length);
return comparePath(pathA, pathB);
};
var isPathIn = exp.isPathIn = function (path, categories) {
if (!categories) { return; }
var idx = categories.indexOf('hrefArray');
if (idx !== -1) {
categories.splice(idx, 1);
categories = categories.concat(getHrefArray());
}
return categories.some(function (c) {
return Array.isArray(path) && path[0] === c;
});
};
var isInTrashRoot = exp.isInTrashRoot = function (path) {
return path[0] === TRASH && path.length === 4;
};
// FIND
var findElement = function (root, pathInput) {
if (!pathInput) {
error("Invalid path:\n", pathInput, "\nin root\n", root);
return;
}
if (pathInput.length === 0) { return root; }
var path = pathInput.slice();
var key = path.shift();
if (typeof root[key] === "undefined") {
debug("Unable to find the key '" + key + "' in the root object provided:", root);
return;
}
return findElement(root[key], path);
};
var find = exp.find = function (path) {
return findElement(files, path);
};
// GET FILES
var getFilesRecursively = exp.getFilesRecursively = function (root, arr) {
arr = arr || [];
for (var e in root) {
if (isFile(root[e]) || isSharedFolder(root[e])) {
if(arr.indexOf(root[e]) === -1) { arr.push(root[e]); }
} else if (!isFolderData(root[e])) {
getFilesRecursively(root[e], arr);
}
}
return arr;
};
var _getFiles = {};
_getFiles['array'] = function (cat) {
if (!files[cat]) { files[cat] = []; }
return files[cat].slice();
};
getHrefArray().forEach(function (c) {
_getFiles[c] = function () { return _getFiles['array'](c); };
});
_getFiles['hrefArray'] = function () {
var ret = [];
if (sharedFolder) { return ret; }
getHrefArray().forEach(function (c) {
ret = ret.concat(_getFiles[c]());
});
return Util.deduplicateString(ret);
};
_getFiles[ROOT] = function () {
var ret = [];
getFilesRecursively(files[ROOT], ret);
return ret;
};
_getFiles[TRASH] = function () {
var root = files[TRASH];
var ret = [];
var addFiles = function (el) {
if (isFile(el.element) || isSharedFolder(el.element)) {
if(ret.indexOf(el.element) === -1) { ret.push(el.element); }
} else {
getFilesRecursively(el.element, ret);
}
};
for (var e in root) {
if (!Array.isArray(root[e])) {
error("Trash contains a non-array element");
return;
}
root[e].forEach(addFiles);
}
return ret;
};
_getFiles[OLD_FILES_DATA] = function () {
var ret = [];
if (!files[OLD_FILES_DATA]) { return ret; }
files[OLD_FILES_DATA].forEach(function (el) {
if (el.href && ret.indexOf(el.href) === -1) {
ret.push(el.href);
}
});
return ret;
};
_getFiles[STATIC_DATA] = function () {
var ret = [];
if (!files[STATIC_DATA]) { return ret; }
return Object.keys(files[STATIC_DATA]).map(Number).filter(Boolean);
};
_getFiles[FILES_DATA] = function () {
var ret = [];
if (!files[FILES_DATA]) { return ret; }
return Object.keys(files[FILES_DATA]).map(Number).filter(Boolean);
};
_getFiles[SHARED_FOLDERS] = function () {
var ret = [];
if (!files[SHARED_FOLDERS]) { return ret; }
return Object.keys(files[SHARED_FOLDERS]).map(Number).filter(Boolean);
};
var getFiles = exp.getFiles = function (categories) {
var ret = [];
if (!categories || !categories.length) {
categories = [ROOT, 'hrefArray', TRASH, OLD_FILES_DATA, FILES_DATA, SHARED_FOLDERS];
}
categories.forEach(function (c) {
if (typeof _getFiles[c] === "function") {
ret = ret.concat(_getFiles[c]());
}
});
return Util.deduplicateString(ret);
};
var getIdFromHref = exp.getIdFromHref = function (_href) {
var result;
var noPassword = function (str) {
if (!str) { return; }
var parsed = Hash.parsePadUrl(str);
return parsed.getUrl().replace(/\/p\/?/, '/');
};
var href = noPassword(_href);
getFiles([FILES_DATA]).some(function (id) {
if (noPassword(getHref(files[FILES_DATA][id])) === href ||
noPassword(files[FILES_DATA][id].roHref) === href) {
result = id;
return true;
}
});
return result;
};
exp.getSFIdFromHref = function (_href) {
var result;
var noPassword = function (str) {
if (!str) { return; }
var parsed = Hash.parsePadUrl(str);
return parsed.getUrl().replace(/\/p\/?/, '/');
};
var href = noPassword(_href);
getFiles([SHARED_FOLDERS]).some(function (id) {
if (noPassword(getHref(files[SHARED_FOLDERS][id])) === href ||
noPassword(files[SHARED_FOLDERS][id].roHref) === href) {
result = id;
return true;
}
});
return result;
};
// SEARCH
var _findFileInRoot = function (path, file) {
if (!isPathIn(path, [ROOT, TRASH])) { return []; }
var paths = [];
var root = find(path);
var addPaths = function (p) {
if (paths.indexOf(p) === -1) {
paths.push(p);
}
};
if (isFile(root) || isSharedFolder(root)) {
if (compareFiles(file, root)) {
if (paths.indexOf(path) === -1) {
paths.push(path);
}
}
return paths;
}
if (isFolder(root)) {
for (var e in root) {
var nPath = path.slice();
nPath.push(e);
_findFileInRoot(nPath, file).forEach(addPaths);
}
}
return paths;
};
exp.findFileInRoot = function (file) {
return _findFileInRoot([ROOT], file);
};
var _findFileInHrefArray = function (rootName, file) {
if (sharedFolder) { return []; }
if (!files[rootName]) { return []; }
var unsorted = files[rootName].slice();
var ret = [];
var i = -1;
while ((i = unsorted.indexOf(file, i+1)) !== -1){
ret.push([rootName, i]);
}
return ret;
};
var _findFileInTrash = function (path, file) {
if (sharedFolder) { return []; }
var root = find(path);
var paths = [];
var addPaths = function (p) {
if (paths.indexOf(p) === -1) {
paths.push(p);
}
};
if (path.length === 1 && typeof(root) === 'object') {
Object.keys(root).forEach(function (key) {
var arr = root[key];
if (!Array.isArray(arr)) { return; }
var nPath = path.slice();
nPath.push(key);
_findFileInTrash(nPath, file).forEach(addPaths);
});
}
if (path.length === 2) {
if (!Array.isArray(root)) { return []; }
root.forEach(function (el, i) {
var nPath = path.slice();
nPath.push(i);
nPath.push('element');
if (isFile(el.element)) {
if (compareFiles(file, el.element)) {
addPaths(nPath);
}
return;
}
_findFileInTrash(nPath, file).forEach(addPaths);
});
}
if (path.length >= 4) {
_findFileInRoot(path, file).forEach(addPaths);
}
return paths;
};
var findFile = exp.findFile = function (file) {
var rootpaths = _findFileInRoot([ROOT], file);
var templatepaths = _findFileInHrefArray(TEMPLATE, file);
var trashpaths = _findFileInTrash([TRASH], file);
return rootpaths.concat(templatepaths, trashpaths);
};
// Get drive ids of files from their channel ids
exp.findChannels = function (channels, includeSharedFolders) {
var allFilesList = files[FILES_DATA];
var sfList = files[SHARED_FOLDERS];
var paths = [FILES_DATA];
if (includeSharedFolders) { paths.push(SHARED_FOLDERS); }
return getFiles(paths).filter(function (k) {
var data = allFilesList[k] || sfList[k] || {};
return channels.indexOf(data.channel) !== -1;
});
};
exp.search = function (value) {
if (typeof(value) !== "string") { return []; }
value = value.trim();
var res = [];
// Search title
var allFilesList = files[FILES_DATA];
var allSFList = files[SHARED_FOLDERS];
var lValue = value.toLowerCase();
// parse the search string into tags
var tags;
if (/^#/.test(lValue)) {
tags = [lValue.slice(1).trim()];
}
/* returns true if an entry's tags are at least a partial match for
one of the specified tags */
var containsSearchedTag = function (T) {
if (!tags) { return false; }
if (!T.length) { return false; }
T = T.map(function (t) { return t.toLowerCase(); });
return tags.some(function (tag) {
return T.some(function (t) {
return t === tag;
});
});
};
getFiles([FILES_DATA, SHARED_FOLDERS]).forEach(function (id) {
var data = allFilesList[id] || allSFList[id];
if (!data) { return; }
if (Array.isArray(data.tags) && containsSearchedTag(data.tags)) {
return void res.push(id);
}
var title = data.title || data.lastTitle;
if ((title && title.toLowerCase().indexOf(lValue) !== -1) ||
(data.filename && data.filename.toLowerCase().indexOf(lValue) !== -1)) {
res.push(id);
}
});
// Search Href
var href = Hash.getRelativeHref(value);
if (href) {
var id = getIdFromHref(href);
if (id) { res.push(id); }
}
res = Util.deduplicateString(res);
var ret = [];
res.forEach(function (l) {
//var paths = findFile(l);
ret.push({
id: l,
paths: findFile(l),
data: exp.getFileData(l)
});
});
// find folders
var resFolders = [];
var findFoldersRec = function (folder, path) {
for (var key in folder) {
if (isFolder(folder[key]) && !isSharedFolder(folder[key])) {
if (key.toLowerCase().indexOf(lValue) !== -1) {
resFolders.push({
id: null,
paths: [path.concat(key)],
data: {
title: key
}
});
}
findFoldersRec(folder[key], path.concat(key));
}
}
};
findFoldersRec(files[ROOT], [ROOT]);
resFolders = resFolders.sort(function (a, b) {
return a.data.title.toLowerCase() > b.data.title.toLowerCase();
});
ret = resFolders.concat(ret);
return ret;
};
exp.getRecentPads = function () {
var allFiles = files[FILES_DATA];
var sorted = Object.keys(allFiles).filter(function (a) { return allFiles[a]; })
.sort(function (a,b) {
return allFiles[b].atime - allFiles[a].atime;
})
.map(function (str) { return Number(str); });
return sorted;
};
exp.getOwnedPads = function (edPub) {
var allFiles = files[FILES_DATA];
return Object.keys(allFiles).filter(function (id) {
return allFiles[id].owners && allFiles[id].owners.indexOf(edPub) !== -1;
}).map(function (k) { return Number(k); });
};
/**
* OPERATIONS
*/
var getAvailableName = exp.getAvailableName = function (parentEl, name) {
if (typeof(parentEl[name]) === "undefined") { return name; }
var newName = name;
var i = 1;
while (typeof(parentEl[newName]) !== "undefined") {
newName = name + "_" + i;
i++;
}
return newName;
};
// MOVE
var move = exp.move = function (paths, newPath, cb) {
if (sframeChan) {
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "move",
data: {
paths: paths,
newPath: newPath
}
}, cb);
}
// Copy the elements to their new location
var toRemove = [];
paths.forEach(function (p) {
var parentPath = p.slice();
parentPath.pop();
if (comparePath(parentPath, newPath)) { return; }
if (isSubpath(newPath, p)) {
log(Messages.fo_moveFolderToChildError);
return;
}
// Try to copy, and if success, remove the element from the old location
if (exp.copyElement(p.slice(), newPath)) {
toRemove.push(p);
}
});
exp.delete(toRemove, cb);
};
exp.restore = function (path, cb) {
if (sframeChan) {
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "restore",
data: {
path: path
}
}, cb);
}
if (!isInTrashRoot(path)) { return; }
var parentPath = path.slice();
parentPath.pop();
var oldPath = find(parentPath).path;
move([path], oldPath, cb);
};
// ADD
exp.addFolder = function (folderPath, name, cb) {
if (sframeChan) {
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "addFolder",
data: {
path: folderPath,
name: name
}
}, cb);
}
var parentEl = find(folderPath);
var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME);
parentEl[folderName] = {};
var newPath = folderPath.slice();
newPath.push(folderName);
cb({
newPath: newPath
});
};
// DELETE
// Permanently delete multiple files at once using a list of paths
// NOTE: We have to be careful when removing elements from arrays (trash root, unsorted or template)
exp.delete = function (paths, cb, nocheck) {
if (sframeChan) {
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "delete",
data: {
paths: paths,
nocheck: nocheck,
}
}, cb);
}
cb = cb || function () {};
exp.deleteMultiplePermanently(paths, nocheck, cb);
//if (typeof cb === "function") { cb(); }
};
exp.emptyTrash = function (cb) {
cb = cb || function () {};
if (sframeChan) {
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "emptyTrash"
}, cb);
}
files[TRASH] = {};
exp.checkDeletedFiles(cb);
};
exp.ownedInTrash = function (isOwned) {
return getFiles([TRASH]).map(function (id) {
var data = isSharedFolder(id) ? files[SHARED_FOLDERS][id] : exp.getFileData(id);
if (!data) { return; }
return isOwned(data.owners) ? data.channel : undefined;
}).filter(Boolean);
};
// RENAME
exp.rename = function (path, newName, cb) {
cb = cb || function () {};
if (sframeChan) {
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "rename",
data: {
path: path,
newName: newName
}
}, cb);
}
if (path.length <= 1) {
logError('Renaming `root` is forbidden');
return;
}
// Copy the element path and remove the last value to have the parent path and the old name
var element = find(path);
// Folders
if (isFolder(element) && !isSharedFolder(element)) {
var parentPath = path.slice();
var oldName = parentPath.pop();
if (!newName || !newName.trim() || oldName === newName) { return; }
var parentEl = find(parentPath);
if (typeof(parentEl[newName]) !== "undefined") {
log(Messages.fo_existingNameError);
return;
}
parentEl[newName] = element;
delete parentEl[oldName];
if (typeof cb === "function") { cb(); }
return;
}
// Files or Shared folder
var data;
if (isSharedFolder(element)) {
data = files[SHARED_FOLDERS][element];
} else {
data = files[FILES_DATA][element] || files[STATIC_DATA][element];
}
if (!data) { return; }
if (files[STATIC_DATA][element]) {
if (!newName || !newName.trim()) { return void cb(); }
data.name = newName;
cb();
return;
}
if (!newName || newName.trim() === "") {
delete data.filename;
if (typeof cb === "function") { cb(); }
return;
}
if (getTitle(element, 'name') === newName) { return; }
data.filename = newName;
if (typeof cb === "function") { cb(); }
};
// Tags
exp.getTagsList = function () {
var tags = {};
var data;
var pushTag = function (tag) {
tags[tag] = tags[tag] ? ++tags[tag] : 1;
};
for (var id in files[FILES_DATA]) {
data = files[FILES_DATA][id];
if (!data.tags || !Array.isArray(data.tags)) { continue; }
data.tags.forEach(pushTag);
}
return tags;
};
return exp;
};
return module;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
undefined,
require('./common-util'),
require('./common-hash'),
require('./common-constants'),
require('./user-object-setter'),
require('chainpad-crypto'),
undefined
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/customize/application_config.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/user-object-setter.js',
'/components/chainpad-crypto/crypto.js',
'/customize/messages.js',
], factory);
} else {
// unsupported initialization
}
})();

15
src/messages.js Normal file
View File

@ -0,0 +1,15 @@
(() => {
const factory = (Messages) => {
Messages = Messages || {};
Messages._getKey = k => { return Messages[k]; };
return Messages;
};
if (typeof(module) !== 'undefined' && module.exports) {
var Msg = require('../www/common/translations/messages.json');
module.exports = factory(Msg);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([], factory);
}
})();

View File

@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(function () {
var factory = function (Util, Cred, Nacl, Crypto) {
var Invite = {};
var encode64 = Nacl.util.encodeBase64;
var decode64 = Nacl.util.decodeBase64;
// ed and curve keys can be random...
Invite.generateKeys = function () {
var ed = Nacl.sign.keyPair();
var curve = Nacl.box.keyPair();
return {
edPublic: encode64(ed.publicKey),
edPrivate: encode64(ed.secretKey),
curvePublic: encode64(curve.publicKey),
curvePrivate: encode64(curve.secretKey),
};
};
Invite.generateSignPair = function () {
var ed = Nacl.sign.keyPair();
return {
validateKey: encode64(ed.publicKey),
signKey: encode64(ed.secretKey),
};
};
var b64ToChannelKeys = function (b64) {
var dispense = Cred.dispenser(decode64(b64));
return {
channel: Util.uint8ArrayToHex(dispense(16)),
cryptKey: dispense(Nacl.secretbox.keyLength),
};
};
// the secret invite values (cryptkey and channel) can be derived
// from the link seed and (optional) password
Invite.deriveInviteKeys = b64ToChannelKeys;
// the preview values (cryptkey and channel) are less sensitive than the invite values
// as they cannot be leveraged to access any further content on their own
// unless the message contains secrets.
// derived from the link seed alone.
Invite.derivePreviewKeys = b64ToChannelKeys;
Invite.createRosterEntry = function (roster, data, cb) {
var toInvite = {};
toInvite[data.curvePublic] = data.content;
roster.invite(toInvite, cb);
};
// Invite links should only be visible to members or above, so
// we store them in the roster encrypted with a string only available
// to users with edit rights
var decodeUTF8 = Nacl.util.decodeUTF8;
Invite.encryptHash = function (data, seedStr) {
var array = decodeUTF8(seedStr);
var bytes = Nacl.hash(array);
var cryptKey = bytes.subarray(0, 32);
return Crypto.encrypt(data, cryptKey);
};
Invite.decryptHash = function (encryptedStr, seedStr) {
var array = decodeUTF8(seedStr);
var bytes = Nacl.hash(array);
var cryptKey = bytes.subarray(0, 32);
return Crypto.decrypt(encryptedStr, cryptKey);
};
/* INPUTS
* password (for scrypt)
* message (personal note)
* link hash
* bytes64 (scrypt output)
* preview_hash
*/
/* IO / FUNCTIONALITY
* creator
* generate a random signKey (prevent writes to preview channel)
* encrypt and upload the preview content
* via CryptGet
* owned by:
* the ephemeral edPublic
* the invite creator
* create a roster entry for the invitation
* with encrypted notes for the creator
* redeemer
* get the preview content
* redeem the invite
* add yourself to the roster
* add the team to your proxy-manager
*/
return Invite;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require("../../common/common-util"),
require("../../common/common-credential"),
require("tweetnacl/nacl-fast"),
require("chainpad-crypto")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-credential.js',
'/components/chainpad-crypto/crypto.js',
'/components/tweetnacl/nacl-fast.min.js',
], function (Util, Cred, Crypto) {
return factory(Util, Cred, window.nacl, Crypto);
});
}
}());

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,544 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (AppConfig = {}, Feedback, Hash, Util,
Messaging, Crypt, Mailbox, Messages = {},
Realtime, nThen, Crypto) => {
const setCustomize = data => {
AppConfig = data.AppConfig;
Messages = data.Messages;
};
// Start migration check
// Versions:
// 1: migrate pad attributes
// 2: migrate indent settings (codemirror)
let migrate = function (userObject, cb, progress, store) {
var version = userObject.version || 0;
nThen(function () {
// DEPRECATED
// Migration 1: pad attributes moved to filesData
var migratePadAttributesToData = function () {
return true;
};
if (version < 1) {
migratePadAttributesToData();
}
}).nThen(function () {
// Migration 2: global attributes from root to 'settings' subobjects
var migrateAttributes = function () {
var drawer = 'cryptpad.userlist-drawer';
var polls = 'cryptpad.hide_poll_text';
var indentKey = 'cryptpad.indentUnit';
var useTabsKey = 'cryptpad.indentWithTabs';
var settings = userObject.settings = userObject.settings || {};
if (typeof(userObject[indentKey]) !== "undefined") {
settings.codemirror = settings.codemirror || {};
settings.codemirror.indentUnit = userObject[indentKey];
delete userObject[indentKey];
}
if (typeof(userObject[useTabsKey]) !== "undefined") {
settings.codemirror = settings.codemirror || {};
settings.codemirror.indentWithTabs = userObject[useTabsKey];
delete userObject[useTabsKey];
}
if (typeof(userObject[drawer]) !== "undefined") {
settings.toolbar = settings.toolbar || {};
settings.toolbar['userlist-drawer'] = userObject[drawer];
delete userObject[drawer];
}
if (typeof(userObject[polls]) !== "undefined") {
settings.poll = settings.poll || {};
settings.poll['hide-text'] = userObject[polls];
delete userObject[polls];
}
};
if (version < 2) {
migrateAttributes();
Feedback.send('Migrate-2', true);
userObject.version = version = 2;
}
}).nThen(function () {
// Migration 3: language from localStorage to settings
var migrateLanguage = function () {
if (!localStorage.CRYPTPAD_LANG) { return; }
var l = localStorage.CRYPTPAD_LANG;
userObject.settings.language = l;
};
if (version < 3) {
migrateLanguage();
Feedback.send('Migrate-3', true);
userObject.version = version = 3;
}
}).nThen(function () {
// Migration 4: allowUserFeedback to settings
var migrateFeedback = function () {
var settings = userObject.settings = userObject.settings || {};
if (typeof(userObject['allowUserFeedback']) !== "undefined") {
settings.general = settings.general || {};
settings.general.allowUserFeedback = userObject['allowUserFeedback'];
delete userObject['allowUserFeedback'];
}
};
if (version < 4) {
migrateFeedback();
Feedback.send('Migrate-4', true);
userObject.version = version = 4;
}
}).nThen(function () {
// Migration 5: dates to Number
var migrateDates = function () {
var data = userObject.drive && userObject.drive.filesData;
if (data) {
for (var id in data) {
if (typeof data[id].ctime !== "number") {
data[id].ctime = +new Date(data[id].ctime);
}
if (typeof data[id].atime !== "number") {
data[id].atime = +new Date(data[id].atime);
}
}
}
};
if (version < 5) {
migrateDates();
Feedback.send('Migrate-5', true);
userObject.version = version = 5;
}
}).nThen(function (waitFor) {
var addChannelId = function () {
var data = userObject.drive.filesData || {};
var el, parsed;
var n = nThen(function () {});
var padsLength = Object.keys(data).length;
Object.keys(data).forEach(function (k, i) {
n = n.nThen(function (w) {
setTimeout(w(function () {
el = data[k];
parsed = Hash.parsePadUrl(el.href);
if (!el.href) { return; }
if (!el.channel) {
var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
el.channel = secret.channel;
progress(6, Math.round(100*i/padsLength));
console.log('Adding missing channel in filesData ', el.channel);
}
}));
});
});
n.nThen(waitFor(function () {
Feedback.send('Migrate-6', true);
userObject.version = version = 6;
}));
};
if (version < 6) {
addChannelId();
}
}).nThen(function (waitFor) {
var addRoHref = function () {
var data = userObject.drive.filesData;
var el, parsed;
var n = nThen(function () {});
var padsLength = Object.keys(data).length;
Object.keys(data).forEach(function (k, i) {
n = n.nThen(function (w) {
setTimeout(w(function () {
el = data[k];
if (!el.href) {
// Already migrated
return void progress(7, Math.round(100*i/padsLength));
}
if (el.href.indexOf('#') === -1) {
// Encrypted href: already migrated
return void progress(7, Math.round(100*i/padsLength));
}
parsed = Hash.parsePadUrl(el.href);
if (parsed.hashData.type !== "pad") {
// No read-only mode for files
return void progress(7, Math.round(100*i/padsLength));
}
if (parsed.hashData.mode === "view") {
// This is a read-only pad in our drive
el.roHref = el.href;
delete el.href;
console.log('Move href to roHref in filesData ', el.roHref);
} else {
var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
var hash = Hash.getViewHashFromKeys(secret);
if (hash) {
// Version 0 won't have a view hash available
el.roHref = '/' + parsed.type + '/#' + hash;
console.log('Adding missing roHref in filesData ', el.href);
}
}
progress(6, Math.round(100*i/padsLength));
}));
});
});
n.nThen(waitFor(function () {
Feedback.send('Migrate-7', true);
userObject.version = version = 7;
}));
};
if (version < 7) {
addRoHref();
}
}).nThen(function () {
// Migration 8: remove duplicate entries in proxy.FS_hashes (list of migrated anon drives)
var fixDuplicate = function () {
userObject.FS_hashes = Util.deduplicateString(userObject.FS_hashes || []);
};
if (version < 8) {
fixDuplicate();
Feedback.send('Migrate-8', true);
userObject.version = version = 8;
}
}).nThen(function () {
// Migration 9: send our mailbox channel to existing friends
var migrateFriends = function () {
var network = store.network;
var channels = {};
var ctx = {
store: store
};
var myData = Messaging.createData(userObject);
var close = function (chan) {
var channel = channels[chan];
if (!channel) { return; }
try {
channel.wc.leave();
} catch (e) {}
delete channels[chan];
};
var onDirectMessage = function (msg, sender) {
if (sender !== network.historyKeeper) { return; }
var parsed = JSON.parse(msg);
// Metadata msg? we don't care
if ((parsed.validateKey || parsed.owners) && parsed.channel) { return; }
// End of history message, "onReady"
if (parsed.channel && channels[parsed.channel]) {
// History cleared while we were offline
// ==> we asked for an invalid last known hash
if (parsed.error && parsed.error === "EINVAL") {
var histMsg = ['GET_HISTORY', parsed.channel, {}];
network.sendto(network.historyKeeper, JSON.stringify(histMsg))
.then(function () {}, function () {});
return;
}
// End of history
if (parsed.state && parsed.state === 1) {
// Channel is ready and we didn't receive their mailbox channel: send our channel
myData.channel = parsed.channel;
var updateMsg = ['UPDATE', myData.curvePublic, +new Date(), myData];
var cryptMsg = channels[parsed.channel].encrypt(JSON.stringify(updateMsg));
channels[parsed.channel].wc.bcast(cryptMsg).then(function () {}, function (err) {
console.error("Can't migrate this friend", channels[parsed.channel].friend, err);
});
close(parsed.channel);
return;
}
} else if (parsed.channel) {
return;
}
// History message: we only care about "UPDATE" messages
var chan = parsed[3];
if (!chan || !channels[chan]) { return; }
var channel = channels[chan];
var msgIn = channel.decrypt(parsed[4]);
var parsedMsg = JSON.parse(msgIn);
if (parsedMsg[0] === 'UPDATE') {
if (parsedMsg[1] === myData.curvePublic) { return; }
var data = parsedMsg[3];
// If it doesn't contain the mailbox channel, ignore the message
if (!data.notifications) { return; }
// Otherwise we know their channel, we can send them our own
channel.friend.notifications = data.notifications;
myData.channel = chan;
Mailbox.sendTo(ctx, 'UPDATE_DATA', myData, {
channel: data.notifications,
curvePublic: data.curvePublic
}, function (obj) {
if (obj && obj.error) { return void console.error(obj); }
console.log('friend migrated', channel.friend);
});
close(chan);
}
};
network.on('message', function(msg, sender) {
try {
onDirectMessage(msg, sender);
} catch (e) {
console.error(e);
}
});
var friends = userObject.friends || {};
Object.keys(friends).forEach(function (curve) {
if (curve.length !== 44) { return; }
var friend = friends[curve];
// Check if it is already a "new" friend
if (friend.notifications) { return; }
/** Old friend:
* 1. Open the messenger channel
* 2. Check if they sent us their mailbox channel
* 3.a. Yes ==> sent them a mail containing our mailbox channel
* 3.b. No ==> post our mailbox data to the messenger channel
*/
network.join(friend.channel).then(function (wc) {
var keys = Crypto.Curve.deriveKeys(friend.curvePublic, userObject.curvePrivate);
var encryptor = Crypto.Curve.createEncryptor(keys);
channels[friend.channel] = {
wc: wc,
friend: friend,
decrypt: encryptor.decrypt,
encrypt: encryptor.encrypt
};
var cfg = {
lastKnownHash: friend.lastKnownHash
};
var msg = ['GET_HISTORY', friend.channel, cfg];
network.sendto(network.historyKeeper, JSON.stringify(msg))
.then(function () {}, function (err) {
console.error("Can't migrate this friend", friend, err);
});
}, function (err) {
console.error("Can't migrate this friend", friend, err);
});
});
};
if (version < 9) {
migrateFriends();
Feedback.send('Migrate-9', true);
userObject.version = version = 9;
}
}).nThen(function (waitFor) {
// Migration 10: deprecate todo
var fixTodo = function () {
var h = store.proxy.todo;
if (!h) { return; }
var next = waitFor(function () {
Feedback.send('Migrate-10', true);
userObject.version = version = 10;
});
var old;
var opts = {
network: store.network,
initialState: '{}',
metadata: {
owners: store.proxy.edPublic ? [store.proxy.edPublic] : []
}
};
nThen(function (w) {
Crypt.get(h, w(function (err, val) {
if (err || !val) {
w.abort();
next();
return;
}
try {
old = JSON.parse(val);
} catch (e) {} // We will abort in the next step in case of error
}), opts);
}).nThen(function (w) {
if (!old || typeof(old) !== "object") {
w.abort();
next();
return;
}
var k = {
content: {
data: {
"1": {
id: "1",
color: 'color6',
item: [],
title: Messages.kanban_todo
},
"2": {
id: "2",
color: 'color3',
item: [],
title: Messages.kanban_working
},
"3": {
id: "3",
color: 'color5',
item: [],
title: Messages.kanban_done
},
},
items: {},
list: [1, 2, 3]
},
metadata: {
title: Messages.type.todo,
defaultTitle: Messages.type.todo,
type: "kanban"
}
};
var i = 4;
var items = false;
(old.order || []).forEach(function (key) {
var data = old.data[key];
if (!data || !data.task) { return; }
items = true;
var column = data.state ? '3' : '1';
k.content.data[column].item.push(i);
k.content.items[i] = {
id: i,
title: data.task
};
i++;
});
if (!items) {
w.abort();
next();
return;
}
var newH = Hash.createRandomHash('kanban');
var secret = Hash.getSecrets('kanban', newH);
var oldSecret = Hash.getSecrets('todo', h);
Crypt.put(newH, JSON.stringify(k), w(function (err) {
if (err) {
w.abort();
next();
return;
}
if (store.rpc) {
store.rpc.pin([secret.channel], function () {
// Try to pin and ignore errors...
// Todo won't be available anyway so keep your unpinned kanban
});
store.rpc.unpin([oldSecret.channel], function () {
// Try to unpin and ignore errors...
});
}
var href = Hash.hashToHref(newH, 'kanban');
store.manager.addPad(['root'], {
title: Messages.type.todo,
owners: opts.metadata.owners,
channel: secret.channel,
href: href,
roHref: Hash.hashToHref(Hash.getViewHashFromKeys(secret), 'kanban'),
atime: +new Date(),
ctime: +new Date()
}, w(function (e) {
if (e) { return void console.error(e); }
delete store.proxy.todo;
var myData = Messaging.createData(userObject);
var ctx = { store: store };
Mailbox.sendTo(ctx, 'MOVE_TODO', {
user: myData,
href: href,
}, {
channel: myData.notifications,
curvePublic: myData.curvePublic
}, function (obj) {
if (obj && obj.error) { return void console.error(obj); }
});
}));
}), opts);
}).nThen(function () {
next();
});
};
if (version < 10) {
fixTodo();
}
}).nThen(function (waitFor) {
if (version >= 11) { return; }
// Migration 11: alert users of safe links as the new default
var done = function () {
Feedback.send('Migrate-11', true);
userObject.version = version = 11;
};
/* userObject.settings.security.unsafeLinks
undefined => the user has never touched it
false => the user has explicitly enabled "safe links"
true => the user has explicitly disabled "safe links"
*/
var unsafeLinks = Util.find(userObject, [ 'settings', 'security', 'unsafeLinks' ]);
if (unsafeLinks !== undefined) { return void done(); }
var ctx = {
store: store,
};
var myData = Messaging.createData(userObject);
if (!myData.curvePublic) { return void done(); }
Mailbox.sendTo(ctx, 'SAFE_LINKS_DEFAULT', {
user: myData,
}, {
channel: myData.notifications,
curvePublic: myData.curvePublic
}, waitFor(function (obj) {
if (obj && obj.error) { return void console.error(obj); }
done();
}));
/*}).nThen(function (waitFor) {
// Test progress bar in the loading screen
var i = 0;
var w = waitFor();
var it = setInterval(function () {
i += 5;
if (i >= 100) { w(); clearInterval(it); i = 100;}
progress(0, i);
}, 500);
progress(0, 0);*/
}).nThen(function () {
Realtime.whenRealtimeSyncs(store.realtime, Util.mkAsync(Util.bake(cb)));
});
};
migrate.setCustomize = setCustomize;
return migrate;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
undefined,
require('../../common/common-feedback'),
require('../../common/common-hash'),
require('../../common/common-util'),
require('../../common/common-messaging'),
require('../../common/cryptget'),
require('../modules/mailbox'),
undefined,
require('../../common/common-realtime'),
require('nthen'),
require('chainpad-crypto')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/customize/application_config.js',
'/common/common-feedback.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-messaging.js',
'/common/cryptget.js',
'/common/outer/mailbox.js',
'/customize/messages.js',
'/common/common-realtime.js',
'/components/nthen/index.js',
'/components/chainpad-crypto/crypto.js',
], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,908 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Util) => {
var Rec = {};
const window = globalThis;
var debug = function () {};
// Get week number with any "WKST" (firts day of the week)
// Week 1 is the first week of the year containing at least 4 days in this year
// It depends on which day is considered the first day of the week (default Monday)
// In our case, wkst is a number matching the JS rule: 0 == Sunday
var getWeekNo = Rec.getWeekNo = function (date, wkst) {
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var newYear = new Date(date.getFullYear(),0,1);
var day = newYear.getDay() - wkst; //the day of week the year begins on
day = (day >= 0 ? day : day + 7);
var daynum = Math.floor((date.getTime() - newYear.getTime())/86400000) + 1;
var weeknum;
// Week 1 / week 53
if (day < 4) {
weeknum = Math.floor((daynum+day-1)/7) + 1;
if (weeknum > 52) {
var nYear = new Date(date.getFullYear() + 1,0,1);
var nday = nYear.getDay() - wkst;
nday = nday >= 0 ? nday : nday + 7;
weeknum = nday < 4 ? 1 : 53;
}
}
else {
weeknum = Math.floor((daynum+day-1)/7);
}
return weeknum;
};
var getYearDay = function (date) {
var start = new Date(date.getFullYear(), 0, 0);
var diff = (date - start) +
((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000);
var oneDay = 1000 * 60 * 60 * 24;
return Math.floor(diff / oneDay);
};
var setYearDay = function (date, day) {
if (typeof(day) !== "number" || Math.abs(day) < 1 || Math.abs(day) > 366) { return; }
if (day < 0) {
var max = getYearDay(new Date(date.getFullYear(), 11, 31));
day = max + day + 1;
}
date.setMonth(0);
date.setDate(day);
return true;
};
var getEndData = function (s, e) {
if (s > e) { return void console.error("Wrong data"); }
var days;
if (e.getFullYear() === s.getFullYear()) {
days = getYearDay(e) - getYearDay(s);
} else { // eYear < sYear
var tmp = new Date(s.getFullYear(), 11, 31);
var d1 = getYearDay(tmp) - getYearDay(s); // Number of days before December 31st
var de = getYearDay(e);
days = d1 + de;
while ((tmp.getFullYear()+1) < e.getFullYear()) {
tmp.setFullYear(tmp.getFullYear()+1);
days += getYearDay(tmp);
}
}
return {
h: e.getHours(),
m: e.getMinutes(),
days: days
};
};
var setEndData = function (s, e, data) {
e.setTime(+s);
if (!data) { return; }
e.setHours(data.h);
e.setMinutes(data.m);
e.setSeconds(0);
e.setDate(s.getDate() + data.days);
};
var DAYORDER = Rec.DAYORDER = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
var getDayData = function (str) {
var pos = Number(str.slice(0,-2));
var day = DAYORDER.indexOf(str.slice(-2));
return pos ? [pos, day] : day;
};
var goToFirstWeekDay = function (date, wkst) {
var d = date.getDay();
wkst = typeof(wkst) === "number" ? wkst : 1;
if (d >= wkst) {
date.setDate(date.getDate() - (d-wkst));
} else {
date.setDate(date.getDate() - (7+d-wkst));
}
};
var getDateStr = function (date) {
return date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate();
};
var FREQ = {};
FREQ['daily'] = function (s, i) {
s.setDate(s.getDate()+i);
};
FREQ['weekly'] = function (s,i) {
s.setDate(s.getDate()+(i*7));
};
FREQ['monthly'] = function (s,i) {
s.setMonth(s.getMonth()+i);
};
FREQ['yearly'] = function (s,i) {
s.setFullYear(s.getFullYear()+i);
};
// EXPAND is used to create iterations added from a BYxxx rule
// dateA is the start date and b is the number or id of the BYxxx rule item
var EXPAND = {};
EXPAND['month'] = function (dateS, origin, b) {
var oS = new Date(origin.start);
var a = dateS.getMonth() + 1;
var toAdd = (b-a+12)%12;
var m = dateS.getMonth() + toAdd;
dateS.setMonth(m);
dateS.setDate(oS.getDate());
if (dateS.getMonth() !== m) { return; } // Day 31 may move us to the next month
return true;
};
EXPAND['weekno'] = function (dateS, origin, week, rule) {
var wkst = rule && rule.wkst;
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var oS = new Date(origin.start);
var lastD = new Date(dateS.getFullYear(), 11, 31); // December 31st
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
var doubleOne = lastW === 1;
if (lastW === 1) { lastW = 52; }
var a = getWeekNo(dateS, wkst);
if (!week || week > lastW) { return false; } // Week 53 may not exist this year
if (week < 0) { week = lastW + week + 1; } // Turn negative week number into positive
var toAdd = week - a;
var weekS = new Date(+dateS);
// Go to the selected week
weekS.setDate(weekS.getDate() + (toAdd * 7));
goToFirstWeekDay(weekS, wkst);
// Then make sure we are in the correct start day
var all = 'aaaaaaa'.split('').map(function (o, i) {
var date = new Date(+weekS);
date.setDate(date.getDate() + i);
if (date.getFullYear() !== dateS.getFullYear()) { return; }
return date.toLocaleDateString() !== oS.toLocaleDateString() && date;
}).filter(Boolean);
// If we're looking for week 1 and the last week is a week 1, add the days
if (week === 1 && doubleOne) {
goToFirstWeekDay(lastD, wkst);
'aaaaaaa'.split('').some(function (o, i) {
var date = new Date(+lastD);
date.setDate(date.getDate() + i);
if (date.toLocaleDateString() === oS.toLocaleDateString()) { return; }
if (date.getFullYear() > dateS.getFullYear()) { return true; }
all.push(date);
});
}
return all.length ? all : undefined;
};
EXPAND['yearday'] = function (dateS, origin, b) {
var y = dateS.getFullYear();
var state = setYearDay(dateS, b);
if (!state) { return; } // Invalid day "b"
if (dateS.getFullYear() !== y) { return; } // Day 366 make move us to the next year
return true;
};
EXPAND['monthday'] = function (dateS, origin, b, rule) {
if (typeof(b) !== "number" || Math.abs(b) < 1 || Math.abs(b) > 31) { return false; }
var setMonthDay = function (date, day) {
var m = date.getMonth();
if (day < 0) {
var tmp = new Date(date.getFullYear(), date.getMonth()+1, 0); // Last day
day = tmp.getDate() + day + 1;
}
date.setDate(day);
return date.getMonth() === m; // Don't push if day 31 moved us to the next month
};
// Monthly events
if (rule.freq === 'monthly') {
return setMonthDay(dateS, b);
}
var all = 'aaaaaaaaaaaa'.split('').map(function (o, i) {
var date = new Date(dateS.getFullYear(), i, 1);
var ok = setMonthDay(date, b);
return ok ? date : undefined;
}).filter(Boolean);
return all.length ? all : undefined;
};
EXPAND['day'] = function (dateS, origin, b, rule) {
// Here "b" can be a single day ("TU") or a position and a day ("1MO")
var day = getDayData(b);
var pos;
if (Array.isArray(day)) {
pos = day[0];
day = day[1];
}
var all = [];
if (![0,1,2,3,4,5,6].includes(day)) { return false; }
var filterPos = function (m) {
if (!pos) { return; }
var _all = [];
'aaaaaaaaaaaa'.split('').some(function (a, i) {
if (typeof(m) !== "undefined" && i !== m) { return; }
var _pos;
var tmp = all.filter(function (d) {
return d.getMonth() === i;
});
if (pos < 0) {
_pos = tmp.length + pos;
} else {
_pos = pos - 1; // An array starts at 0 but the recurrence rule starts at 1
}
_all.push(tmp[_pos]);
return typeof(m) !== "undefined" && i === m;
});
all = _all.filter(Boolean); // The "5th" {day} won't always exist
};
var tmp;
if (rule.freq === 'yearly') {
tmp = new Date(+dateS);
var y = dateS.getFullYear();
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
while (tmp.getFullYear() === y) {
all.push(new Date(+tmp));
tmp.setDate(tmp.getDate()+7);
}
filterPos();
return all;
}
if (rule.freq === 'monthly') {
tmp = new Date(+dateS);
var m = dateS.getMonth();
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
while (tmp.getMonth() === m) {
all.push(new Date(+tmp));
tmp.setDate(tmp.getDate()+7);
}
filterPos(m);
return all;
}
if (rule.freq === 'weekly') {
while (dateS.getDay() !== day) { dateS.setDate(dateS.getDate()+1); }
}
return true;
};
var LIMIT = {};
LIMIT['month'] = function (events, rule) {
return events.filter(function (s) {
return rule.includes(s.getMonth()+1);
});
};
LIMIT['weekno'] = function (events, weeks, rules) {
return events.filter(function (s) {
var wkst = rules && rules.wkst;
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var lastD = new Date(s.getFullYear(), 11, 31); // December 31st
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
if (lastW === 1) { lastW = 52; }
var w = getWeekNo(s, wkst);
return weeks.some(function (week) {
if (week > 0) { return week === w; }
return w === (lastW + week + 1);
});
});
};
LIMIT['yearday'] = function (events, days) {
return events.filter(function (s) {
var d = getYearDay(s);
var max = getYearDay(new Date(s.getFullYear(), 11, 31));
return days.some(function (day) {
if (day > 0) { return day === d; }
return d === (max + day + 1);
});
});
};
LIMIT['monthday'] = function (events, rule) {
return events.filter(function (s) {
var r = Util.clone(rule);
// Transform the negative monthdays into positive for this specific month
r = r.map(function (b) {
if (b < 0) {
var tmp = new Date(s.getFullYear(), s.getMonth()+1, 0); // Last day
b = tmp.getDate() + b + 1;
}
return b;
});
return r.includes(s.getDate());
});
};
LIMIT['day'] = function (events, days, rules) {
return events.filter(function (s) {
var dayStr = s.toLocaleDateString();
// Check how to handle position in BYDAY rules (last day of the month or the year?)
var type = 'yearly';
if (rules.freq === 'monthly' ||
(rules.freq === 'yearly' && rules.by && rules.by.month)) {
type = 'monthly';
}
// Check if this event matches one of the allowed days
return days.some(function (r) {
// rule elements are strings with pos and day
var day = getDayData(r);
var pos;
if (Array.isArray(day)) {
pos = day[0];
day = day[1];
}
if (!pos) {
return s.getDay() === day;
}
// If we have a position, we can use EXPAND.day to get the nth {day} of the
// year/month and compare if it matches with
var d = new Date(s.getFullYear(), s.getMonth(), 1);
if (type === 'yearly') { d.setMonth(0); }
var res = EXPAND["day"](d, {}, r, {freq: type});
return res.some(function (date) {
return date.toLocaleDateString() === dayStr;
});
});
});
};
LIMIT['setpos'] = function (events, rule) {
var init = events.slice();
var rules = Util.deduplicateString(rule.slice().map(function (n) {
if (n > 0) { return (n-1); }
if (n === 0) { return; }
return init.length + n;
}));
return events.filter(function (ev) {
var idx = init.indexOf(ev);
return rules.includes(idx);
});
};
var BYORDER = ['month','weekno','yearday','monthday','day'];
var BYDAYORDER = ['month','monthday','day'];
Rec.getMonthId = function (d) {
return d.getFullYear() + '-' + d.getMonth();
};
var cache = window.CP_calendar_cache = {};
var recurringAcross = {};
Rec.resetCache = function () {
cache = window.CP_calendar_cache = {};
recurringAcross = {};
};
var iterate = function (rule, _origin, s) {
// "origin" is the original event to detect the start of BYxxx
var origin = Util.clone(_origin);
var oS = new Date(origin.start);
var id = origin.id.split('|')[0]; // Use same cache when updating recurrence rule
// "uid" is used for the cache
var uid = s.toLocaleDateString();
cache[id] = cache[id] || {};
var inter = rule.interval || 1;
var freq = rule.freq;
var all = [];
var limit = function (byrule, n) {
all = LIMIT[byrule](all, n, rule);
};
var expand = function (byrule) {
return function (n) {
// Set the start date at the beginning of the current FREQ
var _s = new Date(+s);
if (rule.freq === 'yearly') {
// January 1st
_s.setMonth(0);
_s.setDate(1);
} else if (rule.freq === 'monthly') {
_s.setDate(1);
} else if (rule.freq === 'weekly') {
goToFirstWeekDay(_s, rule.wkst);
} else if (rule.freq === 'daily') {
// We don't have < byday rules so we can't expand daily rules
}
var add = EXPAND[byrule](_s, origin, n, rule);
if (!add) { return; }
if (Array.isArray(add)) {
add = add.filter(function (dateS) {
return dateS.toLocaleDateString() !== oS.toLocaleDateString();
});
Array.prototype.push.apply(all, add);
} else {
if (_s.toLocaleDateString() === oS.toLocaleDateString()) { return; }
all.push(_s);
}
};
};
// Manage interval for the next iteration
var it = Util.once(function () {
FREQ[freq](s, inter);
});
var addDefault = function () {
if (freq === "monthly") {
s.setDate(15);
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
s.setDate(28);
}
it();
var _s = new Date(+s);
if (freq === "monthly" || freq === "yearly") {
_s.setDate(oS.getDate());
if (_s.getDate() !== oS.getDate()) { return; } // If 31st or Feb 29th doesn't exist
if (freq === "yearly" && _s.getMonth() !== oS.getMonth()) { return; }
// FIXME if there is a recUpdate that moves the 31st to the 30th, the event
// will still only be displayed on months with 31 days
}
all.push(_s);
};
if (Array.isArray(cache[id][uid])) {
debug('Get cache', id, uid);
if (freq === "monthly") {
s.setDate(15);
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
s.setDate(28);
}
it();
return cache[id][uid];
}
if (rule.by && freq === 'yearly') {
var order = BYORDER.slice();
var monthLimit = false;
if (rule.by.weekno || rule.by.yearday || rule.by.monthday || rule.by.day) {
order.shift();
monthLimit = true;
}
var first = true;
order.forEach(function (_order) {
var r = rule.by[_order];
if (!r) { return; }
if (first) {
r.forEach(expand(_order));
first = false;
} else if (_order === "day") {
if (rule.by.yearday || rule.by.monthday || rule.by.weekno) {
limit('day', rule.by.day);
} else {
rule.by.day.forEach(expand('day'));
}
} else {
limit(_order, r);
}
});
if (rule.by.month && monthLimit) {
limit('month', rule.by.month);
}
}
if (rule.by && freq === 'monthly') {
// We're going to compute all the entries for the coming month
if (!rule.by.monthday && !rule.by.day) {
addDefault();
} else if (rule.by.monthday) {
rule.by.monthday.forEach(expand('monthday'));
} else if (rule.by.day) {
rule.by.day.forEach(expand('day'));
}
if (rule.by.month) {
limit('month', rule.by.month);
}
if (rule.by.day && rule.by.monthday) {
limit('day', rule.by.day);
}
}
if (rule.by && freq === 'weekly') {
// We're going to compute all the entries for the coming week
if (!rule.by.day) {
addDefault();
} else {
rule.by.day.forEach(expand('day'));
}
if (rule.by.month) {
limit('month', rule.by.month);
}
}
if (rule.by && freq === 'daily') {
addDefault();
BYDAYORDER.forEach(function (_order) {
var r = rule.by[_order];
if (!r) { return; }
limit(_order, r);
});
}
all.sort(function (a, b) {
return a-b;
});
if (rule.by && rule.by.setpos) {
limit('setpos', rule.by.setpos);
}
if (!rule.by || !Object.keys(rule.by).length) {
addDefault();
} else {
it();
}
var done = [];
all = all.filter(function (newS) {
var start = new Date(+newS).toLocaleDateString();
if (done.includes(start)) { return false; }
done.push(start);
return true;
});
debug('Set cache', id, uid);
cache[id][uid] = all;
return all;
};
var getNextRules = function (obj) {
if (!obj.recUpdate) { return []; }
var _allRules = {};
var _obj = obj.recUpdate.from;
Object.keys(_obj || {}).forEach(function (d) {
var u = _obj[d];
if (u.recurrenceRule) { _allRules[d] = u.recurrenceRule; }
});
return Object.keys(_allRules).sort(function (a, b) { return Number(a)-Number(b); })
.map(function (k) {
var r = Util.clone(_allRules[k]);
if (!FREQ[r.freq]) { return; }
if (r.interval && r.interval < 1) { return; }
r._start = Number(k);
return r;
}).filter(Boolean);
};
var fixTimeZone = function (evTimeZone, origin, target) {
var getOffset = function (date, tz) {
// Get an ISO string using Canadian local format
let iso = date.toLocaleString('en-CA', { timeZone:tz, hour12: false }).replace(', ', 'T');
iso += '.' + date.getMilliseconds().toString().padStart(3, '0');
// Get a UTC version of this time
let utcDate = new Date(iso + 'Z');
// Return the difference in timestamps, as minutes (60*1000)
return -(utcDate - date);
};
var myTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
var offset = getOffset(origin, evTimeZone) - getOffset(target, evTimeZone);
var myOffset = getOffset(origin, myTimeZone) - getOffset(target, myTimeZone);
return myOffset - offset;
};
Rec.getRecurring = function (months, events) {
if (window.CP_DEV_MODE) { debug = console.warn; }
var toAdd = [];
months.forEach(function (monthId) {
// from 1st day of the month at 00:00 to last day at 23:59:59:999
var ms = monthId.split('-');
var _startMonth = new Date(ms[0], ms[1]);
var _endMonth = new Date(+_startMonth);
_endMonth.setMonth(_endMonth.getMonth() + 1);
_endMonth.setMilliseconds(-1);
debug('Compute month', _startMonth.toLocaleDateString());
var rec = events || [];
rec.forEach(function (obj) {
var _start = new Date(obj.start);
var _end = new Date(obj.end);
var _origin = obj;
var rule = obj.recurrenceRule;
if (!rule) { return; }
var nextRules = getNextRules(obj);
var nextRule = nextRules.shift();
if (_start >= _endMonth) { return; }
// Check the "until" date of the latest rule we can use and stop now
// if the recurrence ends before the current month
var until = rule.until;
var _nextRules = nextRules.slice();
var _nextRule = nextRule;
while (_nextRule && _nextRule._start && _nextRule._start < _startMonth) {
until = nextRule.until;
_nextRule = _nextRules.shift();
}
if (until < _startMonth) { return; }
var endData = getEndData(_start, _end);
if (rule.interval && rule.interval < 1) { return; }
if (!FREQ[rule.freq]) { return; }
/*
// Rule examples
rule.by = {
//month: [1, 4, 5, 8, 12],
//weekno: [1, 2, 4, 5, 32, 34, 35, 50],
//yearday: [1, 2, 29, 30, -2, -1, 250],
//monthday: [1, 2, 3, -3, -2, -1],
//day: ["MO", "WE", "FR"],
//setpos: [1, 2, -1, -2]
};
rule.wkst = 0;
rule.interval = 2;
rule.freq = 'yearly';
rule.count = 10;
*/
debug('Iterate over', obj.title, obj);
debug('Use rule', rule);
var count = rule.count;
var c = 1;
var next = function (start) {
var evS = new Date(+start);
if (count && c >= count) { return; }
debug('Start iteration', evS.toLocaleDateString());
var _toAdd = iterate(rule, obj, evS);
debug('Iteration results', JSON.stringify(_toAdd.map(function (o) { return new Date(o).toLocaleDateString();})));
// Make sure to continue if the current year doesn't provide any result
if (!_toAdd.length) {
if (evS.getFullYear() < _startMonth.getFullYear() ||
evS < _endMonth) {
return void next(evS);
}
return;
}
var stop = false;
var newrule = false;
_toAdd.some(function (_newS) {
// Make event with correct start and end time
var _ev = Util.clone(obj);
_ev.id = _origin.id + '|' + (+_newS);
var _evS = new Date(+_newS);
var _evE = new Date(+_newS);
setEndData(_evS, _evE, endData);
_ev.start = +_evS;
_ev.end = +_evE;
_ev._count = c;
if (_ev.isAllDay && _ev.startDay) { _ev.startDay = getDateStr(_evS); }
if (_ev.isAllDay && _ev.endDay) { _ev.endDay = getDateStr(_evE); }
if (nextRule && _ev.start === nextRule._start) {
newrule = true;
}
var useNewRule = function () {
if (!newrule) { return; }
debug('Use new rule', nextRule);
_ev._count = c;
count = nextRule.count;
c = 1;
evS = +_evS;
obj = _ev;
rule = nextRule;
nextRule = nextRules.shift();
};
if (c >= count) { // Limit reached
debug(_evS.toLocaleDateString(), 'count');
stop = true;
return true;
}
if (_evS >= _endMonth) { // Won't affect us anymore
debug(_evS.toLocaleDateString(), 'endMonth');
stop = true;
return true;
}
if (rule.until && _evS > rule.until) {
debug(_evS.toLocaleDateString(), 'until');
stop = true;
return true;
}
if (_evS < _start) { // "Expand" rules may create events before the _start
debug(_evS.toLocaleDateString(), 'start');
return;
}
c++;
if (_evE < _startMonth) { // Ended before the current month
// Nothing to display but continue the recurrence
debug(_evS.toLocaleDateString(), 'startMonth');
if (newrule) { useNewRule(); }
return;
}
// If a recurring event start and end in different months, make sure
// it is only added once
if ((_evS < _endMonth && _evE >= _endMonth) ||
(_evS < _startMonth && _evE >= _startMonth)) {
if (recurringAcross[_ev.id] && recurringAcross[_ev.id].includes(_ev.start)) {
return;
} else {
recurringAcross[_ev.id] = recurringAcross[_ev.id] || [];
recurringAcross[_ev.id].push(_ev.start);
}
}
// Add this event
if (_origin.timeZone && !_ev.isAllDay) {
var offset = fixTimeZone(_origin.timeZone, _start, _evS);
_ev.start += offset;
_ev.end += offset;
}
toAdd.push(_ev);
if (newrule) {
useNewRule();
return true;
}
});
if (!stop) { next(evS); }
};
next(_start);
debug('Added this month (all events)', toAdd.map(function (ev) {
return new Date(ev.start).toLocaleDateString();
}));
});
});
return toAdd;
};
Rec.getAllOccurrences = function (ev) {
if (!ev.recurrenceRule) { return [ev.start]; }
var r = ev.recurrenceRule;
// In case of infinite recursion, we can't get all
if (!r.until && !r.count) { return false; }
var all = [ev.start];
var d = new Date(ev.start);
d.setDate(15); // Make sure we won't skip a month if the event starts on day > 28
var toAdd = [];
var i = 0;
var check = function () {
return r.count ? (all.length < r.count) : (+d <= r.until);
};
while ((toAdd = Rec.getRecurring([Rec.getMonthId(d)], [ev])) && check() && i < (r.count*12)) {
Array.prototype.push.apply(all, toAdd.map(function (_ev) { return _ev.start; }));
d.setMonth(d.getMonth() + 1);
i++;
}
return all;
};
Rec.diffDate = function (oldTime, newTime) {
var n = new Date(newTime);
var o = new Date(oldTime);
// Diff Days
var d = 0;
var mult = n < o ? -1 : 1;
while (n.toLocaleDateString() !== o.toLocaleDateString() || mult >= 10000) {
n.setDate(n.getDate() - mult);
d++;
}
d = mult * d;
// Diff hours
n = new Date(newTime);
var h = n.getHours() - o.getHours();
// Diff minutes
var m = n.getMinutes() - o.getMinutes();
return {
d: d,
h: h,
m: m
};
};
var sortUpdate = function (obj) {
return Object.keys(obj).sort(function (d1, d2) {
return Number(d1) - Number(d2);
});
};
Rec.applyUpdates = function (events) {
events.forEach(function (ev) {
ev.raw = {
start: ev.start,
end: ev.end,
};
if (!ev.recUpdate) { return; }
var from = ev.recUpdate.from || {};
var one = ev.recUpdate.one || {};
var s = ev.start;
// Add "until" date to our recurrenceRule if it has been modified in future occurences
var nextRules = getNextRules(ev).filter(function (r) {
return r._start > s;
});
var nextRule = nextRules.shift();
var applyDiff = function (obj, k) {
var diff = obj[k]; // Diff is always compared to origin start/end
var d = new Date(ev.raw[k]);
d.setDate(d.getDate() + diff.d);
d.setHours(d.getHours() + diff.h);
d.setMinutes(d.getMinutes() + diff.m);
ev[k] = +d;
};
sortUpdate(from).forEach(function (d) {
if (s < Number(d)) { return; }
Object.keys(from[d]).forEach(function (k) {
if (k === 'start' || k === 'end') { return void applyDiff(from[d], k); }
if (k === "recurrenceRule" && !from[d][k]) { return; }
ev[k] = from[d][k];
});
});
Object.keys(one[s] || {}).forEach(function (k) {
if (k === 'start' || k === 'end') { return void applyDiff(one[s], k); }
if (k === "recurrenceRule" && !one[s][k]) { return; }
ev[k] = one[s][k];
});
if (ev.deleted) {
Object.keys(ev).forEach(function (k) {
delete ev[k];
});
}
if (nextRule && ev.recurrenceRule) {
ev.recurrenceRule._next = nextRule._start - 1;
}
if (ev.reminders) {
ev.raw.reminders = ev.reminders;
}
});
return events;
};
return Rec;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory('../../common/common-util');
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define(['/common/common-util'], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,958 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(function () {
var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback) {
var Roster = {};
// this constant is somewhat arbitrary.
// Adjust it as you like to suit performance expectations
var CHECKPOINT_INTERVAL = 25;
var TIMEOUT_INTERVAL = 30000; // TIMEOUT after 30s
/*
roster: {
state: {
members: {
user0CurveKey: {
notifications: "", // required
displayName: "", // required
role: "OWNER|ADMIN|MEMBER|VIEWER", // VIEWER if not specified
profile: "",
title: ""
},
user1CurveKey: {
...
}
},
metadata: {
// guaranteed to be strings, but may be empty
topic: '',
name: '',
avatar: '',
// anything else you use may not be defined
}
}
}
*/
var isMap = function (obj) {
return Boolean(obj && typeof(obj) === 'object' && !Array.isArray(obj));
};
var getMessageId = function (msgString) {
return msgString.slice(0, 64);
};
var canCheckpoint = function (author, members) {
// if you're here then you've received a checkpoint message
// that you don't necessarily trust.
// find the author's role from your knoweldge of the state
var role = Util.find(members, [author, 'role']);
// and check if it is 'OWNER' or 'ADMIN'
return ['OWNER', 'ADMIN'].indexOf(role) !== -1;
};
var isValidRole = function (role) {
return ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1;
};
var isSelfDowngrade = function (author, curve, role, state) {
// Make sure you want to describe yourself
var selfDescribe = author === curve && state[curve];
if (!selfDescribe) { return false; }
// ADMIN and OWNER can always update roles
// we only need to allow MEMBER to downgrade themselves to VIEWER
var authorRole = Util.find(state, [author, 'role']);
if (authorRole === "MEMBER") { return role === 'VIEWER'; }
};
var canAddRole = function (author, role, members) {
var authorRole = Util.find(members, [author, 'role']);
if (!authorRole) { return false; }
// nobody can add an invalid role
if (!isValidRole(role)) { return false; }
// owners can add any valid role they want
if (authorRole === 'OWNER') { return true; }
// admins can add other admins or members or viewers
if (authorRole === "ADMIN") { return ['ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1; }
// (MEMBER, other) can't add anyone of any role
return false;
};
var isValidId = function (id) {
return typeof(id) === 'string' && id.length === 44;
};
var canDescribeTarget = function (author, curve, state) {
// you must be in the group to describe anyone
if (!state[curve]) { return false; }
// anyone can describe themself
if (author === curve && state[curve]) { return true; }
var authorRole = Util.find(state, [author, 'role']);
var targetRole = Util.find(state, [curve, 'role']);
// something is really wrong if there's no authorRole
if (!authorRole) { return false; }
// owners can do whatever they want
if (authorRole === 'OWNER') { return true; }
// admins can describe anyone escept owners
if (authorRole === 'ADMIN' && targetRole !== 'OWNER') { return true; }
// members can't describe others
return false;
};
var canRemoveRole = function (author, role, members) {
var authorRole = Util.find(members, [author, 'role']);
if (!authorRole) { return false; }
// owners can remove anyone they want
if (authorRole === 'OWNER') { return true; }
// admins can remove other admins or members
if (authorRole === "ADMIN") { return ["ADMIN", "MEMBER", "VIEWER"].indexOf(role) !== -1; }
// MEMBERS and non-members cannot remove anyone of any role
return false;
};
var canUpdateMetadata = function (author, members) {
var authorRole = Util.find(members, [author, 'role']);
return Boolean(authorRole && ['OWNER', 'ADMIN'].indexOf(authorRole) !== -1);
};
var shouldCheckpoint = function (me, ref) {
// if you can't send valid checkpoints, don't try
if (!canCheckpoint(me, ref.state.members)) { return false; }
// avoid sending checkpoints too often
// it's a balance between network constraints
// and the size of the roster's log
var since = ref.internal.sinceLastCheckpoint;
if (!since || typeof(since) !== 'number' || since < CHECKPOINT_INTERVAL) {
return false;
}
// if you can't think of any other reason not to...
return true;
};
var commands = Roster.commands = {};
/* Commands are functions with the signature
(args_any, base46_author_string, roster_map, optional_base64_message_id) => boolean
they:
* throw if any of their arguments are invalid
* return true if their application to previous state results in a change
* mutate the local account of the current state
changes to the state can be simulated locally before being sent.
if the simulation throws or returns false, don't send.
*/
// the author is trying to add someone to the roster
// owners can add any role
commands.ADD = function (args, author, roster) {
if (!isMap(args)) { throw new Error("INVALID ARGS"); }
if (!roster.internal.initialized) { throw new Error("UNITIALIZED"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT_ADD_TO_UNITIALIZED_ROSTER");
}
var members = roster.state.members;
// iterate over everything and make sure it is valid, throw if not
Object.keys(args).forEach(function (curve) {
// FIXME only allow valid curve keys, anything else is pollution
if (!isValidId(curve)) {
console.log(curve, curve.length);
throw new Error("INVALID_CURVE_KEY");
}
// reject commands where the members are not proper objects
if (!isMap(args[curve])) { throw new Error("INVALID_CONTENT"); }
if (members[curve]) { throw new Error("ALREADY_PRESENT"); }
var data = args[curve];
// if no role was provided, assume MEMBER
if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; }
if (!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS");
}
if (typeof(data.displayName) !== 'string') { throw new Error("DISPLAYNAME_REQUIRED"); }
if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); }
});
var changed = false;
// then iterate again and apply it
Object.keys(args).forEach(function (curve) {
// this will result in a change
changed = true;
members[curve] = args[curve];
});
return changed;
};
commands.RM = function (args, author, roster) {
if (!Array.isArray(args)) { throw new Error("INVALID_ARGS"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT_RM_FROM_UNITIALIZED_ROSTER");
}
var members = roster.state.members;
// validate first...
args.forEach(function (curve) {
if (!isValidId(curve)) { throw new Error("INVALID_CURVE_KEY"); }
// even members can remove themselves
if (curve === author) { return; }
// but if it concerns anyone else, validate that the author has sufficient permissions
var role = members[curve].role;
if (!canRemoveRole(author, role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
});
var changed = false;
args.forEach(function (curve) {
// don't try to remove something that isn't there
if (!members[curve]) { return; }
changed = true;
delete members[curve];
});
return changed;
};
commands.DESCRIBE = function (args, author, roster) {
if (!args || typeof(args) !== 'object' || Array.isArray(args)) {
throw new Error("INVALID_ARGUMENTS");
}
if (typeof(roster.state.members) === 'undefined') {
throw new Error("NOT_READY");
}
var members = roster.state.members;
// iterate over all the data and make sure it is valid, throw otherwise
Object.keys(args).forEach(function (curve) {
if (!isValidId(curve)) { throw new Error("INVALID_ID"); }
if (!members[curve]) { throw new Error("NOT_PRESENT"); }
if (!canDescribeTarget(author, curve, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
var data = args[curve];
if (!isMap(data)) { throw new Error("INVALID_ARGUMENTS"); }
var current = Util.clone(members[curve]);
if (typeof(data.role) === 'string') { // they're trying to change the role...
// throw if they're trying to upgrade to something greater
if (!isSelfDowngrade(author, curve, data.role, members) &&
!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS");
}
}
// DESCRIBE commands must initialize a displayName if it isn't already present
if (typeof(current.displayName) !== 'string' && typeof(data.displayName) !== 'string') {
throw new Error('DISPLAYNAME_REQUIRED');
}
if (['undefined', 'string'].indexOf(typeof(data.displayName)) === -1) {
throw new Error("INVALID_DISPLAYNAME");
}
// DESCRIBE commands must initialize a mailbox channel if it isn't already present
if (typeof(current.notifications) !== 'string' && typeof(data.notifications) !== 'string') {
throw new Error('NOTIFICATIONS_REQUIRED');
}
if (['undefined', 'string'].indexOf(typeof(data.notifications)) === -1) {
throw new Error("INVALID_NOTIFICATIONS");
}
});
var changed = false;
// then do a second pass and apply it if there were changes
Object.keys(args).forEach(function (curve) {
var current = Util.clone(members[curve]);
var data = args[curve];
Object.keys(data).forEach(function (key) {
// when null is passed as new data and it wasn't considered an invalid change
// remove it from the map. This is how you delete things properly
if (typeof(current[key]) !== 'undefined' && data[key] === null) { return void delete current[key]; }
current[key] = data[key];
});
if (Sortify(current) !== Sortify(members[curve])) {
changed = true;
members[curve] = current;
}
});
return changed;
};
commands.CHECKPOINT = function (args, author, roster) {
// args: complete state
// args should be a map
if (!isMap(args)) { throw new Error("INVALID_CHECKPOINT_STATE"); }
if (!roster.internal.initialized) {
//console.log("INITIALIZING");
// either you're connecting from the beginning of the log
// or from a trusted lastKnownHash.
// Either way, initialize the roster state
roster.state = args;
var metadata = roster.state.metadata = roster.state.metadata || {};
metadata.topic = metadata.topic || '';
metadata.name = metadata.name || '';
metadata.avatar = metadata.avatar || '';
roster.internal.initialized = true;
return true;
} else if (Sortify(args) !== Sortify(roster.state)) {
// a checkpoint must reinsert the previous state
throw new Error("CHECKPOINT_DOES_NOT_MATCH_PREVIOUS_STATE");
}
// otherwise, you're iterating over the log from a previous checkpoint
// so you should know everyone's role
// owners and admins can checkpoint. members and non-members cannot
if (!canCheckpoint(author, roster.state.members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
// set the state, and indicate that a change was made
roster.state = args;
return true;
};
var MANDATORY_METADATA_FIELDS = [
'avatar',
'name',
'topic',
];
// only admin/owner can change group metadata
commands.METADATA = function (args, author, roster) {
if (!isMap(args)) { throw new Error("INVALID_ARGS"); }
if (!canUpdateMetadata(author, roster.state.members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
// validate inputs
Object.keys(args).forEach(function (k) {
if (args[k] === null) {
if (MANDATORY_METADATA_FIELDS.indexOf(k) === -1) { return; }
throw new Error('CANNOT_REMOVE_MANDATORY_METADATA');
}
// can't set metadata to anything other than strings
// use empty string to unset a value if you must
if (typeof(args[k]) !== 'string') { throw new Error("INVALID_ARGUMENTS"); }
});
var changed = false;
// {topic, name, avatar} are all strings...
Object.keys(args).forEach(function (k) {
if (typeof(roster.state.metadata[k]) !== 'undefined' && args[k] === null) {
changed = true;
delete roster.state.metadata[k];
}
// ignore things that won't cause changes
if (args[k] === roster.state.metadata[k]) { return; }
changed = true;
roster.state.metadata[k] = args[k];
});
return changed;
};
commands.INVITE = function (args, author, roster) {
// an invitation is created with an ephemeral curve public key
// that key is ultimately given to the user you'd like on your team
// that user can exploit their possession of the public key to remove
// the pending invitation with their actual data.
if (!isMap(args)) { throw new Error('INVALID_ARGS'); }
if (!roster.internal.initialized) { throw new Error("UNINITIALIED"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT+INVITE_TO_UNINITIALIED_ROSTER");
}
var members = roster.state.members;
Object.keys(args).forEach(function (curve) {
if (!isValidId(curve)) {
console.log(curve, curve.length);
throw new Error("INVALID_CURVE_KEY");
}
// reject commandws wehere the members are not proper objects
if (!isMap(args[curve])) { throw new Error("INVALID_CONTENT"); }
if (members[curve]) { throw new Error("ARLEADY_PRESENT"); }
var data = args[curve];
// if no role was provided, assume VIEWER
if (typeof(data.role) !== 'string') { data.role = "VIEWER"; }
// assume that invitations are 'pending' unless stated otherwise
if (typeof(data.pending) === 'undefined') { data.pending = true; }
if (!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS");
}
if (typeof(data.displayName) !== 'string' || !data.displayName) { throw new Error("DISPLAYNAME_REQUIRED"); }
//if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); }
});
/*
{
<ephemeralCurveKey>: {
role: ??? || 'VIEWER',
displayName: '',
pending: true,
}
}
*/
var changed = false;
Object.keys(args).forEach(function (curve) {
changed = true;
members[curve] = args[curve];
});
return changed;
};
commands.ACCEPT = function (args, author, roster) {
if (!roster.internal.initialized) { throw new Error("UNINITIALIED"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT_ADD_TO_UNINITIALIED_ROSTER");
}
// an ACCEPT command replaces a pending invitation's curve key with a new one
// after which the invited member can use their actual curve key to describe themselves
// the author must have been invited already...
var members = roster.state.members;
// so you must already be in the members list
if (!isMap(members[author])) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
// and your membership must indicate that you are 'pending'
if (!members[author].pending) { throw new Error("ALREADY_PRESENT"); }
// args should be a string
if (typeof(args) !== 'string') { throw new Error("INVALID_ARGS"); }
// ...and a valid curve key
if (!isValidId(args)) { throw new Error("INVALID_CURVE_KEY"); }
var curve = args;
// and the curve key must not already be a member
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
// copy the new profile from the old one
var clone = Util.clone(members[author]);
delete clone.remaining;
delete clone.totalUses;
delete clone.inviteChannel;
delete clone.previewChannel;
members[curve] = clone;
var remaining = members[author].remaining || 1;
if (remaining === -1) { return true; } // Infinite uses, keep the link
if (remaining > 1) { // Remove 1 use
members[author].remaining = remaining - 1;
} else { // Disable link
delete members[author];
}
return true;
};
var handleCommand = function (content, author, roster) {
if (!(Array.isArray(content) && typeof(author) === 'string')) {
throw new Error("INVALID ARGUMENTS");
}
var command = content[0];
if (typeof(commands[command]) !== 'function') { throw new Error('INVALID_COMMAND'); }
return commands[command](content[1], author, roster);
};
var simulate = function (content, author, roster) {
return handleCommand(content, author, Util.clone(roster));
};
Roster.create = function (config, _cb) {
if (typeof(_cb) !== 'function') { throw new Error("EXPECTED_CALLBACK"); }
var cb = Util.once(Util.mkAsync(_cb));
if (!config.network) { return void cb("EXPECTED_NETWORK"); }
if (!config.channel || typeof(config.channel) !== 'string' || config.channel.length !== 32) { return void cb("EXPECTED_CHANNEL"); }
if (!config.keys || typeof(config.keys) !== 'object') { return void cb("EXPECTED_CRYPTO_KEYS"); }
if (!config.store) { return void cb("EXPECTED_STORE"); }
var response = Util.response(function (label, info) {
console.error('ROSTER_RESPONSE__' + label, info);
});
var store = config.store;
var keys = config.keys;
var me = keys.myCurvePublic;
var channel = config.channel;
var lastKnownHash = config.lastKnownHash || -1;
// make sure we don't send -1 (ask for full history) when we are trying to create a new team
if (config.newTeam) {
lastKnownHash = undefined;
}
var ref = {
state: {
members: { },
metadata: { },
},
internal: {
initialized: false,
sinceLastCheckpoint: 0,
lastCheckpointHash: lastKnownHash,
},
};
var roster = {};
var events = {
change: Util.mkEvent(),
checkpoint: Util.mkEvent(),
};
roster.on = function (key, handler) {
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
events[key].reg(handler);
return roster;
};
roster.off = function (key, handler) {
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
events[key].unreg(handler);
return roster;
};
roster.once = function (key, handler) {
if (typeof(events[key]) !== 'object') { throw new Error("unsupported event"); }
var f = function () {
handler.apply(null, Array.prototype.slice.call(arguments));
events[key].unreg(f);
};
events[key].reg(f);
return roster;
};
roster.getState = function () {
//if (!isMap(ref.state)) { return; }
return Util.clone(ref.state);
};
roster.getLastCheckpointHash = function () {
return ref.internal.lastCheckpointHash || -1;
};
var clearPendingCheckpoints = function () {
// clear any pending checkpoints you might have...
if (ref.internal.pendingCheckpointId) {
response.clear(ref.internal.pendingCheckpointId);
delete ref.internal.pendingCheckpointId;
}
clearTimeout(ref.internal.checkpointTimeout);
delete ref.internal.checkpointTimeout;
};
roster.stop = function () {
if (ref.internal.cpNetflux && typeof(ref.internal.cpNetflux.stop) === "function") {
ref.internal.cpNetflux.stop();
clearPendingCheckpoints();
} else {
console.log("FAILED TO LEAVE");
}
};
var ready = false;
var onCacheReady = function () {
if (!config.onCacheReady) { return; }
var state = ref.state;
if (!Object.keys(state.members || {}).length) {
// No member, corrupted cache
try {
ref.internal.cpNetflux.resetCache();
} catch (e) { console.error(e); }
return void config.onCacheReady({error: "CORRUPTED"});
}
config.onCacheReady(roster);
};
var onReady = function () {
//console.log("READY");
ready = true;
cb(void 0, roster);
};
// onError (deleted or expired)
// you won't be able to connect
// onMetadataUpdate
// update owners?
// deleted while you are open
// emit an event
var onChannelError = function (info) {
if (Feedback) { Feedback.send('ROSTER_CHANNEL_ERROR='+(info && info.type)); }
if (info && info.type === "EUNKNOWN") {
// chainpad-netflux should recover by itself
return;
}
if (!ready) { return void cb(info); }
console.error("CHANNEL_ERROR", info);
};
var onConnectionChange = function (info) {
if (info.state) { return; }
// Disconnect: don't send event anymore until ready
ready = false;
};
var onConnect = function (/* wc, sendMessage */) {
console.log("ROSTER CONNECTED");
};
var isReady = function () {
return Boolean(ready && me);
};
var onMessage = function (msg, user, vKey, isCp , hash, author) {
// count messages received since the last checkpoint
// even if they fail to parse
ref.internal.sinceLastCheckpoint++;
var parsed = Util.tryParse(msg);
if (!parsed) { return void console.error("could not parse"); }
var changed;
var error;
try {
changed = handleCommand(parsed, author, ref);
} catch (err) {
error = err.message;
}
var id = getMessageId(hash);
if (response.expected(id)) {
if (error) { return void response.handle(id, [error]); }
try {
if (!changed) {
response.handle(id, ['NO_CHANGE']);
console.log(msg);
} else {
response.handle(id, [void 0, roster.getState()]);
}
} catch (err) {
console.log('CAUGHT', err);
}
}
// if a checkpoint was successfully applied, emit an event
if (parsed[0] === 'CHECKPOINT' && changed) {
if (isReady()) { events.checkpoint.fire(hash); }
// reset the counter for messages since the last checkpoint
ref.internal.sinceLastCheckpoint = 0;
ref.internal.lastCheckpointHash = hash;
} else if (changed) {
if (isReady()) { events.change.fire(); }
}
// CHECKPOINT logic...
clearPendingCheckpoints();
if (!isReady() || !shouldCheckpoint(me, ref)) { return; }
// a random number of seconds between 5 and 25
var delay = (1000 * Math.floor(Math.random() * 20)) + 5000;
// if you're here then you can and should send a checkpoint
// but since multiple users who can and should might be online at once
// and since they'll all trigger this process at the same time...
// we want to stagger attempts at random intervals
ref.internal.checkpointTimeout = setTimeout(function () {
ref.internal.pendingCheckpointId = roster.checkpoint(function (err) {
if (err) { console.error(err); }
});
}, delay);
};
var isCacheCheckpoint = function (msg, author) {
var parsed = Util.tryParse(msg);
if (parsed[0] !== 'CHECKPOINT') { return false; }
var changed = simulate(parsed, author, ref);
return changed;
};
var metadata, crypto;
var send = function (msg, cb) {
if (!isReady()) { return void cb("NOT_READY"); }
var anon_rpc = store.anon_rpc;
if (!anon_rpc) { return void cb("ANON_RPC_NOT_READY"); }
var changed = false;
try {
// simulate the command before you send it
changed = simulate(msg, keys.myCurvePublic, ref);
} catch (err) {
return void cb(err.message);
}
if (!changed) { return void cb("NO_CHANGE"); }
var ciphertext = crypto.encrypt(Sortify(msg));
var id = getMessageId(ciphertext);
//console.log("Sending with id [%s]", id, msg);
//console.log();
response.expect(id, function (err, state) {
if (err) { return void cb(err); }
cb(void 0, state, id);
}, TIMEOUT_INTERVAL);
anon_rpc.send('WRITE_PRIVATE_MESSAGE', [
channel,
ciphertext
], function (err) {
if (err) { return response.handle(id, [err.message || err]); }
});
return id;
};
roster.init = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (ref.internal.initialized) { return void cb("ALREADY_INITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
data.role = 'OWNER';
var members = {};
members[me] = data;
send([ 'CHECKPOINT', { members: members } ], cb);
};
// commands
roster.checkpoint = function (_cb) {
var cb = Util.once(Util.mkAsync(_cb));
send([ 'CHECKPOINT', Util.clone(ref.state)], cb);
};
roster.add = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
//var state = ref.state;
if (!ref.internal.initialized) { return cb("UNINITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
// don't add members that are already present
// use DESCRIBE to amend
Object.keys(data).forEach(function (curve) {
if (!isValidId(curve) || isMap(ref.state.members[curve])) { return delete data[curve]; }
});
send([ 'ADD', data ], cb);
};
roster.remove = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var state = ref.state;
if (!state) { return cb("UNINITIALIZED"); }
if (!Array.isArray(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
var toRemove = [];
var current = Object.keys(state.members);
data.forEach(function (curve) {
// don't try to remove elements which are not in the current state
if (current.indexOf(curve) === -1) { return; }
toRemove.push(curve);
});
send([ 'RM', toRemove ], cb);
};
roster.describe = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var state = ref.state;
if (!state) { return cb("UNINITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
if (Object.keys(data).some(function (curve) {
var member = data[curve];
if (!isMap(member)) { delete data[curve]; }
// validate that you're trying to describe a user that is present
if (!isMap(state.members[curve])) { return true; }
// don't send fields that won't result in a change
Object.keys(member).forEach(function (k) {
if (member[k] === state.members[curve][k]) { delete member[k]; }
});
})) {
// returning true in the above loop indicates that something was invalid
return void cb("INVALID_ARGUMENTS");
}
send(['DESCRIBE', data], cb);
};
roster.metadata = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var metadata = ref.state.metadata;
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
Object.keys(data).forEach(function (k) {
if (data[k] === metadata[k]) { delete data[k]; }
});
send(['METADATA', data], cb);
};
// supports multiple invite
roster.invite = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var state = ref.state;
if (!state) { return cb("UNINITIALIZED"); }
if (!ref.internal.initialized) { return cb("UNINITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
Object.keys(data).forEach(function (curve) {
if (!isValidId(curve) || isMap(ref.state.members[curve])) { return delete data[curve]; }
});
send(['INVITE', data], cb);
};
roster.accept = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (typeof(_data) !== 'string' || !isValidId(_data)) {
return void cb("INVALID_ARGUMENTS");
}
send([ 'ACCEPT', _data ], cb);
};
nThen(function (w) {
// get metadata so we know the owners and validateKey
if (!store.anon_rpc) { return; }
store.anon_rpc.send('GET_METADATA', channel, function (err, data) {
if (err) {
w.abort();
return void console.error(err);
}
metadata = ref.internal.metadata = (data && data[0]) || undefined;
});
}).nThen(function (w) {
if (!config.keys.teamEdPublic && metadata && metadata.validateKey) {
config.keys.teamEdPublic = metadata.validateKey;
}
if (!config.keys.teamEdPublic) {
w.abort();
return void cb("NO_VALIDATE_KEY");
}
try {
crypto = Crypto.Team.createEncryptor(config.keys);
} catch (err) {
w.abort();
return void cb(err);
}
}).nThen(function () {
if (typeof(lastKnownHash) === 'string') {
console.log("Synchronizing from checkpoint");
}
ref.internal.cpNetflux = CPNetflux.start({
// if you don't have a lastKnownHash you will need the full history
// passing -1 forces the server to send all messages, otherwise
// malicious users with the signing key could send cp| messages
// and fool new users into initializing their session incorrectly
lastKnownHash: lastKnownHash,
network: config.network,
channel: config.channel,
crypto: crypto,
validateKey: config.keys.teamEdPublic,
owners: config.owners,
Cache: config.Cache,
isCacheCheckpoint: isCacheCheckpoint,
onCacheReady: onCacheReady,
onChannelError: onChannelError,
onReady: onReady,
onConnect: onConnect,
onConnectionChange: onConnectionChange,
onMessage: onMessage,
noChainPad: true,
});
});
};
return Roster;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require("../../common/common-util"),
require("../../common/common-hash"),
require('chainpad-netflux'),
require('json.sortify'),
require("nthen"),
require("chainpad-crypto"),
null // no feedback here
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
require.config({ paths: { 'json.sortify': '/components/json.sortify/dist/JSON.sortify' } });
define([
'/common/common-util.js',
'/common/common-hash.js',
'chainpad-netflux',
'json.sortify',
'/components/nthen/index.js',
'/components/chainpad-crypto/crypto.js',
'/common/common-feedback.js',
//'/components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, CPNF, Sortify, nThen, Crypto, Feedback) {
return factory.apply(null, [
Util,
Hash,
CPNF,
Sortify,
nThen,
Crypto,
Feedback
]);
});
} else {
// I'm not gonna bother supporting any other kind of instanciation
}
}());

View File

@ -0,0 +1,417 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Hash, Util, UserObject, Cache,
nThen, Crypto, Listmap, ChainPad) => {
var SF = {};
/* load
create and load a proxy using listmap for a given shared folder
- config: network and "manager" (either the user one or a team manager)
- id: shared folder id
*/
var allSharedFolders = {};
// No version: visible edit
// Version 2: encrypted edit links
SF.checkMigration = function (secondaryKey, proxy, uo, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var drive = proxy.drive || proxy;
// View access: can't migrate
if (!secondaryKey) { return void cb(); }
// Already migrated: nothing to do
if (drive.version >= 2) { return void cb(); }
// Not yet migrating: migrate
if (!drive.migrateRo) { return void uo.migrateReadOnly(cb); }
// Already migrating: wait for the end...
var done = false;
var to;
var it = setInterval(function () {
if (drive.version >= 2) {
done = true;
clearTimeout(to);
clearInterval(it);
return void cb();
}
}, 100);
to = setTimeout(function () {
clearInterval(it);
uo.migrateReadOnly(function () {
done = true;
cb();
});
}, 20000);
var path = proxy.drive ? ['drive', 'version'] : ['version'];
proxy.on('change', path, function () {
if (done) { return; }
if (drive.version >= 2) {
done = true;
clearTimeout(to);
clearInterval(it);
cb();
}
});
};
// SFMIGRATION: only needed if we want a manual migration from the share modal...
SF.migrate = function (channel) {
var sf = allSharedFolders[channel];
if (!sf) { return; }
var clients = sf.teams;
if (!Array.isArray(clients) || !clients.length) { return; }
var c = clients[0];
// No secondaryKey? ==> already migrated ==> abort
if (!c.secondaryKey) { return; }
var f = Util.find(c, ['store', 'manager', 'folders', c.id]);
// Can't find the folder: abort
if (!f) { return; }
// Already migrated: abort
if (!f.proxy || f.proxy.version) { return; }
f.userObject.migrateReadOnly(function () {
clients.forEach(function (obj) {
var uo = Util.find(obj, ['store', 'manager', 'folders', obj.id, 'userObject']);
uo.setReadOnly(false, obj.secondarykey);
});
});
};
SF.load = function (config, id, data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var network = config.network;
var store = config.store;
var isNew = config.isNew;
var isNewChannel = config.isNewChannel;
var teamId = store.id;
var handler = store.handleSharedFolder;
var href = store.manager.user.userObject.getHref(data);
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets('drive', parsed.hash, data.password);
// If we don't have valid keys, abort and remove the proxy to make sure
// we don't block the drive permanently
if (!secret.keys) {
store.manager.deprecateProxy(id);
return void cb(null);
}
var secondaryKey = secret.keys.secondaryKey;
// If we try to load an existing shared folder (isNew === false) but this folder
// doesn't exist in the database, abort and cb
nThen(function (waitFor) {
// If we're in onCacheReady, make sure we have a cache for this shared folder
if (config.cache) {
Cache.getChannelCache(secret.channel, waitFor(function (err) {
if (err === "EINVAL") { // Cache not found
waitFor.abort();
store.manager.restrictedProxy(id, secret.channel);
return void cb(null);
}
}));
}
}).nThen(function (waitFor) {
isNewChannel(null, { channel: secret.channel }, waitFor(function (obj) {
if (obj.isNew && !isNew) {
store.manager.deprecateProxy(id, secret.channel, obj.reason);
waitFor.abort();
return void cb(null);
}
}));
}).nThen(function () {
var sf = allSharedFolders[secret.channel];
if (sf && sf.readOnly && secondaryKey) {
// We were in readOnly mode and now we know the edit keys!
SF.upgrade(secret.channel, secret);
}
if (sf && sf.ready && sf.rt) {
// The shared folder is already loaded, return its data
setTimeout(function () {
var leave = function () { SF.leave(secret.channel, teamId); };
/*
var uo = store.manager.addProxy(id, sf.rt, leave, secondaryKey);
// NOTE: Shared folder migration, disable for now
SF.checkMigration(secondaryKey, sf.rt.proxy, uo, function () {
cb(sf.rt);
});
*/
store.manager.addProxy(id, sf.rt, leave, secondaryKey);
cb(sf.rt);
});
sf.teams.push({
cb: cb,
store: store,
id: id
});
if (handler) { handler(id, sf.rt); }
return;
}
if (sf && !sf.ready && sf.rt) {
// The shared folder is loading, add our callbacks to the queue
sf.teams.push({
cb: cb,
store: store,
secondaryKey: secondaryKey,
id: id
});
if (handler) { handler(id, sf.rt); }
return;
}
sf = allSharedFolders[secret.channel] = {
teams: [{
cb: cb,
store: store,
secondaryKey: secondaryKey,
id: id
}],
readOnly: !Boolean(secondaryKey)
};
var owners = data.owners;
var listmapConfig = {
data: {},
channel: secret.channel,
readOnly: !Boolean(secondaryKey),
crypto: Crypto.createEncryptor(secret.keys),
userName: 'sharedFolder',
logLevel: 1,
ChainPad: ChainPad,
classic: true,
network: network,
Cache: Cache, // shared-folder cache
metadata: {
validateKey: secret.keys.validateKey || undefined,
owners: owners
},
onRejected: config.Store && config.Store.onRejected
};
var rt = sf.rt = Listmap.create(listmapConfig);
rt.proxy.on('cacheready', function () {
if (!sf.teams) {
return;
}
sf.teams.forEach(function (obj) {
var leave = function () { SF.leave(secret.channel, obj.store.id); };
// We can safely call addProxy and obj.cb here because
// 1. addProxy won't re-add the same folder twice on 'ready'
// 2. obj.cb is using Util.once
rt.cache = true;
// If we're updating the password of an existing folder, force the creation
// of a new userobject in proxy-manager. Once it's done, remove this flag
// to make sure we won't create a second new userobject on 'ready'
obj.store.manager.addProxy(obj.id, rt, leave, obj.secondaryKey, config.updatePassword);
config.updatePassword = false;
obj.cb(sf.rt);
});
sf.ready = true;
});
rt.proxy.on('ready', function () {
if (isNew && !Object.keys(rt.proxy).length) {
// New Shared folder: no migration required
rt.proxy.version = 2;
}
if (!sf.teams) {
return;
}
sf.teams.forEach(function (obj) {
var leave = function () { SF.leave(secret.channel, obj.store.id); };
/*
var uo = obj.store.manager.addProxy(obj.id, rt, leave, obj.secondaryKey);
// NOTE: Shared folder migration, disable for now
SF.checkMigration(secondaryKey, rt.proxy, uo, function () {
obj.cb(sf.rt);
});
*/
rt.cache = false;
obj.store.manager.addProxy(obj.id, rt, leave, obj.secondaryKey, config.updatePassword);
obj.cb(sf.rt);
});
sf.ready = true;
});
rt.proxy.on('error', function (info) {
if (info && info.error) {
if (info.error === "EDELETED" ) {
try {
// 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, info.message);
if (obj.store.handleSharedFolder) {
obj.store.handleSharedFolder(obj.id, null);
}
obj.cb();
});
} catch (e) {}
delete allSharedFolders[secret.channel];
// This shouldn't be called on init because we're calling "isNewChannel" first,
// but we can still call "cb" just in case. This wait we make sure we won't block
// the initial "waitFor"
return void cb();
}
if (info.error === "ERESTRICTED" ) {
sf.teams.forEach(function (obj) {
obj.store.manager.restrictedProxy(obj.id, secret.channel);
obj.cb();
});
delete allSharedFolders[secret.channel];
return void cb();
}
}
});
if (handler) { handler(id, rt); }
});
};
SF.upgrade = function (channel, secret) {
var sf = allSharedFolders[channel];
if (!sf || !sf.readOnly) { return; }
if (!sf.rt.setReadOnly) { return; }
if (!secret.keys || !secret.keys.editKeyStr) { return; }
var crypto = Crypto.createEncryptor(secret.keys);
sf.readOnly = false;
sf.rt.setReadOnly(false, crypto);
};
SF.leave = function (channel, teamId) {
var sf = allSharedFolders[channel];
if (!sf) { return; }
var clients = sf.teams;
if (!Array.isArray(clients)) { return; }
// Remove the shared folder from the client's store and
// remove the client/team from our list
var idx;
clients.some(function (obj, i) {
if (obj.store.id === teamId) {
if (obj.store.handleSharedFolder) {
obj.store.handleSharedFolder(obj.id, null);
}
idx = i;
return true;
}
});
if (typeof (idx) === "undefined") { return; }
// Remove the selected team
clients.splice(idx, 1);
//If all the teams have closed this shared folder, stop it
if (clients.length) { return; }
if (sf.rt && sf.rt.stop) {
sf.rt.stop();
}
};
// Update the password locally
SF.updatePassword = function (Store, data, network, cb) {
var oldChannel = data.oldChannel;
var href = data.href;
var password = data.password;
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets(parsed.type, parsed.hash, password);
var sf = allSharedFolders[oldChannel];
if (!sf) { return void cb({ error: 'ENOTFOUND' }); }
if (sf.rt && sf.rt.stop) {
try { sf.rt.stop(); } catch (e) {}
}
var nt = nThen;
sf.teams.forEach(function (obj) {
nt = nt(function (waitFor) {
var s = obj.store;
var sfId = obj.id;
var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {};
if (!sfId || !shared[sfId]) { return; }
var sf = JSON.parse(JSON.stringify(shared[sfId]));
sf.password = password;
SF.load({
network: network,
store: s,
updatePassword: true,
Store: Store,
isNewChannel: Store.isNewChannel
}, sfId, sf, waitFor());
if (!s.rpc) { return; }
s.rpc.unpin([oldChannel], waitFor());
s.rpc.pin([secret.channel], waitFor());
}).nThen;
});
nt(function () {
cb();
});
};
/* loadSharedFolders
load all shared folder stored in a given drive
- store: user or team main store
- userObject: userObject associated to the main drive
- handler: a function (sfid, rt) called for each shared folder loaded
*/
SF.loadSharedFolders = function (Store, network, store, userObject, waitFor, progress, cache) {
var shared = Util.find(store.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {};
var steps = Object.keys(shared).length;
var i = 1;
var w = waitFor();
progress = progress || function () {};
nThen(function (waitFor) {
Object.keys(shared).forEach(function (id) {
var sf = shared[id];
SF.load({
network: network,
store: store,
Store: Store,
cache: cache,
isNewChannel: Store.isNewChannel
}, id, sf, waitFor(function () {
progress({
progress: i,
max: steps
});
i++;
}));
});
}).nThen(function () {
setTimeout(w);
});
};
SF.isSharedFolderChannel = function (chanId) {
return Object.keys(allSharedFolders).includes(chanId);
};
return SF;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require('../../common/common-hash'),
require('../../common/common-util'),
require('../../common/user-object'),
require('../../common/cache-store'),
require('nthen'),
require('chainpad-crypto'),
require('chainpad-listmap'),
require('chainpad')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-hash.js',
'/common/common-util.js',
'/common/user-object.js',
'/common/outer/cache-store.js',
'/components/nthen/index.js',
'/components/chainpad-crypto/crypto.js',
'chainpad-listmap',
'/components/chainpad/chainpad.dist.js',
], factory);
} else {
// unsupported initialization
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,312 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Util, Constants, Messages = {},
AppConfig = {}, Crypto) => {
var Cursor = {};
Cursor.setCustomize = data => {
Messages = data.Messages;
AppConfig = data.AppConfig;
};
var DEGRADED = AppConfig.degradedLimit || 8;
var convertToUint8 = function (obj) {
var l = Object.keys(obj).length;
var u = new Uint8Array(l);
for (var i = 0; i<l; i++) {
u[i] = obj[i];
}
return u;
};
// Send the client's cursor to their channel when we receive an update
var sendMyCursor = function (ctx, clientId) {
var client = ctx.clients[clientId];
if (!client || !client.cursor) { return; }
var chan = ctx.channels[client.channel];
if (!chan) { return; }
if (chan.degraded) { return; }
if (!chan.sendMsg) { return; } // Store not synced yet, we're running with the cache
var data = {
id: client.id,
cursor: client.cursor
};
chan.sendMsg(JSON.stringify(data));
ctx.emit('MESSAGE', data, chan.clients.filter(function (cl) {
return cl !== clientId;
}));
};
// Send all our cursors data when someone remote joins the channel
var sendOurCursors = function (ctx, chan) {
if (chan.degraded) { return; }
chan.clients.forEach(function (c) {
var client = ctx.clients[c];
if (!client) { return; }
var data = {
id: client.id,
cursor: client.cursor
};
// Send our data to the other users (NOT including the other tabs of the same worker)
chan.sendMsg(JSON.stringify(data));
});
};
var updateDegraded = function (ctx, wc, chan) {
var m = wc.members;
chan.degraded = (m.length-1) >= DEGRADED;
ctx.emit('DEGRADED', { degraded: chan.degraded }, chan.clients);
};
var initCursor = function (ctx, obj, client, cb) {
var channel = obj.channel;
var secret = obj.secret;
if (secret.keys.cryptKey) {
secret.keys.cryptKey = convertToUint8(secret.keys.cryptKey);
}
var padChan = secret.channel;
var network = ctx.store.network;
var first = true;
var c = ctx.clients[client];
if (!c) {
c = ctx.clients[client] = {
channel: channel,
cursor: {}
};
} else {
return void cb();
}
var chan = ctx.channels[channel];
if (chan) {
// This channel is already open in another tab
// ==> Set the ID to our client object
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
// ==> Send the cursor position of the other tabs
chan.clients.forEach(function (cl) {
var clientObj = ctx.clients[cl];
if (chan.degraded) { return; }
if (!clientObj) { return; }
ctx.emit('MESSAGE', {
id: clientObj.id,
cursor: clientObj.cursor
}, [client]);
});
chan.sendMsg(JSON.stringify({join: true, id: c.id}));
// ==> And push the new tab to the list
chan.clients.push(client);
updateDegraded(ctx, chan.wc, chan);
return void cb();
}
var onOpen = function (wc) {
ctx.channels[channel] = ctx.channels[channel] || {};
var chan = ctx.channels[channel];
chan.padChan = padChan;
if (!c.id) { c.id = wc.myID + '-' + client; }
if (chan.clients) {
// If 2 tabs from the same worker have been opened at the same time,
// we have to fix both of them
chan.clients.forEach(function (cl) {
if (ctx.clients[cl] && !ctx.clients[cl].id) {
ctx.clients[cl].id = wc.myID + '-' + cl;
}
});
}
if (!chan.encryptor) { chan.encryptor = Crypto.createEncryptor(secret.keys); }
wc.on('join', function () {
sendOurCursors(ctx, chan);
updateDegraded(ctx, wc, chan);
});
wc.on('leave', function (peer) {
ctx.emit('MESSAGE', {leave: true, id: peer}, chan.clients);
updateDegraded(ctx, wc, chan);
});
wc.on('message', function (cryptMsg) {
if (chan.degraded) { return; }
var msg = chan.encryptor.decrypt(cryptMsg, secret.keys && secret.keys.validateKey);
var parsed;
try {
parsed = JSON.parse(msg);
if (parsed && parsed.join) {
return void sendOurCursors(ctx, chan);
}
ctx.emit('MESSAGE', parsed, chan.clients);
} catch (e) { console.error(e); }
});
chan.wc = wc;
chan.sendMsg = function (msg, cb) {
cb = cb || function () {};
var cmsg = chan.encryptor.encrypt(msg);
wc.bcast(cmsg).then(function () {
cb();
}, function (err) {
cb({error: err});
});
};
if (!first) { return; }
chan.clients = [client];
first = false;
cb();
updateDegraded(ctx, wc, chan);
};
network.join(channel).then(onOpen, function (err) {
return void cb({error: err});
});
var onReconnect = function () {
if (!ctx.channels[channel]) { console.log("cant reconnect", channel); return; }
network.join(channel).then(onOpen, function (err) {
console.error(err);
});
};
ctx.channels[channel] = ctx.channels[channel] || {};
ctx.channels[channel].onReconnect = onReconnect;
network.on('reconnect', onReconnect);
};
var updateCursor = function (ctx, data, client, cb) {
var c = ctx.clients[client];
if (!c) { return void cb({error: 'NO_CLIENT'}); }
var proxy = ctx.store.proxy || {};
data.color = Util.find(proxy, ['settings', 'general', 'cursor', 'color']);
data.name = proxy[Constants.displayNameKey] || ctx.store.noDriveName || Messages.anonymous;
data.avatar = Util.find(proxy, ['profile', 'avatar']);
data.uid = Util.find(proxy, ['uid']) || ctx.store.noDriveUid;
c.cursor = data;
sendMyCursor(ctx, client);
cb();
};
var leaveChannel = function (ctx, padChan) {
// Leave channel and prevent reconnect when we leave a pad
Object.keys(ctx.channels).some(function (cursorChan) {
var channel = ctx.channels[cursorChan];
if (channel.padChan !== padChan) { return; }
if (channel.wc) { channel.wc.leave(); }
if (channel.onReconnect) {
var network = ctx.store.network;
network.off('reconnect', channel.onReconnect);
}
delete ctx.channels[cursorChan];
return true;
});
};
// Remove the client from all its channels when a tab is closed
var removeClient = function (ctx, clientId) {
var filter = function (c) {
return c !== clientId;
};
// Remove the client from our channels
var chan;
for (var k in ctx.channels) {
chan = ctx.channels[k];
chan.clients = chan.clients.filter(filter);
if (chan.clients.length === 0) {
if (chan.wc) { chan.wc.leave(); }
if (chan.onReconnect) {
var network = ctx.store.network;
network.off('reconnect', chan.onReconnect);
}
delete ctx.channels[k];
}
}
// Send the leave message to the channel we were in
if (ctx.clients[clientId]) {
var leaveMsg = {
leave: true,
id: ctx.clients[clientId].id
};
chan = ctx.channels[ctx.clients[clientId].channel];
if (chan) {
chan.sendMsg(JSON.stringify(leaveMsg));
ctx.emit('MESSAGE', leaveMsg, chan.clients);
}
}
delete ctx.clients[clientId];
};
Cursor.init = function (cfg, waitFor, emit) {
var cursor = {};
// Already initialized by a "noDrive" tab?
if (cfg.store && cfg.store.modules && cfg.store.modules['cursor']) {
return cfg.store.modules['cursor'];
}
var ctx = {
store: cfg.store,
emit: emit,
channels: {},
clients: {}
};
cursor.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
cursor.leavePad = function (padChan) {
leaveChannel(ctx, padChan);
};
cursor.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'INIT_CURSOR') {
return void initCursor(ctx, data, clientId, cb);
}
if (cmd === 'UPDATE') {
return void updateCursor(ctx, data, clientId, cb);
}
};
return cursor;
};
return Cursor;
};
if (typeof(module) !== 'undefined' && module.exports) {
// Code from customize can't be laoded directly in the build
module.exports = factory(
require('../../common/common-util'),
require('../../common/common-constants'),
undefined,
undefined,
require('chainpad-crypto')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-constants.js',
'/customize/messages.js',
'/customize/application_config.js',
'/components/chainpad-crypto/crypto.js',
], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,279 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Util, Hash, UserObject, nThen) => {
var History = {};
var commands = {};
var getAccountChannels = function (ctx) {
var channels = [];
var edPublic = Util.find(ctx.store, ['proxy', 'edPublic']);
// Drive
var driveOwned = (Util.find(ctx.store, ['driveMetadata', 'owners']) || []).indexOf(edPublic) !== -1;
if (driveOwned) {
channels.push(ctx.store.driveChannel);
}
// Profile
var profile = ctx.store.proxy.profile;
if (profile) {
var profileChan = profile.edit ? Hash.hrefToHexChannelId('/profile/#' + profile.edit, null) : null;
if (profileChan) { channels.push(profileChan); }
}
// Todo
if (ctx.store.proxy.todo) {
channels.push(Hash.hrefToHexChannelId('/todo/#' + ctx.store.proxy.todo, null));
}
// Mailboxes
var mailboxes = ctx.store.proxy.mailboxes;
if (mailboxes) {
var mList = Object.keys(mailboxes).map(function (m) {
return {
lastKnownHash: mailboxes[m].lastKnownHash,
channel: mailboxes[m].channel
};
});
Array.prototype.push.apply(channels, mList);
}
// Shared folders owned by me
var sf = ctx.store.proxy[UserObject.SHARED_FOLDERS];
if (sf) {
var sfChannels = Object.keys(sf).map(function (fId) {
var data = sf[fId];
if (!data || !data.owners) { return; }
var isOwner = Array.isArray(data.owners) && data.owners.indexOf(edPublic) !== -1;
if (!isOwner) { return; }
return data.channel;
}).filter(Boolean);
Array.prototype.push.apply(channels, sfChannels);
}
return channels;
};
let getTeamChannels = function (ctx, teamId) {
let team = Util.find(ctx.store, ['proxy', 'teams', teamId]);
if (!team) { return []; }
let channels = [team.channel];
let roster = team.keys.roster;
channels.push({
channel: roster.channel,
lastKnownHash: roster.lastKnownHash
});
return channels;
};
var getEdPublic = function (ctx, teamId) {
if (!teamId) { return Util.find(ctx.store, ['proxy', 'edPublic']); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
return Util.find(teamData, ['keys', 'drive', 'edPublic']);
};
var getRpc = function (ctx, teamId) {
if (!teamId) { return ctx.store.rpc; }
var teams = ctx.store.modules['team'];
if (!teams) { return; }
var team = teams.getTeam(teamId);
if (!team) { return; }
return team.rpc;
};
var getHistoryData = function (ctx, channel, lastKnownHash, teamId, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var edPublic = getEdPublic(ctx, teamId);
var Store = ctx.Store;
var total = 0;
var history = 0;
var metadata = 0;
var hash;
nThen(function (waitFor) {
// Total size
Store.getFileSize(null, {
channel: channel
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
if (typeof(obj.size) === "undefined") {
waitFor.abort();
return void cb({error: 'ENOENT'});
}
total = obj.size;
}));
// Pad
Store.getHistory(null, {
channel: channel,
lastKnownHash: lastKnownHash
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
if (!Array.isArray(obj)) {
waitFor.abort();
return void cb({error: 'EINVAL'});
}
if (!obj.length) { return; }
hash = obj[0].hash;
var messages = obj.map(function(data) {
return data.msg;
});
history = messages.join('\n').length;
}), true);
// Metadata
Store.getPadMetadata(null, {
channel: channel
}, waitFor(function (obj) {
if (obj && obj.error) { return; }
if (!obj || typeof(obj) !== "object") { return; }
metadata = JSON.stringify(obj).length;
if (!obj || !Array.isArray(obj.owners) ||
obj.owners.indexOf(edPublic) === -1) {
waitFor.abort();
return void cb({error: 'INSUFFICIENT_PERMISSIONS'});
}
}));
}).nThen(function () {
cb({
size: (total - metadata - history),
hash: hash
});
});
};
commands.GET_HISTORY_SIZE = function (ctx, data, cId, cb) {
if (!ctx.store.loggedIn || !ctx.store.rpc) { return void cb({ error: 'INSUFFICIENT_PERMISSIONS' }); }
var channels = data.channels;
if (!Array.isArray(channels)) { return void cb({ error: 'EINVAL' }); }
var warning = [];
// If account trim history, get the correct channels here
if (data.account) {
channels = getAccountChannels(ctx);
} else if (data.team) {
channels = getTeamChannels(ctx, data.team);
}
var size = 0;
var res = [];
nThen(function (waitFor) {
channels.forEach(function (chan) {
var channel = chan;
var lastKnownHash;
if (typeof (chan) === "object" && chan.channel) {
channel = chan.channel;
lastKnownHash = chan.lastKnownHash;
}
getHistoryData(ctx, channel, lastKnownHash, data.teamId, waitFor(function (obj) {
if (obj && obj.error) {
warning.push(obj.error);
return;
}
size += obj.size;
if (!obj.hash) { return; }
res.push({
channel: channel,
hash: obj.hash
});
}));
});
}).nThen(function () {
cb({
warning: warning.length ? warning : undefined,
channels: res,
size: size
});
});
};
commands.TRIM_HISTORY = function (ctx, data, cId, cb) {
if (!ctx.store.loggedIn || !ctx.store.rpc) { return void cb({ error: 'INSUFFICIENT_PERMISSIONS' }); }
var channels = data.channels;
if (!Array.isArray(channels)) { return void cb({ error: 'EINVAL' }); }
var rpc = getRpc(ctx, data.teamId);
if (!rpc) { return void cb({ error: 'ENORPC'}); }
var warning = [];
nThen(function (waitFor) {
channels.forEach(function (obj) {
rpc.trimHistory(obj, waitFor(function (err) {
if (err) {
warning.push(err);
return;
}
}));
});
}).nThen(function () {
// Only one channel and warning: error
if (channels.length === 1 && warning.length) {
return void cb({error: warning[0]});
}
cb({
warning: warning.length ? warning : undefined
});
});
};
History.init = function (cfg, waitFor, emit) {
var history = {};
if (!cfg.store) { return; }
var ctx = {
store: cfg.store,
Store: cfg.Store,
pinPads: cfg.pinPads,
updateMetadata: cfg.updateMetadata,
emit: emit,
};
history.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
try {
commands[cmd](ctx, data, clientId, cb);
} catch (e) {
console.error(e);
}
};
return history;
};
return History;
};
if (typeof(module) !== 'undefined' && module.exports) {
// Code from customize can't be laoded directly in the build
module.exports = factory(
undefined,
require('../../common/common-util'),
require('../../common/common-hash'),
require('../../common/user-object'),
require('nthen')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/user-object.js',
'/components/nthen/index.js',
], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,219 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Crypto) => {
var Integration = {};
var convertToUint8 = function (obj) {
var l = Object.keys(obj).length;
var u = new Uint8Array(l);
for (var i = 0; i<l; i++) {
u[i] = obj[i];
}
return u;
};
var sendMsg = function (ctx, data, client, cb) {
var c = ctx.clients[client];
if (!c) { return void cb({error: 'NO_CLIENT'}); }
var chan = ctx.channels[c.channel];
if (!chan) { return void cb({error: 'NO_CHAN'}); }
var obj = {
id: client,
msg: data.msg,
uid: data.uid,
};
chan.sendMsg(JSON.stringify(obj), cb);
ctx.emit('MESSAGE', obj, chan.clients.filter(function (cl) {
return cl !== client;
}));
};
var initIntegration = function (ctx, obj, client, cb) {
var channel = obj.channel;
var secret = obj.secret;
if (secret.keys.cryptKey) {
secret.keys.cryptKey = convertToUint8(secret.keys.cryptKey);
}
var padChan = secret.channel;
var network = ctx.store.network;
var first = true;
var c = ctx.clients[client];
if (!c) {
c = ctx.clients[client] = {
channel: channel
};
} else {
return void cb();
}
var chan = ctx.channels[channel];
if (chan) {
// This channel is already open in another tab
// ==> Set the ID to our client object
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
// ==> And push the new tab to the list
chan.clients.push(client);
return void cb();
}
var onOpen = function (wc) {
ctx.channels[channel] = ctx.channels[channel] || {};
var chan = ctx.channels[channel];
chan.padChan = padChan;
if (!c.id) { c.id = wc.myID + '-' + client; }
if (chan.clients) {
// If 2 tabs from the same worker have been opened at the same time,
// we have to fix both of them
chan.clients.forEach(function (cl) {
if (ctx.clients[cl] && !ctx.clients[cl].id) {
ctx.clients[cl].id = wc.myID + '-' + cl;
}
});
}
if (!chan.encryptor) { chan.encryptor = Crypto.createEncryptor(secret.keys); }
wc.on('message', function (cryptMsg) {
var msg = chan.encryptor.decrypt(cryptMsg, secret.keys && secret.keys.validateKey);
var parsed;
try {
parsed = JSON.parse(msg);
ctx.emit('MESSAGE', parsed, chan.clients);
} catch (e) { console.error(e); }
});
chan.wc = wc;
chan.sendMsg = function (msg, cb) {
cb = cb || function () {};
var cmsg = chan.encryptor.encrypt(msg);
wc.bcast(cmsg).then(function () {
cb();
}, function (err) {
cb({error: err});
});
};
if (!first) { return; }
chan.clients = [client];
first = false;
cb();
};
network.join(channel).then(onOpen, function (err) {
return void cb({error: err});
});
var onReconnect = function () {
if (!ctx.channels[channel]) { console.log("cant reconnect", channel); return; }
network.join(channel).then(onOpen, function (err) {
console.error(err);
});
};
ctx.channels[channel] = ctx.channels[channel] || {};
ctx.channels[channel].onReconnect = onReconnect;
network.on('reconnect', onReconnect);
};
var leaveChannel = function (ctx, padChan) {
// Leave channel and prevent reconnect when we leave a pad
Object.keys(ctx.channels).some(function (cursorChan) {
var channel = ctx.channels[cursorChan];
if (channel.padChan !== padChan) { return; }
if (channel.wc) { channel.wc.leave(); }
if (channel.onReconnect) {
var network = ctx.store.network;
network.off('reconnect', channel.onReconnect);
}
delete ctx.channels[cursorChan];
return true;
});
};
// Remove the client from all its channels when a tab is closed
var removeClient = function (ctx, clientId) {
var filter = function (c) {
return c !== clientId;
};
// Remove the client from our channels
var chan;
for (var k in ctx.channels) {
chan = ctx.channels[k];
chan.clients = chan.clients.filter(filter);
if (chan.clients.length === 0) {
if (chan.wc) { chan.wc.leave(); }
if (chan.onReconnect) {
var network = ctx.store.network;
network.off('reconnect', chan.onReconnect);
}
delete ctx.channels[k];
}
}
delete ctx.clients[clientId];
};
Integration.init = function (cfg, waitFor, emit) {
var integration = {};
// Already initialized by a "noDrive" tab?
if (cfg.store && cfg.store.modules && cfg.store.modules['integration']) {
return cfg.store.modules['integration'];
}
var ctx = {
store: cfg.store,
emit: emit,
channels: {},
clients: {}
};
integration.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
integration.leavePad = function (padChan) {
leaveChannel(ctx, padChan);
};
integration.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'INIT') {
return void initIntegration(ctx, data, clientId, cb);
}
if (cmd === 'SEND') {
return void sendMsg(ctx, data, clientId, cb);
}
};
return integration;
};
return Integration;
};
if (typeof(module) !== 'undefined' && module.exports) {
// Code from customize can't be laoded directly in the build
module.exports = factory(
require('chainpad-crypto')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/components/chainpad-crypto/crypto.js',
], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,707 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Config = {}, BCast = {}, Util, Hash,
Realtime, Messaging, Notify, Handlers, CpNetflux, Crypto) => {
var Mailbox = {};
Config.setCustomize = data => {
Config = data.ApiConfig;
BCast = data.Broadcast;
};
var TYPES = [
'notifications',
'supportteam',
'broadcast'
];
var BLOCKING_TYPES = [
];
var BROADCAST_CHAN = '000000000000000000000000000000000'; // Admin channel, 33 characters
var initializeMailboxes = function (ctx, mailboxes) {
if (!mailboxes['notifications'] && ctx.loggedIn) {
mailboxes.notifications = {
channel: Hash.createChannelId(),
lastKnownHash: '',
viewed: []
};
ctx.pinPads([mailboxes.notifications.channel], function (res) {
if (res.error) { console.error(res); }
});
}
// no need for the "support" mailbox anymore
if (mailboxes.support) {
delete mailboxes.support;
}
if (!mailboxes['broadcast']) {
mailboxes.broadcast = {
channel: BROADCAST_CHAN,
lastKnownHash: BCast.lastBroadcastHash,
decrypted: true,
viewed: []
};
}
};
/*
proxy.mailboxes = {
friends: {
channel: '',
lastKnownHash: '',
viewed: []
}
};
*/
var isMessageNew = function (hash, m) {
return (m.viewed || []).indexOf(hash) === -1 && hash !== m.lastKnownHash;
};
var showMessage = function (ctx, type, msg, cId, cb) {
ctx.emit('MESSAGE', {
type: type,
content: msg
}, cId ? [cId] : ctx.clients, cb);
};
var hideMessage = function (ctx, type, hash, clients) {
ctx.emit('VIEWED', {
type: type,
hash: hash
}, clients || ctx.clients);
};
var getMyKeys = function (ctx) {
var proxy = ctx.store && ctx.store.proxy;
if (!proxy.curvePrivate || !proxy.curvePublic) { return; }
return {
curvePrivate: proxy.curvePrivate,
curvePublic: proxy.curvePublic
};
};
// Send a message to someone else
var sendTo = Mailbox.sendTo = function (ctx, type, msg, user, _cb) {
user = user || {};
var cb = _cb || function (obj) {
if (obj && obj.error) {
console.error(obj.error);
}
};
if (!Crypto.Mailbox) {
return void cb({error: "chainpad-crypto is outdated and doesn't support mailboxes."});
}
var anonRpc = Util.find(ctx, [ 'store', 'anon_rpc', ]);
if (!anonRpc) { return void cb({error: "anonymous rpc session not ready"}); }
// Broadcast mailbox doesn't use encryption. Sending messages there is restricted
// to admins in the server directly
var crypto = { encrypt: function (x) { return x; } };
var channel = BROADCAST_CHAN;
var obj = {
uid: Util.uid(), // add uid at the beginning to have a unique server hash
type: type,
content: msg
};
if (!/^BROADCAST/.test(type)) {
var keys = getMyKeys(ctx);
if (!keys) { return void cb({error: "missing asymmetric encryption keys"}); }
if (!user || !user.channel || !user.curvePublic) { return void cb({error: "no notification channel"}); }
channel = user.channel;
crypto = Crypto.Mailbox.createEncryptor(keys);
// Always send your data
if (typeof(msg) === "object" && !msg.user) {
var myData = Messaging.createData(ctx.store.proxy, false);
msg.user = myData;
}
obj = {
type: type,
content: msg
};
}
var text = JSON.stringify(obj);
var ciphertext = crypto.encrypt(text, user.curvePublic);
// If we've sent this message to one of our teams' mailbox, we may want to "dismiss" it
// automatically
if (user.viewed) {
var team = Util.find(ctx, ['store', 'proxy', 'teams', user.viewed]);
if (team) {
var hash = ciphertext.slice(0,64);
var viewed = Util.find(team, ['keys', 'mailbox', 'viewed']);
if (Array.isArray(viewed)) { viewed.push(hash); }
}
}
anonRpc.send("WRITE_PRIVATE_MESSAGE", [
channel,
ciphertext
], function (err /*, response */) {
if (err) {
return void cb({
error: err,
});
}
return void cb({
hash: ciphertext.slice(0,64)
});
});
};
Mailbox.sendToAnon = function (anonRpc, type, msg, user, cb) {
var Nacl = Crypto.Nacl;
var curveSeed = Nacl.randomBytes(32);
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
var curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
var curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
sendTo({
store: {
anon_rpc: anonRpc,
proxy: {
curvePrivate: curvePrivate,
curvePublic: curvePublic
}
}
}, type, msg, user, cb);
};
// Mark a message as read
var dismiss = function (ctx, data, cId, cb) {
var type = data.type;
var hash = data.hash;
// Reminder messages don't persist
if (/^REMINDER\|/.test(hash)) {
cb();
delete ctx.boxes.reminders.content[hash];
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
var uid = hash.slice(9).split('-')[0];
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]);
if (!d) {
var h = ctx.store.proxy.hideReminders = ctx.store.proxy.hideReminders || {};
d = h[uid] = h[uid] || [];
}
var delay = hash.split('-')[1];
if (delay && !d.includes(delay)) { d.push(Number(delay)); }
return;
}
var box = ctx.boxes[type];
if (!box) { return void cb({error: 'NOT_LOADED'}); }
var m = box.data || {};
// If the hash in in our history, get the index from the history:
// - if the index is 0, we can change our lastKnownHash
// - otherwise, just push to view
var idx = box.history.indexOf(hash);
if (idx !== -1) {
if (idx === 0) {
m.lastKnownHash = hash;
box.history.shift();
} else if (m.viewed.indexOf(hash) === -1) {
m.viewed.push(hash);
}
}
// Clear data in memory if needed
// Check the "viewed" array to see if we're able to bump lastKnownhash more
var sliceIdx;
var lastKnownHash;
var toForget = [];
box.history.some(function (hash, i) {
// naming here is confusing... isViewed implies it's a boolean
// when in fact it's an index
var isViewed = m.viewed.indexOf(hash);
// iterate over your history until you hit an element you haven't viewed
if (isViewed === -1) { return true; }
// update the index that you'll use to slice off viewed parts of history
sliceIdx = i + 1;
// keep track of which hashes you should remove from your 'viewed' array
toForget.push(hash);
// prevent fetching dismissed messages on (re)connect
lastKnownHash = hash;
});
// remove all elements in 'toForget' from the 'viewed' array in one step
m.viewed = m.viewed.filter(function (hash) {
return toForget.indexOf(hash) === -1;
});
if (sliceIdx) {
box.history = box.history.slice(sliceIdx);
m.lastKnownHash = lastKnownHash;
}
// Make sure we remove data about dismissed messages
Object.keys(box.content).forEach(function (h) {
if (box.history.indexOf(h) === -1 || m.viewed.indexOf(h) !== -1) {
Handlers.remove(ctx, box, box.content[h], h);
delete box.content[h];
}
});
Realtime.whenRealtimeSyncs(ctx.store.realtime, function () {
cb();
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
});
};
var leaveChannel = function (ctx, type, cb) {
cb = cb || function () {};
var box = ctx.boxes[type];
if (!box) { return void cb(); }
if (!box.cpNf || typeof(box.cpNf.stop) !== "function") { return void cb('EINVAL'); }
box.cpNf.stop();
Object.keys(box.content).forEach(function (h) {
Handlers.remove(ctx, box, box.content[h], h);
hideMessage(ctx, type, h, ctx.clients);
});
delete ctx.boxes[type];
};
var openChannel = function (ctx, type, m, onReady, opts) {
opts = opts || {};
var box = ctx.boxes[type] = {
channel: m.channel,
type: type,
queue: [], // Store the messages to send when the channel is ready
history: [], // All the hashes loaded from the server in corretc order
content: {}, // Content of the messages that should be displayed
sendMessage: function (msg) { // To send a message to our box
// Always send your data
if (typeof(msg) === "object" && !msg.user) {
var myData = Messaging.createData(ctx.store.proxy, false);
msg.user = myData;
}
try {
msg = JSON.stringify(msg);
} catch (e) {
console.error(e);
}
box.queue.push(msg);
},
data: m
};
if (!Crypto.Mailbox) {
return void console.error("chainpad-crypto is outdated and doesn't support mailboxes.");
}
var keys = m.keys || getMyKeys(ctx);
if (!keys && !m.decrypted) { return void console.error("missing asymmetric encryption keys"); }
var crypto = m.decrypted ? {
encrypt: function (x) { return x; },
decrypt: function (x) { return x; }
} : Crypto.Mailbox.createEncryptor(keys);
box.encryptor = crypto;
var cfg = {
network: ctx.store.network,
channel: m.channel,
noChainPad: true,
crypto: crypto,
owners: type === 'broadcast' ? [] : (opts.owners || [ctx.store.proxy.edPublic]),
lastKnownHash: m.lastKnownHash
};
cfg.onConnectionChange = function () {}; // Allow reconnections in chainpad-netflux
cfg.onConnect = function (wc, sendMessage) {
// Send a message to our box?
// NOTE: we use our own curvePublic so that we can decrypt our own message :)
box.sendMessage = function (_msg, cb) {
cb = cb || function () {};
var msg;
try {
msg = JSON.stringify(_msg);
} catch (e) {
console.error(e);
}
sendMessage(msg, function (err, hash) {
if (err) { return void console.error(err); }
box.history.push(hash);
_msg.ctime = +new Date();
box.content[hash] = _msg;
var message = {
msg: _msg,
hash: hash
};
showMessage(ctx, type, message);
cb(hash);
}, keys.curvePublic);
};
box.queue.forEach(function (msg) {
box.sendMessage(msg);
});
box.queue = [];
};
var lastReceivedHash; // Don't send a duplicate of the last known hash on reconnect
box.onMessage = cfg.onMessage = function (msg, user, vKey, isCp, hash, author, data) {
if (hash === m.lastKnownHash) { return; }
if (hash === lastReceivedHash) { return; }
var time = data && data.time;
lastReceivedHash = hash;
try {
msg = JSON.parse(msg);
} catch (e) {
console.error(e);
}
if (author) { msg.author = author; }
box.history.push(hash);
if (isMessageNew(hash, m)) {
// Message should be displayed
var message = {
msg: msg,
hash: hash,
time: time
};
var notify = box.ready;
Handlers.add(ctx, box, message, function (dismissed, toDismiss, invalid) {
if (toDismiss) { // List of other messages to remove
dismiss(ctx, toDismiss, '', function () {
console.log('Notification handled automatically');
});
}
if (invalid || dismissed) { // This message should be removed
dismiss(ctx, {
type: type,
hash: hash
}, '', function () {
console.log('Notification handled automatically');
});
return;
}
msg.ctime = time || 0;
box.content[hash] = msg;
if (opts.dump) { return; }
showMessage(ctx, type, message, null, function (obj) {
if (!obj || !obj.msg || !notify) { return; }
Notify.system(undefined, obj.msg);
});
});
} else {
// Message has already been viewed by the user
if (Object.keys(box.content).length === 0) {
// If nothing is displayed yet, we can bump our lastKnownHash and remove this hash
// from our "viewed" array
m.lastKnownHash = hash;
box.history = [];
var idxViewed = m.viewed.indexOf(hash);
if (idxViewed !== -1) { m.viewed.splice(idxViewed, 1); }
}
}
};
cfg.onReady = function () {
// Clean the "viewed" array: make sure all the "viewed" hashes are
// in history
var toClean = [];
m.viewed.forEach(function (h, i) {
if (box.history.indexOf(h) === -1) {
toClean.push(i);
}
});
for (var i = toClean.length-1; i>=0; i--) {
m.viewed.splice(toClean[i], 1);
}
// Listen for changes in the "viewed" and lastKnownHash values
var view = function (h) {
Handlers.remove(ctx, box, box.content[h], h);
delete box.content[h];
hideMessage(ctx, type, h);
};
ctx.store.proxy.on('change', ['mailboxes', type], function (o, n, p) {
if (p[2] === 'lastKnownHash') {
// Hide everything up to this hash
var sliceIdx;
box.history.some(function (h, i) {
sliceIdx = i + 1;
view(h);
if (h === n) { return true; }
});
box.history = box.history.slice(sliceIdx);
}
if (p[2] === 'viewed') {
// Hide this message
view(n);
}
});
box.ready = true;
// Continue
onReady(box.content);
};
box.cpNf = CpNetflux.start(cfg);
};
var initializeHistory = function (ctx) {
var network = ctx.store.network;
network.on('message', function (msg, sender) {
if (sender !== network.historyKeeper) { return; }
var parsed = JSON.parse(msg);
if (!/HISTORY_RANGE/.test(parsed[0])) { return; }
var txid = parsed[1];
var req = ctx.req[txid];
if (!req) { return; }
var type = parsed[0];
var _msg = parsed[2];
var box = req.box;
if (type === 'HISTORY_RANGE') {
if (!Array.isArray(_msg)) { return; }
var message;
if (req.box.type === 'broadcast') {
message = Util.tryParse(_msg[4]);
} else {
try {
var decrypted = box.encryptor.decrypt(_msg[4]);
message = JSON.parse(decrypted.content);
message.author = decrypted.author;
} catch (e) {
console.log(e);
}
}
var hash = _msg[4].slice(0,64);
ctx.emit('HISTORY', {
txid: txid,
time: _msg[5],
message: message,
hash: hash
}, [req.cId]);
} else if (type === 'HISTORY_RANGE_END') {
ctx.emit('HISTORY', {
txid: txid,
complete: true
}, [req.cId]);
delete ctx.req[txid];
}
});
};
var loadHistory = function (ctx, clientId, data, cb) {
var box = ctx.boxes[data.type];
if (!box) { return void cb({error: 'ENOENT'}); }
var msg = [ 'GET_HISTORY_RANGE', box.channel, {
from: data.lastKnownHash,
count: data.count,
txid: data.txid
}
];
if (data.type === 'broadcast') {
msg = [ 'GET_HISTORY_RANGE', box.channel, {
to: data.lastKnownHash,
txid: data.txid
}
];
}
ctx.req[data.txid] = {
cId: clientId,
box: box
};
var network = ctx.store.network;
network.sendto(network.historyKeeper, JSON.stringify(msg)).then(function () {
}, function (err) {
console.error(err);
});
};
var subscribe = function (ctx, data, cId, cb) {
// Get existing notifications
Object.keys(ctx.boxes).forEach(function (type) {
Object.keys(ctx.boxes[type].content).forEach(function (h) {
var message = {
msg: ctx.boxes[type].content[h],
hash: h
};
showMessage(ctx, type, message, cId, function (obj) {
if (obj.error) { return; }
// Notify only if "requiresNotif" is true
if (!message.msg || !message.msg.requiresNotif) { return; }
Notify.system(undefined, obj.msg);
delete message.msg.requiresNotif;
});
});
});
// Subscribe to new notifications
var idx = ctx.clients.indexOf(cId);
if (idx === -1) {
ctx.clients.push(cId);
}
cb();
};
var removeClient = function (ctx, cId) {
var idx = ctx.clients.indexOf(cId);
ctx.clients.splice(idx, 1);
};
Mailbox.init = function (cfg, waitFor, emit) {
var mailbox = {};
var store = cfg.store;
var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {};
var ctx = {
Store: cfg.Store,
store: store,
pinPads: cfg.pinPads,
updateMetadata: cfg.updateMetadata,
updateDrive: cfg.updateDrive,
mailboxes: mailboxes,
emit: emit,
clients: [],
boxes: {},
req: {},
loggedIn: store.loggedIn && store.proxy.edPublic
};
initializeMailboxes(ctx, mailboxes);
if (ctx.loggedIn) {
initializeHistory(ctx);
}
ctx.boxes.reminders = {
content: {}
};
Object.keys(mailboxes).forEach(function (key) {
if (TYPES.indexOf(key) === -1) { return; }
var m = mailboxes[key];
if (BLOCKING_TYPES.indexOf(key) === -1) {
openChannel(ctx, key, m, function () {
//console.log(key + ' mailbox is ready');
});
} else {
openChannel(ctx, key, m, waitFor(function () {
//console.log(key + ' mailbox is ready');
}));
}
});
if (ctx.loggedIn) {
Object.keys(store.proxy.teams || {}).forEach(function (teamId) {
var team = store.proxy.teams[teamId];
if (!team) { return; }
var teamMailbox = team.keys.mailbox || {};
if (!teamMailbox.channel) { return; }
var opts = {
owners: [Util.find(team, ['keys', 'drive', 'edPublic'])]
};
openChannel(ctx, 'team-'+teamId, teamMailbox, function () {
//console.log('Mailbox team', teamId);
}, opts);
});
}
mailbox.post = function (box, type, content) {
var b = ctx.boxes[box];
if (!b) { return; }
b.sendMessage({
type: type,
content: content,
sender: store.proxy.curvePublic
});
};
mailbox.hideMessage = function (type, msg) {
hideMessage(ctx, type, msg.hash, ctx.clients);
};
mailbox.showMessage = function (type, msg, cId, cb) {
if (type === "reminders" && msg) {
ctx.boxes.reminders.content[msg.hash] = msg.msg;
if (!ctx.clients.length) {
ctx.boxes.reminders.content[msg.hash].requiresNotif = true;
}
// Hide existing messages for this event
hideMessage(ctx, type, msg.hash, ctx.clients);
}
showMessage(ctx, type, msg, cId, function (obj) {
Notify.system(undefined, obj.msg);
if (cb) { cb(); }
});
};
mailbox.open = function (key, m, cb, team, opts) {
if (TYPES.indexOf(key) === -1 && !team) { return; }
openChannel(ctx, key, m, cb, opts);
};
mailbox.close = function (key, cb) {
leaveChannel(ctx, key, cb);
};
mailbox.dismiss = function (data, cb) {
dismiss(ctx, data, '', cb);
};
mailbox.sendTo = function (type, msg, user, cb) {
if (!ctx.loggedIn) { return void cb({error:'NOT_LOGGED_IN'}); }
sendTo(ctx, type, msg, user, cb);
};
mailbox.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
mailbox.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {
return void subscribe(ctx, data, clientId, cb);
}
if (cmd === 'DISMISS') {
return void dismiss(ctx, data, clientId, cb);
}
if (cmd === 'SENDTO') {
return void sendTo(ctx, data.type, data.msg, data.user, cb);
}
if (cmd === 'LOAD_HISTORY') {
return void loadHistory(ctx, clientId, data, cb);
}
};
return mailbox;
};
return Mailbox;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
undefined,
undefined,
require('../../common/common-util'),
require('../../common/common-hash'),
require('../../common/common-realtime'),
require('../../common/common-messaging'),
require('../../common/notify'),
require('../components/mailbox-handlers'),
require('chainpad-netflux'),
require('chainpad-crypto')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/api/config',
'/api/broadcast',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-realtime.js',
'/common/common-messaging.js',
'/common/notify.js',
'/common/outer/mailbox-handlers.js',
'chainpad-netflux',
'/components/chainpad-crypto/crypto.js',
], factory);
} else {
// unsupported initialization
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,361 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = () => {
var OO = {};
var getHistory = function (ctx, client, cb) {
var c = ctx.clients[client];
if (!c) { return void cb({error: 'ENOENT'}); }
var chan = ctx.channels[c.channel];
if (!chan) { return void cb({error: 'ENOCHAN'}); }
cb();
chan.history.forEach(function (msg) {
ctx.emit('MESSAGE', {
msg: msg,
validateKey: chan.validateKey
}, [client]);
});
ctx.emit('HISTORY_SYNCED', {}, [client]);
};
var openChannel = function (ctx, obj, client, cb) {
var channel = obj.channel;
var padChan = obj.padChan;
var network = ctx.store.network;
var first = true;
var c = ctx.clients[client];
if (!c) {
c = ctx.clients[client] = {
channel: channel,
};
} else {
return void cb();
}
var chan = ctx.channels[channel];
if (chan) {
// This channel is already open in another tab
// ==> Use our netflux ID to create our client ID
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
getHistory(ctx, client, function () {
ctx.emit('READY', chan.clients, [client]);
});
// ==> And push the new tab to the list
chan.clients.push(client);
return void cb();
}
var txid = Math.floor(Math.random() * 1000000);
var onOpen = function (wc) {
ctx.channels[channel] = ctx.channels[channel] || {
history: [],
validateKey: obj.validateKey
};
chan = ctx.channels[channel];
chan.padChan = padChan;
// Create our client ID using the netflux ID
if (!c.id) { c.id = wc.myID + '-' + client; }
// If this is a reconnect, we have a new netflux ID so we're going to fix
// all our client IDs.
if (chan.clients) {
chan.clients.forEach(function (cl) {
if (ctx.clients[cl]) {
ctx.clients[cl].id = wc.myID + '-' + cl;
}
});
}
wc.on('join', function () {
});
wc.on('leave', function () {
});
wc.on('message', function (msg) {
chan.history.push(msg);
ctx.emit('MESSAGE', {
msg: msg,
validateKey: chan.validateKey
}, chan.clients);
});
chan.wc = wc;
chan.sendMsg = function (msg, cb) {
cb = cb || function () {};
var hash = msg.slice(0, 64);
wc.bcast(msg).then(function () {
chan.history.push(msg);
chan.lastKnownHash = hash;
cb();
}, function (err) {
cb({error: err});
});
};
if (first) {
chan.clients = [client];
chan.lastCpHash = obj.lastCpHash;
first = false;
cb();
}
var hk = network.historyKeeper;
var cfg = {
txid: txid,
lastKnownHash: chan.lastKnownHash || chan.lastCpHash,
metadata: {
validateKey: obj.validateKey,
owners: obj.owners,
expire: obj.expire
}
};
var msg = ['GET_HISTORY', wc.id, cfg];
// Add the validateKey if we are the channel creator and we have a validateKey
if (hk) {
network.sendto(hk, JSON.stringify(msg)).then(function () {
}, function (err) {
console.error(err);
});
}
};
network.on('message', function (msg, sender) {
if (!ctx.channels[channel]) { return; }
var hk = network.historyKeeper;
if (sender !== hk) { return; }
// Parse the message
var parsed;
try {
parsed = JSON.parse(msg);
} catch (e) {}
if (!parsed) { return; }
// If there is a txid, make sure it's ours or abort
if (parsed.txid && parsed.txid !== txid) { return; }
// Keep only metadata messages for the current channel
if (parsed.channel && parsed.channel !== channel) { return; }
// Ignore the metadata message
if (parsed.validateKey && parsed.channel) {
if (!chan.validateKey) {
chan.validateKey = parsed.validateKey;
}
return;
}
// End of history: emit READY
if (parsed.state && parsed.state === 1 && parsed.channel) {
ctx.emit('READY', chan.clients, chan.clients);
return;
}
if (parsed.error && parsed.channel) { return; }
// If there is a txid, make sure it's ours or abort
if (Array.isArray(parsed) && parsed[0] && parsed[0] !== txid) {
return;
}
msg = parsed[4];
// Keep only the history for our channel
if (parsed[3] !== channel) { return; }
var hash = msg.slice(0,64);
if (hash === chan.lastKnownHash || hash === chan.lastCpHash) { return; }
chan.lastKnownHash = hash;
ctx.emit('MESSAGE', {
msg: msg,
}, chan.clients);
chan.history.push(msg);
});
network.join(channel).then(onOpen, function (err) {
return void cb({error: err});
});
network.on('reconnect', function () {
if (!ctx.channels[channel]) { return; }
network.join(channel).then(onOpen, function (err) {
console.error(err);
});
});
};
var updateHash = function (ctx, data, clientId, cb) {
var c = ctx.clients[clientId];
if (!c) { return void cb({ error: 'NOT_IN_CHANNEL' }); }
var chan = ctx.channels[c.channel];
if (!chan) { return void cb({ error: 'INVALID_CHANNEL' }); }
var hash = data;
var index = -1;
chan.history.some(function (msg, idx) {
if (msg.slice(0,64) === hash) {
index = idx + 1;
return true;
}
});
if (index !== -1) {
chan.history = chan.history.slice(index);
}
cb();
};
var sendMessage = function (ctx, data, clientId, cb) {
var c = ctx.clients[clientId];
if (!c) { return void cb({ error: 'NOT_IN_CHANNEL' }); }
var chan = ctx.channels[c.channel];
if (!chan) { return void cb({ error: 'INVALID_CHANNEL' }); }
// Prepare the callback: broadcast the message to the other local tabs
// if the message is sent
var _cb = function (obj) {
if (obj && obj.error) { return void cb(obj); }
ctx.emit('MESSAGE', {
msg: data.msg
}, chan.clients.filter(function (cl) {
return cl !== clientId;
}));
cb();
};
// Send the message
if (data.isCp) {
return void chan.sendMsg(data.isCp, _cb);
}
chan.sendMsg(data.msg, _cb);
};
var reencrypt = function (ctx, data, cId, cb) {
var channel = data.channel;
var network = ctx.store.network;
var onOpen = function (wc) {
var hk = network.historyKeeper;
var cfg = {
metadata: data.metadata
};
var msg = ['GET_HISTORY', wc.id, cfg];
network.sendto(hk, JSON.stringify(msg));
data.msgs.forEach(function (msg) {
wc.bcast(msg);
});
wc.leave();
cb();
};
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]) === 'object') {
isNew = response[0].isNew;
} else {
cb({error: 'INVALID_RESPONSE'});
}
if (!isNew) { return void cb({error: 'EEXISTS'}); }
// Channel is new: we can push our reencrypted history
network.join(channel).then(onOpen, function (err) {
return void cb({error: err});
});
});
};
var leaveChannel = function (ctx, padChan) {
// Leave channel and prevent reconnect when we leave a pad
Object.keys(ctx.channels).some(function (ooChan) {
var channel = ctx.channels[ooChan];
if (channel.padChan !== padChan) { return; }
if (channel.wc) { channel.wc.leave(); }
delete ctx.channels[ooChan];
return true;
});
};
// Remove the client from all its channels when a tab is closed
var removeClient = function (ctx, clientId) {
var filter = function (c) {
return c !== clientId;
};
// Remove the client from our channels
var chan;
for (var k in ctx.channels) {
chan = ctx.channels[k];
chan.clients = chan.clients.filter(filter);
if (chan.clients.length === 0) {
if (chan.wc) { chan.wc.leave(); }
delete ctx.channels[k];
}
}
if (ctx.clients[clientId]) {
var oldChannel = ctx.clients[clientId].channel;
var oldChan = ctx.channels[oldChannel];
if (oldChan) {
ctx.emit('LEAVE', {id: clientId}, [oldChan.clients[0]]);
}
delete ctx.clients[clientId];
}
};
OO.init = function (store, emit) {
var oo = {};
var ctx = {
store: store,
emit: emit,
channels: {},
clients: {}
};
oo.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
oo.leavePad = function (padChan) {
leaveChannel(ctx, padChan);
};
oo.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SEND_MESSAGE') {
return void sendMessage(ctx, data, clientId, cb);
}
if (cmd === 'UPDATE_HASH') {
return void updateHash(ctx, data, clientId, cb);
}
if (cmd === 'OPEN_CHANNEL') {
return void openChannel(ctx, data, clientId, cb);
}
if (cmd === 'GET_HISTORY') {
return void getHistory(ctx, clientId, cb);
}
if (cmd === 'REENCRYPT') {
return void reencrypt(ctx, data, clientId, cb);
}
};
return oo;
};
return OO;
};
if (typeof(module) !== 'undefined' && module.exports) {
// Code from customize can't be laoded directly in the build
module.exports = factory();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([], factory);
} else {
// unsupported initialization
}
})();

View File

@ -0,0 +1,185 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Util, Hash, Constants, Realtime,
Listmap, Crypto, ChainPad) => {
var Profile = {};
var initializeProfile = function (ctx, cb) {
var profile = ctx.profile;
if (!profile.edit || !profile.view) {
var hash = Hash.createRandomHash('profile');
var secret = Hash.getSecrets('profile', hash);
ctx.pinPads([secret.channel], function (res) {
if (res.error) {
return void cb(res.error);
}
profile.edit = Hash.getEditHashFromKeys(secret);
profile.view = Hash.getViewHashFromKeys(secret);
setTimeout(cb);
});
return;
}
setTimeout(cb);
};
var openChannel = function (ctx) {
var profile = ctx.profile;
var secret = Hash.getSecrets('profile', profile.edit);
var crypto = Crypto.createEncryptor(secret.keys);
var cfg = {
data: {},
network: ctx.store.network,
channel: secret.channel,
crypto: crypto,
owners: [ctx.store.proxy.edPublic],
ChainPad: ChainPad,
validateKey: secret.keys.validateKey || undefined,
userName: 'profile',
classic: true
};
var lm = Listmap.create(cfg);
lm.proxy.on('create', function () {
}).on('ready', function () {
lm.proxy.name = ctx.store.proxy[Constants.displayNameKey] || "";
ctx.listmap = lm;
if (!lm.proxy.curvePublic) {
lm.proxy.curvePublic = ctx.store.proxy.curvePublic;
}
if (!lm.proxy.notifications) {
lm.proxy.notifications = Util.find(ctx.store.proxy, ['mailboxes', 'notifications', 'channel']);
}
if (!lm.proxy.edPublic) {
lm.proxy.edPublic = ctx.store.proxy.edPublic;
}
if (ctx.onReadyHandlers.length) {
ctx.onReadyHandlers.forEach(function (f) {
try {
f(lm.proxy);
} catch (e) { console.error(e); }
});
ctx.onReadyHandlers = [];
}
}).on('change', [], function () {
ctx.emit('UPDATE', lm.proxy, ctx.clients);
});
};
var setName = function (ctx, value) {
ctx.listmap.proxy.name = value;
Realtime.whenRealtimeSyncs(ctx.listmap.realtime, function () {
if (!ctx.listmap) { return; }
ctx.emit('UPDATE', ctx.listmap.proxy, ctx.clients);
});
};
var subscribe = function (ctx, data, cId, cb) {
// Subscribe to new notifications
var idx = ctx.clients.indexOf(cId);
if (idx === -1) {
ctx.clients.push(cId);
}
if (ctx.listmap) {
return void cb(ctx.listmap.proxy);
}
ctx.onReadyHandlers.push(function (proxy) {
cb(proxy);
});
};
var setValue = function (ctx, data, cId, cb) {
var key = data.key;
var value = data.value;
if (!key) { return; }
ctx.listmap.proxy[key] = value;
Realtime.whenRealtimeSyncs(ctx.listmap.realtime, function () {
ctx.emit('UPDATE', ctx.listmap.proxy, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
cb(ctx.listmap.proxy);
});
};
var removeClient = function (ctx, cId) {
var idx = ctx.clients.indexOf(cId);
if (idx !== -1) { ctx.clients.splice(idx, 1); }
};
Profile.init = function (cfg, waitFor, emit) {
var profile = {};
var store = cfg.store;
if (!store.loggedIn || !store.proxy.edPublic) { return; }
var ctx = {
store: store,
pinPads: cfg.pinPads,
updateMetadata: cfg.updateMetadata,
emit: emit,
onReadyHandlers: [],
clients: [],
};
ctx.profile = store.proxy.profile = store.proxy.profile || {};
initializeProfile(ctx, waitFor(function (err) {
if (err) { return; }
openChannel(ctx);
}));
profile.setName = function (value) {
setName(ctx, value);
};
profile.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
profile.update = function () {
if (!ctx.listmap) { return; }
ctx.emit('UPDATE', ctx.listmap.proxy, ctx.clients);
};
profile.execCommand = function (clientId, obj, cb) {
console.log(obj);
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {
return void subscribe(ctx, data, clientId, cb);
}
if (cmd === 'SET') {
return void setValue(ctx, data, clientId, cb);
}
};
return profile;
};
return Profile;
};
if (typeof(module) !== 'undefined' && module.exports) {
// Code from customize can't be laoded directly in the build
module.exports = factory(
undefined,
require('../../common/common-util'),
require('../../common/common-hash'),
require('../../common/common-constants'),
require('../../common/common-realtime'),
require('chainpad-listmap'),
require('chainpad-crypto'),
require('chainpad')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
'chainpad-listmap',
'/components/chainpad-crypto/crypto.js',
'/components/chainpad/chainpad.dist.js',
], factory);
} else {
// unsupported initialization
}
})();

File diff suppressed because it is too large Load Diff

2281
src/worker/modules/team.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
import nacl from 'tweetnacl';
import { Module, ModuleObject } from '../types'
export interface TestModuleObject extends ModuleObject {
toBase64: (str: string) => string
}
const TestModule: Module<TestModuleObject> = {
init: (config, cb) => {
let myTest = {};
cb();
return {
removeClient: (clientId) => {
console.log('Remove client', clientId);
},
execCommand: (clientId, obj, cb) => {
console.log('Exex command', clientId, obj);
cb();
},
toBase64: (str) => {
return nacl.util.encodeBase64(nacl.util.decodeUTF8(str));
}
}
}
};
export { TestModule }

93
src/worker/store.ts Normal file
View File

@ -0,0 +1,93 @@
import nacl from 'tweetnacl/nacl-fast';
import { Module } from './types';
import { TestModule, TestModuleObject } from './modules/test';
import * as Util from '../common/common-util.js';
import * as Hash from '../common/common-hash.js';
import * as Feedback from '../common/common-feedback.js';
import * as Realtime from '../common/common-realtime.js';
import * as Messaging from '../common/common-messaging.js';
import * as Constants from '../common/common-constants.js';
import * as Credential from '../common/common-credential.js';
import * as ProxyManager from '../common/proxy-manager.js';
import * as UO from '../common/user-object.js';
import * as UOSetter from '../common/user-object-setter.js';
import * as Pinpad from '../common/pinpad.js';
import * as PadTypes from '../common/pad-types.js';
import * as NetworkConfig from '../common/network-config.js';
import * as LoginBlock from '../common/login-block.js';
import * as Migrate from './components/migrate-user-object.js';
// Modules
import * as Mailbox from './modules/mailbox.js';
import * as Cursor from './modules/cursor.js';
import * as Support from './modules/support.js';
import * as Integration from './modules/integration.js';
import * as OnlyOffice from './modules/onlyoffice.js';
import * as Profile from './modules/profile.js';
import * as Team from './modules/team.js';
import * as Messenger from './modules/messenger.js';
import * as History from './modules/history.js';
import * as Calendar from './modules/calendar.js';
interface StoreConfig {
ApiConfig: any,
AppConfig: any,
Broadcast: any,
Messages: any
}
let start = (cfg: StoreConfig):void => {
[
Feedback,
UO,
Constants,
ProxyManager,
NetworkConfig,
Migrate,
LoginBlock,
PadTypes,
Credential,
Cursor,
Support,
Calendar
].forEach(dep => {
if (typeof(dep.setCustomize) === "function") {
dep.setCustomize(cfg);
}
});
const AppConfig = cfg.AppConfig;
let test = TestModule.init({
emit: () => {}
}, () => {
console.log('Test initialized');
});
let b64 = test.toBase64('pewpewPEZPEZ');
console.error(b64);
console.error(AppConfig.minimumPasswordLength, AppConfig.degradedLimit);
console.error(Hash.getSecrets);
console.error(Util.mkEvent);
console.error(Messaging.createData);
console.log('UO & UOSetter:');
console.error(UO.getDefaultName({type:'pad'}));
console.error(UO.getDefaultName);
console.error(UOSetter.init);
console.error(ProxyManager.createInner);
console.log('Pinpad');
console.error(Pinpad.create);
console.error(LoginBlock.getBlockUrl);
console.error(Mailbox.init, Cursor.init, Support.init, Integration.init);
console.error(Profile.init, OnlyOffice.init, Team.init, Messenger.init);
console.error(History.init, Calendar.init);
};
export {
start
};

28
src/worker/types.ts Normal file
View File

@ -0,0 +1,28 @@
// Default CryptPad worker module, extended with
// specific methods for each module
type Callback = (...args: any[]) => void
declare module globalThis {
let CryptPad_Messages: any;
let CryptPad_AppConfig: any;
let window: any;
}
export type ModuleConfig = {
emit: Function
}
export type CommandData = {
cmd: string,
data: any
}
export interface ModuleObject {
removeClient: (clientId: string) => void
execCommand: (clientId: string, obj: CommandData, cb: Callback) => void
}
export interface Module<T> {
init: (config: ModuleConfig, cb: Callback) => T
}

View File

@ -1,4 +1,4 @@
const Messages = require('./_src/messages');
const Messages = require('./src/messages');
const AppConfig = require('./customize/application_config');
const App = require('./_build/worker.bundle');
const Http = require('node:http');

View File

@ -6,6 +6,6 @@
"moduleResolution": "node",
"outDir": "./_build"
},
"include": ["_src/**/*"]
"include": ["src/**/*"]
}

View File

@ -2,9 +2,8 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/common/common-util.js',
], function (Util) {
(() => {
const factory = (Util) => {
var Rec = {};
var debug = function () {};
@ -896,4 +895,13 @@ define([
return Rec;
});
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory('../../common/common-util');
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define(['/common/common-util.js'], factory);
} else {
// unsupported initialization
}
})();

View File

@ -17,16 +17,16 @@ define([
'/common/pinpad.js', // OK
'/common/outer/cache-store.js', // OK
'/common/outer/sharedfolder.js', // OK
'/common/outer/cursor.js',
'/common/outer/support.js',
'/common/outer/integration.js',
'/common/outer/onlyoffice.js',
'/common/outer/cursor.js', // OK
'/common/outer/support.js', // OK
'/common/outer/integration.js', // OK
'/common/outer/onlyoffice.js', // OK
'/common/outer/mailbox.js', // OK
'/common/outer/profile.js',
'/common/outer/team.js',
'/common/outer/messenger.js',
'/common/outer/history.js',
'/common/outer/calendar.js',
'/common/outer/profile.js', // OK
'/common/outer/team.js', // OK
'/common/outer/messenger.js', // OK
'/common/outer/history.js', // OK
'/common/outer/calendar.js', // OK
'/common/outer/login-block.js', // OK
'/common/outer/network-config.js', // OK
'/customize/application_config.js', // OK