diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index 98d79f72f..02a8f8b19 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -14,7 +14,12 @@ let historyKeeperKeys = {}; const now = function () { return (new Date()).getTime(); }; +const socketSendable = function (socket) { + return socket && socket.readyState === 1; +}; + const sendMsg = function (ctx, user, msg) { + if (!socketSendable(user.socket)) { return; } try { if (ctx.config.logToStdout) { console.log('<' + JSON.stringify(msg)); } user.socket.send(JSON.stringify(msg)); @@ -272,7 +277,7 @@ let run = module.exports.run = function (storage, socketServer, config) { }); }, 5000); socketServer.on('connection', function(socket) { - if(socket.upgradeReq.url !== '/cryptpad_websocket') { return; } + if(socket.upgradeReq.url !== (config.websocketPath || '/cryptpad_websocket')) { return; } let conn = socket.upgradeReq.connection; let user = { addr: conn.remoteAddress + '|' + conn.remotePort, diff --git a/config.js.dist b/config.js.dist index e88b06b0a..c75748508 100644 --- a/config.js.dist +++ b/config.js.dist @@ -9,8 +9,22 @@ module.exports = { // the port on which your httpd will listen httpPort: 3000, - // the port used for websockets - websocketPort: 3000, + + /* your server's websocket url is configurable + (default: '/cryptpad_websocket') + + websocketPath can be relative, of the form '/path/to/websocket' + or absolute, specifying a particular URL + + 'wss://cryptpad.fr:3000/cryptpad_websocket' + */ + websocketPath: '/cryptpad_websocket', + + /* it is assumed that your websocket will bind to the same port as http + you can override this behaviour by supplying a number via websocketPort + */ + //websocketPort: 3000, + /* Cryptpad can log activity to stdout * This may be useful for debugging diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index 058d2c1d9..d7e9141c1 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -4,7 +4,7 @@ define(function() { /* Select the buttons displayed on the main page to create new collaborative sessions * Existing types : pad, code, poll, slide */ - config.availablePadTypes = ['pad', 'code', 'poll']; + config.availablePadTypes = ['pad', 'code', 'poll', 'slide']; return config; }); diff --git a/customize.dist/index.html b/customize.dist/index.html index acaed958b..32e09f4f2 100644 --- a/customize.dist/index.html +++ b/customize.dist/index.html @@ -57,8 +57,12 @@
@@ -87,8 +91,6 @@ - -
diff --git a/customize.dist/main.css b/customize.dist/main.css index 72190a3a1..0c063e6e8 100644 --- a/customize.dist/main.css +++ b/customize.dist/main.css @@ -154,6 +154,9 @@ tr { margin-bottom: 12px; white-space: nowrap; } +.alertify button { + margin: 3px 0px; +} /* Tables */ table { border-collapse: collapse; diff --git a/customize.dist/main.js b/customize.dist/main.js index 306db9786..2622eef1b 100644 --- a/customize.dist/main.js +++ b/customize.dist/main.js @@ -12,9 +12,6 @@ define([ Cryptpad: Cryptpad, }; - DecorateToolbar.main($('#bottom-bar')); - Cryptpad.styleAlerts(); - var padTypes = { '/pad/': Messages.type.pad, '/code/': Messages.type.code, @@ -22,9 +19,9 @@ define([ '/slide/': Messages.type.slide, }; - var $table = $('table.scroll'); - var $tbody = $table.find('tbody'); - var $tryit = $('#tryit'); + var $table; + var $tbody; + var $tryit; var now = new Date(); var hasRecent = false; @@ -138,11 +135,18 @@ define([ }); }; - displayCreateButtons(); Cryptpad.ready(function () { console.log("ready"); - refreshTable(); + $table = $('table.scroll'); + $tbody = $table.find('tbody'); + $tryit = $('#tryit'); + + DecorateToolbar.main($('#bottom-bar')); + Cryptpad.styleAlerts(); + + displayCreateButtons(); + refreshTable(); if (Cryptpad.store && Cryptpad.store.change) { Cryptpad.store.change(function (data) { if (data.key === 'CryptPad_RECENTPADS') { diff --git a/customize.dist/privacy.html b/customize.dist/privacy.html index b7d4b15cc..b152269da 100644 --- a/customize.dist/privacy.html +++ b/customize.dist/privacy.html @@ -74,8 +74,6 @@ - -
diff --git a/customize.dist/src/cryptpad.less b/customize.dist/src/cryptpad.less index 16448646f..c41af4de9 100644 --- a/customize.dist/src/cryptpad.less +++ b/customize.dist/src/cryptpad.less @@ -187,6 +187,9 @@ p, pre, td, a, table, tr { margin-bottom: 2 * 6px; white-space: nowrap; } +.alertify button { + margin: 3px 0px; +} /* Tables */ table { diff --git a/customize.dist/src/fragments/index.html b/customize.dist/src/fragments/index.html index 6ca24310b..d37dca8ba 100644 --- a/customize.dist/src/fragments/index.html +++ b/customize.dist/src/fragments/index.html @@ -17,8 +17,12 @@
diff --git a/customize.dist/src/template.html b/customize.dist/src/template.html index b165c377e..e696d42a5 100644 --- a/customize.dist/src/template.html +++ b/customize.dist/src/template.html @@ -20,8 +20,6 @@
{{main}}
- -
diff --git a/customize.dist/terms.html b/customize.dist/terms.html index 8db25b6cb..b2c1d4916 100644 --- a/customize.dist/terms.html +++ b/customize.dist/terms.html @@ -51,8 +51,6 @@ - -
diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index c6a9be3fe..8ef04bea5 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -34,7 +34,7 @@ define(function () { out.anonymous = "Vous êtes actuellement anonyme"; out.greenLight = "Tout fonctionne bien"; - out.orangeLight = "Votre connexion est lente, ce qui réduit la qualité de l'éditeur" + out.orangeLight = "Votre connexion est lente, ce qui réduit la qualité de l'éditeur"; out.redLight = "Vous êtes déconnectés de la session"; out.importButton = 'IMPORTER'; @@ -74,7 +74,10 @@ define(function () { out.getViewButton = 'LECTURE SEULE'; out.getViewButtonTitle = "Obtenir l'adresse d'accès à ce document en lecture seule"; - out.readonlyUrl = 'URL de lecture seule'; + out.readonlyUrl = 'Document en lecture seule'; + out.copyReadOnly = "Copier l'URL dans le presse-papiers"; + out.openReadOnly = "Ouvrir dans un nouvel onglet"; + out.disconnectAlert = 'Perte de la connexion au réseau !'; @@ -142,7 +145,6 @@ define(function () { out.main_about = 'À propos'; out.main_about_p1 = 'Vous pouvez en apprendre davantage sur notre politique de confidentialité et nos conditions d\'utilisation.'; out.main_about_p2 = 'Si vous avez des questions ou commentaires, vous pouvez nous tweeter, ouvrir une issue sur Github, venir dire bonjour sur IRC (irc.freenode.net), ou nous envoyer un email.'; - out.main_oops = 'OUPS Afin de pouvoir réaliser le cryptage depuis votre navigateur, Javascript est vraiment requis.'; out.table_type = 'Type'; out.table_link = 'Lien'; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index df1bd18de..09c044481 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -57,7 +57,7 @@ define(function () { out.renameConflict = 'Another pad already has that title'; out.forgetButton = 'FORGET'; - out.forgetButtonTitle = 'remove this document from your home page listings'; + out.forgetButtonTitle = 'Remove this document from your home page listings'; out.forgetPrompt = 'Clicking OK will remove the URL for this pad from localStorage, are you sure?'; out.shareButton = 'SHARE'; @@ -75,7 +75,9 @@ define(function () { out.getViewButton = 'READ-ONLY URL'; out.getViewButtonTitle = 'Get the read-only URL for this document'; - out.readonlyUrl = 'Read only URL'; + out.readonlyUrl = 'Read only document'; + out.copyReadOnly = "Copy URL to clipboard"; + out.openReadOnly = "Open in a new tab"; out.disconnectAlert = 'Network connection lost!'; @@ -142,8 +144,8 @@ define(function () { out.main_howitworks_p1 = 'CryptPad uses a variant of the Operational transformation algorithm which is able to find distributed consensus using a Nakamoto Blockchain, a construct popularized by Bitcoin. This way the algorithm can avoid the need for a central server to resolve Operational Transform Edit Conflicts and without the need for resolving conflicts, the server can be kept unaware of the content which is being edited on the pad.'; out.main_about = 'About'; out.main_about_p1 = 'You can read more about our privacy policy and terms of service.'; + out.main_about_p2 = 'If you have any questions or comments, you can tweet us, open an issue on github, come say hi on irc (irc.freenode.net), or send us an email.'; - out.main_oops = 'OOPS In order to do encryption in your browser, Javascript is really really required.'; out.table_type = 'Type'; out.table_link = 'Link'; diff --git a/server.js b/server.js index dc0077a35..26e9d98ec 100644 --- a/server.js +++ b/server.js @@ -83,10 +83,9 @@ app.get('/api/config', function(req, res){ var host = req.headers.host.replace(/\:[0-9]+/, ''); res.setHeader('Content-Type', 'text/javascript'); res.send('define(' + JSON.stringify({ + websocketPath: config.websocketPath, websocketURL:'ws' + ((httpsOpts) ? 's' : '') + '://' + host + ':' + websocketPort + '/cryptpad_websocket', - webrtcURL:'ws' + ((httpsOpts) ? 's' : '') + '://' + host + ':' + - websocketPort + '/cryptpad_webrtc', }) + ');'); }); diff --git a/www/code/main.js b/www/code/main.js index 71f3bfd14..621095dcc 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -1,6 +1,5 @@ require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ - '/api/config?cb=' + Math.random().toString(16).substring(2), '/customize/messages.js?app=code', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-netflux/chainpad-netflux.js', @@ -16,7 +15,7 @@ define([ '/bower_components/file-saver/FileSaver.min.js', '/bower_components/jquery/dist/jquery.min.js', '/customize/pad.js' -], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Modes, Themes, Visible, Notify) { +], function (Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Modes, Themes, Visible, Notify) { var $ = window.jQuery; var saveAs = window.saveAs; @@ -134,7 +133,7 @@ define([ var config = { //initialState: Messages.codeInitialState, initialState: '{}', - websocketURL: Config.websocketURL, + websocketURL: Cryptpad.getWebsocketURL(), channel: secret.channel, // our public key validateKey: secret.keys.validateKey || undefined, @@ -146,6 +145,11 @@ define([ var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); }; + var isDefaultTitle = function () { + var parsed = Cryptpad.parsePadUrl(window.location.href); + return Cryptpad.isDefaultName(parsed, document.title); + }; + var initializing = true; var onLocal = config.onLocal = function () { @@ -158,9 +162,11 @@ define([ // append the userlist to the hyperjson structure obj.metadata = { - users: userList, - title: document.title + users: userList }; + if (!isDefaultTitle()) { + obj.metadata.title = document.title; + } // set mode too... obj.highlightMode = module.highlightMode; @@ -243,7 +249,7 @@ define([ var parsed = Cryptpad.parsePadUrl(window.location.href); var name = Cryptpad.getDefaultName(parsed, []); - if (document.title.slice(0, name.length) === name) { + if (Cryptpad.isDefaultName(parsed, document.title)) { return getHeadingText() || document.title; } else { return document.title || getHeadingText() || name; @@ -265,6 +271,32 @@ define([ saveAs(blob, filename); }); }; + var importText = function (content, file) { + var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); + var mode; + var mime = CodeMirror.findModeByMIME(file.type); + + if (!mime) { + var ext = /.+\.([^.]+)$/.exec(file.name); + if (ext[1]) { + mode = CodeMirror.findModeByExtension(ext[1]); + } + } else { + mode = mime && mime.mode || null; + } + + if (mode && Modes.list.some(function (o) { return o.mode === mode; })) { + setMode(mode); + $bar.find('#language-mode').val(mode); + } else { + console.log("Couldn't find a suitable highlighting mode: %s", mode); + setMode('text'); + $bar.find('#language-mode').val('text'); + } + + editor.setValue(content); + onLocal(); + }; var onInit = config.onInit = function (info) { var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); @@ -285,113 +317,45 @@ define([ editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); } + /* add a "change username" button */ getLastName(function (err, lastName) { - var $username = Cryptpad.createButton('username', true) - .click(function() { - Cryptpad.prompt(Messages.changeNamePrompt, lastName, function (newName) { - setName(newName); - }); - }); + var usernameCb = function (newName) { + setName (newName); + }; + var $username = Cryptpad.createButton('username', true, {lastName: lastName}, usernameCb); $rightside.append($username); }); /* add an export button */ - var $export = Cryptpad.createButton('export', true).click(exportText); + var $export = Cryptpad.createButton('export', true, {}, exportText); $rightside.append($export); if (!readOnly) { /* add an import button */ - var $import = Cryptpad.createButton('import', true) - .click(Cryptpad.importContent('text/plain', function (content, file) { - var mode; - var mime = CodeMirror.findModeByMIME(file.type); - - if (!mime) { - var ext = /.+\.([^.]+)$/.exec(file.name); - if (ext[1]) { - mode = CodeMirror.findModeByExtension(ext[1]); - } - } else { - mode = mime && mime.mode || null; - } - - if (mode && Modes.list.some(function (o) { return o.mode === mode; })) { - setMode(mode); - $bar.find('#language-mode').val(mode); - } else { - console.log("Couldn't find a suitable highlighting mode: %s", mode); - setMode('text'); - $bar.find('#language-mode').val('text'); - } - - editor.setValue(content); - onLocal(); - })); + var $import = Cryptpad.createButton('import', true, {}, importText); $rightside.append($import); - } - /* add a rename button */ - var $setTitle = Cryptpad.createButton('rename', true) - .click(function () { - var suggestion = suggestName(); - - Cryptpad.prompt(Messages.renamePrompt, - suggestion, function (title, ev) { - if (title === null) { return; } - - Cryptpad.causesNamingConflict(title, function (err, conflicts) { - if (err) { - console.log("Unable to determine if name caused a conflict"); - console.error(err); - return; - } - - if (conflicts) { - Cryptpad.alert(Messages.renameConflict); - return; - } - - Cryptpad.setPadTitle(title, function (err, data) { - if (err) { - console.log("unable to set pad title"); - console.log(err); - return; - } - document.title = title; - onLocal(); - }); - }); - }); - }); - $rightside.append($setTitle); + /* add a rename button */ + var renameCb = function (err, title) { + if (err) { return; } + document.title = title; + onLocal(); + }; + var $setTitle = Cryptpad.createButton('rename', true, {suggestName: suggestName}, renameCb); + $rightside.append($setTitle); + } /* add a forget button */ - var $forgetPad = Cryptpad.createButton('forget', true) - .click(function () { - var href = window.location.href; - Cryptpad.confirm(Messages.forgetPrompt, function (yes) { - if (!yes) { return; } - Cryptpad.forgetPad(href, function (err, data) { - if (err) { - console.log("unable to forget pad"); - console.error(err); - return; - } - var parsed = Cryptpad.parsePadUrl(href); - document.title = Cryptpad.getDefaultName(parsed, []); - }); - }); - }); + var forgetCb = function (err, title) { + if (err) { return; } + document.title = title; + }; + var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); $rightside.append($forgetPad); if (!readOnly && viewHash) { /* add a 'links' button */ - var $links = Cryptpad.createButton('readonly', true) - .click(function () { - var baseUrl = window.location.origin + window.location.pathname + '#'; - var content = '' + Messages.readonlyUrl + '
' + baseUrl + viewHash + '
'; - Cryptpad.alert(content); - }); + var $links = Cryptpad.createButton('readonly', true, {viewHash: viewHash}); $rightside.append($links); } @@ -470,7 +434,7 @@ define([ return; } document.title = title || info.channel.slice(0, 8); - Cryptpad.rememberPad(title, function (err, data) { + Cryptpad.setPadTitle(title, function (err, data) { if (err) { console.log("Unable to set pad title"); console.error(err); @@ -657,11 +621,13 @@ define([ var hjson2 = { content: localDoc, metadata: { - users: userList, - title: document.title + users: userList }, highlightMode: highlightMode, }; + if (!isDefaultTitle()) { + hjson2.metadata.title = document.title; + } var shjson2 = stringify(hjson2); if (shjson2 !== shjson) { console.error("shjson2 !== shjson"); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 68fa9be7a..5a4220f3c 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1,14 +1,16 @@ define([ + '/api/config?cb=' + Math.random().toString(16).slice(2), '/customize/messages.js', '/customize/store.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/alertifyjs/dist/js/alertify.js', '/bower_components/spin.js/spin.min.js', + '/common/clipboard.js', '/customize/user.js', '/bower_components/jquery/dist/jquery.min.js', -], function (Messages, Store, Crypto, Alertify, Spinner, User) { +], function (Config, Messages, Store, Crypto, Alertify, Spinner, Clipboard, User) { /* This file exposes functionality which is specific to Cryptpad, but not to any particular pad type. This includes functions for committing metadata about pads to your local storage for future use and improved usability. @@ -36,6 +38,18 @@ define([ throw new Error("Store is not ready!"); }; + var getWebsocketURL = common.getWebsocketURL = function () { + if (!Config.websocketPath) { return Config.websocketURL; } + var path = Config.websocketPath; + if (/^ws{1,2}:\/\//.test(path)) { return path; } + + var protocol = window.location.protocol.replace(/http/, 'ws'); + var host = window.location.host; + var url = protocol + '//' + host + path; + + return url; + }; + /* * cb(err, proxy); */ @@ -137,6 +151,26 @@ define([ }; + var parseHash = common.parseHash = function (hash) { + var parsed = {}; + if (hash.slice(0,1) !== '/' && hash.length >= 56) { + // Old hash + parsed.channel = hash.slice(0, 32); + parsed.key = hash.slice(32); + parsed.version = 0; + return parsed; + } + var hashArr = hash.split('/'); + if (hashArr[1] && hashArr[1] === '1') { + parsed.version = 1; + parsed.mode = hashArr[2]; + parsed.channel = hashArr[3]; + parsed.key = hashArr[4]; + parsed.present = hashArr[5] && hashArr[5] === 'present'; + return parsed; + } + return; + }; var getEditHashFromKeys = common.getEditHashFromKeys = function (chanKey, keys) { if (typeof keys === 'string') { return chanKey + keys; @@ -295,6 +329,10 @@ define([ while (!isNameAvailable(name + ' - ' + untitledIndex, parsed, recentPads)) { untitledIndex++; } return name + ' - ' + untitledIndex; }; + var isDefaultName = common.isDefaultName = function (parsed, title) { + var name = getDefaultName(parsed, []); + return title.slice(0, name.length) === name; + }; var makePad = function (href, title) { var now = ''+new Date(); @@ -397,51 +435,10 @@ define([ }, legacy); }; - // STORAGE - var rememberPad = common.rememberPad = window.rememberPad = function (title, cb) { - // bail out early - if (!/#/.test(window.location.hash)) { return; } - - getRecentPads(function (err, pads) { - if (err) { - cb(err); - return; - } - - var now = ''+new Date(); - var href = window.location.href; - - var parsed = parsePadUrl(window.location.href); - var isUpdate = false; - - var out = pads.map(function (pad) { - var p = parsePadUrl(pad.href); - if (p.hash === parsed.hash && p.type === parsed.type) { - isUpdate = true; - // bump the atime - pad.atime = now; - - pad.title = title; - pad.href = href; - } - return pad; - }); - - if (!isUpdate) { - // href, ctime, atime, title - out.push(makePad(href, title)); - } - setRecentPads(out, function (err, data) { - cb(err, data); - }); - }); - }; - // STORAGE var setPadTitle = common.setPadTitle = function (name, cb) { var href = window.location.href; var parsed = parsePadUrl(href); - getRecentPads(function (err, recent) { if (err) { cb(err); @@ -449,10 +446,29 @@ define([ } var contains; - var renamed = recent.map(function (pad) { var p = parsePadUrl(pad.href); - if (p.hash === parsed.hash && p.type === parsed.type) { + + if (p.type !== parsed.type) { return pad; } + + var shouldUpdate = p.hash === parsed.hash; + + // Version 1 : we have up to 4 differents hash for 1 pad, keep the strongest : + // Edit > Edit (present) > View > View (present) + var pHash = parseHash(p.hash); + var parsedHash = parseHash(parsed.hash); + if (!shouldUpdate && pHash.version === 1 && parsedHash.version === 1 && pHash.channel === parsedHash.channel) { + if (pHash.mode === 'view' && parsedHash.mode === 'edit') { shouldUpdate = true; } + else if (pHash.mode === parsedHash.mode && pHash.present) { shouldUpdate = true; } + else { + // Editing a "weaker" version of a stored hash : update the date and do not push the current hash + pad.atime = new Date().toISOString(); + contains = true; + return pad; + } + } + + if (shouldUpdate) { contains = true; // update the atime pad.atime = new Date().toISOString(); @@ -541,7 +557,23 @@ define([ Store.ready(function (err, store) { common.store = env.store = store; - cb(); + $(function() { + // Race condition : if document.body is undefined when alertify.js is loaded, Alertify + // won't work. We have to reset it now to make sure it uses a correct "body" + Alertify.reset(); + if($('#pad-iframe').length) { + var $iframe = $('#pad-iframe'); + var iframe = $iframe[0]; + var iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + if (iframeDoc.readyState === 'complete') { + cb(); + return; + } + $iframe.load(cb); + return; + } + cb(); + }); return; /* authorize(function (err, proxy) { @@ -610,7 +642,7 @@ define([ /* * Buttons */ - var createButton = common.createButton = function (type, rightside) { + var createButton = common.createButton = function (type, rightside, data, callback) { var button; var size = "17px"; switch (type) { @@ -620,6 +652,9 @@ define([ 'class': "fa fa-download", style: 'font:'+size+' FontAwesome' }); + if (callback) { + button.click(callback); + } break; case 'import': button = $('