mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-12 19:49:59 +05:00
Convert missing modules
This commit is contained in:
parent
503a7a08b5
commit
df789f8284
@ -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",
|
||||
|
||||
@ -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
210
src/common/cache-store.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
47
src/common/common-constants.js
Normal file
47
src/common/common-constants.js
Normal 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
|
||||
}
|
||||
})();
|
||||
|
||||
116
src/common/common-credential.js
Normal file
116
src/common/common-credential.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}());
|
||||
79
src/common/common-feedback.js
Normal file
79
src/common/common-feedback.js
Normal 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
766
src/common/common-hash.js
Normal 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 : {}));
|
||||
161
src/common/common-messaging.js
Normal file
161
src/common/common-messaging.js
Normal 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
|
||||
}
|
||||
})();
|
||||
36
src/common/common-realtime.js
Normal file
36
src/common/common-realtime.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
110
src/common/common-signing-keys.js
Normal file
110
src/common/common-signing-keys.js
Normal 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
775
src/common/common-util.js
Normal 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 ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[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
238
src/common/cryptget.js
Normal 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
130
src/common/http-command.js
Normal 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
245
src/common/login-block.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
42
src/common/network-config.js
Normal file
42
src/common/network-config.js
Normal 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
201
src/common/notify.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
17
src/common/onlyoffice/current-version.js
Normal file
17
src/common/onlyoffice/current-version.js
Normal 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
64
src/common/pad-types.js
Normal 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
235
src/common/pinpad.js
Normal 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
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
422
src/common/rpc.js
Normal 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
|
||||
}
|
||||
}());
|
||||
1006
src/common/user-object-setter.js
Normal file
1006
src/common/user-object-setter.js
Normal file
File diff suppressed because it is too large
Load Diff
997
src/common/user-object.js
Normal file
997
src/common/user-object.js
Normal 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
15
src/messages.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
|
||||
122
src/worker/components/invitation.js
Normal file
122
src/worker/components/invitation.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}());
|
||||
1006
src/worker/components/mailbox-handlers.js
Normal file
1006
src/worker/components/mailbox-handlers.js
Normal file
File diff suppressed because it is too large
Load Diff
544
src/worker/components/migrate-user-object.js
Normal file
544
src/worker/components/migrate-user-object.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
908
src/worker/components/recurrence.js
Normal file
908
src/worker/components/recurrence.js
Normal 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
|
||||
}
|
||||
})();
|
||||
958
src/worker/components/roster.js
Normal file
958
src/worker/components/roster.js
Normal 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
|
||||
}
|
||||
}());
|
||||
417
src/worker/components/sharedfolder.js
Normal file
417
src/worker/components/sharedfolder.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
1248
src/worker/modules/calendar.js
Normal file
1248
src/worker/modules/calendar.js
Normal file
File diff suppressed because it is too large
Load Diff
312
src/worker/modules/cursor.js
Normal file
312
src/worker/modules/cursor.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
279
src/worker/modules/history.js
Normal file
279
src/worker/modules/history.js
Normal 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
|
||||
}
|
||||
})();
|
||||
219
src/worker/modules/integration.js
Normal file
219
src/worker/modules/integration.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
707
src/worker/modules/mailbox.js
Normal file
707
src/worker/modules/mailbox.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
1146
src/worker/modules/messenger.js
Normal file
1146
src/worker/modules/messenger.js
Normal file
File diff suppressed because it is too large
Load Diff
361
src/worker/modules/onlyoffice.js
Normal file
361
src/worker/modules/onlyoffice.js
Normal 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
|
||||
}
|
||||
})();
|
||||
185
src/worker/modules/profile.js
Normal file
185
src/worker/modules/profile.js
Normal 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
|
||||
}
|
||||
|
||||
})();
|
||||
1446
src/worker/modules/support.js
Normal file
1446
src/worker/modules/support.js
Normal file
File diff suppressed because it is too large
Load Diff
2281
src/worker/modules/team.js
Normal file
2281
src/worker/modules/team.js
Normal file
File diff suppressed because it is too large
Load Diff
32
src/worker/modules/test.ts
Normal file
32
src/worker/modules/test.ts
Normal 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
93
src/worker/store.ts
Normal 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
28
src/worker/types.ts
Normal 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
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -6,6 +6,6 @@
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./_build"
|
||||
},
|
||||
"include": ["_src/**/*"]
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
})();
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user