diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index 8f190a9ad..a91267132 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -32,7 +32,10 @@ define(function() { '#FF00C0', // hot pink '#800080', // purple ]; + config.enableTemplates = true; + config.enableHistory = true; + return config; }); diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less index 9b93a0da6..5e4f74dcf 100644 --- a/customize.dist/src/less/toolbar.less +++ b/customize.dist/src/less/toolbar.less @@ -409,6 +409,31 @@ .cryptpad-toolbar-rightside { text-align: right; } +.cryptpad-toolbar-history { + display: none; + text-align: center; + .next { + float: right; + } + .previous { + float: left; + } + .goto { + display: inline-block; + input { width: 50px; } + } + .gotoInput { + vertical-align: middle; + } +} +.cke_toolbox .cryptpad-toolbar-history { + input.gotoInput { + background: white; + height: 20px; + padding: 3px 3px; + border-radius: 5px; + } +} .cryptpad-spinner { height: 16px; width: 16px; diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index bbcffbf2f..1165c6df4 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -472,6 +472,31 @@ .cryptpad-toolbar-rightside { text-align: right; } +.cryptpad-toolbar-history { + display: none; + text-align: center; +} +.cryptpad-toolbar-history .next { + float: right; +} +.cryptpad-toolbar-history .previous { + float: left; +} +.cryptpad-toolbar-history .goto { + display: inline-block; +} +.cryptpad-toolbar-history .goto input { + width: 50px; +} +.cryptpad-toolbar-history .gotoInput { + vertical-align: middle; +} +.cke_toolbox .cryptpad-toolbar-history input.gotoInput { + background: white; + height: 20px; + padding: 3px 3px; + border-radius: 5px; +} .cryptpad-spinner { height: 16px; width: 16px; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 8879d961e..081cac6b7 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -115,6 +115,17 @@ define(function () { out.cancel = "Annuler"; out.cancelButton = 'Annuler (Echap)'; + out.historyButton = "Afficher l'historique du document"; + out.history_next = "Voir la version suivante"; + out.history_prev = "Voir la version précédente"; + out.history_goTo = "Voir la version sélectionnée"; + out.history_close = "Retour"; + out.history_closeTitle = "Fermer l'historique"; + out.history_restore = "Restaurer"; + out.history_restoreTitle = "Restaurer la version du document sélectionnée"; + out.history_restorePrompt = "Êtes-vous sûr de vouloir remplacer la version actuelle du document par la version affichée ?"; + out.history_restoreDone = "Document restauré"; + // Polls out.poll_title = "Sélecteur de date Zero Knowledge"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 2361becb2..e27a074b5 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -117,6 +117,17 @@ define(function () { out.cancel = "Cancel"; out.cancelButton = 'Cancel (esc)'; + out.historyButton = "Display the document history"; + out.history_next = "Go to the next version"; + out.history_prev = "Go to the previous version"; + out.history_goTo = "Go to the selected version"; + out.history_close = "Back"; + out.history_closeTitle = "Close the history"; + out.history_restore = "Restore"; + out.history_restoreTitle = "Restore the selected version of the document"; + out.history_restorePrompt = "Are you sure you want to replace the current version of the document by the displayed one?"; + out.history_restoreDone = "Document restored"; + // Polls out.poll_title = "Zero Knowledge Date Picker"; diff --git a/www/code/main.js b/www/code/main.js index 50bf06617..4f12d0926 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -52,6 +52,8 @@ define([ var defaultName = Cryptpad.getDefaultName(parsedHash); var initialState = Messages.codeInitialState; + var isHistoryMode = false; + var editor = module.editor = CMeditor.fromTextArea($textarea[0], { lineNumbers: true, lineWrapping: true, @@ -162,6 +164,14 @@ define([ var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); }; + var setHistory = function (bool, update) { + isHistoryMode = bool; + setEditable(!bool); + if (!bool && update) { + config.onRemote(); + } + }; + var isDefaultTitle = function () { var parsed = Cryptpad.parsePadUrl(window.location.href); return Cryptpad.isDefaultName(parsed, document.title); @@ -189,6 +199,7 @@ define([ var onLocal = config.onLocal = function () { if (initializing) { return; } + if (isHistoryMode) { return; } if (readOnly) { return; } editor.save(); @@ -370,7 +381,7 @@ define([ var onInit = config.onInit = function (info) { userList = info.userList; - var config = { + var configTb = { displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], userData: userData, readOnly: readOnly, @@ -386,8 +397,7 @@ define([ }, common: Cryptpad }; - if (readOnly) {delete config.changeNameID; } - toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, userList, config); + toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, userList, configTb); var $rightside = $bar.find('.' + Toolbar.constants.rightside); var $userBlock = $bar.find('.' + Toolbar.constants.username); @@ -400,6 +410,38 @@ define([ editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); } + /* add a history button */ + var histConfig = {}; + histConfig.onRender = function (val) { + if (typeof val === "undefined") { return; } + try { + var hjson = JSON.parse(val || '{}'); + var remoteDoc = hjson.content; + editor.setValue(remoteDoc || ''); + editor.save(); + } catch (e) { + // Probably a parse error + console.error(e); + } + }; + histConfig.onClose = function () { + // Close button clicked + setHistory(false, true); + }; + histConfig.onRevert = function () { + // Revert button clicked + setHistory(false, false); + config.onLocal(); + config.onRemote(); + }; + histConfig.onReady = function () { + // Called when the history is loaded and the UI displayed + setHistory(true); + }; + histConfig.$toolbar = $bar; + var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); + $rightside.append($hist); + /* save as template */ if (!Cryptpad.isTemplate(window.location.href)) { var templateObj = { @@ -646,6 +688,7 @@ define([ var onRemote = config.onRemote = function () { if (initializing) { return; } + if (isHistoryMode) { return; } var scroll = editor.getScrollInfo(); var oldDoc = canonicalize($textarea.val()); diff --git a/www/common/common-history.js b/www/common/common-history.js new file mode 100644 index 000000000..b82c60f23 --- /dev/null +++ b/www/common/common-history.js @@ -0,0 +1,225 @@ +define([ + 'jquery', + '/bower_components/chainpad-json-validator/json-ot.js', + '/bower_components/chainpad-crypto/crypto.js', + '/bower_components/chainpad/chainpad.dist.js', +], function ($, JsonOT, Crypto) { + var ChainPad = window.ChainPad; + var History = {}; + + var getStates = function (rt) { + var states = []; + var b = rt.getAuthBlock(); + if (b) { states.unshift(b); } + while (b.getParent()) { + b = b.getParent(); + states.unshift(b); + } + return states; + }; + + var loadHistory = function (common, cb) { + var network = common.getNetwork(); + var hkn = network.historyKeeper; + + var wcId = common.hrefToHexChannelId(window.location.href); + + var createRealtime = function(chan) { + return ChainPad.create({ + userName: 'history', + initialState: '', + transformFunction: JsonOT.validate, + logLevel: 0, + noPrune: true + }); + }; + var realtime = createRealtime(); + + var secret = common.getSecrets(); + var crypto = Crypto.createEncryptor(secret.keys); + + var to = window.setTimeout(function () { + cb('[GET_FULL_HISTORY_TIMEOUT]'); + }, 30000); + + var parse = function (msg) { + try { + return JSON.parse(msg); + } catch (e) { + return null; + } + }; + var onMsg = function (msg) { + var parsed = parse(msg); + if (parsed[0] === 'FULL_HISTORY_END') { + console.log('END'); + window.clearTimeout(to); + cb(null, realtime); + return; + } + if (parsed[0] !== 'FULL_HISTORY') { return; } + msg = parsed[1][4]; + if (msg) { + msg = msg.replace(/^cp\|/, ''); + var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey); + realtime.message(decryptedMsg); + } + }; + + network.on('message', function (msg, sender) { + onMsg(msg); + }); + + network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', wcId, secret.keys.validateKey])); + }; + + var create = History.create = function (common, config) { + if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");} + var $toolbar = config.$toolbar; + var noFunc = function () {}; + var render = config.onRender || noFunc; + var onClose = config.onClose || noFunc; + var onRevert = config.onRevert || noFunc; + var onReady = config.onReady || noFunc; + + var Messages = common.Messages; + + var realtime; + + var states = []; + var c = states.length - 1; + + var $hist = $toolbar.find('.cryptpad-toolbar-history'); + var $left = $toolbar.find('.cryptpad-toolbar-leftside'); + var $right = $toolbar.find('.cryptpad-toolbar-rightside'); + var $cke = $toolbar.find('.cke_toolbox_main'); + + var onUpdate; + + var update = function () { + if (!realtime) { return []; } + states = getStates(realtime); + if (typeof onUpdate === "function") { onUpdate(); } + return states; + }; + + // Get the content of the selected version, and change the version number + var get = function (i) { + i = parseInt(i); + if (isNaN(i)) { return; } + if (i < 0) { i = 0; } + if (i > states.length - 1) { i = states.length - 1; } + var val = states[i].getContent().doc; + c = i; + if (typeof onUpdate === "function") { onUpdate(); } + $hist.find('.next, .previous').show(); + if (c === states.length - 1) { $hist.find('.next').hide(); } + if (c === 0) { $hist.find('.previous').hide(); } + return val || ''; + }; + + var getNext = function (step) { + return typeof step === "number" ? get(c + step) : get(c + 1); + }; + var getPrevious = function (step) { + return typeof step === "number" ? get(c - step) : get(c - 1); + }; + + // Create the history toolbar + var display = function () { + $hist.html('').show(); + $left.hide(); + $right.hide(); + $cke.hide(); + var $prev =$('