Merge staging into office-9.3

This commit is contained in:
Wolfgang Ginolas 2026-09-09 11:28:49 +02:00
commit 9aafa6c4ea
38 changed files with 2397 additions and 1477 deletions

View File

@ -6,7 +6,7 @@ name: Trigger E2E tests
on:
push:
branches: [ staging, 2025.12-rc, 2025.12-test ]
branches: [ staging, 2026-autumn-rc, 2026-autumn-test ]
jobs:
trigger-tests:
@ -18,9 +18,9 @@ jobs:
if [ "$branch" = "staging" ]; then
event_type="trigger-e2e-tests-staging"
elif [ "$branch" = "2026.4-rc" ]; then
elif [ "$branch" = "2026-autumn-rc" ]; then
event_type="trigger-e2e-tests-rc"
elif [ "$branch" = "2026.4-test" ]; then
elif [ "$branch" = "2026-autumn-test" ]; then
event_type="trigger-e2e-tests-test"
fi

View File

@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
# Security Policy
CryptPad security policy is detailed in the following document: https://cryptpad.org/security/.
## Supported Versions
Considering the amount of resources necessary to backport security or bug fixes to previous, unsupported CryptPad versions, it's not something we do.
@ -13,7 +15,7 @@ However, we quickly release new minor versions in case of need.
Please keep up with the latest release published here: https://github.com/cryptpad/cryptpad/releases
Note that every GitHub release page has an RSS compatible feed that you can subscribe on to be informed of every new release.
Note that every GitHub release page has an [RSS compatible feed](https://github.com/cryptpad/cryptpad/releases.atom) that you can subscribe on to be informed of every new release.
We do also communicate about this topic on:
- [Our blog](https://blog.cryptpad.org)

View File

@ -93,6 +93,10 @@
left: 250px;
}
&.cp-small {
min-width: 50px;
}
&:hover {
display: block;
}

View File

@ -52,10 +52,6 @@
min-width: 0;
text-align: center;
}
.cp-history-timeline-version {
font-size: 12px;
margin-right: 10px;
}
.cp-toolbar-history-previous, .cp-toolbar-history-next {
display: inline-flex;
@ -207,6 +203,37 @@
height: 18px;
display: flex;
}
.cp-history-timeline-patch {
display: flex;
.cp-history-bar-el {
cursor: pointer;
flex: 1;
background-color: @history_userBg1;
border: 2px solid @history_userBg1;
display: flex;
align-items: center;
justify-content: center;
&:nth-child(2n) {
background-color: @history_userBg2;
border: 2px solid @history_userBg2;
}
&.cp-selected {
border: 2px solid @cryptpad_text_col;
position: relative;
svg:not([data-snapshot]) {
position: absolute;
margin: 0;
left: 50%;
bottom: 65%;
transform: translateX(-50%)
}
}
svg[data-snapshot] {
height: 100%;
margin: 0;
}
}
}
.cp-history-timeline-users {
margin-bottom: 1px;
.cp-history-bar-el {
@ -290,6 +317,38 @@
}
}
.cp-history-version {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.8em;
& *:not(button > span) {
font-size: inherit;
}
.cp-dropdown-container {
svg {
margin-top: 3px;
}
button {
text-transform: unset;
min-width: 50px;
width: auto;
padding: 3px;
font-size: 0.9em;
}
}
.cp-history-version-time {
&.cp-hidden {
visibility: hidden;
}
}
.cp-history-version-select {
display: flex;
align-items: center;
gap: 5px;
}
}
.cp-history-oo-timeline-pos {
border: 2px solid @cryptpad_text_col;
height: 37px;

View File

@ -13,6 +13,7 @@ const Channel = require("./channel");
const Invitation = require("./invitation");
const Users = require("./users");
const Moderators = require("./moderators");
const Linked = require("./linked");
const BlockStore = require("../storage/block");
const MFA = require("../storage/mfa");
const ArchiveAccount = require('../archive-account');
@ -195,16 +196,21 @@ var archiveDocument = function (Env, Server, _cb, data) {
switch (id.length) {
case 32:
return void Env.msgStore.archiveChannel(id, archiveReason, Util.both(cb, function (err) {
Env.Log.info("ARCHIVAL_CHANNEL_BY_ADMIN_RPC", {
channelId: id,
reason: reason,
status: err? String(err): "SUCCESS",
});
Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', reasonStr, err => {
if (err) { } // TODO
});
}));
return void Linked.listLinkedDocuments(Env, id, (err, channels) => {
Env.msgStore.archiveChannel(id, archiveReason, Util.both(cb, function (err) {
if (!err && channels) {
Linked.archiveLinkedData(Env, id, archiveReason, channels, () => {});
}
Env.Log.info("ARCHIVAL_CHANNEL_BY_ADMIN_RPC", {
channelId: id,
reason: reason,
status: err? String(err): "SUCCESS",
});
Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', reasonStr, err => {
if (err) { } // TODO
});
}));
});
case 48:
return void Env.blobStore.archive.blob(id, archiveReason, Util.both(cb, function (err) {
Env.Log.info("ARCHIVAL_BLOB_BY_ADMIN_RPC", {
@ -260,16 +266,21 @@ var removeDocument = function (Env, Server, cb, data) {
switch (id.length) {
case 32:
return void Env.msgStore.removeChannel(id, Util.both(cb, function (err) {
Env.Log.info("REMOVAL_CHANNEL_BY_ADMIN_RPC", {
channelId: id,
reason: reason,
status: err? String(err): "SUCCESS",
});
Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', reason, err => {
if (err) { } // TODO
});
}));
return void Linked.listLinkedDocuments(Env, id, (err, channels) => {
Env.msgStore.removeChannel(id, Util.both(cb, function (err) {
if (!err && channels) {
Linked.archiveLinkedData(Env, id, reason, channels, () => {});
}
Env.Log.info("REMOVAL_CHANNEL_BY_ADMIN_RPC", {
channelId: id,
reason: reason,
status: err? String(err): "SUCCESS",
});
Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', reason, err => {
if (err) { } // TODO
});
}));
});
case 48:
return void Env.blobStore.remove.blob(id, Util.both(cb, function (err) {
Env.Log.info("REMOVAL_BLOB_BY_ADMIN_RPC", {

View File

@ -8,6 +8,7 @@ const Util = require("../common-util");
const nThen = require("nthen");
const Core = require("./core");
const Metadata = require("./metadata");
const Linked = require("./linked");
const HK = require("../hk-util");
const Nacl = require("tweetnacl/nacl-fast");
@ -171,7 +172,15 @@ Channel.removeOwnedChannel = function (Env, safeKey, obj, __cb, Server) {
if (Env.blobStore.isFileId(channelId)) {
return void Env.removeOwnedBlob(channelId, safeKey, reason, cb);
}
archiveOwnedChannel(Env, safeKey, channelId, reason, cb, Server);
Linked.listLinkedDocuments(Env, channelId, (err, channels) => {
archiveOwnedChannel(Env, safeKey, channelId, reason, (err, data) => {
if (!channels) { return void cb(err, data); }
if (err) { return void cb(err); }
Linked.archiveLinkedData(Env, channelId, reason, channels, () => {
cb(void 0, data);
});
}, Server);
});
});
};
@ -200,7 +209,11 @@ Channel.trimHistory = function (Env, safeKey, data, cb) {
}
// else fall through to the next block
}));
}).nThen(function (w) {
// Archive old checkpoints
Linked.trimHistory(Env, { channel: channelId }, w());
}).nThen(function () {
// Trim chainpad doc:
Env.msgStore.trimChannel(channelId, hash, function (err) {
Env.Log.info('HK_TRIM_HISTORY', {
unsafeKey: unsafeKey,

445
lib/commands/linked.js Normal file
View File

@ -0,0 +1,445 @@
// SPDX-FileCopyrightText: 2026 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Linked = module.exports;
const nThen = require("nthen");
//const Core = require("./core");
//const CPCrypto = require('../crypto');
const Util = require("../common-util");
const MetaRPC = require("./metadata");
const HK = require("../hk-util");
const getMetadata = (Env, channel, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
const metadata = Env.metadata_cache[channel];
if (metadata && typeof(metadata) === 'object') {
return void cb(undefined, metadata);
}
MetaRPC.getMetadataRaw(Env, channel, (err, metadata) => {
if (err) { return void cb(err); }
if (metadata?.channel !== channel && channel.length !== HK.BLOB_ID_LENGTH) {
return cb();
}
// cache it
if (channel.length !== HK.BLOB_ID_LENGTH) {
Env.metadata_cache[channel] = metadata;
}
cb(undefined, metadata);
});
};
const allowedTypes = ['checkpoints', 'media', 'channels'];
// XXX add logs
Linked.getLinkedDocuments = (Env, data, cb) => {
Env.store.getLinkedDocuments(data.channel, (err, json) => {
if (err && err !== 'ENOENT') { return void cb(err?.message); }
cb(void 0, json || {});
});
};
Linked.listLinkedDocuments = (Env, channel, _cb) => {
const cb = Util.mkAsync(_cb);
if (channel.length !== HK.STANDARD_CHANNEL_LENGTH) {
return void cb(void 0, []);
}
const list = new Set();
Linked.getLinkedDocuments(Env, { channel }, (err, json) => {
if (err) { return void cb(err); }
// For each type, add the channels and/or blobs
allowedTypes.forEach(type => {
const data = json[type];
if (!Array.isArray(data)) { return; }
// Media or channel:
if (type !== 'checkpoints') {
data.forEach(id => { list.add(id); });
return;
}
// Checkpoint:
data.forEach(obj => {
if (obj?.rtChannel) { list.add(obj.rtChannel); }
if (obj?.blob) { list.add(obj.blob); }
});
});
cb(void 0, Array.from(list));
});
};
Linked.listOldCheckpoints = (Env, channel, cb) => {
const list = new Set();
Linked.getLinkedDocuments(Env, { channel }, (err, json) => {
if (err) { return void cb(err); }
const cps = json.checkpoints || [];
cps.pop(); // preserve last cp
cps.forEach(obj => {
if (obj?.rtChannel) { list.add(obj.rtChannel); }
if (obj?.blob) { list.add(obj.blob); }
});
cb(void 0, Array.from(list));
});
};
const checkContent = (content, user) => {
const { type, data } = content;
if (type === 'checkpoints' && data) {
const { rtChannel, blob } = data;
if (rtChannel?.length !== 32 || (blob && blob?.length !== 48)) {
return false;
}
return {
rtChannel, blob, user,
time: Date.now()
};
}
if (type === 'media') {
return data?.length === 48 ? data : false;
}
if (type === 'channels') {
return data?.length === 32 ? data : false;
}
return false;
};
Linked.addLinkedDocument = (Env, data, cb, _S, userId) => {
// data.user
// data.channel
// data.content
// type, data (channelId or blobId or checkpoint {blob, rtChannel}})
// data.proof
// (sign "{ user, channel, content }" with pad signing key
const { user, channel, content, netfluxId, proof } = data;
if (userId !== netfluxId) { return void cb('EFORBIDDEN'); }
const msg = Util.clone(data);
delete msg.proof;
const signedMsg = JSON.stringify(msg);
const type = content?.type;
if (!allowedTypes.includes(type)) {
return void cb('INVALID_TYPE');
}
const value = checkContent(content, user);
if (!value) { return void cb('INVALID_CONTENT'); }
let validateKey;
nThen(waitFor => {
getMetadata(Env, channel, waitFor((err, metadata) => {
if (!metadata?.validateKey) {
waitFor.abort();
return void cb(err || 'METADATA_ERROR');
}
validateKey = metadata.validateKey;
}));
}).nThen(waitFor => {
Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> {
if (err) {
waitFor.abort();
return void cb('INVALID_PROOF');
}
}));
}).nThen(() => {
Env.store.addLinkedDocument(channel, type, value, cb);
});
};
Linked.resetLinkedDocuments = (Env, data, cb, _S, userId) => {
// data.user
// data.channel
// data.content
// data.proof
// (sign "{ user, channel, content }" with pad signing key
const { user, channel, content, netfluxId, proof } = data;
if (userId !== netfluxId) { return void cb('EFORBIDDEN'); }
const msg = Util.clone(data);
delete msg.proof;
const signedMsg = JSON.stringify(msg);
let validateKey;
const newContent = {};
allowedTypes.forEach(type => { newContent[type] = []; });
nThen(waitFor => {
getMetadata(Env, channel, waitFor((err, metadata) => {
if (!metadata?.validateKey) {
waitFor.abort();
return void cb(err || 'METADATA_ERROR');
}
validateKey = metadata.validateKey;
}));
}).nThen(waitFor => {
Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> {
if (err) {
waitFor.abort();
return void cb('INVALID_PROOF');
}
}));
}).nThen(waitFor => {
Linked.getLinkedDocuments(Env, { channel }, waitFor((err, json = {}) => {
// checkpoints
if (Array.isArray(content?.checkpoints)) {
const old = json?.checkpoints || [];
// add last 10 valid checkpoints
let i = 0;
content.checkpoints.reverse().some(data => {
// If cp already exists, recover user and time
// Otherwise, check integrity of new value and add them now
const oldValue = old.find(obj => {
return obj.blob === data.blob &&
obj.rtChannel === data.rtChannel;
});
const toAdd = oldValue || checkContent({
type: 'checkpoints',
data
}, user);
if (!toAdd) { return false; }
newContent.checkpoints.unshift(toAdd);
// Abort after 10 cps
if (++i >= 10) { return true; }
});
}
// channels and media
['channels', 'media'].forEach(type => {
if (!Array.isArray(content?.[type])) { return; }
content[type].forEach(data => {
const toAdd = checkContent({type, data}, user);
if (!toAdd) { return false; }
newContent[type].push(toAdd);
});
});
}));
}).nThen(() => {
Env.store.resetLinkedDocuments(channel, newContent, (err, data) => {
const { oldContent } = data;
Env.Log.info('RESET_LINKED_DOCUMENTS', {user, channel, oldContent, content});
cb();
});
});
};
Linked.removeLinkedDocument = (Env, allData, cb, _S, userId) => {
// data.user
// data.channel
// data.content
// type, channelId or blobId
// data.proof
// (sign "{ user, channel, content }" with pad signing key
const { channel, content, netfluxId, proof } = allData;
if (userId !== netfluxId) { return void cb('EFORBIDDEN'); }
const { type, data } = content;
const msg = Util.clone(data);
delete msg.proof;
const signedMsg = JSON.stringify(msg);
if (!allowedTypes.includes(type)) {
return void cb('INVALID_TYPE');
}
if (typeof(data) !== "string" || ![32,48].includes(data.length)) {
return void cb('INVALID_CONTENT');
}
let validateKey;
nThen(waitFor => {
getMetadata(Env, channel, waitFor((err, metadata) => {
if (!metadata?.validateKey) {
waitFor.abort();
return void cb(err || 'METADATA_ERROR');
}
validateKey = metadata.validateKey;
}));
}).nThen(waitFor => {
Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> {
if (err) {
waitFor.abort();
return void cb('INVALID_PROOF');
}
}));
}).nThen(() => {
Env.store.removeLinkedDocument(channel, type, data, cb);
});
};
Linked.getFileSize = (Env, data, _cb) => {
const cb = Util.once(_cb);
const channel = data.channel;
let linked;
nThen(waitFor => {
Linked.listLinkedDocuments(Env, channel, waitFor((err, channels) => {
if (err) {
waitFor.abort();
return void cb(err);
}
linked = channels || [];
}));
}).nThen(() => {
linked.push(channel);
Env.getTotalSize(linked, cb);
});
};
Linked.getHistorySize = (Env, data, _cb) => {
const cb = Util.once(_cb);
const channel = data.channel;
let linked;
let channelTotalSize = 0;
let size = 0;
let start = 0;
let hash;
nThen(waitFor => {
Linked.getLinkedDocuments(Env, data, waitFor((err, json) => {
if (err) {
waitFor.abort();
return void cb(err);
}
linked = Util.clone(json);
}));
}).nThen(waitFor => {
// Get main channel size (chainpad)
Env.getFileSize(channel, waitFor((err, _size) => {
if (err) {
waitFor.abort();
return void cb(err);
}
channelTotalSize = _size;
}), true);
}).nThen(waitFor => {
// Get history offset to compute non-history size
HK.getHistoryOffset(Env, channel, null, waitFor((err, offset) => {
if (err) {
waitFor.abort();
return void cb(err);
}
start = offset;
const chanSize = channelTotalSize - offset;
size += chanSize;
}));
}).nThen(waitFor => {
// Get oldest hash of non-history data
Env.store.readMessagesBin(channel, start, (msgObj, readMore, abort) => {
const parsed = Util.tryParse(msgObj.buff.toString('utf8'));
if (!parsed) { return void readMore(); }
hash = HK.getHash(parsed[4]);
abort();
}, waitFor());
}).nThen(waitFor => {
// Get last checkpoint size (blob + rtChannel)
// Note: blob may be falsy if no checkpoint
const lastCp = (linked?.checkpoints || []).pop();
if (!lastCp) { return; }
const { blob, rtChannel } = lastCp;
if (blob) {
Env.getFileSize(blob, waitFor((err, _size) => {
if (err) {
waitFor.abort();
return void cb(err);
}
size += _size;
}), true);
}
Env.getFileSize(rtChannel, waitFor((err, _size) => {
if (err) {
waitFor.abort();
return void cb(err);
}
size += _size;
}), true);
}).nThen(() => {
cb(void 0, {
size, hash
});
});
};
Linked.trimHistory = (Env, data, cb) => {
const channel = data.channel;
let linked;
// if we reach this step, it means this user is an owner of "channel"
// so we can also delete any document linked to "channel" (from metadata)
nThen(waitFor => {
// List all but the current checkpoints
Linked.listOldCheckpoints(Env, channel, waitFor((err, channels) => {
if (err) {
waitFor.abort();
return void cb(err);
}
linked = channels || [];
}));
}).nThen(() => {
let n = nThen;
linked.forEach(chan => {
n = n(w => {
// If channel is "linked", we can archive all but last cp
getMetadata(Env, chan, w((err, md) => {
if (md?.linked !== channel) { return; }
// This is an old checkpoint linked to our document,
// we can archive it
const reason = "TRIM_HISTORY";
if (chan.length === HK.BLOB_ID_LENGTH) {
return Env.blobStore.archive.blob(chan, reason, w());
}
Env.store.archiveChannel(chan, reason, w());
}));
}).nThen;
});
n(() => {
cb();
});
});
};
// Archive all linked documents that inherit metadata from their
// parent. We consider ownership has already been checked when
// this function is called.
Linked.archiveLinkedData = (Env, channel, reason, channels, _cb) => {
const cb = Util.once(_cb);
let n = nThen;
channels.forEach(chan => {
n = n(w => {
// For each linked document, check if they inherit properties
getMetadata(Env, chan, w((err, md) => {
if (md?.linked !== channel) { return; }
// If they do, archive the document
if (chan.length === HK.BLOB_ID_LENGTH) {
return Env.blobStore.archive.blob(chan, reason, w());
}
Env.store.archiveChannel(chan, reason, w());
}));
}).nThen;
});
n(() => {
cb();
});
};

View File

@ -9,7 +9,7 @@ const Core = require("./core");
const Util = require("../common-util");
const HK = require("../hk-util");
Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) {
Data.getMetadataRaw = function (Env, channel, _cb, resolveLinked) {
const cb = Util.once(Util.mkAsync(_cb));
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length !== HK.STANDARD_CHANNEL_LENGTH &&
@ -39,6 +39,17 @@ Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) {
// clear metadata after a delay if nobody has joined the channel within 30s
Env.checkCache(channel);
}
if (resolveLinked && meta?.linked?.length === HK.STANDARD_CHANNEL_LENGTH
&& meta?.linked !== channel) {
Data.getMetadataRaw(Env, meta.linked, (err, _meta) => {
meta.owners = _meta.owners;
meta.restricted = _meta.restricted;
meta.allowed = _meta.allowed;
done(err, meta);
});
return;
}
done(err, meta);
});
});
@ -142,7 +153,11 @@ Data.setMetadata = function (Env, safeKey, data, cb, Server) {
cb(void 0, metadata);
return void next();
}
Env.msgStore.writeMetadata(channel, JSON.stringify(line), function (e) {
let store = Env.msgStore;
if (channel.length === HK.BLOB_ID_LENGTH) {
store = Env.blobStore;
}
store.writeMetadata(channel, JSON.stringify(line), function (e) {
if (e) {
cb(e);
return void next();
@ -208,6 +223,6 @@ Data.setMetadata = function (Env, safeKey, data, cb, Server) {
Server.removeFromChannel(channel, toRemove);
});
});
}, true);
});
};

