Add support for onlyoffice history

This commit is contained in:
yflory 2020-09-18 17:54:57 +02:00
parent b31707098a
commit 4f147d4fd2
6 changed files with 439 additions and 42 deletions

View file

@ -255,7 +255,7 @@ const getOlderHistory = function (data, cb) {
} else if (untilHash) {
for (var i = messages.length - 1; i >= 0; i--) {
toSend.unshift(messages[i]);
if (Array.isArray(messages[i]) && HK.getHash(messages[i][4], Log) === untilHash) {
if (Array.isArray(messages[i]) && HK.getHash(messages[i][4]) === untilHash) {
break;
}
}

View file

@ -0,0 +1,340 @@
define([
'jquery',
'/common/common-interface.js',
'/bower_components/nthen/index.js',
//'/bower_components/chainpad-json-validator/json-ot.js',
], function ($, UI, nThen, /* JsonOT */) {
//var ChainPad = window.ChainPad;
var History = {};
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..."); }
History.loading = true;
var $toolbar = config.$toolbar;
var sframeChan = common.getSframeChannel();
if (!config.onlyoffice || !config.setHistory || !config.onCheckpoint || !config.onPatch) {
throw new Error("Missing config element");
}
var cpIndex = -1;
var msgIndex = 0;
var sortedCp;
var ooMessages = {};
var loading = false;
var update = function () {};
// Get an array of the checkpoint IDs sorted their patch index
var hashes = config.onlyoffice.hashes;
var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) {
return hashes[a].index - hashes[b].index;
});
var fillOO = function (id, messages) {
if (!id) { return; }
if (ooMessages[id]) { return; }
ooMessages[id] = messages;
update();
};
// We want to load a checkpoint (or initial state)
var loadMoreOOHistory = function () {
if (!Array.isArray(sortedCp)) { return void console.error("Wrong type"); }
var cp = {};
// Get the checkpoint ID
var id = -1;
if (cpIndex < sortedCp.length) {
id = sortedCp[sortedCp.length - 1 - cpIndex];
cp = hashes[id];
}
var nextId = sortedCp[sortedCp.length - cpIndex];
// 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
// Current cp or initial hash (invalid hash ==> initial hash)
var toHash = cp.hash || 'NONE';
// Next cp or last hash
var fromHash = nextId ? hashes[nextId].hash : config.onlyoffice.lastHash;
msgIndex = 0;
if (ooMessages[id]) {
// Cp already loaded: reload OO
loading = false;
return void config.onCheckpoint(cp);
}
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 messages = (data.messages || []).slice(1);
if (config.debug) { console.log(data.messages); }
fillOO(id, messages);
loading = false;
config.onCheckpoint(cp);
});
};
// config.setHistory(bool, bool)
// - bool1: history value
// - bool2: reset old content?
var render = function (val) {
if (typeof val === "undefined") { return; }
try {
config.applyVal(val);
} catch (e) {
// Probably a parse error
console.error(e);
}
};
var onClose = function () { config.setHistory(false); };
var onRevert = function () {
config.onRevert();
};
config.setHistory(true);
var Messages = common.Messages;
var realtime;
var states = [];
var c = 0;//states.length - 1;
var $hist = $toolbar.find('.cp-toolbar-history');
var $bottom = $toolbar.find('.cp-toolbar-bottom');
$hist.html('').css('display', 'flex');
$bottom.hide();
UI.spinner($hist).get().show();
var $loadMore, $version, get;
var $fastPrev = $('<button>', {
'class': 'cp-toolbar-history-fast-previous fa fa-fast-backward buttonPrimary',
title: Messages.history_prev
});
var $prev = $('<button>', {
'class': 'cp-toolbar-history-previous fa fa-step-backward buttonPrimary',
title: Messages.history_prev
});
var $next = $('<button>', {
'class': 'cp-toolbar-history-next fa fa-step-forward buttonPrimary',
title: Messages.history_next
});
var $fastNext = $('<button>', {
'class': 'cp-toolbar-history-fast-next fa fa-fast-forward buttonPrimary',
title: Messages.history_next
});
update = function () {
var cps = sortedCp.length;
$prev.show();
$fastPrev.show();
$next.show();
$fastNext.show();
console.error(cps, cpIndex);
if (cpIndex >= cps) {
$fastPrev.hide();
}
if (cpIndex === 0) {
$fastNext.hide();
}
var id = sortedCp[cps - cpIndex -1] || -1;
var msgs = (ooMessages[id] || []).length;
if (msgIndex <= 0) {
$prev.hide();
}
if (msgIndex >= (msgs - 1)) {
$next.hide();
}
};
get = function (i, blockOnly) {
i = parseInt(i);
if (isNaN(i)) { return; }
if (i > 0) { i = 0; }
if (i < -(states.length - 2)) { i = -(states.length - 2); }
if (i <= -(states.length - 11)) {
loadMore();
}
var idx = states.length - 1 + i;
if (blockOnly) { return states[idx]; }
var val = states[idx].getContent().doc;
c = i;
$hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous, ' +
'.cp-toolbar-history-fast-next, .cp-toolbar-history-fast-previous')
.css('visibility', '');
if (c === -(states.length-1)) {
$hist.find('.cp-toolbar-history-previous').css('visibility', 'hidden');
$hist.find('.cp-toolbar-history-fast-previous').css('visibility', 'hidden');
}
if (c === 0) {
$hist.find('.cp-toolbar-history-next').css('visibility', 'hidden');
$hist.find('.cp-toolbar-history-fast-next').css('visibility', 'hidden');
}
var $pos = $hist.find('.cp-toolbar-history-pos');
var p = 100 * (1 - (-c / (states.length-2)));
$pos.css('margin-left', p+'%');
// Display the version when the full history is loaded
// Note: the first version is always empty and probably can't be displayed, so
// we can consider we have only states.length - 1 versions
$version.text(idx + ' / ' + (states.length-1));
if (config.debug) {
console.log(states[idx]);
var ops = states[idx] && states[idx].getPatch() && states[idx].getPatch().operations;
if (Array.isArray(ops)) {
ops.forEach(function (op) { console.log(op); });
}
}
return val || '';
};
var next = function () {
var cps = sortedCp.length;
var id = sortedCp[cps - cpIndex -1] || -1;
if (!ooMessages[id]) { return; }
var msgs = ooMessages[id];
var patch = msgs[msgIndex];
config.onPatch(patch);
loading = false;
msgIndex++;
};
// Create the history toolbar
var display = function () {
$hist.html('');
var $rev = $('<button>', {
'class':'cp-toolbar-history-revert buttonSuccess fa fa-check-circle-o',
title: Messages.history_restoreTitle
}).appendTo($hist);//.text(Messages.history_restore);
if (History.readOnly) { $rev.css('visibility', 'hidden'); }
$('<span>', {'class': 'cp-history-filler'}).appendTo($hist);
$fastPrev.appendTo($hist);
$prev.hide().appendTo($hist);
var $nav = $('<div>', {'class': 'cp-toolbar-history-goto'}).appendTo($hist);
$next.hide().appendTo($hist);
$fastNext.hide().appendTo($hist);
var $share = $('<button>', {
'class': 'fa fa-shhare-alt buttonPrimary',
title: Messages.shareButton
}).appendTo($hist);
$('<span>', {'class': 'cp-history-filler'}).appendTo($hist);
var $close = $('<button>', {
'class':'cp-toolbar-history-close fa fa-window-close',
title: Messages.history_closeTitle
}).appendTo($hist);
var $bar = $('<div>', {'class': 'cp-toolbar-history-bar'}).appendTo($nav);
var $container = $('<div>', {'class':'cp-toolbar-history-pos-container'}).appendTo($bar);
$('<div>', {'class': 'cp-toolbar-history-pos'}).appendTo($container);
$version = $('<span>', {
'class': 'cp-toolbar-history-version'
}).prependTo($bar).hide();
/*
$loadMore = $('<button>', {
'class':'cp-toolbar-history-loadmore fa fa-ellipsis-h',
title: Messages.history_loadMore
}).click(function () {
loadMore(function () {
get(c);
});
}).prependTo($container);
*/
var onKeyDown, onKeyUp;
var close = function () {
$hist.hide();
$bottom.show();
$(window).trigger('resize');
$(window).off('keydown', onKeyDown);
$(window).off('keyup', onKeyUp);
};
// Close & restore buttons
$close.click(function () {
states = [];
close();
onClose();
});
$rev.click(function () {
UI.confirm(Messages.history_restorePrompt, function (yes) {
if (!yes) { return; }
close();
onRevert();
UI.log(Messages.history_restoreDone);
});
});
// Push one patch
$next.click(function () {
if (loading) { return; }
loading = true;
next();
update();
});
// Reset current checkpoint
$prev.click(function () {
if (loading) { return; }
loading = true;
loadMoreOOHistory();
update();
});
// Go to previous checkpoint
$fastNext.click(function () {
if (loading) { return; }
loading = true;
cpIndex--;
loadMoreOOHistory();
update();
});
// Go to next checkpoint
$fastPrev.click(function () {
if (loading) { return; }
loading = true;
cpIndex++;
loadMoreOOHistory();
update();
});
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(); $close.click(); }
};
onKeyUp = function (e) { e.stopPropagation(); };
$(window).on('keydown', onKeyDown).on('keyup', onKeyUp).focus();
$(window).trigger('resize');
};
display();
//return void loadMoreOOHistory();
};
return History;
});

