diff --git a/.gitignore b/.gitignore index 996e55b97..139fab33c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ www/scratch data npm-debug.log pins/ +blob/ +privileged.conf diff --git a/.jshintignore b/.jshintignore index ae60d4ba0..919395546 100644 --- a/.jshintignore +++ b/.jshintignore @@ -9,4 +9,7 @@ server.js NetFluxWebsocketSrv.js NetFluxWebsocketServer.js WebRTCSrv.js +www/common/media-tag.js +www/scratch +www/common/toolbar.js diff --git a/.jshintrc b/.jshintrc index c55ec0518..4928c524d 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,7 +10,7 @@ "notypeof": true, "shadow": false, "undef": true, - "unused": false, + "unused": true, "futurehostile":true, "browser": true, "predef": [ diff --git a/config.example.js b/config.example.js index 692a91c59..76bba6eae 100644 --- a/config.example.js +++ b/config.example.js @@ -39,10 +39,10 @@ module.exports = { if you are deploying to production, you'll probably want to remove the ws://* directive, and change '*' to your domain */ - "connect-src 'self' ws://* wss://*", + "connect-src 'self' ws: wss:", // data: is used by codemirror - "img-src 'self' data:", + "img-src 'self' data: blob:", ].join('; '), // CKEditor requires significantly more lax content security policy in order to function. @@ -59,7 +59,7 @@ module.exports = { "child-src 'self' *", // see the comment above in the 'contentSecurity' section - "connect-src 'self' ws://* wss://*", + "connect-src 'self' ws: wss:", // (insecure remote) images are included by users of the wysiwyg who embed photos in their pads "img-src *", @@ -141,6 +141,23 @@ module.exports = { */ filePath: './datastore/', + /* CryptPad allows logged in users to request that particular documents be + * stored by the server indefinitely. This is called 'pinning'. + * Pin requests are stored in a pin-store. The location of this store is + * defined here. + */ + pinPath: './pins', + + /* CryptPad allows logged in users to upload encrypted files. Files/blobs + * are stored in a 'blob-store'. Set its location here. + */ + blobPath: './blob', + + /* CryptPad stores incomplete blobs in a 'staging' area until they are + * fully uploaded. Set its location here. + */ + blobStagingPath: './blobstage', + /* Cryptpad's file storage adaptor closes unused files after a configurale * number of milliseconds (default 30000 (30 seconds)) */ @@ -163,6 +180,31 @@ module.exports = { */ suppressRPCErrors: false, + + /* WARNING: EXPERIMENTAL + * + * CryptPad features experimental support for encrypted file upload. + * Our encryption format is still liable to change. As such, we do not + * guarantee that files uploaded now will be supported in the future + */ + + /* Setting this value to anything other than true will cause file upload + * attempts to be rejected outright. + */ + enableUploads: true, + + /* If you have enabled file upload, you have the option of restricting it + * to a list of users identified by their public keys. If this value is set + * to true, your server will query a file (cryptpad/privileged.conf) when + * users connect via RPC. Only users whose public keys can be found within + * the file will be allowed to upload. + * + * privileged.conf uses '#' for line comments, and splits keys by newline. + * This is a temporary measure until a better quota system is in place. + * registered users' public keys can be found on the settings page. + */ + restrictUploads: true, + /* it is recommended that you serve cryptpad over https * the filepaths below are used to configure your certificates */ diff --git a/customize.dist/about.html b/customize.dist/about.html index c751125a7..b0b719033 100644 --- a/customize.dist/about.html +++ b/customize.dist/about.html @@ -106,7 +106,7 @@
- + diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index a91267132..949c612ee 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -37,5 +37,20 @@ define(function() { config.enableHistory = true; + //config.enablePinLimit = true; + //config.pinLimit = 1000; + + /* user passwords are hashed with scrypt, and salted with their username. + this value will be appended to the username, causing the resulting hash + to differ from other CryptPad instances if customized. This makes it + such that anyone who wants to bruteforce common credentials must do so + again on each CryptPad instance that they wish to attack. + + WARNING: this should only be set when your CryptPad instance is first + created. Changing it at a later time will break logins for all existing + users. + */ + config.loginSalt = ''; + return config; }); diff --git a/customize.dist/contact.html b/customize.dist/contact.html index 619f98df4..aa81fac60 100644 --- a/customize.dist/contact.html +++ b/customize.dist/contact.html @@ -103,7 +103,7 @@
- + diff --git a/customize.dist/index.html b/customize.dist/index.html index 7aa29d8e9..d246d84d0 100644 --- a/customize.dist/index.html +++ b/customize.dist/index.html @@ -225,7 +225,7 @@
- + diff --git a/customize.dist/main.css b/customize.dist/main.css index 026649207..d54ec6a98 100644 --- a/customize.dist/main.css +++ b/customize.dist/main.css @@ -388,6 +388,11 @@ right: 0; text-align: center; } +@media screen and (max-height: 600px) { + .cp #loadingTip { + display: none; + } +} .cp #loadingTip span { background-color: #302B28; color: #fafafa; diff --git a/customize.dist/main.js b/customize.dist/main.js index a02433b05..088e1f51a 100644 --- a/customize.dist/main.js +++ b/customize.dist/main.js @@ -4,7 +4,7 @@ define([ '/common/cryptpad-common.js' ], function ($, Config, Cryptpad) { - var APP = window.APP = { + window.APP = { Cryptpad: Cryptpad, }; @@ -118,68 +118,70 @@ define([ $('button.login').click(); }); - $('button.login').click(function (e) { - Cryptpad.addLoadingScreen(Messages.login_hashing); - // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password + $('button.login').click(function () { + // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up window.setTimeout(function () { - loginReady(function () { - var uname = $uname.val(); - var passwd = $passwd.val(); - Login.loginOrRegister(uname, passwd, false, function (err, result) { - if (!err) { - var proxy = result.proxy; - - // successful validation and user already exists - // set user hash in localStorage and redirect to drive - if (proxy && !proxy.login_name) { - proxy.login_name = result.userName; - } - - proxy.edPrivate = result.edPrivate; - proxy.edPublic = result.edPublic; - - Cryptpad.whenRealtimeSyncs(result.realtime, function () { - Cryptpad.login(result.userHash, result.userName, function () { - document.location.href = '/drive/'; - }); - }); - return; - } - switch (err) { - case 'NO_SUCH_USER': - Cryptpad.removeLoadingScreen(function () { - Cryptpad.alert(Messages.login_noSuchUser); - }); - break; - case 'INVAL_USER': - Cryptpad.removeLoadingScreen(function () { - Cryptpad.alert(Messages.login_invalUser); + Cryptpad.addLoadingScreen(Messages.login_hashing); + // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password + window.setTimeout(function () { + loginReady(function () { + var uname = $uname.val(); + var passwd = $passwd.val(); + Login.loginOrRegister(uname, passwd, false, function (err, result) { + if (!err) { + var proxy = result.proxy; + + // successful validation and user already exists + // set user hash in localStorage and redirect to drive + if (proxy && !proxy.login_name) { + proxy.login_name = result.userName; + } + + proxy.edPrivate = result.edPrivate; + proxy.edPublic = result.edPublic; + + Cryptpad.whenRealtimeSyncs(result.realtime, function () { + Cryptpad.login(result.userHash, result.userName, function () { + document.location.href = '/drive/'; + }); }); - break; - case 'INVAL_PASS': - Cryptpad.removeLoadingScreen(function () { - Cryptpad.alert(Messages.login_invalPass); - }); - break; - default: // UNHANDLED ERROR - Cryptpad.errorLoadingScreen(Messages.login_unhandledError); - } + return; + } + switch (err) { + case 'NO_SUCH_USER': + Cryptpad.removeLoadingScreen(function () { + Cryptpad.alert(Messages.login_noSuchUser); + }); + break; + case 'INVAL_USER': + Cryptpad.removeLoadingScreen(function () { + Cryptpad.alert(Messages.login_invalUser); + }); + break; + case 'INVAL_PASS': + Cryptpad.removeLoadingScreen(function () { + Cryptpad.alert(Messages.login_invalPass); + }); + break; + default: // UNHANDLED ERROR + Cryptpad.errorLoadingScreen(Messages.login_unhandledError); + } + }); }); - }); - }, 0); + }, 0); + }, 100); }); /* End Log in UI */ var addButtonHandlers = function () { - $('button.register').click(function (e) { + $('button.register').click(function () { var username = $('#name').val(); var passwd = $('#password').val(); - var remember = $('#rememberme').is(':checked'); sessionStorage.login_user = username; sessionStorage.login_pass = passwd; document.location.href = '/register/'; }); - $('button.gotodrive').click(function (e) { + $('button.gotodrive').click(function () { document.location.href = '/drive/'; }); }; diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 18d329d6d..813978b52 100644 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -112,9 +112,7 @@ define(req, function($, Default, Language) { if (!selector.length) { return; } - var $button = $(selector).find('button .buttonTitle'); // Select the current language in the list - var option = $(selector).find('[data-value="' + language + '"]'); selector.setValue(language || 'English'); // Listen for language change @@ -137,12 +135,12 @@ define(req, function($, Default, Language) { var key = $el.data('localization-append'); $el.append(messages[key]); }; - var translateTitle = function (i, e) { + var translateTitle = function () { var $el = $(this); var key = $el.data('localization-title'); $el.attr('title', messages[key]); }; - var translatePlaceholder = function (i, e) { + var translatePlaceholder = function () { var $el = $(this); var key = $el.data('localization-placeholder'); $el.attr('placeholder', messages[key]); diff --git a/customize.dist/privacy.html b/customize.dist/privacy.html index 8d2e09f5f..00a266236 100644 --- a/customize.dist/privacy.html +++ b/customize.dist/privacy.html @@ -124,7 +124,7 @@
- + diff --git a/customize.dist/share/frame.js b/customize.dist/share/frame.js index a07ff05ce..2698372fd 100644 --- a/customize.dist/share/frame.js +++ b/customize.dist/share/frame.js @@ -10,7 +10,7 @@ // create an invisible iframe with a given source // append it to a parent element // execute a callback when it has loaded - var create = Frame.create = function (parent, src, onload, timeout) { + Frame.create = function (parent, src, onload, timeout) { var iframe = document.createElement('iframe'); timeout = timeout || 10000; @@ -34,7 +34,7 @@ /* given an iframe with an rpc script loaded, create a frame object with an asynchronous 'send' method */ - var open = Frame.open = function (e, A, timeout) { + Frame.open = function (e, A, timeout) { var win = e.contentWindow; var frame = {}; @@ -44,7 +44,7 @@ timeout = timeout || 5000; - var accepts = frame.accepts = function (o) { + frame.accepts = function (o) { return A.some(function (e) { switch (typeof(e)) { case 'string': return e === o; @@ -55,7 +55,7 @@ var changeHandlers = frame.changeHandlers = []; - var change = frame.change = function (f) { + frame.change = function (f) { if (typeof(f) !== 'function') { throw new Error('[Frame.change] expected callback'); } @@ -94,7 +94,7 @@ }; window.addEventListener('message', _listener); - var close = frame.close = function () { + frame.close = function () { window.removeEventListener('message', _listener); }; @@ -130,31 +130,31 @@ win.postMessage(JSON.stringify(req), '*'); }; - var set = frame.set = function (key, val, cb) { + frame.set = function (key, val, cb) { send('set', key, val, cb); }; - var batchset = frame.setBatch = function (map, cb) { + frame.setBatch = function (map, cb) { send('batchset', void 0, map, cb); }; - var get = frame.get = function (key, cb) { + frame.get = function (key, cb) { send('get', key, void 0, cb); }; - var batchget = frame.getBatch = function (keys, cb) { + frame.getBatch = function (keys, cb) { send('batchget', void 0, keys, cb); }; - var remove = frame.remove = function (key, cb) { + frame.remove = function (key, cb) { send('remove', key, void 0, cb); }; - var batchremove = frame.removeBatch = function (keys, cb) { + frame.removeBatch = function (keys, cb) { send('batchremove', void 0, keys, cb); }; - var keys = frame.keys = function (cb) { + frame.keys = function (cb) { send('keys', void 0, void 0, cb); }; @@ -164,7 +164,7 @@ if (typeof(module) !== 'undefined' && module.exports) { module.exports = Frame; } else if (typeof(define) === 'function' && define.amd) { - define(['jquery'], function ($) { + define(['jquery'], function () { return Frame; }); } else { diff --git a/customize.dist/share/test.js b/customize.dist/share/test.js index efc9d81b4..a236dcfab 100644 --- a/customize.dist/share/test.js +++ b/customize.dist/share/test.js @@ -39,7 +39,7 @@ define([ return !keys.some(function (k) { return data[k] !== null; }); }; - Frame.create(document.body, domain + path, function (err, iframe, loadEvent) { + Frame.create(document.body, domain + path, function (err, iframe) { if (handleErr(err)) { return; } console.log("Created iframe"); @@ -50,7 +50,7 @@ define([ [function (i) { // test #1 var pew = randInt(); - frame.set('pew', pew, function (err, data) { + frame.set('pew', pew, function (err) { if (handleErr(err)) { return; } frame.get('pew', function (err, num) { if (handleErr(err)) { return; } @@ -76,9 +76,9 @@ define([ var keys = Object.keys(map); - frame.setBatch(map, function (err, data) { + frame.setBatch(map, function (err) { if (handleErr(err)) { return; } - frame.getBatch(keys, function (err, data) { + frame.getBatch(keys, function (err) { if (handleErr(err)) { return; } frame.removeBatch(Object.keys(map), function (err) { if (handleErr(err)) { return; } diff --git a/customize.dist/src/fragments/footer.html b/customize.dist/src/fragments/footer.html index ec80ed27d..4cf4b101d 100644 --- a/customize.dist/src/fragments/footer.html +++ b/customize.dist/src/fragments/footer.html @@ -31,7 +31,7 @@
- + diff --git a/customize.dist/src/less/loading.less b/customize.dist/src/less/loading.less index b48045622..6dcf2491a 100644 --- a/customize.dist/src/less/loading.less +++ b/customize.dist/src/less/loading.less @@ -36,6 +36,9 @@ left: 0; right: 0; text-align: center; + @media screen and (max-height: @media-medium-screen) { + display: none; + } span { background-color: @bg-loading; color: @color-loading; diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less index 5e4f74dcf..a199730cd 100644 --- a/customize.dist/src/less/toolbar.less +++ b/customize.dist/src/less/toolbar.less @@ -42,12 +42,13 @@ } button { - &#shareButton { + &#shareButton, &.buttonSuccess { // Bootstrap 4 colors color: #fff; background: @toolbar-green; border-color: @toolbar-green; &:hover { + color: #fff; background: #449d44; border: 1px solid #419641; } @@ -58,12 +59,13 @@ margin-left: 5px; } } - &#newdoc { + &#newdoc, &.buttonPrimary { // Bootstrap 4 colors color: #fff; background: #0275d8; border-color: #0275d8; &:hover { + color: #fff; background: #025aa5; border: 1px solid #01549b; } @@ -77,26 +79,84 @@ &.hidden { display: none; } + + // Bootstrap 4 colors (btn-secondary) + border: 1px solid transparent; + border-radius: .25rem; + color: #292b2c; + background-color: #fff; + border-color: #ccc; + &:hover { + color: #292b2c; + background-color: #e6e6e6; + border-color: #adadad; + } } - .cryptpad-lag { - box-sizing: content-box; - height: 16px; - width: 16px; + button.upgrade { + font-size: 14px; + vertical-align: top; + margin-left: 10px; + } + .cryptpad-drive-limit { display: inline-block; - padding: 5px; - margin: 3px 0; - div { + height: 26px; + width: 200px; + margin: 2px; + box-sizing: border-box; + border: 1px solid #999; + background: white; + position: relative; + text-align: center; + line-height: 24px; + .usage { + height: 24px; + display: inline-block; + background: blue; + position: absolute; + left: 0; + z-index:1; + &.normal { + background: @toolbar-green; + } + &.warning { + background: orange; + } + &.above { + background: red; + } + } + .usageText { + position: relative; + color: black; + text-shadow: 1px 0 2px white, 0 1px 2px white, -1px 0 2px white, 0 -1px 2px white; + z-index: 2; + font-size: 16px; + font-weight: bold; + } + } + .cryptpad-limit { + box-sizing: border-box; + height: 26px; + width: 26px; + display: inline-block; + padding: 3px; + margin: 0px; + margin-right: 3px; + vertical-align: middle; + span { + color: red; + cursor: pointer; margin: auto; + font-size: 20px; } } -.clag () { - background: transparent; -} - + .clag () { + background: transparent; + } - #newLag { + .cryptpad-lag { height: 20px; width: 23px; background: transparent; @@ -179,17 +239,6 @@ margin-right: 2px; } - button { - color: #000; - background-color: inherit; - background-image: linear-gradient(to bottom,#fff,#e4e4e4); - border: 1px solid #A6A6A6; - border-bottom-color: #979797; - border-radius: 3px; - &:hover { - background-image:linear-gradient(to bottom,#f2f2f2,#ccc); - } - } .cryptpad-state { line-height: 32px; /* equivalent to 26px + 2*2px margin used for buttons */ } @@ -378,9 +427,8 @@ .cryptpad-user { position: absolute; right: 0; - span:not(.cryptpad-lag) { + :not(.cryptpad-lag) span { vertical-align: top; - //display: inline-block; } button { span.fa { @@ -392,11 +440,9 @@ .cryptpad-toolbar-leftside { float: left; margin-bottom: -1px; - .cryptpad-user-list { - //float: right; + .cryptpad-dropdown-users { pre { - white-space: pre; - margin: 0; + margin: 5px 0px; } } button { @@ -413,14 +459,20 @@ display: none; text-align: center; .next { - float: right; + display: inline-block; + vertical-align: middle; + margin: 20px; } .previous { - float: left; + display: inline-block; + vertical-align: middle; + margin: 20px; } .goto { display: inline-block; - input { width: 50px; } + vertical-align: middle; + text-align: center; + input { width: 75px; } } .gotoInput { vertical-align: middle; @@ -434,7 +486,7 @@ border-radius: 5px; } } -.cryptpad-spinner { +.cryptpad-spinner > span { height: 16px; width: 16px; margin: 8px; diff --git a/customize.dist/terms.html b/customize.dist/terms.html index 73a13cea7..68eb51599 100644 --- a/customize.dist/terms.html +++ b/customize.dist/terms.html @@ -107,7 +107,7 @@
- + diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index 1165c6df4..d2385dfc1 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -117,51 +117,120 @@ .cryptpad-toolbar a { float: right; } -.cryptpad-toolbar button#shareButton { +.cryptpad-toolbar button { + border: 1px solid transparent; + border-radius: .25rem; + color: #292b2c; + background-color: #fff; + border-color: #ccc; +} +.cryptpad-toolbar button#shareButton, +.cryptpad-toolbar button.buttonSuccess { color: #fff; background: #5cb85c; border-color: #5cb85c; } -.cryptpad-toolbar button#shareButton:hover { +.cryptpad-toolbar button#shareButton:hover, +.cryptpad-toolbar button.buttonSuccess:hover { + color: #fff; background: #449d44; border: 1px solid #419641; } -.cryptpad-toolbar button#shareButton span { +.cryptpad-toolbar button#shareButton span, +.cryptpad-toolbar button.buttonSuccess span { color: #fff; } -.cryptpad-toolbar button#shareButton .large { +.cryptpad-toolbar button#shareButton .large, +.cryptpad-toolbar button.buttonSuccess .large { margin-left: 5px; } -.cryptpad-toolbar button#newdoc { +.cryptpad-toolbar button#newdoc, +.cryptpad-toolbar button.buttonPrimary { color: #fff; background: #0275d8; border-color: #0275d8; } -.cryptpad-toolbar button#newdoc:hover { +.cryptpad-toolbar button#newdoc:hover, +.cryptpad-toolbar button.buttonPrimary:hover { + color: #fff; background: #025aa5; border: 1px solid #01549b; } -.cryptpad-toolbar button#newdoc span { +.cryptpad-toolbar button#newdoc span, +.cryptpad-toolbar button.buttonPrimary span { color: #fff; } -.cryptpad-toolbar button#newdoc .large { +.cryptpad-toolbar button#newdoc .large, +.cryptpad-toolbar button.buttonPrimary .large { margin-left: 5px; } .cryptpad-toolbar button.hidden { display: none; } -.cryptpad-toolbar .cryptpad-lag { - box-sizing: content-box; - height: 16px; - width: 16px; +.cryptpad-toolbar button:hover { + color: #292b2c; + background-color: #e6e6e6; + border-color: #adadad; +} +.cryptpad-toolbar button.upgrade { + font-size: 14px; + vertical-align: top; + margin-left: 10px; +} +.cryptpad-toolbar .cryptpad-drive-limit { display: inline-block; - padding: 5px; - margin: 3px 0; + height: 26px; + width: 200px; + margin: 2px; + box-sizing: border-box; + border: 1px solid #999; + background: white; + position: relative; + text-align: center; + line-height: 24px; +} +.cryptpad-toolbar .cryptpad-drive-limit .usage { + height: 24px; + display: inline-block; + background: blue; + position: absolute; + left: 0; + z-index: 1; +} +.cryptpad-toolbar .cryptpad-drive-limit .usage.normal { + background: #5cb85c; +} +.cryptpad-toolbar .cryptpad-drive-limit .usage.warning { + background: orange; +} +.cryptpad-toolbar .cryptpad-drive-limit .usage.above { + background: red; +} +.cryptpad-toolbar .cryptpad-drive-limit .usageText { + position: relative; + color: black; + text-shadow: 1px 0 2px white, 0 1px 2px white, -1px 0 2px white, 0 -1px 2px white; + z-index: 2; + font-size: 16px; + font-weight: bold; +} +.cryptpad-toolbar .cryptpad-limit { + box-sizing: border-box; + height: 26px; + width: 26px; + display: inline-block; + padding: 3px; + margin: 0px; + margin-right: 3px; + vertical-align: middle; } -.cryptpad-toolbar .cryptpad-lag div { +.cryptpad-toolbar .cryptpad-limit span { + color: red; + cursor: pointer; margin: auto; + font-size: 20px; } -.cryptpad-toolbar #newLag { +.cryptpad-toolbar .cryptpad-lag { height: 20px; width: 23px; background: transparent; @@ -171,7 +240,7 @@ vertical-align: top; box-sizing: content-box; } -.cryptpad-toolbar #newLag span { +.cryptpad-toolbar .cryptpad-lag span { display: inline-block; width: 4px; margin: 0; @@ -182,50 +251,50 @@ border: 1px solid black; transition: background 1s, border 1s; } -.cryptpad-toolbar #newLag span:last-child { +.cryptpad-toolbar .cryptpad-lag span:last-child { margin-right: 0; } -.cryptpad-toolbar #newLag span.bar1 { +.cryptpad-toolbar .cryptpad-lag span.bar1 { height: 5px; } -.cryptpad-toolbar #newLag span.bar2 { +.cryptpad-toolbar .cryptpad-lag span.bar2 { height: 10px; } -.cryptpad-toolbar #newLag span.bar3 { +.cryptpad-toolbar .cryptpad-lag span.bar3 { height: 15px; } -.cryptpad-toolbar #newLag span.bar4 { +.cryptpad-toolbar .cryptpad-lag span.bar4 { height: 20px; } -.cryptpad-toolbar #newLag.lag0 span { +.cryptpad-toolbar .cryptpad-lag.lag0 span { background: transparent; border-color: red; } -.cryptpad-toolbar #newLag.lag1 .bar2, -.cryptpad-toolbar #newLag.lag1 .bar3, -.cryptpad-toolbar #newLag.lag1 .bar4 { +.cryptpad-toolbar .cryptpad-lag.lag1 .bar2, +.cryptpad-toolbar .cryptpad-lag.lag1 .bar3, +.cryptpad-toolbar .cryptpad-lag.lag1 .bar4 { background: transparent; } -.cryptpad-toolbar #newLag.lag1 span { +.cryptpad-toolbar .cryptpad-lag.lag1 span { background-color: orange; border-color: orange; } -.cryptpad-toolbar #newLag.lag2 .bar3, -.cryptpad-toolbar #newLag.lag2 .bar4 { +.cryptpad-toolbar .cryptpad-lag.lag2 .bar3, +.cryptpad-toolbar .cryptpad-lag.lag2 .bar4 { background: transparent; } -.cryptpad-toolbar #newLag.lag2 span { +.cryptpad-toolbar .cryptpad-lag.lag2 span { background-color: orange; border-color: orange; } -.cryptpad-toolbar #newLag.lag3 .bar4 { +.cryptpad-toolbar .cryptpad-lag.lag3 .bar4 { background: transparent; } -.cryptpad-toolbar #newLag.lag3 span { +.cryptpad-toolbar .cryptpad-lag.lag3 span { background-color: #5cb85c; border-color: #5cb85c; } -.cryptpad-toolbar #newLag.lag4 span { +.cryptpad-toolbar .cryptpad-lag.lag4 span { background-color: #5cb85c; border-color: #5cb85c; } @@ -250,17 +319,6 @@ margin-top: -3px; margin-right: 2px; } -.cryptpad-toolbar button { - color: #000; - background-color: inherit; - background-image: linear-gradient(to bottom, #fff, #e4e4e4); - border: 1px solid #A6A6A6; - border-bottom-color: #979797; - border-radius: 3px; -} -.cryptpad-toolbar button:hover { - background-image: linear-gradient(to bottom, #f2f2f2, #ccc); -} .cryptpad-toolbar .cryptpad-state { line-height: 32px; /* equivalent to 26px + 2*2px margin used for buttons */ @@ -449,7 +507,7 @@ position: absolute; right: 0; } -.cryptpad-toolbar-top .cryptpad-user span:not(.cryptpad-lag) { +.cryptpad-toolbar-top .cryptpad-user :not(.cryptpad-lag) span { vertical-align: top; } .cryptpad-toolbar-top .cryptpad-user button span.fa { @@ -459,9 +517,8 @@ float: left; margin-bottom: -1px; } -.cryptpad-toolbar-leftside .cryptpad-user-list pre { - white-space: pre; - margin: 0; +.cryptpad-toolbar-leftside .cryptpad-dropdown-users pre { + margin: 5px 0px; } .cryptpad-toolbar-leftside button { margin: 2px 4px 2px 0px; @@ -477,16 +534,22 @@ text-align: center; } .cryptpad-toolbar-history .next { - float: right; + display: inline-block; + vertical-align: middle; + margin: 20px; } .cryptpad-toolbar-history .previous { - float: left; + display: inline-block; + vertical-align: middle; + margin: 20px; } .cryptpad-toolbar-history .goto { display: inline-block; + vertical-align: middle; + text-align: center; } .cryptpad-toolbar-history .goto input { - width: 50px; + width: 75px; } .cryptpad-toolbar-history .gotoInput { vertical-align: middle; @@ -497,7 +560,7 @@ padding: 3px 3px; border-radius: 5px; } -.cryptpad-spinner { +.cryptpad-spinner > span { height: 16px; width: 16px; margin: 8px; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 97aec74ab..d8e6f73d2 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -11,6 +11,8 @@ define(function () { out.type.slide = 'Présentation'; out.type.drive = 'Drive'; out.type.whiteboard = "Tableau Blanc"; + out.type.file = "Fichier"; + out.type.media = "Média"; out.button_newpad = 'Nouveau document texte'; out.button_newcode = 'Nouvelle page de code'; @@ -49,10 +51,22 @@ define(function () { out.language = "Langue"; + out.upgrade = "Améliorer"; + out.upgradeTitle = "Améliorer votre compte pour augmenter la limite de stockage"; + out.MB = "Mo"; + out.greenLight = "Tout fonctionne bien"; 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.pinLimitReached = "Vous avez atteint votre limite de stockage"; + out.pinLimitReachedAlert = "Vous avez atteint votre limite de stockage. Les nouveaux pads ne seront pas enregistrés dans votre CrypDrive.
" + + "Pour résoudre ce problème, vous pouvez soit supprimer des pads de votre CryptDrive (y compris la corbeille), soit vous abonner à une offre premium pour augmenter la limite maximale."; + out.pinLimitNotPinned = "Vous avez atteint votre limite de stockage.
"+ + "Ce pad n'est pas enregistré dans votre CryptDrive."; + out.pinLimitDrive = out.pinLimitReached+ ".
" + + "Vous ne pouvez pas créer de nouveaux pads."; + out.importButtonTitle = 'Importer un pad depuis un fichier local'; out.exportButtonTitle = 'Exporter ce pad vers un fichier local'; @@ -93,6 +107,7 @@ define(function () { out.printDate = "Afficher la date"; out.printTitle = "Afficher le titre du pad"; out.printCSS = "Personnaliser l'apparence (CSS):"; + out.printTransition = "Activer les animations de transition"; out.slideOptionsTitle = "Personnaliser la présentation"; out.slideOptionsButton = "Enregistrer (Entrée)"; @@ -125,6 +140,7 @@ define(function () { out.history_restoreTitle = "Restaurer la version du document sélectionnée"; out.history_restorePrompt = "Êtes-vous sûr de vouloir remplacer la version actuelle du document par la version affichée ?"; out.history_restoreDone = "Document restauré"; + out.history_version = "Version :"; // Polls @@ -313,6 +329,10 @@ define(function () { out.settings_pinningError = "Un problème est survenu"; out.settings_usageAmount = "Vos pads épinglés occupent {0} Mo"; + out.settings_logoutEverywhereTitle = "Se déconnecter partout"; + out.settings_logoutEverywhere = "Se déconnecter de toutes les autres sessions."; + out.settings_logoutEverywhereConfirm = "Êtes-vous sûr ? Vous devrez vous reconnecter sur tous vos autres appareils."; + // index.html //about.html diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index e27a074b5..afdbdc6b2 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -11,6 +11,8 @@ define(function () { out.type.slide = 'Presentation'; out.type.drive = 'Drive'; out.type.whiteboard = 'Whiteboard'; + out.type.file = 'File'; + out.type.media = 'Media'; out.button_newpad = 'New Rich Text pad'; out.button_newcode = 'New Code pad'; @@ -51,10 +53,22 @@ define(function () { out.language = "Language"; + out.upgrade = "Upgrade"; + out.upgradeTitle = "Upgrade your account to increase the storage limit"; + out.MB = "MB"; + out.greenLight = "Everything is working fine"; out.orangeLight = "Your slow connection may impact your experience"; out.redLight = "You are disconnected from the session"; + out.pinLimitReached = "You've reached your storage limit"; + out.pinLimitReachedAlert = "You've reached your storage limit. New pads won't be stored in your CryptDrive.
" + + "To fix this problem, you can either remove pads from your CryptDrive (including the trash) or subscribe to a premium offer to increase your limit."; + out.pinLimitNotPinned = "You've reached your storage limit.
"+ + "This pad is not stored in your CryptDrive."; + out.pinLimitDrive = "You've reached your storage limit.
" + + "You can't create new pads."; + out.importButtonTitle = 'Import a pad from a local file'; out.exportButtonTitle = 'Export this pad to a local file'; @@ -95,6 +109,7 @@ define(function () { out.printDate = "Display the date"; out.printTitle = "Display the pad title"; out.printCSS = "Custom style rules (CSS):"; + out.printTransition = "Enable transition animations"; out.slideOptionsTitle = "Customize your slides"; out.slideOptionsButton = "Save (enter)"; @@ -127,6 +142,7 @@ define(function () { out.history_restoreTitle = "Restore the selected version of the document"; out.history_restorePrompt = "Are you sure you want to replace the current version of the document by the displayed one?"; out.history_restoreDone = "Document restored"; + out.history_version = "Version:"; // Polls @@ -318,6 +334,10 @@ define(function () { out.settings_pinningError = "Something went wrong"; out.settings_usageAmount = "Your pinned pads occupy {0}MB"; + out.settings_logoutEverywhereTitle = "Log out everywhere"; + out.settings_logoutEverywhere = "Log out of all other web sessions"; + out.settings_logoutEverywhereConfirm = "Are you sure? You will need to log in with all your devices."; + // index.html diff --git a/example.nginx.conf b/example.nginx.conf index 5067ca9c0..fcb8b7435 100644 --- a/example.nginx.conf +++ b/example.nginx.conf @@ -32,7 +32,7 @@ server { set $scriptSrc "'self'"; set $connectSrc "'self' wss://cryptpad.fr wss://api.cryptpad.fr"; set $fontSrc "'self'"; - set $imgSrc "data: *"; + set $imgSrc "data: * blob:"; set $frameSrc "'self' beta.cryptpad.fr"; if ($uri = /pad/inner.html) { @@ -65,8 +65,12 @@ server { rewrite ^.*$ /customize/api/config break; } + location ^~ /blob/ { + try_files $uri =404; + } + ## TODO fix in the code so that we don't need this - location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard)$ { + location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media)$ { rewrite ^(.*)$ $1/ redirect; } diff --git a/package.json b/package.json index 4b78c8396..83653bd7b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "1.5.0", + "version": "1.6.0", "dependencies": { "chainpad-server": "^1.0.1", "express": "~4.10.1", diff --git a/readme.md b/readme.md index 60da12331..4ed335c9e 100644 --- a/readme.md +++ b/readme.md @@ -54,6 +54,9 @@ These settings can be found in your configuration file in the `contentSecurity` ## Maintenance +Before upgrading your CryptPad instance to the latest version, we recommend that you check what has changed since your last update. +You can do so by checking which version you have (see package.json), and comparing it against newer [release notes](https://github.com/xwiki-labs/cryptpad/releases). + To get access to the most recent codebase: ``` @@ -70,9 +73,12 @@ bower update; # serverside dependencies npm update; ``` +## Deleting all data and resetting Cryptpad + To reset your instance of Cryptpad and remove all the data that is being stored: +**WARNING: This will reset your Cryptpad instance and remove all data** ``` # change into your cryptpad directory cd /your/cryptpad/instance/location; diff --git a/rpc.js b/rpc.js index 808815d84..ec6e516d0 100644 --- a/rpc.js +++ b/rpc.js @@ -2,12 +2,44 @@ /* Use Nacl for checking signatures of messages */ var Nacl = require("tweetnacl"); +/* globals Buffer*/ +/* globals process */ + +var Fs = require("fs"); +var Path = require("path"); + var RPC = module.exports; var Store = require("./storage/file"); var isValidChannel = function (chan) { - return /^[a-fA-F0-9]/.test(chan); + return /^[a-fA-F0-9]/.test(chan) || + [32, 48].indexOf(chan.length) !== -1; +}; + +var uint8ArrayToHex = function (a) { + // call slice so Uint8Arrays work as expected + return Array.prototype.slice.call(a).map(function (e) { + var n = Number(e & 0xff).toString(16); + if (n === 'NaN') { + throw new Error('invalid input resulted in NaN'); + } + + switch (n.length) { + case 0: return '00'; // just being careful, shouldn't happen + case 1: return '0' + n; + case 2: return n; + default: throw new Error('unexpected value'); + } + }).join(''); +}; + +var createFileId = function () { + var id = uint8ArrayToHex(Nacl.randomBytes(24)); + if (id.length !== 48 || /[^a-f0-9]/.test(id)) { + throw new Error('file ids must consist of 48 hex characters'); + } + return id; }; var makeToken = function () { @@ -21,7 +53,7 @@ var makeCookie = function (token) { return [ time, - process.pid, // jshint ignore:line + process.pid, token ]; }; @@ -59,7 +91,11 @@ var isTooOld = function (time, now) { var expireSessions = function (Sessions) { var now = +new Date(); Object.keys(Sessions).forEach(function (key) { + var session = Sessions[key]; if (isTooOld(Sessions[key].atime, now)) { + if (session.blobstage) { + session.blobstage.close(); + } delete Sessions[key]; } }); @@ -86,7 +122,7 @@ var isValidCookie = function (Sessions, publicKey, cookie) { } // different process. try harder - if (process.pid !== parsed.pid) { // jshint ignore:line + if (process.pid !== parsed.pid) { return false; } @@ -96,7 +132,6 @@ var isValidCookie = function (Sessions, publicKey, cookie) { var idx = user.tokens.indexOf(parsed.seq); if (idx === -1) { return false; } - var next; if (idx > 0) { // make a new token addTokenForKey(Sessions, publicKey, makeToken()); @@ -211,14 +246,32 @@ var getChannelList = function (store, Sessions, publicKey, cb) { }); }; +var getUploadSize = function (store, channel, cb) { + var path = ''; + + Fs.stat(path, function (err, stats) { + if (err) { return void cb(err); } + cb(void 0, stats.size); + }); +}; + var getFileSize = function (store, channel, cb) { if (!isValidChannel(channel)) { return void cb('INVALID_CHAN'); } - if (typeof(store.getChannelSize) !== 'function') { - return cb('GET_CHANNEL_SIZE_UNSUPPORTED'); + + if (channel.length === 32) { + if (typeof(store.getChannelSize) !== 'function') { + return cb('GET_CHANNEL_SIZE_UNSUPPORTED'); + } + + return void store.getChannelSize(channel, function (e, size) { + if (e) { return void cb(e.code); } + cb(void 0, size); + }); } - return void store.getChannelSize(channel, function (e, size) { - if (e) { return void cb(e.code); } + // 'channel' refers to a file, so you need anoter API + getUploadSize(null, channel, function (e, size) { + if (e) { return void cb(e); } cb(void 0, size); }); }; @@ -294,10 +347,11 @@ var getHash = function (store, Sessions, publicKey, cb) { }); }; -var storeMessage = function (store, publicKey, msg, cb) { +/* var storeMessage = function (store, publicKey, msg, cb) { store.message(publicKey, JSON.stringify(msg), cb); -}; +}; */ +// TODO check if new pinned size exceeds user quota var pinChannel = function (store, Sessions, publicKey, channels, cb) { if (!channels && channels.filter) { // expected array @@ -349,8 +403,7 @@ var unpinChannel = function (store, Sessions, publicKey, channels, cb) { function (e) { if (e) { return void cb(e); } toStore.forEach(function (channel) { - // TODO actually delete - session.channels[channel] = false; + delete session.channels[channel]; }); getHash(store, Sessions, publicKey, cb); @@ -358,6 +411,7 @@ var unpinChannel = function (store, Sessions, publicKey, channels, cb) { }); }; +// TODO check if new pinned size exceeds user quota var resetUserPins = function (store, Sessions, publicKey, channelList, cb) { var session = beginSession(Sessions, publicKey); @@ -376,14 +430,191 @@ var resetUserPins = function (store, Sessions, publicKey, channelList, cb) { }); }; +var getPrivilegedUserList = function (cb) { + Fs.readFile('./privileged.conf', 'utf8', function (e, body) { + if (e) { + if (e.code === 'ENOENT') { + return void cb(void 0, []); + } + return void (e.code); + } + var list = body.split(/\n/) + .map(function (line) { + return line.replace(/#.*$/, '').trim(); + }) + .filter(function (x) { return x; }); + cb(void 0, list); + }); +}; + +var isPrivilegedUser = function (publicKey, cb) { + getPrivilegedUserList(function (e, list) { + if (e) { return void cb(false); } + cb(list.indexOf(publicKey) !== -1); + }); +}; + +var getLimit = function (cb) { + cb = cb; // TODO +}; + +var safeMkdir = function (path, cb) { + Fs.mkdir(path, function (e) { + if (!e || e.code === 'EEXIST') { return void cb(); } + cb(e); + }); +}; + +var makeFilePath = function (root, id) { + if (typeof(id) !== 'string' || id.length <= 2) { return null; } + return Path.join(root, id.slice(0, 2), id); +}; + +var makeFileStream = function (root, id, cb) { + var stub = id.slice(0, 2); + var full = makeFilePath(root, id); + safeMkdir(Path.join(root, stub), function (e) { + if (e) { return void cb(e); } + + try { + var stream = Fs.createWriteStream(full, { + flags: 'a', + encoding: 'binary', + }); + stream.on('open', function () { + cb(void 0, stream); + }); + } catch (err) { + cb('BAD_STREAM'); + } + }); +}; + +var upload = function (paths, Sessions, publicKey, content, cb) { + var dec = new Buffer(Nacl.util.decodeBase64(content)); // jshint ignore:line + + var session = Sessions[publicKey]; + session.atime = +new Date(); + if (!session.blobstage) { + makeFileStream(paths.staging, publicKey, function (e, stream) { + if (e) { return void cb(e); } + + var blobstage = session.blobstage = stream; + blobstage.write(dec); + cb(void 0, dec.length); + }); + } else { + session.blobstage.write(dec); + cb(void 0, dec.length); + } +}; + +var upload_cancel = function (paths, Sessions, publicKey, cb) { + var path = makeFilePath(paths.staging, publicKey); + if (!path) { + console.log(paths.staging, publicKey); + console.log(path); + return void cb('NO_FILE'); + } + + Fs.unlink(path, function (e) { + if (e) { return void cb('E_UNLINK'); } + cb(void 0); + }); +}; + +var isFile = function (filePath, cb) { + Fs.stat(filePath, function (e, stats) { + if (e) { + if (e.code === 'ENOENT') { return void cb(void 0, false); } + return void cb(e.message); + } + return void cb(void 0, stats.isFile()); + }); +}; + +/* TODO +change channel IDs to a different length so that when we pin, we will be able +to tell that it is not a channel, but a file, just by its length. + +also, when your upload is complete, pin the resulting file. +*/ +var upload_complete = function (paths, Sessions, publicKey, cb) { + var session = Sessions[publicKey]; + + if (session.blobstage && session.blobstage.close) { + session.blobstage.close(); + delete session.blobstage; + } + + var oldPath = makeFilePath(paths.staging, publicKey); + + var tryRandomLocation = function (cb) { + var id = createFileId(); + var prefix = id.slice(0, 2); + var newPath = makeFilePath(paths.blob, id); + + safeMkdir(Path.join(paths.blob, prefix), function (e) { + if (e) { + console.error(e); + return void cb('RENAME_ERR'); + } + isFile(newPath, function (e, yes) { + if (e) { + console.error(e); + return void cb(e); + } + if (yes) { + return void tryRandomLocation(cb); + } + + cb(void 0, newPath, id); + }); + }); + }; + + tryRandomLocation(function (e, newPath, id) { + Fs.rename(oldPath, newPath, function (e) { + if (e) { + console.error(e); + return cb(e); + } + + cb(void 0, id); + }); + }); +}; + +/* TODO +when asking about your upload status, also send some information about how big +your upload is going to be. if that would exceed your limit, return TOO_LARGE +error. + +*/ +var upload_status = function (paths, Sessions, publicKey, cb) { + var filePath = makeFilePath(paths.staging, publicKey); + if (!filePath) { return void cb('E_INVALID_PATH'); } + isFile(filePath, function (e, yes) { + cb(e, yes); + }); +}; + /*::const ConfigType = require('./config.example.js');*/ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) { // load pin-store... - console.log('loading rpc module...'); var Sessions = {}; + var keyOrDefaultString = function (key, def) { + return typeof(config[key]) === 'string'? config[key]: def; + }; + + var paths = {}; + var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins'); + var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob'); + var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage'); + var store; var rpc = function ( @@ -428,7 +659,6 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY'); } - if (checkSignature(serialized, signature, publicKey) !== true) { return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY"); } @@ -446,20 +676,26 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) var Respond = function (e, msg) { var token = Sessions[publicKey].tokens.slice(-1)[0]; var cookie = makeCookie(token).join('|'); - respond(e, [cookie].concat(msg||[])); + respond(e, [cookie].concat(typeof(msg) !== 'undefined' ?msg: [])); }; if (typeof(msg) !== 'object' || !msg.length) { return void Respond('INVALID_MSG'); } + var deny = function () { + Respond('E_ACCESS_DENIED'); + }; + + var handleMessage = function (privileged) { switch (msg[0]) { case 'COOKIE': return void Respond(void 0); case 'RESET': return resetUserPins(store, Sessions, safeKey, msg[1], function (e, hash) { return void Respond(e, hash); }); - case 'PIN': + case 'PIN': // TODO don't pin if over the limit + // if over, send error E_OVER_LIMIT return pinChannel(store, Sessions, safeKey, msg[1], function (e, hash) { Respond(e, hash); }); @@ -471,32 +707,89 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return void getHash(store, Sessions, safeKey, function (e, hash) { Respond(e, hash); }); - case 'GET_TOTAL_SIZE': + case 'GET_TOTAL_SIZE': // TODO cache this, since it will get called quite a bit return getTotalSize(store, ctx.store, Sessions, safeKey, function (e, size) { if (e) { return void Respond(e); } Respond(e, size); }); case 'GET_FILE_SIZE': return void getFileSize(ctx.store, msg[1], Respond); + case 'GET_LIMIT': // TODO implement this and cache it per-user + return void getLimit(function (e, limit) { + limit = limit; + Respond('NOT_IMPLEMENTED'); + }); case 'GET_MULTIPLE_FILE_SIZE': return void getMultipleFileSize(ctx.store, msg[1], function (e, dict) { if (e) { return void Respond(e); } Respond(void 0, dict); }); + + + // restricted to privileged users... + case 'UPLOAD': + if (!privileged) { return deny(); } + return void upload(paths, Sessions, safeKey, msg[1], function (e, len) { + Respond(e, len); + }); + case 'UPLOAD_STATUS': + if (!privileged) { return deny(); } + return void upload_status(paths, Sessions, safeKey, function (e, stat) { + Respond(e, stat); + }); + case 'UPLOAD_COMPLETE': + if (!privileged) { return deny(); } + return void upload_complete(paths, Sessions, safeKey, function (e, hash) { + Respond(e, hash); + }); + case 'UPLOAD_CANCEL': + if (!privileged) { return deny(); } + return void upload_cancel(paths, Sessions, safeKey, function (e) { + Respond(e); + }); default: return void Respond('UNSUPPORTED_RPC_CALL', msg); } + }; + + // reject uploads unless explicitly enabled + if (config.enableUploads !== true) { + return void handleMessage(false); + } + + // restrict upload capability unless explicitly disabled + if (config.restrictUploads === false) { + return void handleMessage(true); + } + + // if session has not been authenticated, do so + var session = Sessions[publicKey]; + if (typeof(session.privilege) !== 'boolean') { + return void isPrivilegedUser(publicKey, function (yes) { + session.privilege = yes; + handleMessage(yes); + }); + } + + // if authenticated, proceed + handleMessage(session.privilege); }; Store.create({ - filePath: './pins' + filePath: pinPath, }, function (s) { store = s; - cb(void 0, rpc); - // expire old sessions once per minute - setInterval(function () { - expireSessions(Sessions); - }, 60000); + safeMkdir(blobPath, function (e) { + if (e) { throw e; } + safeMkdir(blobStagingPath, function (e) { + if (e) { throw e; } + cb(void 0, rpc); + // expire old sessions once per minute + setInterval(function () { + expireSessions(Sessions); + }, 60000); + }); + }); }); }; diff --git a/server.js b/server.js index f12b90229..d7f5b90fc 100644 --- a/server.js +++ b/server.js @@ -82,6 +82,8 @@ var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'cont var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); app.get(mainPagePattern, Express.static(__dirname + '/customize.dist')); +app.use("/blob", Express.static(__dirname + '/blob')); + app.use("/customize", Express.static(__dirname + '/customize')); app.use("/customize", Express.static(__dirname + '/customize.dist')); app.use(/^\/[^\/]*$/, Express.static('customize')); diff --git a/storage/file.js b/storage/file.js index ab2bce617..857f147f4 100644 --- a/storage/file.js +++ b/storage/file.js @@ -28,7 +28,8 @@ var readMessages = function (path, msgHandler, cb) { }; var checkPath = function (path, callback) { - Fs.stat(path, function (err, stats) { + // TODO check if we actually need to use stat at all + Fs.stat(path, function (err) { if (!err) { callback(undefined, true); return; @@ -166,7 +167,7 @@ var getChannel = function (env, id, callback) { }); } }); - }).nThen(function (waitFor) { + }).nThen(function () { if (errorState) { return; } complete(); }); diff --git a/www/assert/main.js b/www/assert/main.js index eb9bb9157..7da11f408 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -15,31 +15,43 @@ define([ var failMessages = []; var ASSERTS = []; - var runASSERTS = function () { + var runASSERTS = function (cb) { + var count = ASSERTS.length; + var successes = 0; + + var done = function (err) { + count--; + if (err) { failMessages.push(err); } + else { successes++; } + if (count === 0) { cb(); } + }; + ASSERTS.forEach(function (f, index) { - f(index); + f(function (err) { + done(err, index); + }, index); }); }; var assert = function (test, msg) { - ASSERTS.push(function (i) { - var returned = test(); - if (returned === true) { - assertions++; - } else { - failed = true; - failedOn = assertions; - failMessages.push({ - test: i, - message: msg, - output: returned, - }); - } + ASSERTS.push(function (cb, i) { + test(function (result) { + if (result === true) { + assertions++; + cb(); + } else { + failed = true; + failedOn = assertions; + cb({ + test: i, + message: msg, + output: result, + }); + } + }); }); }; - var $body = $('body'); - var HJSON_list = [ '["DIV",{"id":"target"},[["P",{"class":" alice bob charlie has.dot","id":"bang"},["pewpewpew"]]]]', @@ -60,7 +72,7 @@ define([ }; var HJSON_equal = function (shjson) { - assert(function () { + assert(function (cb) { // parse your stringified Hyperjson var hjson; @@ -84,10 +96,10 @@ define([ var diff = TextPatcher.format(shjson, op); if (success) { - return true; + return cb(true); } else { - return '

insert: ' + diff.insert + '

' + - 'remove: ' + diff.remove + '

'; + return cb('

insert: ' + diff.insert + '

' + + 'remove: ' + diff.remove + '

'); } }, "expected hyperjson equality"); }; @@ -96,7 +108,7 @@ define([ var roundTrip = function (sel) { var target = $(sel)[0]; - assert(function () { + assert(function (cb) { var hjson = Hyperjson.fromDOM(target); var cloned = Hyperjson.toDOM(hjson); var success = cloned.outerHTML === target.outerHTML; @@ -113,7 +125,7 @@ define([ TextPatcher.log(target.outerHTML, op); } - return success; + return cb(success); }, "Round trip serialization introduced artifacts."); }; @@ -127,9 +139,9 @@ define([ var strungJSON = function (orig) { var result; - assert(function () { + assert(function (cb) { result = JSON.stringify(JSON.parse(orig)); - return result === orig; + return cb(result === orig); }, "expected result (" + result + ") to equal original (" + orig + ")"); }; @@ -139,6 +151,59 @@ define([ strungJSON(orig); }); + // check that old hashes parse correctly + assert(function (cb) { + var secret = Cryptpad.parseHash('67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy'); + return cb(secret.channel === "67b8385b07352be53e40746d2be6ccd7" && + secret.key === "XAYSuJYYqa9NfmInyHci7LNy" && + secret.version === 0); + }, "Old hash failed to parse"); + + // make sure version 1 hashes parse correctly + assert(function (cb) { + var secret = Cryptpad.parseHash('/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI'); + return cb(secret.version === 1 && + secret.mode === "edit" && + secret.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && + secret.key === "usn4+9CqVja8Q7RZOGTfRgqI" && + !secret.present); + }, "version 1 hash failed to parse"); + + // test support for present mode in hashes + assert(function (cb) { + var secret = Cryptpad.parseHash('/1/edit/CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G/present'); + return cb(secret.version === 1 + && secret.mode === "edit" + && secret.channel === "CmN5+YJkrHFS3NSBg-P7Sg" + && secret.key === "DNZ2wcG683GscU4fyOyqA87G" + && secret.present); + }, "version 1 hash failed to parse"); + + // test support for present mode in hashes + assert(function (cb) { + var secret = Cryptpad.parseHash('/1/edit//CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G//present'); + return cb(secret.version === 1 + && secret.mode === "edit" + && secret.channel === "CmN5+YJkrHFS3NSBg-P7Sg" + && secret.key === "DNZ2wcG683GscU4fyOyqA87G" + && secret.present); + }, "Couldn't handle multiple successive slashes"); + + // test support for trailing slash + assert(function (cb) { + var secret = Cryptpad.parseHash('/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/'); + return cb(secret.version === 1 && + secret.mode === "edit" && + secret.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && + secret.key === "usn4+9CqVja8Q7RZOGTfRgqI" && + !secret.present); + }, "test support for trailing slashes in version 1 hash failed to parse"); + + assert(function (cb) { + // TODO + return cb(true); + }, "version 2 hash failed to parse correctly"); + var swap = function (str, dict) { return str.replace(/\{\{(.*?)\}\}/g, function (all, key) { return typeof dict[key] !== 'undefined'? dict[key] : all; @@ -153,7 +218,7 @@ define([ return str || ''; }; - var formatFailures = function () { + var formatFailures = function () { var template = multiline(function () { /*

Failed on test number {{test}} with error message: @@ -174,16 +239,15 @@ The test returned: }).join("\n"); }; - runASSERTS(); - - $("body").html(function (i, val) { - var dict = { - previous: val, - totalAssertions: ASSERTS.length, - passedAssertions: assertions, - plural: (assertions === 1? '' : 's'), - failMessages: formatFailures() - }; + runASSERTS(function () { + $("body").html(function (i, val) { + var dict = { + previous: val, + totalAssertions: ASSERTS.length, + passedAssertions: assertions, + plural: (assertions === 1? '' : 's'), + failMessages: formatFailures() + }; var SUCCESS = swap(multiline(function(){/*

{{passedAssertions}} / {{totalAssertions}} test{{plural}} passed. @@ -196,12 +260,13 @@ The test returned: {{previous}} */}), dict); - var report = SUCCESS; + var report = SUCCESS; - return report; - }); + return report; + }); - var $report = $('.report'); - $report.addClass(failed?'failure':'success'); + var $report = $('.report'); + $report.addClass(failed?'failure':'success'); + }); }); diff --git a/www/code/main.js b/www/code/main.js index 4f12d0926..85cd26db3 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -3,18 +3,12 @@ define([ '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/textpatcher/TextPatcher.js', - '/common/toolbar.js', + '/common/toolbar2.js', 'json.sortify', '/bower_components/chainpad-json-validator/json-ot.js', '/common/cryptpad-common.js', '/common/cryptget.js', - '/common/modes.js', - '/common/themes.js', - '/common/visible.js', - '/common/notify.js', - '/bower_components/file-saver/FileSaver.min.js' -], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Modes, Themes, Visible, Notify) { - var saveAs = window.saveAs; +], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget) { var Messages = Cryptpad.Messages; var module = window.APP = { @@ -30,6 +24,7 @@ define([ }; var toolbar; + var editor; var secret = Cryptpad.getSecrets(); var readOnly = secret.keys && !secret.keys.editKeyStr; @@ -37,117 +32,26 @@ define([ secret.keys = secret.key; } - var onConnectError = function (info) { + var onConnectError = function () { Cryptpad.errorLoadingScreen(Messages.websocketError); }; var andThen = function (CMeditor) { - var CodeMirror = module.CodeMirror = CMeditor; - CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js"; - var $pad = $('#pad-iframe'); - var $textarea = $pad.contents().find('#editor1'); + var CodeMirror = Cryptpad.createCodemirror(CMeditor, ifrw, Cryptpad); + editor = CodeMirror.editor; var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); - var parsedHash = Cryptpad.parsePadUrl(window.location.href); - var defaultName = Cryptpad.getDefaultName(parsedHash); - var initialState = Messages.codeInitialState; var isHistoryMode = false; - var editor = module.editor = CMeditor.fromTextArea($textarea[0], { - lineNumbers: true, - lineWrapping: true, - autoCloseBrackets: true, - matchBrackets : true, - showTrailingSpace : true, - styleActiveLine : true, - search: true, - highlightSelectionMatches: {showToken: /\w+/}, - extraKeys: {"Shift-Ctrl-R": undefined}, - foldGutter: true, - gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], - mode: "javascript", - readOnly: true - }); - editor.setValue(Messages.codeInitialState); - - var setMode = module.setMode = function (mode, $select) { - module.highlightMode = mode; - if (mode === 'text') { - editor.setOption('mode', 'text'); - return; - } - CodeMirror.autoLoadMode(editor, mode); - editor.setOption('mode', mode); - if ($select) { - var name = $select.find('a[data-value="' + mode + '"]').text() || 'Mode'; - $select.setValue(name); - } - }; - - var setTheme = module.setTheme = (function () { - var path = '/common/theme/'; - - var $head = $(ifrw.document.head); - - var themeLoaded = module.themeLoaded = function (theme) { - return $head.find('link[href*="'+theme+'"]').length; - }; - - var loadTheme = module.loadTheme = function (theme) { - $head.append($('', { - rel: 'stylesheet', - href: path + theme + '.css', - })); - }; - - return function (theme, $select) { - if (!theme) { - editor.setOption('theme', 'default'); - } else { - if (!themeLoaded(theme)) { - loadTheme(theme); - } - editor.setOption('theme', theme); - } - if ($select) { - $select.setValue(theme || 'Theme'); - } - }; - }()); - var setEditable = module.setEditable = function (bool) { if (readOnly && bool) { return; } editor.setOption('readOnly', !bool); }; - var userData = module.userData = {}; // List of pretty name of all users (mapped with their server ID) - var userList; // List of users still connected to the channel (server IDs) - var addToUserData = function(data) { - var users = module.users; - for (var attrname in data) { userData[attrname] = data[attrname]; } - - if (users && users.length) { - for (var userKey in userData) { - if (users.indexOf(userKey) === -1) { - delete userData[userKey]; - } - } - } - - if(userList && typeof userList.onChange === "function") { - userList.onChange(userData); - } - }; - - var myData = {}; - var myUserName = ''; // My "pretty name" - var myID; // My server ID - - var setMyID = function(info) { - myID = info.myID || null; - myUserName = myID; - }; + var Title; + var UserList; + var Metadata; var config = { initialState: '{}', @@ -157,7 +61,6 @@ define([ validateKey: secret.keys.validateKey || undefined, readOnly: readOnly, crypto: Crypto.createEncryptor(secret.keys), - setMyID: setMyID, network: Cryptpad.getNetwork(), transformFunction: JsonOT.validate, }; @@ -172,26 +75,21 @@ define([ } }; - var isDefaultTitle = function () { - var parsed = Cryptpad.parsePadUrl(window.location.href); - return Cryptpad.isDefaultName(parsed, document.title); - }; - var initializing = true; var stringifyInner = function (textValue) { var obj = { content: textValue, metadata: { - users: userData, - defaultTitle: defaultName + users: UserList.userData, + defaultTitle: Title.defaultTitle } }; if (!initializing) { - obj.metadata.title = document.title; + obj.metadata.title = Title.title; } // set mode too... - obj.highlightMode = module.highlightMode; + obj.highlightMode = CodeMirror.highlightMode; // stringify the json and send it into chainpad return stringify(obj); @@ -204,7 +102,7 @@ define([ editor.save(); - var textValue = canonicalize($textarea.val()); + var textValue = canonicalize(CodeMirror.$textarea.val()); var shjson = stringifyInner(textValue); module.patchText(shjson); @@ -214,231 +112,55 @@ define([ } }; - var setName = module.setName = function (newName) { - if (typeof(newName) !== 'string') { return; } - var myUserNameTemp = newName.trim(); - if(newName.trim().length > 32) { - myUserNameTemp = myUserNameTemp.substr(0, 32); - } - myUserName = myUserNameTemp; - myData[myID] = { - name: myUserName, - uid: Cryptpad.getUid(), - }; - addToUserData(myData); - Cryptpad.setAttribute('username', myUserName, function (err, data) { - if (err) { - console.log("Couldn't set username"); - console.error(err); - return; - } - onLocal(); - }); - }; - - var getHeadingText = function () { - var lines = editor.getValue().split(/\n/); - - var text = ''; - lines.some(function (line) { - // lisps? - var lispy = /^\s*(;|#\|)(.*?)$/; - if (lispy.test(line)) { - line.replace(lispy, function (a, one, two) { - text = two; - }); - return true; - } - - // lines beginning with a hash are potentially valuable - // works for markdown, python, bash, etc. - var hash = /^#(.*?)$/; - if (hash.test(line)) { - line.replace(hash, function (a, one) { - text = one; - }); - return true; - } - - // lines including a c-style comment are also valuable - var clike = /^\s*(\/\*|\/\/)(.*)?(\*\/)*$/; - if (clike.test(line)) { - line.replace(clike, function (a, one, two) { - if (!(two && two.replace)) { return; } - text = two.replace(/\*\/\s*$/, '').trim(); - }); - return true; - } - - // TODO make one more pass for multiline comments - }); - - return text.trim(); - }; - - var suggestName = function (fallback) { - if (document.title === defaultName) { - return getHeadingText() || fallback || ""; - } else { - return document.title || getHeadingText() || defaultName; - } - }; - - var exportText = module.exportText = function () { - var text = editor.getValue(); - - var ext = Modes.extensionOf(module.highlightMode); - - var title = Cryptpad.fixFileName(suggestName('cryptpad')) + (ext || '.txt'); - - Cryptpad.prompt(Messages.exportPrompt, title, function (filename) { - if (filename === null) { return; } - var blob = new Blob([text], { - type: 'text/plain;charset=utf-8' - }); - 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 renameCb = function (err, title) { - if (err) { return; } - document.title = title; - onLocal(); - }; - var updateTitle = function (newTitle) { - if (newTitle === document.title) { return; } - // Change the title now, and set it back to the old value if there is an error - var oldTitle = document.title; - document.title = newTitle; - Cryptpad.renamePad(newTitle, function (err, data) { - if (err) { - console.log("Couldn't set pad title"); - console.error(err); - document.title = oldTitle; - return; - } - document.title = data; - $bar.find('.' + Toolbar.constants.title).find('span.title').text(data); - $bar.find('.' + Toolbar.constants.title).find('input').val(data); - }); - }; - var updateDefaultTitle = function (defaultTitle) { - defaultName = defaultTitle; - $bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName); - }; + config.onInit = function (info) { + UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad); - var updateMetadata = function(shjson) { - // Extract the user list (metadata) from the hyperjson - var json = (shjson === "") ? "" : JSON.parse(shjson); - var titleUpdated = false; - if (json && json.metadata) { - if (json.metadata.users) { - var userData = json.metadata.users; - // Update the local user data - addToUserData(userData); - } - if (json.metadata.defaultTitle) { - updateDefaultTitle(json.metadata.defaultTitle); - } - if (typeof json.metadata.title !== "undefined") { - updateTitle(json.metadata.title || defaultName); - titleUpdated = true; - } - } - if (!titleUpdated) { - updateTitle(defaultName); - } - }; + var titleCfg = { getHeadingText: CodeMirror.getHeadingText }; + Title = Cryptpad.createTitle(titleCfg, config.onLocal, Cryptpad); - var onInit = config.onInit = function (info) { - userList = info.userList; + Metadata = Cryptpad.createMetadata(UserList, Title); var configTb = { - displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], - userData: userData, - readOnly: readOnly, - ifrw: ifrw, + displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'], + userList: UserList.getToolbarConfig(), share: { secret: secret, channel: info.channel }, - title: { - onRename: renameCb, - defaultName: defaultName, - suggestName: suggestName - }, - common: Cryptpad + title: Title.getTitleConfig(), + common: Cryptpad, + readOnly: readOnly, + ifrw: ifrw, + realtime: info.realtime, + network: info.network, + $container: $bar }; - toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, userList, configTb); + toolbar = module.toolbar = Toolbar.create(configTb); - var $rightside = $bar.find('.' + Toolbar.constants.rightside); - var $userBlock = $bar.find('.' + Toolbar.constants.username); - var $usernameButton = module.$userNameButton = $($bar.find('.' + Toolbar.constants.changeUsername)); + Title.setToolbar(toolbar); + CodeMirror.init(config.onLocal, Title, toolbar); - var editHash; - var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys); + var $rightside = toolbar.$rightside; + var editHash; if (!readOnly) { editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); } /* add a history button */ - var histConfig = {}; - histConfig.onRender = function (val) { - if (typeof val === "undefined") { return; } - try { - var hjson = JSON.parse(val || '{}'); - var remoteDoc = hjson.content; + var histConfig = { + onLocal: config.onLocal(), + onRemote: config.onRemote(), + setHistory: setHistory, + applyVal: function (val) { + var remoteDoc = JSON.parse(val || '{}').content; editor.setValue(remoteDoc || ''); editor.save(); - } catch (e) { - // Probably a parse error - console.error(e); - } - }; - histConfig.onClose = function () { - // Close button clicked - setHistory(false, true); - }; - histConfig.onRevert = function () { - // Revert button clicked - setHistory(false, false); - config.onLocal(); - config.onRemote(); - }; - histConfig.onReady = function () { - // Called when the history is loaded and the UI displayed - setHistory(true); + }, + $toolbar: $bar }; - histConfig.$toolbar = $bar; var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); $rightside.append($hist); @@ -447,133 +169,42 @@ define([ var templateObj = { rt: info.realtime, Crypt: Cryptget, - getTitle: function () { return document.title; } + getTitle: Title.getTitle }; var $templateButton = Cryptpad.createButton('template', true, templateObj); $rightside.append($templateButton); } /* add an export button */ - var $export = Cryptpad.createButton('export', true, {}, exportText); + var $export = Cryptpad.createButton('export', true, {}, CodeMirror.exportText); $rightside.append($export); if (!readOnly) { /* add an import button */ - var $import = Cryptpad.createButton('import', true, {}, importText); + var $import = Cryptpad.createButton('import', true, {}, CodeMirror.importText); $rightside.append($import); - - /* add a rename button */ - //var $setTitle = Cryptpad.createButton('rename', true, {suggestName: suggestName}, renameCb); - //$rightside.append($setTitle); } /* add a forget button */ - var forgetCb = function (err, title) { + var forgetCb = function (err) { if (err) { return; } setEditable(false); }; var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); $rightside.append($forgetPad); - var configureLanguage = function (cb) { - // FIXME this is async so make it happen as early as possible - var options = []; - Modes.list.forEach(function (l) { - options.push({ - tag: 'a', - attributes: { - 'data-value': l.mode, - 'href': '#', - }, - content: l.language // Pretty name of the language value - }); - }); - var dropdownConfig = { - text: 'Mode', // Button initial text - options: options, // Entries displayed in the menu - left: true, // Open to the left of the button - isSelect: true, - }; - var $block = module.$language = Cryptpad.createDropdown(dropdownConfig); - var $button = $block.find('.buttonTitle'); - - $block.find('a').click(function (e) { - setMode($(this).attr('data-value'), $block); - onLocal(); - }); - - $rightside.append($block); - cb(); - }; - - var configureTheme = function () { - /* Remember the user's last choice of theme using localStorage */ - var themeKey = 'CRYPTPAD_CODE_THEME'; - var lastTheme = localStorage.getItem(themeKey) || 'default'; - - var options = []; - Themes.forEach(function (l) { - options.push({ - tag: 'a', - attributes: { - 'data-value': l.name, - 'href': '#', - }, - content: l.name // Pretty name of the language value - }); - }); - var dropdownConfig = { - text: 'Theme', // Button initial text - options: options, // Entries displayed in the menu - left: true, // Open to the left of the button - isSelect: true, - initialValue: lastTheme - }; - var $block = module.$theme = Cryptpad.createDropdown(dropdownConfig); - var $button = $block.find('.buttonTitle'); - - setTheme(lastTheme, $block); - - $block.find('a').click(function (e) { - var theme = $(this).attr('data-value'); - setTheme(theme, $block); - localStorage.setItem(themeKey, theme); - }); - - $rightside.append($block); - }; - if (!readOnly) { - configureLanguage(function () { - configureTheme(); - }); + CodeMirror.configureLanguage(CodeMirror.configureTheme); } else { - configureTheme(); + CodeMirror.configureTheme(); } // set the hash if (!readOnly) { Cryptpad.replaceHash(editHash); } - - Cryptpad.onDisplayNameChanged(setName); - }; - - var unnotify = module.unnotify = function () { - if (module.tabNotification && - typeof(module.tabNotification.cancel) === 'function') { - module.tabNotification.cancel(); - } - }; - - var notify = module.notify = function () { - if (Visible.isSupported() && !Visible.currently()) { - unnotify(); - module.tabNotification = Notify.tab(1000, 10); - } }; - var onReady = config.onReady = function (info) { - module.users = info.userList.users; + config.onReady = function (info) { if (module.realtime !== info.realtime) { var realtime = module.realtime = info.realtime; module.patchText = TextPatcher.create({ @@ -600,135 +231,58 @@ define([ newDoc = hjson.content; if (hjson.highlightMode) { - setMode(hjson.highlightMode, module.$language); + CodeMirror.setMode(hjson.highlightMode); } } - if (!module.highlightMode) { - setMode('javascript', module.$language); - console.log("%s => %s", module.highlightMode, module.$language.val()); + if (!CodeMirror.highlightMode) { + CodeMirror.setMode('javascript'); + console.log("%s => %s", CodeMirror.highlightMode, CodeMirror.$language.val()); } // Update the user list (metadata) from the hyperjson - updateMetadata(userDoc); + Metadata.update(userDoc); if (newDoc) { editor.setValue(newDoc); } - if (Cryptpad.initialName && document.title === defaultName) { - updateTitle(Cryptpad.initialName); - onLocal(); - } - - if (Visible.isSupported()) { - Visible.onChange(function (yes) { - if (yes) { unnotify(); } - }); + if (Cryptpad.initialName && Title.isDefaultTitle()) { + Title.updateTitle(Cryptpad.initialName); } Cryptpad.removeLoadingScreen(); setEditable(true); initializing = false; - //Cryptpad.log("Your document is ready"); onLocal(); // push local state to avoid parse errors later. - Cryptpad.getLastName(function (err, lastName) { - if (err) { - console.log("Could not get previous name"); - console.error(err); - return; - } - // Update the toolbar list: - // Add the current user in the metadata if he has edit rights - if (readOnly) { return; } - if (typeof(lastName) === 'string') { - setName(lastName); - } else { - myData[myID] = { - name: "", - uid: Cryptpad.getUid(), - }; - addToUserData(myData); - onLocal(); - module.$userNameButton.click(); - } - if (isNew) { - Cryptpad.selectTemplate('code', info.realtime, Cryptget); - } - }); - }; - var cursorToPos = function(cursor, oldText) { - var cLine = cursor.line; - var cCh = cursor.ch; - var pos = 0; - var textLines = oldText.split("\n"); - for (var line = 0; line <= cLine; line++) { - if(line < cLine) { - pos += textLines[line].length+1; - } - else if(line === cLine) { - pos += cCh; - } - } - return pos; - }; - - var posToCursor = function(position, newText) { - var cursor = { - line: 0, - ch: 0 - }; - var textLines = newText.substr(0, position).split("\n"); - cursor.line = textLines.length - 1; - cursor.ch = textLines[cursor.line].length; - return cursor; + if (readOnly) { return; } + UserList.getLastName(toolbar.$userNameButton, isNew); }; - var onRemote = config.onRemote = function () { + config.onRemote = function () { if (initializing) { return; } if (isHistoryMode) { return; } - var scroll = editor.getScrollInfo(); - var oldDoc = canonicalize($textarea.val()); + var oldDoc = canonicalize(CodeMirror.$textarea.val()); var shjson = module.realtime.getUserDoc(); // Update the user list (metadata) from the hyperjson - updateMetadata(shjson); + Metadata.update(shjson); var hjson = JSON.parse(shjson); var remoteDoc = hjson.content; var highlightMode = hjson.highlightMode; if (highlightMode && highlightMode !== module.highlightMode) { - setMode(highlightMode, module.$language); - } - - //get old cursor here - var oldCursor = {}; - oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc); - oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc); - - editor.setValue(remoteDoc); - editor.save(); - - var op = TextPatcher.diff(oldDoc, remoteDoc); - var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { - return TextPatcher.transformCursor(oldCursor[attr], op); - }); - - if(selects[0] === selects[1]) { - editor.setCursor(posToCursor(selects[0], remoteDoc)); - } - else { - editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc)); + CodeMirror.setMode(highlightMode); } - editor.scrollTo(scroll.left, scroll.top); + CodeMirror.setValueAndCursor(oldDoc, remoteDoc, TextPatcher); if (!readOnly) { - var textValue = canonicalize($textarea.val()); + var textValue = canonicalize(CodeMirror.$textarea.val()); var shjson2 = stringifyInner(textValue); if (shjson2 !== shjson) { console.error("shjson2 !== shjson"); @@ -736,19 +290,17 @@ define([ module.patchText(shjson2); } } - if (oldDoc !== remoteDoc) { - notify(); - } + if (oldDoc !== remoteDoc) { Cryptpad.notify(); } }; - var onAbort = config.onAbort = function (info) { + config.onAbort = function () { // inform of network disconnect setEditable(false); toolbar.failed(); Cryptpad.alert(Messages.common_connectionLost, undefined, true); }; - var onConnectionChange = config.onConnectionChange = function (info) { + config.onConnectionChange = function (info) { setEditable(info.state); toolbar.failed(); if (info.state) { @@ -760,9 +312,9 @@ define([ } }; - var onError = config.onError = onConnectError; + config.onError = onConnectError; - var realtime = module.realtime = Realtime.start(config); + module.realtime = Realtime.start(config); editor.on('change', onLocal); @@ -772,7 +324,7 @@ define([ var interval = 100; var second = function (CM) { - Cryptpad.ready(function (err, env) { + Cryptpad.ready(function () { andThen(CM); Cryptpad.reportAppUsage(); }); diff --git a/www/common/clipboard.js b/www/common/clipboard.js index 557c1a809..191895dfd 100644 --- a/www/common/clipboard.js +++ b/www/common/clipboard.js @@ -3,7 +3,7 @@ define(['jquery'], function ($) { // copy arbitrary text to the clipboard // return boolean indicating success - var copy = Clipboard.copy = function (text) { + Clipboard.copy = function (text) { var $ta = $('', { type: 'text', }).val(text); diff --git a/www/common/common-codemirror.js b/www/common/common-codemirror.js new file mode 100644 index 000000000..d9dfddaf6 --- /dev/null +++ b/www/common/common-codemirror.js @@ -0,0 +1,299 @@ +define([ + 'jquery', + '/common/modes.js', + '/common/themes.js', + '/bower_components/file-saver/FileSaver.min.js' +], function ($, Modes, Themes) { + var saveAs = window.saveAs; + var module = {}; + + module.create = function (CMeditor, ifrw, Cryptpad) { + var exp = {}; + + var Messages = Cryptpad.Messages; + + var CodeMirror = exp.CodeMirror = CMeditor; + CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js"; + + var $pad = $('#pad-iframe'); + var $textarea = exp.$textarea = $pad.contents().find('#editor1'); + + var Title; + var onLocal = function () {}; + var $rightside; + exp.init = function (local, title, toolbar) { + if (typeof local === "function") { + onLocal = local; + } + Title = title; + $rightside = toolbar.$rightside; + }; + + var editor = exp.editor = CMeditor.fromTextArea($textarea[0], { + lineNumbers: true, + lineWrapping: true, + autoCloseBrackets: true, + matchBrackets : true, + showTrailingSpace : true, + styleActiveLine : true, + search: true, + highlightSelectionMatches: {showToken: /\w+/}, + extraKeys: {"Shift-Ctrl-R": undefined}, + foldGutter: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + mode: "javascript", + readOnly: true + }); + editor.setValue(Messages.codeInitialState); + + var setMode = exp.setMode = function (mode) { + exp.highlightMode = mode; + if (mode === 'text') { + editor.setOption('mode', 'text'); + return; + } + CMeditor.autoLoadMode(editor, mode); + editor.setOption('mode', mode); + if (exp.$language) { + var name = exp.$language.find('a[data-value="' + mode + '"]').text() || 'Mode'; + exp.$language.setValue(name); + } + }; + + var setTheme = exp.setTheme = (function () { + var path = '/common/theme/'; + + var $head = $(ifrw.document.head); + + var themeLoaded = exp.themeLoaded = function (theme) { + return $head.find('link[href*="'+theme+'"]').length; + }; + + var loadTheme = exp.loadTheme = function (theme) { + $head.append($('', { + rel: 'stylesheet', + href: path + theme + '.css', + })); + }; + + return function (theme, $select) { + if (!theme) { + editor.setOption('theme', 'default'); + } else { + if (!themeLoaded(theme)) { + loadTheme(theme); + } + editor.setOption('theme', theme); + } + if ($select) { + $select.setValue(theme || 'Theme'); + } + }; + }()); + + exp.getHeadingText = function () { + var lines = editor.getValue().split(/\n/); + + var text = ''; + lines.some(function (line) { + // lisps? + var lispy = /^\s*(;|#\|)(.*?)$/; + if (lispy.test(line)) { + line.replace(lispy, function (a, one, two) { + text = two; + }); + return true; + } + + // lines beginning with a hash are potentially valuable + // works for markdown, python, bash, etc. + var hash = /^#(.*?)$/; + if (hash.test(line)) { + line.replace(hash, function (a, one) { + text = one; + }); + return true; + } + + // lines including a c-style comment are also valuable + var clike = /^\s*(\/\*|\/\/)(.*)?(\*\/)*$/; + if (clike.test(line)) { + line.replace(clike, function (a, one, two) { + if (!(two && two.replace)) { return; } + text = two.replace(/\*\/\s*$/, '').trim(); + }); + return true; + } + + // TODO make one more pass for multiline comments + }); + + return text.trim(); + }; + + exp.configureLanguage = function (cb) { + var options = []; + Modes.list.forEach(function (l) { + options.push({ + tag: 'a', + attributes: { + 'data-value': l.mode, + 'href': '#', + }, + content: l.language // Pretty name of the language value + }); + }); + var dropdownConfig = { + text: 'Mode', // Button initial text + options: options, // Entries displayed in the menu + left: true, // Open to the left of the button + isSelect: true, + }; + console.log('here'); + var $block = exp.$language = Cryptpad.createDropdown(dropdownConfig); + console.log(exp); + $block.find('a').click(function () { + setMode($(this).attr('data-value'), $block); + onLocal(); + }); + + if ($rightside) { $rightside.append($block); } + cb(); + }; + + exp.configureTheme = function () { + /* Remember the user's last choice of theme using localStorage */ + var themeKey = 'CRYPTPAD_CODE_THEME'; + var lastTheme = localStorage.getItem(themeKey) || 'default'; + + var options = []; + Themes.forEach(function (l) { + options.push({ + tag: 'a', + attributes: { + 'data-value': l.name, + 'href': '#', + }, + content: l.name // Pretty name of the language value + }); + }); + var dropdownConfig = { + text: 'Theme', // Button initial text + options: options, // Entries displayed in the menu + left: true, // Open to the left of the button + isSelect: true, + initialValue: lastTheme + }; + var $block = exp.$theme = Cryptpad.createDropdown(dropdownConfig); + + setTheme(lastTheme, $block); + + $block.find('a').click(function () { + var theme = $(this).attr('data-value'); + setTheme(theme, $block); + localStorage.setItem(themeKey, theme); + }); + + if ($rightside) { $rightside.append($block); } + }; + + exp.exportText = function () { + var text = editor.getValue(); + + var ext = Modes.extensionOf(exp.highlightMode); + + var title = Cryptpad.fixFileName(Title ? Title.suggestTitle('cryptpad') : "?") + (ext || '.txt'); + + Cryptpad.prompt(Messages.exportPrompt, title, function (filename) { + if (filename === null) { return; } + var blob = new Blob([text], { + type: 'text/plain;charset=utf-8' + }); + saveAs(blob, filename); + }); + }; + exp.importText = function (content, file) { + var $bar = ifrw.$('#cme_toolbox'); + var mode; + var mime = CodeMirror.findModeByMIME(file.type); + + if (!mime) { + var ext = /.+\.([^.]+)$/.exec(file.name); + if (ext[1]) { + mode = CMeditor.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 cursorToPos = function(cursor, oldText) { + var cLine = cursor.line; + var cCh = cursor.ch; + var pos = 0; + var textLines = oldText.split("\n"); + for (var line = 0; line <= cLine; line++) { + if(line < cLine) { + pos += textLines[line].length+1; + } + else if(line === cLine) { + pos += cCh; + } + } + return pos; + }; + + var posToCursor = function(position, newText) { + var cursor = { + line: 0, + ch: 0 + }; + var textLines = newText.substr(0, position).split("\n"); + cursor.line = textLines.length - 1; + cursor.ch = textLines[cursor.line].length; + return cursor; + }; + + exp.setValueAndCursor = function (oldDoc, remoteDoc, TextPatcher) { + var scroll = editor.getScrollInfo(); + //get old cursor here + var oldCursor = {}; + oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc); + oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc); + + editor.setValue(remoteDoc); + editor.save(); + + var op = TextPatcher.diff(oldDoc, remoteDoc); + var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { + return TextPatcher.transformCursor(oldCursor[attr], op); + }); + + if(selects[0] === selects[1]) { + editor.setCursor(posToCursor(selects[0], remoteDoc)); + } + else { + editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc)); + } + + editor.scrollTo(scroll.left, scroll.top); + }; + + return exp; + }; + + return module; +}); + diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 40fe6bc7b..a2e96e2fc 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -23,13 +23,16 @@ define([ return chanKey + keys; } if (!keys.editKeyStr) { return; } - return '/1/edit/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.editKeyStr); + return '/1/edit/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.editKeyStr)+'/'; }; var getViewHashFromKeys = Hash.getViewHashFromKeys = function (chanKey, keys) { if (typeof keys === 'string') { return; } - return '/1/view/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.viewKeyStr); + return '/1/view/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.viewKeyStr)+'/'; + }; + var getFileHashFromKeys = Hash.getFileHashFromKeys = function (fileKey, cryptKey) { + return '/2/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(cryptKey) + '/'; }; var parsePadUrl = Hash.parsePadUrl = function (href) { @@ -38,6 +41,7 @@ define([ var ret = {}; if (!href) { return ret; } + if (href.slice(-1) !== '/') { href += '/'; } if (!/^https*:\/\//.test(href)) { var idx = href.indexOf('/#'); @@ -46,7 +50,7 @@ define([ return ret; } - var hash = href.replace(patt, function (a, domain, type, hash) { + var hash = href.replace(patt, function (a, domain, type) { ret.domain = domain; ret.type = type; return ''; @@ -62,12 +66,16 @@ define([ return '/' + parsed.type + '/#' + parsed.hash; }; + var fixDuplicateSlashes = function (s) { + return s.replace(/\/+/g, '/'); + }; + /* * Returns all needed keys for a realtime channel * - no argument: use the URL hash or create one if it doesn't exist * - secretHash provided: use secretHash to find the keys */ - var getSecrets = Hash.getSecrets = function (secretHash) { + Hash.getSecrets = function (secretHash) { var secret = {}; var generate = function () { secret.keys = Crypto.createEditCryptor(); @@ -91,7 +99,7 @@ define([ } else { // New hash - var hashArray = hash.split('/'); + var hashArray = fixDuplicateSlashes(hash).split('/'); if (hashArray.length < 4) { Hash.alert("Unable to parse the key"); throw new Error("Unable to parse the key"); @@ -119,14 +127,15 @@ define([ } } else if (version === "2") { // version 2 hashes are to be used for encrypted blobs - // TODO + secret.channel = hashArray[2].replace(/-/g, '/'); + secret.keys = { fileKeyStr: hashArray[3].replace(/-/g, '/') }; } } } return secret; }; - var getHashes = Hash.getHashes = function (channel, secret) { + Hash.getHashes = function (channel, secret) { var hashes = {}; if (secret.keys.editKeyStr) { hashes.editHash = getEditHashFromKeys(channel, secret.keys); @@ -134,6 +143,9 @@ define([ if (secret.keys.viewKeyStr) { hashes.viewHash = getViewHashFromKeys(channel, secret.keys); } + if (secret.keys.fileKeyStr) { + hashes.fileHash = getFileHashFromKeys(channel, secret.keys.fileKeyStr); + } return hashes; }; @@ -145,12 +157,12 @@ define([ return id; }; - var createRandomHash = Hash.createRandomHash = function () { + Hash.createRandomHash = function () { // 16 byte channel Id var channelId = Util.hexToBase64(createChannelId()); // 18 byte encryption key var key = Crypto.b64RemoveSlashes(Crypto.rand64(18)); - return '/1/edit/' + [channelId, key].join('/'); + return '/1/edit/' + [channelId, key].join('/') + '/'; }; /* @@ -159,8 +171,8 @@ Version 0 Version 1 /code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI Version 2 - /file//#/2// - /file//#/2/ajExFODrFH4lVBwxxsrOKw/pdf + /file/#/2/// + /file/#/2/K6xWU-LT9BJHCQcDCT-DcQ/ajExFODrFH4lVBwxxsrOKw/image-png */ var parseHash = Hash.parseHash = function (hash) { var parsed = {}; @@ -171,20 +183,26 @@ Version 2 parsed.version = 0; return parsed; } - var hashArr = hash.split('/'); + var hashArr = fixDuplicateSlashes(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'; + parsed.present = typeof(hashArr[5]) === "string" && hashArr[5] === 'present'; + return parsed; + } + if (hashArr[1] && hashArr[1] === '2') { + parsed.version = 2; + parsed.channel = hashArr[2].replace(/-/g, '/'); + parsed.key = hashArr[3].replace(/-/g, '/'); return parsed; } return; }; // STORAGE - var findWeaker = Hash.findWeaker = function (href, recents) { + Hash.findWeaker = function (href, recents) { var rHref = href || getRelativeHref(window.location.href); var parsed = parsePadUrl(rHref); if (!parsed.hash) { return false; } @@ -228,11 +246,11 @@ Version 2 }); return stronger; }; - var isNotStrongestStored = Hash.isNotStrongestStored = function (href, recents) { + Hash.isNotStrongestStored = function (href, recents) { return findStronger(href, recents); }; - var hrefToHexChannelId = Hash.hrefToHexChannelId = function (href) { + Hash.hrefToHexChannelId = function (href) { var parsed = Hash.parsePadUrl(href); if (!parsed || !parsed.hash) { return; } @@ -240,7 +258,7 @@ Version 2 if (parsed.version === 0) { return parsed.channel; - } else if (parsed.version !== 1) { + } else if (parsed.version !== 1 && parsed.version !== 2) { console.error("parsed href had no version"); console.error(parsed); return; @@ -253,5 +271,14 @@ Version 2 return hex; }; + Hash.getBlobPathFromHex = function (id) { + return '/blob/' + id.slice(0,2) + '/' + id; + }; + + Hash.serializeHash = function (hash) { + if (hash && hash.slice(-1) !== "/") { hash += "/"; } + return hash; + }; + return Hash; }); diff --git a/www/common/common-history.js b/www/common/common-history.js index e006210a6..8597598c1 100644 --- a/www/common/common-history.js +++ b/www/common/common-history.js @@ -18,13 +18,13 @@ define([ return states; }; - var loadHistory = function (common, cb) { + var loadHistory = function (config, common, cb) { var network = common.getNetwork(); var hkn = network.historyKeeper; - var wcId = common.hrefToHexChannelId(window.location.href); + var wcId = common.hrefToHexChannelId(config.href || window.location.href); - var createRealtime = function(chan) { + var createRealtime = function () { return ChainPad.create({ userName: 'history', initialState: '', @@ -35,7 +35,8 @@ define([ }; var realtime = createRealtime(); - var secret = common.getSecrets(); + var hash = config.href ? common.parsePadUrl(config.href).hash : undefined; + var secret = common.getSecrets(hash); var crypto = Crypto.createEncryptor(secret.keys); var to = window.setTimeout(function () { @@ -66,21 +67,44 @@ define([ } }; - network.on('message', function (msg, sender) { + network.on('message', function (msg) { onMsg(msg); }); network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', wcId, secret.keys.validateKey])); }; - var create = History.create = function (common, config) { + History.create = function (common, config) { if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");} + if (History.loading) { return void console.error("History is already being loaded..."); } + History.loading = true; var $toolbar = config.$toolbar; - var noFunc = function () {}; - var render = config.onRender || noFunc; - var onClose = config.onClose || noFunc; - var onRevert = config.onRevert || noFunc; - var onReady = config.onReady || noFunc; + + if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) { + throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory"); + } + + // config.setHistory(bool, bool) + // - bool1: history value + // - bool2: reset old content? + var render = function (val) { + if (typeof val === "undefined") { return; } + try { + config.applyVal(val); + } catch (e) { + // Probably a parse error + console.error(e); + } + }; + var onClose = function () { config.setHistory(false, true); }; + var onRevert = function () { + config.setHistory(false, false); + config.onLocal(); + config.onRemote(); + }; + var onReady = function () { + config.setHistory(true); + }; var Messages = common.Messages; @@ -112,9 +136,9 @@ define([ var val = states[i].getContent().doc; c = i; if (typeof onUpdate === "function") { onUpdate(); } - $hist.find('.next, .previous').show(); - if (c === states.length - 1) { $hist.find('.next').hide(); } - if (c === 0) { $hist.find('.previous').hide(); } + $hist.find('.next, .previous').css('visibility', ''); + if (c === states.length - 1) { $hist.find('.next').css('visibility', 'hidden'); } + if (c === 0) { $hist.find('.previous').css('visibility', 'hidden'); } return val || ''; }; @@ -132,15 +156,16 @@ define([ $right.hide(); $cke.hide(); var $prev =$('