View File

@ -7,6 +7,7 @@ const Core = require("./core");
const Pinning = module.exports;
const Util = require("../common-util");
const nThen = require("nthen");
const Linked = require('./linked');
const escapeKeyCharacters = Util.escapeKeyCharacters;
const unescapeKeyCharacters = Util.unescapeKeyCharacters;
@ -74,6 +75,25 @@ var getChannelList = Pinning.getChannelList = function (Env, safeKey, _cb) {
});
};
var addLinkedDocuments = (Env, channels, cb) => {
var arr = Array.from(channels);
var n = nThen;
arr.forEach(chan => {
// For each channel, add their linked documents
n = n(w => {
Linked.listLinkedDocuments(Env, chan, w((err, linked) => {
if (err || !linked) { return; }
linked.forEach(id => {
channels.add(id);
});
}));
}).nThen;
});
n(() => {
cb();
});
};
Pinning.getTotalSize = function (Env, safeKey, cb) {
var unsafeKey = unescapeKeyCharacters(safeKey);
var limit = Env.limits[unsafeKey];
@ -108,6 +128,8 @@ Pinning.getTotalSize = function (Env, safeKey, cb) {
}));
});
}
}).nThen(function (waitFor) {
addLinkedDocuments(Env, channels, waitFor());
}).nThen(function () {
Env.getTotalSize(Array.from(channels), done);
});

View File

@ -69,6 +69,7 @@ Upload.status = function (Env, safeKey, data, _cb) { // FIXME FILES
var user = Core.getSession(Env.Sessions, safeKey);
user.pendingUploadSize = filesize;
user.currentUploadSize = 0;
user.linked = data.linked;
cb(void 0, false);
});
@ -88,7 +89,8 @@ var completeUpload = function (owned) {
Env.blobStore.closeBlobstage(safeKey);
var user = Core.getSession(Env.Sessions, safeKey);
var size = user.pendingUploadSize;
Env.completeUpload(safeKey, arg, Boolean(owned), size, cb);
var linked = user.linked;
Env.completeUpload(safeKey, arg, Boolean(owned), size, linked, cb);
};
};

View File