View file

@ -14,6 +14,7 @@ define([
'/customize/application_config.js',
'/bower_components/chainpad/chainpad.dist.js',
'/file/file-crypto.js',
'/common/onlyoffice/history.js',
'/common/onlyoffice/oocell_base.js',
'/common/onlyoffice/oodoc_base.js',
'/common/onlyoffice/ooslide_base.js',
@ -40,6 +41,7 @@ define([
AppConfig,
ChainPad,
FileCrypto,
History,
EmptyCell,
EmptyDoc,
EmptySlide,
@ -243,8 +245,15 @@ define([
ready: false,
readyCb: undefined,
sendCmd: function (data, cb) {
if (APP.history) { return; }
sframeChan.query('Q_OO_COMMAND', data, cb);
},
getHistory: function (cb) {
rtChannel.sendCmd({
cmd: 'GET_HISTORY',
data: {}
}, cb);
},
sendMsg: function (msg, cp, cb) {
rtChannel.sendCmd({
cmd: 'SEND_MESSAGE',
@ -320,10 +329,14 @@ define([
// Get the last cp idx
var all = sortCpIndex(content.hashes || {});
var current = all[all.length - 1] || 0;
// XXX Keep all cp now
// Get the expected cp idx
var _i = Math.floor(ev.index / CHECKPOINT_INTERVAL);
//var _i = Math.floor(ev.index / CHECKPOINT_INTERVAL);
// Take the max of both
var i = Math.max(_i, current);
//var i = Math.max(_i, current);
var i = current + 1;
content.hashes[i] = {
file: data.url,
hash: ev.hash,
@ -414,8 +427,8 @@ define([
var file = getFileType();
blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
var data = {
hash: ooChannel.lastHash,
index: ooChannel.cpIndex
hash: APP.history ? ooChannel.historyLastHash : ooChannel.lastHash,
index: APP.history ? ooChannel.currentIndex : ooChannel.cpIndex
};
fixSheets();
@ -620,7 +633,11 @@ define([
removeClient(obj.data);
break;
case 'MESSAGE':
if (APP.history) { return; }
if (APP.history) {
ooChannel.historyLastHash = obj.data.hash;
ooChannel.currentIndex++;
return;
}
if (ooChannel.ready) {
// In read-only mode, push the message to the queue and prompt
// the user to refresh OO (without reloading the page)
@ -814,6 +831,8 @@ define([
};
var handleLock = function (obj, send) {
if (APP.history) { return; }
if (content.saveLock) {
if (!isLockedModal.modal) {
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
@ -1043,7 +1062,7 @@ define([
startOO = function (blob, file, force) {
if (APP.ooconfig && !force) { return void console.error('already started'); }
var url = URL.createObjectURL(blob);
var lock = readOnly || APP.migrate || APP.history;
var lock = readOnly || APP.migrate;
// Starting from version 3, we can use the view mode again
// defined but never used
@ -1182,6 +1201,12 @@ define([
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
}
if (APP.history) {
try {
getEditor().asc_setRestriction(true);
} catch (e) {}
}
if (APP.migrate && !readOnly) {
onMigrateRdy.fire();
}
@ -1739,59 +1764,75 @@ define([
/* add a history button */
// XXX move out of dev mode
var commit = function () {
APP.stopHistory = true;
makeCheckpoint(true);
// ~ Commit current version
// XXX make checkpoint?
// APP.stopHistory = true;
};
var loadCp = function (cp) {
loadLastDocument(cp, function () {
var file = getFileType();
var type = common.getMetadataMgr().getPrivateData().ooType;
var blob = loadInitDocument(type, true);
ooChannel.queue = [];
resetData(blob, file);
}, function (blob, file) {
// XXX ?
ooChannel.queue = [];
resetData(blob, file);
});
};
var draw = function (cp, patch) {
if (typeof(cp) === "undefined") {
// Patch on the current cp
// XXX send patch to oo
return;
}
var onPatch = function (patch) {
// Patch on the current cp
console.error(patch);
ooChannel.send(JSON.parse(patch.msg));
// XXX send patch to oo
};
var onCheckpoint = function (cp) {
console.error(cp);
// We want to load a checkpoint:
loadCp(cp);
console.error('draw');
};
var setHistoryMode = function (bool, update) {
var setHistoryMode = function (bool) {
if (bool) {
APP.history = true;
getEditor().setViewModeDisconnect();
return;
}
APP.stopHistory = true;
if (update) {
// We want to commit the current version
// Do nothing: we're going to create a cp
return;
}
// Cancel button: redraw from lastCp
var lastCp = getLastCp();
loadCp(lastCp);
APP.history = false;
ooChannel.queue = [];
ooChannel.ready = false;
rtChannel.getHistory(function () {
var lastCp = getLastCp();
loadCp(lastCp);
});
};
//redraw();
};
var histConfig = {
onLocal: draw,
onRemote: function () {},
setHistory: setHistoryMode,
applyVal: draw,
onlyoffice: content.hashes || {},
$toolbar: $(toolbarContainer)
};
var $hist = common.createButton('history', true, {histConfig: histConfig});
$hist.addClass('cp-hidden-if-readonly');
toolbar.$drawer.append($hist);
common.createButton('', true, {
name: 'history',
icon: 'fa-history',
text: Messages.historyText,
tippy: Messages.historyButton
}).click(function () {
ooChannel.historyLastHash = ooChannel.lastHash;
ooChannel.currentIndex = ooChannel.cpIndex;
//Feedback.send('OO_HISTORY'); // XXX pull Feedback from require
var histConfig = {
onPatch: onPatch,
onCheckpoint: onCheckpoint,
onRevert: commit,
setHistory: setHistoryMode,
onlyoffice: {
hashes: content.hashes || {},
channel: content.channel,
lastHash: ooChannel.lastHash
},
$toolbar: $('.cp-toolbar-container')
};
History.create(common, histConfig);
}).appendTo(toolbar.$drawer);
})();
}
if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) {

View file

@ -2046,6 +2046,8 @@ define([
var lastKnownHash;
var txid = Util.uid();
console.error(data);
var onMsg = function (msg) {
if (completed) { return; }
var parsed = parse(msg);

View file

@ -2,6 +2,20 @@ define([
], function () {
var OO = {};
var getHistory = function (ctx, client, cb) {
var c = ctx.clients[client];
if (!c) { return void cb({error: 'ENOENT'}); }
var chan = ctx.channels[c.channel];
if (!chan) { return void cb({error: 'ENOCHAN'}); }
chan.history.forEach(function (msg) {
ctx.emit('MESSAGE', {
msg: msg,
validateKey: chan.validateKey
}, [client]);
});
cb();
};
var openChannel = function (ctx, obj, client, cb) {
var channel = obj.channel;
var padChan = obj.padChan;
@ -24,11 +38,8 @@ define([
// ==> Use our netflux ID to create our client ID
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
chan.history.forEach(function (msg) {
ctx.emit('MESSAGE', {
msg: msg,
validateKey: chan.validateKey
}, [client]);
getHistory(ctx, client, function () {
ctx.emit('READY', '', [client]);
});
// ==> And push the new tab to the list
@ -318,6 +329,9 @@ define([
if (cmd === 'OPEN_CHANNEL') {
return void openChannel(ctx, data, clientId, cb);
}
if (cmd === 'GET_HISTORY') {
return void getHistory(ctx, clientId, cb);
}
if (cmd === 'REENCRYPT') {
return void reencrypt(ctx, data, clientId, cb);
}

View file

@ -1106,7 +1106,7 @@ define([
var validate = nSecret.keys.validateKey;
var crypto = Crypto.createEncryptor(nSecret.keys);
Cryptpad.getHistoryRange({
channel: channel,
channel: data.channel || channel,
validateKey: validate,
toHash: data.toHash,
lastKnownHash: data.lastKnownHash