Move pad functions to a new component

This commit is contained in:
yflory 2025-04-18 15:06:53 +02:00
parent 900ab02404
commit c7d8750bc0
9 changed files with 592 additions and 510 deletions

View File

@ -617,7 +617,7 @@ const factory = (UserObject, Util, Hash,
if (data.password) { folderData.password = data.password; }
if (data.owned) { folderData.owners = [Env.edPublic]; }
}).nThen(function (waitFor) {
Env.Store.getPadMetadata(null, {
Env.Store.pad.getMetadata(null, {
channel: folderData.channel
}, waitFor(function (obj) {
if (obj && (obj.error || obj.rejected)) {
@ -669,7 +669,7 @@ const factory = (UserObject, Util, Hash,
}
if (data.folderData) {
// If we're importing a folder, check its serverside metadata
Env.Store.getPadMetadata(null, { channel: folderData.channel }, function (md) {
Env.Store.pad.getMetadata(null, { channel: folderData.channel }, function (md) {
var fData = Env.user.proxy[UserObject.SHARED_FOLDERS][id];
if (md.owners) { fData.owners = md.owners; }
if (md.expire) { fData.expire = +md.expire; }

View File

@ -5,7 +5,7 @@
const factory = (Sortify, UserObject, ProxyManager,
Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging, Pinpad, Rpc, Merge, Cache,
SF, AccountTS, DriveTS, Cursor,
SF, AccountTS, DriveTS, PadTS, Cursor,
Support, Integration, OnlyOffice,
Mailbox, Profile, Team, Messenger, History,
Calendar, BadgeTS, Block, NetConfig,
@ -14,6 +14,7 @@ const factory = (Sortify, UserObject, ProxyManager,
const Account = AccountTS.Account;
const Drive = DriveTS.Drive;
const Pad = PadTS.Pad;
const Badge = BadgeTS.Badge;
const window = globalThis;
globalThis.nacl = globalThis.nacl || Crypto.Nacl;
@ -24,7 +25,6 @@ const factory = (Sortify, UserObject, ProxyManager,
const Saferphore = Util.Saferphore;
var onReadyEvt = Util.mkEvent(true);
var onCacheReadyEvt = Util.mkEvent(true);
var onJoinedEvt = Util.mkEvent(true);
var onPadRejectedEvt = Util.mkEvent(true);
const setCustomize = data => {
@ -63,16 +63,20 @@ const factory = (Sortify, UserObject, ProxyManager,
Store.onReadyEvt = onReadyEvt;
Store.pad = Pad.init({
Store, store, postMessage, broadcast
});
// Drive clients
var driveEventClients = [];
var sendDriveEvent = function (q, data, sender) {
var sendDriveEvent = store.sendDriveEvent = function (q, data, sender) {
driveEventClients.forEach(function (cId) {
if (cId === sender) { return; }
postMessage(cId, q, data);
});
};
var getStore = function (teamId) {
var getStore = Store.getStore = function (teamId) {
if (!teamId) { return store; }
try {
var teams = store.modules['team'];
@ -392,38 +396,6 @@ const factory = (Sortify, UserObject, ProxyManager,
});
};
var myDeletions = {};
Store.removeOwnedChannel = function (clientId, data, cb) {
// "data" used to be a string (channelID), now it can also be an object
// data.force tells us we can safely remove the drive ID
var channel = data;
var force = false;
var teamId;
var reason;
if (data && typeof(data) === "object") {
channel = data.channel;
force = data.force;
teamId = data.teamId;
reason = data.reason;
}
if (channel === store.driveChannel && !force) {
return void cb({error: 'User drive removal blocked!'});
}
var s = getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
// If this channel is loaded, remember that we deleted it ourselves
if (Store.channels[channel]) { myDeletions[channel] = true; }
s.rpc.removeOwnedChannel(channel, function (err) {
if (err) { delete myDeletions[channel]; }
cb({error:err});
}, reason);
};
var arePinsSynced = function (cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
@ -855,7 +827,7 @@ const factory = (Sortify, UserObject, ProxyManager,
}));
}).nThen(function (_w) {
if (otherOwners) {
Store.setPadMetadata(null, {
Store.pad.setMetadata(null, {
channel: c,
command: 'RM_OWNERS',
value: [edPublic],
@ -918,7 +890,7 @@ const factory = (Sortify, UserObject, ProxyManager,
}).nThen(function (waitFor) {
// Delete Drive
store.ownDeletion = true;
Store.removeOwnedChannel(clientId, {
Store.pad.destroy(clientId, {
channel: store.driveChannel,
force: true
}, waitFor());
@ -1200,7 +1172,7 @@ const factory = (Sortify, UserObject, ProxyManager,
return void cb({ notStored: true });
}
var channelData = Store.channels && Store.channels[channel];
var channelData = Store.pad.getChannels()[channel];
var owners;
if (channelData && channelData.wc && channel === channelData.wc.id) {
@ -1674,8 +1646,6 @@ const factory = (Sortify, UserObject, ProxyManager,
/////////////////////// PAD //////////////////////////////////////
//////////////////////////////////////////////////////////////////
var channels = Store.channels = store.channels = {};
Store.getSnapshot = function (clientId, data, cb) {
Store.getHistoryRange(clientId, {
cpCount: 1,
@ -1684,55 +1654,6 @@ const factory = (Sortify, UserObject, ProxyManager,
}, cb);
};
var getVersionHash = function (clientId, data) {
var validateKey;
var fakeNetflux = Hash.createChannelId();
nThen(function (waitFor) {
Store.getPadMetadata(null, {
channel: data.channel
}, waitFor(function (md) {
if (md && md.rejected) {
postMessage(clientId, "PAD_ERROR", {type: "ERESTRICTED"});
waitFor.abort();
return;
}
validateKey = md.validateKey;
}));
}).nThen(function () {
Store.getHistoryRange(clientId, {
cpCount: 1,
channel: data.channel,
lastKnownHash: data.versionHash
}, function (obj) {
if (obj && obj.error) {
postMessage(clientId, "PAD_ERROR", obj.error);
return;
}
var msgs = obj.messages || [];
if (msgs.length && msgs[msgs.length - 1].serverHash !== data.versionHash) {
postMessage(clientId, "PAD_ERROR", {type: "HASH_NOT_FOUND"});
return;
}
postMessage(clientId, "PAD_CONNECT", {
myID: fakeNetflux,
id: data.channel,
members: [fakeNetflux]
});
(obj.messages || []).forEach(function (data) {
postMessage(clientId, "PAD_MESSAGE", {
msg: data.msg,
time: data.time,
user: fakeNetflux.slice(0,16), // fake history keeper to avoid validate
});
});
if (validateKey && store.messenger) {
store.messenger.storeValidateKey(data.channel, validateKey);
}
postMessage(clientId, "PAD_READY");
});
});
};
Store.onRejected = function (allowed, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Array.isArray(allowed)) { return void cb('ERESTRICTED'); }
@ -1783,226 +1704,6 @@ const factory = (Sortify, UserObject, ProxyManager,
});
};
Store.joinPad = function (clientId, data) {
if (data.versionHash) {
return void getVersionHash(clientId, data);
}
if (!Hash.isValidChannel(data.channel)) {
return void postMessage(clientId, "PAD_ERROR", 'INVALID_CHAN');
}
var isNew = typeof channels[data.channel] === "undefined";
var channel = channels[data.channel] = channels[data.channel] || {
queue: [],
data: {},
clients: [],
bcast: function (cmd, data, notMe) {
channel.clients.forEach(function (cId) {
if (cId === notMe) { return; }
postMessage(cId, cmd, data);
});
},
history: [],
pushHistory: function (msg, isCp) {
if (isCp) {
// the current message is a checkpoint.
// push it to your worker's history, prepending it with cp|
// cp| and anything else related to checkpoints has already
// been stripped by chainpad-netflux-worker or within async store
// when the message was outgoing.
channel.history.push('cp|' + msg);
// since the latest message is a checkpoint, we are able to drop
// some of the older history, but we can't rely on checkpoints being
// correct, as they might be checkpoints from different forks
var i;
for (i = channel.history.length - 101; i > 0; i--) {
if (/^cp\|/.test(channel.history[i])) { break; }
}
channel.history = channel.history.slice(Math.max(i, 0));
return;
}
channel.history.push(msg);
}
};
if (channel.clients.indexOf(clientId) === -1) {
channel.clients.push(clientId);
}
if (!isNew && channel.wc) {
postMessage(clientId, "PAD_CONNECT", {
myID: channel.wc.myID,
id: channel.wc.id,
members: channel.wc.members
});
channel.wc.members.forEach(function (m) {
postMessage(clientId, "PAD_JOIN", m);
});
channel.history.forEach(function (msg) {
postMessage(clientId, "PAD_MESSAGE", {
msg: CpNetflux.removeCp(msg),
user: channel.wc.myID,
validateKey: channel.data.validateKey
});
});
postMessage(clientId, "PAD_READY");
return;
}
var onError = function (err) {
// If it's a deletion started from this worker, different UI message
if (err && err.type === "EDELETED" && myDeletions[data.channel]) {
delete myDeletions[channel];
err.ownDeletion = true;
}
channel.bcast("PAD_ERROR", err);
if (err && err.type === "EDELETED" && Cache && Cache.clearChannel) {
Cache.clearChannel(data.channel);
}
// If this is a DELETED, EXPIRED or RESTRICTED pad, leave the channel
if (["EDELETED", "EEXPIRED", "ERESTRICTED"].indexOf(err.type) === -1) { return; }
Store.leavePad(null, data, function () {});
};
var conf = {
Cache: store.neverCache ? undefined : Cache,
priority: 1,
onCacheStart: function () {
postMessage(clientId, "PAD_CACHE");
},
onCacheReady: function () {
postMessage(clientId, "PAD_CACHE_READY");
},
onReady: function (pad) {
var padData = pad.metadata || {};
channel.data = padData;
if (padData && padData.validateKey && store.messenger) {
store.messenger.storeValidateKey(data.channel, padData.validateKey);
}
postMessage(clientId, "PAD_READY", pad.noCache);
/*
if (!store.proxy) {
postMessage(clientId, "PAD_READY", pad.noCache);
return;
}
onReadyEvt.reg(function () {
postMessage(clientId, "PAD_READY", pad.noCache);
});
*/
},
onMessage: function (m, user, validateKey, isCp, hash) {
channel.lastHash = hash;
channel.pushHistory(m, isCp);
channel.bcast("PAD_MESSAGE", {
user: user,
msg: m,
validateKey: validateKey
});
},
onJoin: function (m) {
channel.bcast("PAD_JOIN", m);
},
onLeave: function (m) {
channel.bcast("PAD_LEAVE", m);
},
onError: onError,
onChannelError: onError,
onRejected: Store.onRejected,
onConnectionChange: function (info) {
if (!info.state) {
channel.bcast("PAD_DISCONNECT");
}
},
onMetadataUpdate: function (metadata) {
channel.data = metadata || {};
getAllStores().forEach(function (s) {
var allData = s.manager.findChannel(data.channel, true);
allData.forEach(function (obj) {
obj.data.owners = metadata.owners;
obj.data.atime = +new Date();
if (metadata.expire) {
obj.data.expire = +metadata.expire;
}
});
var send = s.sendEvent || sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
});
});
channel.bcast("PAD_METADATA", metadata);
},
crypto: {
// The encryption and decryption is done in the outer window.
// This async-store only deals with already encrypted messages.
encrypt: function (m) { return m; },
decrypt: function (m) { return m; }
},
noChainPad: true,
channel: data.channel,
metadata: data.metadata,
network: store.network || store.networkPromise,
websocketURL: NetConfig.getWebsocketURL(),
onInit: function () {
onJoinedEvt.fire();
},
//readOnly: data.readOnly,
onConnect: function (wc, sendMessage) {
channel.sendMessage = function (msg, cId, cb) {
// Send to server
sendMessage(msg, function (err) {
if (err) {
return void cb({ error: err });
}
// Broadcast to other tabs
channel.lastHash = msg.slice(0,64);
channel.pushHistory(CpNetflux.removeCp(msg), /^cp\|/.test(msg));
channel.bcast("PAD_MESSAGE", {
user: wc.myID,
msg: CpNetflux.removeCp(msg),
validateKey: channel.data.validateKey
}, cId);
cb();
});
};
channel.wc = wc;
channel.queue.forEach(function (data) {
channel.sendMessage(data.message, clientId);
});
channel.queue = [];
channel.bcast("PAD_CONNECT", {
myID: wc.myID,
id: wc.id,
members: wc.members
});
}
};
channel.cpNf = CpNetflux.start(conf);
};
Store.leavePad = function (clientId, data, cb) {
var channel = channels[data.channel];
if (!channel || !channel.cpNf) { return void cb ({error: 'EINVAL'}); }
Store.dropChannel(data.channel);
cb();
};
Store.sendPadMsg = function (clientId, data, cb) {
var msg = data.msg;
var channel = channels[data.channel];
if (!channel) {
return; }
if (!channel.wc) {
channel.queue.push(msg);
return void cb();
}
channel.sendMessage(msg, clientId, cb);
};
Store.corruptedCache = function (clientId, channel) {
var chan = channels[channel];
if (!chan || !chan.cpNf) { return; }
Cache.clearChannel(channel);
if (!chan.cpNf.resetCache) { return; }
chan.cpNf.resetCache();
};
// Unpin and pin the new channel in all team when changing a pad password
Store.changePadPasswordPin = function (clientId, data, cb) {
var oldChannel = data.oldChannel;
@ -2079,15 +1780,6 @@ const factory = (Sortify, UserObject, ProxyManager,
cb();
};
Store.getLastHash = function (clientId, data, cb) {
var chan = channels[data.channel];
if (!chan) { return void cb({error: 'ENOCHAN'}); }
if (!chan.lastHash) { return void cb({error: 'EINVAL'}); }
cb({
hash: chan.lastHash
});
};
// Delete a pad received with a burn after reading URL
var notifyOwnerPadRemoved = function (data, obj) {
@ -2147,7 +1839,7 @@ const factory = (Sortify, UserObject, ProxyManager,
edPrivate: Hash.encodeBase64(pair.secretKey)
}, function (e, rpc) {
if (e) { return void console.error(e); }
Store.getPadMetadata(null, {
Store.pad.getMetadata(null, {
channel: channel
}, function (md) {
rpc.removeOwnedChannel(channel, function (err) {
@ -2162,75 +1854,6 @@ const factory = (Sortify, UserObject, ProxyManager,
}
};
// Fetch the latest version of the metadata on the server and return it.
// If the pad is stored in our drive, update the local values of "owners" and "expire"
Store.getPadMetadata = function (clientId, data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (store.offline || !store.anon_rpc) { return void cb({ error: 'OFFLINE' }); }
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (data.channel.length !== 32) { return void cb({ error: 'EINVAL'}); }
if (!Hash.isValidChannel(data.channel)) {
Feedback.send('METADATA_INVALID_CHAN');
return void cb({ error: 'EINVAL' });
}
store.anon_rpc.send('GET_METADATA', data.channel, function (err, obj) {
if (err) { return void cb({error: err}); }
var metadata = (obj && obj[0]) || {};
cb(metadata);
// If you don't have access to the metadata, stop here
// (we can't update the local data)
if (metadata.rejected) { return; }
// Update owners and expire time in the drive
getAllStores().forEach(function (s) {
var allData = s.manager.findChannel(data.channel, true);
var changed = false;
allData.forEach(function (obj) {
if (Sortify(obj.data.owners) !== Sortify(metadata.owners)) {
changed = true;
}
obj.data.owners = metadata.owners;
obj.data.atime = +new Date();
if (metadata.expire) {
obj.data.expire = +metadata.expire;
}
});
// If we had to change the "owners" field, redraw the drive UI
if (!changed) { return; }
var send = s.sendEvent || sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
});
});
});
};
Store.setPadMetadata = function (clientId, data, cb) {
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (!data.command) { return void cb({ error: 'EINVAL' }); }
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
var otherChannels = data.channels;
delete data.channels;
s.rpc.setMetadata(data, function (err, res) {
if (err) { return void cb({ error: err }); }
if (!Array.isArray(res) || !res.length) { return void cb({}); }
cb(res[0]);
});
// If we have other related channels, send the command for them too
if (Array.isArray(otherChannels)) {
otherChannels.forEach(function (chan) {
var _d = Util.clone(data);
_d.channel = chan;
Store.setPadMetadata(clientId, _d, function () {
});
});
}
};
Store.deleteMailboxMessage = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.anon_rpc.send('DELETE_MAILBOX_MESSAGE', data, function (e) {
@ -2593,114 +2216,21 @@ const factory = (Sortify, UserObject, ProxyManager,
s.manager.command(cmdData, cb2);
};
// Check if this is a channel that we shouldn't leave when closing the debug app
var alwaysOnline = function (chanId) {
if (!store) { return; }
// Drive
if (store.driveChannel === chanId) { return true; }
// Shared folders
if (SF.isSharedFolderChannel(chanId)) { return true; }
// Teams
if (Util.find(store, ['proxy', 'teams'])) {
var t = Util.find(store, ['proxy', 'teams']) || {};
return Object.keys(t).some(function (id) {
return t[id].channel === chanId;
});
}
// Profile
if (Util.find(store, ['proxy', 'profile', 'href'])) {
return Hash.hrefToHexChannelId(Util.find(store, ['proxy', 'profile', 'href']))
=== chanId;
}
};
var dropChannel = Store.dropChannel = function (chanId) {
console.error('Drop channel', chanId);
try {
store.messenger.leavePad(chanId);
} catch (e) { console.error(e); }
try {
store.modules['cursor'].leavePad(chanId);
} catch (e) { console.error(e); }
try {
store.modules['integration'].leavePad(chanId);
} catch (e) { console.error(e); }
try {
store.onlyoffice.leavePad(chanId);
} catch (e) { console.error(e); }
try {
if (alwaysOnline(chanId)) {
delete Store.channels[chanId];
return;
}
} catch (e) { console.error(e); }
try {
Cache.leaveChannel(chanId);
} catch (e) { console.error(e); }
if (!Store.channels[chanId]) { return; }
if (Store.channels[chanId].cpNf) {
Store.channels[chanId].cpNf.stop();
}
delete Store.channels[chanId];
};
Store._removeClient = function (clientId) {
var driveIdx = driveEventClients.indexOf(clientId);
if (driveIdx !== -1) {
driveEventClients.splice(driveIdx, 1);
}
try {
store.onlyoffice?.removeClient(clientId);
} catch (e) { console.error(e); }
try {
store.mailbox?.removeClient(clientId);
} catch (e) { console.error(e); }
store.onlyoffice?.removeClient?.(clientId);
store.mailbox?.removeClient?.(clientId);
Object.keys(store.modules).forEach(function (key) {
if (!store.modules[key]) { return; }
if (!store.modules[key].removeClient) { return; }
try {
store.modules[key].removeClient(clientId);
} catch (e) { console.error(e); }
store.modules[key]?.removeClient?.(clientId);
});
Object.keys(Store.channels).forEach(function (chanId) {
var chanIdx = Store.channels[chanId].clients.indexOf(clientId);
if (chanIdx !== -1) {
Store.channels[chanId].clients.splice(chanIdx, 1);
}
if (Store.channels[chanId].clients.length === 0) {
dropChannel(chanId);
}
});
Store.pad?.removeClient?.(clientId);
};
/*
var loadProfile = function (waitFor) {
store.profile = Profile.init({
store: store,
updateMetadata: function () {
broadcast([], "UPDATE_METADATA");
},
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
}, waitFor, function (ev, data, clients) {
clients.forEach(function (cId) {
postMessage(cId, 'PROFILE_EVENT', {
ev: ev,
data: data
});
});
});
};
*/
var loadOnlyOffice = function () {
if (store.onlyoffice) { return; }
store.onlyoffice = OnlyOffice.init(store, function (ev, data, clients) {
@ -2771,7 +2301,7 @@ const factory = (Sortify, UserObject, ProxyManager,
loadSharedFolder: loadSharedFolder,
settings: proxy.settings,
removeOwnedChannel: function (channel, cb) {
Store.removeOwnedChannel('', channel, cb);
Store.pad.destroy('', channel, cb);
},
store: store,
Store: Store
@ -3038,8 +2568,9 @@ const factory = (Sortify, UserObject, ProxyManager,
setInterval(function () {
var clients = [];
Object.keys(Store.channels).forEach(function (chanId) {
var c = Store.channels[chanId].clients;
const channels = Store.pad.getChannels();
Object.keys(channels).forEach(function (chanId) {
var c = channels[chanId].clients;
Array.prototype.push.apply(clients, c);
});
clients = Util.deduplicateString(clients);
@ -3274,7 +2805,7 @@ const factory = (Sortify, UserObject, ProxyManager,
startModules(clientId, ret, onInit);
});
});
onJoinedEvt.reg(next);
Store.pad.onJoined.reg(next);
onPadRejectedEvt.reg(next);
});
}
@ -3478,6 +3009,7 @@ module.exports = factory(
require('./components/sharedfolder'),
require('./components/account'), // .ts
require('./components/drive'), // .ts
require('./components/pad'), // .ts
require('./modules/cursor'),
require('./modules/support'),
require('./modules/integration'),

View File

@ -0,0 +1,526 @@
// SPDX-FileCopyrightText: 2025 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import nacl from 'tweetnacl/nacl-fast';
import { Pad, RpcCall, Callback } from '../types'
// node modules
import * as ChainPad from 'chainpad';
import * as Crypto from 'chainpad-crypto';
import * as CpNetflux from 'chainpad-netflux';
import nThen from 'nthen';
import Sortify from 'json.sortify';
// custom modules
import * as NetConfig from '../../common/network-config.js';
import * as Hash from '../../common/common-hash.js';
import * as Util from '../../common/common-util.js';
import * as Cache from '../../common/cache-store.js';
import * as UserObject from '../../common/user-object.js';
import * as SF from './sharedfolder.js';
const onJoinedEvt: any = Util.mkEvent(true);
const _getMetadata: Callback = (ctx, clientId, data, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
const { store, Store } = ctx;
if (store.offline || !store.anon_rpc) {
return void cb({ error: 'OFFLINE' });
}
if (!data.channel) {
return void cb({ error: 'ENOTFOUND'});
}
if (data.channel.length !== 32) {
return void cb({ error: 'EINVAL'});
}
if (!Hash.isValidChannel(data.channel)) {
return void cb({ error: 'EINVAL' });
}
store.anon_rpc.send('GET_METADATA', data.channel, (err, obj) => {
if (err) { return void cb({error: err}); }
const metadata = (obj && obj[0]) || {};
cb(metadata);
// If you don't have access to the metadata, stop here
// (we can't update the local data)
if (metadata.rejected) { return; }
// Update owners and expire time in the drive
Store.getAllStores().forEach(s => {
const allData = s.manager.findChannel(data.channel, true);
let changed = false;
allData.forEach(obj => {
if (Sortify(obj.data.owners) !== Sortify(metadata.owners)) {
changed = true;
}
obj.data.owners = metadata.owners;
obj.data.atime = +new Date();
if (metadata.expire) {
obj.data.expire = +metadata.expire;
}
});
// If we had to change the "owners" field,
// redraw the drive UI
if (!changed) { return; }
var send = s.sendEvent || store.sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
});
});
});
};
const _setMetadata: Callback = (ctx, clientId, data, cb) => {
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (!data.command) { return void cb({ error: 'EINVAL' }); }
const { Store } = ctx;
const s = Store.getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
const otherChannels = data.channels;
delete data.channels;
s.rpc.setMetadata(data, (err, res) => {
if (err) { return void cb({ error: err }); }
if (!Array.isArray(res) || !res.length) { return void cb({}); }
cb(res[0]);
});
// If we have other related channels, send the command for them too
if (Array.isArray(otherChannels)) {
otherChannels.forEach(chan => {
var _d = Util.clone(data);
_d.channel = chan;
Store.setPadMetadata(clientId, _d, () => {});
});
}
};
const _getVersionHash: Callback = (ctx, clientId, data) => {
let validateKey;
const { Store, store, postMessage } = ctx;
const channel = data.channel;
const toFind = data.versionHash;
// create fake history keeper to avoid validate
const fakeNetflux = Hash.createChannelId();
nThen(waitFor => {
_getMetadata(ctx, clientId, { channel }, waitFor(md => {
if (md && md.rejected) {
postMessage(clientId, "PAD_ERROR", {
type: "ERESTRICTED"
});
waitFor.abort();
return;
}
validateKey = md.validateKey;
}));
}).nThen(() => {
Store.getHistoryRange(clientId, {
cpCount: 1,
channel,
lastKnownHash: toFind
}, obj => {
if (obj && obj.error) {
return postMessage(clientId, "PAD_ERROR", obj.error);
}
const msgs = obj.messages || [];
if (msgs[msgs.length - 1]?.serverHash !== toFind) {
return postMessage(clientId, "PAD_ERROR", {
type: "HASH_NOT_FOUND"
});
}
postMessage(clientId, "PAD_CONNECT", {
myID: fakeNetflux,
id: channel,
members: [fakeNetflux]
});
(obj.messages || []).forEach(data => {
postMessage(clientId, "PAD_MESSAGE", {
msg: data.msg,
time: data.time,
user: fakeNetflux.slice(0,16)
});
});
if (validateKey) {
store?.messenger?.storeValidateKey(channel, validateKey);
}
postMessage(clientId, "PAD_READY");
});
});
};
const _join: Callback = (ctx, clientId, data) => {
if (data.versionHash) {
return _getVersionHash(ctx, clientId, data);
}
const { channels, store, Store, myDeletions, postMessage } = ctx;
const channelId = data.channel;
if (!Hash.isValidChannel(channelId)) {
return postMessage(clientId, "PAD_ERROR", 'INVALID_CHAN');
}
const isNew = typeof channels[channelId] === "undefined";
// Create or get existing channel object
const channel = channels[channelId] ||= {
queue: [],
data: {},
clients: [],
bcast: (cmd, data, notMe) => {
channel.clients.forEach(function (cId) {
if (cId === notMe) { return; }
postMessage(cId, cmd, data);
});
},
history: [],
pushHistory: (msg, isCp) => {
if (isCp) {
// Checkpoint, re-add "cp|" and clear older history
channel.history.push('cp|' + msg);
let i;
for (i = channel.history.length - 101; i > 0; i--) {
if (/^cp\|/.test(channel.history[i])) { break; }
}
channel.history = channel.history.slice(i);
return;
}
channel.history.push(msg);
}
};
// Add new client
if (channel.clients.indexOf(clientId) === -1) {
channel.clients.push(clientId);
}
// Existing pad already loaded: send userlist and history
if (!isNew && channel.wc) {
postMessage(clientId, "PAD_CONNECT", { // Initialize
myID: channel.wc.myID,
id: channel.wc.id,
members: channel.wc.members
});
channel.wc.members.forEach(m => { // Userlist
postMessage(clientId, "PAD_JOIN", m);
});
channel.history.forEach(msg => { // History
postMessage(clientId, "PAD_MESSAGE", {
msg: CpNetflux.removeCp(msg),
user: channel.wc.myID,
validateKey: channel.data.validateKey
});
});
postMessage(clientId, "PAD_READY"); // Ready
return;
}
// chainpad-netflux config
const onError = err => {
const type = err?.type;
// Deletion started from this worker => different UI message
if (type === "EDELETED" && myDeletions[channelId]) {
delete myDeletions[channelId];
err.ownDeletion = true;
}
channel.bcast("PAD_ERROR", err);
if (type === "EDELETED" && Cache?.clearChannel) {
Cache.clearChannel(channelId);
}
// DELETED, EXPIRED or RESTRICTED pad => leave the channel
if (!["EDELETED","EEXPIRED","ERESTRICTED"].includes(type)) {
return;
}
Store.leavePad(null, data, function () {});
};
const conf = {
Cache: store.neverCache ? undefined : Cache,
priority: 1,
onCacheStart: () => {
postMessage(clientId, "PAD_CACHE");
},
onCacheReady: () => {
postMessage(clientId, "PAD_CACHE_READY");
},
onReady: pad => {
const padData = pad.metadata || {};
channel.data = padData;
if (padData?.validateKey && store.messenger) {
store.messenger.storeValidateKey(channelId, padData.validateKey);
}
postMessage(clientId, "PAD_READY", pad.noCache);
},
onMessage: function (m, user, validateKey, isCp, hash) {
channel.lastHash = hash;
channel.pushHistory(m, isCp);
channel.bcast("PAD_MESSAGE", {
user: user,
msg: m,
validateKey: validateKey
});
},
onJoin: function (m) {
channel.bcast("PAD_JOIN", m);
},
onLeave: function (m) {
channel.bcast("PAD_LEAVE", m);
},
onError: onError,
onChannelError: onError,
onRejected: Store.onRejected,
onConnectionChange: info => {
if (!info.state) {
channel.bcast("PAD_DISCONNECT");
}
},
onMetadataUpdate: metadata => {
channel.data = metadata || {};
Store.getAllStores().forEach(s => {
let allData = s.manager.findChannel(channelId, true);
allData.forEach(obj => {
obj.data.owners = metadata.owners;
obj.data.atime = +new Date();
if (metadata.expire) {
obj.data.expire = +metadata.expire;
}
});
const send = s.sendEvent || store.sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
});
});
channel.bcast("PAD_METADATA", metadata);
},
crypto: {
// Encryption and decryption is done in the outer window
// Async-store only deals with encrypted messages
encrypt: function (m) { return m; },
decrypt: function (m) { return m; }
},
noChainPad: true,
channel: channelId,
metadata: data.metadata,
network: store.network || store.networkPromise,
websocketURL: NetConfig.getWebsocketURL(),
onInit: function () {
onJoinedEvt.fire();
},
//readOnly: data.readOnly,
onConnect: (wc, sendMessage) => {
channel.sendMessage = (msg, cId, cb) => {
// Send to server
sendMessage(msg, err => {
if (err) { return void cb({ error: err }); }
// Broadcast to other tabs
channel.lastHash = msg.slice(0,64);
channel.pushHistory(CpNetflux.removeCp(msg), /^cp\|/.test(msg));
channel.bcast("PAD_MESSAGE", {
user: wc.myID,
msg: CpNetflux.removeCp(msg),
validateKey: channel.data.validateKey
}, cId);
cb();
});
};
channel.wc = wc;
channel.queue.forEach(function (data) {
channel.sendMessage(data.message, clientId);
});
channel.queue = [];
channel.bcast("PAD_CONNECT", {
myID: wc.myID,
id: wc.id,
members: wc.members
});
}
};
channel.cpNf = CpNetflux.start(conf);
};
// Send a message to a pad we already joined
const _sendMessage: Callback = (ctx, clientId, data, cb) => {
var msg = data.msg;
var channel = ctx.channels[data.channel];
if (!channel) { return; }
if (!channel.wc) {
channel.queue.push(msg);
return void cb();
}
channel.sendMessage(msg, clientId, cb);
};
const _onCorruptedCache: Callback = (ctx, clientId, channel) => {
var chan = ctx.channels[channel];
if (!chan || !chan.cpNf) { return; }
Cache.clearChannel(channel);
if (!chan.cpNf.resetCache) { return; }
chan.cpNf.resetCache();
};
const _getLastHash: Callback = (ctx, clientId, data, cb) => {
var chan = ctx.channels[data.channel];
if (!chan) { return void cb({error: 'ENOCHAN'}); }
if (!chan.lastHash) { return void cb({error: 'EINVAL'}); }
cb({
hash: chan.lastHash
});
};
const _destroy: Callback = (ctx, clientId, data, cb) => {
const { store, Store, channels, myDeletions } = ctx;
// "data" used to be a channelId, now it can also be an object
// data.force tells us we can safely remove the drive ID
let channel = data;
let force = false;
let teamId, reason;
if (data && typeof(data) === "object") {
({ channel, force, teamId, reason } = data);
}
if (channel === store.driveChannel && !force) {
return void cb({error: 'User drive removal blocked!'});
}
const s = Store.getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
// If this channel is loaded, store that we deleted it ourselves
if (channels[channel]) { myDeletions[channel] = true; }
s.rpc.removeOwnedChannel(channel, err => {
if (err) { delete myDeletions[channel]; }
cb({error:err});
}, reason);
};
const alwaysOnline = (ctx, chanId) => {
const { store } = ctx;
if (!store) { return false; }
// Drive
if (store.driveChannel === chanId) { return true; }
// Shared folders
if (SF.isSharedFolderChannel(chanId)) { return true; }
// Teams
if (Util.find(store, ['proxy', 'teams'])) {
var t = Util.find(store, ['proxy', 'teams']) || {};
return Object.keys(t).some(id => {
return t[id].channel === chanId;
});
}
// Profile
if (Util.find(store, ['proxy', 'profile', 'href'])) {
let href = Util.find(store, ['proxy', 'profile', 'href']);
return Hash.hrefToHexChannelId(href) === chanId;
}
};
const dropChannel = (ctx, chanId) => {
const store = ctx.store;
store.messenger?.leavePad?.(chanId);
store.onlyoffice?.leavePad?.(chanId);
Object.keys(store.modules).forEach(key => {
store.modules[key]?.leavePad?.(chanId);
});
if (!alwaysOnline(ctx, chanId)) {
try {
Cache.leaveChannel(chanId);
} catch (e) { console.error(e); }
ctx.channels[chanId]?.cpNf?.stop();
}
delete ctx.channels[chanId];
};
const init = (config) => {
const { broadcast, postMessage, store, Store } = config;
/** XXX
* Variables
* Store.channels, store.channels
*
* Methods
* sendPadMsg, corruptedCache, getLastHash
**/
const ctx = {
channels: [],
postMessage,
store,
Store,
myDeletions: []
};
const join: RpcCall = (clientId, data, cb) => {
_join(ctx, clientId, data, cb);
};
const destroy: RpcCall = (clientId, data, cb) => {
_destroy(ctx, clientId, data, cb);
};
const setMetadata: RpcCall = (clientId, data, cb) => {
_setMetadata(ctx, clientId, data, cb);
};
const getMetadata: RpcCall = (clientId, data, cb) => {
_getMetadata(ctx, clientId, data, cb);
};
const sendMessage: RpcCall = (clientId, data, cb) => {
_sendMessage(ctx, clientId, data, cb);
};
const onCorruptedCache: RpcCall = (clientId, data, cb) => {
_onCorruptedCache(ctx, clientId, data, cb);
};
const getLastHash: RpcCall = (clientId, data, cb) => {
_getLastHash(ctx, clientId, data, cb);
};
const removeClient = clientId => {
Object.keys(ctx.channels).forEach(chanId => {
let idx = ctx.channels[chanId].clients.indexOf(clientId);
if (idx !== -1) {
ctx.channels[chanId].clients.splice(idx, 1);
}
if (ctx.channels[chanId].clients.length === 0) {
dropChannel(ctx, chanId);
}
});
};
const leave: RpcCall = (clientId, data, cb) => {
const channel = ctx.channels[data.channel];
if (!channel?.cpNf) { return void cb ({error: 'EINVAL'}); }
dropChannel(ctx, data.channel);
cb();
};
const getChannels = () => {
return ctx.channels;
};
return {
join,
destroy,
setMetadata,
getMetadata,
sendMessage,
onCorruptedCache,
getLastHash,
leave,
removeClient,
onJoined: onJoinedEvt.reg,
getChannels
};
};
const Pad: Pad = {
init: init
};
export { Pad }

View File

@ -20,7 +20,6 @@ const factory = AStore => {
UPDATE_PIN_LIMIT: Store.updatePinLimit,
GET_PIN_LIMIT: Store.getPinLimit,
CLEAR_OWNED_CHANNEL: Store.clearOwnedChannel,
REMOVE_OWNED_CHANNEL: Store.removeOwnedChannel,
UPLOAD_CHUNK: Store.uploadChunk,
UPLOAD_COMPLETE: Store.uploadComplete,
UPLOAD_STATUS: Store.uploadStatus,
@ -73,9 +72,12 @@ const factory = AStore => {
// Universal
UNIVERSAL_COMMAND: Store.universal.execCommand,
// Pad
SEND_PAD_MSG: Store.sendPadMsg,
JOIN_PAD: Store.joinPad,
LEAVE_PAD: Store.leavePad,
SEND_PAD_MSG: Store.pad.sendMessage,
JOIN_PAD: Store.pad.join,
LEAVE_PAD: Store.pad.leave,
REMOVE_OWNED_CHANNEL: Store.pad.destroy,
CORRUPTED_CACHE: Store.pad.onCorruptedCache,
GET_LAST_HASH: Store.pad.getLastHash,
GET_FULL_HISTORY: Store.getFullHistory,
GET_HISTORY: Store.getHistory,
GET_HISTORY_RANGE: Store.getHistoryRange,
@ -83,12 +85,10 @@ const factory = AStore => {
CONTACT_PAD_OWNER: Store.contactPadOwner,
GIVE_PAD_ACCESS: Store.givePadAccess,
BURN_PAD: Store.burnPad,
GET_PAD_METADATA: Store.getPadMetadata,
SET_PAD_METADATA: Store.setPadMetadata,
GET_PAD_METADATA: Store.pad?.getMetadata,
SET_PAD_METADATA: Store.pad?.setMetadata,
CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin,
GET_LAST_HASH: Store.getLastHash,
GET_SNAPSHOT: Store.getSnapshot,
CORRUPTED_CACHE: Store.corruptedCache,
DELETE_MAILBOX_MESSAGE: Store.deleteMailboxMessage,
// Drive
DRIVE_USEROBJECT: Store.userObjectCommand,

View File

@ -132,7 +132,7 @@ const factory = (Util, Hash, UserObject, nThen) => {
history = messages.join('\n').length;
}), true);
// Metadata
Store.getPadMetadata(null, {
Store.pad.getMetadata(null, {
channel: channel
}, waitFor(function (obj) {
if (obj && obj.error) { return; }

View File

@ -297,7 +297,7 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
teamId: id
};
}
ctx.Store.removeOwnedChannel('', data, cb);
ctx.Store.pad.destroy('', data, cb);
},
Store: ctx.Store,
store: ctx.store
@ -879,7 +879,7 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
}));
}).nThen(function (_w) {
if (otherOwners) {
ctx.Store.setPadMetadata(null, {
ctx.Store.pad.setMetadata(null, {
channel: c,
command: 'RM_OWNERS',
value: [teamEdPublic],
@ -983,7 +983,7 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
var md;
nThen(function (waitFor) {
// Get pending owners
ctx.Store.getPadMetadata(null, {
ctx.Store.pad.getMetadata(null, {
channel: teamData.channel
}, waitFor(function (obj) {
if (obj && obj.error) {
@ -1006,7 +1006,7 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
});
if (!member && teamData.owner) {
var removeOwnership = function (chan) {
ctx.Store.setPadMetadata(null, {
ctx.Store.pad.setMetadata(null, {
channel: chan,
command: 'RM_PENDING_OWNERS',
value: [ed],
@ -1119,7 +1119,7 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
}
};
var addPendingOwner = function (chan) {
ctx.Store.setPadMetadata(null, {
ctx.Store.pad.setMetadata(null, {
channel: chan,
command: 'ADD_PENDING_OWNERS',
value: [user.edPublic],
@ -1175,7 +1175,7 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
}
};
var removeOwnership = function (chan) {
ctx.Store.setPadMetadata(null, {
ctx.Store.pad.setMetadata(null, {
channel: chan,
command: cmd,
value: [user.edPublic],
@ -1392,7 +1392,7 @@ const factory = (Util, Hash, Constants, Realtime, ProxyManager,
var md;
nThen(function (waitFor) {
// Get pending owners
ctx.Store.getPadMetadata(null, {
ctx.Store.pad.getMetadata(null, {
channel: teamData.channel
}, waitFor(function (obj) {
if (obj && obj.error) {

View File

@ -5,6 +5,7 @@
// Default CryptPad worker module, extended with
// specific methods for each module
export type Callback = (...args: any[]) => void
export type RpcCall = (clientId: string, data: any, cb: Callback) => void;
export type ModuleConfig = {
store: any,
@ -66,3 +67,26 @@ export interface DriveObject {
export interface Drive {
init: (config: DriveConfig) => DriveObject
}
export type PadConfig = {
Store: any,
store: any,
broadcast: (exclude: object, cmd: string, data?: any, cb?: any) => void,
postMessage: (clientId: string, cmd: string, data?: any, cb?: any) => void
}
export interface PadObject {
join: RpcCall,
destroy: RpcCall,
setMetadata: RpcCall,
getMetadata: RpcCall,
leave: RpcCall,
removeClient: (clientId: string) => void,
sendMessage: RpcCall,
getLastHash: RpcCall,
onCorruptedCache: RpcCall,
getChannels: () => string[],
onJoined: any,
}
export interface Pad {
init: (config: PadConfig) => PadObject
}

View File

@ -4,7 +4,7 @@
(() => {
const factory = function () {
let USE_MIN = true;
let USE_MIN = false;
let path = '/common/worker.bundle.js?';
if (USE_MIN) { path = '/common/worker.bundle.min.js?'; }

File diff suppressed because one or more lines are too long