@ -5,6 +5,7 @@
var nThen = require("nthen");
var Bloom = require("@mcrowe/minibloom");
var Util = require("../lib/common-util");
var Linked = require("../lib/commands/linked");
var Pins = require("../lib/pins");
var Keys = require("./keys");
var Path = require('node:path');
@ -379,10 +380,21 @@ module.exports = function (Env, cb) {
accountRetentionTime = -1;
}
var pinAll = function (pinList) {
var pinAll = function (pinList, cb) {
let n = nThen;
pinList.forEach(function (docId) {
pinnedDocs.add(docId);
// Add linked documents
if (docId.length !== 32) { return; }
n = n(w => {
Linked.listLinkedDocuments(Env, docId, w((err, channels) => {
if (!Array.isArray(channels)) { return; }
channels.forEach(chan => { pinnedDocs.add(chan); });
}));
}).nThen;
});
n(() => { cb(); });
};
var docIsActive = function (docId) {
@ -422,8 +434,7 @@ module.exports = function (Env, cb) {
if (accountIsActive(mtime, pinList)) {
// add active accounts' pinned documents to a second bloom filter
pinAll(pinList);
return void next();
return pinAll(pinList, next);
}
// Otherwise they are inactive.
@ -431,19 +442,21 @@ module.exports = function (Env, cb) {
// we plan to delete them, because it may be interesting information
inactive++;
if (PRESERVE_INACTIVE_ACCOUNTS) {
pinAll(pinList);
return Log.info('EVICT_INACTIVE_ACCOUNT_PRESERVED', {
id: id,
mtime: mtime,
}, next);
return pinAll(pinList, () => {
Log.info('EVICT_INACTIVE_ACCOUNT_PRESERVED', {
id: id,
mtime: mtime,
}, next);
});
}
if (isPremiumAccount(id)) {
pinAll(pinList);
return Log.info("EVICT_INACTIVE_PREMIUM_ACCOUNT", {
id: id,
mtime: mtime,
}, next);
return pinAll(pinList, () => {
Log.info("EVICT_INACTIVE_PREMIUM_ACCOUNT", {
id: id,
mtime: mtime,
}, next);
});
}
// remove the pin logs of inactive accounts if inactive account removal is configured

View File

@ -448,7 +448,7 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash, tim
* -1 if you didn't find it
*/
const getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
const getHistoryOffset = HK.getHistoryOffset = (Env, channelName, lastKnownHash, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
// lastKnownhash === -1 means we want the complete history
@ -866,7 +866,7 @@ const handleGetFullHistory = function (Env, Server, seq, userId, parsed) {
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(['FULL_HISTORY', msg])], readMore);
}, (err) => {
let parsedMsg = ['FULL_HISTORY_END', parsed[1]];
if (err) {
if (err && err?.code !== 'ENOENT') {
Log.error('HK_GET_FULL_HISTORY', err.stack);
parsedMsg = ['ERROR', parsed[1], err.message];
}

View File

@ -335,6 +335,21 @@ commands.RESET_OWNERS = function (meta, args) {
return true;
};
// ["SET_LINKED", "9fb7485b8e824d07489ed5586d848232", 1561623439989]
commands.SET_LINKED = function (meta, linked) {
if (typeof(linked) !== "string") {
throw new Error('METADATA_INVALID_LINKED');
}
// reject the proposed command if there is no change in state
if (meta.linked === linked) { return false; }
// apply the new state
meta.linked = linked;
return true;
};
// ["ADD_MAILBOX", {"7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I=": mailbox, ...}, 1561623439989]
commands.ADD_MAILBOX = function (meta, args) {
// expect a new array, even if it's empty

View File

@ -11,6 +11,7 @@ const Quota = require("./commands/quota");
const Metadata = require("./commands/metadata");
const Channel = require("./commands/channel");
const Upload = require("./commands/upload");
const Linked = require("./commands/linked");
const HK = require("./hk-util");
var RPC = module.exports;
@ -25,7 +26,11 @@ const UNAUTHENTICATED_CALLS = {
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
GET_METADATA: Metadata.getMetadata,
IS_PREMIUM: Pinning.isPremium,
ADD_FIRST_ADMIN: Admin.addFirstAdmin
ADD_FIRST_ADMIN: Admin.addFirstAdmin,
GET_LINKED_DOCUMENTS: Linked.getLinkedDocuments,
ADD_LINKED_DOCUMENT: Linked.addLinkedDocument,
RESET_LINKED_DOCUMENTS: Linked.resetLinkedDocuments,
GET_HISTORY_SIZE: Linked.getHistorySize
};
var isUnauthenticateMessage = function (msg) {

View File

@ -378,7 +378,7 @@ var upload_cancel = function (Env, safeKey, fileSize, cb) {
};
// upload_complete
var upload_complete = function (Env, safeKey, id, cb) {
var upload_complete = function (Env, safeKey, id, cb, linked) {
closeBlobstage(Env, safeKey);
var oldPath = makeStagePath(Env, safeKey);
@ -405,6 +405,12 @@ var upload_complete = function (Env, safeKey, id, cb) {
}
cb(void 0, id);
}));
}).nThen(function (w) {
if (!linked) { return; }
// Write the metadata
let meta = { linked };
let md = JSON.stringify(meta);
writeMetadata(Env, id, md, w());
}).nThen(function () {
// finally, move the old file to the new path
// FIXME we could just move and handle the EEXISTS instead of the above block
@ -438,7 +444,7 @@ var tryId = function (path, cb) {
let unescapeKeyCharacters = function (key) {
return key.replace(/\-/g, '/');
};
var owned_upload_complete = function (Env, safeKey, id, cb) {
var owned_upload_complete = function (Env, safeKey, id, cb, linked) {
closeBlobstage(Env, safeKey);
if (!isValidId(id)) {
return void cb('EINVAL_ID');
@ -472,9 +478,9 @@ var owned_upload_complete = function (Env, safeKey, id, cb) {
}));
}).nThen(function (w) {
// Write the metadata
let md = JSON.stringify({
owners: [unsafeKey]
});
let meta = { owners: [unsafeKey] };
if (linked) { meta.linked = linked; }
let md = JSON.stringify(meta);
writeMetadata(Env, id, md, w((e) => {
if (e) {
w.abort();
@ -658,6 +664,8 @@ var listBlobs = function (root, handler, fast, cb) {
var nestedDirPath = Path.join(root, dir);
Fs.readdir(nestedDirPath, w(function (err, list) {
if (err) { return void handler(err); } // Is this correct?
const s = new Set(list);
list.forEach(function (item) {
// ignore hidden files
if (/^\./.test(item)) { return; }
@ -671,7 +679,7 @@ var listBlobs = function (root, handler, fast, cb) {
if (!/^[0-9a-fA-F]{48}$/.test(item)) {
blobName = item.replace(/\.metadata\.ndjson/, '');
// check if blob already exists
if (list.indexOf(blobName) !== -1) { return; }
if (s.has(blobName)) { return; }
// otherwise set a flag indicating that we should
// handle the metadata on its own
isLonelyMetadata = true;
@ -883,17 +891,17 @@ BlobStore.create = function (config, _cb) {
closeBlobstage: function (safeKey) {
closeBlobstage(Env, safeKey);
},
complete: function (safeKey, id, _cb) {
complete: function (safeKey, id, _cb, linked) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
if (!isValidId(id)) { return void cb("INVALID_ID"); }
upload_complete(Env, safeKey, id, cb);
upload_complete(Env, safeKey, id, cb, linked);
},
completeOwned: function (safeKey, id, _cb) {
completeOwned: function (safeKey, id, _cb, linked) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidSafeKey(safeKey)) { return void cb('INVALID_SAFEKEY'); }
if (!isValidId(id)) { return void cb("INVALID_ID"); }
owned_upload_complete(Env, safeKey, id, cb);
owned_upload_complete(Env, safeKey, id, cb, linked);
},
size: function (id, _cb) {
var cb = Util.once(Util.mkAsync(_cb));

View File

@ -38,6 +38,8 @@ const STREAM_CLOSE_TIMEOUT = 120000;
leaks are bad. */
const STREAM_DESTROY_TIMEOUT = 30000;
const LINKED_CP = 10;
const isValidChannelId = function (id) {
return typeof(id) === 'string' &&
id.length >= 32 && id.length < 50 &&
@ -76,6 +78,21 @@ var mkPlaceholderPath = function (env, channelId) {
return mkPath(env, channelId) + '.placeholder';
};
// Linked documents (e.g. office patches, form responses)
/*
{
"media": [], // images
"checkpoints": [], // office checkpoints
"channels": [] // form responses
}
*/
var mkLinkedPath = function (env, channelId) {
return mkPath(env, channelId) + '.linked';
};
var mkArchiveLinkedPath = function (env, channelId) {
return mkArchivePath(env, channelId) + '.linked';
};
// pass in the path so we can reuse the same function for archived files
var channelExists = function (filepath, cb) {
Fs.stat(filepath, function (err, stat) {
@ -165,6 +182,91 @@ var readPlaceholder = function (env, channelId, cb) {
});
};
const readLinkedFile = (env, path, cb) => {
Fs.readFile(path, function (err, content) {
if (err) { return void cb(); }
try {
cb(void 0, JSON.parse(content.toString('utf8')));
} catch (e) {
cb();
}
});
};
const writeLinkedFile = (env, path, content, cb) => {
let s_data;
try {
s_data = JSON.stringify(content);
} catch (e) {
return cb(e);
}
Fs.writeFile(path, s_data, err => {
if (err) { return void cb(err); }
cb(void 0, content);
});
};
const addLinkedDocument = (env, channelId, type, data, cb) => {
const path = mkLinkedPath(env, channelId);
readLinkedFile(env, path, (_err, content = {}) => {
if (type === 'checkpoints') {
if (!(data?.rtChannel?.length === 32 &&
(!data.blob || data?.blob?.length === 48) &&
data?.user && data?.time)) {
return void cb('EINVAL');
}
content.checkpoints ||= [];
content.checkpoints.push(data);
content.checkpoints = content.checkpoints.slice(-LINKED_CP);
} else if (type === 'media') {
if (data.length !== 48) { return void cb('EINVAL'); }
content.media ||= [];
if (content.media.includes(data)) {
return void cb(void 0, content);
}
content.media.push(data);
} else if (type === 'channels') {
if (data.length !== 32) { return void cb('EINVAL'); }
content.channels ||= [];
if (content.channels.includes(data)) {
return void cb(void 0, content);
}
content.channels.push(data);
}
writeLinkedFile(env, path, content, cb);
});
};
const resetLinkedDocuments = (env, channelId, content, cb) => {
const path = mkLinkedPath(env, channelId);
readLinkedFile(env, path, (_err, oldContent = {}) => {
writeLinkedFile(env, path, content, err => {
if (err) { return void cb(err); }
cb(void 0, { oldContent, content });
});
});
};
const removeLinkedDocument = (env, channelId, type, data, cb) => {
const path = mkLinkedPath(env, channelId);
readLinkedFile(env, path, (_err, content = {}) => {
if (type === 'checkpoint' && content.checkpoints) {
content.checkpoints = content.checkpoints.filter(obj => {
return obj.blob !== data;
});
} else if (type === "media" && content.media) {
content.media.filter(str => {
return str !== data;
});
} else if (type === "channel" && content.channels) {
content.channels.filter(str => {
return str !== data;
});
}
writeLinkedFile(env, path, content, cb);
});
};
const getLinkedDocuments = (env, channelId, cb) => {
const path = mkLinkedPath(env, channelId);
readLinkedFile(env, path, cb);
};
const destroyStream = function (stream) {
if (!stream) { return; }
@ -567,6 +669,7 @@ var removeChannel = function (env, channelName, cb) {
var removeArchivedChannel = function (env, channelName, cb) {
var channelPath = mkArchivePath(env, channelName);
var metadataPath = mkArchiveMetadataPath(env, channelName);
var linkedPath = mkArchiveLinkedPath(env, channelName);
var CB = Util.once(cb);
@ -585,6 +688,13 @@ var removeArchivedChannel = function (env, channelName, cb) {
CB(labelError("E_ARCHIVED_METADATA_REMOVAL", err));
}
}));
Fs.unlink(linkedPath, w(function (err) {
if (err) {
if (err.code === "ENOENT") { return; }
w.abort();
CB(labelError("E_ARCHIVED_LINKED_REMOVAL", err));
}
}));
}).nThen(function () {
CB();
});
@ -680,6 +790,7 @@ var listChannels = function (root, handler, cb, fast) {
Fs.readdir(nestedDirPath, w(function (err, list) {
if (err) { return void handler(err); } // Is this correct?
const s = new Set(list);
list.forEach(function (item) {
// ignore hidden files
if (/^\./.test(item)) { return; }
@ -698,7 +809,7 @@ var listChannels = function (root, handler, cb, fast) {
// if there is a corresponding channel present in the list,
// then we should stop here and handle everything when we get to the channel
if (list.indexOf(channelName) !== -1) { return; }
if (s.has(channelName)) { return; }
// otherwise set a flag indicating that we should
// handle the metadata on its own
isLonelyMetadata = true;
@ -772,6 +883,12 @@ var archiveChannel = function (env, channelName, reason, cb) {
}).nThen(function (w) {
if (!reason) { return; }
addPlaceholder(env, channelName, reason, w());
}).nThen(function (w) {
// archive the dedicated .linked channel
var linkedPath = mkLinkedPath(env, channelName);
var archivePath = mkArchiveLinkedPath(env, channelName);
// Ignore errors, proceed to archive metadata
Fse.move(linkedPath, archivePath, { overwrite: true }, w());
}).nThen(function (w) {
// archive the dedicated metadata channel
var metadataPath = mkMetadataPath(env, channelName);
@ -849,6 +966,13 @@ var unarchiveChannel = function (env, channelName, cb) {
}));
}).nThen(function (w) {
clearPlaceholder(env, channelName, w());
}).nThen(function (w) {
// construct archive linked path
var archiveLinkedPath = mkArchiveLinkedPath(env, channelName);
var linkedPath = mkLinkedPath(env, channelName);
// restore the archived .linked file
// ignore errors, always try to restore metadata
Fse.move(archiveLinkedPath, linkedPath, w());
}).nThen(function (w) {
var archiveMetadataPath = mkArchiveMetadataPath(env, channelName);
// TODO validate that it's ok to move metadata non-atomically
@ -1439,6 +1563,39 @@ module.exports.create = function (conf, _cb) {
});
},
// LINKED DOCUMENTS
// get linked documents
getLinkedDocuments: (channelId, cb) => {
if (!isValidChannelId(channelId)) { return cb(new Error('EINVAL')); }
// read can be unordered
schedule.unordered(channelId, function (next) {
getLinkedDocuments(env, channelId, Util.both(cb, next));
});
},
// add linked document
addLinkedDocument: (channelId, type, data, cb) => {
if (!isValidChannelId(channelId)) { return cb(new Error('EINVAL')); }
// write to the file: apply in order
schedule.ordered(channelId, function (next) {
addLinkedDocument(env, channelId, type, data, Util.both(cb, next));
});
},
resetLinkedDocuments: (channelId, json, cb) => {
if (!isValidChannelId(channelId)) { return cb(new Error('EINVAL')); }
// write to the file: apply in order
schedule.ordered(channelId, function (next) {
resetLinkedDocuments(env, channelId, json, Util.both(cb, next));
});
},
// remove linked document
removeLinkedDocument: (channelId, type, data, cb) => {
if (!isValidChannelId(channelId)) { return cb(new Error('EINVAL')); }
// write to the file: apply in order
schedule.ordered(channelId, function (next) {
removeLinkedDocument(env, channelId, type, data, Util.both(cb, next));
});
},
// CHANNEL ITERATION
listChannels: function (handler, cb, fastMode) {
listChannels(env.root, handler, cb, fastMode);

View File

@ -702,7 +702,7 @@ const completeUpload = function (data, cb) {
Env.blobStore[method](safeKey, arg, function (err, id) {
reportStatus(Env, label, safeKey, err, id, size);
cb(err, id);
});
}, data.linked);
};
const getPinActivity = function (data, cb) {

View File

@ -10,6 +10,7 @@ const Workers = module.exports;
const PID = process.pid;
const Block = require("../storage/block");
const Environment = require('../env');
const Linked = require("../commands/linked");
const DB_PATH = 'lib/workers/db-worker';
const MAX_JOBS = 16;
@ -480,9 +481,13 @@ Workers.initialize = function (Env, config, _cb) {
}, cb);
};
Env.getFileSize = function (channel, cb) {
Env.getFileSize = function (channel, cb, singleFile) {
if (!singleFile) {
return Linked.getFileSize(Env, { channel }, cb);
}
sendCommand({
command: 'GET_FILE_SIZE',
singleFile: singleFile, // ignore linked documents
channel: channel,
}, cb);
};
@ -579,9 +584,10 @@ Workers.initialize = function (Env, config, _cb) {
}, cb);
};
Env.completeUpload = function (safeKey, arg, owned, size, cb) {
Env.completeUpload = function (safeKey, arg, owned, size, linked, cb) {
sendCommand({
command: "COMPLETE_UPLOAD",
linked,
owned: owned, // Boolean
safeKey: safeKey, // String (public key)
arg: arg, // String (file id)

186
package-lock.json generated
View File

@ -391,29 +391,43 @@
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
"@humanfs/core": "^0.19.2",
"@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@ -1120,9 +1134,9 @@
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"version": "0.8.15",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz",
"integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@ -1298,9 +1312,9 @@
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
"version": "2.11.21",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
"integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -1311,9 +1325,9 @@
}
},
"node_modules/body-parser": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"version": "1.20.8",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
"integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
@ -1324,7 +1338,7 @@
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.15.1",
"qs": "~6.16.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
@ -1363,6 +1377,22 @@
"version": "2.0.0",
"license": "MIT"
},
"node_modules/body-parser/node_modules/qs": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"license": "BSD-3-Clause",
"dependencies": {
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/body-parser/node_modules/statuses": {
"version": "2.0.2",
"license": "MIT",
@ -1395,15 +1425,15 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/braces": {
@ -1428,9 +1458,9 @@
"license": "MIT"
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"version": "4.28.9",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
"integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
"dev": true,
"funding": [
{
@ -1448,11 +1478,11 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
"baseline-browser-mapping": "^2.11.20",
"caniuse-lite": "^1.0.30001810",
"electron-to-chromium": "^1.5.420",
"node-releases": "^2.0.54",
"update-browserslist-db": "^1.3.2"
},
"bin": {
"browserslist": "cli.js"
@ -1539,9 +1569,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001770",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
"integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
"version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"dev": true,
"funding": [
{
@ -1937,9 +1967,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.302",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
"version": "1.5.423",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.423.tgz",
"integrity": "sha512-rRZfTSY8ptHYMQxa+uIycJMFKmY1T0GIApNMXJYGehguTZa56TEEl19pKPCoBqk5Gpf7QizZn/jt7xur+DYxag==",
"dev": true,
"license": "ISC"
},
@ -2391,9 +2421,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
"dev": true,
"funding": [
{
@ -2876,7 +2906,9 @@
}
},
"node_modules/http-proxy-middleware": {
"version": "3.0.5",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.7.tgz",
"integrity": "sha512-iwbQltVlx8bCrqePUM8C+hllHvdawVhQJaLrj1X7qllkvFQdXFsr16pW/mo9+JDVjN+QO2XUx9jd8SmoFkE5qw==",
"license": "MIT",
"dependencies": {
"@types/http-proxy": "^1.17.15",
@ -2887,7 +2919,7 @@
"micromatch": "^4.0.8"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
"node": "^14.18.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/http-proxy-middleware/node_modules/http-proxy": {
@ -3066,8 +3098,20 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@ -3536,7 +3580,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true,
"funding": [
{
@ -3573,11 +3619,14 @@
"license": "LGPL-2.1"
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"version": "2.0.54",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
@ -3816,9 +3865,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.12",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
"version": "8.5.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz",
"integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==",
"dev": true,
"funding": [
{
@ -3836,7 +3885,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@ -3888,9 +3937,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz",
"integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3965,12 +4014,13 @@
}
},
"node_modules/qs": {
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
@ -4314,14 +4364,14 @@
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"object-inspect": "^1.13.4",
"side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
@ -4951,9 +5001,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"dev": true,
"funding": [
{
@ -5064,9 +5114,9 @@
}
},
"node_modules/ws": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"version": "8.21.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"

View File

@ -1339,7 +1339,7 @@ const factory = (UserObject, Util, Hash,
var data = userObject.getFileData(fileId);
if (!data) { return; }
// Pin onlyoffice checkpoints
if (data.lastVersion) {
if (data.lastVersion ) {
var otherChan = Hash.hrefToHexChannelId(data.lastVersion);
result.add(otherChan);
}
@ -1438,6 +1438,7 @@ const factory = (UserObject, Util, Hash,
});
return all;
};
/*
const findMissingRtChannel = (Env) => {
const userObjects = _getUserObjects(Env);
const all = [];
@ -1449,6 +1450,7 @@ const factory = (UserObject, Util, Hash,
});
return all;
};
*/
var create = function (proxy, data, uoConfig) {
var Env = {
@ -1489,19 +1491,6 @@ const factory = (UserObject, Util, Hash,
delete Env.unpinPads;
};
let rtChannelTo;
const setRtChannelTo = () => {
clearTimeout(rtChannelTo);
rtChannelTo = setTimeout(() => {
if (Env.store.offline) { return void setRtChannelTo(); }
const list = findMissingRtChannel(Env);
Env.Store.fixMissingRtChannelInterval(list, () => {
setRtChannelTo();
});
}, 120000);
};
setRtChannelTo();
return {
// Manager
addProxy: callWithEnv(addProxy),

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
(() => {
const factory = (Util, Hash, Realtime, Feedback) => {
const factory = (Util, Hash, Realtime) => {
let window = globalThis;
var module = {};
@ -68,6 +68,8 @@ const factory = (Util, Hash, Realtime, Feedback) => {
var data = exp.getFileData(id, true);
if (attr === "href") {
exp.setHref(null, id, value);
} else if (!value) {
delete data[attr];
} else {
data[attr] = clone(value);
}
@ -86,14 +88,9 @@ const factory = (Util, Hash, Realtime, Feedback) => {
if (readOnly) { return void cb('EFORBIDDEN'); }
var id = Util.createRandomInteger();
var data = clone(_data);
let parsed = Hash.parsePadUrl(data.roHref || data.href);
// If we were given an edit link, encrypt its value if needed
if (data.href && data.href.indexOf('#') !== -1) { data.href = exp.cryptor.encrypt(data.href); }
if (['sheet', 'doc', 'presentation'].includes(parsed?.type) && !data.rtChannel) {
Feedback.send('PUSH_DATA_MISSING_RT_CHANNEL', true);
}
files[FILES_DATA][id] = data;
cb(null, id);
};
@ -891,7 +888,7 @@ const factory = (Util, Hash, Realtime, Feedback) => {
// toClean.push(id);
}
if (['sheet', 'doc', 'presentation'].includes(parsed.type) && !el.rtChannel) {
if (!el.linked && ['sheet', 'doc', 'presentation'].includes(parsed.type) && !el.rtChannel) {
missingRtChannel[el.channel] = el;
}
@ -1027,15 +1024,13 @@ if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require('./common-util'),
require('./common-hash'),
require('./common-realtime'),
require('./common-feedback'),
require('./common-realtime')
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-realtime.js',
'/common/common-feedback.js',
'/common/common-realtime.js'
], factory);
} else {
// unsupported initialization

View File

@ -8,7 +8,7 @@ const factory = (Sortify, UserObject, ProxyManager,
SF, AccountTS, DriveTS, PadTS, Form, Cursor,
Support, Integration, OnlyOffice,
Mailbox, Profile, Team, Messenger, History,
Calendar, BadgeTS, Block, NetConfig,
Calendar, BadgeTS, LinkedTS, Block, NetConfig,
Crypto, ChainPad, CpNetflux, Listmap,
Netflux, nThen) => {
@ -16,6 +16,7 @@ const factory = (Sortify, UserObject, ProxyManager,
const Drive = DriveTS.Drive;
const Pad = PadTS.Pad;
const Badge = BadgeTS.Badge;
const LinkedDoc = LinkedTS.LinkedDoc;
const window = globalThis;
globalThis.nacl = globalThis.nacl || Crypto.Nacl;
@ -392,7 +393,8 @@ const factory = (Sortify, UserObject, ProxyManager,
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.uploadStatus({
id: data.id,
size: data.size
size: data.size,
linked: data.linked
}, function (err, res) {
if (err) { return void cb({error:err}); }
cb(res);
@ -736,7 +738,7 @@ const factory = (Sortify, UserObject, ProxyManager,
});
if (['doc', 'sheet', 'presentation'].includes(parsed.type)) {
if (!pad.rtChannel) {
if (!pad.rtChannel && !pad.linked) {
return getRtChannelFromPad(pad, (err, rtChannel) => {
const key = 'ADDPAD_NO_RT_CHANNEL' ;
if (!err) {
@ -1785,6 +1787,7 @@ const factory = (Sortify, UserObject, ProxyManager,
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
if (obj && obj.error) { return; }
if (!obj.mailbox) { return; }
if (!store.loggedIn) { return; }
// Decrypt the mailbox
var crypto = Crypto.createEncryptor(secret.keys);
@ -1891,7 +1894,7 @@ const factory = (Sortify, UserObject, ProxyManager,
if (msg) {
msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, '');
//var decryptedMsg = crypto.decrypt(msg, true);
if (data.debug) {
if (data.debug || data.full) {
msgs.push({
serverHash: msg.slice(0,64),
msg: msg,
@ -2428,6 +2431,7 @@ const factory = (Sortify, UserObject, ProxyManager,
loadUniversal(Messenger, 'messenger', waitFor);
loadUniversal(History, 'history', waitFor);
loadUniversal(Badge, 'badge', waitFor);
loadUniversal(LinkedDoc, 'linked-doc', waitFor);
loadOnlyOffice();
if (store) {
store.messenger = store.modules['messenger'];
@ -3126,6 +3130,7 @@ module.exports = factory(
require('./modules/history'),
require('./modules/calendar'),
require('./modules/badge'), // .ts
require('./modules/linked'), // .ts
require('../common/outer/login-block'),
require('../common/network-config'),
require('chainpad-crypto'),

View File

@ -85,13 +85,33 @@ const factory = (Util, Hash, UserObject, nThen) => {
return team.rpc;
};
var getHistoryData = function (ctx, channel, lastKnownHash, teamId, _cb) {
/*
Before (old cp still active or checking from drive)
- for each channel, call getHistoryData
- get file total size
- get required history size (data to preserve)
- get metadata size (to preserver)
- compute removable history size
After
- only one channel
- call getHistorySize from lib/commands/linked.js
- call getTotalSize from lib/commands/linked.js
*/
var getHistoryData = function (ctx, channel, lastKnownHash, teamId, linked, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var edPublic = getEdPublic(ctx, teamId);
var Store = ctx.Store;
var total = 0;
var history = 0;
var dataSize = 0;
var metadata = 0;
var hash;
nThen(function (waitFor) {
@ -110,27 +130,42 @@ const factory = (Util, Hash, UserObject, nThen) => {
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 (linked) {
Store.anonRpcMsg('', {
msg: 'GET_HISTORY_SIZE',
data: { channel }
}, waitFor(obj => {
if (obj?.error) {
waitFor.abort();
return void cb(obj);
}
let value = obj[0];
dataSize = value?.size || 0;
hash = value?.hash;
}));
} else {
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; }
if (!obj.length) { return; }
hash = obj[0].hash;
var messages = obj.map(function(data) {
return data.msg;
});
history = messages.join('\n').length;
}), true);
hash = obj[0].hash;
var messages = obj.map(function(data) {
return data.msg;
});
dataSize = messages.join('\n').length;
}), true);
}
// Metadata
Store.pad.getMetadata(null, {
channel: channel
@ -146,7 +181,8 @@ const factory = (Util, Hash, UserObject, nThen) => {
}));
}).nThen(function () {
cb({
size: (total - metadata - history),
total,
size: (total - metadata - dataSize),
hash: hash
});
});
@ -154,7 +190,10 @@ const factory = (Util, Hash, UserObject, nThen) => {
};
commands.GET_HISTORY_SIZE = function (ctx, data, cId, cb) {
if (!ctx.store.loggedIn || !ctx.store.rpc) { return void cb({ error: 'INSUFFICIENT_PERMISSIONS' }); }
if (!ctx.store.loggedIn || !ctx.store.rpc || !ctx.store.anon_rpc) {
return void cb({ error: 'INSUFFICIENT_PERMISSIONS' });
}
var channels = data.channels;
if (!Array.isArray(channels)) { return void cb({ error: 'EINVAL' }); }
@ -168,6 +207,7 @@ const factory = (Util, Hash, UserObject, nThen) => {
}
var size = 0;
var total = 0;
var res = [];
nThen(function (waitFor) {
channels.forEach(function (chan) {
@ -177,12 +217,13 @@ const factory = (Util, Hash, UserObject, nThen) => {
channel = chan.channel;
lastKnownHash = chan.lastKnownHash;
}
getHistoryData(ctx, channel, lastKnownHash, data.teamId, waitFor(function (obj) {
getHistoryData(ctx, channel, lastKnownHash, data.teamId, data.linked, waitFor(function (obj) {
if (obj && obj.error) {
warning.push(obj.error);
return;
}
size += obj.size;
total += obj.total;
if (!obj.hash) { return; }
res.push({
channel: channel,
@ -194,11 +235,13 @@ const factory = (Util, Hash, UserObject, nThen) => {
cb({
warning: warning.length ? warning : undefined,
channels: res,
size: size
size: size, // history size
total // total size
});
});
};
// XXX
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;

View File

@ -0,0 +1,161 @@
// 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 nThen from 'nthen';
import { Module, ModuleObject, Command } from '../types'
import * as Util from '../../common/common-util.js';
import Sortify from 'json.sortify';
export interface LinkedDocModule<T> extends Module<T> {
setCustomize: (data: any) => void
}
let ApiConfig:any = {};
const getLinkedDocuments = (ctx, channel, cb) => {
ctx.Store.anonRpcMsg('', {
msg: 'GET_LINKED_DOCUMENTS',
data: { channel }
}, cb);
};
const resetLinkedDocuments = (ctx, data, cb) => {
ctx.Store.anonRpcMsg('', {
msg: 'RESET_LINKED_DOCUMENTS',
data // { channel, user, content, netfluxId, proof }
}, cb);
};
const addLinkedDocument = (ctx, data, cb) => {
ctx.Store.anonRpcMsg('', {
msg: 'ADD_LINKED_DOCUMENT',
data // { channel, user, content, netfluxId, proof }
}, cb);
};
const signData = (data, signKey) => {
try {
const edPrivate = Util.decodeBase64(signKey); // pad signing key
const msg = Util.decodeUTF8(JSON.stringify(data));
data.proof = Util.encodeBase64(nacl.sign.detached(msg, edPrivate));
return data;
} catch (e) {
return false;
}
};
// TODO
/*
- one RPC to add or remove multiple elements?
- add a single element (checkpoint or image?)
- remove single?
*/
const getLinkedData:Command = (ctx, data, clientId, cb) => {
const { channel } = data;
getLinkedDocuments(ctx, channel, obj => {
if (obj?.error) { return void cb(obj); }
const json = obj?.[0] || {};
cb(json);
});
};
const checkCurrentDoc:Command = (ctx, data, clientId, cb) => {
const { channel, expectedJSON, signKey64 } = data;
const missing = {};
getLinkedDocuments(ctx, channel, (obj) => {
const json = obj?.[0] || {};
json?.checkpoints?.forEach(obj => {
delete obj.time;
delete obj.user;
});
expectedJSON.media ||= [];
expectedJSON.checkpoints ||= [];
expectedJSON.channels ||= [];
if (Sortify(json) === Sortify(expectedJSON)) { return void cb(); }
// "user" won't be encrypted so we can't add the username
const user = ctx.store.proxy.edPublic || 'GUEST';
// Add netfluxId to guard against replay attacks
const netfluxId = ctx.store.network?.webChannels?.[0]?.myID;
const toSend = signData({
user, channel, netfluxId,
content: expectedJSON
}, signKey64);
if (!toSend) {
return void cb({error: 'SIGN_ERROR'});
}
resetLinkedDocuments(ctx, toSend, cb);
});
};
const addLinkedData:Command = (ctx, data, clientId, cb) => {
const { channel, content, signKey64 } = data;
// content.type, content.data
// "user" won't be encrypted so we can't add the username
const user = ctx.store.proxy.edPublic || 'GUEST';
// Add netfluxId to guard against replay attacks
const netfluxId = ctx.store.network?.webChannels?.[0]?.myID;
const toSend = signData({
user, channel, netfluxId, content
}, signKey64);
if (!toSend) {
return void cb({error: 'SIGN_ERROR'});
}
addLinkedDocument(ctx, toSend, cb);
};
const LinkedDoc: LinkedDocModule<ModuleObject> = {
init: (config, cb, emit) => {
const ctx:any = {
store: config.store,
Store: config.Store
//updateMetadata: config.updateMetadata
};
return {
removeClient: () => {},
execCommand: (clientId, obj, cb) => {
if (!ctx.store?.network?.webChannels.length ||
!ctx.store?.ready) {
return void cb({error: 'OFFLINE'});
}
const cmd = obj.cmd;
const data = obj.data;
if (cmd === 'CHECK_CURRENT_DOC') {
return void checkCurrentDoc(ctx, data, clientId, cb);
}
if (cmd === 'GET_LINKED_DATA') {
return void getLinkedData(ctx, data, clientId, cb);
}
if (cmd === 'ADD_LINKED_DATA') {
return void addLinkedData(ctx, data, clientId, cb);
}
cb();
},
}
},
setCustomize: data => {
ApiConfig = data?.ApiConfig;
}
};
export { LinkedDoc }

View File

@ -27,15 +27,26 @@ const factory = (Feedback) => {
var first = true;
var c = ctx.clients[client];
if (!c) {
var chan = ctx.channels[channel];
if (!c) { // new tab
c = ctx.clients[client] = {
channel: channel,
};
} else {
} else if (c?.channel !== channel) { // new channel on existing tab
// Remove client from existing chan
// and disconnect from chan if needed
ctx.removeClient(client, true);
c = ctx.clients[client] = {
channel: channel,
};
} else { // same channel existing tab
setTimeout(() => {
ctx.emit('READY', chan.clients, [client]);
});
return void cb();
}
var chan = ctx.channels[channel];
if (chan) {
// This channel is already open in another tab
@ -114,8 +125,7 @@ const factory = (Feedback) => {
metadata: {
//forcePlaceholder: true,
validateKey: obj.validateKey,
owners: obj.owners,
expire: obj.expire
linked: obj.padChan
}
};
var msg = ['GET_HISTORY', wc.id, cfg];
@ -209,25 +219,6 @@ const factory = (Feedback) => {
});
};
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' }); }
@ -255,18 +246,40 @@ const factory = (Feedback) => {
var channel = data.channel;
var network = ctx.store.network;
var hk = network.historyKeeper;
const txid = Math.floor(Math.random() * 1000000);
var onOpen = function (wc) {
var hk = network.historyKeeper;
const sendEncrypted = () => {
data.msgs.forEach(function (msg) {
wc.bcast(msg);
});
wc.leave();
cb();
};
const onDirectMessage = (msg, sender) => {
// Ignore messages for others
if (sender !== hk) { return; }
try {
const parsed = JSON.parse(msg);
if (parsed?.txid !== txid) { return; }
if (!parsed.channel) { return; }
// Remove listener and send re-encrypted messages
network.off('message', onDirectMessage);
sendEncrypted();
} catch (e) { console.error(e); }
};
network.on('message', onDirectMessage);
var cfg = {
txid: txid,
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) {
@ -298,7 +311,7 @@ const factory = (Feedback) => {
};
// Remove the client from all its channels when a tab is closed
var removeClient = function (ctx, clientId) {
var removeClient = function (ctx, clientId, newChan) {
var filter = function (c) {
return c !== clientId;
};
@ -314,7 +327,7 @@ const factory = (Feedback) => {
}
}
if (ctx.clients[clientId]) {
if (ctx.clients[clientId] && !newChan) {
var oldChannel = ctx.clients[clientId].channel;
var oldChan = ctx.channels[oldChannel];
if (oldChan) {
@ -335,8 +348,8 @@ const factory = (Feedback) => {
clients: {}
};
oo.removeClient = function (clientId) {
removeClient(ctx, clientId);
oo.removeClient = ctx.removeClient = function (clientId, newChan) {
removeClient(ctx, clientId, newChan);
};
oo.leavePad = function (padChan) {
leaveChannel(ctx, padChan);
@ -347,9 +360,6 @@ const factory = (Feedback) => {
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);
}

View File

@ -29,6 +29,7 @@ export interface Module<T> {
init: (config: ModuleConfig, cb: Callback, emit: Callback) => T
}
export type Command = (ctx: any, data: any, clientId: string, cb: Callback) => void;
export type AccountConfig = {
anonHash: string,

View File

@ -1717,6 +1717,9 @@ define([
'role': 'menu',
'tabindex': '-1'
});
if (config.smallContent) {
$innerblock.addClass('cp-small');
}
var $outerblock = $(h('div.cp-dropdown-menu-container', $innerblock[0]));
let $parentMenu = config.isSubmenuOf;
$container.$menu = $innerblock;

View File

@ -635,8 +635,8 @@ define([
});
};
common.uploadStatus = function (teamId, id, size, cb) {
postMessage("UPLOAD_STATUS", {teamId, id, size}, function (obj) {
common.uploadStatus = function (data, cb) {
postMessage("UPLOAD_STATUS", data, function (obj) {
if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj);
});
@ -1386,6 +1386,16 @@ define([
cb();
};
const sendUniversalCmd = (type, cmd, data, cb) => {
universal.execCommand({
type,
data: {
cmd,
data
}
}, cb);
};
common.changePadPassword = function (Crypt, Crypto, data, cb) {
var href = data.href;
var oldPassword = data.oldPassword;
@ -1729,6 +1739,7 @@ define([
var oldMetadata;
var oldRtChannel;
var privateData;
var lastCp;
var newSecret;
if (parsed.hashData.version >= 2) {
@ -1844,14 +1855,36 @@ define([
}), optsGet);
}).nThen(function (waitFor) {
// Re-encrypt rtchannel
oldRtChannel = Util.find(cryptgetVal, ['content', 'channel']);
var newCrypto = Crypto.createEncryptor(newSecret.keys);
var oldCrypto = Crypto.createEncryptor(oldSecret.keys);
var cps = Util.find(cryptgetVal, ['content', 'hashes']);
var cpLength = Object.keys(cps).length;
var lastCp = cpLength ? cps[cpLength] : {};
var cps = cryptgetVal?.content?.hashes;
var sortedCpIdx = Object.keys(cps).map(Number).sort();
var lastCpIdx = sortedCpIdx.pop();
lastCp = cps[lastCpIdx] || {};
oldRtChannel = lastCp?.rtChannel ||
cryptgetVal?.content?.channel;
cryptgetVal.content.hashes = {};
common.getHistory({
// Update rtChannel in content
if (lastCpIdx) {
cryptgetVal.content.hashes[lastCpIdx] = {
file: lastCp.file,
rtChannel: newRtChannel,
version: lastCp.version
};
delete cryptgetVal.content.channel;
} else {
cryptgetVal.content.channel = newRtChannel;
}
// Support old and new cp format
let f = common.getHistory;
if (!lastCp?.hash) { f = common.getFullHistory; }
f({
channel: oldRtChannel,
lastKnownHash: lastCp.hash
}, waitFor(function (obj) {
@ -1860,6 +1893,7 @@ define([
console.error(obj);
return void cb(obj.error);
}
// Re-encrypt messages
var msgs = obj;
var newHistory = msgs.map(function (str) {
try {
@ -1871,18 +1905,16 @@ define([
return void cb({error: e});
}
});
// Update last knwon hash in cryptgetVal
if (cpLength && newHistory.length) {
lastCp.hash = newHistory[0].slice(0, 64);
lastCp.index = 50;
cryptgetVal.content.hashes[1] = lastCp;
}
common.onlyoffice.execCommand({
cmd: 'REENCRYPT',
data: {
channel: newRtChannel,
msgs: newHistory,
metadata: optsPut.metadata
metadata: {
linked: newSecret.channel,
validateKey: optsPut.metadata.validateKey
}
}
}, waitFor(function (obj) {
if (obj && obj.error) {
@ -1894,9 +1926,8 @@ define([
}));
Cache.clearChannel(newSecret.channel, waitFor());
}).nThen(function (waitFor) {
// The new rt channel is ready
// The blob uses its own encryption and doesn't need to be reencrypted
cryptgetVal.content.channel = newRtChannel;
// The new rt channel is ready, we can create the new chainpad
if (!newPassword) { optsPut.metadata.forcePlaceholder = true; }
Crypt.put(newHash, JSON.stringify(cryptgetVal), waitFor(function (err) {
if (err) {
@ -1904,6 +1935,60 @@ define([
return void cb({ error: err });
}
}), optsPut);
}).nThen(function (waitFor) {
// The blob uses its own encryption and doesn't need to be
// reencrypted but we should make sure it's linked to the
// newSecret.channel
if (!lastCp.file) { return; }
const parsed = Hash.parsePadUrl(lastCp.file);
const fileSecret = Hash.getSecrets(parsed.type, parsed.hash);
common.setPadMetadata({
channel: fileSecret.channel,
command: 'SET_LINKED',
value: newSecret.channel
}, waitFor());
// If we had a checkpoint, make sure it won't be destroyed
// alongside the old document. We must remove it from the
// "linked" data
// Add the new "linked" data to the new document
const cp = {
blob: fileSecret.channel,
rtChannel: newRtChannel
};
const newLinkedData = { checkpoints: [cp] };
const newSigningKey = newSecret.keys?.signKey;
sendUniversalCmd('linked-doc', 'CHECK_CURRENT_DOC', {
channel: newSecret.channel,
expectedJSON: newLinkedData,
signKey64: newSigningKey
}, waitFor());
// Update old doc "linked" data
const oldSigningKey = oldSecret.keys?.signKey;
sendUniversalCmd('linked-doc', 'GET_LINKED_DATA', {
channel: oldSecret.channel
}, waitFor((json) => {
if (!json || json.error) { return; }
// Remove last cp "blob" from old "linked" data
// because we're re-using the same blob
const cp = json.checkpoints || [];
const lastCp = cp.pop();
// But preserve the associated rtChannel as "linked" so
// that this rtChannel can be destroyed with the main doc
if (lastCp?.rtChannel) {
json.channels ||= [];
json.channels.push(lastCp.rtChannel);
}
sendUniversalCmd('linked-doc', 'CHECK_CURRENT_DOC', {
channel: oldSecret.channel,
expectedJSON: json,
signKey64: oldSigningKey
}, waitFor());
}));
}).nThen(function (waitFor) {
pad.leavePad({
channel: oldSecret.channel
@ -1917,15 +2002,18 @@ define([
common.setPadAttribute('channel', newSecret.channel, waitFor(function (err) {
if (err) { warning = true; }
}), href);
common.setPadAttribute('rtChannel', newRtChannel, waitFor(function (err) {
if (err) { warning = true; }
}), href);
var viewHash = Hash.getViewHashFromKeys(newSecret);
newRoHref = '/' + parsed.type + '/#' + viewHash;
common.setPadAttribute('roHref', newRoHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
// Remove rtChannel and last cp data and mark as "linked"
common.setPadAttribute('linked', true, waitFor(() => {}), href);
common.setPadAttribute('rtChannel', void 0, waitFor(() => {}), href);
common.setPadAttribute('lastVersion', void 0, waitFor(() => {}), href);
common.setPadAttribute('lastCpHash', void 0, waitFor(() => {}), href);
if (parsed.hashData.password && newPassword) { return; } // same hash
common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; }

View File

@ -19,6 +19,7 @@ define([
var getPadProperties = function (Env, data, opts, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var common = Env.common;
var sframeChan = common.getSframeChannel();
opts = opts || {};
var $d = $('<div>');
if (!data) { return void cb(void 0, $d); }
@ -32,6 +33,7 @@ define([
data.rtChannel = data.rtChannel || p.rtChannel;
data.lastVersion = data.lastVersion || p.lastVersion;
data.lastCpHash = data.lastCpHash || p.lastCpHash;
data.linked ||= p.linked;
}
if (data.channel) {
@ -78,12 +80,14 @@ define([
var bytes = 0;
var historyBytes;
var chan = [data.channel];
if (data.answersChannel) { chan.push(data.answersChannel); }
if (data.rtChannel) { chan.push(data.rtChannel); }
if (data.lastVersion) { chan.push(Hash.hrefToHexChannelId(data.lastVersion)); }
if (!data?.linked) {
if (data.answersChannel) { chan.push(data.answersChannel); }
if (data.rtChannel) { chan.push(data.rtChannel); }
if (data.lastVersion) { chan.push(Hash.hrefToHexChannelId(data.lastVersion)); }
}
// Get the channels with history (no blobs)
var channels = chan.filter(function (c) { return c.length === 32; }).map(function (id) {
var channels = chan.filter(c => c.length === 32).map(id => {
if (id === data.rtChannel && data.lastVersion && data.lastCpHash) {
return {
channel: id,
@ -98,25 +102,29 @@ define([
var history = common.makeUniversal('history');
var trimChannels = [];
nThen(function (waitFor) {
// Get total size
chan.forEach(function (c) {
common.getFileSize(c, waitFor(function (e, _bytes) {
if (e) {
// there was a problem with the RPC
console.error(e);
}
bytes += _bytes;
}), true);
});
if (!owned) {
// Get total size
chan.forEach(function (c) {
common.getFileSize(c, waitFor(function (e, _bytes) {
if (e) {
// there was a problem with the RPC
console.error(e);
}
bytes += _bytes;
}), true);
});
return;
}
if (!owned) { return; }
// Get history size
// Get data and history size
history.execCommand('GET_HISTORY_SIZE', {
pad: true,
linked: data?.linked,
channels: channels,
teamId: typeof(owned) === "number" && owned
}, waitFor(function (obj) {
if (obj && obj.error) { return; }
bytes = obj.total;
historyBytes = obj.size;
trimChannels = obj.channels;
}));
@ -168,23 +176,29 @@ define([
spinner.spin();
history.execCommand('TRIM_HISTORY', {
pad: true,
linked: data?.linked,
channels: trimChannels,
teamId: typeof(owned) === "number" && owned
}, function (obj) {
spinner.hide();
if (obj && obj.error || obj.warning) {
if (obj && obj.error || obj.warning) {
console.error(obj.warning);
$(size).append(h('div.alert.alert-danger', Messages.trimHistory_error));
spinner.hide();
return;
}
$(size).remove();
var formatted = UIElements.prettySize(bytes - historyBytes);
$d.append(h('div.cp-app-prop', [
Messages.upload_size,
h('br'),
h('span.cp-app-prop-content', formatted)
]));
$d.append(h('div.alert.alert-success', Messages.trimHistory_success));
sframeChan.query('Q_TRIM_HISTORY', {
href: data.href
}, function () {
spinner.hide();
$(size).remove();
var formatted = UIElements.prettySize(bytes - historyBytes);
$d.append(h('div.cp-app-prop', [
Messages.upload_size,
h('br'),
h('span.cp-app-prop-content', formatted)
]));
$d.append(h('div.alert.alert-success', Messages.trimHistory_success));
});
});
});

View File

@ -5,14 +5,93 @@
define([
'jquery',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/hyperscript.js',
'/common/common-icons.js',
'/common/common-util.js',
], function ($, UI, h, Icons, Util) {
], function ($, UI, UIElements, h, Icons, Util) {
//var ChainPad = window.ChainPad;
var History = {};
History.sortCpIndex = function (hashes) {
return Object.keys(hashes).map(Number).sort((a, b) => {
return a-b;
});
};
History.loadHistoryData = (cfg) => {
const {
sframeChan, mainRtChannel, downloadId,
currentCp, nextCp, href, password
} = cfg;
return new Promise((resolve, reject) => {
// New CP: use this cp's rtChannel
if (currentCp?.rtChannel) {
// Load all messages from currentCp.rtChannel
sframeChan.query('Q_GET_FULL_HISTORY', {
href, password, // get secret from other pad (template)
channel: currentCp.rtChannel,
isDownload: downloadId,
full: true
}, function (err, data) {
if (err) { return void reject(err); }
resolve(data);
});
return;
}
// Old CP or no CP: use mainRtChannel
if (!mainRtChannel) {
return void reject('EINVAL');
}
// No CP and no nextCp hash
if (!currentCp?.file && !nextCp?.hash) {
// Load all messages from mainRtChannel
sframeChan.query('Q_GET_FULL_HISTORY', {
href, password, // get secret from other pad (template)
channel: mainRtChannel,
isDownload: downloadId,
full: true
}, function (err, data) {
if (err) { return void reject(err); }
resolve(data);
});
return;
}
// If we have a startHash or an endHash, use
// the GET_HISTORY_RANGE command
let startHash = currentCp?.hash || 'NONE';
let endHash = nextCp?.hash;
if (currentCp?.hash || nextCp?.hash) {
sframeChan.query('Q_GET_HISTORY_RANGE', {
href, password, // get secret from other pad (template)
channel: mainRtChannel,
lastKnownHash: endHash,
toHash: startHash,
isDownload: downloadId,
}, function (err, data) {
if (err || !Array.isArray(data.messages)) {
return void reject(err || 'EINVAL');
}
let msgs = data.messages;
if (data.messages[0].serverHash === startHash) {
msgs.shift();
}
resolve(msgs);
});
return;
}
reject('INVALID_CP');
});
};
History.create = function (common, config) {
if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");}
if (History.loading) { return void console.error("History is already being loaded..."); }
@ -25,537 +104,268 @@ define([
throw new Error("Missing config element");
}
var cpIndex = -1;
var msgIndex = -1;
var ooMessages = {};
var msgs;
var loading = false;
var currentTime;
//Defining position here means it can be passed to the showVersion and share functions
var position;
//Defining patch here means it can be passed to the snapshot function
var patch;
var currentVersion;
var forward;
var revertCheckpoint;
var previousRevertCheckpoint;
const metadataMgr = common.getMetadataMgr();
// Get an array of the checkpoint IDs sorted their patch index
var hashes = config.onlyoffice.hashes;
var id;
var sortedCp = Object.keys(hashes).map(Number);
const ooMessages = {};
var getId = function () {
var cps = sortedCp.length;
id = sortedCp[cps -1] || -1;
return id;
const hashes = config.onlyoffice.hashes;
const mainRtChannel = config.onlyoffice.channel;
const ctime = config.onlyoffice.ctime;
const sortedCp = History.sortCpIndex(hashes);
let cpIdx = sortedCp.length - 1;
let msgIdx = 0;
let loading = true;
const getCpId = () => {
return sortedCp[cpIdx] || 0;
};
const getCpMsgs = () => {
return ooMessages[getCpId()] || [];
};
const getCurrentMsg = () => {
const msgs = getCpMsgs();
return msgs[msgIdx];
};
const getCurrentVersion = () => {
return `${getCpId()}.${msgIdx}`;
};
var endWithCp = sortedCp.length &&
config.onlyoffice.lastHash === hashes[sortedCp[sortedCp.length - 1]].hash;
const loadMessages = () => {
return new Promise((resolve, reject) => {
let cpId = getCpId();
var fillOO = function (messages) {
ooMessages = {};
ooMessages[id] = messages;
if (Array.isArray(ooMessages[cpId])) {
return void resolve(ooMessages[cpId]);
}
let currentCp = hashes[cpId];
let nextCp = hashes[sortedCp[cpIdx + 1]];
History.loadHistoryData({
sframeChan,
mainRtChannel, hashes, sortedCp, currentCp, nextCp
}).then(data => {
data.unshift(void 0);
ooMessages[cpId] = data;
resolve(data);
}).catch(reject);
});
};
if (endWithCp) { cpIndex = 0; }
var $version, $share;
var $hist = $toolbar.find('.cp-toolbar-history');
let $version, $share, $timeline;
let $next, $prev;
const $hist = $toolbar.find('.cp-toolbar-history');
$hist.addClass('cp-smallpatch');
$hist.addClass('cp-history-oo');
var $bottom = $toolbar.find('.cp-toolbar-bottom');
var Messages = common.Messages;
const $bottom = $toolbar.find('.cp-toolbar-bottom');
const Messages = common.Messages;
var getVersion = function (position, initial, revert) {
let version = (id === -1 || id === 0) ? 0 : id;
if (!Object.keys(ooMessages).length) {
return '0.0';
}
if (typeof(position) === "undefined" || position === -1) {
position = ooMessages[id]?.length || 0;
} else if (msgs?.length === position &&
id !== parseInt(Object.keys(hashes)[Object.keys(hashes).length-1]) &&
!initial && !revert && $(`[data^="${id+1},"][data*=","]`).length > 1) {
version = id+1;
position = 0;
}
return version + '.' + position;
};
var getMessages = function(fromHash, toHash, callback) {
sframeChan.query('Q_GET_HISTORY_RANGE', {
channel: config.onlyoffice.channel,
lastKnownHash: fromHash,
toHash: toHash,
}, function (err, data) {
if (err) { return void console.error(err); }
if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); }
var isEmptyPatch = function(msg) {
return msg?.changes?.length === 2 &&
msg.changes.some(c => c.change.includes('64;AgAAA')) &&
msg.changes.some(c => c.change.includes('18;BgAAA') || c.change.includes('23;BgAAAD'));
};
var messages;
if (data.messages[0] && hashes[id]?.index > JSON.parse(data.messages?.[0]?.msg).changesIndex+1 ) {
messages = [];
} else if (config.docType() === 'spreadsheet' && toHash === 'NONE') {
messages = (data.messages || []);
} else {
messages = (data.messages || []).slice(1);
}
if (revertCheckpoint && !forward) {
previousRevertCheckpoint = revertCheckpoint;
}
if (messages[0] && isEmptyPatch(JSON.parse(messages[0].msg))) {
revertCheckpoint = false;
messages.splice(0, 1);
} else if (messages[1] && isEmptyPatch(JSON.parse(messages[1].msg))) {
revertCheckpoint = false;
messages.splice(1, 1);
} else if (id === 0) {
revertCheckpoint = false;
} else {
revertCheckpoint = true;
}
if (config.debug) { console.log(data.messages); }
id = typeof(id) !== "undefined" ? id : getId();
fillOO(messages);
loading = false;
callback(null, messages);
});
};
// We want to load a checkpoint (or initial state)
var loadMoreOOHistory = function () {
return new Promise((resolve, reject) => {
if (!Array.isArray(sortedCp)) {
console.error("Wrong type");
return reject();
}
// Get the checkpoint ID
id = typeof(id) !== "undefined" ? id : getId();
var cp = hashes[id];
// Get the history between "toHash" and "fromHash". This function is using
// "getOlderHistory", that's why we start from the more recent hash
// and we go back in time to an older hash
// We need to get all the patches between the current cp hash and the next cp hash
var nextId = hashes[id+1] ? hashes[id+1] : undefined;
// Current cp or initial hash (invalid hash ==> initial hash)
var fromHash = cp?.hash || 'NONE';
// Next cp or last hash
var toHash = nextId ? nextId.hash : config.onlyoffice.lastHash;
getMessages(toHash, fromHash, function (err) {
if (err) {
console.error(err);
reject(err);
return;
}
resolve();
});
});
};
var onClose = function () { config.setHistory(false); };
var onRevert = function () {
const onClose = function () { config.setHistory(false); };
const onRevert = function () {
config.onRevert();
};
config.setHistory(true);
config.setHistory(true);
$hist.html('').css('display', 'flex');
$bottom.hide();
// UI.spinner($hist).get().show();
const updateNavButtons = () => {
const max = getCpMsgs().length - 1;
var $fastPrev, $fastNext, $next, $prev;
var updateButtons = function () {
$fastPrev.show();
$next.show();
$prev.show();
$fastNext.show();
$hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous')
.prop('disabled', '');
if ((id === -1 || id === 0) && (ooMessages[id]?.length+1 === Math.abs(msgIndex) || !ooMessages[id]?.length && id === 0)){
if (msgIdx <= 0) {
$prev.prop('disabled', 'disabled');
$fastPrev.prop('disabled', 'disabled');
} else {
$prev.prop('disabled', '');
}
var version = currentVersion.split('.');
var hashesLength = Object.keys(hashes).length;
if (currentVersion === Messages.oo_version_latest || hashesLength === parseInt(version[0]) && ooMessages[id]?.length === parseInt(version[1]) ||
hashesLength+1 === id && (msgIndex === -1) && forward ||
hashes[hashesLength-1] === id && !ooMessages[id].length && msgIndex === 0) {
if (msgIdx >= max) {
$next.prop('disabled', 'disabled');
$fastNext.prop('disabled', 'disabled');
}
};
var loadingFalse = function () {
setTimeout(function () {
$('iframe').blur();
loading = false;
}, 200);
};
var showVersion = function (initial, revert, empty) {
$('.cp-history-timeline-pos-oo').remove();
$('.cp-history-oo-timeline-pos').removeClass('cp-history-oo-timeline-pos');
var currentPatch;
if (initial) {
currentPatch = $('.cp-history-patch').last();
currentVersion = getVersion(position, initial);
} else if (empty) {
currentPatch = $(`[data="${id},0"]`);
currentVersion = getVersion(position, initial);
} else if ($(`[data="${id},${position}"]`).length) {
currentPatch = $(`[data="${id},${position}"]`);
currentVersion = getVersion(position, initial, true);
} else if (msgs?.length === position &&
id !== parseInt(Object.keys(hashes)[Object.keys(hashes).length-1]) ) {
currentPatch = $(`[data="${id+1},0"]`);
currentVersion = getVersion(position, initial);
}
if (initial || position === msgs?.length && (id === -1 ||
id === Object.keys(hashes)[Object.keys(hashes).length-1])) {
currentVersion = Messages.oo_version_latest;
}
var patchTime = patch ? new Date(patch.time).toLocaleString() : '';
$version.text(Messages.oo_version + currentVersion + ' ' + patchTime);
var pos = Icons.get('chevron-down', {'class': 'cp-history-timeline-pos-oo'});
$(currentPatch).addClass('cp-history-oo-timeline-pos').append(pos);
updateButtons();
loadingFalse();
};
var displayCheckpointTimeline = function(initial) {
var bar = $hist.find('.cp-history-timeline-container');
$(bar).addClass('cp-history-timeline-bar').addClass('cp-oohistory-bar-el');
msgs = ooMessages[id];
if (initial) {
var snapshotsEl = [];
var msgsRev = msgs;
} else {
snapshotsEl = Array.from($hist.find('.cp-history-snapshots')[0].childNodes);
msgsRev = msgs.slice().reverse();
$next.prop('disabled', '');
}
};
var cpNfInner = common.startRealtime(config);
var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
var snapshots = md.snapshots;
const updateTimeline = () => {
$timeline.empty();
const msgs = getCpMsgs();
const cp = hashes[getCpId()];
var patchWidth;
var patchDiv;
for (var i = 0; i < msgsRev.length; i++) {
var msg = msgs[i];
if (initial || id === -1) {
patchWidth = (1/msgs?.length)*100;
} else {
patchWidth = (1/(msgs?.length+Array.from($hist.find('.cp-history-snapshots')[0].childNodes).length))*100;
const md = Util.clone(metadataMgr.getMetadata());
const snapshots = md?.snapshots || {};
const els = msgs.map((msg, i) => {
const selected = i === msgIdx;
const selClass = selected ? '.cp-selected' : '';
const content = selected ? Icons.get('chevron-down', {})
: undefined;
let title = `${getCpId()}.${i}`;
const s = snapshots[title];
if (msg?.time) {
title += ' - ' + new Date(msg.time).toLocaleString();
} else if (i === 0 && cp?.time) {
title += ' - ' + new Date(cp.time).toLocaleString();
}
patchDiv = h('div.cp-history-patch', {
style: 'width:'+patchWidth+'%;',
title: new Date(msgsRev[i].time).toLocaleString(),
data: [id, msgsRev.indexOf(msg)]
});
if (initial) {
snapshotsEl.push(patchDiv);
} else {
snapshotsEl.unshift(patchDiv);
let snap;
if (s) {
if (s?.title) { title += `\n${Util.fixHTML(s.title)}`; }
snap = Icons.get('snapshot', {'data-snapshot': '1'});
}
if (snapshots) {
var match = Object.values(snapshots).find(item => item.time === msg.time);
if (match) { $(patchDiv).addClass('cp-history-snapshot').append(Icons.get('snapshot', {title: match.title})); }
}
}
var finalpatchDiv = h('div.cp-history-patch', {
style: 'width:'+patchWidth+'%; height: 100%; position: relative',
title: new Date().toLocaleString(),
data: [id, msgs?.length]
return h('span.cp-history-bar-el'+selClass, {
title,
'data-msg': i
}, [content, snap]);
});
if (initial) {
snapshotsEl.push(finalpatchDiv);
} else if (previousRevertCheckpoint) {
snapshotsEl.splice(msgs.length, 0, finalpatchDiv);
$timeline.append(els);
updateNavButtons();
};
const hideVersion = () => {
$version[0].classList.add('cp-hidden');
};
const showVersion = () => {
const patch = getCurrentMsg();
if (!patch) { return $version.hide(); }
const time = new Date(patch?.time).toLocaleString();
$version.text(`${getCurrentVersion()} - ${time}`).show();
$version[0].classList.remove('cp-hidden');
};
const prev = (i = 1) => {
if ((msgIdx - i) < 0) { loading = false; return; }
msgIdx -= i;
const msgs = getCpMsgs().slice(1, (msgIdx + 1));
const cp = hashes[getCpId()] || {};
config.onPatchBack(cp, msgs);
loading = false;
showVersion();
updateTimeline();
};
const next = (i = 1) => {
const max = getCpMsgs().length - 1;
if (msgIdx > (max - i)) { return; }
for (let j = 0; j < i; j++) {
msgIdx++;
const msg = getCurrentMsg();
if (msg) { config.onPatch(msg); }
}
showVersion();
updateTimeline();
};
if (!msgsRev.length && !Object.keys(hashes).length || initial && !msgs?.length) {
$(finalpatchDiv).css('width', '100%');
} else {
$(finalpatchDiv).css('width', `${($(snapshotsEl[snapshotsEl.indexOf(finalpatchDiv)+1])?.width()/ $(snapshotsEl[snapshotsEl.indexOf(finalpatchDiv)+1])?.parent().width())*100}%`);
patchWidth = ($(snapshotsEl[snapshotsEl.indexOf(finalpatchDiv)+1])?.width()/ $(snapshotsEl[snapshotsEl.indexOf(finalpatchDiv)+1])?.parent().width())*100;
}
// Dropdown to select checkpoint (or "major version")
const makeDropdown = ($dropdown) => {
const all = sortedCp.slice();
if (mainRtChannel) { all.unshift(0); }
const options = all.map((id, idx) => {
const cp = hashes[id] || {};
let time = '';;
let t = cp?.time || (idx === 0 && id === 0 && ctime);
if (t) { time = ` - ${new Date(t).toLocaleString()}`; }
return {
tag: 'a',
attributes: {
'class': 'cp-history-major-version',
'data-value': idx,
},
content: `${id}.x` + time
};
});
const dropdownConfig = {
text: `${getCpId()}.x`, // Button initial text
options, // Entries displayed in the menu
isSelect: true,
caretDown: true,
smallContent: true,
buttonCls: 'btn btn-default small'
};
const dd = UIElements.createDropdown(dropdownConfig);
if (mainRtChannel) { dd.setValue(cpIdx + 1); }
else { dd.setValue(cpIdx); }
dd.onChange.reg((id, idx) => {
loading = true;
dd.find('> button').attr('disabled', 'disabled');
var pos = Icons.get('chevron-down', {'class': 'cp-history-timeline-pos-oo'});
if (!mainRtChannel) { cpIdx = idx; }
else {
cpIdx = idx - 1; // -1 because we've added "0" to the list
}
var patches = h('div.cp-history-snapshots.cp-history-snapshots-oo', [
snapshotsEl
]);
$(patches).css('height', '100%');
$(patches).css('display', 'flex');
hideVersion();
bar.html('').append([
patches
]);
loadMessages().then(() => {
loading = false;
dd.find('> button').removeAttr('disabled');
if (snapshotsEl.length === 1) {
$('.cp-history-patch').css('width', '100%');
}
if (!initial) {
var finalPatchWidth = patchWidth ? patchWidth : 100/$hist.find('.cp-history-snapshots')[0].childNodes.length;
Array.from($hist.find('.cp-history-snapshots')[0].childNodes).forEach(function(patch) {
$(patch).css('width', `${finalPatchWidth}%`);
});
}
if (initial) {
$('.cp-history-patch').last().addClass('cp-history-oo-timeline-pos').append(pos);
}
msgIdx = 0;
showVersion();
updateTimeline();
$('.cp-history-patch').on('click', function(e) {
var patchData = $(e.target).attr('data').split(',');
var cpNo = parseInt(patchData[0]);
var patchNo = parseInt(patchData[1]);
id = cpNo;
loadMoreOOHistory().then(() => {
msgs = ooMessages[id];
if (cpNo === -1) {
var q = msgs.slice(0, patchNo);
config.onPatchBack({}, q);
patch = msgs[patchNo];
position = (patchNo === msgs?.length) ? msgs?.length : msgs.indexOf(patch);
msgIndex = position === -1 ? -1 : position - msgs?.length-1;
showVersion(false, true);
updateButtons();
return;
} else if (cpNo === 0 && patchNo === 0) {
config.onPatchBack({});
patch = msgs[0];
} else if (!msgs?.length ) {
q = msgs.slice(0, patchNo);
config.onPatchBack(hashes[cpNo], q);
patch = msgs[msgs?.length-1];
position = patch ? msgs.indexOf(patch)+1 : 0;
msgIndex = position === -1 ? -1 : position - msgs?.length-1;
showVersion(false, false, true);
updateButtons();
return;
} else if (patchNo === msgs?.length && msgs?.length < $(`[data^="${id},"][data*=","]`).length) {
q = msgs.slice(0, patchNo);
config.onPatchBack(hashes[cpNo], q);
patch = msgs[msgs?.length-1];
position = patch ? msgs.indexOf(patch)+1 : 0;
msgIndex = position === -1 ? -1 : position - msgs?.length-1;
showVersion(false, true);
updateButtons();
return;
} else {
q = msgs.slice(0, patchNo);
config.onPatchBack(hashes[cpNo], q);
patch = msgs[patchNo];
}
position = patch ? msgs.indexOf(patch) : 0;
msgIndex = position === -1 ? -1 : position - msgs?.length-1;
showVersion(false);
updateButtons();
const cp = hashes[getCpId()];
config.onCheckpoint(cp);
});
});
$dropdown.empty().append([
h('span', Messages.oo_version),
dd
]);
};
loadMoreOOHistory().then(() => {
displayCheckpointTimeline(true);
showVersion(true);
});
var restore;
var next = async function () {
forward = true;
msgIndex++;
msgs = ooMessages[id];
var hasHashes = Object.keys(hashes).length;
if (hasHashes) {
// /Check if the end of the checkpoint has been reached and the next one should be loaded
if (msgIndex === 0) {
id++;
await loadMoreOOHistory();
msgs = ooMessages[id];
// Empty checkpoint (checkpoint created/history restored with no further changes)
if (!msgs?.length) {
config.loadHistoryCp(hashes[id]);
msgs = ooMessages[id];
msgIndex = -msgs?.length;
patch = msgs[0];
position = 0;
showVersion(false);
return;
}
//Is the checkpoint the result of restoring history? If yes, we need to load an extra patch
if (!revertCheckpoint) {
msgIndex = -msgs?.length;
config.onPatchBack(hashes[id], [msgs[0]]);
position = 1;
showVersion(false);
} else {
msgIndex = -msgs?.length - 1;
config.loadHistoryCp(hashes[id]);
position = 0;
showVersion(false, true);
}
return;
}
if (!msgs?.length) {
position = 0;
msgIndex = -1;
id++;
showVersion(false);
return config.loadHistoryCp(hashes[id]);
}
// Adjust msgIndex after fastPrev
if (Math.abs(msgIndex) > msgs?.length) { msgIndex = -msgs?.length; }
}
else if (msgs?.length + msgIndex === -1) { msgIndex++; }
patch = msgs[msgs?.length + msgIndex];
position = msgs.indexOf(patch) + 1;
config.onPatch?.(patch);
msgIndex === -1
? showVersion(false, true)
: showVersion(false);
};
var prev = function () {
forward = false;
msgs = ooMessages[id];
let hasHashes = Object.keys(hashes).length;
let cp = hasHashes ? hashes[id] : {};
let loadPrevCp = (!msgs?.length) ||
(msgs?.length + 1 === Math.abs(msgIndex) && id !== 0) ||
(msgs?.length - Math.abs(msgIndex) === -2);
var isRevert = revertCheckpoint;
//Check if the end of the checkpoint has been reached and the previous one should be loaded
if (hasHashes && loadPrevCp) {
id--;
msgIndex = -1;
return loadMoreOOHistory().then(() => {
msgs = ooMessages[id];
//Empty checkpoint - checkpoint saved with no further changes
if (!msgs?.length) {
msgs = ooMessages[id];
config.onPatchBack(hashes[id], msgs.slice(0, msgIndex));
msgIndex--;
patch = msgs[msgs?.length-1];
position = msgs.indexOf(patch);
if (!$(`[data="${id},0"]`).length) {
displayCheckpointTimeline();
}
showVersion(false, false, true);
return;
}
cp = hashes[id];
var q = msgs.slice(0, msgIndex);
patch = msgs[msgs?.length-1];
//Is the checkpoint the result of restoring history? If yes, we need to load an extra patch
if (!isRevert) {
config.onPatchBack(cp, q);
msgIndex--;
position = msgs?.length-1;
} else {
restore = true;
if (!$(`[data="${id},${position}"]`).length) {
displayCheckpointTimeline(false, true);
}
config.onPatchBack(cp, msgs);
patch = msgs[msgs?.length-1];
position = msgs?.length;
}
//Check if this checkpoint has already been added to the timeline
if (!$(`[data="${id},0"]`).length) {
displayCheckpointTimeline();
}
showVersion(false, restore);
restore = false;
});
}
var q = msgs.slice(0, msgIndex);
config.onPatchBack(cp, q);
patch = msgs[msgs?.length + msgIndex];
msgIndex--;
position = msgs.indexOf(patch);
showVersion(false);
};
// Create the history toolbar
var display = function () {
$hist.html('');
var fastPrev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [
Icons.get('history-fast-prev'),
]);
var fastNext = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
Icons.get('history-fast-next'),
]);
var _next = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
const _next = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
Icons.get('history-next'),
]);
var _prev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [
const _prev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [
Icons.get('history-prev')
]);
$fastPrev = $(fastPrev);
$prev = $(_prev);
$fastNext = $(fastNext).prop('disabled', 'disabled');
$next = $(_next).prop('disabled', 'disabled');
$next = $(_next);
var time = h('div.cp-history-timeline-time');
var version = h('div.cp-history-timeline-version');
var version = h('div.cp-history-version-time');
$version = $(version);
var dropdown = h('div.cp-history-version-select');
var $dropdown = $(dropdown);
var line = h('span.cp-history-timeline-patch');
$timeline = $(line);
var pos = h('span.cp-history-snapshots');
var timeline = h('div.cp-toolbar-history-timeline', [
h('div.cp-history-timeline-line', [
h('span.cp-history-timeline-container')
h('span.cp-history-timeline-container', [
h('span.cp-history-timeline-bar', [
line
]),
pos
])
]),
h('div.cp-history-timeline-actions', [
h('span.cp-history-timeline-prev', [
fastPrev,
_prev
]),
time,
version,
h('div.cp-history-version', [
dropdown,
version,
]),
h('span.cp-history-timeline-next', [
_next,
fastNext
_next
])
])
]);
var snapshot = h('button', {
title: Messages.snapshots_new,
class: 'cp-history-create-snapshot'
@ -595,6 +405,8 @@ define([
$share = $(share);
$hist.append([timeline, actions]);
makeDropdown($dropdown);
var onKeyDown, onKeyUp;
var closeUI = function () {
$hist.hide();
@ -607,7 +419,6 @@ define([
// Push one patch
$next.click(function () {
if (loading) { return; }
loading = true;
next();
});
$prev.click(function () {
@ -616,81 +427,38 @@ define([
prev();
});
// Go to next checkpoint
$fastNext.click(function () {
if (loading) { return; }
loading = true;
var msgs = ooMessages[id];
if (id < Object.keys(hashes).length && id !== -1) {
if (id === -1) {
id = 1;
} else {
id++;
}
loadMoreOOHistory().then(() => {
var cp = hashes[id];
config.loadHistoryCp(cp);
var msgs = ooMessages[id];
msgIndex = -msgs?.length-1;
position = 0;
showVersion(false);
loadingFalse();
return;
});
}
else {
var cp = hashes[id];
msgs = ooMessages[id];
msgIndex = -1;
config.onPatchBack(cp, msgs);
}
loadingFalse();
position = msgs?.length;
showVersion(false);
});
// Go to previous checkpoint
$fastPrev.click(function () {
if (loading) { return; }
loading = true;
if (!ooMessages[id].length || ooMessages[id].length+1 === Math.abs(msgIndex)) {
id--;
}
var cp = hashes[id];
config.loadHistoryCp(cp);
loadMoreOOHistory().then(() => {
var msgs = ooMessages[id];
msgIndex = -msgs?.length-1;
if (!$(`[data="${id},0"]`).length) {
displayCheckpointTimeline();
}
patch = msgs[msgs?.length-1];
position = 0;
showVersion(false);
updateButtons(true);
});
loadingFalse();
});
// XXX
onKeyDown = function (e) {
var p = function () { e.preventDefault(); };
if ([38, 39].indexOf(e.which) >= 0) { p(); return $next.click(); } // Right
if (e.which === 33) { p(); return $fastNext.click(); } // PageUp
if (e.which === 34) { p(); return $fastPrev.click(); } // PageUp
if (e.which === 27) { p(); return $(close).click(); }
};
onKeyUp = function (e) { e.stopPropagation(); };
$(window).on('keydown', onKeyDown).on('keyup', onKeyUp).focus();
$timeline.on('click', '.cp-history-bar-el', (ev) => {
let target = ev.target;
if (!target) { return; }
if (!target.classList.contains('cp-history-bar-el')) {
target = $(target).closest('.cp-history-bar-el')[0];
}
const attr = target?.attributes?.getNamedItem('data-msg');
const idx = Number(attr?.value || 0);
if (idx > msgIdx) {
return void next(idx - msgIdx);
}
if (idx < msgIdx) {
return void prev(msgIdx - idx);
}
});
// Versioned link
$share.click(function () {
common.getSframeChannel().event('EV_SHARE_OPEN', {
versionHash: getVersion(position)
versionHash: getCurrentVersion()
});
});
$(snapshot).click(function () {
if (cpIndex === -1 && msgIndex === -1) { return void UI.warn(Messages.snapshots_ooPickVersion); }
var input = h('input', {
placeholder: Messages.snapshots_placeholder
});
@ -712,14 +480,14 @@ define([
onClick: function () {
var val = $input.val();
if (!val) { return true; }
msgs = ooMessages[id];
const patch = getCurrentMsg();
config.makeSnapshot(val, function (err) {
if (err) { return; }
$input.val('');
UI.log(Messages.saved);
}, {
hash: getVersion(position),
time: currentTime || patch && patch.time || 0
hash: getCurrentVersion(),
time: patch?.time || +new Date()
});
},
keys: [13],
@ -748,11 +516,18 @@ define([
});
};
// Build UI
display();
// Load initial state
loadMessages().then((msgs) => {
loading = false;
msgIdx = msgs.length - 1;
showVersion();
updateTimeline();
}).catch(err => { console.error(err); });
};
return History;
});

File diff suppressed because it is too large Load Diff

View File

@ -38,74 +38,42 @@ define([
obj.ooVersionHash = version;
obj.ooForceVersion = localStorage.CryptPad_ooVersion || "";
};
var channels = {};
const channels = {};
var getPropChannels = function () {
return channels;
};
var addRpc = function (sframeChan, Cryptpad, Utils) {
Cryptpad.otherPadAttrs = channels;
sframeChan.on('Q_OO_SAVE', function (data, cb) {
var chanId = Utils.Hash.hrefToHexChannelId(data.url);
Cryptpad.getPadAttribute('lastVersion', function (err, data) {
if (!data) { return; }
var oldChanId = Utils.Hash.hrefToHexChannelId(data);
if (oldChanId !== chanId) { Cryptpad.unpinPads([oldChanId], function () {}); }
});
// If pad is stored, pin
Cryptpad.getPadAttribute('channel', function (err, data) {
if (err || !data) { return; }
Cryptpad.pinPads([chanId], function (e) {
if (e) { return void cb(e); }
});
});
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
// Only called onReady when loading legacy checkpoints
channels.rtChannel = data.channel;
channels.lastVersion = data.url;
if (data.hash) { channels.lastCpHash = data.hash; }
Cryptpad.setPadAttribute('lastCpHash', data.hash, cb);
channels.lastCpHash = data.hash;
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
});
sframeChan.on('Q_OO_OPENCHANNEL', function (data, cb) {
const other = Cryptpad.otherPadAttrs = {
rtChannel: data.channel
};
if (channels.lastVersion) {
other.lastVersion = channels.lastVersion;
}
other.lastCpHash = channels.lastCpHash;
Cryptpad.getPadAttribute('rtChannel', function (err, res) {
// If already stored, don't pin it again
channels.rtChannel = data.channel;
if (res && res === data.channel) { return; }
Cryptpad.pinPads([data.channel], function () {
Cryptpad.setPadAttribute('rtChannel', data.channel, function () {});
});
});
var owners, expire;
nThen(function (waitFor) {
if (Utils.rtConfig) {
owners = Utils.Util.find(Utils.rtConfig, ['metadata', 'owners']);
expire = Utils.Util.find(Utils.rtConfig, ['metadata', 'expire']);
return;
sframeChan.on('Q_OO_OPENCHANNEL', function (data, cb) {
// If we don't have "channels" values, it means we're not
// loading an "old" checkpoint so we can clean attributes
if (!channels?.lastVersion) {
channels.linked = true;
Cryptpad.setPadAttribute('linked', true, () => {});
Cryptpad.setPadAttribute('rtChannel', void 0, () => {});
Cryptpad.setPadAttribute('lastVersion', void 0, () => {});
Cryptpad.setPadAttribute('lastCpHash', void 0, () => {});
}
Cryptpad.onlyoffice.execCommand({
cmd: 'OPEN_CHANNEL',
data: {
channel: data.channel,
lastCpHash: data.lastCpHash,
padChan: Utils.secret.channel, // metadata inherited from this pad
validateKey: Utils.secret.keys.validateKey
}
Cryptpad.getPadAttribute('owners', waitFor(function (err, res) {
owners = res;
}));
Cryptpad.getPadAttribute('expire', waitFor(function (err, res) {
expire = res;
}));
}).nThen(function () {
Cryptpad.onlyoffice.execCommand({
cmd: 'OPEN_CHANNEL',
data: {
owners: owners,
expire: expire,
channel: data.channel,
lastCpHash: data.lastCpHash,
padChan: Utils.secret.channel,
validateKey: Utils.secret.keys.validateKey
}
}, cb);
});
}, cb);
});
sframeChan.on('EV_OO_PIN_IMAGES', function (list) {
Cryptpad.getPadAttribute('ooImages', function (err, res) {
@ -181,7 +149,7 @@ define([
type: 'oo',
addData: addData,
addRpc: addRpc,
getPropChannels: getPropChannels,
getPropChannels: getPropChannels, // XXX TODO
messaging: true,
useCreationScreen: !isIntegration,
noDrive: true,

View File

@ -0,0 +1,32 @@
const factory = () => {
const sortCpIndex = (hashes) => {
return Object.keys(hashes).map(Number).sort(function (a, b) {
return a-b;
});
};
const trim = (content) => {
let hashes = content?.content?.hashes || {};
if (!hashes) { return content; }
const sortedCp = sortCpIndex(hashes);
const lastIdx = sortedCp.pop();
if (!lastIdx) { return content; }
const lastCp = hashes[lastIdx];
if (!lastCp) { return content; }
content.content.hashes = hashes = {};
if (lastCp.rtChannel) {
delete content.content.channel;
}
hashes[lastIdx] = lastCp;
return content;
};
return { trim };
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([], factory);
}

View File

@ -20,6 +20,7 @@ define([
var u8 = data.u8;
var metadata = data.metadata;
var key = data.key;
var linked = data.linked;
var onError = data.onError || function () {};
var onPending = data.onPending || function () {};
@ -141,7 +142,9 @@ define([
});
};
common.uploadStatus(teamId, id, estimate, function (e, pending) {
common.uploadStatus({
teamId, id, linked, size: estimate
}, function (e, pending) {
if (e) {
console.error(e);
onError(e);
@ -166,6 +169,7 @@ define([
module.upload = function (file, noStore, common, updateProgress, onComplete, onError, onPending) {
var u8 = file.blob; // This is not a blob but a uint8array
var metadata = file.metadata;
var linked = file.linked;
var owned = file.owned;
var teamId = file.teamId;
@ -209,6 +213,7 @@ define([
module.uploadU8(common, {
teamId: teamId,
u8: u8,
linked,
metadata: metadata,
key: key,
id: id,

View File

@ -485,6 +485,7 @@ define([
queue.push({
blob: file_arraybuffer,
metadata: metadata,
linked: file.linked,
password: password,
owned: owned,
forceSave: forceSave,

View File

@ -957,8 +957,12 @@ define([
Cryptpad.universal.onEvent.reg(function (data) {
sframeChan.event('EV_UNIVERSAL_EVENT', data);
});
sframeChan.on('Q_UNIVERSAL_COMMAND', function (data, cb) {
Cryptpad.universal.execCommand(data, cb);
sframeChan.on('Q_UNIVERSAL_COMMAND', function (content, cb) {
if (content?.type === 'linked-doc') {
content.data.data.signKey64 = secret.keys?.signKey;
content.data.data.channel = secret.channel;
}
Cryptpad.universal.execCommand(content, cb);
});
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) {
@ -1439,6 +1443,53 @@ define([
}
}, cb);
});
// History
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
let nSecret = secret;
if (data.isDownload && ooDownloadData[data.isDownload]) {
var ooData = ooDownloadData[data.isDownload];
delete ooDownloadData[data.isDownload];
nSecret = Utils.Hash.getSecrets('sheet', ooData.hash, ooData.password);
} else if (data.href) {
var _parsed = Utils.Hash.parsePadUrl(data.href);
nSecret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, data.password);
}
var crypto = Crypto.createEncryptor(nSecret.keys);
Cryptpad.getFullHistory({
debug: data?.debug,
full: data?.full,
channel: data.channel || nSecret.channel,
validateKey: nSecret.keys.validateKey
}, function (encryptedMsgs) {
var nt = nThen;
var decryptedMsgs = [];
var total = encryptedMsgs.length;
encryptedMsgs.forEach(function (_msg, i) {
nt = nt(function (waitFor) {
// The 3rd parameter "true" means we're going to skip signature validation.
// We don't need it since the message is already validated serverside by hk
if (typeof(_msg) === "object") {
decryptedMsgs.push({
author: _msg.author,
serverHash: _msg.serverHash,
time: _msg.time,
msg: crypto.decrypt(_msg.msg, true, true)
});
} else {
decryptedMsgs.push(crypto.decrypt(_msg, true, true));
}
setTimeout(waitFor(function () {
sframeChan.event('EV_FULL_HISTORY_STATUS', (i+1)/total);
}));
}).nThen;
});
nt(function () {
cb(decryptedMsgs);
});
});
});
sframeChan.on('Q_GET_HISTORY_RANGE', function (data, cb) {
var nSecret = secret;
if (cfg.isDrive) {
@ -1533,6 +1584,39 @@ define([
});
});
});
sframeChan.on('Q_TRIM_HISTORY', function (data, cb) {
const { href } = data;
const parsed = Utils.Hash.parsePadUrl(href);
let type = parsed.type;
if (['sheet', 'doc', 'presentation'].includes(type)) {
type = 'common/onlyoffice';
}
const path = `/${type}/trim-history.js`;
const cfg = {
password: data.password
};
require([path], (Trimming) => {
nThen(waitFor => {
Cryptpad.getAccessKeys(waitFor((keys) => {
cfg.accessKeys = keys;
}));
}).nThen(function () {
Cryptget.get(parsed.hash, (err, val) => {
if (err) { return void cb(); }
const json = Utils.Util.tryParse(val);
if (!json) { return void cb(); }
const newJson = Trimming.trim(json);
if (!newJson) { return void cb(); }
Cryptget.put(parsed.hash, JSON.stringify(newJson), () => {
cb();
}, cfg);
}, cfg);
});
}, () => {
cb();
});
});
};
addCommonRpc(sframeChan, isSafe);
@ -1710,42 +1794,6 @@ define([
Cryptpad.anonGetPreviewContent(data, cb);
});
// History
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
var crypto = Crypto.createEncryptor(secret.keys);
Cryptpad.getFullHistory({
debug: data && data.debug,
channel: secret.channel,
validateKey: secret.keys.validateKey
}, function (encryptedMsgs) {
var nt = nThen;
var decryptedMsgs = [];
var total = encryptedMsgs.length;
encryptedMsgs.forEach(function (_msg, i) {
nt = nt(function (waitFor) {
// The 3rd parameter "true" means we're going to skip signature validation.
// We don't need it since the message is already validated serverside by hk
if (typeof(_msg) === "object") {
decryptedMsgs.push({
author: _msg.author,
serverHash: _msg.serverHash,
time: _msg.time,
msg: crypto.decrypt(_msg.msg, true, true)
});
} else {
decryptedMsgs.push(crypto.decrypt(_msg, true, true));
}
setTimeout(waitFor(function () {
sframeChan.event('EV_FULL_HISTORY_STATUS', (i+1)/total);
}));
}).nThen;
});
nt(function () {
cb(decryptedMsgs);
});
});
});
// Store
sframeChan.on('Q_DRIVE_GETDELETED', function (data, cb) {
Cryptpad.getDeletedPads(data, function (err, obj) {
@ -2049,7 +2097,7 @@ define([
nThen(function (waitFor) {
channels.forEach(function (chan) {
if (chan === "chainpad") { chan = secret.channel; }
console.error(chan);
if (!chan) { return; }
Utils.Cache.clearChannel(chan, waitFor());
});
}).nThen(cb);
@ -2368,7 +2416,10 @@ define([
};
if (burnAfterReading) {
Cryptpad.padRpc.onReadyEvent.reg(function () {
nThen(w => {
Cryptpad.padRpc.onReadyEvent.reg(w());
if (isOO) { sframeChan.on('EV_OO_DOC_READY', w()); }
}).nThen(() => {
Cryptpad.burnPad({
password: password,
href: currentPad.href,

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<script async data-main="main" src="/components/requirejs/require.js?ver=2.3.7"></script>
<script async data-main="/logout/main.js" src="/components/requirejs/require.js?ver=2.3.7"></script>
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
</head>
<body class="html">