From 368e253c9fc2ee1524ea29312f108722585eeeba Mon Sep 17 00:00:00 2001 From: Yann Flory Date: Fri, 16 Sep 2016 18:45:40 +0200 Subject: [PATCH 01/10] Ability to send a read-only URL --- NetfluxWebsocketSrv.js | 21 ++++++++++++++++ package.json | 3 ++- www/common/cryptpad-common.js | 46 +++++++++++++++++++++++++++++------ www/pad/main.js | 13 ++++++---- 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index a5e75c6cf..499a3cde8 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -1,5 +1,6 @@ ;(function () { 'use strict'; const Crypto = require('crypto'); +const Nacl = require('tweetnacl'); const LogStore = require('./storage/LogStore'); @@ -27,6 +28,15 @@ const sendMsg = function (ctx, user, msg) { const sendChannelMessage = function (ctx, channel, msgStruct) { msgStruct.unshift(0); + if (msgStruct[2] === 'MSG' && channel.validateKey) { + let msg = Nacl.util.decodeBase64(msgStruct[4]); + let validated = Nacl.sign.open(msg, channel.validateKey); + if (!validated) { + console.log("Unsigned message rejected"); + return; + } + msgStruct[4] = Nacl.util.encodeUTF8(validated); + } channel.forEach(function (user) { if(msgStruct[2] !== 'MSG' || user.id !== msgStruct[1]) { // We don't want to send back a message to its sender, in order to save bandwidth sendMsg(ctx, user, msgStruct); @@ -149,6 +159,17 @@ const handleMessage = function (ctx, user, msg) { clearTimeout(ctx.timeouts[chanName]); } + // validation key? + if (json[2]) { + if (chan.validateKey && Nacl.util.encodeBase64(chan.validateKey) !== json[2]) { + sendMsg(ctx, user, [seq, 'ERROR', 'INVALID_KEY', obj]); + return; + } + if (!chan.validateKey) { + chan.validateKey = Nacl.util.decodeBase64(json[2]); + } + } + chan.id = chanName; if (USE_HISTORY_KEEPER) { sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'JOIN', chanName]); diff --git a/package.json b/package.json index c61266ee7..ec9dbf734 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "dependencies": { "express": "~4.10.1", "ws": "^1.0.1", - "nthen": "~0.1.0" + "nthen": "~0.1.0", + "tweetnacl": "~0.12.2" }, "devDependencies": { "jshint": "~2.9.1", diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 1e1a67069..6b8415fd6 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -128,7 +128,7 @@ define([ var base64ToHex = common.base64ToHex = function (b64String) { var hexArray = []; - atob(b64String.replace(/-/g, '/') + "==").split("").forEach(function(e){ + atob(b64String.replace(/-/g, '/')).split("").forEach(function(e){ var h = e.charCodeAt(0).toString(16); if (h.length === 1) { h = "0"+h; } hexArray.push(h); @@ -136,18 +136,28 @@ define([ return hexArray.join(""); }; - var getHashFromKeys = common.getHashFromKeys = function (chanKey, cryptKey) { - return '/1/' + hexToBase64(chanKey) + '/' + cryptKey.replace(/\//g, '-'); + + var getEditHashFromKeys = common.getEditHashFromKeys = function (chanKey, keys) { + if (typeof keys === 'string') { + return chanKey + Crypto.b64RemoveSlashes(keys); + } + return '/1/edit/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.editKeyStr); + }; + var getViewHashFromKeys = common.getViewHashFromKeys = function (chanKey, keys) { + return '/1/view/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.viewKeyStr); }; + var getHashFromKeys = common.getHashFromKeys = getEditHashFromKeys; var getSecrets = common.getSecrets = function () { var secret = {}; if (!/#/.test(window.location.href)) { - secret.key = Crypto.genKey(); + secret.keys = Crypto.createEditCryptor(); + secret.key = Crypto.createEditCryptor().editKeyStr; } else { var hash = window.location.hash.slice(1); if (hash.length === 0) { - secret.key = Crypto.genKey(); + secret.keys = Crypto.createEditCryptor(); + secret.key = Crypto.createEditCryptor().editKeyStr; return secret; } common.redirect(hash); @@ -166,15 +176,35 @@ define([ throw new Error("Unable to parse the key"); } var version = hashArray[1]; - if (version === "1") { + /*if (version === "1") { secret.channel = base64ToHex(hashArray[2]); secret.key = hashArray[3].replace(/-/g, '/'); //TODO replace / by - if (secret.channel.length !== 32 || secret.key.length !== 24) { common.alert("The channel key and/or the encryption key is invalid"); - console.log("Channel key length : " + secret.channel.length + " != 32"); - console.log("Encryption key length : " + secret.key.length + " != 24"); throw new Error("The channel key and/or the encryption key is invalid"); } + }*/ + if (version === "1") { + var mode = hashArray[2]; + if (mode === 'edit') { + secret.channel = base64ToHex(hashArray[3]); + var keys = Crypto.createEditCryptor(hashArray[4].replace(/-/g, '/')); + secret.keys = keys; + secret.key = keys.editKeyStr; + if (secret.channel.length !== 32 || secret.key.length !== 24) { + common.alert("The channel key and/or the encryption key is invalid"); + throw new Error("The channel key and/or the encryption key is invalid"); + } + } + else if (mode === 'view') { + secret.channel = base64ToHex(hashArray[3]); + var keys = Crypto.createViewCryptor(hashArray[4].replace(/-/g, '/')); + secret.keys = keys; + if (secret.channel.length !== 32) { + common.alert("The channel key is invalid"); + throw new Error("The channel key is invalid"); + } + } } } } diff --git a/www/pad/main.js b/www/pad/main.js index fac663e51..922cf1f02 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -115,7 +115,6 @@ define([ } else { module.spinner.show(); } - inner.setAttribute('contenteditable', bool); }; @@ -317,14 +316,15 @@ define([ // the channel we will communicate over channel: secret.channel, - // our encryption key - cryptKey: secret.key, + // our public key. send -1 if view mode + validateKey: secret.keys.validateKey || undefined, + readOnly: secret.keys.editKeyStr ? undefined : 1, // method which allows us to get the id of the user setMyID: setMyID, // Pass in encrypt and decrypt methods - crypto: Crypto.createEncryptor(secret.key), + crypto: Crypto.createEncryptor(secret.keys), // really basic operational transform transformFunction : JsonOT.validate, @@ -545,7 +545,10 @@ define([ $rightside.append($forgetPad); // set the hash - window.location.hash = Cryptpad.getHashFromKeys(info.channel, secret.key); + if (secret.keys.editKeyStr) { + window.location.hash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + } + console.log("View Hash : " + Cryptpad.getViewHashFromKeys(info.channel, secret.keys)); Cryptpad.getPadTitle(function (err, title) { if (err) { From 391b7690e6887900c472d245740f65e6e4fb15f1 Mon Sep 17 00:00:00 2001 From: Yann Flory Date: Mon, 19 Sep 2016 14:52:36 +0200 Subject: [PATCH 02/10] Set the pad to readonly mode when using a 'view' url --- www/common/toolbar.js | 15 +++++++++++++-- www/pad/main.js | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 956defc4d..57e533b74 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -135,12 +135,21 @@ define([ return $span[0]; }; - var updateUserList = function (myUserName, listElement, userList, userData) { + var arrayIntersect = function(a, b) { + return $.grep(a, function(i) { + return $.inArray(i, b) > -1; + }); + }; + + var updateUserList = function (myUserName, listElement, userList, userData, readOnly) { var meIdx = userList.indexOf(myUserName); if (meIdx === -1) { listElement.textContent = Messages.synchronizing; return; } + console.log(userList); + userList = readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData)); + console.log(userList); if (userList.length === 1) { listElement.innerHTML = Messages.editingAlone; } else if (userList.length === 2) { @@ -187,6 +196,8 @@ define([ var changeNameID = config.changeNameID; var saveContentID = config.saveContentID || config.exportContentID; var loadContentID = config.loadContentID || config.importContentID; + // readOnly = 1 (readOnly enabled), 0 (disabled), -1 (old pad without readOnly mode) + var readOnly = (typeof config.readOnly !== "undefined") ? (readOnly ? 1 : 0) : -1; var saveElement; var loadElement; @@ -205,7 +216,7 @@ define([ if(newUserData) { // Someone has changed his name/color userData = newUserData; } - updateUserList(myUserName, userListElement, users, userData); + updateUserList(myUserName, userListElement, users, userData, readOnly); }; var ks = function () { diff --git a/www/pad/main.js b/www/pad/main.js index 922cf1f02..348e507d8 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -65,6 +65,7 @@ define([ var andThen = function (Ckeditor) { var secret = Cryptpad.getSecrets(); + var readOnly = !secret.keys.editKeyStr; var fixThings = false; @@ -115,7 +116,9 @@ define([ } else { module.spinner.show(); } - inner.setAttribute('contenteditable', bool); + if (!readOnly || !bool) { + inner.setAttribute('contenteditable', bool); + } }; // don't let the user edit until the pad is ready @@ -191,6 +194,12 @@ define([ } } + // Do not change the contenteditable value in view mode + if (readOnly && info.node && info.node.tagName === 'BODY' && + info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') { + return true; + } + // no use trying to recover the cursor if it doesn't exist if (!cursor.exists()) { return; } @@ -295,7 +304,9 @@ define([ var applyHjson = function (shjson) { var userDocStateDom = hjsonToDom(JSON.parse(shjson)); - userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf + if (!readOnly) { + userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf + } var patch = (DD).diff(inner, userDocStateDom); (DD).apply(inner, patch); }; @@ -318,7 +329,7 @@ define([ // our public key. send -1 if view mode validateKey: secret.keys.validateKey || undefined, - readOnly: secret.keys.editKeyStr ? undefined : 1, + readOnly: readOnly, // method which allows us to get the id of the user setMyID: setMyID, @@ -383,6 +394,8 @@ define([ // build a dom from HJSON, diff, and patch the editor applyHjson(shjson); + if (readOnly) { return; } + var shjson2 = stringifyDOM(inner); if (shjson2 !== shjson) { console.error("shjson2 !== shjson"); @@ -467,9 +480,11 @@ define([ var config = { userData: userList, changeNameID: Toolbar.constants.changeName, + readOnly: readOnly }; + if (readOnly) {delete config.changeNameID; } toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, info.userList, config); - createChangeName(Toolbar.constants.changeName, $bar); + if (!readOnly) { createChangeName(Toolbar.constants.changeName, $bar); } var $rightside = $bar.find('.' + Toolbar.constants.rightside); @@ -545,7 +560,7 @@ define([ $rightside.append($forgetPad); // set the hash - if (secret.keys.editKeyStr) { + if (!readOnly) { window.location.hash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); } console.log("View Hash : " + Cryptpad.getViewHashFromKeys(info.channel, secret.keys)); @@ -585,12 +600,17 @@ define([ } getLastName(function (err, lastName) { - if (typeof(lastName) === 'string' && lastName.length) { - setName(lastName); - } console.log("Unlocking editor"); setEditable(true); initializing = false; + myData[myID] = { + name: "" + }; + addToUserList(myData); + if (typeof(lastName) === 'string' && lastName.length) { + setName(lastName); + } + onLocal(); }); }; @@ -617,6 +637,7 @@ define([ var onLocal = realtimeOptions.onLocal = function () { if (initializing) { return; } + if (readOnly) { return; } // stringify the json and send it into chainpad var shjson = stringifyDOM(inner); From 99b50465c7b56b8d8d5c3a17c1badb10e548ba0e Mon Sep 17 00:00:00 2001 From: Yann Flory Date: Mon, 19 Sep 2016 17:52:37 +0200 Subject: [PATCH 03/10] Add the read-only mode for /pad and /code --- customize.dist/toolbar.css | 5 +++ customize.dist/translations/messages.fr.js | 6 +++ customize.dist/translations/messages.js | 6 +++ www/code/main.js | 52 +++++++++++++++------- www/common/toolbar.js | 40 +++++++++++++---- www/pad/main.js | 8 +++- 6 files changed, 91 insertions(+), 26 deletions(-) diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index 61b592eda..b03039473 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -83,3 +83,8 @@ .cryptpad-spinner { float: left; } +.cryptpad-readonly { + margin-right: 20px; + font-weight: bold; + text-transform: uppercase; +} \ No newline at end of file diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 4ddd2a2f6..813620dfb 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -20,6 +20,12 @@ define(function () { out.synchronizing = 'Synchronisation'; out.reconnecting = 'Reconnexion...'; out.lag = 'Latence'; + out.readonly = 'Lecture seule'; + out.nobodyIsEditing = "Personne n'édite le document"; + out.onePersonIsEditing = 'Une personne édite le document'; + out.peopleAreEditing = '{0} personnes éditent le document'; + out.oneViewer = '1 lecteur'; + out.viewers = '{0} lecteurs'; out.importButton = 'IMPORTER'; out.importButtonTitle = 'Importer un document depuis un fichier local'; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 3a43d9209..e4dec777e 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -20,6 +20,12 @@ define(function () { out.synchronizing = 'Synchronizing'; out.reconnecting = 'Reconnecting...'; out.lag = 'Lag'; + out.readonly = 'Read only'; + out.nobodyIsEditing = 'Nobody is editing'; + out.onePersonIsEditing = 'One person is editing'; + out.peopleAreEditing = '{0} people are editing'; + out.oneViewer = '1 viewer'; + out.viewers = '{0} viewers'; out.importButton = 'IMPORT'; out.importButtonTitle = 'Import a document from a local file'; diff --git a/www/code/main.js b/www/code/main.js index 764ff6698..db84ec3ec 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -39,6 +39,7 @@ define([ toolbar; var secret = Cryptpad.getSecrets(); + var readOnly = !secret.keys.editKeyStr; var andThen = function (CMeditor) { var CodeMirror = module.CodeMirror = CMeditor; @@ -106,6 +107,7 @@ define([ }()); var setEditable = module.setEditable = function (bool) { + if (readOnly && bool) { return; } editor.setOption('readOnly', !bool); }; @@ -132,8 +134,10 @@ define([ userName: userName, websocketURL: Config.websocketURL, channel: secret.channel, - //cryptKey: key, - crypto: Crypto.createEncryptor(secret.key), + // our public key + validateKey: secret.keys.validateKey || undefined, + readOnly: readOnly, + crypto: Crypto.createEncryptor(secret.keys), setMyID: setMyID, transformFunction: JsonOT.validate }; @@ -274,9 +278,11 @@ define([ var config = { userData: userList, changeNameID: Toolbar.constants.changeName, + readOnly: readOnly }; + if (readOnly) {delete config.changeNameID; } toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, info.userList, config); - createChangeName(Toolbar.constants.changeName, $bar); + if (!readOnly) { createChangeName(Toolbar.constants.changeName, $bar); } var $rightside = $bar.find('.' + Toolbar.constants.rightside); @@ -443,7 +449,11 @@ define([ configureTheme(); }); - window.location.hash = Cryptpad.getHashFromKeys(info.channel, secret.key); + // set the hash + if (!readOnly) { + window.location.hash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + } + console.log("View Hash : " + Cryptpad.getViewHashFromKeys(info.channel, secret.keys)); Cryptpad.getPadTitle(function (err, title) { if (err) { console.log("Unable to get pad title"); @@ -510,7 +520,7 @@ define([ } // Update the user list (metadata) from the hyperjson - //updateUserList(shjson); + updateUserList(userDoc); editor.setValue(newDoc || Messages.codeInitialState); @@ -531,9 +541,17 @@ define([ console.error(err); return; } + // Update the toolbar list: + // Add the current user in the metadata if he has edit rights + if (readOnly) { return; } + myData[myID] = { + name: "" + }; + addToUserList(myData); if (typeof(lastName) === 'string' && lastName.length) { setName(lastName); } + onLocal(); }); }; @@ -604,17 +622,19 @@ define([ editor.scrollTo(scroll.left, scroll.top); - var localDoc = canonicalize($textarea.val()); - var hjson2 = { - content: localDoc, - metadata: userList, - highlightMode: highlightMode, - }; - var shjson2 = stringify(hjson2); - if (shjson2 !== shjson) { - console.error("shjson2 !== shjson"); - TextPatcher.log(shjson, TextPatcher.diff(shjson, shjson2)); - module.patchText(shjson2); + if (!readOnly) { + var localDoc = canonicalize($textarea.val()); + var hjson2 = { + content: localDoc, + metadata: userList, + highlightMode: highlightMode, + }; + var shjson2 = stringify(hjson2); + if (shjson2 !== shjson) { + console.error("shjson2 !== shjson"); + TextPatcher.log(shjson, TextPatcher.diff(shjson, shjson2)); + module.patchText(shjson2); + } } notify(); diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 2f9ca2ce6..865529d78 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -141,22 +141,44 @@ define([ }); }; + var getViewers = function (n) { + if (!n || !parseInt(n) || n === 0) { return ''; } + if (n === 1) { return '; + ' + Messages.oneViewer; } + return '; + ' + Messages._getKey('viewers', [n]); + } var updateUserList = function (myUserName, listElement, userList, userData, readOnly) { var meIdx = userList.indexOf(myUserName); if (meIdx === -1) { listElement.textContent = Messages.synchronizing; return; } - console.log(userList); + var numberOfUsers = userList.length; userList = readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData)); - console.log(userList); - if (userList.length === 1) { - listElement.innerHTML = Messages.editingAlone; - } else if (userList.length === 2) { - listElement.innerHTML = Messages.editingWithOneOtherPerson + getOtherUsers(myUserName, userList, userData); - } else { - listElement.innerHTML = Messages.editingWith + ' ' + (userList.length - 1) + ' ' + Messages.otherPeople + getOtherUsers(myUserName, userList, userData); + var innerHTML; + var numberOfViewUsers = numberOfUsers - userList.length; + if (readOnly === 1) { + innerHTML = '' + Messages.readonly + ''; + if (userList.length === 0) { + innerHTML += Messages.nobodyIsEditing; + } else if (userList.length === 1) { + innerHTML += Messages.onePersonIsEditing + getOtherUsers(myUserName, userList, userData); + } else { + innerHTML += Messages._getKey('peopleAreEditing', [userList.length]) + getOtherUsers(myUserName, userList, userData); + } + // Remove the current user + numberOfViewUsers--; + } + else { + if (userList.length === 1) { + innerHTML = Messages.editingAlone; + } else if (userList.length === 2) { + innerHTML = Messages.editingWithOneOtherPerson + getOtherUsers(myUserName, userList, userData); + } else { + innerHTML = Messages.editingWith + ' ' + (userList.length - 1) + ' ' + Messages.otherPeople + getOtherUsers(myUserName, userList, userData); + } } + innerHTML += getViewers(numberOfViewUsers); + listElement.innerHTML = innerHTML; }; var createLagElement = function ($container) { @@ -197,7 +219,7 @@ define([ var saveContentID = config.saveContentID || config.exportContentID; var loadContentID = config.loadContentID || config.importContentID; // readOnly = 1 (readOnly enabled), 0 (disabled), -1 (old pad without readOnly mode) - var readOnly = (typeof config.readOnly !== "undefined") ? (readOnly ? 1 : 0) : -1; + var readOnly = (typeof config.readOnly !== "undefined") ? (config.readOnly ? 1 : 0) : -1; var saveElement; var loadElement; diff --git a/www/pad/main.js b/www/pad/main.js index 348e507d8..f94c86981 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -327,7 +327,7 @@ define([ // the channel we will communicate over channel: secret.channel, - // our public key. send -1 if view mode + // our public key validateKey: secret.keys.validateKey || undefined, readOnly: readOnly, @@ -593,6 +593,9 @@ define([ var shjson = info.realtime.getUserDoc(); applyHjson(shjson); + // Update the user list (metadata) from the hyperjson + updateUserList(shjson); + if (Visible.isSupported()) { Visible.onChange(function (yes) { if (yes) { unnotify(); } @@ -603,6 +606,9 @@ define([ console.log("Unlocking editor"); setEditable(true); initializing = false; + // Update the toolbar list: + // Add the current user in the metadata if he has edit rights + if (readOnly) { return; } myData[myID] = { name: "" }; From bf8c9a2c4be480de4dafa8b68573a781ffee0cac Mon Sep 17 00:00:00 2001 From: Yann Flory Date: Tue, 20 Sep 2016 11:35:57 +0200 Subject: [PATCH 04/10] Add a button to get the read only URL --- customize.dist/translations/messages.fr.js | 16 ++++++++----- customize.dist/translations/messages.js | 17 ++++++++------ www/code/main.js | 27 ++++++++++++++++++++-- www/pad/main.js | 26 +++++++++++++++++++-- 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 813620dfb..9fd72124a 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -3,6 +3,11 @@ define(function () { out.main_title = "Cryptpad: Editeur collaboratif en temps réel, zero knowledge"; + out.type = {}; + out.type.pad = 'Pad'; + out.type.code = 'Code'; + out.type.poll = 'Sondage'; + out.type.slide = 'Présentation'; out.errorBox_errorType_disconnected = 'Connexion perdue'; out.errorBox_errorExplanation_disconnected = [ @@ -60,6 +65,11 @@ define(function () { out.commitButton = 'VALIDER'; + out.linksButton = 'LIENS'; + out.linksButtonTitle = 'Obtenir les liens disponibles pour accéder à ce document'; + out.readonlyUrl = 'Lien de lecture seule'; + out.editUrl = "Lien d'édition"; + out.disconnectAlert = 'Perte de la connexion au réseau !'; out.tryIt = 'Essayez-le !'; @@ -71,12 +81,6 @@ define(function () { out.loginText = '

Votre nom d\'utilisateur et votre mot de passe sont utilisés pour générer une clé unique qui reste inconnue de notre serveur.

\n' + '

Faites attention de ne pas oublier vos identifiants puisqu\'ils seront impossible à récupérer.

'; - out.type = {}; - out.type.pad = 'Pad'; - out.type.code = 'Code'; - out.type.poll = 'Sondage'; - out.type.slide = 'Présentation'; - out.forget = "Oublier"; // Polls diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index e4dec777e..eda389589 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -3,6 +3,11 @@ define(function () { out.main_title = "Cryptpad: Zero Knowledge, Collaborative Real Time Editing"; + out.type = {}; + out.type.pad = 'Pad'; + out.type.code = 'Code'; + out.type.poll = 'Poll'; + out.type.slide = 'Presentation'; out.errorBox_errorType_disconnected = 'Connection Lost'; out.errorBox_errorExplanation_disconnected = [ @@ -59,6 +64,11 @@ define(function () { out.presentSuccess = 'Hit ESC to exit presentation mode'; out.commitButton = 'COMMIT'; + + out.linksButton = 'LINKS'; + out.linksButtonTitle = 'Get the available links for this document'; + out.readonlyUrl = 'Read only URL'; + out.editUrl = 'Edit URL'; out.disconnectAlert = 'Network connection lost!'; @@ -71,13 +81,6 @@ define(function () { out.loginText = '

Your username and password are used to generate a unique key which is never known by our server.

\n' + '

Be careful not to forget your credentials, as they are impossible to recover

'; - // TODO : move at the beginning - out.type = {}; - out.type.pad = 'Pad'; - out.type.code = 'Code'; - out.type.poll = 'Poll'; - out.type.slide = 'Presentation'; - out.forget = "Forget"; // Polls diff --git a/www/code/main.js b/www/code/main.js index db84ec3ec..491de8e43 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -286,6 +286,13 @@ define([ var $rightside = $bar.find('.' + Toolbar.constants.rightside); + var editHash; + var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys); + + if (!readOnly) { + editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + } + /* add an export button */ var $export = $('