Merge branch 'newhashNoConflict' into merge-hash

pull/1/head
ansuz 8 years ago
commit 6315915b5d

@ -1,5 +1,6 @@
;(function () { 'use strict'; ;(function () { 'use strict';
const Crypto = require('crypto'); const Crypto = require('crypto');
const Nacl = require('tweetnacl');
const LogStore = require('./storage/LogStore'); const LogStore = require('./storage/LogStore');
@ -12,6 +13,7 @@ const USE_FILE_BACKUP_STORAGE = true;
let dropUser; let dropUser;
let historyKeeperKeys = {};
const now = function () { return (new Date()).getTime(); }; const now = function () { return (new Date()).getTime(); };
@ -25,6 +27,16 @@ const sendMsg = function (ctx, user, msg) {
} }
}; };
const storeMessage = function (ctx, channel, msg) {
ctx.store.message(channel.id, msg, function (err) {
if (err && typeof(err) !== 'function') {
// ignore functions because older datastores
// might pass waitFors into the callback
console.log("Error writing message: " + err);
}
});
};
const sendChannelMessage = function (ctx, channel, msgStruct) { const sendChannelMessage = function (ctx, channel, msgStruct) {
msgStruct.unshift(0); msgStruct.unshift(0);
channel.forEach(function (user) { channel.forEach(function (user) {
@ -33,13 +45,17 @@ const sendChannelMessage = function (ctx, channel, msgStruct) {
} }
}); });
if (USE_HISTORY_KEEPER && msgStruct[2] === 'MSG') { if (USE_HISTORY_KEEPER && msgStruct[2] === 'MSG') {
ctx.store.message(channel.id, JSON.stringify(msgStruct), function (err) { if (historyKeeperKeys[channel.id]) {
if (err && typeof(err) !== 'function') { let signedMsg = msgStruct[4].replace(/^cp\|/, '');
// ignore functions because older datastores signedMsg = Nacl.util.decodeBase64(signedMsg);
// might pass waitFors into the callback let validateKey = Nacl.util.decodeBase64(historyKeeperKeys[channel.id]);
console.log("Error writing message: " + err); let validated = Nacl.sign.open(signedMsg, validateKey);
if (!validated) {
console.log("Signed message rejected");
return;
} }
}); }
storeMessage(ctx, channel, JSON.stringify(msgStruct));
} }
}; };
@ -68,6 +84,7 @@ dropUser = function (ctx, user) {
if (chan.length === 0) { if (chan.length === 0) {
console.log("Removing empty channel ["+chanName+"]"); console.log("Removing empty channel ["+chanName+"]");
delete ctx.channels[chanName]; delete ctx.channels[chanName];
delete historyKeeperKeys[chanName];
/* Call removeChannel if it is a function and channel removal is /* Call removeChannel if it is a function and channel removal is
set to true in the config file */ set to true in the config file */
@ -94,8 +111,15 @@ dropUser = function (ctx, user) {
const getHistory = function (ctx, channelName, handler, cb) { const getHistory = function (ctx, channelName, handler, cb) {
var messageBuf = []; var messageBuf = [];
var messageKey;
ctx.store.getMessages(channelName, function (msgStr) { ctx.store.getMessages(channelName, function (msgStr) {
messageBuf.push(JSON.parse(msgStr)); var parsed = JSON.parse(msgStr);
if (parsed.validateKey) {
historyKeeperKeys[channelName] = parsed.validateKey;
handler(parsed);
return;
}
messageBuf.push(parsed);
}, function (err) { }, function (err) {
if (err) { if (err) {
console.log("Error getting messages " + err.stack); console.log("Error getting messages " + err.stack);
@ -120,7 +144,7 @@ const getHistory = function (ctx, channelName, handler, cb) {
// no checkpoints. // no checkpoints.
for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); } for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); }
} }
cb(); cb(messageBuf);
}); });
}; };
@ -166,8 +190,15 @@ const handleMessage = function (ctx, user, msg) {
sendMsg(ctx, user, [seq, 'ACK']); sendMsg(ctx, user, [seq, 'ACK']);
getHistory(ctx, parsed[1], function (msg) { getHistory(ctx, parsed[1], function (msg) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]); sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]);
}, function () { }, function (messages) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, 0]); // parsed[2] is a validation key if it exists
if (messages.length === 0 && parsed[2] && !historyKeeperKeys[parsed[1]]) {
var key = {channel: parsed[1], validateKey: parsed[2]};
storeMessage(ctx, ctx.channels[parsed[1]], JSON.stringify(key));
historyKeeperKeys[parsed[1]] = parsed[2];
}
let parsedMsg = {state: 1, channel: parsed[1]};
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(parsedMsg)]);
}); });
} }
return; return;

@ -106,9 +106,17 @@ define([
}); });
}); });
var readOnly = false;
if (pad.href.indexOf('#') !== -1) {
var modeArray = pad.href.split('#')[1].split('/');
if (modeArray.length >= 3 && modeArray[2] === 'view') {
readOnly = true;
}
}
var readOnlyText = readOnly ? '(' + Messages.readonly + ') ' : '';
$row $row
.append($('<td>').text(name)) .append($('<td>').text(name))
.append($('<td>').append($('<a>', { .append($('<td>').append(readOnlyText).append($('<a>', {
href: pad.href, href: pad.href,
title: pad.title, title: pad.title,
}).text(shortTitle))) }).text(shortTitle)))
@ -134,6 +142,9 @@ define([
if (hasRecent) { if (hasRecent) {
$('table').attr('style', ''); $('table').attr('style', '');
// Race condition here, this is triggered before the localization in HTML
// so we have to remove the data-localization attr
$tryit.removeAttr('data-localization');
$tryit.text(Messages.recentPads); $tryit.text(Messages.recentPads);
} }
}); });

