mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-12 19:49:59 +05:00
Merge staging into office-9.3
This commit is contained in:
commit
9aafa6c4ea
6
.github/workflows/test-trigger.yml
vendored
6
.github/workflows/test-trigger.yml
vendored
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -93,6 +93,10 @@
|
||||
left: 250px;
|
||||
}
|
||||
|
||||
&.cp-small {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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
445
lib/commands/linked.js
Normal 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();
|
||||
});
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
186
package-lock.json
generated
@ -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"
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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;
|
||||
|
||||
161
src/worker/modules/linked.ts
Normal file
161
src/worker/modules/linked.ts
Normal 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 }
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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
@ -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,
|
||||
|
||||
32
www/common/onlyoffice/trim-history.js
Normal file
32
www/common/onlyoffice/trim-history.js
Normal 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);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -485,6 +485,7 @@ define([
|
||||
queue.push({
|
||||
blob: file_arraybuffer,
|
||||
metadata: metadata,
|
||||
linked: file.linked,
|
||||
password: password,
|
||||
owned: owned,
|
||||
forceSave: forceSave,
|
||||
|
||||
@ -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,
|
||||
|
||||
2
www/common/worker.bundle.min.js
vendored
2
www/common/worker.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user