diff --git a/customize.dist/src/less2/include/toolbar-history.less b/customize.dist/src/less2/include/toolbar-history.less index 155265a44..2759fe71b 100644 --- a/customize.dist/src/less2/include/toolbar-history.less +++ b/customize.dist/src/less2/include/toolbar-history.less @@ -71,6 +71,10 @@ min-width: 0; text-align: center; } + .cp-history-timeline-version { + font-size: 12px; + margin-right: 10px; + } } .cp-toolbar-history-actions { display: flex; @@ -120,6 +124,9 @@ .cp-history-timeline-actions { margin-left: 21px !important; } + .cp-toolbar-history-actions { + align-items: baseline !important; + } } &.cp-smallpatch { .cp-history-snapshot { @@ -137,6 +144,17 @@ } } } + &.cp-history-oo { + .cp-history-timeline-container { + height: 20px !important; + .cp-history-timeline-pos { + height: 20px !important; + } + } + .cp-history-timeline-actions { + margin-left: 0 !important; + } + } .cp-history-timeline-line { display: flex; .cp-history-timeline-legend { @@ -229,6 +247,12 @@ } } } + .cp-history-timeline-version:empty { + display: none; + } + .cp-history-timeline-time:empty { + display: none; + } .cp-history-timeline-next { button:last-child { margin-right: 0; @@ -254,7 +278,6 @@ left: ~"calc(50% - 6px)"; } } - } } diff --git a/lib/hk-util.js b/lib/hk-util.js index ed5f43297..14263e481 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -662,10 +662,11 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) { } var oldestKnownHash = map.from; + var untilHash = map.to; var desiredMessages = map.count; var desiredCheckpoint = map.cpCount; var txid = map.txid; - if (typeof(desiredMessages) !== 'number' && typeof(desiredCheckpoint) !== 'number') { + if (typeof(desiredMessages) !== 'number' && typeof(desiredCheckpoint) !== 'number' && !untilHash) { return void Server.send(userId, [seq, 'ERROR', 'UNSPECIFIED_COUNT', HISTORY_KEEPER_ID]); } @@ -674,7 +675,7 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) { } Server.send(userId, [seq, 'ACK']); - Env.getOlderHistory(channelName, oldestKnownHash, desiredMessages, desiredCheckpoint, function (err, toSend) { + Env.getOlderHistory(channelName, oldestKnownHash, untilHash, desiredMessages, desiredCheckpoint, function (err, toSend) { if (err && err.code !== 'ENOENT') { Env.Log.error("HK_GET_OLDER_HISTORY", err); } diff --git a/lib/workers/db-worker.js b/lib/workers/db-worker.js index 50f2dd472..42c75fdca 100644 --- a/lib/workers/db-worker.js +++ b/lib/workers/db-worker.js @@ -230,6 +230,7 @@ const computeMetadata = function (data, cb) { const getOlderHistory = function (data, cb) { const oldestKnownHash = data.hash; + const untilHash = data.toHash; const channelName = data.channel; const desiredMessages = data.desiredMessages; const desiredCheckpoint = data.desiredCheckpoint; @@ -260,6 +261,13 @@ const getOlderHistory = function (data, cb) { var toSend = []; if (typeof (desiredMessages) === "number") { toSend = messages.slice(-desiredMessages); + } else if (untilHash) { + for (var j = messages.length - 1; j >= 0; j--) { + toSend.unshift(messages[j]); + if (Array.isArray(messages[j]) && HK.getHash(messages[j][4]) === untilHash) { + break; + } + } } else { let cpCount = 0; for (var i = messages.length - 1; i >= 0; i--) { diff --git a/lib/workers/index.js b/lib/workers/index.js index c904bc3ac..ec06e8099 100644 --- a/lib/workers/index.js +++ b/lib/workers/index.js @@ -281,12 +281,13 @@ Workers.initialize = function (Env, config, _cb) { }); }; - Env.getOlderHistory = function (channel, oldestKnownHash, desiredMessages, desiredCheckpoint, cb) { + Env.getOlderHistory = function (channel, oldestKnownHash, toHash, desiredMessages, desiredCheckpoint, cb) { Env.store.getWeakLock(channel, function (next) { sendCommand({ channel: channel, command: "GET_OLDER_HISTORY", hash: oldestKnownHash, + toHash: toHash, desiredMessages: desiredMessages, desiredCheckpoint: desiredCheckpoint, }, Util.both(next, cb)); diff --git a/www/common/onlyoffice/history.js b/www/common/onlyoffice/history.js new file mode 100644 index 000000000..af00c8103 --- /dev/null +++ b/www/common/onlyoffice/history.js @@ -0,0 +1,384 @@ +define([ + 'jquery', + '/common/common-interface.js', + '/common/hyperscript.js', +], function ($, UI, h) { + //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(); + History.readOnly = common.getMetadataMgr().getPrivateData().readOnly || !common.isLoggedIn(); + + if (!config.onlyoffice || !config.setHistory || !config.onCheckpoint || !config.onPatch || !config.makeSnapshot) { + throw new Error("Missing config element"); + } + + var cpIndex = -1; + var msgIndex = -1; + var ooMessages = {}; + var loading = false; + var update = function () {}; + var currentTime; + + // 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 endWithCp = sortedCp.length && + config.onlyoffice.lastHash === hashes[sortedCp[sortedCp.length - 1]].hash; + + var fillOO = function (id, messages) { + if (!id) { return; } + if (ooMessages[id]) { return; } + ooMessages[id] = messages; + update(); + }; + + if (endWithCp) { cpIndex = 0; } + + var $version, $time, $share; + var $hist = $toolbar.find('.cp-toolbar-history'); + $hist.addClass('cp-smallpatch'); + $hist.addClass('cp-history-oo'); + var $bottom = $toolbar.find('.cp-toolbar-bottom'); + + var getVersion = function () { + var major = sortedCp.length - cpIndex; + return major + '.' + (msgIndex+1); + }; + var showVersion = function (initial) { + var v = getVersion(); + if (initial) { + v = "Latest"; // XXX + } + $version.text("Version: " + v); // XXX + + var $pos = $hist.find('.cp-history-timeline-pos'); + var cps = sortedCp.length; + var id = sortedCp[cps - cpIndex -1] || -1; + if (!ooMessages[id]) { return; } + var msgs = ooMessages[id]; + console.log(ooMessages); + var p = 100*((msgIndex+1) / (msgs.length)); + $pos.css('margin-left', p+'%'); + + var time = currentTime = msgs[msgIndex] && msgs[msgIndex].time; + if (time) { $time.text(new Date(time).toLocaleString()); } + else { $time.text(''); } + }; + + // 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 = -1; + + showVersion(); + 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!'); } + + // The first "cp" in history is the empty doc. It doesn't include the first patch + // of the history + var initialCp = cpIndex === sortedCp.length; + + var messages = (data.messages || []).slice(initialCp ? 0 : 1); + + if (config.debug) { console.log(data.messages); } + fillOO(id, messages); + loading = false; + config.onCheckpoint(cp); + $share.show(); + }); + }; + + var onClose = function () { config.setHistory(false); }; + var onRevert = function () { + config.onRevert(); + }; + + config.setHistory(true); + + var Messages = common.Messages; + + $hist.html('').css('display', 'flex'); + $bottom.hide(); + + UI.spinner($hist).get().show(); + + var $fastPrev, $fastNext, $next; + + var getId = function () { + var cps = sortedCp.length; + return sortedCp[cps - cpIndex -1] || -1; + }; + + update = function () { + var cps = sortedCp.length; + $fastPrev.show(); + $next.show(); + $fastNext.show(); + if (cpIndex >= cps && msgIndex === 0) { + $fastPrev.hide(); + } + if (cpIndex === 0) { + $fastNext.hide(); + } + var id = getId(); + var msgs = (ooMessages[id] || []).length; + if (msgIndex >= (msgs-1)) { + $next.hide(); + } + }; + + var next = function () { + var id = getId(); + if (!ooMessages[id]) { loading = false; return; } + var msgs = ooMessages[id]; + msgIndex++; + var patch = msgs[msgIndex]; + if (!patch) { loading = false; return; } + config.onPatch(patch); + showVersion(); + setTimeout(function () { + $('iframe').blur(); + loading = false; + }, 200); + }; + + + // Create the history toolbar + var display = function () { + $hist.html(''); + + var fastPrev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [ + h('i.fa.fa-fast-backward'), + ]); + var fastNext = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [ + h('i.fa.fa-fast-forward'), + ]); + var _next = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [ + h('i.fa.fa-step-forward') + ]); + $fastPrev = $(fastPrev); + $fastNext = $(fastNext).hide(); + $next = $(_next).hide(); + + var pos = h('span.cp-history-timeline-pos.fa.fa-caret-down'); + var time = h('div.cp-history-timeline-time'); + var version = h('div.cp-history-timeline-version'); + $time = $(time); + $version = $(version); + var bar; + var timeline = h('div.cp-toolbar-history-timeline', [ + h('div.cp-history-timeline-line', [ + bar = h('span.cp-history-timeline-container') + ]), + h('div.cp-history-timeline-actions', [ + h('span.cp-history-timeline-prev', [ + fastPrev, + ]), + time, + version, + h('span.cp-history-timeline-next', [ + _next, + fastNext + ]) + ]) + ]); + var snapshot = h('button', { + title: Messages.snapshots_new, + }, [ + h('i.fa.fa-camera') + ]); + var share = h('button', { title: Messages.history_shareTitle }, [ + h('i.fa.fa-shhare-alt'), + h('span', Messages.shareButton) + ]); + var restore = h('button', { + title: Messages.history_restoreTitle, + }, [ + h('i.fa.fa-check'), + h('span', Messages.history_restore) + ]); + var close = h('button', { title: Messages.history_closeTitle }, [ + h('i.fa.fa-times'), + h('span', Messages.history_close) + ]); + var actions = h('div.cp-toolbar-history-actions', [ + h('span.cp-history-actions-first', [ + snapshot, + share + ]), + h('span.cp-history-actions-last', [ + restore, + close + ]) + ]); + + if (History.readOnly) { + snapshot.disabled = true; + restore.disabled = true; + } + + $share = $(share); + $hist.append([timeline, actions]); + $(bar).append(pos); + + var onKeyDown, onKeyUp; + var closeUI = function () { + $hist.hide(); + $bottom.show(); + $(window).trigger('resize'); + $(window).off('keydown', onKeyDown); + $(window).off('keyup', onKeyUp); + }; + + // Push one patch + $next.click(function () { + if (loading) { return; } + loading = true; + next(); + 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; + if (msgIndex === -1) { + 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(); return $(close).click(); } + }; + onKeyUp = function (e) { e.stopPropagation(); }; + $(window).on('keydown', onKeyDown).on('keyup', onKeyUp).focus(); + + // Versioned link + $share.click(function () { + common.getSframeChannel().event('EV_SHARE_OPEN', { + versionHash: getVersion() + }); + }); + Messages.snapshots_ooPickVersion = "You must select a version before creating a snapshot"; // XXX + $(snapshot).click(function () { + if (cpIndex === -1 && msgIndex === -1) { return void UI.warn(Messages.snapshots_ooPickVersion); } + var input = h('input', { + placeholder: Messages.snapshots_placeholder + }); + var $input = $(input); + var content = h('div', [ + h('h5', Messages.snapshots_new), + input + ]); + + var buttons = [{ + className: 'cancel', + name: Messages.filePicker_close, + onClick: function () {}, + keys: [27], + }, { + className: 'primary', + iconClass: '.fa.fa-camera', + name: Messages.snapshots_new, + onClick: function () { + var val = $input.val(); + if (!val) { return true; } + config.makeSnapshot(val, function (err) { + if (err) { return; } + UI.log(Messages.saved); + }, { + hash: getVersion(), + time: currentTime || 0 + }); + }, + keys: [13], + }]; + + UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons })); + setTimeout(function () { + $input.focus(); + }); + }); + + // Close & restore buttons + $(close).click(function () { + History.loading = false; + onClose(); + closeUI(); + }); + $(restore).click(function () { + UI.confirm(Messages.history_restorePrompt, function (yes) { + if (!yes) { return; } + closeUI(); + History.loading = false; + onRevert(); + UI.log(Messages.history_restoreDone); + }); + }); + }; + + display(); + + showVersion(true); + + //return void loadMoreOOHistory(); + }; + + return History; +}); + + diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 5c36f6604..7d1c2e909 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -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,17 @@ 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: {} + }, function () { + APP.onHistorySynced = cb; + }); + }, sendMsg: function (msg, cp, cb) { rtChannel.sendCmd({ cmd: 'SEND_MESSAGE', @@ -320,10 +331,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, @@ -396,7 +411,7 @@ define([ } myUniqueOOId = undefined; setMyId(); - APP.docEditor.destroyEditor(); // Kill the old editor + if (APP.docEditor) { APP.docEditor.destroyEditor(); } // Kill the old editor $('iframe[name="frameEditor"]').after(h('div#cp-app-oo-placeholder')).remove(); ooLoaded = false; oldLocks = {}; @@ -404,6 +419,7 @@ define([ clearTimeout(pendingChanges[key]); delete pendingChanges[key]; }); + if (APP.stopHistory) { APP.history = false; } startOO(blob, type, true); }; @@ -413,8 +429,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(); @@ -532,6 +548,9 @@ define([ return new Blob([newText], {type: 'text/plain'}); }; var loadLastDocument = function (lastCp, onCpError, cb) { + if (!lastCp || !lastCp.file) { + return void onCpError('EEMPTY'); + } ooChannel.cpIndex = lastCp.index || 0; ooChannel.lastHash = lastCp.hash; var parsed = Hash.parsePadUrl(lastCp.file); @@ -597,6 +616,94 @@ define([ }); }; + var openVersionHash = function (version) { + readOnly = true; + var hashes = content.hashes || {}; + var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) { + return hashes[a].index - hashes[b].index; + }); + var s = version.split('.'); + if (s.length !== 2) { return UI.errorLoadingScreen(Messages.error); } + + var major = Number(s[0]); + var cpId = sortedCp[major - 1]; + var nextCpId = sortedCp[major]; + var cp = hashes[cpId] || {}; + + var minor = Number(s[1]) + 1; + + var toHash = cp.hash || 'NONE'; + var fromHash = nextCpId ? hashes[nextCpId].hash : 'NONE'; + + sframeChan.query('Q_GET_HISTORY_RANGE', { + channel: content.channel, + lastKnownHash: fromHash, + toHash: toHash, + }, function (err, data) { + if (err) { console.error(err); return void UI.errorLoadingScreen(Messages.error); } + if (!Array.isArray(data.messages)) { + console.error('Not an array'); + return void UI.errorLoadingScreen(Messages.error); + } + + // The first "cp" in history is the empty doc. It doesn't include the first patch + // of the history + var initialCp = major === 0; + var messages = (data.messages || []).slice(initialCp ? 0 : 1, minor); + + messages.forEach(function (obj) { + try { obj.msg = JSON.parse(obj.msg); } catch (e) { console.error(e); } + }); + + // The version exists if we have results in the "messages" array + // or if we requested a x.0 version + var exists = !Number(s[1]) || messages.length; + var vHashEl; + Messages.oo_deletedVersion = "This version no longer exists in the history."; // XXX + + if (!privateData.embed) { + Messages.infobar_versionHash = "You're currently viewing an old version of this document ({0})."; // XXX (duplicate from history branch) + var vTime = (messages[messages.length - 1] || {}).time; + var vTimeStr = vTime ? new Date(vTime).toLocaleString() + : 'v' + privateData.ooVersionHash; + var vTxt = Messages._getKey('infobar_versionHash',  [vTimeStr]); + + // If we expected patched and we don't have any, it means this part + // of the history has been deleted + var vType = "warning"; + if (!exists) { + vTxt = Messages.oo_deletedVersion + vType = "danger"; + } + + vHashEl = h('div.alert.alert-'+vType+'.cp-burn-after-reading', vTxt); + $('#cp-app-oo-editor').prepend(vHashEl); + } + + if (!exists) { return void UI.removeLoadingScreen(); } + + loadLastDocument(cp, function () { + if (cp.hash && vHashEl) { + // We requested a checkpoint but we can't find it... + UI.removeLoadingScreen(); + vHashEl.innerText = Messages.oo_deletedVersion; + $(vHashEl).removeClass('alert-warning').addClass('alert-danger'); + return; + } + var file = getFileType(); + var type = common.getMetadataMgr().getPrivateData().ooType; + var blob = loadInitDocument(type, true); + ooChannel.queue = messages; + resetData(blob, file); + UI.removeLoadingScreen(); + }, function (blob, file) { + ooChannel.queue = messages; + resetData(blob, file); + UI.removeLoadingScreen(); + }); + }); + }; + var openRtChannel = function (cb) { if (rtChannel.ready) { return void cb(); } var chan = content.channel || Hash.createChannelId(); @@ -619,10 +726,15 @@ define([ removeClient(obj.data); break; case 'MESSAGE': + 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) - if (readOnly) { + /*if (readOnly) { ooChannel.queue.push(obj.data); if (APP.refreshPopup) { return; } APP.refreshPopup = true; @@ -631,7 +743,7 @@ define([ // 1 popup every 15s APP.refreshRoTo = setTimeout(refreshReadOnly, READONLY_REFRESH_TO); return; - } + }*/ ooChannel.send(obj.data.msg); ooChannel.lastHash = obj.data.hash; ooChannel.cpIndex++; @@ -639,6 +751,12 @@ define([ ooChannel.queue.push(obj.data); } break; + case 'HISTORY_SYNCED': + if (typeof(APP.onHistorySynced) !== "function") { return; } + APP.onHistorySynced(); + delete APP.onHistorySynced; + break; + } }); }; @@ -812,6 +930,8 @@ define([ }; var handleLock = function (obj, send) { + if (APP.history) { return; } + if (content.saveLock) { if (!isLockedModal.modal) { isLockedModal.modal = UI.openCustomModal(isLockedModal.content); @@ -842,7 +962,9 @@ define([ if (isLockedModal.modal) { isLockedModal.modal.closeModal(); delete isLockedModal.modal; - $('#cp-app-oo-editor > iframe')[0].contentWindow.focus(); + if (!APP.history) { + $('#cp-app-oo-editor > iframe')[0].contentWindow.focus(); + } } send({ type: "getLock", @@ -1041,7 +1163,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; + var lock = !APP.history && (APP.migrate); // Starting from version 3, we can use the view mode again // defined but never used @@ -1170,6 +1292,10 @@ define([ if (lock) { getEditor().setViewModeDisconnect(); + } else if (readOnly) { + try { + getEditor().asc_setRestriction(true); + } catch (e) {} } else { setEditable(true); } @@ -1177,7 +1303,15 @@ define([ if (isLockedModal.modal && force) { isLockedModal.modal.closeModal(); delete isLockedModal.modal; - $('#cp-app-oo-editor > iframe')[0].contentWindow.focus(); + if (!APP.history) { + $('#cp-app-oo-editor > iframe')[0].contentWindow.focus(); + } + } + + if (APP.history) { + try { + getEditor().asc_setRestriction(true); + } catch (e) {} } if (APP.migrate && !readOnly) { @@ -1733,6 +1867,125 @@ define([ }); $save.appendTo(toolbar.$bottomM); } + + if (!privateData.ooVersionHash) { + (function () { + /* add a history button */ + var commit = function () { + // Wait for the checkpoint to be uploaded before leaving history mode + // (race condition). We use "stopHistory" to remove the history + // flag only when the checkpoint is ready. + APP.stopHistory = true; + makeCheckpoint(true); + }; + var loadCp = function (cp, keepQueue) { + loadLastDocument(cp, function () { + var file = getFileType(); + var type = common.getMetadataMgr().getPrivateData().ooType; + var blob = loadInitDocument(type, true); + if (!keepQueue) { ooChannel.queue = []; } + resetData(blob, file); + }, function (blob, file) { + if (!keepQueue) { ooChannel.queue = []; } + resetData(blob, file); + }); + }; + var onPatch = function (patch) { + // Patch on the current cp + ooChannel.send(JSON.parse(patch.msg)); + }; + var onCheckpoint = function (cp) { + // We want to load a checkpoint: + loadCp(cp); + }; + var setHistoryMode = function (bool) { + if (bool) { + APP.history = true; + getEditor().setViewModeDisconnect(); + return; + } + // Cancel button: redraw from lastCp + APP.history = false; + ooChannel.queue = []; + ooChannel.ready = false; + // Fill the queue and then load the last CP + rtChannel.getHistory(function () { + var lastCp = getLastCp(); + loadCp(lastCp, true); + }); + }; + + var deleteSnapshot = function (hash) { + var md = Util.clone(cpNfInner.metadataMgr.getMetadata()); + var snapshots = md.snapshots = md.snapshots || {}; + delete snapshots[hash]; + metadataMgr.updateMetadata(md); + APP.onLocal(); + }; + var makeSnapshot = function (title, cb, obj) { + var hash, time; + if (obj && obj.hash && obj.time) { + hash = obj.hash; + time = obj.time + } else { + var major = Object.keys(content.hashes).length; + var cpIndex = getLastCp().index || 0; + var minor = ooChannel.cpIndex - cpIndex; + hash = major+'.'+minor; + time = +new Date(); + } + var md = Util.clone(metadataMgr.getMetadata()); + var snapshots = md.snapshots = md.snapshots || {}; + if (snapshots[hash]) { cb('EEXISTS'); return void UI.warn(Messages.error); } // XXX + snapshots[hash] = { + title: title, + time: time + }; + metadataMgr.updateMetadata(md); + APP.onLocal(); + APP.realtime.onSettle(cb); + }; + var loadSnapshot = function (hash) { + sframeChan.event('EV_OO_OPENVERSION', { + hash: hash + }); + }; + + 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, + makeSnapshot: makeSnapshot, + onlyoffice: { + hashes: content.hashes || {}, + channel: content.channel, + lastHash: ooChannel.lastHash + }, + $toolbar: $('.cp-toolbar-container') + }; + History.create(common, histConfig); + }).appendTo(toolbar.$drawer); + + // Snapshots + var $snapshot = common.createButton('snapshots', true, { + remove: deleteSnapshot, + make: makeSnapshot, + load: loadSnapshot + }); + toolbar.$drawer.append($snapshot); + })(); + } + if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) { common.createButton('', true, { name: 'delete', @@ -1879,6 +2132,11 @@ define([ sframeChan.event('EV_BURN_PAD', content.channel); } + if (privateData.ooVersionHash) { + return void openVersionHash(privateData.ooVersionHash); + } + + var useNewDefault = content.version && content.version >= 2; openRtChannel(function () { setMyId(); diff --git a/www/common/onlyoffice/main.js b/www/common/onlyoffice/main.js index 9096cd730..f3bf744d4 100644 --- a/www/common/onlyoffice/main.js +++ b/www/common/onlyoffice/main.js @@ -4,12 +4,13 @@ define([ '/api/config', '/common/dom-ready.js', '/common/requireconfig.js', + '/common/common-hash.js', '/common/sframe-common-outer.js' -], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) { +], function (nThen, ApiConfig, DomReady, RequireConfig, Hash, SFCommonO) { var requireConfig = RequireConfig(); // Loaded in load #2 - var hash, href; + var hash, href, version; nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { @@ -27,6 +28,13 @@ define([ if (window.history && window.history.replaceState && hash) { window.history.replaceState({}, window.document.title, '#'); } + + var parsed = Hash.parsePadUrl(href); + if (parsed && parsed.hashData) { + var opts = parsed.getOptions(); + version = opts.versionHash; + } + document.getElementById('sbox-iframe').setAttribute('src', ApiConfig.httpSafeOrigin + window.location.pathname + 'inner.html?' + requireConfig.urlArgs + '#' + encodeURIComponent(JSON.stringify(req))); @@ -46,6 +54,7 @@ define([ }).nThen(function (/*waitFor*/) { var addData = function (obj) { obj.ooType = window.location.pathname.replace(/^\//, '').replace(/\/$/, ''); + obj.ooVersionHash = version; obj.ooForceVersion = localStorage.CryptPad_ooVersion || sessionStorage.CryptPad_ooVersion || ""; }; var addRpc = function (sframeChan, Cryptpad, Utils) { @@ -135,6 +144,13 @@ define([ } Cryptpad.onlyoffice.execCommand(obj, cb); }); + sframeChan.on('EV_OO_OPENVERSION', function (obj, cb) { + if (!obj || !obj.hash) { return; } + var parsed = Hash.parsePadUrl(window.location.href); + var opts = parsed.getOptions(); + opts.versionHash = obj.hash; + window.open(parsed.getUrl(opts)); + }); Cryptpad.onlyoffice.onEvent.reg(function (obj) { if (obj.ev === 'MESSAGE' && !/^cp\|/.test(obj.data)) { try { diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 2271df3ed..36e88f067 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -2090,7 +2090,7 @@ define([ if (first) { // If the first message if not a checkpoint, it means it is the first // message of the pad, so we have the full history! - if (!/^cp\|/.test(msg)) { fullHistory = true; } + if (!/^cp\|/.test(msg) && !data.toHash) { fullHistory = true; } lastKnownHash = msg.slice(0,64); first = false; } @@ -2107,7 +2107,8 @@ define([ network.on('message', onMsg); network.sendto(hk, JSON.stringify(['GET_HISTORY_RANGE', data.channel, { from: data.lastKnownHash, - cpCount: data.cpCount || 2, + to: data.toHash, + cpCount: data.cpCount || 2, // Ignored if "to" is provided txid: txid }])); }; diff --git a/www/common/outer/onlyoffice.js b/www/common/outer/onlyoffice.js index 567bc180b..773774c5a 100644 --- a/www/common/outer/onlyoffice.js +++ b/www/common/outer/onlyoffice.js @@ -2,6 +2,21 @@ 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'}); } + cb(); + chan.history.forEach(function (msg) { + ctx.emit('MESSAGE', { + msg: msg, + validateKey: chan.validateKey + }, [client]); + }); + ctx.emit('HISTORY_SYNCED', {}, [client]); + }; + var openChannel = function (ctx, obj, client, cb) { var channel = obj.channel; var padChan = obj.padChan; @@ -24,11 +39,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 +330,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); } diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index f5ba98bb8..bba393afb 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1106,8 +1106,9 @@ 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 }, function (data) { cb({