@ -60,14 +60,6 @@
margin-right: 5px; margin-right: 5px;
padding-left: 5px; padding-left: 5px;
} }
.cryptpad-changeName {
float: left;
cursor: pointer;
}
.cryptpad-changeName button {
padding: 0;
margin-right: 5px;
}
.cryptpad-toolbar-leftside { .cryptpad-toolbar-leftside {
float: left; float: left;
} }
@ -83,3 +75,11 @@
.cryptpad-spinner { .cryptpad-spinner {
float: left; float: left;
} }
.cryptpad-readonly {
margin-right: 20px;
font-weight: bold;
text-transform: uppercase;
}
.cryptpad-toolbar-username {
font-style: italic;
};

@ -3,6 +3,11 @@ define(function () {
out.main_title = "Cryptpad: Editeur collaboratif en temps réel, zero knowledge"; 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_errorType_disconnected = 'Connexion perdue';
out.errorBox_errorExplanation_disconnected = [ out.errorBox_errorExplanation_disconnected = [
@ -20,6 +25,12 @@ define(function () {
out.synchronizing = 'Synchronisation'; out.synchronizing = 'Synchronisation';
out.reconnecting = 'Reconnexion...'; out.reconnecting = 'Reconnexion...';
out.lag = 'Latence'; 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.importButton = 'IMPORTER';
out.importButtonTitle = 'Importer un document depuis un fichier local'; out.importButtonTitle = 'Importer un document depuis un fichier local';
@ -54,6 +65,10 @@ define(function () {
out.commitButton = 'VALIDER'; out.commitButton = 'VALIDER';
out.getViewButton = 'LECTURE SEULE';
out.getViewButtonTitle = "Obtenir l'adresse d'accès à ce document en lecture seule";
out.readonlyUrl = 'URL de lecture seule';
out.disconnectAlert = 'Perte de la connexion au réseau !'; out.disconnectAlert = 'Perte de la connexion au réseau !';
out.tryIt = 'Essayez-le !'; out.tryIt = 'Essayez-le !';
@ -65,12 +80,6 @@ define(function () {
out.loginText = '<p>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.</p>\n' + out.loginText = '<p>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.</p>\n' +
'<p>Faites attention de ne pas oublier vos identifiants puisqu\'ils seront impossible à récupérer.</p>'; '<p>Faites attention de ne pas oublier vos identifiants puisqu\'ils seront impossible à récupérer.</p>';
out.type = {};
out.type.pad = 'Pad';
out.type.code = 'Code';
out.type.poll = 'Sondage';
out.type.slide = 'Présentation';
out.forget = "Oublier"; out.forget = "Oublier";
// Polls // Polls

@ -3,6 +3,11 @@ define(function () {
out.main_title = "Cryptpad: Zero Knowledge, Collaborative Real Time Editing"; 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_errorType_disconnected = 'Connection Lost';
out.errorBox_errorExplanation_disconnected = [ out.errorBox_errorExplanation_disconnected = [
@ -20,6 +25,12 @@ define(function () {
out.synchronizing = 'Synchronizing'; out.synchronizing = 'Synchronizing';
out.reconnecting = 'Reconnecting...'; out.reconnecting = 'Reconnecting...';
out.lag = 'Lag'; 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.importButton = 'IMPORT';
out.importButtonTitle = 'Import a document from a local file'; out.importButtonTitle = 'Import a document from a local file';
@ -54,6 +65,10 @@ define(function () {
out.commitButton = 'COMMIT'; out.commitButton = 'COMMIT';
out.getViewButton = 'READ-ONLY URL';
out.getViewButtonTitle = 'Get the read-only URL for this document';
out.readonlyUrl = 'Read only URL';
out.disconnectAlert = 'Network connection lost!'; out.disconnectAlert = 'Network connection lost!';
out.tryIt = 'Try it out!'; out.tryIt = 'Try it out!';
@ -65,13 +80,6 @@ define(function () {
out.loginText = '<p>Your username and password are used to generate a unique key which is never known by our server.</p>\n' + out.loginText = '<p>Your username and password are used to generate a unique key which is never known by our server.</p>\n' +
'<p>Be careful not to forget your credentials, as they are impossible to recover</p>'; '<p>Be careful not to forget your credentials, as they are impossible to recover</p>';
// 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"; out.forget = "Forget";
// Polls // Polls

@ -5,7 +5,8 @@
"dependencies": { "dependencies": {
"express": "~4.10.1", "express": "~4.10.1",
"ws": "^1.0.1", "ws": "^1.0.1",
"nthen": "~0.1.0" "nthen": "~0.1.0",
"tweetnacl": "~0.12.2"
}, },
"devDependencies": { "devDependencies": {
"jshint": "~2.9.1", "jshint": "~2.9.1",

@ -38,6 +38,10 @@ define([
var toolbar; var toolbar;
var secret = Cryptpad.getSecrets(); var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
if (!secret.keys) {
secret.keys = secret.key;
}
var andThen = function (CMeditor) { var andThen = function (CMeditor) {
var CodeMirror = module.CodeMirror = CMeditor; var CodeMirror = module.CodeMirror = CMeditor;
@ -105,6 +109,7 @@ define([
}()); }());
var setEditable = module.setEditable = function (bool) { var setEditable = module.setEditable = function (bool) {
if (readOnly && bool) { return; }
editor.setOption('readOnly', !bool); editor.setOption('readOnly', !bool);
}; };
@ -131,7 +136,10 @@ define([
initialState: '{}', initialState: '{}',
websocketURL: Config.websocketURL, websocketURL: Config.websocketURL,
channel: secret.channel, channel: secret.channel,
crypto: Crypto.createEncryptor(secret.key), // our public key
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys),
setMyID: setMyID, setMyID: setMyID,
transformFunction: JsonOT.validate transformFunction: JsonOT.validate
}; };
@ -142,6 +150,7 @@ define([
var onLocal = config.onLocal = function () { var onLocal = config.onLocal = function () {
if (initializing) { return; } if (initializing) { return; }
if (readOnly) { return; }
editor.save(); editor.save();
var textValue = canonicalize($textarea.val()); var textValue = canonicalize($textarea.val());
@ -177,7 +186,7 @@ define([
name: myUserName name: myUserName
}; };
addToUserList(myData); addToUserList(myData);
Cryptpad.setPadAttribute('username', myUserName, function (err, data) { Cryptpad.setAttribute('username', myUserName, function (err, data) {
if (err) { if (err) {
console.log("Couldn't set username"); console.log("Couldn't set username");
console.error(err); console.error(err);
@ -188,7 +197,7 @@ define([
}; };
var getLastName = function (cb) { var getLastName = function (cb) {
Cryptpad.getPadAttribute('username', function (err, userName) { Cryptpad.getAttribute('username', function (err, userName) {
cb(err, userName || ''); cb(err, userName || '');
}); });
}; };
@ -275,12 +284,21 @@ define([
var config = { var config = {
userData: userList, userData: userList,
changeNameID: Toolbar.constants.changeName, 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); 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); 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 */ /* add an export button */
var $export = $('<button>', { var $export = $('<button>', {
title: Messages.exportButtonTitle, title: Messages.exportButtonTitle,
@ -290,6 +308,7 @@ define([
.click(exportText); .click(exportText);
$rightside.append($export); $rightside.append($export);
if (!readOnly) {
/* add an import button */ /* add an import button */
var $import = $('<button>',{ var $import = $('<button>',{
title: Messages.importButtonTitle title: Messages.importButtonTitle
@ -322,6 +341,7 @@ define([
onLocal(); onLocal();
})); }));
$rightside.append($import); $rightside.append($import);
}
/* add a rename button */ /* add a rename button */
var $setTitle = $('<button>', { var $setTitle = $('<button>', {
@ -387,6 +407,21 @@ define([
}); });
$rightside.append($forgetPad); $rightside.append($forgetPad);
if (!readOnly && viewHash) {
/* add a 'links' button */
var $links = $('<button>', {
title: Messages.getViewButtonTitle
})
.text(Messages.getViewButton)
.addClass('rightside-button')
.click(function () {
var baseUrl = window.location.origin + window.location.pathname + '#';
var content = '<b>' + Messages.readonlyUrl + '</b><br><a>' + baseUrl + viewHash + '</a><br>';
Cryptpad.alert(content);
});
$rightside.append($links);
}
var configureLanguage = function (cb) { var configureLanguage = function (cb) {
// FIXME this is async so make it happen as early as possible // FIXME this is async so make it happen as early as possible
@ -441,11 +476,20 @@ define([
}); });
}; };
if (!readOnly) {
configureLanguage(function () { configureLanguage(function () {
configureTheme(); configureTheme();
}); });
}
else {
configureTheme();
}
// set the hash
if (!readOnly) {
window.location.hash = editHash;
}
window.location.hash = Cryptpad.getHashFromKeys(info.channel, secret.key);
Cryptpad.getPadTitle(function (err, title) { Cryptpad.getPadTitle(function (err, title) {
if (err) { if (err) {
console.log("Unable to get pad title"); console.log("Unable to get pad title");
@ -532,7 +576,7 @@ define([
} }
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
//updateUserList(shjson); updateMetadata(userDoc);
editor.setValue(newDoc || Messages.codeInitialState); editor.setValue(newDoc || Messages.codeInitialState);
@ -554,9 +598,17 @@ define([
console.error(err); console.error(err);
return; 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) { if (typeof(lastName) === 'string' && lastName.length) {
setName(lastName); setName(lastName);
} }
onLocal();
}); });
}; };
@ -627,6 +679,7 @@ define([
editor.scrollTo(scroll.left, scroll.top); editor.scrollTo(scroll.left, scroll.top);
if (!readOnly) {
var localDoc = canonicalize($textarea.val()); var localDoc = canonicalize($textarea.val());
var hjson2 = { var hjson2 = {
content: localDoc, content: localDoc,
@ -642,6 +695,7 @@ define([
TextPatcher.log(shjson, TextPatcher.diff(shjson, shjson2)); TextPatcher.log(shjson, TextPatcher.diff(shjson, shjson2));
module.patchText(shjson2); module.patchText(shjson2);
} }
}
notify(); notify();
}; };

@ -128,7 +128,7 @@ define([
var base64ToHex = common.base64ToHex = function (b64String) { var base64ToHex = common.base64ToHex = function (b64String) {
var hexArray = []; 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); var h = e.charCodeAt(0).toString(16);
if (h.length === 1) { h = "0"+h; } if (h.length === 1) { h = "0"+h; }
hexArray.push(h); hexArray.push(h);
@ -136,18 +136,31 @@ define([
return hexArray.join(""); 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 + keys;
}
return '/1/edit/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.editKeyStr);
};
var getViewHashFromKeys = common.getViewHashFromKeys = function (chanKey, keys) {
if (typeof keys === 'string') {
return;
}
return '/1/view/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.viewKeyStr);
}; };
var getHashFromKeys = common.getHashFromKeys = getEditHashFromKeys;
var getSecrets = common.getSecrets = function () { var getSecrets = common.getSecrets = function () {
var secret = {}; var secret = {};
if (!/#/.test(window.location.href)) { if (!/#/.test(window.location.href)) {
secret.key = Crypto.genKey(); secret.keys = Crypto.createEditCryptor();
secret.key = Crypto.createEditCryptor().editKeyStr;
} else { } else {
var hash = window.location.hash.slice(1); var hash = window.location.hash.slice(1);
if (hash.length === 0) { if (hash.length === 0) {
secret.key = Crypto.genKey(); secret.keys = Crypto.createEditCryptor();
secret.key = Crypto.createEditCryptor().editKeyStr;
return secret; return secret;
} }
common.redirect(hash); common.redirect(hash);
@ -166,16 +179,35 @@ define([
throw new Error("Unable to parse the key"); throw new Error("Unable to parse the key");
} }
var version = hashArray[1]; var version = hashArray[1];
if (version === "1") { /*if (version === "1") {
secret.channel = base64ToHex(hashArray[2]); secret.channel = base64ToHex(hashArray[2]);
secret.key = hashArray[3].replace(/-/g, '/'); //TODO replace / by - secret.key = hashArray[3].replace(/-/g, '/');
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");
}
}*/
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) { if (secret.channel.length !== 32 || secret.key.length !== 24) {
common.alert("The channel key and/or the encryption key is invalid"); 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"); throw new Error("The channel key and/or the encryption key is invalid");
} }
} }
else if (mode === 'view') {
secret.channel = base64ToHex(hashArray[3]);
secret.keys = Crypto.createViewCryptor(hashArray[4].replace(/-/g, '/'));
if (secret.channel.length !== 32) {
common.alert("The channel key is invalid");
throw new Error("The channel key is invalid");
}
}
}
} }
} }
return secret; return secret;
@ -285,6 +317,12 @@ define([
cb(err, data); cb(err, data);
}); });
}; };
var setAttribute = common.setAttribute = function (attr, value, cb, legacy) {
getStore(legacy).set(["cryptpad", attr].join('.'), value, function (err, data) {
cb(err, data);
});
};
// STORAGE // STORAGE
var getPadAttribute = common.getPadAttribute = function (attr, cb, legacy) { var getPadAttribute = common.getPadAttribute = function (attr, cb, legacy) {
@ -292,6 +330,12 @@ define([
cb(err, data); cb(err, data);
}); });
}; };
var getAttribute = common.getAttribute = function (attr, cb, legacy) {
getStore(legacy).get(["cryptpad", attr].join('.'), function (err, data) {
cb(err, data);
});
};
// STORAGE // STORAGE
/* fetch and migrate your pad history from localStorage */ /* fetch and migrate your pad history from localStorage */

@ -27,6 +27,9 @@ define([
var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner'; var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner';
var USERNAME_CLS = Bar.constants.username = 'cryptpad-toolbar-username';
var READONLY_CLS = Bar.constants.readonly = 'cryptpad-readonly';
/** Key in the localStore which indicates realtime activity should be disallowed. */ /** Key in the localStore which indicates realtime activity should be disallowed. */
// TODO remove? will never be used in cryptpad // TODO remove? will never be used in cryptpad
var LOCALSTORAGE_DISALLOW = Bar.constants.localstorageDisallow = 'cryptpad-disallow'; var LOCALSTORAGE_DISALLOW = Bar.constants.localstorageDisallow = 'cryptpad-disallow';
@ -121,33 +124,67 @@ define([
return (i > 0) ? list.slice(0, -2) : list; return (i > 0) ? list.slice(0, -2) : list;
}; };
var createChangeName = function($container, userList, buttonID) { var createChangeName = function($container, buttonID) {
var $span = $('<span>', { var $span = $('<span>', {
id: uid(), id: uid(),
}); });
var $button = $('<button>', { var $button = $('<button>', {
id: buttonID, id: buttonID,
'class': USERNAME_BUTTON_GROUP, 'class': 'rightside-button',
}).text(Messages.changeNameButton); }).text(Messages.changeNameButton);
$(userList).append($button); $container.append($button);
$button.after($span); $button.after($span);
return $span[0]; 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 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); var meIdx = userList.indexOf(myUserName);
if (meIdx === -1) { if (meIdx === -1) {
listElement.textContent = Messages.synchronizing; listElement.textContent = Messages.synchronizing;
return; return;
} }
var numberOfUsers = userList.length;
userList = readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData));
var innerHTML;
var numberOfViewUsers = numberOfUsers - userList.length;
if (readOnly === 1) {
innerHTML = '<span class="' + READONLY_CLS + '">' + Messages.readonly + '</span>';
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) { if (userList.length === 1) {
listElement.innerHTML = Messages.editingAlone; innerHTML = Messages.editingAlone;
} else if (userList.length === 2) { } else if (userList.length === 2) {
listElement.innerHTML = Messages.editingWithOneOtherPerson + getOtherUsers(myUserName, userList, userData); innerHTML = Messages.editingWithOneOtherPerson + getOtherUsers(myUserName, userList, userData);
} else { } else {
listElement.innerHTML = Messages.editingWith + ' ' + (userList.length - 1) + ' ' + Messages.otherPeople + getOtherUsers(myUserName, userList, userData); innerHTML = Messages.editingWith + ' ' + (userList.length - 1) + ' ' + Messages.otherPeople + getOtherUsers(myUserName, userList, userData);
}
}
innerHTML += getViewers(numberOfViewUsers);
if (userData[myUserName] && userData[myUserName].name) {
innerHTML = '<span class="' + USERNAME_CLS + '">' + userData[myUserName].name + '</span> | ' + innerHTML;
} }
listElement.innerHTML = innerHTML;
}; };
var createLagElement = function ($container) { var createLagElement = function ($container) {
@ -187,13 +224,16 @@ define([
var changeNameID = config.changeNameID; var changeNameID = config.changeNameID;
var saveContentID = config.saveContentID || config.exportContentID; var saveContentID = config.saveContentID || config.exportContentID;
var loadContentID = config.loadContentID || config.importContentID; var loadContentID = config.loadContentID || config.importContentID;
// readOnly = 1 (readOnly enabled), 0 (disabled), -1 (old pad without readOnly mode)
var readOnly = (typeof config.readOnly !== "undefined") ? (config.readOnly ? 1 : 0) : -1;
var saveElement; var saveElement;
var loadElement; var loadElement;
// Check if the user is allowed to change his name // Check if the user is allowed to change his name
if(changeNameID) { if(changeNameID) {
// Create the button and update the element containing the user list // Create the button and update the element containing the user list
userListElement = createChangeName($container, userListElement, changeNameID); //userListElement = createChangeName($container, userListElement, changeNameID);
createChangeName(toolbar.find('.' + RIGHTSIDE_CLS), changeNameID);
} }
var connected = false; var connected = false;
@ -205,7 +245,7 @@ define([
if(newUserData) { // Someone has changed his name/color if(newUserData) { // Someone has changed his name/color
userData = newUserData; userData = newUserData;
} }
updateUserList(myUserName, userListElement, users, userData); updateUserList(myUserName, userListElement, users, userData, readOnly);
}; };
var ks = function () { var ks = function () {

@ -65,6 +65,10 @@ define([
var andThen = function (Ckeditor) { var andThen = function (Ckeditor) {
var secret = Cryptpad.getSecrets(); var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
if (!secret.keys) {
secret.keys = secret.key;
}
var fixThings = false; var fixThings = false;
@ -82,6 +86,11 @@ define([
editor.on('instanceReady', function (Ckeditor) { editor.on('instanceReady', function (Ckeditor) {
if (readOnly) {
$('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox > .cke_toolbar').hide();
}
/* add a class to the magicline plugin so we can pick it out more easily */ /* add a class to the magicline plugin so we can pick it out more easily */
var ml = $('iframe')[0].contentWindow.CKEDITOR.instances.editor1.plugins.magicline var ml = $('iframe')[0].contentWindow.CKEDITOR.instances.editor1.plugins.magicline
@ -115,8 +124,9 @@ define([
} else { } else {
module.spinner.show(); module.spinner.show();
} }
if (!readOnly || !bool) {
inner.setAttribute('contenteditable', bool); inner.setAttribute('contenteditable', bool);
}
}; };
// don't let the user edit until the pad is ready // don't let the user edit until the pad is ready
@ -192,6 +202,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 // no use trying to recover the cursor if it doesn't exist
if (!cursor.exists()) { return; } if (!cursor.exists()) { return; }
@ -253,13 +269,13 @@ define([
}; };
var getLastName = function (cb) { var getLastName = function (cb) {
Cryptpad.getPadAttribute('username', function (err, userName) { Cryptpad.getAttribute('username', function (err, userName) {
cb(err, userName || ''); cb(err, userName || '');
}); });
}; };
var setName = module.setName = function (newName) { var setName = module.setName = function (newName) {
if (!(typeof(newName) === 'string' && newName.trim())) { return; } if (typeof(newName) !== 'string') { return; }
var myUserNameTemp = Cryptpad.fixHTML(newName.trim()); var myUserNameTemp = Cryptpad.fixHTML(newName.trim());
if(myUserNameTemp.length > 32) { if(myUserNameTemp.length > 32) {
myUserNameTemp = myUserNameTemp.substr(0, 32); myUserNameTemp = myUserNameTemp.substr(0, 32);
@ -271,7 +287,7 @@ define([
addToUserList(myData); addToUserList(myData);
editor.fire('change'); editor.fire('change');
Cryptpad.setPadAttribute('username', newName, function (err, data) { Cryptpad.setAttribute('username', newName, function (err, data) {
if (err) { if (err) {
console.error("Couldn't set username"); console.error("Couldn't set username");
} }
@ -296,7 +312,9 @@ define([
var applyHjson = function (shjson) { var applyHjson = function (shjson) {
var userDocStateDom = hjsonToDom(JSON.parse(shjson)); var userDocStateDom = hjsonToDom(JSON.parse(shjson));
if (!readOnly) {
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
}
var patch = (DD).diff(inner, userDocStateDom); var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch); (DD).apply(inner, patch);
}; };
@ -322,14 +340,15 @@ define([
// the channel we will communicate over // the channel we will communicate over
channel: secret.channel, channel: secret.channel,
// our encryption key // our public key
cryptKey: secret.key, validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
// method which allows us to get the id of the user // method which allows us to get the id of the user
setMyID: setMyID, setMyID: setMyID,
// Pass in encrypt and decrypt methods // Pass in encrypt and decrypt methods
crypto: Crypto.createEncryptor(secret.key), crypto: Crypto.createEncryptor(secret.keys),
// really basic operational transform // really basic operational transform
transformFunction : JsonOT.validate, transformFunction : JsonOT.validate,
@ -407,6 +426,7 @@ define([
// build a dom from HJSON, diff, and patch the editor // build a dom from HJSON, diff, and patch the editor
applyHjson(shjson); applyHjson(shjson);
if (!readOnly) {
var shjson2 = stringifyDOM(inner); var shjson2 = stringifyDOM(inner);
if (shjson2 !== shjson) { if (shjson2 !== shjson) {
console.error("shjson2 !== shjson"); console.error("shjson2 !== shjson");
@ -435,6 +455,7 @@ define([
} }
} }
} }
}
notify(); notify();
}; };
@ -491,12 +512,21 @@ define([
var config = { var config = {
userData: userList, userData: userList,
changeNameID: Toolbar.constants.changeName, 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); 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); 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 */ /* add an export button */
var $export = $('<button>', { var $export = $('<button>', {
title: Messages.exportButtonTitle, title: Messages.exportButtonTitle,
@ -504,7 +534,9 @@ define([
.text(Messages.exportButton) .text(Messages.exportButton)
.addClass('rightside-button') .addClass('rightside-button')
.click(exportFile); .click(exportFile);
$rightside.append($export);
if (!readOnly) {
/* add an import button */ /* add an import button */
var $import = $('<button>', { var $import = $('<button>', {
title: Messages.importButtonTitle title: Messages.importButtonTitle
@ -516,7 +548,8 @@ define([
applyHjson(shjson); applyHjson(shjson);
realtimeOptions.onLocal(); realtimeOptions.onLocal();
})); }));
$rightside.append($export).append($import); $rightside.append($import);
}
/* add a rename button */ /* add a rename button */
var $rename = $('<button>', { var $rename = $('<button>', {
@ -569,8 +602,25 @@ define([
}); });
$rightside.append($forgetPad); $rightside.append($forgetPad);
if (!readOnly && viewHash) {
/* add a 'links' button */
var $links = $('<button>', {
title: Messages.getViewButtonTitle
})
.text(Messages.getViewButton)
.addClass('rightside-button')
.click(function () {
var baseUrl = window.location.origin + window.location.pathname + '#';
var content = '<b>' + Messages.readonlyUrl + '</b><br><a>' + baseUrl + viewHash + '</a><br>';
Cryptpad.alert(content);
});
$rightside.append($links);
}
// set the hash // set the hash
window.location.hash = Cryptpad.getHashFromKeys(info.channel, secret.key); if (!readOnly) {
window.location.hash = editHash;
}
Cryptpad.getPadTitle(function (err, title) { Cryptpad.getPadTitle(function (err, title) {
if (err) { if (err) {
@ -588,18 +638,6 @@ define([
}); });
}; };
var onLocal = realtimeOptions.onLocal = function () {
if (initializing) { return; }
// stringify the json and send it into chainpad
var shjson = stringifyDOM(inner);
module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
// this should only ever get called once, when the chain syncs // this should only ever get called once, when the chain syncs
var onReady = realtimeOptions.onReady = function (info) { var onReady = realtimeOptions.onReady = function (info) {
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
@ -612,6 +650,9 @@ define([
var shjson = info.realtime.getUserDoc(); var shjson = info.realtime.getUserDoc();
applyHjson(shjson); applyHjson(shjson);
// Update the user list (metadata) from the hyperjson
updateMetadata(shjson);
if (Visible.isSupported()) { if (Visible.isSupported()) {
Visible.onChange(function (yes) { Visible.onChange(function (yes) {
if (yes) { unnotify(); } if (yes) { unnotify(); }
@ -619,13 +660,20 @@ define([
} }
getLastName(function (err, lastName) { getLastName(function (err, lastName) {
if (typeof(lastName) === 'string' && lastName.length) {
setName(lastName);
}
console.log("Unlocking editor"); console.log("Unlocking editor");
setEditable(true); setEditable(true);
initializing = false; initializing = false;
onLocal(); // 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);
}
realtimeOptions.onLocal();
}); });
}; };
@ -650,6 +698,18 @@ define([
} }
}; };
var onLocal = realtimeOptions.onLocal = function () {
if (initializing) { return; }
if (readOnly) { return; }
// stringify the json and send it into chainpad
var shjson = stringifyDOM(inner);
module.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
var rti = module.realtimeInput = realtimeInput.start(realtimeOptions); var rti = module.realtimeInput = realtimeInput.start(realtimeOptions);

@ -40,7 +40,7 @@
<div id="toolbar" class="buttons"> <div id="toolbar" class="buttons">
<sub><a href="/"></a></sub> <sub><a href="/"></a></sub>
</div> </div>
<h1>CryptPoll</h1> <h1 id="mainTitle">CryptPoll</h1>
<h2 data-localization="poll_subtitle"></h2> <h2 data-localization="poll_subtitle"></h2>
<p data-localization="poll_p_save"></p> <p data-localization="poll_p_save"></p>
@ -53,7 +53,7 @@
<input type="text" id="title" placeholder="title"><br /> <input type="text" id="title" placeholder="title"><br />
<textarea id="description" placeholder="description"></textarea> <textarea id="description" placeholder="description"></textarea>
<p data-localization="poll_p_howtouse"></p> <p id="howToUse" data-localization="poll_p_howtouse"></p>
<!-- Table markup--> <!-- Table markup-->
<table id="table"> <table id="table">

@ -39,6 +39,14 @@ define([
*/ */
var secret = Cryptpad.getSecrets(); var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
if (!secret.keys) {
secret.keys = secret.key;
}
if (readOnly) {
$('#mainTitle').html($('#mainTitle').html() + ' - ' + Messages.readonly);
$('#adduser, #addoption, #howToUse').remove();
}
var module = window.APP = { var module = window.APP = {
Cryptpad: Cryptpad, Cryptpad: Cryptpad,
@ -139,6 +147,7 @@ define([
var table = module.table = Table($('#table'), xy); var table = module.table = Table($('#table'), xy);
var setEditable = function (bool) { var setEditable = function (bool) {
if (readOnly && bool) { return; }
module.isEditable = bool; module.isEditable = bool;
items.forEach(function ($item) { items.forEach(function ($item) {
@ -163,6 +172,7 @@ define([
}; };
var removeRow = function (proxy, uid) { var removeRow = function (proxy, uid) {
if (readOnly) { return; }
// remove proxy.table.rows[uid] // remove proxy.table.rows[uid]
proxy.table.rows[uid] = undefined; proxy.table.rows[uid] = undefined;
@ -186,6 +196,7 @@ define([
}; };
var removeColumn = function (proxy, uid) { var removeColumn = function (proxy, uid) {
if (readOnly) { return; }
// remove proxy.table.cols[uid] // remove proxy.table.cols[uid]
proxy.table.cols[uid] = undefined; proxy.table.cols[uid] = undefined;
delete proxy.table.rows[uid]; delete proxy.table.rows[uid];
@ -212,6 +223,7 @@ define([
}; };
var makeUserEditable = module.makeUserEditable = function (id, bool) { var makeUserEditable = module.makeUserEditable = function (id, bool) {
if (readOnly) { return; }
var $name = $('input[type="text"][id="' + id + '"]').attr('disabled', !bool); var $name = $('input[type="text"][id="' + id + '"]').attr('disabled', !bool);
var $edit = $name.parent().find('.edit'); var $edit = $name.parent().find('.edit');
@ -289,6 +301,11 @@ define([
}); });
}); });
if (readOnly) {
$edit = '';
$remove = '';
}
var $wrapper = $('<div>', { var $wrapper = $('<div>', {
'class': 'text-cell', 'class': 'text-cell',
}) })
@ -313,6 +330,7 @@ define([
}; };
var makeOptionEditable = function (id, bool) { var makeOptionEditable = function (id, bool) {
if (readOnly) { return; }
if (bool) { if (bool) {
module.rt.proxy.table.rowsOrder.forEach(function (rowuid) { module.rt.proxy.table.rowsOrder.forEach(function (rowuid) {
$('#' + rowuid) $('#' + rowuid)
@ -363,6 +381,11 @@ define([
}); });
}); });
if (readOnly) {
$edit = '';
$remove = '';
}
var $wrapper = $('<div>', { var $wrapper = $('<div>', {
'class': 'text-cell', 'class': 'text-cell',
}) })
@ -738,6 +761,7 @@ define([
}); });
})); }));
if (!readOnly) {
$toolbar.append(Button({ $toolbar.append(Button({
id: 'wizard', id: 'wizard',
'class': 'wizard button action', 'class': 'wizard button action',
@ -748,6 +772,22 @@ define([
Cryptpad.log(Messages.wizardLog); Cryptpad.log(Messages.wizardLog);
Wizard.hasBeenDisplayed = true; Wizard.hasBeenDisplayed = true;
})); }));
}
if (!readOnly && module.viewHash) {
/* add a 'links' button */
var $links = $('<button>', {
title: Messages.getViewButtonTitle
})
.text(Messages.getViewButton)
.addClass('button action')
.click(function () {
var baseUrl = window.location.origin + window.location.pathname + '#';
var content = '<b>' + Messages.readonlyUrl + '</b><br><a>' + baseUrl + module.viewHash + '</a><br>';
Cryptpad.alert(content);
});
$toolbar.append($links);
}
/* Import/Export buttons */ /* Import/Export buttons */
/* /*
@ -807,6 +847,7 @@ define([
} }
Cryptpad.getPadAttribute('column', function (err, column) { Cryptpad.getPadAttribute('column', function (err, column) {
if (readOnly) { return; }
if (err) { if (err) {
console.log("unable to retrieve column"); console.log("unable to retrieve column");
return; return;
@ -854,7 +895,10 @@ define([
websocketURL: Config.websocketURL, websocketURL: Config.websocketURL,
channel: secret.channel, channel: secret.channel,
data: {}, data: {},
crypto: Crypto.createEncryptor(secret.key), // our public key
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys),
}; };
// don't initialize until the store is ready. // don't initialize until the store is ready.
@ -863,7 +907,17 @@ define([
var rt = window.rt = module.rt = Listmap.create(config); var rt = window.rt = module.rt = Listmap.create(config);
rt.proxy.on('create', function (info) { rt.proxy.on('create', function (info) {
var realtime = module.realtime = info.realtime; var realtime = module.realtime = info.realtime;
window.location.hash = Cryptpad.getHashFromKeys(info.channel, secret.key);
var editHash;
var viewHash = module.viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
}
// set the hash
if (!readOnly) {
window.location.hash = editHash;
}
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
realtime: realtime, realtime: realtime,
logging: true, logging: true,

@ -32,6 +32,11 @@ define([
Cryptpad.styleAlerts(); Cryptpad.styleAlerts();
var secret = Cryptpad.getSecrets(); var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
Slide.readOnly = readOnly;
if (!secret.keys) {
secret.keys = secret.key;
}
var APP = window.APP = { var APP = window.APP = {
TextPatcher: TextPatcher, TextPatcher: TextPatcher,
@ -72,14 +77,30 @@ define([
var $content = $('#content'); var $content = $('#content');
Slide.setModal($modal, $content); Slide.setModal($modal, $content);
var enterPresentationMode = function (shouldLog) {
Slide.show(true, $textarea.val());
if (shouldLog) {
Cryptpad.log(Messages.presentSuccess);
}
};
if (readOnly) {
enterPresentationMode(false);
}
var config = APP.config = { var config = APP.config = {
initialState: '{}', initialState: '{}',
websocketURL: Config.websocketURL, websocketURL: Config.websocketURL,
channel: secret.channel, channel: secret.channel,
crypto: Crypto.createEncryptor(secret.key), crypto: Crypto.createEncryptor(secret.keys),
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
}; };
var setEditable = function (bool) { $textarea.attr('disabled', !bool); }; var setEditable = function (bool) {
if (readOnly && bool) { return; }
$textarea.attr('disabled', !bool);
};
var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); }; var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); };
setEditable(false); setEditable(false);
@ -101,6 +122,7 @@ define([
var onLocal = config.onLocal = function () { var onLocal = config.onLocal = function () {
if (initializing) { return; } if (initializing) { return; }
if (readOnly) { return; }
var textContent = canonicalize($textarea.val()); var textContent = canonicalize($textarea.val());
@ -119,7 +141,13 @@ define([
}; };
var onInit = config.onInit = function (info) { var onInit = config.onInit = function (info) {
window.location.hash = Cryptpad.getHashFromKeys(info.channel, secret.key); var editHash;
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
window.location.hash = editHash;
}
$(window).on('hashchange', function() { $(window).on('hashchange', function() {
window.location.reload(); window.location.reload();
}); });
@ -147,8 +175,7 @@ define([
}) })
.text(Messages.presentButton) .text(Messages.presentButton)
.click(function () { .click(function () {
Slide.show(true, $textarea.val()); enterPresentationMode(true);
Cryptpad.log(Messages.presentSuccess);
}); });
var $forget = Button({ var $forget = Button({
@ -247,13 +274,35 @@ define([
Cryptpad.warn(Messages.shareFailed); Cryptpad.warn(Messages.shareFailed);
}); });
/* add a 'links' button */
var $links = Button({
title: Messages.getViewButtonTitle,
'class': 'links button action',
})
.text(Messages.getViewButton)
.click(function () {
var baseUrl = window.location.origin + window.location.pathname + '#';
var content = '<b>' + Messages.readonlyUrl + '</b><br><a>' + baseUrl + viewHash + '</a><br>';
Cryptpad.alert(content);
});
if (readOnly) {
$links = '';
$import = '';
$present = '';
}
if (!viewHash) {
$links = '';
}
$bar $bar
.append($present) .append($present)
.append($forget) .append($forget)
.append($rename) .append($rename)
.append($import) .append($import)
.append($export) .append($export)
.append($share); .append($share)
.append($links);
}; };
var onRemote = config.onRemote = function (info) { var onRemote = config.onRemote = function (info) {
if (initializing) { return; } if (initializing) { return; }
@ -273,7 +322,7 @@ define([
elem.selectionStart = selects[0]; elem.selectionStart = selects[0];
elem.selectionEnd = selects[1]; elem.selectionEnd = selects[1];
Slide.update(content); Slide.update(userDoc);
notify(); notify();
}; };

@ -160,7 +160,9 @@ define([
Slide.right(); Slide.right();
break; break;
case 27: // esc case 27: // esc
if (!Slide.readOnly) {
show(false); show(false);
}
break; break;
default: default:
console.log(e.which); console.log(e.which);

Loading…
Cancel
Save