Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging

pull/1/head
Caleb James DeLisle 8 years ago
commit b2b1f08d01

2
.gitignore vendored

@ -12,3 +12,5 @@ www/scratch
data data
npm-debug.log npm-debug.log
pins/ pins/
blob/
privileged.conf

@ -9,4 +9,7 @@ server.js
NetFluxWebsocketSrv.js NetFluxWebsocketSrv.js
NetFluxWebsocketServer.js NetFluxWebsocketServer.js
WebRTCSrv.js WebRTCSrv.js
www/common/media-tag.js
www/scratch
www/common/toolbar.js

@ -10,7 +10,7 @@
"notypeof": true, "notypeof": true,
"shadow": false, "shadow": false,
"undef": true, "undef": true,
"unused": false, "unused": true,
"futurehostile":true, "futurehostile":true,
"browser": true, "browser": true,
"predef": [ "predef": [

@ -39,10 +39,10 @@ module.exports = {
if you are deploying to production, you'll probably want to remove if you are deploying to production, you'll probably want to remove
the ws://* directive, and change '*' to your domain the ws://* directive, and change '*' to your domain
*/ */
"connect-src 'self' ws://* wss://*", "connect-src 'self' ws: wss:",
// data: is used by codemirror // data: is used by codemirror
"img-src 'self' data:", "img-src 'self' data: blob:",
].join('; '), ].join('; '),
// CKEditor requires significantly more lax content security policy in order to function. // CKEditor requires significantly more lax content security policy in order to function.
@ -59,7 +59,7 @@ module.exports = {
"child-src 'self' *", "child-src 'self' *",
// see the comment above in the 'contentSecurity' section // 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 // (insecure remote) images are included by users of the wysiwyg who embed photos in their pads
"img-src *", "img-src *",
@ -141,6 +141,23 @@ module.exports = {
*/ */
filePath: './datastore/', 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 /* Cryptpad's file storage adaptor closes unused files after a configurale
* number of milliseconds (default 30000 (30 seconds)) * number of milliseconds (default 30000 (30 seconds))
*/ */
@ -163,6 +180,31 @@ module.exports = {
*/ */
suppressRPCErrors: false, 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 /* it is recommended that you serve cryptpad over https
* the filepaths below are used to configure your certificates * the filepaths below are used to configure your certificates
*/ */

@ -106,7 +106,7 @@
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li> <li class="title" data-localization="footer_contact"><li>
<li><a href="http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7" target="_blank" rel="noopener noreferrer">IRC</a></li> <li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li> <li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li> <li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li> <li><a href="/contact.html">Email</a></li>
@ -114,7 +114,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div> <div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer> </footer>
</body> </body>

@ -37,5 +37,20 @@ define(function() {
config.enableHistory = true; 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; return config;
}); });

@ -103,7 +103,7 @@
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li> <li class="title" data-localization="footer_contact"><li>
<li><a href="http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7" target="_blank" rel="noopener noreferrer">IRC</a></li> <li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li> <li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li> <li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li> <li><a href="/contact.html">Email</a></li>
@ -111,7 +111,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div> <div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer> </footer>
</body> </body>

@ -225,7 +225,7 @@
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li> <li class="title" data-localization="footer_contact"><li>
<li><a href="http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7" target="_blank" rel="noopener noreferrer">IRC</a></li> <li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li> <li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li> <li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li> <li><a href="/contact.html">Email</a></li>
@ -233,7 +233,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div> <div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer> </footer>
</body> </body>

@ -388,6 +388,11 @@
right: 0; right: 0;
text-align: center; text-align: center;
} }
@media screen and (max-height: 600px) {
.cp #loadingTip {
display: none;
}
}
.cp #loadingTip span { .cp #loadingTip span {
background-color: #302B28; background-color: #302B28;
color: #fafafa; color: #fafafa;

@ -4,7 +4,7 @@ define([
'/common/cryptpad-common.js' '/common/cryptpad-common.js'
], function ($, Config, Cryptpad) { ], function ($, Config, Cryptpad) {
var APP = window.APP = { window.APP = {
Cryptpad: Cryptpad, Cryptpad: Cryptpad,
}; };
@ -118,68 +118,70 @@ define([
$('button.login').click(); $('button.login').click();
}); });
$('button.login').click(function (e) { $('button.login').click(function () {
Cryptpad.addLoadingScreen(Messages.login_hashing); // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
window.setTimeout(function () { window.setTimeout(function () {
loginReady(function () { Cryptpad.addLoadingScreen(Messages.login_hashing);
var uname = $uname.val(); // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
var passwd = $passwd.val(); window.setTimeout(function () {
Login.loginOrRegister(uname, passwd, false, function (err, result) { loginReady(function () {
if (!err) { var uname = $uname.val();
var proxy = result.proxy; var passwd = $passwd.val();
Login.loginOrRegister(uname, passwd, false, function (err, result) {
// successful validation and user already exists if (!err) {
// set user hash in localStorage and redirect to drive var proxy = result.proxy;
if (proxy && !proxy.login_name) {
proxy.login_name = result.userName; // successful validation and user already exists
} // set user hash in localStorage and redirect to drive
if (proxy && !proxy.login_name) {
proxy.edPrivate = result.edPrivate; proxy.login_name = result.userName;
proxy.edPublic = result.edPublic; }
Cryptpad.whenRealtimeSyncs(result.realtime, function () { proxy.edPrivate = result.edPrivate;
Cryptpad.login(result.userHash, result.userName, function () { proxy.edPublic = result.edPublic;
document.location.href = '/drive/';
}); Cryptpad.whenRealtimeSyncs(result.realtime, function () {
}); Cryptpad.login(result.userHash, result.userName, function () {
return; document.location.href = '/drive/';
} });
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; return;
case 'INVAL_PASS': }
Cryptpad.removeLoadingScreen(function () { switch (err) {
Cryptpad.alert(Messages.login_invalPass); case 'NO_SUCH_USER':
}); Cryptpad.removeLoadingScreen(function () {
break; Cryptpad.alert(Messages.login_noSuchUser);
default: // UNHANDLED ERROR });
Cryptpad.errorLoadingScreen(Messages.login_unhandledError); 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 */ /* End Log in UI */
var addButtonHandlers = function () { var addButtonHandlers = function () {
$('button.register').click(function (e) { $('button.register').click(function () {
var username = $('#name').val(); var username = $('#name').val();
var passwd = $('#password').val(); var passwd = $('#password').val();
var remember = $('#rememberme').is(':checked');
sessionStorage.login_user = username; sessionStorage.login_user = username;
sessionStorage.login_pass = passwd; sessionStorage.login_pass = passwd;
document.location.href = '/register/'; document.location.href = '/register/';
}); });
$('button.gotodrive').click(function (e) { $('button.gotodrive').click(function () {
document.location.href = '/drive/'; document.location.href = '/drive/';
}); });
}; };

@ -112,9 +112,7 @@ define(req, function($, Default, Language) {
if (!selector.length) { return; } if (!selector.length) { return; }
var $button = $(selector).find('button .buttonTitle');
// Select the current language in the list // Select the current language in the list
var option = $(selector).find('[data-value="' + language + '"]');
selector.setValue(language || 'English'); selector.setValue(language || 'English');
// Listen for language change // Listen for language change
@ -137,12 +135,12 @@ define(req, function($, Default, Language) {
var key = $el.data('localization-append'); var key = $el.data('localization-append');
$el.append(messages[key]); $el.append(messages[key]);
}; };
var translateTitle = function (i, e) { var translateTitle = function () {
var $el = $(this); var $el = $(this);
var key = $el.data('localization-title'); var key = $el.data('localization-title');
$el.attr('title', messages[key]); $el.attr('title', messages[key]);
}; };
var translatePlaceholder = function (i, e) { var translatePlaceholder = function () {
var $el = $(this); var $el = $(this);
var key = $el.data('localization-placeholder'); var key = $el.data('localization-placeholder');
$el.attr('placeholder', messages[key]); $el.attr('placeholder', messages[key]);

@ -124,7 +124,7 @@
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li> <li class="title" data-localization="footer_contact"><li>
<li><a href="http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7" target="_blank" rel="noopener noreferrer">IRC</a></li> <li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li> <li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li> <li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li> <li><a href="/contact.html">Email</a></li>
@ -132,7 +132,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div> <div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer> </footer>
</body> </body>

@ -10,7 +10,7 @@
// create an invisible iframe with a given source // create an invisible iframe with a given source
// append it to a parent element // append it to a parent element
// execute a callback when it has loaded // 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'); var iframe = document.createElement('iframe');
timeout = timeout || 10000; timeout = timeout || 10000;
@ -34,7 +34,7 @@
/* given an iframe with an rpc script loaded, create a frame object /* given an iframe with an rpc script loaded, create a frame object
with an asynchronous 'send' method */ with an asynchronous 'send' method */
var open = Frame.open = function (e, A, timeout) { Frame.open = function (e, A, timeout) {
var win = e.contentWindow; var win = e.contentWindow;
var frame = {}; var frame = {};
@ -44,7 +44,7 @@
timeout = timeout || 5000; timeout = timeout || 5000;
var accepts = frame.accepts = function (o) { frame.accepts = function (o) {
return A.some(function (e) { return A.some(function (e) {
switch (typeof(e)) { switch (typeof(e)) {
case 'string': return e === o; case 'string': return e === o;
@ -55,7 +55,7 @@
var changeHandlers = frame.changeHandlers = []; var changeHandlers = frame.changeHandlers = [];
var change = frame.change = function (f) { frame.change = function (f) {
if (typeof(f) !== 'function') { if (typeof(f) !== 'function') {
throw new Error('[Frame.change] expected callback'); throw new Error('[Frame.change] expected callback');
} }
@ -94,7 +94,7 @@
}; };
window.addEventListener('message', _listener); window.addEventListener('message', _listener);
var close = frame.close = function () { frame.close = function () {
window.removeEventListener('message', _listener); window.removeEventListener('message', _listener);
}; };
@ -130,31 +130,31 @@
win.postMessage(JSON.stringify(req), '*'); win.postMessage(JSON.stringify(req), '*');
}; };
var set = frame.set = function (key, val, cb) { frame.set = function (key, val, cb) {
send('set', 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); send('batchset', void 0, map, cb);
}; };
var get = frame.get = function (key, cb) { frame.get = function (key, cb) {
send('get', key, void 0, 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); send('batchget', void 0, keys, cb);
}; };
var remove = frame.remove = function (key, cb) { frame.remove = function (key, cb) {
send('remove', key, void 0, 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); send('batchremove', void 0, keys, cb);
}; };
var keys = frame.keys = function (cb) { frame.keys = function (cb) {
send('keys', void 0, void 0, cb); send('keys', void 0, void 0, cb);
}; };
@ -164,7 +164,7 @@
if (typeof(module) !== 'undefined' && module.exports) { if (typeof(module) !== 'undefined' && module.exports) {
module.exports = Frame; module.exports = Frame;
} else if (typeof(define) === 'function' && define.amd) { } else if (typeof(define) === 'function' && define.amd) {
define(['jquery'], function ($) { define(['jquery'], function () {
return Frame; return Frame;
}); });
} else { } else {

@ -39,7 +39,7 @@ define([
return !keys.some(function (k) { return data[k] !== null; }); 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; } if (handleErr(err)) { return; }
console.log("Created iframe"); console.log("Created iframe");
@ -50,7 +50,7 @@ define([
[function (i) { // test #1 [function (i) { // test #1
var pew = randInt(); var pew = randInt();
frame.set('pew', pew, function (err, data) { frame.set('pew', pew, function (err) {
if (handleErr(err)) { return; } if (handleErr(err)) { return; }
frame.get('pew', function (err, num) { frame.get('pew', function (err, num) {
if (handleErr(err)) { return; } if (handleErr(err)) { return; }
@ -76,9 +76,9 @@ define([
var keys = Object.keys(map); var keys = Object.keys(map);
frame.setBatch(map, function (err, data) { frame.setBatch(map, function (err) {
if (handleErr(err)) { return; } if (handleErr(err)) { return; }
frame.getBatch(keys, function (err, data) { frame.getBatch(keys, function (err) {
if (handleErr(err)) { return; } if (handleErr(err)) { return; }
frame.removeBatch(Object.keys(map), function (err) { frame.removeBatch(Object.keys(map), function (err) {
if (handleErr(err)) { return; } if (handleErr(err)) { return; }

@ -31,7 +31,7 @@
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li> <li class="title" data-localization="footer_contact"><li>
<li><a href="http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7" target="_blank" rel="noopener noreferrer">IRC</a></li> <li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li> <li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li> <li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li> <li><a href="/contact.html">Email</a></li>
@ -39,5 +39,5 @@
</div> </div>
</div> </div>
</div> </div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div> <div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer> </footer>

@ -36,6 +36,9 @@
left: 0; left: 0;
right: 0; right: 0;
text-align: center; text-align: center;
@media screen and (max-height: @media-medium-screen) {
display: none;
}
span { span {
background-color: @bg-loading; background-color: @bg-loading;
color: @color-loading; color: @color-loading;

@ -42,12 +42,13 @@
} }
button { button {
&#shareButton { &#shareButton, &.buttonSuccess {
// Bootstrap 4 colors // Bootstrap 4 colors
color: #fff; color: #fff;
background: @toolbar-green; background: @toolbar-green;
border-color: @toolbar-green; border-color: @toolbar-green;
&:hover { &:hover {
color: #fff;
background: #449d44; background: #449d44;
border: 1px solid #419641; border: 1px solid #419641;
} }
@ -58,12 +59,13 @@
margin-left: 5px; margin-left: 5px;
} }
} }
&#newdoc { &#newdoc, &.buttonPrimary {
// Bootstrap 4 colors // Bootstrap 4 colors
color: #fff; color: #fff;
background: #0275d8; background: #0275d8;
border-color: #0275d8; border-color: #0275d8;
&:hover { &:hover {
color: #fff;
background: #025aa5; background: #025aa5;
border: 1px solid #01549b; border: 1px solid #01549b;
} }
@ -77,26 +79,84 @@
&.hidden { &.hidden {
display: none; 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 { button.upgrade {
box-sizing: content-box; font-size: 14px;
height: 16px; vertical-align: top;
width: 16px; margin-left: 10px;
}
.cryptpad-drive-limit {
display: inline-block; display: inline-block;
padding: 5px; height: 26px;
margin: 3px 0; width: 200px;
div { 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; margin: auto;
font-size: 20px;
} }
} }
.clag () { .clag () {
background: transparent; background: transparent;
} }
#newLag { .cryptpad-lag {
height: 20px; height: 20px;
width: 23px; width: 23px;
background: transparent; background: transparent;
@ -179,17 +239,6 @@
margin-right: 2px; 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 { .cryptpad-state {
line-height: 32px; /* equivalent to 26px + 2*2px margin used for buttons */ line-height: 32px; /* equivalent to 26px + 2*2px margin used for buttons */
} }
@ -378,9 +427,8 @@
.cryptpad-user { .cryptpad-user {
position: absolute; position: absolute;
right: 0; right: 0;
span:not(.cryptpad-lag) { :not(.cryptpad-lag) span {
vertical-align: top; vertical-align: top;
//display: inline-block;
} }
button { button {
span.fa { span.fa {
@ -392,11 +440,9 @@
.cryptpad-toolbar-leftside { .cryptpad-toolbar-leftside {
float: left; float: left;
margin-bottom: -1px; margin-bottom: -1px;
.cryptpad-user-list { .cryptpad-dropdown-users {
//float: right;
pre { pre {
white-space: pre; margin: 5px 0px;
margin: 0;
} }
} }
button { button {
@ -413,14 +459,20 @@
display: none; display: none;
text-align: center; text-align: center;
.next { .next {
float: right; display: inline-block;
vertical-align: middle;
margin: 20px;
} }
.previous { .previous {
float: left; display: inline-block;
vertical-align: middle;
margin: 20px;
} }
.goto { .goto {
display: inline-block; display: inline-block;
input { width: 50px; } vertical-align: middle;
text-align: center;
input { width: 75px; }
} }
.gotoInput { .gotoInput {
vertical-align: middle; vertical-align: middle;
@ -434,7 +486,7 @@
border-radius: 5px; border-radius: 5px;
} }
} }
.cryptpad-spinner { .cryptpad-spinner > span {
height: 16px; height: 16px;
width: 16px; width: 16px;
margin: 8px; margin: 8px;

@ -107,7 +107,7 @@
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li> <li class="title" data-localization="footer_contact"><li>
<li><a href="http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7" target="_blank" rel="noopener noreferrer">IRC</a></li> <li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li> <li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li> <li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li> <li><a href="/contact.html">Email</a></li>
@ -115,7 +115,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div> <div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer> </footer>
</body> </body>

@ -117,51 +117,120 @@
.cryptpad-toolbar a { .cryptpad-toolbar a {
float: right; 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; color: #fff;
background: #5cb85c; background: #5cb85c;
border-color: #5cb85c; border-color: #5cb85c;
} }
.cryptpad-toolbar button#shareButton:hover { .cryptpad-toolbar button#shareButton:hover,
.cryptpad-toolbar button.buttonSuccess:hover {
color: #fff;
background: #449d44; background: #449d44;
border: 1px solid #419641; border: 1px solid #419641;
} }
.cryptpad-toolbar button#shareButton span { .cryptpad-toolbar button#shareButton span,
.cryptpad-toolbar button.buttonSuccess span {
color: #fff; color: #fff;
} }
.cryptpad-toolbar button#shareButton .large { .cryptpad-toolbar button#shareButton .large,
.cryptpad-toolbar button.buttonSuccess .large {
margin-left: 5px; margin-left: 5px;
} }
.cryptpad-toolbar button#newdoc { .cryptpad-toolbar button#newdoc,
.cryptpad-toolbar button.buttonPrimary {
color: #fff; color: #fff;
background: #0275d8; background: #0275d8;
border-color: #0275d8; border-color: #0275d8;
} }
.cryptpad-toolbar button#newdoc:hover { .cryptpad-toolbar button#newdoc:hover,
.cryptpad-toolbar button.buttonPrimary:hover {
color: #fff;
background: #025aa5; background: #025aa5;
border: 1px solid #01549b; border: 1px solid #01549b;
} }
.cryptpad-toolbar button#newdoc span { .cryptpad-toolbar button#newdoc span,
.cryptpad-toolbar button.buttonPrimary span {
color: #fff; color: #fff;
} }
.cryptpad-toolbar button#newdoc .large { .cryptpad-toolbar button#newdoc .large,
.cryptpad-toolbar button.buttonPrimary .large {
margin-left: 5px; margin-left: 5px;
} }
.cryptpad-toolbar button.hidden { .cryptpad-toolbar button.hidden {
display: none; display: none;
} }
.cryptpad-toolbar .cryptpad-lag { .cryptpad-toolbar button:hover {
box-sizing: content-box; color: #292b2c;
height: 16px; background-color: #e6e6e6;
width: 16px; border-color: #adadad;
}
.cryptpad-toolbar button.upgrade {
font-size: 14px;
vertical-align: top;
margin-left: 10px;
}
.cryptpad-toolbar .cryptpad-drive-limit {
display: inline-block; display: inline-block;
padding: 5px; height: 26px;
margin: 3px 0; 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; margin: auto;
font-size: 20px;
} }
.cryptpad-toolbar #newLag { .cryptpad-toolbar .cryptpad-lag {
height: 20px; height: 20px;
width: 23px; width: 23px;
background: transparent; background: transparent;
@ -171,7 +240,7 @@
vertical-align: top; vertical-align: top;
box-sizing: content-box; box-sizing: content-box;
} }
.cryptpad-toolbar #newLag span { .cryptpad-toolbar .cryptpad-lag span {
display: inline-block; display: inline-block;
width: 4px; width: 4px;
margin: 0; margin: 0;
@ -182,50 +251,50 @@
border: 1px solid black; border: 1px solid black;
transition: background 1s, border 1s; transition: background 1s, border 1s;
} }
.cryptpad-toolbar #newLag span:last-child { .cryptpad-toolbar .cryptpad-lag span:last-child {
margin-right: 0; margin-right: 0;
} }
.cryptpad-toolbar #newLag span.bar1 { .cryptpad-toolbar .cryptpad-lag span.bar1 {
height: 5px; height: 5px;
} }
.cryptpad-toolbar #newLag span.bar2 { .cryptpad-toolbar .cryptpad-lag span.bar2 {
height: 10px; height: 10px;
} }
.cryptpad-toolbar #newLag span.bar3 { .cryptpad-toolbar .cryptpad-lag span.bar3 {
height: 15px; height: 15px;
} }
.cryptpad-toolbar #newLag span.bar4 { .cryptpad-toolbar .cryptpad-lag span.bar4 {
height: 20px; height: 20px;
} }
.cryptpad-toolbar #newLag.lag0 span { .cryptpad-toolbar .cryptpad-lag.lag0 span {
background: transparent; background: transparent;
border-color: red; border-color: red;
} }
.cryptpad-toolbar #newLag.lag1 .bar2, .cryptpad-toolbar .cryptpad-lag.lag1 .bar2,
.cryptpad-toolbar #newLag.lag1 .bar3, .cryptpad-toolbar .cryptpad-lag.lag1 .bar3,
.cryptpad-toolbar #newLag.lag1 .bar4 { .cryptpad-toolbar .cryptpad-lag.lag1 .bar4 {
background: transparent; background: transparent;
} }
.cryptpad-toolbar #newLag.lag1 span { .cryptpad-toolbar .cryptpad-lag.lag1 span {
background-color: orange; background-color: orange;
border-color: orange; border-color: orange;
} }
.cryptpad-toolbar #newLag.lag2 .bar3, .cryptpad-toolbar .cryptpad-lag.lag2 .bar3,
.cryptpad-toolbar #newLag.lag2 .bar4 { .cryptpad-toolbar .cryptpad-lag.lag2 .bar4 {
background: transparent; background: transparent;
} }
.cryptpad-toolbar #newLag.lag2 span { .cryptpad-toolbar .cryptpad-lag.lag2 span {
background-color: orange; background-color: orange;
border-color: orange; border-color: orange;
} }
.cryptpad-toolbar #newLag.lag3 .bar4 { .cryptpad-toolbar .cryptpad-lag.lag3 .bar4 {
background: transparent; background: transparent;
} }
.cryptpad-toolbar #newLag.lag3 span { .cryptpad-toolbar .cryptpad-lag.lag3 span {
background-color: #5cb85c; background-color: #5cb85c;
border-color: #5cb85c; border-color: #5cb85c;
} }
.cryptpad-toolbar #newLag.lag4 span { .cryptpad-toolbar .cryptpad-lag.lag4 span {
background-color: #5cb85c; background-color: #5cb85c;
border-color: #5cb85c; border-color: #5cb85c;
} }
@ -250,17 +319,6 @@
margin-top: -3px; margin-top: -3px;
margin-right: 2px; 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 { .cryptpad-toolbar .cryptpad-state {
line-height: 32px; line-height: 32px;
/* equivalent to 26px + 2*2px margin used for buttons */ /* equivalent to 26px + 2*2px margin used for buttons */
@ -449,7 +507,7 @@
position: absolute; position: absolute;
right: 0; right: 0;
} }
.cryptpad-toolbar-top .cryptpad-user span:not(.cryptpad-lag) { .cryptpad-toolbar-top .cryptpad-user :not(.cryptpad-lag) span {
vertical-align: top; vertical-align: top;
} }
.cryptpad-toolbar-top .cryptpad-user button span.fa { .cryptpad-toolbar-top .cryptpad-user button span.fa {
@ -459,9 +517,8 @@
float: left; float: left;
margin-bottom: -1px; margin-bottom: -1px;
} }
.cryptpad-toolbar-leftside .cryptpad-user-list pre { .cryptpad-toolbar-leftside .cryptpad-dropdown-users pre {
white-space: pre; margin: 5px 0px;
margin: 0;
} }
.cryptpad-toolbar-leftside button { .cryptpad-toolbar-leftside button {
margin: 2px 4px 2px 0px; margin: 2px 4px 2px 0px;
@ -477,16 +534,22 @@
text-align: center; text-align: center;
} }
.cryptpad-toolbar-history .next { .cryptpad-toolbar-history .next {
float: right; display: inline-block;
vertical-align: middle;
margin: 20px;
} }
.cryptpad-toolbar-history .previous { .cryptpad-toolbar-history .previous {
float: left; display: inline-block;
vertical-align: middle;
margin: 20px;
} }
.cryptpad-toolbar-history .goto { .cryptpad-toolbar-history .goto {
display: inline-block; display: inline-block;
vertical-align: middle;
text-align: center;
} }
.cryptpad-toolbar-history .goto input { .cryptpad-toolbar-history .goto input {
width: 50px; width: 75px;
} }
.cryptpad-toolbar-history .gotoInput { .cryptpad-toolbar-history .gotoInput {
vertical-align: middle; vertical-align: middle;
@ -497,7 +560,7 @@
padding: 3px 3px; padding: 3px 3px;
border-radius: 5px; border-radius: 5px;
} }
.cryptpad-spinner { .cryptpad-spinner > span {
height: 16px; height: 16px;
width: 16px; width: 16px;
margin: 8px; margin: 8px;

@ -11,6 +11,8 @@ define(function () {
out.type.slide = 'Présentation'; out.type.slide = 'Présentation';
out.type.drive = 'Drive'; out.type.drive = 'Drive';
out.type.whiteboard = "Tableau Blanc"; out.type.whiteboard = "Tableau Blanc";
out.type.file = "Fichier";
out.type.media = "Média";
out.button_newpad = 'Nouveau document texte'; out.button_newpad = 'Nouveau document texte';
out.button_newcode = 'Nouvelle page de code'; out.button_newcode = 'Nouvelle page de code';
@ -49,10 +51,22 @@ define(function () {
out.language = "Langue"; 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.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.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.<br>" +
"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.<br>"+
"Ce pad n'est pas enregistré dans votre CryptDrive.";
out.pinLimitDrive = out.pinLimitReached+ ".<br>" +
"Vous ne pouvez pas créer de nouveaux pads.";
out.importButtonTitle = 'Importer un pad depuis un fichier local'; out.importButtonTitle = 'Importer un pad depuis un fichier local';
out.exportButtonTitle = 'Exporter ce pad vers un fichier local'; out.exportButtonTitle = 'Exporter ce pad vers un fichier local';
@ -93,6 +107,7 @@ define(function () {
out.printDate = "Afficher la date"; out.printDate = "Afficher la date";
out.printTitle = "Afficher le titre du pad"; out.printTitle = "Afficher le titre du pad";
out.printCSS = "Personnaliser l'apparence (CSS):"; out.printCSS = "Personnaliser l'apparence (CSS):";
out.printTransition = "Activer les animations de transition";
out.slideOptionsTitle = "Personnaliser la présentation"; out.slideOptionsTitle = "Personnaliser la présentation";
out.slideOptionsButton = "Enregistrer (Entrée)"; out.slideOptionsButton = "Enregistrer (Entrée)";
@ -125,6 +140,7 @@ define(function () {
out.history_restoreTitle = "Restaurer la version du document sélectionnée"; 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_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_restoreDone = "Document restauré";
out.history_version = "Version :";
// Polls // Polls
@ -313,6 +329,10 @@ define(function () {
out.settings_pinningError = "Un problème est survenu"; out.settings_pinningError = "Un problème est survenu";
out.settings_usageAmount = "Vos pads épinglés occupent {0} Mo"; 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 // index.html
//about.html //about.html

@ -11,6 +11,8 @@ define(function () {
out.type.slide = 'Presentation'; out.type.slide = 'Presentation';
out.type.drive = 'Drive'; out.type.drive = 'Drive';
out.type.whiteboard = 'Whiteboard'; out.type.whiteboard = 'Whiteboard';
out.type.file = 'File';
out.type.media = 'Media';
out.button_newpad = 'New Rich Text pad'; out.button_newpad = 'New Rich Text pad';
out.button_newcode = 'New Code pad'; out.button_newcode = 'New Code pad';
@ -51,10 +53,22 @@ define(function () {
out.language = "Language"; 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.greenLight = "Everything is working fine";
out.orangeLight = "Your slow connection may impact your experience"; out.orangeLight = "Your slow connection may impact your experience";
out.redLight = "You are disconnected from the session"; 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.<br>" +
"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.<br>"+
"This pad is not stored in your CryptDrive.";
out.pinLimitDrive = "You've reached your storage limit.<br>" +
"You can't create new pads.";
out.importButtonTitle = 'Import a pad from a local file'; out.importButtonTitle = 'Import a pad from a local file';
out.exportButtonTitle = 'Export this pad to a local file'; out.exportButtonTitle = 'Export this pad to a local file';
@ -95,6 +109,7 @@ define(function () {
out.printDate = "Display the date"; out.printDate = "Display the date";
out.printTitle = "Display the pad title"; out.printTitle = "Display the pad title";
out.printCSS = "Custom style rules (CSS):"; out.printCSS = "Custom style rules (CSS):";
out.printTransition = "Enable transition animations";
out.slideOptionsTitle = "Customize your slides"; out.slideOptionsTitle = "Customize your slides";
out.slideOptionsButton = "Save (enter)"; out.slideOptionsButton = "Save (enter)";
@ -127,6 +142,7 @@ define(function () {
out.history_restoreTitle = "Restore the selected version of the document"; 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_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_restoreDone = "Document restored";
out.history_version = "Version:";
// Polls // Polls
@ -318,6 +334,10 @@ define(function () {
out.settings_pinningError = "Something went wrong"; out.settings_pinningError = "Something went wrong";
out.settings_usageAmount = "Your pinned pads occupy {0}MB"; 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 // index.html

@ -32,7 +32,7 @@ server {
set $scriptSrc "'self'"; set $scriptSrc "'self'";
set $connectSrc "'self' wss://cryptpad.fr wss://api.cryptpad.fr"; set $connectSrc "'self' wss://cryptpad.fr wss://api.cryptpad.fr";
set $fontSrc "'self'"; set $fontSrc "'self'";
set $imgSrc "data: *"; set $imgSrc "data: * blob:";
set $frameSrc "'self' beta.cryptpad.fr"; set $frameSrc "'self' beta.cryptpad.fr";
if ($uri = /pad/inner.html) { if ($uri = /pad/inner.html) {
@ -65,8 +65,12 @@ server {
rewrite ^.*$ /customize/api/config break; rewrite ^.*$ /customize/api/config break;
} }
location ^~ /blob/ {
try_files $uri =404;
}
## TODO fix in the code so that we don't need this ## 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; rewrite ^(.*)$ $1/ redirect;
} }

@ -1,7 +1,7 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server", "description": "realtime collaborative visual editor with zero knowlege server",
"version": "1.5.0", "version": "1.6.0",
"dependencies": { "dependencies": {
"chainpad-server": "^1.0.1", "chainpad-server": "^1.0.1",
"express": "~4.10.1", "express": "~4.10.1",

@ -54,6 +54,9 @@ These settings can be found in your configuration file in the `contentSecurity`
## Maintenance ## 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: To get access to the most recent codebase:
``` ```
@ -70,9 +73,12 @@ bower update;
# serverside dependencies # serverside dependencies
npm update; npm update;
``` ```
## Deleting all data and resetting Cryptpad
To reset your instance of Cryptpad and remove all the data that is being stored: 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 # change into your cryptpad directory
cd /your/cryptpad/instance/location; cd /your/cryptpad/instance/location;

339
rpc.js

@ -2,12 +2,44 @@
/* Use Nacl for checking signatures of messages */ /* Use Nacl for checking signatures of messages */
var Nacl = require("tweetnacl"); var Nacl = require("tweetnacl");
/* globals Buffer*/
/* globals process */
var Fs = require("fs");
var Path = require("path");
var RPC = module.exports; var RPC = module.exports;
var Store = require("./storage/file"); var Store = require("./storage/file");
var isValidChannel = function (chan) { 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 () { var makeToken = function () {
@ -21,7 +53,7 @@ var makeCookie = function (token) {
return [ return [
time, time,
process.pid, // jshint ignore:line process.pid,
token token
]; ];
}; };
@ -59,7 +91,11 @@ var isTooOld = function (time, now) {
var expireSessions = function (Sessions) { var expireSessions = function (Sessions) {
var now = +new Date(); var now = +new Date();
Object.keys(Sessions).forEach(function (key) { Object.keys(Sessions).forEach(function (key) {
var session = Sessions[key];
if (isTooOld(Sessions[key].atime, now)) { if (isTooOld(Sessions[key].atime, now)) {
if (session.blobstage) {
session.blobstage.close();
}
delete Sessions[key]; delete Sessions[key];
} }
}); });
@ -86,7 +122,7 @@ var isValidCookie = function (Sessions, publicKey, cookie) {
} }
// different process. try harder // different process. try harder
if (process.pid !== parsed.pid) { // jshint ignore:line if (process.pid !== parsed.pid) {
return false; return false;
} }
@ -96,7 +132,6 @@ var isValidCookie = function (Sessions, publicKey, cookie) {
var idx = user.tokens.indexOf(parsed.seq); var idx = user.tokens.indexOf(parsed.seq);
if (idx === -1) { return false; } if (idx === -1) { return false; }
var next;
if (idx > 0) { if (idx > 0) {
// make a new token // make a new token
addTokenForKey(Sessions, publicKey, makeToken()); 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) { var getFileSize = function (store, channel, cb) {
if (!isValidChannel(channel)) { return void cb('INVALID_CHAN'); } 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) { // 'channel' refers to a file, so you need anoter API
if (e) { return void cb(e.code); } getUploadSize(null, channel, function (e, size) {
if (e) { return void cb(e); }
cb(void 0, size); 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); store.message(publicKey, JSON.stringify(msg), cb);
}; }; */
// TODO check if new pinned size exceeds user quota
var pinChannel = function (store, Sessions, publicKey, channels, cb) { var pinChannel = function (store, Sessions, publicKey, channels, cb) {
if (!channels && channels.filter) { if (!channels && channels.filter) {
// expected array // expected array
@ -349,8 +403,7 @@ var unpinChannel = function (store, Sessions, publicKey, channels, cb) {
function (e) { function (e) {
if (e) { return void cb(e); } if (e) { return void cb(e); }
toStore.forEach(function (channel) { toStore.forEach(function (channel) {
// TODO actually delete delete session.channels[channel];
session.channels[channel] = false;
}); });
getHash(store, Sessions, publicKey, cb); 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 resetUserPins = function (store, Sessions, publicKey, channelList, cb) {
var session = beginSession(Sessions, publicKey); 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');*/ /*::const ConfigType = require('./config.example.js');*/
RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) { RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) {
// load pin-store... // load pin-store...
console.log('loading rpc module...'); console.log('loading rpc module...');
var Sessions = {}; 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 store;
var rpc = function ( var rpc = function (
@ -428,7 +659,6 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)
return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY'); return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY');
} }
if (checkSignature(serialized, signature, publicKey) !== true) { if (checkSignature(serialized, signature, publicKey) !== true) {
return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY"); 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 Respond = function (e, msg) {
var token = Sessions[publicKey].tokens.slice(-1)[0]; var token = Sessions[publicKey].tokens.slice(-1)[0];
var cookie = makeCookie(token).join('|'); var cookie = makeCookie(token).join('|');
respond(e, [cookie].concat(msg||[])); respond(e, [cookie].concat(typeof(msg) !== 'undefined' ?msg: []));
}; };
if (typeof(msg) !== 'object' || !msg.length) { if (typeof(msg) !== 'object' || !msg.length) {
return void Respond('INVALID_MSG'); return void Respond('INVALID_MSG');
} }
var deny = function () {
Respond('E_ACCESS_DENIED');
};
var handleMessage = function (privileged) {
switch (msg[0]) { switch (msg[0]) {
case 'COOKIE': return void Respond(void 0); case 'COOKIE': return void Respond(void 0);
case 'RESET': case 'RESET':
return resetUserPins(store, Sessions, safeKey, msg[1], function (e, hash) { return resetUserPins(store, Sessions, safeKey, msg[1], function (e, hash) {
return void Respond(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) { return pinChannel(store, Sessions, safeKey, msg[1], function (e, hash) {
Respond(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) { return void getHash(store, Sessions, safeKey, function (e, hash) {
Respond(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) { return getTotalSize(store, ctx.store, Sessions, safeKey, function (e, size) {
if (e) { return void Respond(e); } if (e) { return void Respond(e); }
Respond(e, size); Respond(e, size);
}); });
case 'GET_FILE_SIZE': case 'GET_FILE_SIZE':
return void getFileSize(ctx.store, msg[1], Respond); 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': case 'GET_MULTIPLE_FILE_SIZE':
return void getMultipleFileSize(ctx.store, msg[1], function (e, dict) { return void getMultipleFileSize(ctx.store, msg[1], function (e, dict) {
if (e) { return void Respond(e); } if (e) { return void Respond(e); }
Respond(void 0, dict); 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: default:
return void Respond('UNSUPPORTED_RPC_CALL', msg); 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({ Store.create({
filePath: './pins' filePath: pinPath,
}, function (s) { }, function (s) {
store = s; store = s;
cb(void 0, rpc);
// expire old sessions once per minute safeMkdir(blobPath, function (e) {
setInterval(function () { if (e) { throw e; }
expireSessions(Sessions); safeMkdir(blobStagingPath, function (e) {
}, 60000); if (e) { throw e; }
cb(void 0, rpc);
// expire old sessions once per minute
setInterval(function () {
expireSessions(Sessions);
}, 60000);
});
});
}); });
}; };

@ -82,6 +82,8 @@ var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'cont
var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
app.get(mainPagePattern, Express.static(__dirname + '/customize.dist')); 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'));
app.use("/customize", Express.static(__dirname + '/customize.dist')); app.use("/customize", Express.static(__dirname + '/customize.dist'));
app.use(/^\/[^\/]*$/, Express.static('customize')); app.use(/^\/[^\/]*$/, Express.static('customize'));

@ -28,7 +28,8 @@ var readMessages = function (path, msgHandler, cb) {
}; };
var checkPath = function (path, callback) { 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) { if (!err) {
callback(undefined, true); callback(undefined, true);
return; return;
@ -166,7 +167,7 @@ var getChannel = function (env, id, callback) {
}); });
} }
}); });
}).nThen(function (waitFor) { }).nThen(function () {
if (errorState) { return; } if (errorState) { return; }
complete(); complete();
}); });

@ -15,31 +15,43 @@ define([
var failMessages = []; var failMessages = [];
var ASSERTS = []; 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) { ASSERTS.forEach(function (f, index) {
f(index); f(function (err) {
done(err, index);
}, index);
}); });
}; };
var assert = function (test, msg) { var assert = function (test, msg) {
ASSERTS.push(function (i) { ASSERTS.push(function (cb, i) {
var returned = test(); test(function (result) {
if (returned === true) { if (result === true) {
assertions++; assertions++;
} else { cb();
failed = true; } else {
failedOn = assertions; failed = true;
failMessages.push({ failedOn = assertions;
test: i, cb({
message: msg, test: i,
output: returned, message: msg,
}); output: result,
} });
}
});
}); });
}; };
var $body = $('body');
var HJSON_list = [ var HJSON_list = [
'["DIV",{"id":"target"},[["P",{"class":" alice bob charlie has.dot","id":"bang"},["pewpewpew"]]]]', '["DIV",{"id":"target"},[["P",{"class":" alice bob charlie has.dot","id":"bang"},["pewpewpew"]]]]',
@ -60,7 +72,7 @@ define([
}; };
var HJSON_equal = function (shjson) { var HJSON_equal = function (shjson) {
assert(function () { assert(function (cb) {
// parse your stringified Hyperjson // parse your stringified Hyperjson
var hjson; var hjson;
@ -84,10 +96,10 @@ define([
var diff = TextPatcher.format(shjson, op); var diff = TextPatcher.format(shjson, op);
if (success) { if (success) {
return true; return cb(true);
} else { } else {
return '<br><br>insert: ' + diff.insert + '<br><br>' + return cb('<br><br>insert: ' + diff.insert + '<br><br>' +
'remove: ' + diff.remove + '<br><br>'; 'remove: ' + diff.remove + '<br><br>');
} }
}, "expected hyperjson equality"); }, "expected hyperjson equality");
}; };
@ -96,7 +108,7 @@ define([
var roundTrip = function (sel) { var roundTrip = function (sel) {
var target = $(sel)[0]; var target = $(sel)[0];
assert(function () { assert(function (cb) {
var hjson = Hyperjson.fromDOM(target); var hjson = Hyperjson.fromDOM(target);
var cloned = Hyperjson.toDOM(hjson); var cloned = Hyperjson.toDOM(hjson);
var success = cloned.outerHTML === target.outerHTML; var success = cloned.outerHTML === target.outerHTML;
@ -113,7 +125,7 @@ define([
TextPatcher.log(target.outerHTML, op); TextPatcher.log(target.outerHTML, op);
} }
return success; return cb(success);
}, "Round trip serialization introduced artifacts."); }, "Round trip serialization introduced artifacts.");
}; };
@ -127,9 +139,9 @@ define([
var strungJSON = function (orig) { var strungJSON = function (orig) {
var result; var result;
assert(function () { assert(function (cb) {
result = JSON.stringify(JSON.parse(orig)); result = JSON.stringify(JSON.parse(orig));
return result === orig; return cb(result === orig);
}, "expected result (" + result + ") to equal original (" + orig + ")"); }, "expected result (" + result + ") to equal original (" + orig + ")");
}; };
@ -139,6 +151,59 @@ define([
strungJSON(orig); 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) { var swap = function (str, dict) {
return str.replace(/\{\{(.*?)\}\}/g, function (all, key) { return str.replace(/\{\{(.*?)\}\}/g, function (all, key) {
return typeof dict[key] !== 'undefined'? dict[key] : all; return typeof dict[key] !== 'undefined'? dict[key] : all;
@ -153,7 +218,7 @@ define([
return str || ''; return str || '';
}; };
var formatFailures = function () { var formatFailures = function () {
var template = multiline(function () { /* var template = multiline(function () { /*
<p class="error"> <p class="error">
Failed on test number {{test}} with error message: Failed on test number {{test}} with error message:
@ -174,16 +239,15 @@ The test returned:
}).join("\n"); }).join("\n");
}; };
runASSERTS(); runASSERTS(function () {
$("body").html(function (i, val) {
$("body").html(function (i, val) { var dict = {
var dict = { previous: val,
previous: val, totalAssertions: ASSERTS.length,
totalAssertions: ASSERTS.length, passedAssertions: assertions,
passedAssertions: assertions, plural: (assertions === 1? '' : 's'),
plural: (assertions === 1? '' : 's'), failMessages: formatFailures()
failMessages: formatFailures() };
};
var SUCCESS = swap(multiline(function(){/* var SUCCESS = swap(multiline(function(){/*
<div class="report">{{passedAssertions}} / {{totalAssertions}} test{{plural}} passed. <div class="report">{{passedAssertions}} / {{totalAssertions}} test{{plural}} passed.
@ -196,12 +260,13 @@ The test returned:
{{previous}} {{previous}}
*/}), dict); */}), dict);
var report = SUCCESS; var report = SUCCESS;
return report; return report;
}); });
var $report = $('.report'); var $report = $('.report');
$report.addClass(failed?'failure':'success'); $report.addClass(failed?'failure':'success');
});
}); });

@ -3,18 +3,12 @@ define([
'/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/textpatcher/TextPatcher.js', '/bower_components/textpatcher/TextPatcher.js',
'/common/toolbar.js', '/common/toolbar2.js',
'json.sortify', 'json.sortify',
'/bower_components/chainpad-json-validator/json-ot.js', '/bower_components/chainpad-json-validator/json-ot.js',
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/cryptget.js', '/common/cryptget.js',
'/common/modes.js', ], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget) {
'/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;
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
var module = window.APP = { var module = window.APP = {
@ -30,6 +24,7 @@ define([
}; };
var toolbar; var toolbar;
var editor;
var secret = Cryptpad.getSecrets(); var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr; var readOnly = secret.keys && !secret.keys.editKeyStr;
@ -37,117 +32,26 @@ define([
secret.keys = secret.key; secret.keys = secret.key;
} }
var onConnectError = function (info) { var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError); Cryptpad.errorLoadingScreen(Messages.websocketError);
}; };
var andThen = function (CMeditor) { var andThen = function (CMeditor) {
var CodeMirror = module.CodeMirror = CMeditor; var CodeMirror = Cryptpad.createCodemirror(CMeditor, ifrw, Cryptpad);
CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js"; editor = CodeMirror.editor;
var $pad = $('#pad-iframe');
var $textarea = $pad.contents().find('#editor1');
var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); 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 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($('<link />', {
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) { var setEditable = module.setEditable = function (bool) {
if (readOnly && bool) { return; } if (readOnly && bool) { return; }
editor.setOption('readOnly', !bool); editor.setOption('readOnly', !bool);
}; };
var userData = module.userData = {}; // List of pretty name of all users (mapped with their server ID) var Title;
var userList; // List of users still connected to the channel (server IDs) var UserList;
var addToUserData = function(data) { var Metadata;
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 config = { var config = {
initialState: '{}', initialState: '{}',
@ -157,7 +61,6 @@ define([
validateKey: secret.keys.validateKey || undefined, validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly, readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys), crypto: Crypto.createEncryptor(secret.keys),
setMyID: setMyID,
network: Cryptpad.getNetwork(), network: Cryptpad.getNetwork(),
transformFunction: JsonOT.validate, 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 initializing = true;
var stringifyInner = function (textValue) { var stringifyInner = function (textValue) {
var obj = { var obj = {
content: textValue, content: textValue,
metadata: { metadata: {
users: userData, users: UserList.userData,
defaultTitle: defaultName defaultTitle: Title.defaultTitle
} }
}; };
if (!initializing) { if (!initializing) {
obj.metadata.title = document.title; obj.metadata.title = Title.title;
} }
// set mode too... // set mode too...
obj.highlightMode = module.highlightMode; obj.highlightMode = CodeMirror.highlightMode;
// stringify the json and send it into chainpad // stringify the json and send it into chainpad
return stringify(obj); return stringify(obj);
@ -204,7 +102,7 @@ define([
editor.save(); editor.save();
var textValue = canonicalize($textarea.val()); var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson = stringifyInner(textValue); var shjson = stringifyInner(textValue);
module.patchText(shjson); 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) { config.onInit = function (info) {
defaultName = defaultTitle; UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad);
$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName);
};
var updateMetadata = function(shjson) { var titleCfg = { getHeadingText: CodeMirror.getHeadingText };
// Extract the user list (metadata) from the hyperjson Title = Cryptpad.createTitle(titleCfg, config.onLocal, Cryptpad);
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 onInit = config.onInit = function (info) { Metadata = Cryptpad.createMetadata(UserList, Title);
userList = info.userList;
var configTb = { var configTb = {
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'],
userData: userData, userList: UserList.getToolbarConfig(),
readOnly: readOnly,
ifrw: ifrw,
share: { share: {
secret: secret, secret: secret,
channel: info.channel channel: info.channel
}, },
title: { title: Title.getTitleConfig(),
onRename: renameCb, common: Cryptpad,
defaultName: defaultName, readOnly: readOnly,
suggestName: suggestName ifrw: ifrw,
}, realtime: info.realtime,
common: Cryptpad 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); Title.setToolbar(toolbar);
var $userBlock = $bar.find('.' + Toolbar.constants.username); CodeMirror.init(config.onLocal, Title, toolbar);
var $usernameButton = module.$userNameButton = $($bar.find('.' + Toolbar.constants.changeUsername));
var editHash; var $rightside = toolbar.$rightside;
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
var editHash;
if (!readOnly) { if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
} }
/* add a history button */ /* add a history button */
var histConfig = {}; var histConfig = {
histConfig.onRender = function (val) { onLocal: config.onLocal(),
if (typeof val === "undefined") { return; } onRemote: config.onRemote(),
try { setHistory: setHistory,
var hjson = JSON.parse(val || '{}'); applyVal: function (val) {
var remoteDoc = hjson.content; var remoteDoc = JSON.parse(val || '{}').content;
editor.setValue(remoteDoc || ''); editor.setValue(remoteDoc || '');
editor.save(); editor.save();
} catch (e) { },
// Probably a parse error $toolbar: $bar
console.error(e);
}
};
histConfig.onClose = function () {
// Close button clicked
setHistory(false, true);
};
histConfig.onRevert = function () {
// Revert button clicked
setHistory(false, false);
config.onLocal();
config.onRemote();
};
histConfig.onReady = function () {
// Called when the history is loaded and the UI displayed
setHistory(true);
}; };
histConfig.$toolbar = $bar;
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist); $rightside.append($hist);
@ -447,133 +169,42 @@ define([
var templateObj = { var templateObj = {
rt: info.realtime, rt: info.realtime,
Crypt: Cryptget, Crypt: Cryptget,
getTitle: function () { return document.title; } getTitle: Title.getTitle
}; };
var $templateButton = Cryptpad.createButton('template', true, templateObj); var $templateButton = Cryptpad.createButton('template', true, templateObj);
$rightside.append($templateButton); $rightside.append($templateButton);
} }
/* add an export button */ /* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportText); var $export = Cryptpad.createButton('export', true, {}, CodeMirror.exportText);
$rightside.append($export); $rightside.append($export);
if (!readOnly) { if (!readOnly) {
/* add an import button */ /* add an import button */
var $import = Cryptpad.createButton('import', true, {}, importText); var $import = Cryptpad.createButton('import', true, {}, CodeMirror.importText);
$rightside.append($import); $rightside.append($import);
/* add a rename button */
//var $setTitle = Cryptpad.createButton('rename', true, {suggestName: suggestName}, renameCb);
//$rightside.append($setTitle);
} }
/* add a forget button */ /* add a forget button */
var forgetCb = function (err, title) { var forgetCb = function (err) {
if (err) { return; } if (err) { return; }
setEditable(false); setEditable(false);
}; };
var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb);
$rightside.append($forgetPad); $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) { if (!readOnly) {
configureLanguage(function () { CodeMirror.configureLanguage(CodeMirror.configureTheme);
configureTheme();
});
} }
else { else {
configureTheme(); CodeMirror.configureTheme();
} }
// set the hash // set the hash
if (!readOnly) { Cryptpad.replaceHash(editHash); } 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) { config.onReady = function (info) {
module.users = info.userList.users;
if (module.realtime !== info.realtime) { if (module.realtime !== info.realtime) {
var realtime = module.realtime = info.realtime; var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
@ -600,135 +231,58 @@ define([
newDoc = hjson.content; newDoc = hjson.content;
if (hjson.highlightMode) { if (hjson.highlightMode) {
setMode(hjson.highlightMode, module.$language); CodeMirror.setMode(hjson.highlightMode);
} }
} }
if (!module.highlightMode) { if (!CodeMirror.highlightMode) {
setMode('javascript', module.$language); CodeMirror.setMode('javascript');
console.log("%s => %s", module.highlightMode, module.$language.val()); console.log("%s => %s", CodeMirror.highlightMode, CodeMirror.$language.val());
} }
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
updateMetadata(userDoc); Metadata.update(userDoc);
if (newDoc) { if (newDoc) {
editor.setValue(newDoc); editor.setValue(newDoc);
} }
if (Cryptpad.initialName && document.title === defaultName) { if (Cryptpad.initialName && Title.isDefaultTitle()) {
updateTitle(Cryptpad.initialName); Title.updateTitle(Cryptpad.initialName);
onLocal();
}
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
} }
Cryptpad.removeLoadingScreen(); Cryptpad.removeLoadingScreen();
setEditable(true); setEditable(true);
initializing = false; initializing = false;
//Cryptpad.log("Your document is ready");
onLocal(); // push local state to avoid parse errors later. 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) { if (readOnly) { return; }
var cLine = cursor.line; UserList.getLastName(toolbar.$userNameButton, isNew);
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;
}; };
var onRemote = config.onRemote = function () { config.onRemote = function () {
if (initializing) { return; } if (initializing) { return; }
if (isHistoryMode) { return; } if (isHistoryMode) { return; }
var scroll = editor.getScrollInfo();
var oldDoc = canonicalize($textarea.val()); var oldDoc = canonicalize(CodeMirror.$textarea.val());
var shjson = module.realtime.getUserDoc(); var shjson = module.realtime.getUserDoc();
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
updateMetadata(shjson); Metadata.update(shjson);
var hjson = JSON.parse(shjson); var hjson = JSON.parse(shjson);
var remoteDoc = hjson.content; var remoteDoc = hjson.content;
var highlightMode = hjson.highlightMode; var highlightMode = hjson.highlightMode;
if (highlightMode && highlightMode !== module.highlightMode) { if (highlightMode && highlightMode !== module.highlightMode) {
setMode(highlightMode, module.$language); CodeMirror.setMode(highlightMode);
}
//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); CodeMirror.setValueAndCursor(oldDoc, remoteDoc, TextPatcher);
if (!readOnly) { if (!readOnly) {
var textValue = canonicalize($textarea.val()); var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson2 = stringifyInner(textValue); var shjson2 = stringifyInner(textValue);
if (shjson2 !== shjson) { if (shjson2 !== shjson) {
console.error("shjson2 !== shjson"); console.error("shjson2 !== shjson");
@ -736,19 +290,17 @@ define([
module.patchText(shjson2); module.patchText(shjson2);
} }
} }
if (oldDoc !== remoteDoc) { if (oldDoc !== remoteDoc) { Cryptpad.notify(); }
notify();
}
}; };
var onAbort = config.onAbort = function (info) { config.onAbort = function () {
// inform of network disconnect // inform of network disconnect
setEditable(false); setEditable(false);
toolbar.failed(); toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true); Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}; };
var onConnectionChange = config.onConnectionChange = function (info) { config.onConnectionChange = function (info) {
setEditable(info.state); setEditable(info.state);
toolbar.failed(); toolbar.failed();
if (info.state) { 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); editor.on('change', onLocal);
@ -772,7 +324,7 @@ define([
var interval = 100; var interval = 100;
var second = function (CM) { var second = function (CM) {
Cryptpad.ready(function (err, env) { Cryptpad.ready(function () {
andThen(CM); andThen(CM);
Cryptpad.reportAppUsage(); Cryptpad.reportAppUsage();
}); });

@ -3,7 +3,7 @@ define(['jquery'], function ($) {
// copy arbitrary text to the clipboard // copy arbitrary text to the clipboard
// return boolean indicating success // return boolean indicating success
var copy = Clipboard.copy = function (text) { Clipboard.copy = function (text) {
var $ta = $('<input>', { var $ta = $('<input>', {
type: 'text', type: 'text',
}).val(text); }).val(text);

@ -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($('<link />', {
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;
});

@ -23,13 +23,16 @@ define([
return chanKey + keys; return chanKey + keys;
} }
if (!keys.editKeyStr) { return; } 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) { var getViewHashFromKeys = Hash.getViewHashFromKeys = function (chanKey, keys) {
if (typeof keys === 'string') { if (typeof keys === 'string') {
return; 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) { var parsePadUrl = Hash.parsePadUrl = function (href) {
@ -38,6 +41,7 @@ define([
var ret = {}; var ret = {};
if (!href) { return ret; } if (!href) { return ret; }
if (href.slice(-1) !== '/') { href += '/'; }
if (!/^https*:\/\//.test(href)) { if (!/^https*:\/\//.test(href)) {
var idx = href.indexOf('/#'); var idx = href.indexOf('/#');
@ -46,7 +50,7 @@ define([
return ret; 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.domain = domain;
ret.type = type; ret.type = type;
return ''; return '';
@ -62,12 +66,16 @@ define([
return '/' + parsed.type + '/#' + parsed.hash; return '/' + parsed.type + '/#' + parsed.hash;
}; };
var fixDuplicateSlashes = function (s) {
return s.replace(/\/+/g, '/');
};
/* /*
* Returns all needed keys for a realtime channel * Returns all needed keys for a realtime channel
* - no argument: use the URL hash or create one if it doesn't exist * - no argument: use the URL hash or create one if it doesn't exist
* - secretHash provided: use secretHash to find the keys * - secretHash provided: use secretHash to find the keys
*/ */
var getSecrets = Hash.getSecrets = function (secretHash) { Hash.getSecrets = function (secretHash) {
var secret = {}; var secret = {};
var generate = function () { var generate = function () {
secret.keys = Crypto.createEditCryptor(); secret.keys = Crypto.createEditCryptor();
@ -91,7 +99,7 @@ define([
} }
else { else {
// New hash // New hash
var hashArray = hash.split('/'); var hashArray = fixDuplicateSlashes(hash).split('/');
if (hashArray.length < 4) { if (hashArray.length < 4) {
Hash.alert("Unable to parse the key"); Hash.alert("Unable to parse the key");
throw new Error("Unable to parse the key"); throw new Error("Unable to parse the key");
@ -119,14 +127,15 @@ define([
} }
} else if (version === "2") { } else if (version === "2") {
// version 2 hashes are to be used for encrypted blobs // 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; return secret;
}; };
var getHashes = Hash.getHashes = function (channel, secret) { Hash.getHashes = function (channel, secret) {
var hashes = {}; var hashes = {};
if (secret.keys.editKeyStr) { if (secret.keys.editKeyStr) {
hashes.editHash = getEditHashFromKeys(channel, secret.keys); hashes.editHash = getEditHashFromKeys(channel, secret.keys);
@ -134,6 +143,9 @@ define([
if (secret.keys.viewKeyStr) { if (secret.keys.viewKeyStr) {
hashes.viewHash = getViewHashFromKeys(channel, secret.keys); hashes.viewHash = getViewHashFromKeys(channel, secret.keys);
} }
if (secret.keys.fileKeyStr) {
hashes.fileHash = getFileHashFromKeys(channel, secret.keys.fileKeyStr);
}
return hashes; return hashes;
}; };
@ -145,12 +157,12 @@ define([
return id; return id;
}; };
var createRandomHash = Hash.createRandomHash = function () { Hash.createRandomHash = function () {
// 16 byte channel Id // 16 byte channel Id
var channelId = Util.hexToBase64(createChannelId()); var channelId = Util.hexToBase64(createChannelId());
// 18 byte encryption key // 18 byte encryption key
var key = Crypto.b64RemoveSlashes(Crypto.rand64(18)); 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 Version 1
/code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI /code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI
Version 2 Version 2
/file/<fileId>/#/2/<cryptKey>/<contentType> /file/#/2/<fileId>/<cryptKey>/<contentType>
/file/<fileId>/#/2/ajExFODrFH4lVBwxxsrOKw/pdf /file/#/2/K6xWU-LT9BJHCQcDCT-DcQ/ajExFODrFH4lVBwxxsrOKw/image-png
*/ */
var parseHash = Hash.parseHash = function (hash) { var parseHash = Hash.parseHash = function (hash) {
var parsed = {}; var parsed = {};
@ -171,20 +183,26 @@ Version 2
parsed.version = 0; parsed.version = 0;
return parsed; return parsed;
} }
var hashArr = hash.split('/'); var hashArr = fixDuplicateSlashes(hash).split('/');
if (hashArr[1] && hashArr[1] === '1') { if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1; parsed.version = 1;
parsed.mode = hashArr[2]; parsed.mode = hashArr[2];
parsed.channel = hashArr[3]; parsed.channel = hashArr[3];
parsed.key = hashArr[4]; 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 parsed;
} }
return; return;
}; };
// STORAGE // STORAGE
var findWeaker = Hash.findWeaker = function (href, recents) { Hash.findWeaker = function (href, recents) {
var rHref = href || getRelativeHref(window.location.href); var rHref = href || getRelativeHref(window.location.href);
var parsed = parsePadUrl(rHref); var parsed = parsePadUrl(rHref);
if (!parsed.hash) { return false; } if (!parsed.hash) { return false; }
@ -228,11 +246,11 @@ Version 2
}); });
return stronger; return stronger;
}; };
var isNotStrongestStored = Hash.isNotStrongestStored = function (href, recents) { Hash.isNotStrongestStored = function (href, recents) {
return findStronger(href, recents); return findStronger(href, recents);
}; };
var hrefToHexChannelId = Hash.hrefToHexChannelId = function (href) { Hash.hrefToHexChannelId = function (href) {
var parsed = Hash.parsePadUrl(href); var parsed = Hash.parsePadUrl(href);
if (!parsed || !parsed.hash) { return; } if (!parsed || !parsed.hash) { return; }
@ -240,7 +258,7 @@ Version 2
if (parsed.version === 0) { if (parsed.version === 0) {
return parsed.channel; 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 href had no version");
console.error(parsed); console.error(parsed);
return; return;
@ -253,5 +271,14 @@ Version 2
return hex; 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; return Hash;
}); });

@ -18,13 +18,13 @@ define([
return states; return states;
}; };
var loadHistory = function (common, cb) { var loadHistory = function (config, common, cb) {
var network = common.getNetwork(); var network = common.getNetwork();
var hkn = network.historyKeeper; 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({ return ChainPad.create({
userName: 'history', userName: 'history',
initialState: '', initialState: '',
@ -35,7 +35,8 @@ define([
}; };
var realtime = createRealtime(); 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 crypto = Crypto.createEncryptor(secret.keys);
var to = window.setTimeout(function () { var to = window.setTimeout(function () {
@ -66,21 +67,44 @@ define([
} }
}; };
network.on('message', function (msg, sender) { network.on('message', function (msg) {
onMsg(msg); onMsg(msg);
}); });
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', wcId, secret.keys.validateKey])); 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 (!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 $toolbar = config.$toolbar;
var noFunc = function () {};
var render = config.onRender || noFunc; if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
var onClose = config.onClose || noFunc; throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
var onRevert = config.onRevert || noFunc; }
var onReady = config.onReady || noFunc;
// 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; var Messages = common.Messages;
@ -112,9 +136,9 @@ define([
var val = states[i].getContent().doc; var val = states[i].getContent().doc;
c = i; c = i;
if (typeof onUpdate === "function") { onUpdate(); } if (typeof onUpdate === "function") { onUpdate(); }
$hist.find('.next, .previous').show(); $hist.find('.next, .previous').css('visibility', '');
if (c === states.length - 1) { $hist.find('.next').hide(); } if (c === states.length - 1) { $hist.find('.next').css('visibility', 'hidden'); }
if (c === 0) { $hist.find('.previous').hide(); } if (c === 0) { $hist.find('.previous').css('visibility', 'hidden'); }
return val || ''; return val || '';
}; };
@ -132,15 +156,16 @@ define([
$right.hide(); $right.hide();
$cke.hide(); $cke.hide();
var $prev =$('<button>', { var $prev =$('<button>', {
'class': 'previous fa fa-step-backward', 'class': 'previous fa fa-step-backward buttonPrimary',
title: Messages.history_prev title: Messages.history_prev
}).appendTo($hist); }).appendTo($hist);
var $nav = $('<div>', {'class': 'goto'}).appendTo($hist);
var $next = $('<button>', { var $next = $('<button>', {
'class': 'next fa fa-step-forward', 'class': 'next fa fa-step-forward buttonPrimary',
title: Messages.history_next title: Messages.history_next
}).appendTo($hist); }).appendTo($hist);
var $nav = $('<div>', {'class': 'goto'}).appendTo($hist); $('<label>').text(Messages.history_version).appendTo($nav);
var $cur = $('<input>', { var $cur = $('<input>', {
'class' : 'gotoInput', 'class' : 'gotoInput',
'type' : 'number', 'type' : 'number',
@ -150,25 +175,21 @@ define([
// stopPropagation because the event would be cancelled by the dropdown menus // stopPropagation because the event would be cancelled by the dropdown menus
e.stopPropagation(); e.stopPropagation();
}); });
var $label = $('<label>').text(' / '+ states.length).appendTo($nav); var $label2 = $('<label>').text(' / '+ states.length).appendTo($nav);
var $goTo = $('<button>', {
'class': 'fa fa-check',
'title': Messages.history_goTo
}).appendTo($nav);
$('<br>').appendTo($nav); $('<br>').appendTo($nav);
var $rev = $('<button>', {
'class':'revertHistory',
title: Messages.history_restoreTitle
}).text(Messages.history_restore).appendTo($nav);
var $close = $('<button>', { var $close = $('<button>', {
'class':'closeHistory', 'class':'closeHistory',
title: Messages.history_closeTitle title: Messages.history_closeTitle
}).text(Messages.history_close).appendTo($nav); }).text(Messages.history_close).appendTo($nav);
var $rev = $('<button>', {
'class':'revertHistory buttonSuccess',
title: Messages.history_restoreTitle
}).text(Messages.history_restore).appendTo($nav);
onUpdate = function () { onUpdate = function () {
$cur.attr('max', states.length); $cur.attr('max', states.length);
$cur.val(c+1); $cur.val(c+1);
$label.text(' / ' + states.length); $label2.text(' / ' + states.length);
}; };
var close = function () { var close = function () {
@ -181,7 +202,6 @@ define([
// Buttons actions // Buttons actions
$prev.click(function () { render(getPrevious()); }); $prev.click(function () { render(getPrevious()); });
$next.click(function () { render(getNext()); }); $next.click(function () { render(getNext()); });
$goTo.click(function () { render( get($cur.val() - 1) ); });
$cur.keydown(function (e) { $cur.keydown(function (e) {
var p = function () { e.preventDefault(); }; var p = function () { e.preventDefault(); };
if (e.which === 13) { p(); return render( get($cur.val() - 1) ); } // Enter if (e.which === 13) { p(); return render( get($cur.val() - 1) ); } // Enter
@ -192,7 +212,7 @@ define([
if (e.which === 27) { p(); $close.click(); } if (e.which === 27) { p(); $close.click(); }
}).focus(); }).focus();
$cur.on('change', function () { $cur.on('change', function () {
$goTo.click(); render( get($cur.val() - 1) );
}); });
$close.click(function () { $close.click(function () {
states = []; states = [];
@ -213,7 +233,8 @@ define([
}; };
// Load all the history messages into a new chainpad object // Load all the history messages into a new chainpad object
loadHistory(common, function (err, newRt) { loadHistory(config, common, function (err, newRt) {
History.loading = false;
if (err) { throw new Error(err); } if (err) { throw new Error(err); }
realtime = newRt; realtime = newRt;
update(); update();

@ -3,8 +3,10 @@ define([
'/customize/messages.js', '/customize/messages.js',
'/common/common-util.js', '/common/common-util.js',
'/customize/application_config.js', '/customize/application_config.js',
'/bower_components/alertifyjs/dist/js/alertify.js' '/bower_components/alertifyjs/dist/js/alertify.js',
], function ($, Messages, Util, AppConfig, Alertify) { '/common/notify.js',
'/common/visible.js'
], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible) {
var UI = {}; var UI = {};
@ -48,7 +50,7 @@ define([
UI.alert = function (msg, cb, force) { UI.alert = function (msg, cb, force) {
cb = cb || function () {}; cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); } if (force !== true) { msg = Util.fixHTML(msg); }
var close = function (e) { var close = function () {
findOKButton().click(); findOKButton().click();
}; };
var keyHandler = listenForKeys(close, close); var keyHandler = listenForKeys(close, close);
@ -66,9 +68,9 @@ define([
cb = cb || function () {}; cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); } if (force !== true) { msg = Util.fixHTML(msg); }
var keyHandler = listenForKeys(function (e) { // yes var keyHandler = listenForKeys(function () { // yes
findOKButton().click(); findOKButton().click();
}, function (e) { // no }, function () { // no
findCancelButton().click(); findCancelButton().click();
}); });
@ -90,9 +92,9 @@ define([
cb = cb || function () {}; cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); } if (force !== true) { msg = Util.fixHTML(msg); }
var keyHandler = listenForKeys(function (e) { var keyHandler = listenForKeys(function () {
findOKButton().click(); findOKButton().click();
}, function (e) { }, function () {
findCancelButton().click(); findCancelButton().click();
}); });
@ -141,7 +143,7 @@ define([
return { return {
show: function () { show: function () {
$target.show(); $target.css('display', 'inline');
return this; return this;
}, },
hide: function () { hide: function () {
@ -183,7 +185,7 @@ define([
} }
if (Messages.tips && !hideTips) { if (Messages.tips && !hideTips) {
var $loadingTip = $('<div>', {'id': 'loadingTip'}); var $loadingTip = $('<div>', {'id': 'loadingTip'});
var $tip = $('<span>', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip); $('<span>', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip);
$loadingTip.css({ $loadingTip.css({
'top': $('body').height()/2 + $container.height()/2 + 20 + 'px' 'top': $('body').height()/2 + $container.height()/2 + 20 + 'px'
}); });
@ -204,7 +206,29 @@ define([
$('#' + LOADING).find('p').html(error || Messages.error); $('#' + LOADING).find('p').html(error || Messages.error);
}; };
var importContent = UI.importContent = function (type, f) { // Notify
var notify = {};
UI.unnotify = function () {
if (notify.tabNotification &&
typeof(notify.tabNotification.cancel) === 'function') {
notify.tabNotification.cancel();
}
};
UI.notify = function () {
if (Visible.isSupported() && !Visible.currently()) {
UI.unnotify();
notify.tabNotification = Notify.tab(1000, 10);
}
};
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { UI.unnotify(); }
});
}
UI.importContent = function (type, f) {
return function () { return function () {
var $files = $('<input type="file">').click(); var $files = $('<input type="file">').click();
$files.on('change', function (e) { $files.on('change', function (e) {

@ -0,0 +1,51 @@
define(function () {
var module = {};
module.create = function (UserList, Title, cfg) {
var exp = {};
exp.update = function (shjson) {
// Extract the user list (metadata) from the hyperjson
var json = (!shjson || typeof shjson !== "string") ? "" : JSON.parse(shjson);
var titleUpdated = false;
var metadata;
if (Array.isArray(json)) {
metadata = json[3] && json[3].metadata;
} else {
metadata = json.metadata;
}
if (typeof metadata === "object") {
if (metadata.users) {
var userData = metadata.users;
// Update the local user data
UserList.addToUserData(userData);
}
if (metadata.defaultTitle) {
Title.updateDefaultTitle(metadata.defaultTitle);
}
if (typeof metadata.title !== "undefined") {
Title.updateTitle(metadata.title || Title.defaultTitle);
titleUpdated = true;
}
if (metadata.slideOptions && cfg.slideOptions) {
cfg.slideOptions(metadata.slideOptions);
}
if (metadata.color && cfg.slideColors) {
cfg.slideColors(metadata.color, metadata.backColor);
}
if (typeof(metadata.palette) !== 'undefined' && cfg.updatePalette) {
cfg.updatePalette(metadata.palette);
}
}
if (!titleUpdated) {
Title.updateTitle(Title.defaultTitle);
}
};
return exp;
};
return module;
});

@ -0,0 +1,84 @@
define(function () {
var module = {};
module.create = function (cfg, onLocal, Cryptpad) {
var exp = {};
var parsed = exp.parsedHref = Cryptpad.parsePadUrl(window.location.href);
exp.defaultTitle = Cryptpad.getDefaultName(parsed);
exp.title = document.title; // TOOD slides
cfg = cfg || {};
var getHeadingText = cfg.getHeadingText || function () { return; };
var updateLocalTitle = function (newTitle) {
exp.title = newTitle;
if (typeof cfg.updateLocalTitle === "function") {
cfg.updateLocalTitle(newTitle);
} else {
document.title = newTitle;
}
};
var $title;
exp.setToolbar = function (toolbar) {
$title = toolbar && toolbar.title;
};
exp.getTitle = function () { return exp.title; };
var isDefaultTitle = exp.isDefaultTitle = function (){return exp.title === exp.defaultTitle;};
var suggestTitle = exp.suggestTitle = function (fallback) {
if (isDefaultTitle()) {
return getHeadingText() || fallback || "";
} else {
return exp.title || getHeadingText() || exp.defaultTitle;
}
};
var renameCb = function (err, newTitle) {
if (err) { return; }
updateLocalTitle(newTitle);
console.log('here');
onLocal();
};
exp.updateTitle = function (newTitle) {
if (newTitle === exp.title) { return; }
// Change the title now, and set it back to the old value if there is an error
var oldTitle = exp.title;
Cryptpad.renamePad(newTitle, function (err, data) {
if (err) {
console.log("Couldn't set pad title");
console.error(err);
updateLocalTitle(oldTitle);
return;
}
updateLocalTitle(data);
if (!$title) { return; }
$title.find('span.title').text(data);
$title.find('input').val(data);
});
};
exp.updateDefaultTitle = function (newDefaultTitle) {
exp.defaultTitle = newDefaultTitle;
if (!$title) { return; }
$title.find('input').attr("placeholder", exp.defaultTitle);
};
exp.getTitleConfig = function () {
return {
onRename: renameCb,
suggestName: suggestTitle,
defaultName: exp.defaultTitle
};
};
return exp;
};
return module;
});

@ -0,0 +1,105 @@
define(function () {
var module = {};
module.create = function (info, onLocal, Cryptget, Cryptpad) {
var exp = {};
var userData = exp.userData = {};
var userList = exp.userList = info.userList;
var myData = exp.myData = {};
exp.myUserName = info.myID;
exp.myNetfluxId = info.myID;
var network = Cryptpad.getNetwork();
var parsed = Cryptpad.parsePadUrl(window.location.href);
var appType = parsed ? parsed.type : undefined;
var addToUserData = exp.addToUserData = function(data) {
var users = userList.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);
}
};
exp.getToolbarConfig = function () {
return {
data: userData,
list: userList,
userNetfluxId: exp.myNetfluxId
};
};
var setName = exp.setName = function (newName, cb) {
if (typeof(newName) !== 'string') { return; }
var myUserNameTemp = newName.trim();
if(myUserNameTemp.length > 32) {
myUserNameTemp = myUserNameTemp.substr(0, 32);
}
exp.myUserName = myUserNameTemp;
myData = {};
myData[exp.myNetfluxId] = {
name: exp.myUserName,
uid: Cryptpad.getUid(),
};
addToUserData(myData);
Cryptpad.setAttribute('username', exp.myUserName, function (err) {
if (err) {
console.log("Couldn't set username");
console.error(err);
return;
}
if (typeof cb === "function") { cb(); }
});
};
exp.getLastName = function ($changeNameButton, isNew) {
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 (typeof(lastName) === 'string') {
setName(lastName, onLocal);
} else {
myData[exp.myNetfluxId] = {
name: "",
uid: Cryptpad.getUid(),
};
addToUserData(myData);
onLocal();
$changeNameButton.click();
}
if (isNew && appType) {
Cryptpad.selectTemplate(appType, info.realtime, Cryptget);
}
});
};
Cryptpad.onDisplayNameChanged(function (newName) {
setName(newName, onLocal);
});
network.on('reconnect', function (uid) {
exp.myNetfluxId = uid;
exp.setName(exp.myUserName);
});
return exp;
};
return module;
});

@ -1,20 +1,20 @@
define([], function () { define([], function () {
var Util = {}; var Util = {};
var find = Util.find = function (map, path) { Util.find = function (map, path) {
return (map && path.reduce(function (p, n) { return (map && path.reduce(function (p, n) {
return typeof(p[n]) !== 'undefined' && p[n]; return typeof(p[n]) !== 'undefined' && p[n];
}, map)); }, map));
}; };
var fixHTML = Util.fixHTML = function (str) { Util.fixHTML = function (str) {
if (!str) { return ''; } if (!str) { return ''; }
return str.replace(/[<>&"']/g, function (x) { return str.replace(/[<>&"']/g, function (x) {
return ({ "<": "&lt;", ">": "&gt", "&": "&amp;", '"': "&#34;", "'": "&#39;" })[x]; return ({ "<": "&lt;", ">": "&gt", "&": "&amp;", '"': "&#34;", "'": "&#39;" })[x];
}); });
}; };
var hexToBase64 = Util.hexToBase64 = function (hex) { Util.hexToBase64 = function (hex) {
var hexArray = hex var hexArray = hex
.replace(/\r|\n/g, "") .replace(/\r|\n/g, "")
.replace(/([\da-fA-F]{2}) ?/g, "0x$1 ") .replace(/([\da-fA-F]{2}) ?/g, "0x$1 ")
@ -24,7 +24,7 @@ define([], function () {
return window.btoa(byteString).replace(/\//g, '-').slice(0,-2); return window.btoa(byteString).replace(/\//g, '-').slice(0,-2);
}; };
var base64ToHex = Util.base64ToHex = function (b64String) { Util.base64ToHex = function (b64String) {
var hexArray = []; var hexArray = [];
atob(b64String.replace(/-/g, '/')).split("").forEach(function(e){ atob(b64String.replace(/-/g, '/')).split("").forEach(function(e){
var h = e.charCodeAt(0).toString(16); var h = e.charCodeAt(0).toString(16);
@ -34,9 +34,9 @@ define([], function () {
return hexArray.join(""); return hexArray.join("");
}; };
var uint8ArrayToHex = Util.uint8ArrayToHex = function (a) { Util.uint8ArrayToHex = function (a) {
// call slice so Uint8Arrays work as expected // call slice so Uint8Arrays work as expected
return Array.prototype.slice.call(a).map(function (e, i) { return Array.prototype.slice.call(a).map(function (e) {
var n = Number(e & 0xff).toString(16); var n = Number(e & 0xff).toString(16);
if (n === 'NaN') { if (n === 'NaN') {
throw new Error('invalid input resulted in NaN'); throw new Error('invalid input resulted in NaN');
@ -51,7 +51,7 @@ define([], function () {
}).join(''); }).join('');
}; };
var deduplicateString = Util.deduplicateString = function (array) { Util.deduplicateString = function (array) {
var a = array.slice(); var a = array.slice();
for(var i=0; i<a.length; i++) { for(var i=0; i<a.length; i++) {
for(var j=i+1; j<a.length; j++) { for(var j=i+1; j<a.length; j++) {
@ -61,11 +61,11 @@ define([], function () {
return a; return a;
}; };
var getHash = Util.getHash = function () { Util.getHash = function () {
return window.location.hash.slice(1); return window.location.hash.slice(1);
}; };
var replaceHash = Util.replaceHash = function (hash) { Util.replaceHash = function (hash) {
if (window.history && window.history.replaceState) { if (window.history && window.history.replaceState) {
if (!/^#/.test(hash)) { hash = '#' + hash; } if (!/^#/.test(hash)) { hash = '#' + hash; }
return void window.history.replaceState({}, window.document.title, hash); return void window.history.replaceState({}, window.document.title, hash);
@ -76,18 +76,28 @@ define([], function () {
/* /*
* Saving files * Saving files
*/ */
var fixFileName = Util.fixFileName = function (filename) { Util.fixFileName = function (filename) {
return filename.replace(/ /g, '-').replace(/[\/\?]/g, '_') return filename.replace(/ /g, '-').replace(/[\/\?]/g, '_')
.replace(/_+/g, '_'); .replace(/_+/g, '_');
}; };
var bytesToMegabytes = Util.bytesToMegabytes = function (bytes) { Util.bytesToMegabytes = function (bytes) {
return Math.floor((bytes / (1024 * 1024) * 100)) / 100; return Math.floor((bytes / (1024 * 1024) * 100)) / 100;
}; };
var bytesToKilobytes = Util.bytesToKilobytes = function (bytes) { Util.bytesToKilobytes = function (bytes) {
return Math.floor(bytes / 1024 * 100) / 100; return Math.floor(bytes / 1024 * 100) / 100;
}; };
Util.fetch = function (src, cb) {
var xhr = new XMLHttpRequest();
xhr.open("GET", src, true);
xhr.responseType = "arraybuffer";
xhr.onload = function () {
return void cb(void 0, new Uint8Array(xhr.response));
};
xhr.send(null);
};
return Util; return Util;
}); });

@ -1,6 +1,7 @@
define([ define([
'/customize/application_config.js',
'/bower_components/scrypt-async/scrypt-async.min.js', '/bower_components/scrypt-async/scrypt-async.min.js',
], function () { ], function (AppConfig) {
var Cred = {}; var Cred = {};
var Scrypt = window.scrypt; var Scrypt = window.scrypt;
@ -8,22 +9,26 @@ define([
return typeof(x) === 'string'; return typeof(x) === 'string';
}; };
var isValidUsername = Cred.isValidUsername = function (name) { Cred.isValidUsername = function (name) {
return !!(name && isString(name)); return !!(name && isString(name));
}; };
var isValidPassword = Cred.isValidPassword = function (passwd) { Cred.isValidPassword = function (passwd) {
return !!(passwd && isString(passwd)); return !!(passwd && isString(passwd));
}; };
var passwordsMatch = Cred.passwordsMatch = function (a, b) { Cred.passwordsMatch = function (a, b) {
return isString(a) && isString(b) && a === b; return isString(a) && isString(b) && a === b;
}; };
var deriveFromPassphrase = Cred.deriveFromPassphrase = function Cred.customSalt = function () {
(username, password, len, cb) { return typeof(AppConfig.loginSalt) === 'string'?
AppConfig.loginSalt: '';
};
Cred.deriveFromPassphrase = function (username, password, len, cb) {
Scrypt(password, Scrypt(password,
username, username + Cred.customSalt(), // salt
8, // memoryCost (n) 8, // memoryCost (n)
1024, // block size parameter (r) 1024, // block size parameter (r)
len || 128, // dkLen len || 128, // dkLen
@ -32,7 +37,7 @@ define([
undefined); // format, could be 'base64' undefined); // format, could be 'base64'
}; };
var dispenser = Cred.dispenser = function (bytes) { Cred.dispenser = function (bytes) {
var entropy = { var entropy = {
used: 0, used: 0,
}; };

@ -5,8 +5,8 @@ define([
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/bower_components/textpatcher/TextPatcher.js' '/bower_components/textpatcher/TextPatcher.js'
], function ($, Crypto, Realtime, Cryptpad, TextPatcher) { ], function ($, Crypto, Realtime, Cryptpad, TextPatcher) {
var Messages = Cryptpad.Messages; //var Messages = Cryptpad.Messages;
var noop = function () {}; //var noop = function () {};
var finish = function (S, err, doc) { var finish = function (S, err, doc) {
if (S.done) { return; } if (S.done) { return; }
S.cb(err, doc); S.cb(err, doc);
@ -50,14 +50,14 @@ define([
var Session = { cb: cb, }; var Session = { cb: cb, };
var config = makeConfig(hash); var config = makeConfig(hash);
var onReady = config.onReady = function (info) { config.onReady = function (info) {
var rt = Session.session = info.realtime; var rt = Session.session = info.realtime;
Session.network = info.network; Session.network = info.network;
finish(Session, void 0, rt.getUserDoc()); finish(Session, void 0, rt.getUserDoc());
}; };
overwrite(config, opt); overwrite(config, opt);
var realtime = Session.realtime = Realtime.start(config); Session.realtime = Realtime.start(config);
}; };
var put = function (hash, doc, cb, opt) { var put = function (hash, doc, cb, opt) {
@ -87,7 +87,7 @@ define([
}; };
overwrite(config, opt); overwrite(config, opt);
var realtime = Session.session = Realtime.start(config); Session.session = Realtime.start(config);
}; };
return { return {

@ -7,11 +7,15 @@ define([
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-interface.js', '/common/common-interface.js',
'/common/common-history.js', '/common/common-history.js',
'/common/common-userlist.js',
'/common/common-title.js',
'/common/common-metadata.js',
'/common/common-codemirror.js',
'/common/clipboard.js', '/common/clipboard.js',
'/common/pinpad.js', '/common/pinpad.js',
'/customize/application_config.js' '/customize/application_config.js'
], function ($, Config, Messages, Store, Util, Hash, UI, History, Clipboard, Pinpad, AppConfig) { ], function ($, Config, Messages, Store, Util, Hash, UI, History, UserList, Title, Metadata, CodeMirror, Clipboard, Pinpad, AppConfig) {
/* This file exposes functionality which is specific to Cryptpad, but not to /* This file exposes functionality which is specific to Cryptpad, but not to
any particular pad type. This includes functions for committing metadata any particular pad type. This includes functions for committing metadata
@ -29,7 +33,7 @@ define([
var userHashKey = common.userHashKey = 'User_hash'; var userHashKey = common.userHashKey = 'User_hash';
var userNameKey = common.userNameKey = 'User_name'; var userNameKey = common.userNameKey = 'User_name';
var fileHashKey = common.fileHashKey = 'FS_hash'; var fileHashKey = common.fileHashKey = 'FS_hash';
var displayNameKey = common.displayNameKey = 'cryptpad.username'; common.displayNameKey = 'cryptpad.username';
var newPadNameKey = common.newPadNameKey = "newPadName"; var newPadNameKey = common.newPadNameKey = "newPadName";
var newPadPathKey = common.newPadPathKey = "newPadPath"; var newPadPathKey = common.newPadPathKey = "newPadPath";
var storageKey = common.storageKey = 'CryptPad_RECENTPADS'; var storageKey = common.storageKey = 'CryptPad_RECENTPADS';
@ -39,10 +43,10 @@ define([
var rpc; var rpc;
// import UI elements // import UI elements
var findCancelButton = common.findCancelButton = UI.findCancelButton; common.findCancelButton = UI.findCancelButton;
var findOKButton = common.findOKButton = UI.findOKButton; common.findOKButton = UI.findOKButton;
var listenForKeys = common.listenForKeys = UI.listenForKeys; common.listenForKeys = UI.listenForKeys;
var stopListening = common.stopListening = UI.stopListening; common.stopListening = UI.stopListening;
common.prompt = UI.prompt; common.prompt = UI.prompt;
common.confirm = UI.confirm; common.confirm = UI.confirm;
common.alert = UI.alert; common.alert = UI.alert;
@ -52,19 +56,22 @@ define([
common.addLoadingScreen = UI.addLoadingScreen; common.addLoadingScreen = UI.addLoadingScreen;
common.removeLoadingScreen = UI.removeLoadingScreen; common.removeLoadingScreen = UI.removeLoadingScreen;
common.errorLoadingScreen = UI.errorLoadingScreen; common.errorLoadingScreen = UI.errorLoadingScreen;
common.notify = UI.notify;
common.unnotify = UI.unnotify;
// import common utilities for export // import common utilities for export
var find = common.find = Util.find; common.find = Util.find;
var fixHTML = common.fixHTML = Util.fixHTML; var fixHTML = common.fixHTML = Util.fixHTML;
var hexToBase64 = common.hexToBase64 = Util.hexToBase64; common.hexToBase64 = Util.hexToBase64;
var base64ToHex = common.base64ToHex = Util.base64ToHex; common.base64ToHex = Util.base64ToHex;
var deduplicateString = common.deduplicateString = Util.deduplicateString; var deduplicateString = common.deduplicateString = Util.deduplicateString;
var uint8ArrayToHex = common.uint8ArrayToHex = Util.uint8ArrayToHex; common.uint8ArrayToHex = Util.uint8ArrayToHex;
var replaceHash = common.replaceHash = Util.replaceHash; common.replaceHash = Util.replaceHash;
var getHash = common.getHash = Util.getHash; var getHash = common.getHash = Util.getHash;
var fixFileName = common.fixFileName = Util.fixFileName; common.fixFileName = Util.fixFileName;
common.bytesToMegabytes = Util.bytesToMegabytes; common.bytesToMegabytes = Util.bytesToMegabytes;
common.bytesToKilobytes = Util.bytesToKilobytes; common.bytesToKilobytes = Util.bytesToKilobytes;
common.fetch = Util.fetch;
// import hash utilities for export // import hash utilities for export
var createRandomHash = common.createRandomHash = Hash.createRandomHash; var createRandomHash = common.createRandomHash = Hash.createRandomHash;
@ -73,14 +80,29 @@ define([
var hrefToHexChannelId = common.hrefToHexChannelId = Hash.hrefToHexChannelId; var hrefToHexChannelId = common.hrefToHexChannelId = Hash.hrefToHexChannelId;
var parseHash = common.parseHash = Hash.parseHash; var parseHash = common.parseHash = Hash.parseHash;
var getRelativeHref = common.getRelativeHref = Hash.getRelativeHref; var getRelativeHref = common.getRelativeHref = Hash.getRelativeHref;
common.getBlobPathFromHex = Hash.getBlobPathFromHex;
common.getEditHashFromKeys = Hash.getEditHashFromKeys; common.getEditHashFromKeys = Hash.getEditHashFromKeys;
common.getViewHashFromKeys = Hash.getViewHashFromKeys; common.getViewHashFromKeys = Hash.getViewHashFromKeys;
common.getFileHashFromKeys = Hash.getFileHashFromKeys;
common.getSecrets = Hash.getSecrets; common.getSecrets = Hash.getSecrets;
common.getHashes = Hash.getHashes; common.getHashes = Hash.getHashes;
common.createChannelId = Hash.createChannelId; common.createChannelId = Hash.createChannelId;
common.findWeaker = Hash.findWeaker; common.findWeaker = Hash.findWeaker;
common.findStronger = Hash.findStronger; common.findStronger = Hash.findStronger;
common.serializeHash = Hash.serializeHash;
// Userlist
common.createUserList = UserList.create;
// Title
common.createTitle = Title.create;
// Metadata
common.createMetadata = Metadata.create;
// CodeMirror
common.createCodemirror = CodeMirror.create;
// History // History
common.getHistory = function (config) { return History.create(common, config); }; common.getHistory = function (config) { return History.create(common, config); };
@ -119,13 +141,13 @@ define([
}); });
}; };
var reportAppUsage = common.reportAppUsage = function () { common.reportAppUsage = function () {
var pattern = window.location.pathname.split('/') var pattern = window.location.pathname.split('/')
.filter(function (x) { return x; }).join('.'); .filter(function (x) { return x; }).join('.');
feedback(pattern); feedback(pattern);
}; };
var getUid = common.getUid = function () { common.getUid = function () {
if (store && store.getProxy() && store.getProxy().proxy) { if (store && store.getProxy() && store.getProxy().proxy) {
return store.getProxy().proxy.uid; return store.getProxy().proxy.uid;
} }
@ -150,7 +172,7 @@ define([
}, 0); }, 0);
}; };
var getWebsocketURL = common.getWebsocketURL = function () { common.getWebsocketURL = function () {
if (!Config.websocketPath) { return Config.websocketURL; } if (!Config.websocketPath) { return Config.websocketURL; }
var path = Config.websocketPath; var path = Config.websocketPath;
if (/^ws{1,2}:\/\//.test(path)) { return path; } if (/^ws{1,2}:\/\//.test(path)) { return path; }
@ -162,9 +184,10 @@ define([
return url; return url;
}; };
var login = common.login = function (hash, name, cb) { common.login = function (hash, name, cb) {
if (!hash) { throw new Error('expected a user hash'); } if (!hash) { throw new Error('expected a user hash'); }
if (!name) { throw new Error('expected a user name'); } if (!name) { throw new Error('expected a user name'); }
hash = common.serializeHash(hash);
localStorage.setItem(userHashKey, hash); localStorage.setItem(userHashKey, hash);
localStorage.setItem(userNameKey, name); localStorage.setItem(userNameKey, name);
if (cb) { cb(); } if (cb) { cb(); }
@ -185,10 +208,11 @@ define([
}; };
var logoutHandlers = []; var logoutHandlers = [];
var logout = common.logout = function (cb) { common.logout = function (cb) {
[ [
userNameKey, userNameKey,
userHashKey, userHashKey,
'loginToken',
].forEach(function (k) { ].forEach(function (k) {
sessionStorage.removeItem(k); sessionStorage.removeItem(k);
localStorage.removeItem(k); localStorage.removeItem(k);
@ -208,18 +232,19 @@ define([
if (cb) { cb(); } if (cb) { cb(); }
}; };
var onLogout = common.onLogout = function (h) { common.onLogout = function (h) {
if (typeof (h) !== "function") { return; } if (typeof (h) !== "function") { return; }
if (logoutHandlers.indexOf(h) !== -1) { return; } if (logoutHandlers.indexOf(h) !== -1) { return; }
logoutHandlers.push(h); logoutHandlers.push(h);
}; };
var getUserHash = common.getUserHash = function () { var getUserHash = common.getUserHash = function () {
var hash; var hash = localStorage[userHashKey];
[sessionStorage, localStorage].some(function (s) {
var h = s[userHashKey]; if (hash) {
if (h) { return (hash = h); } var sHash = common.serializeHash(hash);
}); if (sHash !== hash) { localStorage[userHashKey] = sHash; }
}
return hash; return hash;
}; };
@ -228,7 +253,7 @@ define([
return typeof getUserHash() === "string"; return typeof getUserHash() === "string";
}; };
var hasSigningKeys = common.hasSigningKeys = function (proxy) { common.hasSigningKeys = function (proxy) {
return typeof(proxy) === 'object' && return typeof(proxy) === 'object' &&
typeof(proxy.edPrivate) === 'string' && typeof(proxy.edPrivate) === 'string' &&
typeof(proxy.edPublic) === 'string'; typeof(proxy.edPublic) === 'string';
@ -295,16 +320,20 @@ define([
pads.forEach(function (pad, i) { pads.forEach(function (pad, i) {
if (pad && typeof(pad) === 'object') { if (pad && typeof(pad) === 'object') {
var hash = checkObjectData(pad); var hash = checkObjectData(pad);
if (!hash || !common.parseHash(hash)) { return; } if (!hash || !common.parseHash(hash)) {
console.error("[Cryptpad.checkRecentPads] pad had unexpected value", pad);
getStore().removeData(i);
return;
}
return pad; return pad;
} }
console.error("[Cryptpad.migrateRecentPads] pad had unexpected value"); console.error("[Cryptpad.checkRecentPads] pad had unexpected value", pad);
getStore().removeData(i); getStore().removeData(i);
}); });
}; };
// Get the pads from localStorage to migrate them to the object store // Get the pads from localStorage to migrate them to the object store
var getLegacyPads = common.getLegacyPads = function (cb) { common.getLegacyPads = function (cb) {
require(['/customize/store.js'], function(Legacy) { // TODO DEPRECATE_F require(['/customize/store.js'], function(Legacy) { // TODO DEPRECATE_F
Legacy.ready(function (err, legacy) { Legacy.ready(function (err, legacy) {
if (err) { cb(err, null); return; } if (err) { cb(err, null); return; }
@ -324,7 +353,6 @@ define([
// Create untitled documents when no name is given // Create untitled documents when no name is given
var getDefaultName = common.getDefaultName = function (parsed) { var getDefaultName = common.getDefaultName = function (parsed) {
var type = parsed.type; var type = parsed.type;
var untitledIndex = 1;
var name = (Messages.type)[type] + ' - ' + new Date().toString().split(' ').slice(0,4).join(' '); var name = (Messages.type)[type] + ' - ' + new Date().toString().split(' ').slice(0,4).join(' ');
return name; return name;
}; };
@ -344,37 +372,37 @@ define([
}; };
/* Sort pads according to how recently they were accessed */ /* Sort pads according to how recently they were accessed */
var mostRecent = common.mostRecent = function (a, b) { common.mostRecent = function (a, b) {
return new Date(b.atime).getTime() - new Date(a.atime).getTime(); return new Date(b.atime).getTime() - new Date(a.atime).getTime();
}; };
// STORAGE // STORAGE
var setPadAttribute = common.setPadAttribute = function (attr, value, cb) { common.setPadAttribute = function (attr, value, cb) {
getStore().setDrive([getHash(), attr].join('.'), value, function (err, data) { getStore().setDrive([getHash(), attr].join('.'), value, function (err, data) {
cb(err, data); cb(err, data);
}); });
}; };
var setAttribute = common.setAttribute = function (attr, value, cb) { common.setAttribute = function (attr, value, cb) {
getStore().set(["cryptpad", attr].join('.'), value, function (err, data) { getStore().set(["cryptpad", attr].join('.'), value, function (err, data) {
cb(err, data); cb(err, data);
}); });
}; };
var setLSAttribute = common.setLSAttribute = function (attr, value) { common.setLSAttribute = function (attr, value) {
localStorage[attr] = value; localStorage[attr] = value;
}; };
// STORAGE // STORAGE
var getPadAttribute = common.getPadAttribute = function (attr, cb) { common.getPadAttribute = function (attr, cb) {
getStore().getDrive([getHash(), attr].join('.'), function (err, data) { getStore().getDrive([getHash(), attr].join('.'), function (err, data) {
cb(err, data); cb(err, data);
}); });
}; };
var getAttribute = common.getAttribute = function (attr, cb) { common.getAttribute = function (attr, cb) {
getStore().get(["cryptpad", attr].join('.'), function (err, data) { getStore().get(["cryptpad", attr].join('.'), function (err, data) {
cb(err, data); cb(err, data);
}); });
}; };
var getLSAttribute = common.getLSAttribute = function (attr) { common.getLSAttribute = function (attr) {
return localStorage[attr]; return localStorage[attr];
}; };
@ -389,19 +417,19 @@ define([
}); });
return templates; return templates;
}; };
var addTemplate = common.addTemplate = function (data) { common.addTemplate = function (data) {
getStore().pushData(data); getStore().pushData(data);
getStore().addPad(data.href, ['template']); getStore().addPad(data.href, ['template']);
}; };
var isTemplate = common.isTemplate = function (href) { common.isTemplate = function (href) {
var rhref = getRelativeHref(href); var rhref = getRelativeHref(href);
var templates = listTemplates(); var templates = listTemplates();
return templates.some(function (t) { return templates.some(function (t) {
return t.href === rhref; return t.href === rhref;
}); });
}; };
var selectTemplate = common.selectTemplate = function (type, rt, Crypt) { common.selectTemplate = function (type, rt, Crypt) {
if (!AppConfig.enableTemplates) { return; } if (!AppConfig.enableTemplates) { return; }
var temps = listTemplates(type); var temps = listTemplates(type);
if (temps.length === 0) { return; } if (temps.length === 0) { return; }
@ -419,7 +447,7 @@ define([
Crypt.get(parsed.hash, function (err, val) { Crypt.get(parsed.hash, function (err, val) {
if (err) { throw new Error(err); } if (err) { throw new Error(err); }
var p = parsePadUrl(window.location.href); var p = parsePadUrl(window.location.href);
Crypt.put(p.hash, val, function (e) { Crypt.put(p.hash, val, function () {
common.findOKButton().click(); common.findOKButton().click();
common.removeLoadingScreen(); common.removeLoadingScreen();
}); });
@ -444,28 +472,28 @@ define([
}; };
// STORAGE: Display Name // STORAGE: Display Name
var getLastName = common.getLastName = function (cb) { common.getLastName = function (cb) {
common.getAttribute('username', function (err, userName) { common.getAttribute('username', function (err, userName) {
cb(err, userName); cb(err, userName);
}); });
}; };
var _onDisplayNameChanged = []; var _onDisplayNameChanged = [];
var onDisplayNameChanged = common.onDisplayNameChanged = function (h) { common.onDisplayNameChanged = function (h) {
if (typeof(h) !== "function") { return; } if (typeof(h) !== "function") { return; }
if (_onDisplayNameChanged.indexOf(h) !== -1) { return; } if (_onDisplayNameChanged.indexOf(h) !== -1) { return; }
_onDisplayNameChanged.push(h); _onDisplayNameChanged.push(h);
}; };
var changeDisplayName = common.changeDisplayName = function (newName) { common.changeDisplayName = function (newName) {
_onDisplayNameChanged.forEach(function (h) { _onDisplayNameChanged.forEach(function (h) {
h(newName); h(newName);
}); });
}; };
// STORAGE // STORAGE
var forgetPad = common.forgetPad = function (href, cb) { common.forgetPad = function (href, cb) {
var parsed = parsePadUrl(href); var parsed = parsePadUrl(href);
var callback = function (err, data) { var callback = function (err) {
if (err) { if (err) {
cb(err); cb(err);
return; return;
@ -495,7 +523,19 @@ define([
} }
}; };
var setPadTitle = common.setPadTitle = function (name, cb) { var updateFileName = function (href, oldName, newName) {
var fo = getStore().getProxy().fo;
var paths = fo.findFileInRoot(href);
paths.forEach(function (path) {
if (path.length !== 2) { return; }
var name = path[1].split('_')[0];
var parsed = parsePadUrl(href);
if (path.length === 2 && name === oldName && isDefaultName(parsed, name)) {
fo.rename(path, newName);
}
});
};
common.setPadTitle = function (name, cb) {
var href = window.location.href; var href = window.location.href;
var parsed = parsePadUrl(href); var parsed = parsePadUrl(href);
href = getRelativeHref(href); href = getRelativeHref(href);
@ -509,12 +549,12 @@ define([
var updateWeaker = []; var updateWeaker = [];
var contains; var contains;
var renamed = recent.map(function (pad) { recent.forEach(function (pad) {
var p = parsePadUrl(pad.href); var p = parsePadUrl(pad.href);
if (p.type !== parsed.type) { return pad; } if (p.type !== parsed.type) { return pad; }
var shouldUpdate = p.hash === parsed.hash; var shouldUpdate = p.hash.replace(/\/$/, '') === parsed.hash.replace(/\/$/, '');
// Version 1 : we have up to 4 differents hash for 1 pad, keep the strongest : // Version 1 : we have up to 4 differents hash for 1 pad, keep the strongest :
// Edit > Edit (present) > View > View (present) // Edit > Edit (present) > View > View (present)
@ -540,6 +580,7 @@ define([
pad.atime = +new Date(); pad.atime = +new Date();
// set the name // set the name
var old = pad.title;
pad.title = name; pad.title = name;
// If we now have a stronger version of a stored href, replace the weaker one by the strong one // If we now have a stronger version of a stored href, replace the weaker one by the strong one
@ -550,14 +591,23 @@ define([
}); });
} }
pad.href = href; pad.href = href;
updateFileName(href, old, name);
} }
return pad; return pad;
}); });
if (!contains) { if (!contains && href) {
var data = makePad(href, name); var data = makePad(href, name);
getStore().pushData(data); getStore().pushData(data, function (e) {
getStore().addPad(href, common.initialPath, common.initialName || name); if (e) {
if (e === 'E_OVER_LIMIT' && AppConfig.enablePinLimit) {
common.alert(Messages.pinLimitNotPinned, null, true);
return;
}
else { throw new Error("Cannot push this pad to CryptDrive", e); }
}
getStore().addPad(data, common.initialPath);
});
} }
if (updateWeaker.length > 0) { if (updateWeaker.length > 0) {
updateWeaker.forEach(function (obj) { updateWeaker.forEach(function (obj) {
@ -584,7 +634,7 @@ define([
/* /*
* Buttons * Buttons
*/ */
var renamePad = common.renamePad = function (title, callback) { common.renamePad = function (title, callback) {
if (title === null) { return; } if (title === null) { return; }
if (title.trim() === "") { if (title.trim() === "") {
@ -592,7 +642,7 @@ define([
title = getDefaultName(parsed); title = getDefaultName(parsed);
} }
common.setPadTitle(title, function (err, data) { common.setPadTitle(title, function (err) {
if (err) { if (err) {
console.log("unable to set pad title"); console.log("unable to set pad title");
console.log(err); console.log(err);
@ -642,7 +692,7 @@ define([
return true; return true;
}; };
var arePinsSynced = common.arePinsSynced = function (cb) { common.arePinsSynced = function (cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); } if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
var list = getCanonicalChannelList(); var list = getCanonicalChannelList();
@ -653,7 +703,7 @@ define([
}); });
}; };
var resetPins = common.resetPins = function (cb) { common.resetPins = function (cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); } if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
var list = getCanonicalChannelList(); var list = getCanonicalChannelList();
@ -663,7 +713,7 @@ define([
}); });
}; };
var pinPads = common.pinPads = function (pads, cb) { common.pinPads = function (pads, cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); } if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
rpc.pin(pads, function (e, hash) { rpc.pin(pads, function (e, hash) {
@ -672,7 +722,7 @@ define([
}); });
}; };
var unpinPads = common.unpinPads = function (pads, cb) { common.unpinPads = function (pads, cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); } if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
rpc.unpin(pads, function (e, hash) { rpc.unpin(pads, function (e, hash) {
@ -681,12 +731,12 @@ define([
}); });
}; };
var getPinnedUsage = common.getPinnedUsage = function (cb) { common.getPinnedUsage = function (cb) {
if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); } if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); }
rpc.getFileListSize(cb); rpc.getFileListSize(cb);
}; };
var getFileSize = common.getFileSize = function (href, cb) { common.getFileSize = function (href, cb) {
var channelId = Hash.hrefToHexChannelId(href); var channelId = Hash.hrefToHexChannelId(href);
rpc.getFileSize(channelId, function (e, bytes) { rpc.getFileSize(channelId, function (e, bytes) {
if (e) { return void cb(e); } if (e) { return void cb(e); }
@ -694,7 +744,30 @@ define([
}); });
}; };
var createButton = common.createButton = function (type, rightside, data, callback) { common.getPinLimit = function (cb) {
cb(void 0, typeof(AppConfig.pinLimit) === 'number'? AppConfig.pinLimit: 1000);
};
common.isOverPinLimit = function (cb) {
if (!common.isLoggedIn() || !AppConfig.enablePinLimit) { return void cb(null, false); }
var usage;
var andThen = function (e, limit) {
if (e) { return void cb(e); }
var data = {usage: usage, limit: limit};
if (usage > limit) {
return void cb (null, true, data);
}
return void cb (null, false, data);
};
var todo = function (e, used) {
usage = common.bytesToMegabytes(used);
if (e) { return void cb(e); }
common.getPinLimit(andThen);
};
common.getPinnedUsage(todo);
};
common.createButton = function (type, rightside, data, callback) {
var button; var button;
var size = "17px"; var size = "17px";
switch (type) { switch (type) {
@ -783,7 +856,7 @@ define([
var href = window.location.href; var href = window.location.href;
common.confirm(Messages.forgetPrompt, function (yes) { common.confirm(Messages.forgetPrompt, function (yes) {
if (!yes) { return; } if (!yes) { return; }
common.forgetPad(href, function (err, data) { common.forgetPad(href, function (err) {
if (err) { if (err) {
console.log("unable to forget pad"); console.log("unable to forget pad");
console.error(err); console.error(err);
@ -1002,7 +1075,7 @@ define([
// Provide $container if you want to put the generated block in another element // Provide $container if you want to put the generated block in another element
// Provide $initBlock if you already have the menu block and you want the content inserted in it // Provide $initBlock if you already have the menu block and you want the content inserted in it
var createLanguageSelector = common.createLanguageSelector = function ($container, $initBlock) { common.createLanguageSelector = function ($container, $initBlock) {
var options = []; var options = [];
var languages = Messages._languages; var languages = Messages._languages;
var keys = Object.keys(languages).sort(); var keys = Object.keys(languages).sort();
@ -1036,7 +1109,7 @@ define([
return $block; return $block;
}; };
var createUserAdminMenu = common.createUserAdminMenu = function (config) { common.createUserAdminMenu = function (config) {
var $displayedName = $('<span>', {'class': config.displayNameCls || 'displayName'}); var $displayedName = $('<span>', {'class': config.displayNameCls || 'displayName'});
var accountName = localStorage[common.userNameKey]; var accountName = localStorage[common.userNameKey];
var account = isLoggedIn(); var account = isLoggedIn();
@ -1123,24 +1196,24 @@ define([
}; };
var $userAdmin = createDropdown(dropdownConfigUser); var $userAdmin = createDropdown(dropdownConfigUser);
$userAdmin.find('a.logout').click(function (e) { $userAdmin.find('a.logout').click(function () {
common.logout(); common.logout();
window.location.href = '/'; window.location.href = '/';
}); });
$userAdmin.find('a.settings').click(function (e) { $userAdmin.find('a.settings').click(function () {
if (parsed && parsed.type) { if (parsed && parsed.type) {
window.open('/settings/'); window.open('/settings/');
} else { } else {
window.location.href = '/settings/'; window.location.href = '/settings/';
} }
}); });
$userAdmin.find('a.login').click(function (e) { $userAdmin.find('a.login').click(function () {
if (window.location.pathname !== "/") { if (window.location.pathname !== "/") {
sessionStorage.redirectTo = window.location.href; sessionStorage.redirectTo = window.location.href;
} }
window.location.href = '/login/'; window.location.href = '/login/';
}); });
$userAdmin.find('a.register').click(function (e) { $userAdmin.find('a.register').click(function () {
if (window.location.pathname !== "/") { if (window.location.pathname !== "/") {
sessionStorage.redirectTo = window.location.href; sessionStorage.redirectTo = window.location.href;
} }
@ -1189,6 +1262,11 @@ define([
if (typeof(window.Proxy) === 'undefined') { if (typeof(window.Proxy) === 'undefined') {
feedback("NO_PROXIES"); feedback("NO_PROXIES");
} }
if (typeof(Array.isArray) !== 'function') {
feedback("NO_ISARRAY");
}
$(function() { $(function() {
// Race condition : if document.body is undefined when alertify.js is loaded, Alertify // 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" // won't work. We have to reset it now to make sure it uses a correct "body"
@ -1227,7 +1305,8 @@ define([
common.arePinsSynced(function (err, yes) { common.arePinsSynced(function (err, yes) {
if (!yes) { if (!yes) {
common.resetPins(function (err, hash) { common.resetPins(function (err) {
if (err) { console.error(err); }
console.log('RESET DONE'); console.log('RESET DONE');
}); });
} }

@ -1,9 +1,7 @@
define([ define([
'/common/treesome.js', '/common/treesome.js',
'/bower_components/rangy/rangy-core.min.js' '/bower_components/rangy/rangy-core.min.js'
], function (Tree, Rangy, saveRestore) { ], function (Tree, Rangy) {
var log = function (x) { console.log(x); };
var error = function (x) { console.log(x); };
var verbose = function (x) { if (window.verboseMode) { console.log(x); } }; var verbose = function (x) { if (window.verboseMode) { console.log(x); } };
/* accepts the document used by the editor */ /* accepts the document used by the editor */
@ -45,7 +43,7 @@ define([
}); });
}; };
var exists = cursor.exists = function () { cursor.exists = function () {
return (Range.start.el?1:0) | (Range.end.el?2:0); return (Range.start.el?1:0) | (Range.end.el?2:0);
}; };
@ -55,7 +53,7 @@ define([
2 if end 2 if end
3 if start and end 3 if start and end
*/ */
var inNode = cursor.inNode = function (el) { cursor.inNode = function (el) {
var state = ['start', 'end'].map(function (pos, i) { var state = ['start', 'end'].map(function (pos, i) {
return Tree.contains(el, Range[pos].el)? i +1: 0; return Tree.contains(el, Range[pos].el)? i +1: 0;
}); });
@ -122,7 +120,7 @@ define([
} }
}; };
var pushDelta = cursor.pushDelta = function (oldVal, newVal, offset) { cursor.pushDelta = function (oldVal, newVal) {
if (oldVal === newVal) { return; } if (oldVal === newVal) { return; }
var commonStart = 0; var commonStart = 0;
while (oldVal.charAt(commonStart) === newVal.charAt(commonStart)) { while (oldVal.charAt(commonStart) === newVal.charAt(commonStart)) {

@ -1,7 +1,7 @@
define([], function () { define([], function () {
var exports = {}; var exports = {};
var hexToUint8Array = exports.hexToUint8Array = function (s) { exports.hexToUint8Array = function (s) {
// if not hex or odd number of characters // if not hex or odd number of characters
if (!/[a-fA-F0-9]+/.test(s) || s.length % 2) { throw new Error("string is not hex"); } if (!/[a-fA-F0-9]+/.test(s) || s.length % 2) { throw new Error("string is not hex"); }
return s.split(/([0-9a-fA-F]{2})/) return s.split(/([0-9a-fA-F]{2})/)
@ -9,7 +9,7 @@ define([], function () {
.map(function (x) { return Number('0x' + x); }); .map(function (x) { return Number('0x' + x); });
}; };
var uint8ArrayToHex = exports.uint8ArrayToHex = function (a) { exports.uint8ArrayToHex = function (a) {
return a.reduce(function(memo, i) { return a.reduce(function(memo, i) {
return memo + ((i < 16) ? '0' : '') + i.toString(16); return memo + ((i < 16) ? '0' : '') + i.toString(16);
}, ''); }, '');

@ -10,7 +10,7 @@ define([
var TRASH = module.TRASH = "trash"; var TRASH = module.TRASH = "trash";
var TEMPLATE = module.TEMPLATE = "template"; var TEMPLATE = module.TEMPLATE = "template";
var init = module.init = function (files, config) { module.init = function (files, config) {
var Cryptpad = config.Cryptpad; var Cryptpad = config.Cryptpad;
Messages = Cryptpad.Messages; Messages = Cryptpad.Messages;
@ -18,7 +18,7 @@ define([
var NEW_FOLDER_NAME = Messages.fm_newFolder; var NEW_FOLDER_NAME = Messages.fm_newFolder;
var NEW_FILE_NAME = Messages.fm_newFile; var NEW_FILE_NAME = Messages.fm_newFile;
var DEBUG = config.DEBUG || false; //var DEBUG = config.DEBUG || false;
var logging = function () { var logging = function () {
console.log.apply(console, arguments); console.log.apply(console, arguments);
}; };
@ -34,7 +34,7 @@ define([
console.error.apply(console, arguments); console.error.apply(console, arguments);
}; };
var getStructure = exp.getStructure = function () { exp.getStructure = function () {
var a = {}; var a = {};
a[ROOT] = {}; a[ROOT] = {};
a[UNSORTED] = []; a[UNSORTED] = [];
@ -92,7 +92,7 @@ define([
return path[0] === TRASH && path.length === 4; return path[0] === TRASH && path.length === 4;
}; };
var isPathInFilesData = exp.isPathInFilesData = function (path) { exp.isPathInFilesData = function (path) {
return path[0] && path[0] === FILES_DATA; return path[0] && path[0] === FILES_DATA;
}; };
@ -100,7 +100,7 @@ define([
return typeof(element) === "string"; return typeof(element) === "string";
}; };
var isReadOnlyFile = exp.isReadOnlyFile = function (element) { exp.isReadOnlyFile = function (element) {
if (!isFile(element)) { return false; } if (!isFile(element)) { return false; }
var parsed = Cryptpad.parsePadUrl(element); var parsed = Cryptpad.parsePadUrl(element);
if (!parsed) { return false; } if (!parsed) { return false; }
@ -114,15 +114,15 @@ define([
return typeof(element) !== "string"; return typeof(element) !== "string";
}; };
var isFolderEmpty = exp.isFolderEmpty = function (element) { exp.isFolderEmpty = function (element) {
if (typeof(element) !== "object") { return false; } if (typeof(element) !== "object") { return false; }
return Object.keys(element).length === 0; return Object.keys(element).length === 0;
}; };
var hasSubfolder = exp.hasSubfolder = function (element, trashRoot) { exp.hasSubfolder = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; } if (typeof(element) !== "object") { return false; }
var subfolder = 0; var subfolder = 0;
var addSubfolder = function (el, idx) { var addSubfolder = function (el) {
subfolder += isFolder(el.element) ? 1 : 0; subfolder += isFolder(el.element) ? 1 : 0;
}; };
for (var f in element) { for (var f in element) {
@ -137,10 +137,10 @@ define([
return subfolder; return subfolder;
}; };
var hasFile = exp.hasFile = function (element, trashRoot) { exp.hasFile = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; } if (typeof(element) !== "object") { return false; }
var file = 0; var file = 0;
var addFile = function (el, idx) { var addFile = function (el) {
file += isFile(el.element) ? 1 : 0; file += isFile(el.element) ? 1 : 0;
}; };
for (var f in element) { for (var f in element) {
@ -189,10 +189,10 @@ define([
return inTree; return inTree;
}; };
var isFileInTrash = function (file) { /* var isFileInTrash = function (file) {
var inTrash = false; var inTrash = false;
var root = files[TRASH]; var root = files[TRASH];
var filter = function (trashEl, idx) { var filter = function (trashEl) {
inTrash = isFileInTree(file, trashEl.element); inTrash = isFileInTree(file, trashEl.element);
return inTrash; return inTrash;
}; };
@ -205,11 +205,7 @@ define([
if (inTrash) { break; } if (inTrash) { break; }
} }
return inTrash; return inTrash;
}; };*/
var isFileInUnsorted = function (file) {
return files[UNSORTED].indexOf(file) !== -1;
};
var getUnsortedFiles = exp.getUnsortedFiles = function () { var getUnsortedFiles = exp.getUnsortedFiles = function () {
if (!files[UNSORTED]) { if (!files[UNSORTED]) {
@ -244,7 +240,7 @@ define([
var getTrashFiles = exp.getTrashFiles = function () { var getTrashFiles = exp.getTrashFiles = function () {
var root = files[TRASH]; var root = files[TRASH];
var ret = []; var ret = [];
var addFiles = function (el, idx) { var addFiles = function (el) {
if (isFile(el.element)) { if (isFile(el.element)) {
if(ret.indexOf(el.element) === -1) { ret.push(el.element); } if(ret.indexOf(el.element) === -1) { ret.push(el.element); }
} else { } else {
@ -261,7 +257,7 @@ define([
return ret; return ret;
}; };
var getFilesDataFiles = exp.getFilesDataFiles = function () { exp.getFilesDataFiles = function () {
var ret = []; var ret = [];
files[FILES_DATA].forEach(function (el) { files[FILES_DATA].forEach(function (el) {
if (el.href && ret.indexOf(el.href) === -1) { if (el.href && ret.indexOf(el.href) === -1) {
@ -351,7 +347,7 @@ define([
return rootpaths.concat(unsortedpaths, templatepaths, trashpaths); return rootpaths.concat(unsortedpaths, templatepaths, trashpaths);
}; };
var search = exp.search = function (value) { exp.search = function (value) {
if (typeof(value) !== "string") { return []; } if (typeof(value) !== "string") { return []; }
var res = []; var res = [];
// Search in ROOT // Search in ROOT
@ -402,7 +398,7 @@ define([
var ret = []; var ret = [];
res.forEach(function (l) { res.forEach(function (l) {
var paths = findFile(l); //var paths = findFile(l);
ret.push({ ret.push({
paths: findFile(l), paths: findFile(l),
data: exp.getFileData(l) data: exp.getFileData(l)
@ -509,7 +505,7 @@ define([
files[TRASH][obj.name].splice(idx, 1); files[TRASH][obj.name].splice(idx, 1);
}); });
}; };
var deleteMultiplePermanently = exp.deletePathsPermanently = function (paths) { exp.deletePathsPermanently = function (paths) {
var hrefPaths = paths.filter(isPathInHrefArray); var hrefPaths = paths.filter(isPathInHrefArray);
var rootPaths = paths.filter(isPathInRoot); var rootPaths = paths.filter(isPathInRoot);
var trashPaths = paths.filter(isPathInTrash); var trashPaths = paths.filter(isPathInTrash);
@ -723,7 +719,7 @@ define([
if (cb) { cb(); } if (cb) { cb(); }
}; };
var moveElements = exp.moveElements = function (paths, newParentPath, cb) { exp.moveElements = function (paths, newParentPath, cb) {
var unsortedPaths = paths.filter(isPathInHrefArray); var unsortedPaths = paths.filter(isPathInHrefArray);
moveHrefArrayElements(unsortedPaths, newParentPath); moveHrefArrayElements(unsortedPaths, newParentPath);
// Copy the elements to their new location // Copy the elements to their new location
@ -735,7 +731,7 @@ define([
}; };
// Import elements in the file manager // Import elements in the file manager
var importElements = exp.importElements = function (elements, path, cb) { exp.importElements = function (elements, path, cb) {
if (!elements || elements.length === 0) { return; } if (!elements || elements.length === 0) { return; }
var newParent = findElement(files, path); var newParent = findElement(files, path);
if (!newParent) { debug("Trying to import elements into a non-existing folder"); return; } if (!newParent) { debug("Trying to import elements into a non-existing folder"); return; }
@ -748,7 +744,7 @@ define([
if(cb) { cb(); } if(cb) { cb(); }
}; };
var createNewFolder = exp.createNewFolder = function (folderPath, name, cb) { exp.createNewFolder = function (folderPath, name, cb) {
var parentEl = findElement(files, folderPath); var parentEl = findElement(files, folderPath);
var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME); var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME);
parentEl[folderName] = {}; parentEl[folderName] = {};
@ -767,7 +763,7 @@ define([
ctime: +new Date() ctime: +new Date()
}); });
}; };
var createNewFile = exp.createNewFile = function (filePath, name, type, cb) { exp.createNewFile = function (filePath, name, type, cb) {
var parentEl = findElement(files, filePath); var parentEl = findElement(files, filePath);
var fileName = getAvailableName(parentEl, name || NEW_FILE_NAME); var fileName = getAvailableName(parentEl, name || NEW_FILE_NAME);
var href = '/' + type + '/#' + Cryptpad.createRandomHash(); var href = '/' + type + '/#' + Cryptpad.createRandomHash();
@ -799,7 +795,7 @@ define([
}; };
// Restore an element (copy it elsewhere and remove from the trash root) // Restore an element (copy it elsewhere and remove from the trash root)
var restoreTrash = exp.restoreTrash = function (path, cb) { exp.restoreTrash = function (path, cb) {
if (!path || path.length !== 4 || path[0] !== TRASH) { if (!path || path.length !== 4 || path[0] !== TRASH) {
debug("restoreTrash was called from an element not in the trash root: ", path); debug("restoreTrash was called from an element not in the trash root: ", path);
return; return;
@ -838,7 +834,7 @@ define([
// Remove the last element from the path to get the parent path and the element name // Remove the last element from the path to get the parent path and the element name
var parentPath = path.slice(); var parentPath = path.slice();
var name; var name;
var element = findElement(files, path); //var element = findElement(files, path);
if (path.length === 4) { // Trash root if (path.length === 4) { // Trash root
name = path[1]; name = path[1];
parentPath.pop(); parentPath.pop();
@ -860,13 +856,13 @@ define([
if(cb) { cb(); } if(cb) { cb(); }
}; };
var emptyTrash = exp.emptyTrash = function (cb) { exp.emptyTrash = function (cb) {
files[TRASH] = {}; files[TRASH] = {};
checkDeletedFiles(); checkDeletedFiles();
if(cb) { cb(); } if(cb) { cb(); }
}; };
var deleteFileData = exp.deleteFileData = function (href, cb) { exp.deleteFileData = function (href, cb) {
if (workgroup) { return; } if (workgroup) { return; }
var toRemove = []; var toRemove = [];
@ -889,7 +885,7 @@ define([
if(cb) { cb(); } if(cb) { cb(); }
}; };
var renameElement = exp.renameElement = function (path, newName, cb) { exp.renameElement = function (path, newName, cb) {
if (path.length <= 1) { if (path.length <= 1) {
logError('Renaming `root` is forbidden'); logError('Renaming `root` is forbidden');
return; return;
@ -914,7 +910,7 @@ define([
}; };
var forgetPad = exp.forgetPad = function (href) { exp.forgetPad = function (href) {
if (workgroup) { return; } if (workgroup) { return; }
if (!href || !isFile(href)) { return; } if (!href || !isFile(href)) { return; }
var path; var path;
@ -985,7 +981,7 @@ define([
}; };
// Replace a href by a stronger one everywhere in the drive (except FILES_DATA) // Replace a href by a stronger one everywhere in the drive (except FILES_DATA)
var replaceHref = exp.replaceHref = function (o, n) { exp.replaceHref = function (o, n) {
if (!isFile(o) || !isFile(n)) { return; } if (!isFile(o) || !isFile(n)) { return; }
var paths = findFile(o); var paths = findFile(o);
@ -1012,7 +1008,7 @@ define([
// addTemplate is called when we want to add a new pad, never visited, to the templates list // addTemplate is called when we want to add a new pad, never visited, to the templates list
// first, we must add it to FILES_DATA, so the input has to be an fileDAta object // first, we must add it to FILES_DATA, so the input has to be an fileDAta object
var addTemplate = exp.addTemplate = function (fileData) { exp.addTemplate = function (fileData) {
if (workgroup) { return; } if (workgroup) { return; }
if (typeof fileData !== "object" || !fileData.href || !fileData.title) { if (typeof fileData !== "object" || !fileData.href || !fileData.title) {
console.error("filedata object expected to add a new template"); console.error("filedata object expected to add a new template");
@ -1031,7 +1027,7 @@ define([
} }
}; };
var listTemplates = exp.listTemplates = function (type) { exp.listTemplates = function () {
if (workgroup) { return; } if (workgroup) { return; }
var templateFiles = getTemplateFiles(); var templateFiles = getTemplateFiles();
var res = []; var res = [];
@ -1049,7 +1045,7 @@ define([
}); });
}; };
var fixFiles = exp.fixFiles = function () { exp.fixFiles = function () {
// Explore the tree and check that everything is correct: // Explore the tree and check that everything is correct:
// * 'root', 'trash', 'unsorted' and 'filesData' exist and are objects // * 'root', 'trash', 'unsorted' and 'filesData' exist and are objects
// * ROOT: Folders are objects, files are href // * ROOT: Folders are objects, files are href
@ -1138,7 +1134,7 @@ define([
var templateFiles = getTemplateFiles(); var templateFiles = getTemplateFiles();
var trashFiles = getTrashFiles(); var trashFiles = getTrashFiles();
var toClean = []; var toClean = [];
fd.forEach(function (el, idx) { fd.forEach(function (el) {
if (!el || typeof(el) !== "object") { if (!el || typeof(el) !== "object") {
debug("An element in filesData was not an object.", el); debug("An element in filesData was not an object.", el);
toClean.push(el); toClean.push(el);

@ -88,8 +88,8 @@ define([
ret.removeData = filesOp.removeData; ret.removeData = filesOp.removeData;
ret.pushData = filesOp.pushData; ret.pushData = filesOp.pushData;
ret.addPad = function (href, path, name) { ret.addPad = function (data, path) {
filesOp.add(href, path, name); filesOp.add(data, path);
}; };
ret.forgetPad = function (href, cb) { ret.forgetPad = function (href, cb) {
@ -127,13 +127,21 @@ define([
return filesOp.replace(o, n); return filesOp.replace(o, n);
}; };
var changeHandlers = ret.changeHandlers = []; ret.changeHandlers = [];
ret.change = function (f) {}; ret.change = function () {};
return ret; return ret;
}; };
var tryParsing = function (x) {
try { return JSON.parse(x); }
catch (e) {
console.error(e);
return null;
}
};
var onReady = function (f, proxy, Cryptpad, exp) { var onReady = function (f, proxy, Cryptpad, exp) {
var fo = exp.fo = FO.init(proxy.drive, { var fo = exp.fo = FO.init(proxy.drive, {
Cryptpad: Cryptpad Cryptpad: Cryptpad
@ -145,6 +153,37 @@ define([
f(void 0, store); f(void 0, store);
} }
var requestLogin = function () {
// log out so that you don't go into an endless loop...
Cryptpad.logout();
// redirect them to log in, and come back when they're done.
sessionStorage.redirectTo = window.location.href;
window.location.href = '/login/';
};
var tokenKey = 'loginToken';
if (Cryptpad.isLoggedIn()) {
/* This isn't truly secure, since anyone who can read the user's object can
set their local loginToken to match that in the object. However, it exposes
a UI that will work most of the time. */
// every user object should have a persistent, random number
if (typeof(proxy.loginToken) !== 'number') {
proxy[tokenKey] = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);
}
var localToken = tryParsing(localStorage.getItem(tokenKey));
if (localToken === null) {
// if that number hasn't been set to localStorage, do so.
localStorage.setItem(tokenKey, proxy.loginToken);
} else if (localToken !== proxy[tokenKey]) {
// if it has been, and the local number doesn't match that in
// the user object, request that they reauthenticate.
return void requestLogin();
}
}
if (typeof(proxy.allowUserFeedback) !== 'boolean') { if (typeof(proxy.allowUserFeedback) !== 'boolean') {
proxy.allowUserFeedback = true; proxy.allowUserFeedback = true;
} }
@ -157,19 +196,20 @@ define([
// if the user is logged in, but does not have signing keys... // if the user is logged in, but does not have signing keys...
if (Cryptpad.isLoggedIn() && !Cryptpad.hasSigningKeys(proxy)) { if (Cryptpad.isLoggedIn() && !Cryptpad.hasSigningKeys(proxy)) {
// log out so that you don't go into an endless loop... return void requestLogin();
Cryptpad.logout();
// redirect them to log in, and come back when they're done.
sessionStorage.redirectTo = window.location.href;
window.location.href = '/login/';
return;
} }
proxy.on('change', [Cryptpad.displayNameKey], function (o, n, p) { proxy.on('change', [Cryptpad.displayNameKey], function (o, n) {
if (typeof(n) !== "string") { return; } if (typeof(n) !== "string") { return; }
Cryptpad.changeDisplayName(n); Cryptpad.changeDisplayName(n);
}); });
proxy.on('change', [tokenKey], function () {
console.log('wut');
var localToken = tryParsing(localStorage.getItem(tokenKey));
if (localToken !== proxy[tokenKey]) {
return void requestLogin();
}
});
}; };
var initialized = false; var initialized = false;
@ -197,7 +237,6 @@ define([
var exp = {}; var exp = {};
window.addEventListener('storage', function (e) { window.addEventListener('storage', function (e) {
var key = e.key;
if (e.key !== Cryptpad.userHashKey) { return; } if (e.key !== Cryptpad.userHashKey) { return; }
var o = e.oldValue; var o = e.oldValue;
var n = e.newValue; var n = e.newValue;

@ -22,7 +22,7 @@ define([
// 16 bytes for a deterministic channel key // 16 bytes for a deterministic channel key
var channelSeed = dispense(16); var channelSeed = dispense(16);
// 32 bytes for a curve key // 32 bytes for a curve key
var curveSeed = opt.curveSeed = dispense(32); opt.curveSeed = dispense(32);
// 32 more for a signing key // 32 more for a signing key
var edSeed = opt.edSeed = dispense(32); var edSeed = opt.edSeed = dispense(32);
@ -43,9 +43,9 @@ define([
// should never happen // should never happen
if (channelHex.length !== 32) { throw new Error('invalid channel id'); } if (channelHex.length !== 32) { throw new Error('invalid channel id'); }
var channel64 = opt.channel64 = Cryptpad.hexToBase64(channelHex); opt.channel64 = Cryptpad.hexToBase64(channelHex);
var userHash = opt.userHash = '/1/edit/' + [opt.channel64, opt.keys.editKeyStr].join('/'); opt.userHash = '/1/edit/' + [opt.channel64, opt.keys.editKeyStr].join('/');
return opt; return opt;
}; };
@ -62,7 +62,7 @@ define([
var rt = opt.rt = Listmap.create(config); var rt = opt.rt = Listmap.create(config);
rt.proxy rt.proxy
.on('ready', function (info) { .on('ready', function () {
cb(void 0, rt); cb(void 0, rt);
}) })
.on('disconnect', function (info) { .on('disconnect', function (info) {

File diff suppressed because one or more lines are too long

@ -46,7 +46,7 @@ define([
var merge = function (obj1, obj2, keepOld) { var merge = function (obj1, obj2, keepOld) {
if (typeof (obj1) !== "object" || typeof (obj2) !== "object") { return; } if (typeof (obj1) !== "object" || typeof (obj2) !== "object") { return; }
Object.keys(obj2).forEach(function (k) { Object.keys(obj2).forEach(function (k) {
var v = obj2[k]; //var v = obj2[k];
// If one of them is not an object or if we have a map and a array, don't override, create a new key // If one of them is not an object or if we have a map and a array, don't override, create a new key
if (!obj1[k] || typeof(obj1[k]) !== "object" || typeof(obj2[k]) !== "object" || if (!obj1[k] || typeof(obj1[k]) !== "object" || typeof(obj2[k]) !== "object" ||
(getType(obj1[k]) !== getType(obj2[k]))) { (getType(obj1[k]) !== getType(obj2[k]))) {
@ -80,7 +80,7 @@ define([
path.pop(); path.pop();
} }
var p, next, nextRoot; var next, nextRoot;
path.forEach(function (p, i) { path.forEach(function (p, i) {
if (!root) { return; } if (!root) { return; }
if (typeof(p) === "string") { if (typeof(p) === "string") {
@ -128,7 +128,7 @@ define([
}); });
}; };
var mergeAnonDrive = exp.anonDriveIntoUser = function (proxy, cb) { exp.anonDriveIntoUser = function (proxy, cb) {
// Make sure we have an FS_hash and we don't use it, otherwise just stop the migration and cb // Make sure we have an FS_hash and we don't use it, otherwise just stop the migration and cb
if (!localStorage.FS_hash || !Cryptpad.isLoggedIn()) { if (!localStorage.FS_hash || !Cryptpad.isLoggedIn()) {
if (typeof(cb) === "function") { cb(); } if (typeof(cb) === "function") { cb(); }

@ -132,7 +132,7 @@ define(function () {
}; };
}); });
var extensionOf = Modes.extensionOf = function (mode) { Modes.extensionOf = function (mode) {
var ext = ''; var ext = '';
list.some(function (o) { list.some(function (o) {
if (o.mode !== mode) { return; } if (o.mode !== mode) { return; }

@ -16,14 +16,14 @@
}); });
}; };
var create = Module.create = function (msg, title, icon) { var create = Module.create = function (msg, title) {
return new Notification(title,{ return new Notification(title,{
// icon: icon, // icon: icon,
body: msg, body: msg,
}); });
}; };
var system = Module.system = function (msg, title, icon) { Module.system = function (msg, title, icon) {
// Let's check if the browser supports notifications // Let's check if the browser supports notifications
if (!isSupported()) { console.log("Notifications are not supported"); } if (!isSupported()) { console.log("Notifications are not supported"); }
@ -41,7 +41,7 @@
} }
}; };
var tab = Module.tab = function (frequency, count) { Module.tab = function (frequency, count) {
var key = '_pendingTabNotification'; var key = '_pendingTabNotification';
var favicon = document.getElementById('favicon'); var favicon = document.getElementById('favicon');

@ -1,7 +1,6 @@
define([ define([
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
], function () { ], function () {
var MAX_LAG_BEFORE_TIMEOUT = 30000;
var Nacl = window.nacl; var Nacl = window.nacl;
var uid = function () { var uid = function () {
@ -102,9 +101,16 @@ types of messages:
timeouts: {}, // timeouts timeouts: {}, // timeouts
pending: {}, // callbacks pending: {}, // callbacks
cookie: null, cookie: null,
connected: true,
}; };
var send = function (type, msg, cb) { var send = function (type, msg, cb) {
if (!ctx.connected && type !== 'COOKIE') {
return void window.setTimeout(function () {
cb('DISCONNECTED');
});
}
// construct a signed message... // construct a signed message...
var data = [type, msg]; var data = [type, msg];
@ -123,11 +129,22 @@ types of messages:
return sendMsg(ctx, data, cb); return sendMsg(ctx, data, cb);
}; };
network.on('message', function (msg, sender) { network.on('message', function (msg) {
onMsg(ctx, msg); onMsg(ctx, msg);
}); });
send('COOKIE', "", function (e, msg) { network.on('disconnect', function () {
ctx.connected = false;
});
network.on('reconnect', function () {
send('COOKIE', "", function (e) {
if (e) { return void cb(e); }
ctx.connected = true;
});
});
send('COOKIE', "", function (e) {
if (e) { return void cb(e); } if (e) { return void cb(e); }
// callback to provide 'send' method to whatever needs it // callback to provide 'send' method to whatever needs it
cb(void 0, { send: send, }); cb(void 0, { send: send, });

@ -16,6 +16,8 @@ define([
/** Id of the div containing the lag info. */ /** Id of the div containing the lag info. */
var LAG_ELEM_CLS = Bar.constants.lag = 'cryptpad-lag'; var LAG_ELEM_CLS = Bar.constants.lag = 'cryptpad-lag';
var LIMIT_ELEM_CLS = Bar.constants.lag = 'cryptpad-limit';
/** The toolbar class which contains the user list, debug link and lag. */ /** The toolbar class which contains the user list, debug link and lag. */
var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar'; var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar';
@ -94,14 +96,14 @@ define([
var createSpinner = function ($container, config) { var createSpinner = function ($container, config) {
if (config.displayed.indexOf('spinner') !== -1) { if (config.displayed.indexOf('spinner') !== -1) {
var $spin = $('<span>'); var $spin = $('<span>', {'class':SPINNER_CLS});
var $spinner = $('<span>', { var $spinner = $('<span>', {
id: uid(), id: uid(),
'class': SPINNER_CLS + ' spin fa fa-spinner fa-pulse', 'class': 'spin fa fa-spinner fa-pulse',
}).appendTo($spin).hide(); }).appendTo($spin).hide();
$('<span>', { $('<span>', {
id: uid(), id: uid(),
'class': SPINNER_CLS + ' synced fa fa-check', 'class': 'synced fa fa-check',
title: Messages.synced title: Messages.synced
}).appendTo($spin); }).appendTo($spin);
$container.prepend($spin); $container.prepend($spin);
@ -205,6 +207,13 @@ define([
}); });
} }
} }
if (hashes.fileHash) {
options.push({
tag: 'a',
attributes: {title: Messages.viewShareTitle, 'class': 'fileShare'},
content: '<span class="fa fa-eye"></span> ' + Messages.viewShare
});
}
var dropdownConfigShare = { var dropdownConfigShare = {
text: $('<div>').append($shareIcon).append($span).html(), text: $('<div>').append($shareIcon).append($span).html(),
options: options options: options
@ -223,7 +232,14 @@ define([
} }
if (hashes.viewHash) { if (hashes.viewHash) {
$shareBlock.find('a.viewShare').click(function () { $shareBlock.find('a.viewShare').click(function () {
var url = window.location.origin + window.location.pathname + '#' + hashes.viewHash; var url = window.location.origin + window.location.pathname + '#' + hashes.viewHash ;
var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
}
if (hashes.fileHash) {
$shareBlock.find('a.fileShare').click(function () {
var url = window.location.origin + window.location.pathname + '#' + hashes.fileHash ;
var success = Cryptpad.Clipboard.copy(url); var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); } if (success) { Cryptpad.log(Messages.shareSuccess); }
}); });
@ -372,7 +388,7 @@ define([
'class': LAG_ELEM_CLS, 'class': LAG_ELEM_CLS,
id: uid(), id: uid(),
}); });
var $a = $('<span>', {id: 'newLag'}); var $a = $('<span>', {'class': 'cryptpad-lag', id: 'newLag'});
$('<span>', {'class': 'bar1'}).appendTo($a); $('<span>', {'class': 'bar1'}).appendTo($a);
$('<span>', {'class': 'bar2'}).appendTo($a); $('<span>', {'class': 'bar2'}).appendTo($a);
$('<span>', {'class': 'bar3'}).appendTo($a); $('<span>', {'class': 'bar3'}).appendTo($a);
@ -391,7 +407,7 @@ define([
var title; var title;
var $lag = $(lagElement); var $lag = $(lagElement);
if (lag) { if (lag) {
$lag.attr('class', ''); $lag.attr('class', 'cryptpad-lag');
firstConnection = false; firstConnection = false;
title = Messages.lag + ' : ' + lag + ' ms\n'; title = Messages.lag + ' : ' + lag + ' ms\n';
if (lag > 30000) { if (lag > 30000) {
@ -412,7 +428,7 @@ define([
} }
} }
else if (!firstConnection) { else if (!firstConnection) {
$lag.attr('class', ''); $lag.attr('class', 'cryptpad-lag');
// Display the red light at the 2nd failed attemp to get the lag // Display the red light at the 2nd failed attemp to get the lag
lagLight.addClass('lag-red'); lagLight.addClass('lag-red');
title = Messages.redLight; title = Messages.redLight;
@ -474,6 +490,24 @@ define([
$userContainer.append($lag); $userContainer.append($lag);
} }
if (config.displayed.indexOf('limit') !== -1 && Config.enablePinning) {
var usage;
var $limitIcon = $('<span>', {'class': 'fa fa-exclamation-triangle'});
var $limit = $('<span>', {
'class': LIMIT_ELEM_CLS,
'title': Messages.pinLimitReached
}).append($limitIcon).hide().appendTo($userContainer);
var todo = function (e, overLimit) {
if (e) { return void console.error("Unable to get the pinned usage"); }
if (overLimit) {
$limit.show().click(function () {
Cryptpad.alert(Messages.pinLimitReachedAlert, null, true);
});
}
};
Cryptpad.isOverPinLimit(todo);
}
if (config.displayed.indexOf('newpad') !== -1) { if (config.displayed.indexOf('newpad') !== -1) {
var pads_options = []; var pads_options = [];
Config.availablePadTypes.forEach(function (p) { Config.availablePadTypes.forEach(function (p) {
@ -504,7 +538,7 @@ define([
// User dropdown // User dropdown
if (config.displayed.indexOf('useradmin') !== -1) { if (config.displayed.indexOf('useradmin') !== -1) {
var userMenuCfg = {}; var userMenuCfg = {};
if (config.userData) { if (!config.hideDisplayName) {
userMenuCfg = { userMenuCfg = {
displayNameCls: USERNAME_CLS, displayNameCls: USERNAME_CLS,
changeNameButtonCls: USERBUTTON_CLS, changeNameButtonCls: USERBUTTON_CLS,
@ -524,7 +558,8 @@ define([
$userButton.click(function (e) { $userButton.click(function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
Cryptpad.getLastName(function (lastName) { Cryptpad.getLastName(function (err, lastName) {
if (err) { return void console.error("Cannot get last name", err); }
Cryptpad.prompt(Messages.changeNamePrompt, lastName || '', function (newName) { Cryptpad.prompt(Messages.changeNamePrompt, lastName || '', function (newName) {
if (newName === null && typeof(lastName) === "string") { return; } if (newName === null && typeof(lastName) === "string") { return; }
if (newName === null) { newName = ''; } if (newName === null) { newName = ''; }
@ -791,7 +826,7 @@ define([
if (!connected) { return; } if (!connected) { return; }
checkLag(getLag, lagElement); checkLag(getLag, lagElement);
}, 3000); }, 3000);
} } else { connected = true; }
var failed = function () { var failed = function () {
connected = false; connected = false;

@ -0,0 +1,905 @@
define([
'jquery',
'/customize/application_config.js',
'/api/config'
], function ($, Config, ApiConfig) {
var Messages = {};
var Cryptpad;
var Bar = {
constants: {},
};
var SPINNER_DISAPPEAR_TIME = 3000;
// Toolbar parts
var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar';
var TOP_CLS = Bar.constants.top = 'cryptpad-toolbar-top';
var LEFTSIDE_CLS = Bar.constants.leftside = 'cryptpad-toolbar-leftside';
var RIGHTSIDE_CLS = Bar.constants.rightside = 'cryptpad-toolbar-rightside';
var HISTORY_CLS = Bar.constants.history = 'cryptpad-toolbar-history';
// Userlist
var USERLIST_CLS = Bar.constants.userlist = "cryptpad-dropdown-users";
var EDITSHARE_CLS = Bar.constants.editShare = "cryptpad-dropdown-editShare";
var VIEWSHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-viewShare";
var SHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-share";
// Top parts
var USER_CLS = Bar.constants.userAdmin = "cryptpad-user";
var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner';
var STATE_CLS = Bar.constants.state = 'cryptpad-state';
var LAG_CLS = Bar.constants.lag = 'cryptpad-lag';
var LIMIT_CLS = Bar.constants.lag = 'cryptpad-limit';
var TITLE_CLS = Bar.constants.title = "cryptpad-title";
var NEWPAD_CLS = Bar.constants.newpad = "cryptpad-newpad";
// User admin menu
var USERADMIN_CLS = Bar.constants.user = 'cryptpad-user-dropdown';
var USERNAME_CLS = Bar.constants.username = 'cryptpad-toolbar-username';
var READONLY_CLS = Bar.constants.readonly = 'cryptpad-readonly';
var USERBUTTON_CLS = Bar.constants.changeUsername = "cryptpad-change-username";
// Create the toolbar element
var uid = function () {
return 'cryptpad-uid-' + String(Math.random()).substring(2);
};
var styleToolbar = function ($container, href, version) {
href = href || '/customize/toolbar.css' + (version?('?' + version): '');
$.ajax({
url: href,
dataType: 'text',
success: function (data) {
$container.append($('<style>').text(data));
},
});
};
var createRealtimeToolbar = function (config) {
if (!config.$container) { return; }
var $container = config.$container;
var $toolbar = $('<div>', {
'class': TOOLBAR_CLS,
id: uid(),
});
var $topContainer = $('<div>', {'class': TOP_CLS});
var $userContainer = $('<span>', {
'class': USER_CLS
}).appendTo($topContainer);
$('<span>', {'class': SPINNER_CLS}).hide().appendTo($userContainer);
$('<span>', {'class': STATE_CLS}).hide().appendTo($userContainer);
$('<span>', {'class': LAG_CLS}).hide().appendTo($userContainer);
$('<span>', {'class': LIMIT_CLS}).hide().appendTo($userContainer);
$('<span>', {'class': NEWPAD_CLS + ' dropdown-bar'}).hide().appendTo($userContainer);
$('<span>', {'class': USERADMIN_CLS + ' dropdown-bar'}).hide().appendTo($userContainer);
$toolbar.append($topContainer)
.append($('<div>', {'class': LEFTSIDE_CLS}))
.append($('<div>', {'class': RIGHTSIDE_CLS}))
.append($('<div>', {'class': HISTORY_CLS}));
// The 'notitle' class removes the line added for the title with a small screen
if (!config.title || typeof config.title !== "object") {
$toolbar.addClass('notitle');
}
$container.prepend($toolbar);
if (ApiConfig && ApiConfig.requireConf && ApiConfig.requireConf.urlArgs) {
styleToolbar($container, undefined, ApiConfig.requireConf.urlArgs);
} else {
styleToolbar($container);
}
return $toolbar;
};
// Userlist elements
var checkSynchronizing = function (toolbar, config) {
if (!toolbar.state) { return; }
var userList = config.userList.list.users;
var userNetfluxId = config.userList.userNetfluxId;
var meIdx = userList.indexOf(userNetfluxId);
if (meIdx === -1) {
toolbar.state.text(Messages.synchronizing);
return;
}
toolbar.state.text('');
};
var getOtherUsers = function(config) {
var userList = config.userList.list.users;
var userData = config.userList.data;
var userNetfluxId = config.userList.userNetfluxId;
var i = 0; // duplicates counter
var list = [];
// Display only one time each user (if he is connected in multiple tabs)
var myUid = userData[userNetfluxId] ? userData[userNetfluxId].uid : undefined;
var uids = [];
userList.forEach(function(user) {
if (user !== userNetfluxId) {
var data = userData[user] || {};
var userName = data.name;
var userId = data.uid;
if (userName && uids.indexOf(userId) === -1 && (!myUid || userId !== myUid)) {
uids.push(userId);
list.push(userName);
} else if (userName) { i++; }
}
});
return {
list: list,
duplicates: i
};
};
var arrayIntersect = function(a, b) {
return $.grep(a, function(i) {
return $.inArray(i, b) > -1;
});
};
var updateUserList = function (toolbar, config) {
// Make sure the elements are displayed
var $userButtons = toolbar.userlist;
var userList = config.userList.list.users;
var userData = config.userList.data;
var userNetfluxId = config.userList.userNetfluxId;
var numberOfUsers = userList.length;
// If we are using old pads (readonly unavailable), only editing users are in userList.
// With new pads, we also have readonly users in userList, so we have to intersect with
// the userData to have only the editing users. We can't use userData directly since it
// may contain data about users that have already left the channel.
userList = config.readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData));
// Names of editing users
var others = getOtherUsers(config);
var editUsersNames = others.list;
var duplicates = others.duplicates; // Number of duplicates
var numberOfEditUsers = userList.length - duplicates;
var numberOfViewUsers = numberOfUsers - userList.length;
// Number of anonymous editing users
var anonymous = numberOfEditUsers - editUsersNames.length;
// Update the userlist
var $usersTitle = $('<h2>').text(Messages.users);
var $editUsers = $userButtons.find('.' + USERLIST_CLS);
$editUsers.html('').append($usersTitle);
var $editUsersList = $('<pre>');
// Yourself (edit only)
if (config.readOnly !== 1) {
$editUsers.append('<span class="yourself">' + Messages.yourself + '</span>');
anonymous--;
}
// Editors
$editUsersList.text(editUsersNames.join('\n')); // .text() to avoid XSS
$editUsers.append($editUsersList);
// Anonymous editors
if (anonymous > 0) {
var text = anonymous === 1 ? Messages.anonymousUser : Messages.anonymousUsers;
$editUsers.append('<span class="anonymous">' + anonymous + ' ' + text + '</span>');
}
// Viewers
if (numberOfViewUsers > 0) {
var viewText = '<span class="viewer">';
if (numberOfEditUsers > 0) {
$editUsers.append('<br>');
viewText += Messages.and + ' ';
}
var viewerText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer;
viewText += numberOfViewUsers + ' ' + viewerText + '</span>';
$editUsers.append(viewText);
}
// Update the buttons
var fa_editusers = '<span class="fa fa-users"></span>';
var fa_viewusers = '<span class="fa fa-eye"></span>';
var viewersText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer;
var editorsText = numberOfEditUsers !== 1 ? Messages.editors : Messages.editor;
var $span = $('<span>', {'class': 'large'}).html(fa_editusers + ' ' + numberOfEditUsers + ' ' + editorsText + '&nbsp;&nbsp; ' + fa_viewusers + ' ' + numberOfViewUsers + ' ' + viewersText);
var $spansmall = $('<span>', {'class': 'narrow'}).html(fa_editusers + ' ' + numberOfEditUsers + '&nbsp;&nbsp; ' + fa_viewusers + ' ' + numberOfViewUsers);
$userButtons.find('.buttonTitle').html('').append($span).append($spansmall);
// Change username in useradmin dropdown
if (config.displayed.indexOf('useradmin') !== -1) {
var $userAdminElement = toolbar.$userAdmin;
var $userElement = $userAdminElement.find('.' + USERNAME_CLS);
$userElement.show();
if (config.readOnly === 1) {
$userElement.addClass(READONLY_CLS).text(Messages.readonly);
}
else {
var name = userData[userNetfluxId] && userData[userNetfluxId].name;
if (!name) {
name = Messages.anonymous;
}
$userElement.removeClass(READONLY_CLS).text(name);
}
}
};
var initUserList = function (toolbar, config) {
if (config.userList && config.userList.list && config.userList.userNetfluxId) {
var userList = config.userList.list;
userList.change.push(function () {
var users = userList.users;
if (users.indexOf(config.userList.userNetfluxId) !== -1) {toolbar.connected = true;}
if (!toolbar.connected) { return; }
checkSynchronizing(toolbar, config);
if (config.userList.data) {
updateUserList(toolbar, config);
}
});
}
};
// Create sub-elements
var createUserList = function (toolbar, config) {
if (!config.userList || !config.userList.list ||
!config.userList.data || !config.userList.userNetfluxId) {
throw new Error("You must provide a `userList` object to display the userlist");
}
var dropdownConfig = {
options: [{
tag: 'p',
attributes: {'class': USERLIST_CLS},
}]
};
var $block = Cryptpad.createDropdown(dropdownConfig);
$block.attr('id', 'userButtons');
toolbar.$leftside.prepend($block);
return $block;
};
var createShare = function (toolbar, config) {
var secret = Cryptpad.find(config, ['share', 'secret']);
var channel = Cryptpad.find(config, ['share', 'channel']);
if (!secret || !channel) {
throw new Error("Unable to display the share button: share.secret and share.channel required");
}
Cryptpad.getRecentPads(function (err, recent) {
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'});
var $span = $('<span>', {'class': 'large'}).append(' ' +Messages.shareButton);
var hashes = Cryptpad.getHashes(channel, secret);
var options = [];
// If we have a stronger version in drive, add it and add a redirect button
var stronger = recent && Cryptpad.findStronger(null, recent);
if (stronger) {
var parsed = Cryptpad.parsePadUrl(stronger);
hashes.editHash = parsed.hash;
}
if (hashes.editHash) {
options.push({
tag: 'a',
attributes: {title: Messages.editShareTitle, 'class': 'editShare'},
content: '<span class="fa fa-users"></span> ' + Messages.editShare
});
if (stronger) {
// We're in view mode, display the "open editing link" button
options.push({
tag: 'a',
attributes: {
title: Messages.editOpenTitle,
'class': 'editOpen',
href: window.location.pathname + '#' + hashes.editHash,
target: '_blank'
},
content: '<span class="fa fa-users"></span> ' + Messages.editOpen
});
}
options.push({tag: 'hr'});
}
if (hashes.viewHash) {
options.push({
tag: 'a',
attributes: {title: Messages.viewShareTitle, 'class': 'viewShare'},
content: '<span class="fa fa-eye"></span> ' + Messages.viewShare
});
if (hashes.editHash && !stronger) {
// We're in edit mode, display the "open readonly" button
options.push({
tag: 'a',
attributes: {
title: Messages.viewOpenTitle,
'class': 'viewOpen',
href: window.location.pathname + '#' + hashes.viewHash,
target: '_blank'
},
content: '<span class="fa fa-eye"></span> ' + Messages.viewOpen
});
}
}
if (hashes.fileHash) {
options.push({
tag: 'a',
attributes: {title: Messages.viewShareTitle, 'class': 'fileShare'},
content: '<span class="fa fa-eye"></span> ' + Messages.viewShare
});
}
var dropdownConfigShare = {
text: $('<div>').append($shareIcon).append($span).html(),
options: options
};
var $shareBlock = Cryptpad.createDropdown(dropdownConfigShare);
$shareBlock.find('button').attr('id', 'shareButton');
$shareBlock.find('.dropdown-bar-content').addClass(SHARE_CLS).addClass(EDITSHARE_CLS).addClass(VIEWSHARE_CLS);
if (hashes.editHash) {
$shareBlock.find('a.editShare').click(function () {
var url = window.location.origin + window.location.pathname + '#' + hashes.editHash;
var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
}
if (hashes.viewHash) {
$shareBlock.find('a.viewShare').click(function () {
var url = window.location.origin + window.location.pathname + '#' + hashes.viewHash ;
var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
}
if (hashes.fileHash) {
$shareBlock.find('a.fileShare').click(function () {
var url = window.location.origin + window.location.pathname + '#' + hashes.fileHash ;
var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
}
toolbar.$leftside.append($shareBlock);
toolbar.share = $shareBlock;
});
return "Loading share button";
};
var createFileShare = function (toolbar) {
if (!window.location.hash) {
throw new Error("Unable to display the share button: hash required in the URL");
}
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'});
var $span = $('<span>', {'class': 'large'}).append(' ' +Messages.shareButton);
var $button = $('<button>', {'id': 'shareButton'}).append($shareIcon).append($span);
$button.click(function () {
var url = window.location.href;
var success = Cryptpad.Clipboard.copy(url);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
toolbar.$leftside.append($button);
return $button;
};
var createTitle = function (toolbar, config) {
var $titleContainer = $('<span>', {
id: 'toolbarTitle',
'class': TITLE_CLS
}).appendTo(toolbar.$top);
// TODO: move these functions to toolbar or common?
if (typeof config.title !== "object") {
console.error("config.title", config);
throw new Error("config.title is not an object");
}
var callback = config.title.onRename;
var placeholder = config.title.defaultName;
var suggestName = config.title.suggestName;
// Buttons
var $text = $('<span>', {
'class': 'title'
}).appendTo($titleContainer);
var $pencilIcon = $('<span>', {
'class': 'pencilIcon',
'title': Messages.clickToEdit
});
if (config.readOnly === 1 || typeof(Cryptpad) === "undefined") { return $titleContainer; }
var $input = $('<input>', {
type: 'text',
placeholder: placeholder
}).appendTo($titleContainer).hide();
if (config.readOnly !== 1) {
$text.attr("title", Messages.clickToEdit);
$text.addClass("editable");
var $icon = $('<span>', {
'class': 'fa fa-pencil readonly',
style: 'font-family: FontAwesome;'
});
$pencilIcon.append($icon).appendTo($titleContainer);
}
// Events
$input.on('mousedown', function (e) {
if (!$input.is(":focus")) {
$input.focus();
}
e.stopPropagation();
return true;
});
$input.on('keyup', function (e) {
if (e.which === 13 && toolbar.connected === true) {
var name = $input.val().trim();
if (name === "") {
name = $input.attr('placeholder');
}
Cryptpad.renamePad(name, function (err, newtitle) {
if (err) { return; }
$text.text(newtitle);
callback(null, newtitle);
$input.hide();
$text.show();
//$pencilIcon.css('display', '');
});
} else if (e.which === 27) {
$input.hide();
$text.show();
//$pencilIcon.css('display', '');
}
});
var displayInput = function () {
if (toolbar.connected === false) { return; }
$text.hide();
//$pencilIcon.css('display', 'none');
var inputVal = suggestName() || "";
$input.val(inputVal);
$input.show();
$input.focus();
};
$text.on('click', displayInput);
$pencilIcon.on('click', displayInput);
return $titleContainer;
};
var createLinkToMain = function (toolbar) {
var $linkContainer = $('<span>', {
'class': "cryptpad-link"
}).appendTo(toolbar.$top);
var $imgTag = $('<img>', {
src: "/customize/cryptofist_mini.png",
alt: "Cryptpad"
});
// We need to override the "a" tag action here because it is inside the iframe!
var $aTagSmall = $('<a>', {
href: "/",
title: Messages.header_logoTitle,
'class': "cryptpad-logo"
}).append($imgTag);
var $span = $('<span>').text('CryptPad');
var $aTagBig = $aTagSmall.clone().addClass('large').append($span);
$aTagSmall.addClass('narrow');
var onClick = function (e) {
e.preventDefault();
if (e.ctrlKey) {
window.open('/');
return;
}
window.location = "/";
};
var onContext = function (e) { e.stopPropagation(); };
$aTagBig.click(onClick).contextmenu(onContext);
$aTagSmall.click(onClick).contextmenu(onContext);
$linkContainer.append($aTagSmall).append($aTagBig);
return $linkContainer;
};
var checkLag = function (toolbar, config, $lagEl) {
var lag;
var $lag = $lagEl || toolbar.lag;
if (!$lag) { return; }
var getLag = config.network.getLag;
if(typeof getLag === "function") {
lag = getLag();
}
var lagLight = $('<div>', {
'class': 'lag'
});
var title;
if (lag && toolbar.connected) {
$lag.attr('class', LAG_CLS);
toolbar.firstConnection = false;
title = Messages.lag + ' : ' + lag + ' ms\n';
if (lag > 30000) {
$lag.addClass('lag0');
title = Messages.redLight;
} else if (lag > 5000) {
$lag.addClass('lag1');
title += Messages.orangeLight;
} else if (lag > 1000) {
$lag.addClass('lag2');
title += Messages.orangeLight;
} else if (lag > 300) {
$lag.addClass('lag3');
title += Messages.greenLight;
} else {
$lag.addClass('lag4');
title += Messages.greenLight;
}
}
else if (!toolbar.firstConnection) {
$lag.attr('class', LAG_CLS);
// Display the red light at the 2nd failed attemp to get the lag
lagLight.addClass('lag-red');
title = Messages.redLight;
}
if (title) {
$lag.attr('title', title);
}
};
var createLag = function (toolbar, config) {
var $a = toolbar.$userAdmin.find('.'+LAG_CLS).show();
$('<span>', {'class': 'bar1'}).appendTo($a);
$('<span>', {'class': 'bar2'}).appendTo($a);
$('<span>', {'class': 'bar3'}).appendTo($a);
$('<span>', {'class': 'bar4'}).appendTo($a);
if (config.realtime) {
checkLag(toolbar, config, $a);
setInterval(function () {
if (!toolbar.connected) { return; }
checkLag(toolbar, config);
}, 3000);
}
return $a;
};
var kickSpinner = function (toolbar, config, local) {
if (!toolbar.spinner) { return; }
var $spin = toolbar.spinner;
$spin.find('.spin').show();
$spin.find('.synced').hide();
var onSynced = function () {
if ($spin.timeout) { clearTimeout($spin.timeout); }
$spin.timeout = setTimeout(function () {
$spin.find('.spin').hide();
$spin.find('.synced').show();
}, local ? 0 : SPINNER_DISAPPEAR_TIME);
};
if (Cryptpad) {
Cryptpad.whenRealtimeSyncs(config.realtime, onSynced);
return;
}
onSynced();
};
var ks = function (toolbar, config, local) {
return function () {
if (toolbar.connected) { kickSpinner(toolbar, config, local); }
};
};
var createSpinner = function (toolbar, config) {
var $spin = toolbar.$userAdmin.find('.'+SPINNER_CLS).show();
$('<span>', {
id: uid(),
'class': 'spin fa fa-spinner fa-pulse',
}).appendTo($spin).hide();
$('<span>', {
id: uid(),
'class': 'synced fa fa-check',
title: Messages.synced
}).appendTo($spin);
toolbar.$userAdmin.prepend($spin);
if (config.realtime) {
config.realtime.onPatch(ks(toolbar, config));
config.realtime.onMessage(ks(toolbar, config, true));
}
return $spin;
};
var createState = function (toolbar) {
return toolbar.$userAdmin.find('.'+STATE_CLS).text(Messages.synchronizing).show();
};
var createLimit = function (toolbar) {
if (!Config.enablePinning) { return; }
var $limitIcon = $('<span>', {'class': 'fa fa-exclamation-triangle'});
var $limit = toolbar.$userAdmin.find('.'+LIMIT_CLS).attr({
'title': Messages.pinLimitReached
}).append($limitIcon).hide();
var todo = function (e, overLimit) {
if (e) { return void console.error("Unable to get the pinned usage"); }
if (overLimit) {
$limit.show().click(function () {
Cryptpad.alert(Messages.pinLimitReachedAlert, null, true);
});
}
};
Cryptpad.isOverPinLimit(todo);
return $limit;
};
var createNewPad = function (toolbar) {
var $newPad = toolbar.$userAdmin.find('.'+NEWPAD_CLS).show();
var pads_options = [];
Config.availablePadTypes.forEach(function (p) {
if (p === 'drive') { return; }
pads_options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': '/' + p + '/',
},
content: Messages.type[p]
});
});
var $plusIcon = $('<span>', {'class': 'fa fa-plus'});
var $newbig = $('<span>', {'class': 'big'}).append(' ' +Messages.newButton);
var $newButton = $('<div>').append($plusIcon).append($newbig);
var dropdownConfig = {
text: $newButton.html(), // Button initial text
options: pads_options, // Entries displayed in the menu
left: true, // Open to the left of the button,
container: $newPad
};
var $newPadBlock = Cryptpad.createDropdown(dropdownConfig);
$newPadBlock.find('button').attr('title', Messages.newButtonTitle);
$newPadBlock.find('button').attr('id', 'newdoc');
return $newPadBlock;
};
var createUserAdmin = function (toolbar, config) {
var $userAdmin = toolbar.$userAdmin.find('.'+USERADMIN_CLS).show();
var userMenuCfg = {
$initBlock: $userAdmin
};
if (!config.hideDisplayName) { // TODO: config.userAdmin.hideDisplayName?
$.extend(true, userMenuCfg, {
displayNameCls: USERNAME_CLS,
changeNameButtonCls: USERBUTTON_CLS,
});
}
if (config.readOnly !== 1) {
userMenuCfg.displayName = 1;
userMenuCfg.displayChangeName = 1;
}
Cryptpad.createUserAdminMenu(userMenuCfg);
var $userButton = toolbar.$userNameButton = $userAdmin.find('a.' + USERBUTTON_CLS);
$userButton.click(function (e) {
e.preventDefault();
e.stopPropagation();
Cryptpad.getLastName(function (err, lastName) {
if (err) { return void console.error("Cannot get last name", err); }
Cryptpad.prompt(Messages.changeNamePrompt, lastName || '', function (newName) {
if (newName === null && typeof(lastName) === "string") { return; }
if (newName === null) { newName = ''; }
Cryptpad.changeDisplayName(newName);
});
});
});
Cryptpad.onDisplayNameChanged(function () {
Cryptpad.findCancelButton().click();
});
return $userAdmin;
};
// Events
var initClickEvents = function (toolbar, config) {
var removeDropdowns = function () {
toolbar.$toolbar.find('.cryptpad-dropdown').hide();
};
var cancelEditTitle = function (e) {
// Now we want to apply the title even if we click somewhere else
if ($(e.target).parents('.' + TITLE_CLS).length || !toolbar.title) {
return;
}
var $title = toolbar.title;
if (!$title.find('input').is(':visible')) { return; }
// Press enter
var ev = $.Event("keyup");
ev.which = 13;
$title.find('input').trigger(ev);
};
// Click in the main window
var w = config.ifrw || window;
$(w).on('click', removeDropdowns);
$(w).on('click', cancelEditTitle);
// Click in iframes
try {
if (w.$ && w.$('iframe').length) {
config.ifrw.$('iframe').each(function (i, el) {
$(el.contentWindow).on('click', removeDropdowns);
$(el.contentWindow).on('click', cancelEditTitle);
});
}
} catch (e) {
// empty try catch in case this iframe is problematic
}
};
// Notifications
var initNotifications = function (toolbar, config) {
// Display notifications when users are joining/leaving the session
var oldUserData;
if (!config.userList || !config.userList.list || !config.userList.userNetfluxId) { return; }
var userList = config.userList.list;
var userNetfluxId = config.userList.userNetfluxId;
if (typeof Cryptpad !== "undefined" && userList) {
var notify = function(type, name, oldname) {
// type : 1 (+1 user), 0 (rename existing user), -1 (-1 user)
if (typeof name === "undefined") { return; }
name = name || Messages.anonymous;
switch(type) {
case 1:
Cryptpad.log(Messages._getKey("notifyJoined", [name]));
break;
case 0:
oldname = (oldname === "") ? Messages.anonymous : oldname;
Cryptpad.log(Messages._getKey("notifyRenamed", [oldname, name]));
break;
case -1:
Cryptpad.log(Messages._getKey("notifyLeft", [name]));
break;
default:
console.log("Invalid type of notification");
break;
}
};
var userPresent = function (id, user, data) {
if (!(user && user.uid)) {
console.log('no uid');
return 0;
}
if (!data) {
console.log('no data');
return 0;
}
var count = 0;
Object.keys(data).forEach(function (k) {
if (data[k] && data[k].uid === user.uid) { count++; }
});
return count;
};
userList.change.push(function (newdata) {
// Notify for disconnected users
if (typeof oldUserData !== "undefined") {
for (var u in oldUserData) {
// if a user's uid is still present after having left, don't notify
if (userList.users.indexOf(u) === -1) {
var temp = JSON.parse(JSON.stringify(oldUserData[u]));
delete oldUserData[u];
if (userPresent(u, temp, newdata || oldUserData) < 1) {
notify(-1, temp.name);
}
}
}
}
// Update the "oldUserData" object and notify for new users and names changed
if (typeof newdata === "undefined") { return; }
if (typeof oldUserData === "undefined") {
oldUserData = JSON.parse(JSON.stringify(newdata));
return;
}
if (config.readOnly === 0 && !oldUserData[userNetfluxId]) {
oldUserData = JSON.parse(JSON.stringify(newdata));
return;
}
for (var k in newdata) {
if (k !== userNetfluxId && userList.users.indexOf(k) !== -1) {
if (typeof oldUserData[k] === "undefined") {
// if the same uid is already present in the userdata, don't notify
if (!userPresent(k, newdata[k], oldUserData)) {
notify(1, newdata[k].name);
}
} else if (oldUserData[k].name !== newdata[k].name) {
notify(0, newdata[k].name, oldUserData[k].name);
}
}
}
oldUserData = JSON.parse(JSON.stringify(newdata));
});
}
};
// Main
Bar.create = function (cfg) {
var config = cfg || {};
Cryptpad = config.common;
Messages = Cryptpad.Messages;
config.readOnly = (typeof config.readOnly !== "undefined") ? (config.readOnly ? 1 : 0) : -1;
config.displayed = config.displayed || [];
config.network = cfg.network || Cryptpad.getNetwork();
var toolbar = {};
toolbar.connected = false;
toolbar.firstConnection = true;
var $toolbar = toolbar.$toolbar = createRealtimeToolbar(config);
toolbar.$leftside = $toolbar.find('.'+Bar.constants.leftside);
toolbar.$rightside = $toolbar.find('.'+Bar.constants.rightside);
toolbar.$top = $toolbar.find('.'+Bar.constants.top);
toolbar.$history = $toolbar.find('.'+Bar.constants.history);
toolbar.$userAdmin = $toolbar.find('.'+Bar.constants.userAdmin);
// Create the subelements
var tb = {};
tb['userlist'] = createUserList;
tb['share'] = createShare;
tb['fileshare'] = createFileShare;
tb['title'] = createTitle;
tb['lag'] = createLag;
tb['spinner'] = createSpinner;
tb['state'] = createState;
tb['limit'] = createLimit;
tb['newpad'] = createNewPad;
tb['useradmin'] = createUserAdmin;
var addElement = toolbar.addElement = function (arr, additionnalCfg, init) {
if (typeof additionnalCfg === "object") { $.extend(true, config, additionnalCfg); }
arr.forEach(function (el) {
if (typeof el !== "string" || !el.trim()) { return; }
if (typeof tb[el] === "function") {
if (!init && config.displayed.indexOf(el) !== -1) { return; } // Already done
toolbar[el] = tb[el](toolbar, config);
if (!init) { config.displayed.push(el); }
}
});
};
addElement(config.displayed, {}, true);
initUserList(toolbar, config);
toolbar['linkToMain'] = createLinkToMain(toolbar, config);
if (!config.realtime) { toolbar.connected = true; }
initClickEvents(toolbar, config);
initNotifications(toolbar, config);
var failed = toolbar.failed = function () {
toolbar.connected = false;
if (toolbar.state) {
toolbar.state.text(Messages.disconnected);
}
checkLag(toolbar, config);
};
toolbar.reconnecting = function (userId) {
if (config.userList) { config.userList.userNetfluxId = userId; }
toolbar.connected = false;
if (toolbar.state) {
toolbar.state.text(Messages.reconnecting);
}
checkLag(toolbar, config);
};
// On log out, remove permanently the realtime elements of the toolbar
Cryptpad.onLogout(function () {
failed();
if (toolbar.useradmin) { toolbar.useradmin.hide(); }
if (toolbar.userlist) { toolbar.userlist.hide(); }
});
return toolbar;
};
return Bar;
});

@ -71,7 +71,7 @@ define([], function () {
} }
}; };
var orderOfNodes = tree.orderOfNodes = function (a, b, root) { tree.orderOfNodes = function (a, b, root) {
// b might not be supplied // b might not be supplied
if (!b) { return; } if (!b) { return; }
// a and b might be the same element // a and b might be the same element

@ -1,6 +1,7 @@
define([ define([
'jquery', 'jquery',
], function ($) { '/customize/application_config.js'
], function ($, AppConfig) {
var module = {}; var module = {};
var ROOT = module.ROOT = "root"; var ROOT = module.ROOT = "root";
@ -8,7 +9,7 @@ define([
var TRASH = module.TRASH = "trash"; var TRASH = module.TRASH = "trash";
var TEMPLATE = module.TEMPLATE = "template"; var TEMPLATE = module.TEMPLATE = "template";
var init = module.init = function (files, config) { module.init = function (files, config) {
var exp = {}; var exp = {};
var Cryptpad = config.Cryptpad; var Cryptpad = config.Cryptpad;
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
@ -18,7 +19,6 @@ define([
var NEW_FILE_NAME = Messages.fm_newFile; var NEW_FILE_NAME = Messages.fm_newFile;
// Logging // Logging
var DEBUG = config.DEBUG || false;
var logging = function () { var logging = function () {
console.log.apply(console, arguments); console.log.apply(console, arguments);
}; };
@ -38,17 +38,16 @@ define([
* UTILS * UTILS
*/ */
var getStructure = exp.getStructure = function () { exp.getStructure = function () {
var a = {}; var a = {};
a[ROOT] = {}; a[ROOT] = {};
a[UNSORTED] = [];
a[TRASH] = {}; a[TRASH] = {};
a[FILES_DATA] = []; a[FILES_DATA] = [];
a[TEMPLATE] = []; a[TEMPLATE] = [];
return a; return a;
}; };
var getHrefArray = function () { var getHrefArray = function () {
return [UNSORTED, TEMPLATE]; return [TEMPLATE];
}; };
@ -58,7 +57,7 @@ define([
return typeof(element) === "string"; return typeof(element) === "string";
}; };
var isReadOnlyFile = exp.isReadOnlyFile = function (element) { exp.isReadOnlyFile = function (element) {
if (!isFile(element)) { return false; } if (!isFile(element)) { return false; }
var parsed = Cryptpad.parsePadUrl(element); var parsed = Cryptpad.parsePadUrl(element);
if (!parsed) { return false; } if (!parsed) { return false; }
@ -69,17 +68,17 @@ define([
}; };
var isFolder = exp.isFolder = function (element) { var isFolder = exp.isFolder = function (element) {
return typeof(element) !== "string"; return typeof(element) === "object";
}; };
var isFolderEmpty = exp.isFolderEmpty = function (element) { exp.isFolderEmpty = function (element) {
if (typeof(element) !== "object") { return false; } if (typeof(element) !== "object") { return false; }
return Object.keys(element).length === 0; return Object.keys(element).length === 0;
}; };
var hasSubfolder = exp.hasSubfolder = function (element, trashRoot) { exp.hasSubfolder = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; } if (typeof(element) !== "object") { return false; }
var subfolder = 0; var subfolder = 0;
var addSubfolder = function (el, idx) { var addSubfolder = function (el) {
subfolder += isFolder(el.element) ? 1 : 0; subfolder += isFolder(el.element) ? 1 : 0;
}; };
for (var f in element) { for (var f in element) {
@ -94,10 +93,10 @@ define([
return subfolder; return subfolder;
}; };
var hasFile = exp.hasFile = function (element, trashRoot) { exp.hasFile = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; } if (typeof(element) !== "object") { return false; }
var file = 0; var file = 0;
var addFile = function (el, idx) { var addFile = function (el) {
file += isFile(el.element) ? 1 : 0; file += isFile(el.element) ? 1 : 0;
}; };
for (var f in element) { for (var f in element) {
@ -232,7 +231,7 @@ define([
_getFiles[TRASH] = function () { _getFiles[TRASH] = function () {
var root = files[TRASH]; var root = files[TRASH];
var ret = []; var ret = [];
var addFiles = function (el, idx) { var addFiles = function (el) {
if (isFile(el.element)) { if (isFile(el.element)) {
if(ret.indexOf(el.element) === -1) { ret.push(el.element); } if(ret.indexOf(el.element) === -1) { ret.push(el.element); }
} else { } else {
@ -297,6 +296,9 @@ define([
return paths; return paths;
}; };
exp.findFileInRoot = function (href) {
return _findFileInRoot([ROOT], href);
};
var _findFileInHrefArray = function (rootName, href) { var _findFileInHrefArray = function (rootName, href) {
var unsorted = files[rootName].slice(); var unsorted = files[rootName].slice();
var ret = []; var ret = [];
@ -345,12 +347,11 @@ define([
}; };
var findFile = exp.findFile = function (href) { var findFile = exp.findFile = function (href) {
var rootpaths = _findFileInRoot([ROOT], href); var rootpaths = _findFileInRoot([ROOT], href);
var unsortedpaths = _findFileInHrefArray(UNSORTED, href);
var templatepaths = _findFileInHrefArray(TEMPLATE, href); var templatepaths = _findFileInHrefArray(TEMPLATE, href);
var trashpaths = _findFileInTrash([TRASH], href); var trashpaths = _findFileInTrash([TRASH], href);
return rootpaths.concat(unsortedpaths, templatepaths, trashpaths); return rootpaths.concat(templatepaths, trashpaths);
}; };
var search = exp.search = function (value) { exp.search = function (value) {
if (typeof(value) !== "string") { return []; } if (typeof(value) !== "string") { return []; }
var res = []; var res = [];
// Search in ROOT // Search in ROOT
@ -401,7 +402,7 @@ define([
var ret = []; var ret = [];
res.forEach(function (l) { res.forEach(function (l) {
var paths = findFile(l); //var paths = findFile(l);
ret.push({ ret.push({
paths: findFile(l), paths: findFile(l),
data: exp.getFileData(l) data: exp.getFileData(l)
@ -426,19 +427,24 @@ define([
}; };
// FILES DATA // FILES DATA
var pushFileData = exp.pushData = function (data) { var pushFileData = exp.pushData = function (data, cb) {
Cryptpad.pinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { if (typeof cb !== "function") { cb = function () {}; }
if (e) { console.log(e); return; } var todo = function () {
console.log(hash); files[FILES_DATA].push(data);
cb();
};
if (!Cryptpad.isLoggedIn() || !AppConfig.enablePinning) { return void todo(); }
Cryptpad.pinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e) {
if (e) { return void cb(e); }
todo();
}); });
files[FILES_DATA].push(data);
}; };
var spliceFileData = exp.removeData = function (idx) { var spliceFileData = exp.removeData = function (idx) {
var data = files[FILES_DATA][idx]; var data = files[FILES_DATA][idx];
if (typeof data === "object") { if (typeof data === "object" && Cryptpad.isLoggedIn() && AppConfig.enablePinning) {
Cryptpad.unpinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { Cryptpad.unpinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) {
if (e) { console.log(e); return; } if (e) { return void logError(e); }
console.log(hash); debug('UNPIN', hash);
}); });
} }
files[FILES_DATA].splice(idx, 1); files[FILES_DATA].splice(idx, 1);
@ -524,7 +530,7 @@ define([
}); });
exp.delete(toRemove, cb); exp.delete(toRemove, cb);
}; };
var restore = exp.restore = function (path, cb) { exp.restore = function (path, cb) {
if (!isInTrashRoot(path)) { return; } if (!isInTrashRoot(path)) { return; }
var parentPath = path.slice(); var parentPath = path.slice();
parentPath.pop(); parentPath.pop();
@ -534,8 +540,10 @@ define([
// ADD // ADD
var add = exp.add = function (href, path, name, cb) { var add = exp.add = function (data, path) {
if (!href) { return; } if (!data || typeof(data) !== "object") { return; }
var href = data.href;
var name = data.title;
var newPath = path, parentEl; var newPath = path, parentEl;
if (path && !Array.isArray(path)) { if (path && !Array.isArray(path)) {
newPath = decodeURIComponent(path).split(','); newPath = decodeURIComponent(path).split(',');
@ -546,53 +554,50 @@ define([
parentEl.push(href); parentEl.push(href);
return; return;
} }
// Add to root // Add to root if path is ROOT or if no path
if (path && isPathIn(newPath, [ROOT]) && name) { var filesList = getFiles([ROOT, TRASH, 'hrefArray']);
parentEl = find(newPath); if ((path && isPathIn(newPath, [ROOT]) || filesList.indexOf(href) === -1) && name) {
parentEl = find(newPath || [ROOT]);
if (parentEl) { if (parentEl) {
var newName = getAvailableName(parentEl, name); var newName = getAvailableName(parentEl, name);
parentEl[newName] = href; parentEl[newName] = href;
return; return;
} }
} }
// No path: push to unsorted
var filesList = getFiles([ROOT, TRASH, 'hrefArray']);
if (filesList.indexOf(href) === -1) { files[UNSORTED].push(href); }
if (typeof cb === "function") { cb(); }
}; };
var addFile = exp.addFile = function (filePath, name, type, cb) { exp.addFile = function (filePath, name, type, cb) {
var parentEl = findElement(files, filePath); var parentEl = findElement(files, filePath);
var fileName = getAvailableName(parentEl, name || NEW_FILE_NAME); var fileName = getAvailableName(parentEl, name || NEW_FILE_NAME);
var href = '/' + type + '/#' + Cryptpad.createRandomHash(); var href = '/' + type + '/#' + Cryptpad.createRandomHash();
parentEl[fileName] = href;
pushFileData({ pushFileData({
href: href, href: href,
title: fileName, title: fileName,
atime: +new Date(), atime: +new Date(),
ctime: +new Date() ctime: +new Date()
}); }, function (err) {
if (err) { return void cb(err); }
var newPath = filePath.slice(); parentEl[fileName] = href;
newPath.push(fileName); var newPath = filePath.slice();
cb({ newPath.push(fileName);
newPath: newPath cb(void 0, {
newPath: newPath
});
}); });
}; };
var addFolder = exp.addFolder = function (folderPath, name, cb) { exp.addFolder = function (folderPath, name, cb) {
var parentEl = find(folderPath); var parentEl = find(folderPath);
var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME); var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME);
parentEl[folderName] = {}; parentEl[folderName] = {};
var newPath = folderPath.slice(); var newPath = folderPath.slice();
newPath.push(folderName); newPath.push(folderName);
cb({ cb(void 0, {
newPath: newPath newPath: newPath
}); });
}; };
// FORGET (move with href not path) // FORGET (move with href not path)
var forget = exp.forget = function (href) { exp.forget = function (href) {
var paths = findFile(href); var paths = findFile(href);
move(paths, [TRASH]); move(paths, [TRASH]);
}; };
@ -601,6 +606,10 @@ define([
// Permanently delete multiple files at once using a list of paths // Permanently delete multiple files at once using a list of paths
// NOTE: We have to be careful when removing elements from arrays (trash root, unsorted or template) // NOTE: We have to be careful when removing elements from arrays (trash root, unsorted or template)
var removePadAttribute = function (f) { var removePadAttribute = function (f) {
if (typeof(f) !== 'string') {
console.error("Can't find pad attribute for an undefined pad");
return;
}
Object.keys(files).forEach(function (key) { Object.keys(files).forEach(function (key) {
var hash = f.indexOf('#') !== -1 ? f.slice(f.indexOf('#') + 1) : null; var hash = f.indexOf('#') !== -1 ? f.slice(f.indexOf('#') + 1) : null;
if (hash && key.indexOf(hash) === 0) { if (hash && key.indexOf(hash) === 0) {
@ -690,18 +699,18 @@ define([
// FILES_DATA (replaceHref) // FILES_DATA (replaceHref)
if (!nocheck) { checkDeletedFiles(); } if (!nocheck) { checkDeletedFiles(); }
}; };
var deletePath = exp.delete = function (paths, cb, nocheck) { exp.delete = function (paths, cb, nocheck) {
deleteMultiplePermanently(paths, nocheck); deleteMultiplePermanently(paths, nocheck);
if (typeof cb === "function") { cb(); } if (typeof cb === "function") { cb(); }
}; };
var emptyTrash = exp.emptyTrash = function (cb) { exp.emptyTrash = function (cb) {
files[TRASH] = {}; files[TRASH] = {};
checkDeletedFiles(); checkDeletedFiles();
if(cb) { cb(); } if(cb) { cb(); }
}; };
// RENAME // RENAME
var rename = exp.rename = function (path, newName, cb) { exp.rename = function (path, newName, cb) {
if (path.length <= 1) { if (path.length <= 1) {
logError('Renaming `root` is forbidden'); logError('Renaming `root` is forbidden');
return; return;
@ -722,7 +731,7 @@ define([
parentEl[newName] = element; parentEl[newName] = element;
parentEl[oldName] = undefined; parentEl[oldName] = undefined;
delete parentEl[oldName]; delete parentEl[oldName];
cb(); if (typeof cb === "function") { cb(); }
}; };
// REPLACE // REPLACE
@ -743,7 +752,7 @@ define([
} }
}; };
// Replace a href by a stronger one everywhere in the drive (except FILES_DATA) // Replace a href by a stronger one everywhere in the drive (except FILES_DATA)
var replaceHref = exp.replace = function (o, n) { exp.replace = function (o, n) {
if (!isFile(o) || !isFile(n)) { return; } if (!isFile(o) || !isFile(n)) { return; }
var paths = findFile(o); var paths = findFile(o);
@ -772,7 +781,7 @@ define([
* INTEGRITY CHECK * INTEGRITY CHECK
*/ */
var fixFiles = exp.fixFiles = function () { exp.fixFiles = function () {
// Explore the tree and check that everything is correct: // Explore the tree and check that everything is correct:
// * 'root', 'trash', 'unsorted' and 'filesData' exist and are objects // * 'root', 'trash', 'unsorted' and 'filesData' exist and are objects
// * ROOT: Folders are objects, files are href // * ROOT: Folders are objects, files are href
@ -780,7 +789,7 @@ define([
// * FILES_DATA: - Data (title, cdate, adte) are stored in filesData. filesData contains only href keys linking to object with title, cdate, adate. // * FILES_DATA: - Data (title, cdate, adte) are stored in filesData. filesData contains only href keys linking to object with title, cdate, adate.
// - Dates (adate, cdate) can be parsed/formatted // - Dates (adate, cdate) can be parsed/formatted
// - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' // - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted'
// * UNSORTED: Contains only files (href), and does not contains files that are in ROOT // * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT
debug("Cleaning file system..."); debug("Cleaning file system...");
var before = JSON.stringify(files); var before = JSON.stringify(files);
@ -821,26 +830,37 @@ define([
} }
} }
}; };
// Make sure unsorted doesn't exist anymore
var fixUnsorted = function () { var fixUnsorted = function () {
if (!Array.isArray(files[UNSORTED])) { debug("UNSORTED was not an array"); files[UNSORTED] = []; } if (!files[UNSORTED]) { return; }
files[UNSORTED] = Cryptpad.deduplicateString(files[UNSORTED].slice()); debug("UNSORTED still exists in the object, removing it...");
var us = files[UNSORTED]; var us = files[UNSORTED];
if (us.length === 0) {
delete files[UNSORTED];
return;
}
var rootFiles = getFiles([ROOT, TEMPLATE]).slice(); var rootFiles = getFiles([ROOT, TEMPLATE]).slice();
var toClean = []; //var toClean = [];
us.forEach(function (el, idx) { var root = find([ROOT]);
us.forEach(function (el) {
if (!isFile(el) || rootFiles.indexOf(el) !== -1) { if (!isFile(el) || rootFiles.indexOf(el) !== -1) {
toClean.push(idx); return;
//toClean.push(idx);
} }
var name = getFileData(el).title || NEW_FILE_NAME;
var newName = getAvailableName(root, name);
root[newName] = el;
}); });
toClean.forEach(function (idx) { delete files[UNSORTED];
/*toClean.forEach(function (idx) {
us.splice(idx, 1); us.splice(idx, 1);
}); });*/
}; };
var fixTemplate = function () { var fixTemplate = function () {
if (!Array.isArray(files[TEMPLATE])) { debug("TEMPLATE was not an array"); files[TEMPLATE] = []; } if (!Array.isArray(files[TEMPLATE])) { debug("TEMPLATE was not an array"); files[TEMPLATE] = []; }
files[TEMPLATE] = Cryptpad.deduplicateString(files[TEMPLATE].slice()); files[TEMPLATE] = Cryptpad.deduplicateString(files[TEMPLATE].slice());
var us = files[TEMPLATE]; var us = files[TEMPLATE];
var rootFiles = getFiles([ROOT, UNSORTED]).slice(); var rootFiles = getFiles([ROOT]).slice();
var toClean = []; var toClean = [];
us.forEach(function (el, idx) { us.forEach(function (el, idx) {
if (!isFile(el) || rootFiles.indexOf(el) !== -1) { if (!isFile(el) || rootFiles.indexOf(el) !== -1) {
@ -855,16 +875,24 @@ define([
if (!$.isArray(files[FILES_DATA])) { debug("FILES_DATA was not an array"); files[FILES_DATA] = []; } if (!$.isArray(files[FILES_DATA])) { debug("FILES_DATA was not an array"); files[FILES_DATA] = []; }
var fd = files[FILES_DATA]; var fd = files[FILES_DATA];
var rootFiles = getFiles([ROOT, TRASH, 'hrefArray']); var rootFiles = getFiles([ROOT, TRASH, 'hrefArray']);
var root = find([ROOT]);
var toClean = []; var toClean = [];
fd.forEach(function (el, idx) { fd.forEach(function (el) {
if (!el || typeof(el) !== "object") { if (!el || typeof(el) !== "object") {
debug("An element in filesData was not an object.", el); debug("An element in filesData was not an object.", el);
toClean.push(el); toClean.push(el);
return; return;
} }
if (!el.href) {
debug("Rmoving an element in filesData with a missing href.", el);
toClean.push(el);
return;
}
if (rootFiles.indexOf(el.href) === -1) { if (rootFiles.indexOf(el.href) === -1) {
debug("An element in filesData was not in ROOT, UNSORTED or TRASH.", el); debug("An element in filesData was not in ROOT, TEMPLATE or TRASH.", el);
files[UNSORTED].push(el.href); var name = el.title || NEW_FILE_NAME;
var newName = getAvailableName(root, name);
root[newName] = el.href;
return; return;
} }
}); });

@ -20,18 +20,18 @@
visibilityChange: visibilityChange, visibilityChange: visibilityChange,
}; };
var isSupported = Visible.isSupported = function () { Visible.isSupported = function () {
return !(typeof(document.addEventListener) === "undefined" || return !(typeof(document.addEventListener) === "undefined" ||
typeof document[hidden] === "undefined"); typeof document[hidden] === "undefined");
}; };
var onChange = Visible.onChange = function (f) { Visible.onChange = function (f) {
document.addEventListener(visibilityChange, function (ev) { document.addEventListener(visibilityChange, function (ev) {
f(!document[hidden], ev); f(!document[hidden], ev);
}, false); }, false);
}; };
var currently = Visible.currently = function () { Visible.currently = function () {
return !document[hidden]; return !document[hidden];
}; };

@ -7,7 +7,7 @@ body {
padding: 0; padding: 0;
margin: 0; margin: 0;
position: relative; position: relative;
font-size: 20px; font-size: 16px;
overflow: auto; overflow: auto;
} }
body { body {
@ -43,6 +43,9 @@ body {
margin-top: 0.5em; margin-top: 0.5em;
} }
} }
div:focus {
outline: none;
}
.fa { .fa {
/*min-width: 17px;*/ /*min-width: 17px;*/
margin-right: 3px; margin-right: 3px;
@ -66,7 +69,7 @@ li {
.contextMenu { .contextMenu {
display: none; display: none;
position: absolute; position: absolute;
z-index: 50; z-index: 500;
} }
.contextMenu li { .contextMenu li {
padding: 0; padding: 0;
@ -89,6 +92,16 @@ li {
.selected .fa-plus-square-o { .selected .fa-plus-square-o {
color: #000; color: #000;
} }
.selectedTmp {
border: 1px dotted #bbb;
background: #AAA;
color: #ddd;
margin: -1px;
}
.selectedTmp .fa-minus-square-o,
.selectedTmp .fa-plus-square-o {
color: #000;
}
span.fa-folder, span.fa-folder,
span.fa-folder-open { span.fa-folder-open {
color: #FEDE8B; color: #FEDE8B;
@ -215,6 +228,13 @@ span.fa-folder-open {
flex: 1; flex: 1;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
position: relative;
}
#content .selectBox {
display: none;
background-color: rgba(100, 100, 100, 0.7);
position: absolute;
z-index: 50;
} }
#content.readonly { #content.readonly {
background: #e6e6e6; background: #e6e6e6;
@ -242,7 +262,7 @@ span.fa-folder-open {
#content li:not(.header) *:not(input) { #content li:not(.header) *:not(input) {
/*pointer-events: none;*/ /*pointer-events: none;*/
} }
#content li:not(.header):hover:not(.selected) { #content li:not(.header):hover:not(.selected, .selectedTmp) {
background-color: #eee; background-color: #eee;
} }
#content li:not(.header):hover .name { #content li:not(.header):hover .name {
@ -289,6 +309,9 @@ span.fa-folder-open {
width: 50px; width: 50px;
font-size: 40px; font-size: 40px;
} }
#content .element .truncated {
display: none;
}
#content div.grid { #content div.grid {
padding: 20px; padding: 20px;
} }
@ -296,19 +319,35 @@ span.fa-folder-open {
display: inline-block; display: inline-block;
margin: 10px 10px; margin: 10px 10px;
width: 140px; width: 140px;
height: 140px;
text-align: center; text-align: center;
vertical-align: top; vertical-align: top;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-top: 10px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
max-height: 145px;
} }
#content div.grid li:not(.selected) { #content div.grid li:not(.selected):not(.selectedTmp) {
border: transparent 1px; border: 1px solid #CCC;
} }
#content div.grid li .name { #content div.grid li .name {
width: 100%; width: 100%;
height: 48px;
margin: 8px 0;
display: inline-flex;
justify-content: center;
overflow: hidden;
}
#content div.grid li.element {
position: relative;
}
#content div.grid li .truncated {
display: block;
position: absolute;
bottom: 0px;
left: 0;
right: 0;
text-align: center;
} }
#content div.grid li input { #content div.grid li input {
width: 100%; width: 100%;
@ -317,7 +356,8 @@ span.fa-folder-open {
#content div.grid li .fa { #content div.grid li .fa {
display: block; display: block;
margin: auto; margin: auto;
font-size: 40px; font-size: 48px;
margin: 8px 0;
text-align: center; text-align: center;
} }
#content div.grid li .fa.listonly { #content div.grid li .fa.listonly {
@ -326,6 +366,9 @@ span.fa-folder-open {
#content div.grid .listElement { #content div.grid .listElement {
display: none; display: none;
} }
#content .list {
padding-left: 20px;
}
#content .list ul { #content .list ul {
display: table; display: table;
width: 100%; width: 100%;

@ -32,7 +32,7 @@ html, body {
padding: 0; padding: 0;
margin: 0; margin: 0;
position: relative; position: relative;
font-size: 20px; font-size: 16px;
overflow: auto; overflow: auto;
} }
@ -70,6 +70,10 @@ body {
} }
} }
div:focus {
outline: none;
}
.fa { .fa {
/*min-width: 17px;*/ /*min-width: 17px;*/
margin-right: 3px; margin-right: 3px;
@ -96,7 +100,7 @@ li {
.contextMenu { .contextMenu {
display: none; display: none;
position: absolute; position: absolute;
z-index: 50; z-index: 500;
li { li {
padding: 0; padding: 0;
font-size: 16px; font-size: 16px;
@ -121,6 +125,16 @@ li {
} }
} }
.selectedTmp {
border: 1px dotted #bbb;
background: #AAA;
color: #ddd;
margin: -1px;
.fa-minus-square-o, .fa-plus-square-o {
color: @tree-fg;
}
}
span { span {
&.fa-folder, &.fa-folder-open { &.fa-folder, &.fa-folder-open {
color: #FEDE8B; color: #FEDE8B;
@ -260,6 +274,13 @@ span {
flex: 1; flex: 1;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
position: relative;
.selectBox {
display: none;
background-color: rgba(100, 100, 100, 0.7);
position: absolute;
z-index: 50;
}
&.readonly { &.readonly {
background: @content-bg-ro; background: @content-bg-ro;
} }
@ -287,7 +308,7 @@ span {
/*pointer-events: none;*/ /*pointer-events: none;*/
} }
&:hover { &:hover {
&:not(.selected) { &:not(.selected, .selectedTmp) {
background-color: @drive-hover; background-color: @drive-hover;
} }
.name { .name {
@ -343,25 +364,45 @@ span {
} }
} }
} }
.element {
.truncated { display: none; }
}
div.grid { div.grid {
padding: 20px; padding: 20px;
li { li {
display: inline-block; display: inline-block;
margin: 10px 10px; margin: 10px 10px;
width: 140px; width: 140px;
height: 140px;
text-align: center; text-align: center;
vertical-align: top; vertical-align: top;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-top: 10px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
max-height: 145px;
&:not(.selected) { &:not(.selected):not(.selectedTmp) {
border: transparent 1px; border: 1px solid #CCC;
} }
.name { .name {
width: 100%; width: 100%;
height: 48px;
margin: 8px 0;
display: inline-flex;
//align-items: center;
justify-content: center;
overflow: hidden;
//text-overflow: ellipsis;
}
&.element {
position: relative;
}
.truncated {
display: block;
position: absolute;
bottom: 0px;
left: 0; right: 0;
text-align: center;
} }
input { input {
width: 100%; width: 100%;
@ -370,7 +411,8 @@ span {
.fa { .fa {
display: block; display: block;
margin: auto; margin: auto;
font-size: 40px; font-size: 48px;
margin: 8px 0;
text-align: center; text-align: center;
&.listonly { &.listonly {
display: none; display: none;
@ -384,6 +426,7 @@ span {
.list { .list {
// Make it act as a table! // Make it act as a table!
padding-left: 20px;
ul { ul {
display: table; display: table;
width: 100%; width: 100%;

@ -14,7 +14,7 @@
<div class="app-container" tabindex="0"> <div class="app-container" tabindex="0">
<div id="tree"> <div id="tree">
</div> </div>
<div id="content"> <div id="content" tabindex="2">
</div> </div>
<div id="treeContextMenu" class="contextMenu dropdown clearfix"> <div id="treeContextMenu" class="contextMenu dropdown clearfix">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;"> <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
@ -33,7 +33,7 @@
<li><a tabindex="-1" data-icon="fa-file-code-o" class="newdoc own editable dropdown-item" data-type="code" data-localization="button_newcode">New code</a></li> <li><a tabindex="-1" data-icon="fa-file-code-o" class="newdoc own editable dropdown-item" data-type="code" data-localization="button_newcode">New code</a></li>
<li><a tabindex="-1" data-icon="fa-file-powerpoint-o" class="newdoc own editable dropdown-item" data-type="slide" data-localization="button_newslide">New slide</a></li> <li><a tabindex="-1" data-icon="fa-file-powerpoint-o" class="newdoc own editable dropdown-item" data-type="slide" data-localization="button_newslide">New slide</a></li>
<li><a tabindex="-1" data-icon="fa-calendar" class="newdoc own editable dropdown-item" data-type="poll" data-localization="button_newpoll">New poll</a></li> <li><a tabindex="-1" data-icon="fa-calendar" class="newdoc own editable dropdown-item" data-type="poll" data-localization="button_newpoll">New poll</a></li>
<li><a tabindex="-1" data-icon="fa-calendar" class="newdoc own editable dropdown-item" data-type="whiteboard" data-localization="button_newwhiteboard">New whiteboard</a></li> <li><a tabindex="-1" data-icon="fa-paint-brush" class="newdoc own editable dropdown-item" data-type="whiteboard" data-localization="button_newwhiteboard">New whiteboard</a></li>
</ul> </ul>
</div> </div>
<div id="defaultContextMenu" class="contextMenu dropdown clearfix"> <div id="defaultContextMenu" class="contextMenu dropdown clearfix">

File diff suppressed because it is too large Load Diff

@ -169,6 +169,7 @@ define([
var $input = Input({ var $input = Input({
placeholder: 'card description', placeholder: 'card description',
id: id,
}) })
.addClass('card-title'); .addClass('card-title');
@ -206,7 +207,7 @@ define([
/* /*
*/ */
Card.move = function (uid, A, B) { Card.move = function (/*uid, A, B*/) {
}; };
@ -228,11 +229,10 @@ define([
} }
var card = proxy.cards[cid]; var card = proxy.cards[cid];
card = card; // TODO actually draw
}; };
var Draw = Board.Draw = function ($lists) { Board.Draw = function ($lists) {
proxy.listOrder.forEach(function (luid) { proxy.listOrder.forEach(function (luid) {
List.draw($lists, luid); List.draw($lists, luid);
}); });

@ -7,12 +7,12 @@ define([
'/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/visible.js', //'/common/visible.js',
'/common/notify.js', //'/common/notify.js',
'/bower_components/file-saver/FileSaver.min.js' '/bower_components/file-saver/FileSaver.min.js'
], function ($, Config, Messages, Board, TextPatcher, Listmap, Crypto, Cryptpad, Visible, Notify) { ], function ($, Config, Messages, Board, TextPatcher, Listmap, Crypto, Cryptpad /*, Visible, Notify*/) {
var saveAs = window.saveAs; // var saveAs = window.saveAs;
Cryptpad.styleAlerts(); Cryptpad.styleAlerts();
console.log("Initializing your realtime session..."); console.log("Initializing your realtime session...");
@ -23,28 +23,28 @@ define([
Board: Board, Board: Board,
}; };
/*
var unnotify = function () { var unnotify = function () {
if (!(module.tabNotification && if (!(module.tabNotification &&
typeof(module.tabNotification.cancel) === 'function')) { return; } typeof(module.tabNotification.cancel) === 'function')) { return; }
module.tabNotification.cancel(); module.tabNotification.cancel();
}; };
var notify = function () { var notify = function () {
if (!(Visible.isSupported() && !Visible.currently())) { return; } if (!(Visible.isSupported() && !Visible.currently())) { return; }
unnotify(); unnotify();
module.tabNotification = Notify.tab(1000, 10); module.tabNotification = Notify.tab(1000, 10);
}; };
*/
var setEditable = function (bool) { var setEditable = function (bool) {
bool = bool;
}; };
setEditable(false); setEditable(false);
var $lists = $('#lists'); var $lists = $('#lists');
var $addList = $('#create-list').click(function () { $('#create-list').click(function () {
Board.List.draw($lists); Board.List.draw($lists);
}); });
@ -52,7 +52,7 @@ define([
Cryptpad.log("You are the first user to visit this board"); Cryptpad.log("You are the first user to visit this board");
}; };
var whenReady = function (opt) { var whenReady = function () {
var rt = module.rt; var rt = module.rt;
var proxy = rt.proxy; var proxy = rt.proxy;
@ -63,7 +63,6 @@ define([
Board.Draw($lists); Board.Draw($lists);
if (first) { firstUser(); } if (first) { firstUser(); }
}; };
var config = { var config = {
@ -78,10 +77,10 @@ define([
var proxy = rt.proxy; var proxy = rt.proxy;
proxy proxy
.on('create', function (info) { .on('create', function (info) {
var realtime = module.realtime = info.realtime; module.realtime = info.realtime;
window.location.hash = info.channel + secret.key; window.location.hash = info.channel + secret.key;
}) })
.on('ready', function (info) { .on('ready', function () {
Cryptpad.log("Ready!"); Cryptpad.log("Ready!");
whenReady({ whenReady({

@ -1,52 +0,0 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/toolbar.js',
'/common/cryptpad-common.js',
'/common/visible.js',
'/common/notify.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify) {
var Messages = Cryptpad.Messages;
window.Nacl = window.nacl;
$(function () {
var ifrw = $('#pad-iframe')[0].contentWindow;
var $iframe = $('#pad-iframe').contents();
Cryptpad.addLoadingScreen();
var andThen = function () {
var $bar = $iframe.find('.toolbar-container');
var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
if (!secret.keys) {
secret.keys = secret.key;
}
var $mt = $iframe.find('#encryptedFile');
$mt.attr('src', './assets/image.png-encrypted');
$mt.attr('data-crypto-key', 'TBo77200c0e/FdldQFcnQx4Y');
$mt.attr('data-type', 'image/png');
require(['/common/media-tag.js'], function (MediaTag) {
MediaTag($mt[0]);
Cryptpad.removeLoadingScreen();
var configTb = {
displayed: ['useradmin', 'newpad'],
ifrw: ifrw,
common: Cryptpad
};
toolbar = Toolbar.create($bar, null, null, null, null, configTb);
});
};
Cryptpad.ready(function (err, anv) {
andThen();
Cryptpad.reportAppUsage();
});
});
});

@ -128,7 +128,7 @@ define([
setEditable(false); setEditable(false);
var onInit = config.onInit = function (info) { config.onInit = function (info) {
var realtime = module.realtime = info.realtime; var realtime = module.realtime = info.realtime;
window.location.hash = info.channel + secret.key; window.location.hash = info.channel + secret.key;
@ -140,7 +140,7 @@ define([
}; };
var readValues = function () { var readValues = function () {
UI.each(function (ui, i, list) { UI.each(function (ui) {
Map[ui.id] = ui.value(); Map[ui.id] = ui.value();
}); });
}; };
@ -165,7 +165,7 @@ define([
if (UI.ids.indexOf(key) === -1) { Map[key] = parsed[key]; } if (UI.ids.indexOf(key) === -1) { Map[key] = parsed[key]; }
}); });
UI.each(function (ui, i, list) { UI.each(function (ui) {
var newval = parsed[ui.id]; var newval = parsed[ui.id];
var oldval = ui.value(); var oldval = ui.value();
@ -178,9 +178,7 @@ define([
if (ui.preserveCursor) { if (ui.preserveCursor) {
op = TextPatcher.diff(oldval, newval); op = TextPatcher.diff(oldval, newval);
selects = ['selectionStart', 'selectionEnd'].map(function (attr) { selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
var before = element[attr]; return TextPatcher.transformCursor(element[attr], op);
var after = TextPatcher.transformCursor(element[attr], op);
return after;
}); });
} }
@ -195,13 +193,13 @@ define([
}); });
}; };
var onRemote = config.onRemote = function (info) { config.onRemote = function () {
if (initializing) { return; } if (initializing) { return; }
/* integrate remote changes */ /* integrate remote changes */
updateValues(); updateValues();
}; };
var onReady = config.onReady = function (info) { config.onReady = function () {
updateValues(); updateValues();
console.log("READY"); console.log("READY");
@ -209,13 +207,13 @@ define([
initializing = false; initializing = false;
}; };
var onAbort = config.onAbort = function (info) { config.onAbort = function () {
window.alert("Network Connection Lost"); window.alert("Network Connection Lost");
}; };
var rt = Realtime.start(config); Realtime.start(config);
UI.each(function (ui, i, list) { UI.each(function (ui) {
var type = ui.type; var type = ui.type;
var events = eventsByType[type]; var events = eventsByType[type];
ui.$.on(events, onLocal); ui.$.on(events, onLocal);

@ -1,7 +1,7 @@
define([], function () { define([], function () {
var ula = {}; var ula = {};
var uid = ula.uid = (function () { ula.uid = (function () {
var i = 0; var i = 0;
var prefix = 'rt_'; var prefix = 'rt_';
return function () { return prefix + i++; }; return function () { return prefix + i++; };

@ -1,77 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<style>
html, body{
padding: 0px;
margin: 0px;
overflow: hidden;
box-sizing: border-box;
}
textarea{
position: absolute;
top: 5vh;
left: 0px;
border: 0px;
padding-top: 15px;
width: 100%;
height: 95vh;
max-width: 100%;
max-height: 100vh;
font-size: 30px;
background-color: #073642;
color: #839496;
overflow-x: hidden;
/* disallow textarea resizes */
resize: none;
}
textarea[disabled] {
background-color: #275662;
color: #637476;
}
#panel {
position: fixed;
top: 0px;
right: 0px;
width: 100%;
height: 5vh;
z-index: 95;
background-color: #777;
/* min-height: 75px; */
}
#run {
display: block;
float: right;
height: 100%;
width: 10vw;
z-index: 100;
line-height: 5vw;
font-size: 1.5em;
background-color: #222;
color: #CCC;
text-align: center;
border-radius: 5%;
border: 0px;
}
</style>
</head>
<body>
<textarea></textarea>
<div id="panel">
<!-- TODO update this element when new users join -->
<span id="users"></span>
<!-- what else should go in the panel? -->
<a href="#" id="run">RUN</a>
</div>
</body>
</html>

@ -1,161 +0,0 @@
define([
'jquery',
'/api/config',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js',
'/common/cryptpad-common.js'
], function ($, Config, Realtime, Crypto, TextPatcher, Cryptpad) {
var secret = Cryptpad.getSecrets();
var $textarea = $('textarea'),
$run = $('#run');
var module = {};
var config = {
initialState: '',
websocketURL: Config.websocketURL,
channel: secret.channel,
crypto: Crypto.createEncryptor(secret.key),
};
var initializing = true;
var setEditable = function (bool) { $textarea.attr('disabled', !bool); };
var canonicalize = function (text) { return text.replace(/\r\n/g, '\n'); };
setEditable(false);
var onInit = config.onInit = function (info) {
window.location.hash = info.channel + secret.key;
$(window).on('hashchange', function() { window.location.reload(); });
};
var onRemote = config.onRemote = function (info) {
if (initializing) { return; }
var userDoc = info.realtime.getUserDoc();
var current = canonicalize($textarea.val());
var op = TextPatcher.diff(current, userDoc);
var elem = $textarea[0];
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
return TextPatcher.transformCursor(elem[attr], op);
});
$textarea.val(userDoc);
elem.selectionStart = selects[0];
elem.selectionEnd = selects[1];
// TODO do something on external messages
// http://webdesign.tutsplus.com/tutorials/how-to-display-update-notifications-in-the-browser-tab--cms-23458
};
var onReady = config.onReady = function (info) {
module.patchText = TextPatcher.create({
realtime: info.realtime
// logging: true
});
initializing = false;
setEditable(true);
$textarea.val(info.realtime.getUserDoc());
};
var onAbort = config.onAbort = function (info) {
setEditable(false);
window.alert("Server Connection Lost");
};
var onLocal = config.onLocal = function () {
if (initializing) { return; }
module.patchText(canonicalize($textarea.val()));
};
var rt = window.rt = Realtime.start(config);
var splice = function (str, index, chars) {
var count = chars.length;
return str.slice(0, index) + chars + str.slice((index -1) + count);
};
var setSelectionRange = function (input, start, end) {
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(start, end);
} else if (input.createTextRange) {
var range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
range.select();
}
};
var setCursor = function (el, pos) {
setSelectionRange(el, pos, pos);
};
var state = {};
// TODO
$textarea.on('keydown', function (e) {
// track when control keys are pushed down
//switch (e.key) { }
});
// TODO
$textarea.on('keyup', function (e) {
// track when control keys are released
});
//$textarea.on('change', onLocal);
$textarea.on('keypress', function (e) {
onLocal();
switch (e.key) {
case 'Tab':
// insert a tab wherever the cursor is...
var start = $textarea.prop('selectionStart');
var end = $textarea.prop('selectionEnd');
if (typeof start !== 'undefined') {
if (start === end) {
$textarea.val(function (i, val) {
return splice(val, start, "\t");
});
setCursor($textarea[0], start +1);
} else {
// indentation?? this ought to be fun.
}
}
// simulate a keypress so the event goes through..
// prevent default behaviour for tab
e.preventDefault();
onLocal();
break;
default:
break;
}
});
['cut', 'paste', 'change', 'keyup', 'keydown', 'select', 'textInput']
.forEach(function (evt) {
$textarea.on(evt, onLocal);
});
$run.click(function (e) {
e.preventDefault();
var content = $textarea.val();
try {
eval(content); // jshint ignore:line
} catch (err) {
// FIXME don't use alert, make an errorbox
window.alert(err.message);
console.error(err);
}
});
});

@ -11,12 +11,10 @@
<ul> <ul>
<li><a href="/examples/form/">forms</a></li> <li><a href="/examples/form/">forms</a></li>
<li><a href="/examples/text/">text</a></li> <li><a href="/examples/text/">text</a></li>
<li><a href="/examples/hack/">textareas with executable content</a></li>
<li><a href="/examples/board/">kanban board</a></li> <li><a href="/examples/board/">kanban board</a></li>
<li><a href="/examples/json/">json objects</a></li> <!-- <li><a href="/examples/json/">json objects</a></li> -->
<li><a href="/examples/read/">ajax-like get/put behaviour</a></li> <li><a href="/examples/read/">ajax-like get/put behaviour</a></li>
<li><a href="/examples/render/">render markdown content as html</a></li> <li><a href="/examples/render/">render markdown content as html</a></li>
<li><a href="/examples/style/">edit a page's style tag</a></li> <li><a href="/examples/style/">edit a page's style tag</a></li>
<li><a href="/examples/upload/">upload content</a></li>
</ul> </ul>

@ -26,15 +26,13 @@ define([
}); });
}; };
var initializing = true;
setEditable(false); setEditable(false);
var rt = module.rt = RtListMap.create(config); var rt = module.rt = RtListMap.create(config);
rt.proxy.on('create', function (info) { rt.proxy.on('create', function (info) {
console.log("initializing..."); console.log("initializing...");
window.location.hash = info.channel + secret.key; window.location.hash = info.channel + secret.key;
}).on('ready', function (info) { }).on('ready', function () {
console.log("...your realtime object is ready"); console.log("...your realtime object is ready");
rt.proxy rt.proxy
@ -42,7 +40,7 @@ define([
.on('change', [], function (o, n, p) { .on('change', [], function (o, n, p) {
console.log("root change event firing for path [%s]: %s => %s", p.join(','), o, n); console.log("root change event firing for path [%s]: %s => %s", p.join(','), o, n);
}) })
.on('remove', [], function (o, p, root) { .on('remove', [], function (o, p) {
console.log("Removal of value [%s] at path [%s]", o, p.join(',')); console.log("Removal of value [%s] at path [%s]", o, p.join(','));
}) })
.on('change', ['a', 'b', 'c'], function (o, n, p) { .on('change', ['a', 'b', 'c'], function (o, n, p) {
@ -51,7 +49,7 @@ define([
return false; return false;
}) })
// on(event, cb) // on(event, cb)
.on('disconnect', function (info) { .on('disconnect', function () {
setEditable(false); setEditable(false);
window.alert("Network connection lost"); window.alert("Network connection lost");
}); });
@ -65,6 +63,7 @@ define([
console.log("evaluating `%s`", value); console.log("evaluating `%s`", value);
var x = rt.proxy; var x = rt.proxy;
x = x; // LOL jshint says this is unused otherwise <3
console.log('> ', eval(value)); // jshint ignore:line console.log('> ', eval(value)); // jshint ignore:line
console.log(); console.log();

@ -3,7 +3,7 @@ define([
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/pinpad.js' '/common/pinpad.js'
], function ($, Cryptpad, Pinpad) { ], function ($, Cryptpad, Pinpad) {
var APP = window.APP = { window.APP = {
Cryptpad: Cryptpad, Cryptpad: Cryptpad,
}; };
@ -37,7 +37,7 @@ define([
}; };
$(function () { $(function () {
Cryptpad.ready(function (err, env) { Cryptpad.ready(function () {
var network = Cryptpad.getNetwork(); var network = Cryptpad.getNetwork();
var proxy = Cryptpad.getStore().getProxy().proxy; var proxy = Cryptpad.getStore().getProxy().proxy;

@ -2,9 +2,7 @@ define([
'jquery', 'jquery',
'/common/cryptget.js' '/common/cryptget.js'
], function ($, Crypt) { ], function ($, Crypt) {
var $target = $('#target'); var $target = $('#target');
var $dest = $('#dest');
var useDoc = function (err, doc) { var useDoc = function (err, doc) {
if (err) { return console.error(err); } if (err) { return console.error(err); }

@ -55,7 +55,7 @@ define([
var initializing = true; var initializing = true;
var onInit = config.onInit = function (info) { config.onInit = function (info) {
window.location.hash = info.channel + secret.key; window.location.hash = info.channel + secret.key;
module.realtime = info.realtime; module.realtime = info.realtime;
}; };
@ -73,7 +73,7 @@ define([
}; };
// when your editor is ready // when your editor is ready
var onReady = config.onReady = function (info) { config.onReady = function () {
console.log("Realtime is ready!"); console.log("Realtime is ready!");
var userDoc = module.realtime.getUserDoc(); var userDoc = module.realtime.getUserDoc();
lazyDraw(getContent(userDoc)); lazyDraw(getContent(userDoc));
@ -81,13 +81,13 @@ define([
}; };
// when remote editors do things... // when remote editors do things...
var onRemote = config.onRemote = function () { config.onRemote = function () {
if (initializing) { return; } if (initializing) { return; }
var userDoc = module.realtime.getUserDoc(); var userDoc = module.realtime.getUserDoc();
lazyDraw(getContent(userDoc)); lazyDraw(getContent(userDoc));
}; };
var onLocal = config.onLocal = function () { config.onLocal = function () {
// we're not really expecting any local events for this editor... // we're not really expecting any local events for this editor...
/* but we might add a second pane in the future so that you don't need /* but we might add a second pane in the future so that you don't need
a second window to edit your markdown */ a second window to edit your markdown */
@ -96,9 +96,9 @@ define([
lazyDraw(userDoc); lazyDraw(userDoc);
}; };
var onAbort = config.onAbort = function () { config.onAbort = function () {
window.alert("Network Connection Lost"); window.alert("Network Connection Lost");
}; };
var rts = Realtime.start(config); Realtime.start(config);
}); });

@ -20,8 +20,6 @@ define([
crypto: Crypto.createEncryptor(secret.key), crypto: Crypto.createEncryptor(secret.key),
}; };
var userName = module.userName = config.userName = Crypto.rand64(8);
var lazyDraw = (function () { var lazyDraw = (function () {
var to, var to,
delay = 500; delay = 500;
@ -37,7 +35,7 @@ define([
var initializing = true; var initializing = true;
var onInit = config.onInit = function (info) { config.onInit = function (info) {
window.location.hash = info.channel + secret.key; window.location.hash = info.channel + secret.key;
var realtime = module.realtime = info.realtime; var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
@ -50,28 +48,28 @@ define([
}); });
}; };
var onReady = config.onReady = function (info) { config.onReady = function () {
var userDoc = module.realtime.getUserDoc(); var userDoc = module.realtime.getUserDoc();
draw(userDoc); draw(userDoc);
console.log("Ready"); console.log("Ready");
initializing = false; initializing = false;
}; };
var onRemote = config.onRemote = function () { config.onRemote = function () {
draw(module.realtime.getUserDoc()); draw(module.realtime.getUserDoc());
}; };
var onAbort = config.onAbort = function (info) { config.onAbort = function () {
// notify the user of the abort // notify the user of the abort
window.alert("Network Connection Lost"); window.alert("Network Connection Lost");
}; };
var onLocal = config.onLocal = function () { config.onLocal = function () {
// nope // nope
}; };
$edit.attr('href', '/examples/text/'+ window.location.hash); $edit.attr('href', '/examples/text/'+ window.location.hash);
var rt = Realtime.start(config); Realtime.start(config);
}); });

@ -13,8 +13,6 @@ define([
TextPatcher: TextPatcher TextPatcher: TextPatcher
}; };
var userName = module.userName = Crypto.rand64(8);
var initializing = true; var initializing = true;
var $textarea = $('textarea'); var $textarea = $('textarea');
@ -30,14 +28,14 @@ define([
setEditable(false); setEditable(false);
var onInit = config.onInit = function (info) { config.onInit = function (info) {
window.location.hash = info.channel + secret.key; window.location.hash = info.channel + secret.key;
$(window).on('hashchange', function() { $(window).on('hashchange', function() {
window.location.reload(); window.location.reload();
}); });
}; };
var onRemote = config.onRemote = function (info) { config.onRemote = function () {
if (initializing) { return; } if (initializing) { return; }
var userDoc = module.realtime.getUserDoc(); var userDoc = module.realtime.getUserDoc();
var content = canonicalize($textarea.val()); var content = canonicalize($textarea.val());
@ -59,7 +57,7 @@ define([
module.patchText(canonicalize($textarea.val())); module.patchText(canonicalize($textarea.val()));
}; };
var onReady = config.onReady = function (info) { config.onReady = function (info) {
var realtime = module.realtime = info.realtime; var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
realtime: realtime realtime: realtime
@ -71,12 +69,12 @@ define([
initializing = false; initializing = false;
}; };
var onAbort = config.onAbort = function (info) { config.onAbort = function () {
setEditable(false); setEditable(false);
window.alert("Server Connection Lost"); window.alert("Server Connection Lost");
}; };
var onConnectionChange = config.onConnectionChange = function (info) { config.onConnectionChange = function (info) {
if (info.state) { if (info.state) {
initializing = true; initializing = true;
} else { } else {
@ -85,7 +83,7 @@ define([
} }
}; };
var rt = Realtime.start(config); Realtime.start(config);
['cut', 'paste', 'change', 'keyup', 'keydown', 'select', 'textInput'] ['cut', 'paste', 'change', 'keyup', 'keydown', 'select', 'textInput']
.forEach(function (evt) { .forEach(function (evt) {

@ -1,75 +0,0 @@
define([
'jquery',
'/common/cryptget.js',
'/bower_components/chainpad-crypto/crypto.js'
], function ($, Crypt, Crypto) {
var Nacl = window.nacl;
var key = Nacl.randomBytes(32);
var handleFile = function (body) {
//console.log("plaintext");
//console.log(body);
/*
0 && Crypt.put(body, function (e, out) {
if (e) { return void console.error(e); }
if (out) {
console.log(out);
}
}); */
var data = {};
(function () {
var cyphertext = data.payload = Crypto.encrypt(body, key);
console.log("encrypted");
console.log(cyphertext);
console.log(data);
var decrypted = Crypto.decrypt(cyphertext, key);
//console.log('decrypted');
//console.log(decrypted);
if (decrypted !== body) {
throw new Error("failed to maintain integrity with round trip");
}
// finding... files are entirely too large.
console.log(data.payload.length, body.length); // 1491393, 588323
console.log(body.length / data.payload.length); // 0.3944788529918003
console.log(data.payload.length / body.length); // 2.534990132971174
/*
http://stackoverflow.com/questions/19959072/sending-binary-data-in-javascript-over-http
// Since we deal with Firefox and Chrome only
var bytesToSend = [253, 0, 128, 1];
var bytesArray = new Uint8Array(bytesToSend);
$.ajax({
url: '%your_service_url%',
type: 'POST',
contentType: 'application/octet-stream',
data: bytesArray,
processData: false
});
*/
})();
};
var $file = $('input[type="file"]');
$file.on('change', function (e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = function (e) {
handleFile(e.target.result);
};
reader.readAsText(file);
});
});

@ -0,0 +1,188 @@
define([
'/bower_components/tweetnacl/nacl-fast.min.js',
], function () {
var Nacl = window.nacl;
var PARANOIA = true;
var plainChunkLength = 128 * 1024;
var cypherChunkLength = 131088;
var encodePrefix = function (p) {
return [
65280, // 255 << 8
255,
].map(function (n, i) {
return (p & n) >> ((1 - i) * 8);
});
};
var decodePrefix = function (A) {
return (A[0] << 8) | A[1];
};
var slice = function (A) {
return Array.prototype.slice.call(A);
};
var createNonce = function () {
return new Uint8Array(new Array(24).fill(0));
};
var increment = function (N) {
var l = N.length;
while (l-- > 1) {
if (PARANOIA) {
if (typeof(N[l]) !== 'number') {
throw new Error('E_UNSAFE_TYPE');
}
if (N[l] > 255) {
throw new Error('E_OUT_OF_BOUNDS');
}
}
/* jshint probably suspects this is unsafe because we lack types
but as long as this is only used on nonces, it should be safe */
if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line
N[l] = 0;
// you don't need to worry about this running out.
// you'd need a REAAAALLY big file
if (l === 0) {
throw new Error('E_NONCE_TOO_LARGE');
}
}
};
var joinChunks = function (chunks) {
return new Uint8Array(chunks.reduce(function (A, B) {
return slice(A).concat(slice(B));
}, []));
};
var decrypt = function (u8, key, cb) {
var fail = function (e) {
cb(e || "DECRYPTION_ERROR");
};
var nonce = createNonce();
var i = 0;
var prefix = u8.subarray(0, 2);
var metadataLength = decodePrefix(prefix);
var res = {
metadata: undefined,
};
var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
var metaChunk = Nacl.secretbox.open(metaBox, nonce, key);
increment(nonce);
try {
res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk));
} catch (e) {
return fail('E_METADATA_DECRYPTION');
}
if (!res.metadata) {
return void setTimeout(function () {
cb('NO_METADATA');
});
}
var takeChunk = function () {
var start = i * cypherChunkLength + 2 + metadataLength;
var end = start + cypherChunkLength;
i++;
var box = new Uint8Array(u8.subarray(start, end));
// decrypt the chunk
var plaintext = Nacl.secretbox.open(box, nonce, key);
increment(nonce);
return plaintext;
};
var chunks = [];
// decrypt file contents
var chunk;
for (;i * cypherChunkLength < u8.length;) {
chunk = takeChunk();
if (!chunk) {
return window.setTimeout(fail);
}
chunks.push(chunk);
}
// send chunks
res.content = joinChunks(chunks);
cb(void 0, res);
};
// metadata
/* { filename: 'raccoon.jpg', type: 'image/jpeg' } */
var encrypt = function (u8, metadata, key) {
var nonce = createNonce();
// encode metadata
var metaBuffer = Array.prototype.slice
.call(Nacl.util.decodeUTF8(JSON.stringify(metadata)));
var plaintext = new Uint8Array(metaBuffer);
var i = 0;
/*
0: metadata
1: u8
2: done
*/
var state = 0;
var next = function (cb) {
var start;
var end;
var part;
var box;
// DONE
if (state === 2) { return void cb(); }
if (state === 0) { // metadata...
part = new Uint8Array(plaintext);
box = Nacl.secretbox(part, nonce, key);
increment(nonce);
if (box.length > 65535) {
return void cb('METADATA_TOO_LARGE');
}
var prefixed = new Uint8Array(encodePrefix(box.length)
.concat(slice(box)));
state++;
return void cb(void 0, prefixed);
}
// encrypt the rest of the file...
start = i * plainChunkLength;
end = start + plainChunkLength;
part = u8.subarray(start, end);
box = Nacl.secretbox(part, nonce, key);
increment(nonce);
i++;
// regular data is done
if (i * plainChunkLength >= u8.length) { state = 2; }
return void cb(void 0, box);
};
return next;
};
return {
decrypt: decrypt,
encrypt: encrypt,
joinChunks: joinChunks,
};
});

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<style>
html, body {
margin: 0px;
}
.cryptpad-toolbar {
margin-bottom: 1px;
padding: 0px;
display: inline-block;
}
#file {
display: block;
height: 300px;
width: 300px;
border: 2px solid black;
margin: 50px;
}
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.block {
display: block;
height: 500px;
width: 500px;
}
.hidden {
display: none;
}
.inputfile + label {
border: 2px solid black;
background-color: rgba(50, 50, 50, .10);
margin: 50px;
}
.inputfile:focus + label,
.inputfile + label:hover {
background-color: rgba(50, 50, 50, 0.30);
}
</style>
</head>
<body>
<div id="toolbar" class="toolbar-container"></div>
<div id="upload-form" style="display: none;">
<input type="file" name="file" id="file" class="inputfile" />
<label for="file" class="block">Choose a file</label>
</div>
<div id="feedback" class="block hidden">
</div>
</body>
</html>

@ -0,0 +1,236 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/toolbar2.js',
'/common/cryptpad-common.js',
'/common/visible.js',
'/common/notify.js',
'/file/file-crypto.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
'/bower_components/file-saver/FileSaver.min.js',
], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify, FileCrypto) {
var Messages = Cryptpad.Messages;
var saveAs = window.saveAs;
var Nacl = window.nacl;
var APP = {};
$(function () {
var ifrw = $('#pad-iframe')[0].contentWindow;
var $iframe = $('#pad-iframe').contents();
var $form = $iframe.find('#upload-form');
Cryptpad.addLoadingScreen();
var Title;
var myFile;
var myDataType;
var upload = function (blob, metadata) {
console.log(metadata);
var u8 = new Uint8Array(blob);
var key = Nacl.randomBytes(32);
var next = FileCrypto.encrypt(u8, metadata, key);
var chunks = [];
var sendChunk = function (box, cb) {
var enc = Nacl.util.encodeBase64(box);
chunks.push(box);
Cryptpad.rpc.send('UPLOAD', enc, function (e, msg) {
cb(e, msg);
});
};
var again = function (err, box) {
if (err) { throw new Error(err); }
if (box) {
return void sendChunk(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
}
// if not box then done
Cryptpad.rpc.send('UPLOAD_COMPLETE', '', function (e, res) {
if (e) { return void console.error(e); }
var id = res[0];
var uri = ['', 'blob', id.slice(0,2), id].join('/');
console.log("encrypted blob is now available as %s", uri);
var b64Key = Nacl.util.encodeBase64(key);
window.location.hash = Cryptpad.getFileHashFromKeys(id, b64Key);
$form.hide();
APP.toolbar.addElement(['fileshare'], {});
// check if the uploaded file can be decrypted
var newU8 = FileCrypto.joinChunks(chunks);
FileCrypto.decrypt(newU8, key, function (e, res) {
if (e) { return console.error(e); }
var title = document.title = res.metadata.name;
myFile = res.content;
myDataType = res.metadata.type;
var defaultName = Cryptpad.getDefaultName(Cryptpad.parsePadUrl(window.location.href));
Title.updateTitle(title || defaultName);
APP.toolbar.title.show();
Cryptpad.alert("successfully uploaded: " + title);
});
});
};
Cryptpad.rpc.send('UPLOAD_STATUS', '', function (e, pending) {
if (e) {
console.error(e);
return void Cryptpad.alert("something went wrong");
}
if (pending[0]) {
return void Cryptpad.confirm('upload pending, abort?', function (yes) {
if (!yes) { return; }
Cryptpad.rpc.send('UPLOAD_CANCEL', '', function (e, res) {
if (e) { return void console.error(e); }
console.log(res);
});
});
}
next(again);
});
};
var uploadMode = false;
var andThen = function () {
var $bar = $iframe.find('.toolbar-container');
var secret;
var hexFileName;
if (window.location.hash) {
secret = Cryptpad.getSecrets();
if (!secret.keys) { throw new Error("You need a hash"); } // TODO
hexFileName = Cryptpad.base64ToHex(secret.channel);
} else {
uploadMode = true;
}
var getTitle = function () {
var pad = Cryptpad.getRelativeHref(window.location.href);
var fo = Cryptpad.getStore().getProxy().fo;
var data = fo.getFileData(pad);
return data ? data.title : undefined;
};
var exportFile = function () {
var suggestion = document.title;
Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion), function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; }
var blob = new Blob([myFile], {type: myDataType});
saveAs(blob, filename);
});
};
Title = Cryptpad.createTitle({}, function(){}, Cryptpad);
var displayed = ['title', 'useradmin', 'newpad', 'limit'];
if (secret && hexFileName) {
displayed.push('fileshare');
}
var configTb = {
displayed: displayed,
ifrw: ifrw,
common: Cryptpad,
title: Title.getTitleConfig(),
hideDisplayName: true,
$container: $bar
};
var toolbar = APP.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
if (uploadMode) { toolbar.title.hide(); }
var $rightside = toolbar.$rightside;
var $export = Cryptpad.createButton('export', true, {}, exportFile);
$rightside.append($export);
Title.updateTitle(Cryptpad.initialName || getTitle() || Title.defaultTitle);
if (!uploadMode) {
var src = Cryptpad.getBlobPathFromHex(hexFileName);
return Cryptpad.fetch(src, function (e, u8) {
// now decrypt the u8
if (e) { return window.alert('error'); }
var cryptKey = secret.keys && secret.keys.fileKeyStr;
var key = Nacl.util.decodeBase64(cryptKey);
FileCrypto.decrypt(u8, key, function (e, data) {
if (e) {
Cryptpad.removeLoadingScreen();
return console.error(e);
}
console.log(data);
var title = document.title = data.metadata.name;
myFile = data.content;
myDataType = data.metadata.type;
Title.updateTitle(title || Title.defaultTitle);
Cryptpad.removeLoadingScreen();
});
});
}
if (!Cryptpad.isLoggedIn()) {
return Cryptpad.alert("You must be logged in to upload files");
}
$form.css({
display: 'block',
});
var handleFile = function (file) {
console.log(file);
var reader = new FileReader();
reader.onloadend = function () {
upload(this.result, {
name: file.name,
type: file.type,
});
};
reader.readAsArrayBuffer(file);
};
$form.find("#file").on('change', function (e) {
var file = e.target.files[0];
handleFile(file);
});
$form
.on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
e.preventDefault();
e.stopPropagation();
})
.on('drop', function (e) {
var dropped = e.originalEvent.dataTransfer.files;
handleFile(dropped[0]);
});
// we're in upload mode
Cryptpad.removeLoadingScreen();
};
Cryptpad.ready(function () {
andThen();
Cryptpad.reportAppUsage();
});
});
});

@ -1,28 +1,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html class="cp pad">
<head> <head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> <meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script> <script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<link rel="icon" type="image/png" <link rel="icon" type="image/png"
href="/customize/main-favicon.png" href="/customize/main-favicon.png"
data-main-favicon="/customize/main-favicon.png" data-main-favicon="/customize/main-favicon.png"
data-alt-favicon="/customize/alt-favicon.png" data-alt-favicon="/customize/alt-favicon.png"
id="favicon" /> id="favicon" />
<style> <link rel="stylesheet" href="/customize/main.css" />
input {
width: 50vw;
padding: 15px;
}
pre {
max-width: 90vw;
overflow: auto;
}
</style>
</head> </head>
<body> <body>
<h1>Upload</h1>
<input type="file">

@ -0,0 +1,87 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/toolbar.js',
'/common/cryptpad-common.js',
'/common/visible.js',
'/common/notify.js',
'/file/file-crypto.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
'/bower_components/file-saver/FileSaver.min.js',
], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify, FileCrypto) {
var Nacl = window.nacl;
$(function () {
var filesAreSame = function (a, b) {
var l = a.length;
if (l !== b.length) { return false; }
var i = 0;
for (; i < l; i++) { if (a[i] !== b[i]) { return false; } }
return true;
};
var metadataIsSame = function (A, B) {
return !Object.keys(A).some(function (k) {
return A[k] !== B[k];
});
};
var upload = function (blob, metadata) {
var u8 = new Uint8Array(blob);
var key = Nacl.randomBytes(32);
var next = FileCrypto.encrypt(u8, metadata, key);
var chunks = [];
var sendChunk = function (box, cb) {
chunks.push(box);
cb();
};
var again = function (err, box) {
if (err) { throw new Error(err); }
if (box) {
return void sendChunk(box, function (e) {
if (e) {
console.error(e);
return Cryptpad.alert('Something went wrong');
}
next(again);
});
}
// check if the uploaded file can be decrypted
var newU8 = FileCrypto.joinChunks(chunks);
console.log('encrypted file with metadata is %s uint8s', newU8.length);
FileCrypto.decrypt(newU8, key, function (e, res) {
if (e) { return Cryptpad.alert(e); }
if (filesAreSame(blob, res.content) &&
metadataIsSame(res.metadata, metadata)) {
Cryptpad.alert("successfully uploaded");
} else {
Cryptpad.alert('encryption failure!');
}
});
};
next(again);
};
var andThen = function () {
var src = '/customize/cryptofist_mini.png';
Cryptpad.fetch(src, function (e, file) {
console.log('original file is %s uint8s', file.length);
upload(file, {
pew: 'pew',
bang: 'bang',
});
});
};
andThen();
});
});

@ -3,11 +3,6 @@ define([
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/login.js' '/common/login.js'
], function ($, Cryptpad, Login) { ], function ($, Cryptpad, Login) {
var APP = window.APP = {
Cryptpad: Cryptpad,
};
$(function () { $(function () {
var $main = $('#mainBlock'); var $main = $('#mainBlock');
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
@ -61,66 +56,69 @@ define([
$('button.login').click(); $('button.login').click();
}); });
$('button.login').click(function (e) { $('button.login').click(function () {
Cryptpad.addLoadingScreen(Messages.login_hashing); // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
window.setTimeout(function () { window.setTimeout(function () {
loginReady(function () { Cryptpad.addLoadingScreen(Messages.login_hashing);
var uname = $uname.val(); // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
var passwd = $passwd.val(); window.setTimeout(function () {
Login.loginOrRegister(uname, passwd, false, function (err, result) { loginReady(function () {
if (!err) { var uname = $uname.val();
var proxy = result.proxy; var passwd = $passwd.val();
Login.loginOrRegister(uname, passwd, false, function (err, result) {
if (!err) {
var proxy = result.proxy;
// successful validation and user already exists // successful validation and user already exists
// set user hash in localStorage and redirect to drive // set user hash in localStorage and redirect to drive
if (!proxy.login_name) { if (!proxy.login_name) {
result.proxy.login_name = result.userName; result.proxy.login_name = result.userName;
} }
proxy.edPrivate = result.edPrivate; proxy.edPrivate = result.edPrivate;
proxy.edPublic = result.edPublic; proxy.edPublic = result.edPublic;
Cryptpad.feedback('LOGIN', true); Cryptpad.feedback('LOGIN', true);
Cryptpad.whenRealtimeSyncs(result.realtime, function() { Cryptpad.whenRealtimeSyncs(result.realtime, function() {
Cryptpad.login(result.userHash, result.userName, function () { Cryptpad.login(result.userHash, result.userName, function () {
if (sessionStorage.redirectTo) { if (sessionStorage.redirectTo) {
var h = sessionStorage.redirectTo; var h = sessionStorage.redirectTo;
var parser = document.createElement('a'); var parser = document.createElement('a');
parser.href = h; parser.href = h;
if (parser.origin === window.location.origin) { if (parser.origin === window.location.origin) {
delete sessionStorage.redirectTo; delete sessionStorage.redirectTo;
window.location.href = h; window.location.href = h;
return; return;
}
} }
} window.location.href = '/drive/';
window.location.href = '/drive/'; });
});
});
return;
}
switch (err) {
case 'NO_SUCH_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser);
}); });
break; return;
case 'INVAL_USER': }
Cryptpad.removeLoadingScreen(function () { switch (err) {
Cryptpad.alert(Messages.login_invalUser); case 'NO_SUCH_USER':
}); Cryptpad.removeLoadingScreen(function () {
break; Cryptpad.alert(Messages.login_noSuchUser);
case 'INVAL_PASS': });
Cryptpad.removeLoadingScreen(function () { break;
Cryptpad.alert(Messages.login_invalPass); case 'INVAL_USER':
}); Cryptpad.removeLoadingScreen(function () {
break; Cryptpad.alert(Messages.login_invalUser);
default: // UNHANDLED ERROR });
Cryptpad.errorLoadingScreen(Messages.login_unhandledError); break;
} case 'INVAL_PASS':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalPass);
});
break;
default: // UNHANDLED ERROR
Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
}
});
}); });
}); }, 0);
}, 0); }, 100);
}); });
}); });
}); });

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html class="cp pad">
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<link rel="icon" type="image/png"
href="/customize/main-favicon.png"
data-main-favicon="/customize/main-favicon.png"
data-alt-favicon="/customize/alt-favicon.png"
id="favicon" />
<link rel="stylesheet" href="/customize/main.css" />
<style>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
</style>
</head>
<body>
<iframe id="pad-iframe"></iframe><script src="/common/noscriptfix.js"></script>
<div id="loading">
<div class="loadingContainer">
<img class="cryptofist" src="/customize/cryptofist_small.png" />
<div class="spinnerContainer">
<span class="fa fa-spinner fa-pulse fa-4x fa-fw"></span>
</div>
<p data-localization="loading"></p>
</div>
</div>
</body>
</html>

@ -5,7 +5,6 @@
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script src="/bower_components/jquery/dist/jquery.min.js"></script> <script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<script src="/bower_components/ckeditor/ckeditor.js"></script>
<style> <style>
html, body { html, body {
margin: 0px; margin: 0px;
@ -15,6 +14,9 @@
padding: 0px; padding: 0px;
display: inline-block; display: inline-block;
} }
media-tag * {
max-width: 100%;
}
</style> </style>
</head> </head>
<body> <body>

@ -0,0 +1,106 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/toolbar.js',
'/common/cryptpad-common.js',
//'/common/visible.js',
//'/common/notify.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
'/bower_components/file-saver/FileSaver.min.js',
], function ($, Crypto, realtimeInput, Toolbar, Cryptpad /*, Visible, Notify*/) {
//var Messages = Cryptpad.Messages;
//var saveAs = window.saveAs;
//window.Nacl = window.nacl;
$(function () {
var ifrw = $('#pad-iframe')[0].contentWindow;
var $iframe = $('#pad-iframe').contents();
Cryptpad.addLoadingScreen();
var andThen = function () {
var $bar = $iframe.find('.toolbar-container');
var secret = Cryptpad.getSecrets();
if (!secret.keys) { throw new Error("You need a hash"); } // TODO
var cryptKey = secret.keys && secret.keys.fileKeyStr;
var fileId = secret.channel;
var hexFileName = Cryptpad.base64ToHex(fileId);
var type = "image/png";
var parsed = Cryptpad.parsePadUrl(window.location.href);
var defaultName = Cryptpad.getDefaultName(parsed);
var getTitle = function () {
var pad = Cryptpad.getRelativeHref(window.location.href);
var fo = Cryptpad.getStore().getProxy().fo;
var data = fo.getFileData(pad);
return data ? data.title : undefined;
};
var updateTitle = function (newTitle) {
Cryptpad.renamePad(newTitle, function (err, data) {
if (err) {
console.log("Couldn't set pad title");
console.error(err);
return;
}
document.title = newTitle;
$bar.find('.' + Toolbar.constants.title).find('span.title').text(data);
$bar.find('.' + Toolbar.constants.title).find('input').val(data);
});
};
var suggestName = function () {
return document.title || getTitle() || '';
};
var renameCb = function (err, title) {
document.title = title;
};
var $mt = $iframe.find('#encryptedFile');
$mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName);
$mt.attr('data-crypto-key', 'cryptpad:'+cryptKey);
$mt.attr('data-type', type);
window.onMediaMetadata = function (metadata) {
if (!metadata || metadata.name !== defaultName) { return; }
var title = document.title = metadata.name;
updateTitle(title || defaultName);
};
require(['/common/media-tag.js'], function (MediaTag) {
var configTb = {
displayed: ['useradmin', 'share', 'newpad'],
ifrw: ifrw,
common: Cryptpad,
title: {
onRename: renameCb,
defaultName: defaultName,
suggestName: suggestName
},
share: {
secret: secret,
channel: hexFileName
}
};
Toolbar.create($bar, null, null, null, null, configTb);
updateTitle(Cryptpad.initialName || getTitle() || defaultName);
MediaTag($mt[0]);
Cryptpad.removeLoadingScreen();
});
};
Cryptpad.ready(function () {
andThen();
Cryptpad.reportAppUsage();
});
});
});

@ -42,7 +42,7 @@ define(function () {
}); });
} }
if (editor.contextMenu) { if (editor.contextMenu) {
editor.contextMenu.addListener(function(startElement, selection, path) { editor.contextMenu.addListener(function(startElement) {
if (startElement) { if (startElement) {
var anchor = getActiveLink(editor); var anchor = getActiveLink(editor);
if (anchor && anchor.getAttribute('href')) { if (anchor && anchor.getAttribute('href')) {

@ -3,7 +3,7 @@ define([
'/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/hyperjson/hyperjson.js', '/bower_components/hyperjson/hyperjson.js',
'/common/toolbar.js', '/common/toolbar2.js',
'/common/cursor.js', '/common/cursor.js',
'/bower_components/chainpad-json-validator/json-ot.js', '/bower_components/chainpad-json-validator/json-ot.js',
'/common/TypingTests.js', '/common/TypingTests.js',
@ -11,14 +11,11 @@ define([
'/bower_components/textpatcher/TextPatcher.js', '/bower_components/textpatcher/TextPatcher.js',
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/cryptget.js', '/common/cryptget.js',
'/common/visible.js',
'/common/notify.js',
'/pad/links.js', '/pad/links.js',
'/bower_components/file-saver/FileSaver.min.js', '/bower_components/file-saver/FileSaver.min.js',
'/bower_components/diff-dom/diffDOM.js' '/bower_components/diff-dom/diffDOM.js'
], function ($, Crypto, realtimeInput, Hyperjson, ], function ($, Crypto, realtimeInput, Hyperjson,
Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) {
Visible, Notify, Links) {
var saveAs = window.saveAs; var saveAs = window.saveAs;
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
@ -87,7 +84,7 @@ define([
return hj; return hj;
}; };
var onConnectError = function (info) { var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError); Cryptpad.errorLoadingScreen(Messages.websocketError);
}; };
@ -103,10 +100,8 @@ define([
}); });
editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor)); editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor));
editor.on('instanceReady', function (Ckeditor) { editor.on('instanceReady', function () {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox'); var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
var parsedHash = Cryptpad.parsePadUrl(window.location.href);
var defaultName = Cryptpad.getDefaultName(parsedHash);
var isHistoryMode = false; var isHistoryMode = false;
@ -167,7 +162,7 @@ define([
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
if (info.diff.name === 'href') { if (info.diff.name === 'href') {
// console.log(info.diff); // console.log(info.diff);
var href = info.diff.newValue; //var href = info.diff.newValue;
// TODO normalize HTML entities // TODO normalize HTML entities
if (/javascript *: */.test(info.diff.newValue)) { if (/javascript *: */.test(info.diff.newValue)) {
@ -277,56 +272,10 @@ define([
}; };
var initializing = true; var initializing = true;
var userData = module.userData = {}; // List of pretty names for all users (mapped with their 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;
};
var setName = module.setName = function (newName) { var Title;
if (typeof(newName) !== 'string') { return; } var UserList;
var myUserNameTemp = newName.trim(); var Metadata;
if(myUserNameTemp.length > 32) {
myUserNameTemp = myUserNameTemp.substr(0, 32);
}
myUserName = myUserNameTemp;
myData[myID] = {
name: myUserName,
uid: Cryptpad.getUid(),
};
addToUserData(myData);
Cryptpad.setAttribute('username', newName, function (err, data) {
if (err) {
console.error("Couldn't set username");
return;
}
editor.fire('change');
});
};
var isDefaultTitle = function () {
var parsed = Cryptpad.parsePadUrl(window.location.href);
return Cryptpad.isDefaultName(parsed, document.title);
};
var getHeadingText = function () { var getHeadingText = function () {
var text; var text;
@ -339,14 +288,6 @@ define([
})) { return text; } })) { return text; }
}; };
var suggestName = function (fallback) {
if (document.title === defaultName) {
return getHeadingText() || fallback || "";
} else {
return document.title || getHeadingText() || defaultName;
}
};
var DD = new DiffDom(diffOptions); var DD = new DiffDom(diffOptions);
// apply patches, and try not to lose the cursor in the process! // apply patches, and try not to lose the cursor in the process!
@ -364,12 +305,12 @@ define([
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
hjson[3] = { hjson[3] = {
metadata: { metadata: {
users: userData, users: UserList.userData,
defaultTitle: defaultName defaultTitle: Title.defaultTitle
} }
}; };
if (!initializing) { if (!initializing) {
hjson[3].metadata.title = document.title; hjson[3].metadata.title = Title.title;
} else if (Cryptpad.initialName && !hjson[3].metadata.title) { } else if (Cryptpad.initialName && !hjson[3].metadata.title) {
hjson[3].metadata.title = Cryptpad.initialName; hjson[3].metadata.title = Cryptpad.initialName;
} }
@ -390,9 +331,6 @@ define([
validateKey: secret.keys.validateKey || undefined, validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly, readOnly: readOnly,
// method which allows us to get the id of the user
setMyID: setMyID,
// Pass in encrypt and decrypt methods // Pass in encrypt and decrypt methods
crypto: Crypto.createEncryptor(secret.keys), crypto: Crypto.createEncryptor(secret.keys),
@ -421,69 +359,7 @@ define([
} }
}; };
var updateTitle = function (newTitle) { realtimeOptions.onRemote = function () {
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);
};
var updateMetadata = function(shjson) {
// Extract the user list (metadata) from the hyperjson
if (!shjson || typeof (shjson) !== "string") { updateTitle(defaultName); return; }
var hjson = JSON.parse(shjson);
var peerMetadata = hjson[3];
var titleUpdated = false;
if (peerMetadata && peerMetadata.metadata) {
if (peerMetadata.metadata.users) {
var userData = peerMetadata.metadata.users;
// Update the local user data
addToUserData(userData);
}
if (peerMetadata.metadata.defaultTitle) {
updateDefaultTitle(peerMetadata.metadata.defaultTitle);
}
if (typeof peerMetadata.metadata.title !== "undefined") {
updateTitle(peerMetadata.metadata.title || defaultName);
titleUpdated = true;
}
}
if (!titleUpdated) {
updateTitle(defaultName);
}
};
var unnotify = function () {
if (module.tabNotification &&
typeof(module.tabNotification.cancel) === 'function') {
module.tabNotification.cancel();
}
};
var notify = function () {
if (Visible.isSupported() && !Visible.currently()) {
unnotify();
module.tabNotification = Notify.tab(1000, 10);
}
};
var onRemote = realtimeOptions.onRemote = function () {
if (initializing) { return; } if (initializing) { return; }
if (isHistoryMode) { return; } if (isHistoryMode) { return; }
@ -495,7 +371,7 @@ define([
cursor.update(); cursor.update();
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
updateMetadata(shjson); Metadata.update(shjson);
var newInner = JSON.parse(shjson); var newInner = JSON.parse(shjson);
var newSInner; var newSInner;
@ -540,11 +416,11 @@ define([
// Notify only when the content has changed, not when someone has joined/left // Notify only when the content has changed, not when someone has joined/left
var oldSInner = stringify(JSON.parse(oldShjson)[2]); var oldSInner = stringify(JSON.parse(oldShjson)[2]);
if (newSInner && newSInner !== oldSInner) { if (newSInner && newSInner !== oldSInner) {
notify(); Cryptpad.notify();
} }
}; };
var getHTML = function (Dom) { var getHTML = function () {
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML); return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML);
}; };
@ -554,7 +430,7 @@ define([
var exportFile = function () { var exportFile = function () {
var html = getHTML(); var html = getHTML();
var suggestion = suggestName('cryptpad-document'); var suggestion = Title.suggestTitle('cryptpad-document');
Cryptpad.prompt(Messages.exportPrompt, Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion) + '.html', function (filename) { Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; } if (!(typeof(filename) === 'string' && filename)) { return; }
@ -562,45 +438,42 @@ define([
saveAs(blob, filename); saveAs(blob, filename);
}); });
}; };
var importFile = function (content, file) { var importFile = function (content) {
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body)); var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
applyHjson(shjson); applyHjson(shjson);
realtimeOptions.onLocal(); realtimeOptions.onLocal();
}; };
var renameCb = function (err, title) { realtimeOptions.onInit = function (info) {
if (err) { return; } UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad);
document.title = title;
editor.fire('change');
};
var onInit = realtimeOptions.onInit = function (info) { var titleCfg = { getHeadingText: getHeadingText };
userList = info.userList; Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad);
Metadata = Cryptpad.createMetadata(UserList, Title);
var configTb = { var configTb = {
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'],
userData: userData, userList: UserList.getToolbarConfig(),
readOnly: readOnly,
ifrw: ifrw,
share: { share: {
secret: secret, secret: secret,
channel: info.channel channel: info.channel
}, },
title: { title: Title.getTitleConfig(),
onRename: renameCb, common: Cryptpad,
defaultName: defaultName, readOnly: readOnly,
suggestName: suggestName ifrw: ifrw,
}, realtime: info.realtime,
common: Cryptpad network: info.network,
$container: $bar
}; };
toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, userList, configTb); toolbar = info.realtime.toolbar = Toolbar.create(configTb);
var $rightside = $bar.find('.' + Toolbar.constants.rightside); Title.setToolbar(toolbar);
var $userBlock = $bar.find('.' + Toolbar.constants.username);
var $usernameButton = module.$userNameButton = $($bar.find('.' + Toolbar.constants.changeUsername)); var $rightside = toolbar.$rightside;
var editHash; var editHash;
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
if (!readOnly) { if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
@ -626,31 +499,13 @@ define([
} }
/* add a history button */ /* add a history button */
var histConfig = {}; var histConfig = {
histConfig.onRender = function (val) { onLocal: realtimeOptions.onLocal(),
if (typeof val === "undefined") { return; } onRemote: realtimeOptions.onRemote(),
try { setHistory: setHistory,
applyHjson(val || '["BODY",{},[]]'); applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
} catch (e) { $toolbar: $bar
// 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);
realtimeOptions.onLocal();
realtimeOptions.onRemote();
};
histConfig.onReady = function () {
// Called when the history is loaded and the UI displayed
setHistory(true);
}; };
histConfig.$toolbar = $bar;
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist); $rightside.append($hist);
@ -673,14 +528,10 @@ define([
/* add an import button */ /* add an import button */
var $import = Cryptpad.createButton('import', true, {}, importFile); var $import = Cryptpad.createButton('import', true, {}, importFile);
$rightside.append($import); $rightside.append($import);
/* add a rename button */
//var $setTitle = Cryptpad.createButton('rename', true, {suggestName: suggestName}, renameCb);
//$rightside.append($setTitle);
} }
/* add a forget button */ /* add a forget button */
var forgetCb = function (err, title) { var forgetCb = function (err) {
if (err) { return; } if (err) { return; }
setEditable(false); setEditable(false);
}; };
@ -689,12 +540,10 @@ define([
// set the hash // set the hash
if (!readOnly) { Cryptpad.replaceHash(editHash); } if (!readOnly) { Cryptpad.replaceHash(editHash); }
Cryptpad.onDisplayNameChanged(setName);
}; };
// this should only ever get called once, when the chain syncs // this should only ever get called once, when the chain syncs
var onReady = realtimeOptions.onReady = function (info) { realtimeOptions.onReady = function (info) {
if (!module.isMaximized) { if (!module.isMaximized) {
editor.execCommand('maximize'); editor.execCommand('maximize');
module.isMaximized = true; module.isMaximized = true;
@ -713,7 +562,6 @@ define([
}); });
} }
module.users = info.userList.users;
module.realtime = info.realtime; module.realtime = info.realtime;
var shjson = module.realtime.getUserDoc(); var shjson = module.realtime.getUserDoc();
@ -725,13 +573,7 @@ define([
applyHjson(shjson); applyHjson(shjson);
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
updateMetadata(shjson); Metadata.update(shjson);
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
}
if (!readOnly) { if (!readOnly) {
var shjson2 = stringifyDOM(inner); var shjson2 = stringifyDOM(inner);
@ -745,42 +587,25 @@ define([
} }
} }
} else { } else {
updateTitle(Cryptpad.initialName || defaultName); Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
documentBody.innerHTML = Messages.initialState; documentBody.innerHTML = Messages.initialState;
} }
Cryptpad.getLastName(function (err, lastName) { Cryptpad.removeLoadingScreen(emitResize);
console.log("Unlocking editor"); setEditable(!readOnly);
setEditable(!readOnly); initializing = false;
initializing = false;
Cryptpad.removeLoadingScreen(emitResize);
// 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);
realtimeOptions.onLocal();
module.$userNameButton.click();
}
editor.focus(); if (readOnly) { return; }
if (newPad) { UserList.getLastName(toolbar.$userNameButton, newPad);
Cryptpad.selectTemplate('pad', info.realtime, Cryptget); editor.focus();
cursor.setToEnd(); if (newPad) {
} else { cursor.setToEnd();
cursor.setToStart(); } else {
} cursor.setToStart();
}); }
}; };
var onAbort = realtimeOptions.onAbort = function (info) { realtimeOptions.onAbort = function () {
console.log("Aborting the session!"); console.log("Aborting the session!");
// stop the user from continuing to edit // stop the user from continuing to edit
setEditable(false); setEditable(false);
@ -788,7 +613,7 @@ define([
Cryptpad.alert(Messages.common_connectionLost, undefined, true); Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}; };
var onConnectionChange = realtimeOptions.onConnectionChange = function (info) { realtimeOptions.onConnectionChange = function (info) {
setEditable(info.state); setEditable(info.state);
toolbar.failed(); toolbar.failed();
if (info.state) { if (info.state) {
@ -800,7 +625,7 @@ define([
} }
}; };
var onError = realtimeOptions.onError = onConnectError; realtimeOptions.onError = onConnectError;
var onLocal = realtimeOptions.onLocal = function () { var onLocal = realtimeOptions.onLocal = function () {
if (initializing) { return; } if (initializing) { return; }
@ -816,7 +641,7 @@ define([
} }
}; };
var rti = module.realtimeInput = realtimeInput.start(realtimeOptions); module.realtimeInput = realtimeInput.start(realtimeOptions);
Cryptpad.onLogout(function () { setEditable(false); }); Cryptpad.onLogout(function () { setEditable(false); });
@ -834,7 +659,7 @@ define([
// export the typing tests to the window. // export the typing tests to the window.
// call like `test = easyTest()` // call like `test = easyTest()`
// terminate the test like `test.cancel()` // terminate the test like `test.cancel()`
var easyTest = window.easyTest = function () { window.easyTest = function () {
cursor.update(); cursor.update();
var start = cursor.Range.start; var start = cursor.Range.start;
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
@ -846,7 +671,7 @@ define([
var interval = 100; var interval = 100;
var second = function (Ckeditor) { var second = function (Ckeditor) {
Cryptpad.ready(function (err, env) { Cryptpad.ready(function () {
andThen(Ckeditor); andThen(Ckeditor);
Cryptpad.reportAppUsage(); Cryptpad.reportAppUsage();
}); });

@ -7,11 +7,9 @@ define([
'/common/cryptget.js', '/common/cryptget.js',
'/bower_components/hyperjson/hyperjson.js', '/bower_components/hyperjson/hyperjson.js',
'render.js', 'render.js',
'/common/toolbar.js', '/common/toolbar2.js',
'/common/visible.js',
'/common/notify.js',
'/bower_components/file-saver/FileSaver.min.js' '/bower_components/file-saver/FileSaver.min.js'
], function ($, TextPatcher, Listmap, Crypto, Cryptpad, Cryptget, Hyperjson, Renderer, Toolbar, Visible, Notify) { ], function ($, TextPatcher, Listmap, Crypto, Cryptpad, Cryptget, Hyperjson, Renderer, Toolbar) {
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
@ -34,10 +32,9 @@ define([
if (!DEBUG) { if (!DEBUG) {
debug = function() {}; debug = function() {};
} }
var error = console.error;
Cryptpad.addLoadingScreen(); Cryptpad.addLoadingScreen();
var onConnectError = function (info) { var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError); Cryptpad.errorLoadingScreen(Messages.websocketError);
}; };
@ -97,15 +94,6 @@ define([
return newObj; return newObj;
}; };
var setColumnDisabled = function (id, state) {
if (!state) {
$('input[data-rt-id^="' + id + '"]').removeAttr('disabled');
return;
}
$('input[data-rt-id^="' + id + '"]').attr('disabled', 'disabled');
};
var styleUncommittedColumn = function () { var styleUncommittedColumn = function () {
var id = APP.userid; var id = APP.userid;
@ -195,20 +183,6 @@ define([
} }
}; };
var unnotify = function () {
if (APP.tabNotification &&
typeof(APP.tabNotification.cancel) === 'function') {
APP.tabNotification.cancel();
}
};
var notify = function () {
if (Visible.isSupported() && !Visible.currently()) {
unnotify();
APP.tabNotification = Notify.tab(1000, 10);
}
};
/* Any time the realtime object changes, call this function */ /* Any time the realtime object changes, call this function */
var change = function (o, n, path, throttle, cb) { var change = function (o, n, path, throttle, cb) {
if (path && !Cryptpad.isArray(path)) { if (path && !Cryptpad.isArray(path)) {
@ -237,7 +211,7 @@ define([
https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
*/ */
notify(); Cryptpad.notify();
var getFocus = function () { var getFocus = function () {
var active = document.activeElement; var active = document.activeElement;
@ -286,7 +260,7 @@ define([
}; };
/* Called whenever an event is fired on an input element */ /* Called whenever an event is fired on an input element */
var handleInput = function (input, isKeyup) { var handleInput = function (input) {
var type = input.type.toLowerCase(); var type = input.type.toLowerCase();
var id = getRealtimeId(input); var id = getRealtimeId(input);
@ -451,82 +425,8 @@ define([
}); });
}; };
var userData = APP.userData = {}; // List of pretty names for all users (mapped with their ID) var Title;
var userList; // List of users still connected to the channel (server IDs) var UserList;
var addToUserData = function(data) {
var users = userList ? userList.users : undefined;
//var userData = APP.proxy.info.userData;
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);
}
APP.proxy.info.userData = userData;
};
var setName = APP.setName = function (newName) {
if (typeof(newName) !== 'string') { return; }
var myUserNameTemp = newName.trim();
if(myUserNameTemp.length > 32) {
myUserNameTemp = myUserNameTemp.substr(0, 32);
}
var myUserName = myUserNameTemp;
var myID = APP.myID;
var myData = {};
myData[myID] = {
name: myUserName,
uid: Cryptpad.getUid(),
};
addToUserData(myData);
Cryptpad.setAttribute('username', newName, function (err, data) {
if (err) {
console.error("Couldn't set username");
return;
}
});
};
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) {
debug("Couldn't set pad title");
error(err);
document.title = oldTitle;
return;
}
document.title = data;
APP.$bar.find('.' + Toolbar.constants.title).find('span.title').text(data);
APP.$bar.find('.' + Toolbar.constants.title).find('input').val(data);
});
};
var updateDefaultTitle = function (defaultTitle) {
defaultName = defaultTitle;
APP.$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName);
};
var renameCb = function (err, title) {
if (err) { return; }
document.title = title;
APP.proxy.info.title = title === defaultName ? "" : title;
};
var suggestName = function (fallback) {
if (document.title === defaultName) {
return fallback || "";
}
return document.title || defaultName || "";
};
var copyObject = function (obj) { var copyObject = function (obj) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
@ -535,252 +435,225 @@ define([
// special UI elements // special UI elements
var $description = $('#description').attr('placeholder', Messages.poll_descriptionHint || 'description'); var $description = $('#description').attr('placeholder', Messages.poll_descriptionHint || 'description');
var ready = function (info, userid, readOnly) { var ready = function (info, userid, readOnly) {
debug("READY"); debug("READY");
debug('userid: %s', userid); debug('userid: %s', userid);
var proxy = APP.proxy; var proxy = APP.proxy;
var isNew = false; var isNew = false;
var userDoc = JSON.stringify(proxy); var userDoc = JSON.stringify(proxy);
if (userDoc === "" || userDoc === "{}") { isNew = true; } if (userDoc === "" || userDoc === "{}") { isNew = true; }
var uncommitted = APP.uncommitted = {}; var uncommitted = APP.uncommitted = {};
prepareProxy(proxy, copyObject(Render.Example)); prepareProxy(proxy, copyObject(Render.Example));
prepareProxy(uncommitted, copyObject(Render.Example)); prepareProxy(uncommitted, copyObject(Render.Example));
if (!readOnly && proxy.table.colsOrder.indexOf(userid) === -1 && if (!readOnly && proxy.table.colsOrder.indexOf(userid) === -1 &&
uncommitted.table.colsOrder.indexOf(userid) === -1) { uncommitted.table.colsOrder.indexOf(userid) === -1) {
uncommitted.table.colsOrder.unshift(userid); uncommitted.table.colsOrder.unshift(userid);
} }
var displayedObj = mergeUncommitted(proxy, uncommitted, false); var displayedObj = mergeUncommitted(proxy, uncommitted, false);
var colsOrder = sortColumns(displayedObj.table.colsOrder, userid); var colsOrder = sortColumns(displayedObj.table.colsOrder, userid);
var $table = APP.$table = $(Render.asHTML(displayedObj, null, colsOrder, readOnly)); var $table = APP.$table = $(Render.asHTML(displayedObj, null, colsOrder, readOnly));
var $createRow = APP.$createRow = $('#create-option').click(function () { APP.$createRow = $('#create-option').click(function () {
//console.error("BUTTON CLICKED! LOL"); //console.error("BUTTON CLICKED! LOL");
Render.createRow(proxy, function (empty, id) { Render.createRow(proxy, function (empty, id) {
change(null, null, null, null, function() { change(null, null, null, null, function() {
$('.edit[data-rt-id="' + id + '"]').click(); $('.edit[data-rt-id="' + id + '"]').click();
});
}); });
}); });
});
var $createCol = APP.$createCol = $('#create-user').click(function () { APP.$createCol = $('#create-user').click(function () {
Render.createColumn(proxy, function (empty, id) { Render.createColumn(proxy, function (empty, id) {
change(null, null, null, null, function() { change(null, null, null, null, function() {
$('.edit[data-rt-id="' + id + '"]').click(); $('.edit[data-rt-id="' + id + '"]').click();
});
}); });
}); });
});
// Commit button // Commit button
var $commit = APP.$commit = $('#commit').click(function () { APP.$commit = $('#commit').click(function () {
var uncommittedCopy = JSON.parse(JSON.stringify(APP.uncommitted)); var uncommittedCopy = JSON.parse(JSON.stringify(APP.uncommitted));
APP.uncommitted = {}; APP.uncommitted = {};
prepareProxy(APP.uncommitted, copyObject(Render.Example)); prepareProxy(APP.uncommitted, copyObject(Render.Example));
mergeUncommitted(proxy, uncommittedCopy, true); mergeUncommitted(proxy, uncommittedCopy, true);
APP.$commit.hide(); APP.$commit.hide();
change(); change();
}); });
// #publish button is removed in readonly // #publish button is removed in readonly
var $publish = APP.$publish = $('#publish') APP.$publish = $('#publish')
.click(function () { .click(function () {
publish(true); publish(true);
}); });
// #publish button is removed in readonly // #publish button is removed in readonly
var $admin = APP.$admin = $('#admin') APP.$admin = $('#admin')
.click(function () { .click(function () {
publish(false); publish(false);
}); });
// Title // Title
if (APP.proxy.info.defaultTitle) { if (APP.proxy.info.defaultTitle) {
updateDefaultTitle(APP.proxy.info.defaultTitle); Title.updateDefaultTitle(APP.proxy.info.defaultTitle);
} else { } else {
APP.proxy.info.defaultTitle = defaultName; APP.proxy.info.defaultTitle = Title.defaultTitle;
} }
if (Cryptpad.initialName && !APP.proxy.info.title) { if (Cryptpad.initialName && !APP.proxy.info.title) {
APP.proxy.info.title = Cryptpad.initialName; APP.proxy.info.title = Cryptpad.initialName;
updateTitle(Cryptpad.initialName); Title.updateTitle(Cryptpad.initialName);
} else { } else {
updateTitle(APP.proxy.info.title || defaultName); Title.updateTitle(APP.proxy.info.title || Title.defaultTitle);
} }
// Description // Description
var resize = function () { var resize = function () {
var lineCount = $description.val().split('\n').length; var lineCount = $description.val().split('\n').length;
$description.css('height', lineCount + 'rem'); $description.css('height', lineCount + 'rem');
}; };
$description.on('change keyup', function () { $description.on('change keyup', function () {
var val = $description.val(); var val = $description.val();
proxy.info.description = val; proxy.info.description = val;
resize();
});
resize(); resize();
if (typeof(proxy.info.description) !== 'undefined') { });
$description.val(proxy.info.description); resize();
} if (typeof(proxy.info.description) !== 'undefined') {
$description.val(proxy.info.description);
}
$('#tableScroll').html('').prepend($table); $('#tableScroll').html('').prepend($table);
updateDisplayedTable(); updateDisplayedTable();
$table $table
.click(handleClick) .click(handleClick)
.on('keyup', function (e) { handleClick(e, true); }); .on('keyup', function (e) { handleClick(e, true); });
proxy proxy
.on('change', ['info'], function (o, n, p) { .on('change', ['info'], function (o, n, p) {
if (p[1] === 'title') { if (p[1] === 'title') {
updateTitle(n); Title.updateTitle(n);
notify(); Cryptpad.notify();
} else if (p[1] === "userData") { } else if (p[1] === "userData") {
addToUserData(APP.proxy.info.userData); UserList.addToUserData(APP.proxy.info.userData);
} else if (p[1] === 'description') { } else if (p[1] === 'description') {
var op = TextPatcher.diff(o, n); var op = TextPatcher.diff(o, n);
var el = $description[0]; var el = $description[0];
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
var before = el[attr]; return TextPatcher.transformCursor(el[attr], op);
var after = TextPatcher.transformCursor(el[attr], op); });
return after; $description.val(n);
}); if (op) {
$description.val(n); el.selectionStart = selects[0];
if (op) { el.selectionEnd = selects[1];
el.selectionStart = selects[0];
el.selectionEnd = selects[1];
}
notify();
} }
Cryptpad.notify();
}
debug("change: (%s, %s, [%s])", o, n, p.join(', ')); debug("change: (%s, %s, [%s])", o, n, p.join(', '));
}) })
.on('change', ['table'], change) .on('change', ['table'], change)
.on('remove', [], change); .on('remove', [], change);
addToUserData(APP.proxy.info.userData);
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
}
UserList.addToUserData(APP.proxy.info.userData);
Cryptpad.getLastName(function (err, lastName) { APP.ready = true;
APP.ready = true; if (!proxy.published) {
publish(false);
} else {
publish(true);
}
Cryptpad.removeLoadingScreen();
if (readOnly) { return; }
UserList.getLastName(APP.toolbar.$userNameButton, isNew);
};
var disconnect = function () {
//setEditable(false); // TODO
APP.toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
};
var reconnect = function (info) {
//setEditable(true); // TODO
APP.toolbar.reconnecting(info.myId);
Cryptpad.findOKButton().click();
};
var create = function (info) {
APP.myID = info.myID;
var editHash;
if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
}
if (!proxy.published) { if (APP.realtime !== info.realtime) {
publish(false); APP.realtime = info.realtime;
} else { APP.patchText = TextPatcher.create({
publish(true); realtime: info.realtime,
} logging: true,
Cryptpad.removeLoadingScreen();
// 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 {
var myData = {};
myData[info.myId] = {
name: "",
uid: Cryptpad.getUid(),
};
addToUserData(myData);
APP.$userNameButton.click();
}
if (isNew) {
Cryptpad.selectTemplate('poll', info.realtime, Cryptget);
}
}); });
}; }
var disconnect = function (info) { var onLocal = function () {
//setEditable(false); // TODO APP.proxy.info.userData = UserList.userData;
APP.realtime.toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}; };
UserList = Cryptpad.createUserList(info, onLocal, Cryptget, Cryptpad);
var reconnect = function (info) { var onLocalTitle = function () {
//setEditable(true); // TODO APP.proxy.info.title = Title.isDefaultTitle() ? "" : Title.title;
APP.realtime.toolbar.reconnecting(info.myId);
Cryptpad.findOKButton().click();
}; };
Title = Cryptpad.createTitle({}, onLocalTitle, Cryptpad);
var create = function (info) { var configTb = {
var myID = APP.myID = info.myID; displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'],
userList: UserList.getToolbarConfig(),
share: {
secret: secret,
channel: info.channel
},
title: Title.getTitleConfig(),
common: Cryptpad,
readOnly: readOnly,
ifrw: window,
realtime: info.realtime,
network: info.network,
$container: APP.$bar
};
APP.toolbar = Toolbar.create(configTb);
var editHash; Title.setToolbar(APP.toolbar);
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
if (!readOnly) { var $rightside = APP.toolbar.$rightside;
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
}
if (APP.realtime !== info.realtime) { /* add a forget button */
APP.realtime = info.realtime; var forgetCb = function (err) {
APP.patchText = TextPatcher.create({ if (err) { return; }
realtime: info.realtime, disconnect();
logging: true, };
}); var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb);
} $rightside.append($forgetPad);
userList = APP.userList = info.userList; // set the hash
if (!readOnly) { Cryptpad.replaceHash(editHash); }
var config = { /* save as template */
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], if (!Cryptpad.isTemplate(window.location.href)) {
userData: userData, var templateObj = {
readOnly: readOnly, rt: info.realtime,
share: { Crypt: Cryptget,
secret: secret, getTitle: function () { return document.title; }
channel: info.channel
},
title: {
onRename: renameCb,
defaultName: defaultName,
suggestName: suggestName
},
ifrw: window,
common: Cryptpad,
};
var toolbar = info.realtime.toolbar = Toolbar.create(APP.$bar, info.myID, info.realtime, info.getLag, userList, config);
var $bar = APP.$bar;
var $rightside = $bar.find('.' + Toolbar.constants.rightside);
var $userBlock = $bar.find('.' + Toolbar.constants.username);
var $editShare = $bar.find('.' + Toolbar.constants.editShare);
var $viewShare = $bar.find('.' + Toolbar.constants.viewShare);
var $usernameButton = APP.$userNameButton = $($bar.find('.' + Toolbar.constants.changeUsername));
/* add a forget button */
var forgetCb = function (err, title) {
if (err) { return; }
disconnect();
}; };
var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); var $templateButton = Cryptpad.createButton('template', true, templateObj);
$rightside.append($forgetPad); $rightside.append($templateButton);
}
// set the hash };
if (!readOnly) { Cryptpad.replaceHash(editHash); }
/* save as template */
if (!Cryptpad.isTemplate(window.location.href)) {
var templateObj = {
rt: info.realtime,
Crypt: Cryptget,
getTitle: function () { return document.title; }
};
var $templateButton = Cryptpad.createButton('template', true, templateObj);
$rightside.append($templateButton);
}
Cryptpad.onDisplayNameChanged(setName);
};
// don't initialize until the store is ready. // don't initialize until the store is ready.
Cryptpad.ready(function () { Cryptpad.ready(function () {
@ -814,6 +687,7 @@ define([
if (!userid) { userid = Render.coluid(); } if (!userid) { userid = Render.coluid(); }
APP.userid = userid; APP.userid = userid;
Cryptpad.setPadAttribute('userid', userid, function (e) { Cryptpad.setPadAttribute('userid', userid, function (e) {
if (e) { console.error(e); }
ready(info, userid, readOnly); ready(info, userid, readOnly);
}); });
}); });

@ -57,7 +57,7 @@ var Renderer = function (Cryptpad) {
return null; return null;
}; };
var getCoordinates = Render.getCoordinates = function (id) { Render.getCoordinates = function (id) {
return id.split('_'); return id.split('_');
}; };
@ -91,7 +91,7 @@ var Renderer = function (Cryptpad) {
return null; return null;
}; };
var createColumn = Render.createColumn = function (obj, cb, id, value) { Render.createColumn = function (obj, cb, id, value) {
var order = Cryptpad.find(obj, ['table', 'colsOrder']); var order = Cryptpad.find(obj, ['table', 'colsOrder']);
if (!order) { throw new Error("Uninitialized realtime object!"); } if (!order) { throw new Error("Uninitialized realtime object!"); }
id = id || coluid(); id = id || coluid();
@ -101,7 +101,7 @@ var Renderer = function (Cryptpad) {
if (typeof(cb) === 'function') { cb(void 0, id); } if (typeof(cb) === 'function') { cb(void 0, id); }
}; };
var removeColumn = Render.removeColumn = function (obj, id, cb) { Render.removeColumn = function (obj, id, cb) {
var order = Cryptpad.find(obj, ['table', 'colsOrder']); var order = Cryptpad.find(obj, ['table', 'colsOrder']);
var parent = Cryptpad.find(obj, ['table', 'cols']); var parent = Cryptpad.find(obj, ['table', 'cols']);
@ -126,7 +126,7 @@ var Renderer = function (Cryptpad) {
} }
}; };
var createRow = Render.createRow = function (obj, cb, id, value) { Render.createRow = function (obj, cb, id, value) {
var order = Cryptpad.find(obj, ['table', 'rowsOrder']); var order = Cryptpad.find(obj, ['table', 'rowsOrder']);
if (!order) { throw new Error("Uninitialized realtime object!"); } if (!order) { throw new Error("Uninitialized realtime object!"); }
id = id || rowuid(); id = id || rowuid();
@ -136,7 +136,7 @@ var Renderer = function (Cryptpad) {
if (typeof(cb) === 'function') { cb(void 0, id); } if (typeof(cb) === 'function') { cb(void 0, id); }
}; };
var removeRow = Render.removeRow = function (obj, id, cb) { Render.removeRow = function (obj, id, cb) {
var order = Cryptpad.find(obj, ['table', 'rowsOrder']); var order = Cryptpad.find(obj, ['table', 'rowsOrder']);
var parent = Cryptpad.find(obj, ['table', 'rows']); var parent = Cryptpad.find(obj, ['table', 'rows']);
@ -153,7 +153,7 @@ var Renderer = function (Cryptpad) {
if (typeof(cb) === 'function') { cb(); } if (typeof(cb) === 'function') { cb(); }
}; };
var setValue = Render.setValue = function (obj, id, value) { Render.setValue = function (obj, id, value) {
var type = typeofId(id); var type = typeofId(id);
switch (type) { switch (type) {
@ -167,7 +167,7 @@ var Renderer = function (Cryptpad) {
} }
}; };
var getValue = Render.getValue = function (obj, id) { Render.getValue = function (obj, id) {
switch (typeofId(id)) { switch (typeofId(id)) {
case 'row': return getRowValue(obj, id); case 'row': return getRowValue(obj, id);
case 'col': return getColumnValue(obj, id); case 'col': return getColumnValue(obj, id);
@ -219,7 +219,7 @@ var Renderer = function (Cryptpad) {
})); }));
} }
if (i === rows.length) { if (i === rows.length) {
return [null].concat(cols.map(function (col) { return [null].concat(cols.map(function () {
return { return {
'class': 'lastRow', 'class': 'lastRow',
}; };
@ -359,7 +359,7 @@ var Renderer = function (Cryptpad) {
return ['TABLE', {id:'table'}, [head, foot, body]]; return ['TABLE', {id:'table'}, [head, foot, body]];
}; };
var asHTML = Render.asHTML = function (obj, rows, cols, readOnly) { Render.asHTML = function (obj, rows, cols, readOnly) {
return Hyperjson.toDOM(toHyperjson(cellMatrix(obj, rows, cols, readOnly), readOnly)); return Hyperjson.toDOM(toHyperjson(cellMatrix(obj, rows, cols, readOnly), readOnly));
}; };
@ -384,9 +384,7 @@ var Renderer = function (Cryptpad) {
var op = TextPatcher.diff(o, n); var op = TextPatcher.diff(o, n);
info.selection = ['selectionStart', 'selectionEnd'].map(function (attr) { info.selection = ['selectionStart', 'selectionEnd'].map(function (attr) {
var before = element[attr]; return TextPatcher.transformCursor(element[attr], op);
var after = TextPatcher.transformCursor(element[attr], op);
return after;
}); });
} }
}; };
@ -432,7 +430,7 @@ var Renderer = function (Cryptpad) {
} }
}; };
var updateTable = Render.updateTable = function (table, obj, conf) { Render.updateTable = function (table, obj, conf) {
var DD = new DiffDOM(diffOptions); var DD = new DiffDOM(diffOptions);
var rows = conf ? conf.rows : null; var rows = conf ? conf.rows : null;

@ -2,14 +2,8 @@ define([
'jquery', 'jquery',
'/common/login.js', '/common/login.js',
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/cryptget.js', '/common/credential.js' // preloaded for login.js
'/common/credential.js' ], function ($, Login, Cryptpad) {
], function ($, Login, Cryptpad, Crypt) {
var APP = window.APP = {
Login: Login,
};
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
$(function () { $(function () {
@ -107,57 +101,63 @@ define([
function (yes) { function (yes) {
if (!yes) { return; } if (!yes) { return; }
Cryptpad.addLoadingScreen(Messages.login_hashing); // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up
Login.loginOrRegister(uname, passwd, true, function (err, result) { window.setTimeout(function () {
var proxy = result.proxy; Cryptpad.addLoadingScreen(Messages.login_hashing);
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
if (err) { window.setTimeout(function () {
switch (err) { Login.loginOrRegister(uname, passwd, true, function (err, result) {
case 'NO_SUCH_USER': var proxy = result.proxy;
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser); if (err) {
}); switch (err) {
break; case 'NO_SUCH_USER':
case 'INVAL_USER': Cryptpad.removeLoadingScreen(function () {
Cryptpad.removeLoadingScreen(function () { Cryptpad.alert(Messages.login_noSuchUser);
Cryptpad.alert(Messages.login_invalUser); });
}); break;
break; case 'INVAL_USER':
case 'INVAL_PASS': Cryptpad.removeLoadingScreen(function () {
Cryptpad.removeLoadingScreen(function () { Cryptpad.alert(Messages.login_invalUser);
Cryptpad.alert(Messages.login_invalPass); });
}); break;
break; case 'INVAL_PASS':
case 'ALREADY_REGISTERED': Cryptpad.removeLoadingScreen(function () {
Cryptpad.removeLoadingScreen(function () { Cryptpad.alert(Messages.login_invalPass);
Cryptpad.confirm(Messages.register_alreadyRegistered, function (yes) { });
if (!yes) { return; } break;
proxy.login_name = uname; case 'ALREADY_REGISTERED':
Cryptpad.removeLoadingScreen(function () {
if (!proxy[Cryptpad.displayNameKey]) { Cryptpad.confirm(Messages.register_alreadyRegistered, function (yes) {
proxy[Cryptpad.displayNameKey] = uname; if (!yes) { return; }
} proxy.login_name = uname;
Cryptpad.eraseTempSessionValues();
logMeIn(result); if (!proxy[Cryptpad.displayNameKey]) {
}); proxy[Cryptpad.displayNameKey] = uname;
}); }
break; Cryptpad.eraseTempSessionValues();
default: // UNHANDLED ERROR logMeIn(result);
Cryptpad.errorLoadingScreen(Messages.login_unhandledError); });
} });
return; break;
} default: // UNHANDLED ERROR
Cryptpad.eraseTempSessionValues(); Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
if (shouldImport) { }
sessionStorage.migrateAnonDrive = 1; return;
} }
Cryptpad.eraseTempSessionValues();
proxy.login_name = uname; if (shouldImport) {
proxy[Cryptpad.displayNameKey] = uname; sessionStorage.migrateAnonDrive = 1;
sessionStorage.createReadme = 1; }
logMeIn(result); proxy.login_name = uname;
}); proxy[Cryptpad.displayNameKey] = uname;
sessionStorage.createReadme = 1;
logMeIn(result);
});
}, 0);
}, 100);
}, { }, {
ok: Messages.register_writtenPassword, ok: Messages.register_writtenPassword,
cancel: Messages.register_cancel, cancel: Messages.register_cancel,

@ -97,7 +97,7 @@
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li> <li class="title" data-localization="footer_contact"><li>
<li><a href="http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7" target="_blank" rel="noopener noreferrer">IRC</a></li> <li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li> <li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li> <li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li> <li><a href="/contact.html">Email</a></li>
@ -105,7 +105,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div> <div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer> </footer>
</body> </body>

@ -16,10 +16,6 @@ define([
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
var redirectToMain = function () {
window.location.href = '/';
};
// Manage changes in the realtime object made from another page // Manage changes in the realtime object made from another page
var onRefresh = function (h) { var onRefresh = function (h) {
if (typeof(h) !== "function") { return; } if (typeof(h) !== "function") { return; }
@ -71,7 +67,7 @@ define([
var createDisplayNameInput = function (store) { var createDisplayNameInput = function (store) {
var obj = store.proxy; var obj = store.proxy;
var $div = $('<div>', {'class': 'displayName'}); var $div = $('<div>', {'class': 'displayName'});
var $label = $('<label>', {'for' : 'displayName'}).text(Messages.user_displayName).appendTo($div); $('<label>', {'for' : 'displayName'}).text(Messages.user_displayName).appendTo($div);
$('<br>').appendTo($div); $('<br>').appendTo($div);
var $input = $('<input>', { var $input = $('<input>', {
'type': 'text', 'type': 'text',
@ -114,7 +110,7 @@ define([
}; };
var createResetTips = function () { var createResetTips = function () {
var $div = $('<div>', {'class': 'resetTips'}); var $div = $('<div>', {'class': 'resetTips'});
var $label = $('<label>', {'for' : 'resetTips'}).text(Messages.settings_resetTips).appendTo($div); $('<label>', {'for' : 'resetTips'}).text(Messages.settings_resetTips).appendTo($div);
$('<br>').appendTo($div); $('<br>').appendTo($div);
var $button = $('<button>', {'id': 'resetTips', 'class': 'btn btn-primary'}) var $button = $('<button>', {'id': 'resetTips', 'class': 'btn btn-primary'})
.text(Messages.settings_resetTipsButton).appendTo($div); .text(Messages.settings_resetTipsButton).appendTo($div);
@ -145,7 +141,7 @@ define([
saveAs(blob, filename); saveAs(blob, filename);
}); });
}; };
var importFile = function (content, file) { var importFile = function (content) {
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).appendTo($div); var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).appendTo($div);
Crypt.put(Cryptpad.getUserHash() || localStorage[Cryptpad.fileHashKey], content, function (e) { Crypt.put(Cryptpad.getUserHash() || localStorage[Cryptpad.fileHashKey], content, function (e) {
if (e) { console.error(e); } if (e) { console.error(e); }
@ -153,7 +149,7 @@ define([
}); });
}; };
var $label = $('<label>', {'for' : 'exportDrive'}).text(Messages.settings_backupTitle).appendTo($div); $('<label>', {'for' : 'exportDrive'}).text(Messages.settings_backupTitle).appendTo($div);
$('<br>').appendTo($div); $('<br>').appendTo($div);
/* add an export button */ /* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportFile); var $export = Cryptpad.createButton('export', true, {}, exportFile);
@ -170,7 +166,7 @@ define([
var createResetDrive = function (obj) { var createResetDrive = function (obj) {
var $div = $('<div>', {'class': 'resetDrive'}); var $div = $('<div>', {'class': 'resetDrive'});
var $label = $('<label>', {'for' : 'resetDrive'}).text(Messages.settings_resetTitle).appendTo($div); $('<label>', {'for' : 'resetDrive'}).text(Messages.settings_resetTitle).appendTo($div);
$('<br>').appendTo($div); $('<br>').appendTo($div);
var $button = $('<button>', {'id': 'resetDrive', 'class': 'btn btn-danger'}) var $button = $('<button>', {'id': 'resetDrive', 'class': 'btn btn-danger'})
.text(Messages.settings_reset).appendTo($div); .text(Messages.settings_reset).appendTo($div);
@ -256,10 +252,47 @@ define([
return $div; return $div;
}; };
var createLogoutEverywhere = function (obj) {
var proxy = obj.proxy;
var $div = $('<div>', { 'class': 'logoutEverywhere', });
$('<label>', { 'for': 'logoutEverywhere'})
.text(Messages.settings_logoutEverywhereTitle).appendTo($div);
$('<br>').appendTo($div);
var $button = $('<button>', { id: 'logoutEverywhere', 'class': 'btn btn-primary' })
.text(Messages.settings_logoutEverywhere)
.appendTo($div);
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide().appendTo($div);
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide().appendTo($div);
$button.click(function () {
var realtime = obj.info.realtime;
console.log(realtime);
Cryptpad.confirm(Messages.settings_logoutEverywhereConfirm, function (yes) {
if (!yes) { return; }
$spinner.show();
$ok.hide();
var token = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);
localStorage.setItem('loginToken', token);
proxy.loginToken = token;
Cryptpad.whenRealtimeSyncs(realtime, function () {
$spinner.hide();
$ok.show();
window.setTimeout(function () {
$ok.fadeOut(1500);
}, 2500);
});
});
});
return $div;
};
var createImportLocalPads = function (obj) { var createImportLocalPads = function (obj) {
if (!Cryptpad.isLoggedIn()) { return; } if (!Cryptpad.isLoggedIn()) { return; }
var $div = $('<div>', {'class': 'importLocalPads'}); var $div = $('<div>', {'class': 'importLocalPads'});
var $label = $('<label>', {'for' : 'importLocalPads'}).text(Messages.settings_importTitle).appendTo($div); $('<label>', {'for' : 'importLocalPads'}).text(Messages.settings_importTitle).appendTo($div);
$('<br>').appendTo($div); $('<br>').appendTo($div);
var $button = $('<button>', {'id': 'importLocalPads', 'class': 'btn btn-primary'}) var $button = $('<button>', {'id': 'importLocalPads', 'class': 'btn btn-primary'})
.text(Messages.settings_import).appendTo($div); .text(Messages.settings_import).appendTo($div);
@ -284,7 +317,7 @@ define([
var createLanguageSelector = function () { var createLanguageSelector = function () {
var $div = $('<div>', {'class': 'importLocalPads'}); var $div = $('<div>', {'class': 'importLocalPads'});
var $label = $('<label>').text(Messages.language).appendTo($div); $('<label>').text(Messages.language).appendTo($div);
$('<br>').appendTo($div); $('<br>').appendTo($div);
var $b = Cryptpad.createLanguageSelector().appendTo($div); var $b = Cryptpad.createLanguageSelector().appendTo($div);
$b.find('button').addClass('btn btn-secondary'); $b.find('button').addClass('btn btn-secondary');
@ -296,6 +329,10 @@ define([
APP.$container.append(createInfoBlock(obj)); APP.$container.append(createInfoBlock(obj));
APP.$container.append(createDisplayNameInput(obj)); APP.$container.append(createDisplayNameInput(obj));
APP.$container.append(createLanguageSelector()); APP.$container.append(createLanguageSelector());
if (Cryptpad.isLoggedIn()) {
APP.$container.append(createLogoutEverywhere(obj));
}
APP.$container.append(createResetTips()); APP.$container.append(createResetTips());
APP.$container.append(createBackupDrive(obj)); APP.$container.append(createBackupDrive(obj));
APP.$container.append(createImportLocalPads(obj)); APP.$container.append(createImportLocalPads(obj));
@ -343,7 +380,6 @@ define([
}); });
window.addEventListener('storage', function (e) { window.addEventListener('storage', function (e) {
var key = e.key;
if (e.key !== Cryptpad.userHashKey) { return; } if (e.key !== Cryptpad.userHashKey) { return; }
var o = e.oldValue; var o = e.oldValue;
var n = e.newValue; var n = e.newValue;

@ -3,6 +3,7 @@
<head> <head>
<title>CryptPad</title> <title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> <meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script> <script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<link rel="icon" type="image/png" <link rel="icon" type="image/png"
href="/customize/main-favicon.png" href="/customize/main-favicon.png"

@ -3,20 +3,13 @@ define([
'/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/textpatcher/TextPatcher.js', '/bower_components/textpatcher/TextPatcher.js',
'/common/toolbar.js', '/common/toolbar2.js',
'json.sortify', 'json.sortify',
'/bower_components/chainpad-json-validator/json-ot.js', '/bower_components/chainpad-json-validator/json-ot.js',
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/cryptget.js', '/common/cryptget.js',
'/common/modes.js',
'/common/themes.js',
'/common/visible.js',
'/common/notify.js',
'/slide/slide.js', '/slide/slide.js',
'/bower_components/file-saver/FileSaver.min.js' ], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Slide) {
], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Modes, Themes, Visible, Notify, Slide) {
var saveAs = window.saveAs;
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
var module = window.APP = { var module = window.APP = {
@ -30,23 +23,15 @@ define([
var SLIDE_COLOR_ID = "cryptpad-color"; var SLIDE_COLOR_ID = "cryptpad-color";
var stringify = function (obj) {
return JSONSortify(obj);
};
var setTabTitle = function () {
var slideNumber = '';
if (Slide.index && Slide.content.length) {
slideNumber = ' (' + Slide.index + '/' + Slide.content.length + ')';
}
document.title = APP.title + slideNumber;
};
$(function () { $(function () {
Cryptpad.addLoadingScreen(); Cryptpad.addLoadingScreen();
var stringify = function (obj) {
return JSONSortify(obj);
};
var ifrw = module.ifrw = $('#pad-iframe')[0].contentWindow; var ifrw = module.ifrw = $('#pad-iframe')[0].contentWindow;
var toolbar; var toolbar;
var editor;
var secret = Cryptpad.getSecrets(); var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr; var readOnly = secret.keys && !secret.keys.editKeyStr;
@ -57,82 +42,37 @@ define([
var presentMode = Slide.isPresentURL(); var presentMode = Slide.isPresentURL();
var onConnectError = function (info) { var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError); Cryptpad.errorLoadingScreen(Messages.websocketError);
}; };
var andThen = function (CMeditor) { var andThen = function (CMeditor) {
var CodeMirror = module.CodeMirror = CMeditor; var CodeMirror = Cryptpad.createCodemirror(CMeditor, ifrw, Cryptpad);
CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js"; editor = CodeMirror.editor;
var $pad = $('#pad-iframe');
var $textarea = $pad.contents().find('#editor1');
var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox');
var parsedHash = Cryptpad.parsePadUrl(window.location.href); var $pad = $('#pad-iframe');
var defaultName = Cryptpad.getDefaultName(parsedHash);
var initialState = Messages.slideInitialState;
var isHistoryMode = false; var isHistoryMode = false;
var editor = module.editor = CMeditor.fromTextArea($textarea[0], { var setEditable = module.setEditable = function (bool) {
lineNumbers: true, if (readOnly && bool) { return; }
lineWrapping: true, editor.setOption('readOnly', !bool);
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(initialState);
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 && $select.val) { $select.val(mode); }
}; };
setMode('markdown');
var setTheme = module.setTheme = (function () {
var path = '/common/theme/';
var $head = $(ifrw.document.head); var Title;
var UserList;
var Metadata;
var themeLoaded = module.themeLoaded = function (theme) { var setTabTitle = function (title) {
return $head.find('link[href*="'+theme+'"]').length; var slideNumber = '';
}; if (Slide.index && Slide.content.length) {
slideNumber = ' (' + Slide.index + '/' + Slide.content.length + ')';
var loadTheme = module.loadTheme = function (theme) { }
$head.append($('<link />', { document.title = title + slideNumber;
rel: 'stylesheet', };
href: path + theme + '.css',
}));
};
return function (theme, $select) { var initialState = Messages.slideInitialState;
if (!theme) {
editor.setOption('theme', 'default');
} else {
if (!themeLoaded(theme)) {
loadTheme(theme);
}
editor.setOption('theme', theme);
}
if ($select) {
$select.setValue(theme || 'Theme');
}
};
}());
var $modal = $pad.contents().find('#modal'); var $modal = $pad.contents().find('#modal');
var $content = $pad.contents().find('#content'); var $content = $pad.contents().find('#content');
@ -141,65 +81,21 @@ define([
Slide.setModal(APP, $modal, $content, $pad, ifrw, slideOptions, initialState); Slide.setModal(APP, $modal, $content, $pad, ifrw, slideOptions, initialState);
var setStyleState = function (state) {
$pad.contents().find('#print, #content').find('style').each(function (i, el) {
el.disabled = !state;
});
};
var enterPresentationMode = function (shouldLog) { var enterPresentationMode = function (shouldLog) {
Slide.show(true, editor.getValue()); Slide.show(true, editor.getValue());
if (shouldLog) { if (shouldLog) {
Cryptpad.log(Messages.presentSuccess); Cryptpad.log(Messages.presentSuccess);
} }
}; };
var leavePresentationMode = function () {
setStyleState(false);
Slide.show(false);
};
if (presentMode) { if (presentMode) {
enterPresentationMode(true); enterPresentationMode(true);
} }
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 textColor; var textColor;
var backColor; var backColor;
var myData = {};
var myUserName = ''; // My "pretty name"
var myID; // My server ID
var setMyID = function(info) {
myID = info.myID || null;
myUserName = myID;
};
var config = { var config = {
//initialState: Messages.codeInitialState,
initialState: '{}', initialState: '{}',
websocketURL: Cryptpad.getWebsocketURL(), websocketURL: Cryptpad.getWebsocketURL(),
channel: secret.channel, channel: secret.channel,
@ -207,7 +103,6 @@ define([
validateKey: secret.keys.validateKey || undefined, validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly, readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys), crypto: Crypto.createEncryptor(secret.keys),
setMyID: setMyID,
transformFunction: JsonOT.validate, transformFunction: JsonOT.validate,
network: Cryptpad.getNetwork() network: Cryptpad.getNetwork()
}; };
@ -222,24 +117,19 @@ define([
} }
}; };
var isDefaultTitle = function () {
var parsed = Cryptpad.parsePadUrl(window.location.href);
return Cryptpad.isDefaultName(parsed, APP.title);
};
var initializing = true; var initializing = true;
var stringifyInner = function (textValue) { var stringifyInner = function (textValue) {
var obj = { var obj = {
content: textValue, content: textValue,
metadata: { metadata: {
users: userData, users: UserList.userData,
defaultTitle: defaultName, defaultTitle: Title.defaultTitle,
slideOptions: slideOptions slideOptions: slideOptions
} }
}; };
if (!initializing) { if (!initializing) {
obj.metadata.title = APP.title; obj.metadata.title = Title.title;
} }
if (textColor) { if (textColor) {
obj.metadata.color = textColor; obj.metadata.color = textColor;
@ -258,7 +148,7 @@ define([
editor.save(); editor.save();
var textValue = canonicalize($textarea.val()); var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson = stringifyInner(textValue); var shjson = stringifyInner(textValue);
module.patchText(shjson); module.patchText(shjson);
@ -269,120 +159,16 @@ define([
} }
}; };
var setName = module.setName = function (newName) { var metadataCfg = {
if (typeof(newName) !== 'string') { return; } slideOptions: function (newOpt) {
var myUserNameTemp = newName.trim(); if (stringify(newOpt) !== stringify(slideOptions)) {
if(newName.trim().length > 32) { $.extend(slideOptions, newOpt);
myUserNameTemp = myUserNameTemp.substr(0, 32); // TODO: manage realtime + cursor in the "options" modal ??
} Slide.updateOptions();
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) {
// 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;
} }
});
return text.trim();
};
var suggestName = function () {
if (APP.title === defaultName) {
return getHeadingText() || "";
} else {
return APP.title || getHeadingText() || defaultName;
} }
}; };
var updateColors = metadataCfg.slideColors = function (text, back) {
var exportText = module.exportText = function () {
var text = editor.getValue();
var ext = Modes.extensionOf(module.highlightMode);
var title = Cryptpad.fixFileName(suggestName()) + ext;
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 updateTitle = function (newTitle) {
if (newTitle === APP.title) { return; }
// Change the title now, and set it back to the old value if there is an error
var oldTitle = APP.title;
APP.title = newTitle;
setTabTitle();
Cryptpad.renamePad(newTitle, function (err, data) {
if (err) {
console.log("Couldn't set pad title");
console.error(err);
APP.title = oldTitle;
setTabTitle();
return;
}
APP.title = data;
setTabTitle();
$bar.find('.' + Toolbar.constants.title).find('span.title').text(data);
$bar.find('.' + Toolbar.constants.title).find('input').val(data);
if (slideOptions.title) { Slide.updateOptions(); }
});
};
var updateColors = function (text, back) {
if (text) { if (text) {
textColor = text; textColor = text;
$modal.css('color', text); $modal.css('color', text);
@ -397,56 +183,12 @@ define([
} }
}; };
var updateOptions = function (newOpt) {
if (stringify(newOpt) !== stringify(slideOptions)) {
$.extend(slideOptions, newOpt);
// TODO: manage realtime + cursor in the "options" modal ??
Slide.updateOptions();
}
};
var updateDefaultTitle = function (defaultTitle) {
defaultName = defaultTitle;
$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName);
};
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;
}
updateOptions(json.metadata.slideOptions);
updateColors(json.metadata.color, json.metadata.backColor);
}
if (!titleUpdated) {
updateTitle(defaultName);
}
};
var renameCb = function (err, title) {
if (err) { return; }
APP.title = title;
setTabTitle();
onLocal();
};
var createPrintDialog = function () { var createPrintDialog = function () {
var slideOptionsTmp = { var slideOptionsTmp = {
title: false, title: false,
slide: false, slide: false,
date: false, date: false,
transition: true,
style: '' style: ''
}; };
@ -479,10 +221,20 @@ define([
}).appendTo($p).css('width', 'auto'); }).appendTo($p).css('width', 'auto');
$('<label>', {'for': 'checkTitle'}).text(Messages.printTitle).appendTo($p); $('<label>', {'for': 'checkTitle'}).text(Messages.printTitle).appendTo($p);
$p.append($('<br>')); $p.append($('<br>'));
// Transition
$('<input>', {type: 'checkbox', id: 'checkTransition', checked: slideOptionsTmp.transition}).on('change', function () {
var c = this.checked;
slideOptionsTmp.transition = c;
}).appendTo($p).css('width', 'auto');
$('<label>', {'for': 'checkTransition'}).text(Messages.printTransition).appendTo($p);
$p.append($('<br>'));
// CSS // CSS
$('<label>', {'for': 'cssPrint'}).text(Messages.printCSS).appendTo($p); $('<label>', {'for': 'cssPrint'}).text(Messages.printCSS).appendTo($p);
$p.append($('<br>')); $p.append($('<br>'));
var $textarea = $('<textarea>', {'id':'cssPrint'}).css({'width':'100%', 'height':'100px'}).appendTo($p); var $textarea = $('<textarea>', {'id':'cssPrint'}).css({'width':'100%', 'height':'100px'}).appendTo($p)
.on('keydown keyup', function (e) {
e.stopPropagation();
});
$textarea.val(slideOptionsTmp.style); $textarea.val(slideOptionsTmp.style);
window.setTimeout(function () { $textarea.focus(); }, 0); window.setTimeout(function () { $textarea.focus(); }, 0);
@ -503,73 +255,63 @@ define([
h = Cryptpad.listenForKeys(todo, todoCancel); h = Cryptpad.listenForKeys(todo, todoCancel);
var $nav = $('<nav>').appendTo($div); var $nav = $('<nav>').appendTo($div);
var $cancel = $('<button>', {'class': 'cancel'}).text(Messages.cancelButton).appendTo($nav).click(todoCancel); $('<button>', {'class': 'cancel'}).text(Messages.cancelButton).appendTo($nav).click(todoCancel);
var $ok = $('<button>', {'class': 'ok'}).text(Messages.slideOptionsButton).appendTo($nav).click(todo); $('<button>', {'class': 'ok'}).text(Messages.settings_save).appendTo($nav).click(todo);
return $container; return $container;
}; };
var onInit = config.onInit = function (info) { config.onInit = function (info) {
userList = info.userList; UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad);
var titleCfg = {
updateLocalTitle: setTabTitle,
getHeadingText: CodeMirror.getHeadingText
};
Title = Cryptpad.createTitle(titleCfg, config.onLocal, Cryptpad);
Metadata = Cryptpad.createMetadata(UserList, Title, metadataCfg);
var configTb = { var configTb = {
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'],
userData: userData, userList: UserList.getToolbarConfig(),
readOnly: readOnly,
ifrw: ifrw,
share: { share: {
secret: secret, secret: secret,
channel: info.channel channel: info.channel
}, },
title: { title: Title.getTitleConfig(),
onRename: renameCb, common: Cryptpad,
defaultName: defaultName, readOnly: readOnly,
suggestName: suggestName ifrw: ifrw,
}, realtime: info.realtime,
common: Cryptpad network: info.network,
$container: $bar
}; };
toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, info.userList, configTb); toolbar = module.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
CodeMirror.init(config.onLocal, Title, toolbar);
var $rightside = $bar.find('.' + Toolbar.constants.rightside); var $rightside = toolbar.$rightside;
var $userBlock = $bar.find('.' + Toolbar.constants.username);
var $usernameButton = module.$userNameButton = $($bar.find('.' + Toolbar.constants.changeUsername));
var editHash; var editHash;
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
if (!readOnly) { if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
} }
/* add a history button */ /* add a history button */
var histConfig = {}; var histConfig = {
histConfig.onRender = function (val) { onLocal: config.onLocal(),
if (typeof val === "undefined") { return; } onRemote: config.onRemote(),
try { setHistory: setHistory,
var hjson = JSON.parse(val || '{}'); applyVal: function (val) {
var remoteDoc = hjson.content; var remoteDoc = JSON.parse(val || '{}').content;
editor.setValue(remoteDoc || ''); editor.setValue(remoteDoc || '');
editor.save(); editor.save();
} catch (e) { },
// Probably a parse error $toolbar: $bar
console.error(e);
}
};
histConfig.onClose = function () {
// Close button clicked
setHistory(false, true);
};
histConfig.onRevert = function () {
// Revert button clicked
setHistory(false, false);
config.onLocal();
config.onRemote();
};
histConfig.onReady = function () {
// Called when the history is loaded and the UI displayed
setHistory(true);
}; };
histConfig.$toolbar = $bar;
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist); $rightside.append($hist);
@ -585,21 +327,17 @@ define([
} }
/* add an export button */ /* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportText); var $export = Cryptpad.createButton('export', true, {}, CodeMirror.exportText);
$rightside.append($export); $rightside.append($export);
if (!readOnly) { if (!readOnly) {
/* add an import button */ /* add an import button */
var $import = Cryptpad.createButton('import', true, {}, importText); var $import = Cryptpad.createButton('import', true, {}, CodeMirror.importText);
$rightside.append($import); $rightside.append($import);
/* add a rename button */
//var $setTitle = Cryptpad.createButton('rename', true, {suggestName: suggestName}, renameCb);
//$rightside.append($setTitle);
} }
/* add a forget button */ /* add a forget button */
var forgetCb = function (err, title) { var forgetCb = function (err) {
if (err) { return; } if (err) { return; }
setEditable(false); setEditable(false);
}; };
@ -641,50 +379,6 @@ define([
} }
$rightside.append($present); $rightside.append($present);
var $leavePresent = Cryptpad.createButton('source', true)
.click(leavePresentationMode);
if (!presentMode) {
$leavePresent.hide();
}
$rightside.append($leavePresent);
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);
};
var configureColors = function () { var configureColors = function () {
var $back = $('<button>', { var $back = $('<button>', {
id: SLIDE_BACKCOLOR_ID, id: SLIDE_BACKCOLOR_ID,
@ -731,7 +425,7 @@ define([
}; };
configureColors(); configureColors();
configureTheme(); CodeMirror.configureTheme();
if (presentMode) { if (presentMode) {
$('#top-bar').hide(); $('#top-bar').hide();
@ -741,27 +435,9 @@ define([
if (!window.location.hash || window.location.hash === '#') { if (!window.location.hash || window.location.hash === '#') {
Cryptpad.replaceHash(editHash); 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 () { config.onReady = function (info) {
if (Visible.isSupported() && !Visible.currently()) {
unnotify();
module.tabNotification = Notify.tab(1000, 10);
}
};
var onReady = config.onReady = function (info) {
module.users = info.userList.users;
if (module.realtime !== info.realtime) { if (module.realtime !== info.realtime) {
var realtime = module.realtime = info.realtime; var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
@ -787,142 +463,64 @@ define([
} }
if (hjson.highlightMode) { if (hjson.highlightMode) {
setMode(hjson.highlightMode, module.$language); CodeMirror.setMode(hjson.highlightMode);
} }
} }
if (!CodeMirror.highlightMode) {
if (!module.highlightMode) { CodeMirror.setMode('markdown');
setMode('javascript', module.$language);
console.log("%s => %s", module.highlightMode, module.$language.val());
} }
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
updateMetadata(userDoc); Metadata.update(userDoc);
editor.setValue(newDoc || initialState); editor.setValue(newDoc || initialState);
if (Cryptpad.initialName && APP.title === defaultName) { if (Cryptpad.initialName && Title.isDefaultTitle()) {
updateTitle(Cryptpad.initialName); Title.updateTitle(Cryptpad.initialName);
onLocal(); onLocal();
} }
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
}
Slide.onChange(function (o, n, l) { Slide.onChange(function (o, n, l) {
if (n !== null) { if (n !== null) {
document.title = APP.title + ' (' + (++n) + '/' + l + ')'; document.title = Title.title + ' (' + (++n) + '/' + l + ')';
return; return;
} }
console.log("Exiting presentation mode"); console.log("Exiting presentation mode");
document.title = APP.title; document.title = Title.title;
}); });
Cryptpad.removeLoadingScreen(); Cryptpad.removeLoadingScreen();
setEditable(true); setEditable(true);
initializing = false; initializing = false;
//Cryptpad.log("Your document is ready");
onLocal(); // push local state to avoid parse errors later. 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('slide', 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) { if (readOnly) { return; }
var cursor = { UserList.getLastName(toolbar.$userNameButton, isNew);
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;
}; };
var onRemote = config.onRemote = function () { config.onRemote = function () {
if (initializing) { return; } if (initializing) { return; }
if (isHistoryMode) { return; } if (isHistoryMode) { return; }
var scroll = editor.getScrollInfo();
var oldDoc = canonicalize($textarea.val()); var oldDoc = canonicalize(CodeMirror.$textarea.val());
var shjson = module.realtime.getUserDoc(); var shjson = module.realtime.getUserDoc();
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
updateMetadata(shjson); Metadata.update(shjson);
var hjson = JSON.parse(shjson); var hjson = JSON.parse(shjson);
var remoteDoc = hjson.content; var remoteDoc = hjson.content;
var highlightMode = hjson.highlightMode; var highlightMode = hjson.highlightMode;
if (highlightMode && highlightMode !== module.highlightMode) { if (highlightMode && highlightMode !== CodeMirror.highlightMode) {
setMode(highlightMode, module.$language); CodeMirror.setMode(highlightMode);
}
//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); CodeMirror.setValueAndCursor(oldDoc, remoteDoc, TextPatcher);
if (!readOnly) { if (!readOnly) {
var textValue = canonicalize($textarea.val()); var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson2 = stringifyInner(textValue); var shjson2 = stringifyInner(textValue);
if (shjson2 !== shjson) { if (shjson2 !== shjson) {
console.error("shjson2 !== shjson"); console.error("shjson2 !== shjson");
@ -933,18 +531,18 @@ define([
Slide.update(remoteDoc); Slide.update(remoteDoc);
if (oldDoc !== remoteDoc) { if (oldDoc !== remoteDoc) {
notify(); Cryptpad.notify();
} }
}; };
var onAbort = config.onAbort = function (info) { config.onAbort = function () {
// inform of network disconnect // inform of network disconnect
setEditable(false); setEditable(false);
toolbar.failed(); toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true); Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}; };
var onConnectionChange = config.onConnectionChange = function (info) { config.onConnectionChange = function (info) {
setEditable(info.state); setEditable(info.state);
toolbar.failed(); toolbar.failed();
if (info.state) { if (info.state) {
@ -956,9 +554,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); editor.on('change', onLocal);
@ -968,7 +566,7 @@ define([
var interval = 100; var interval = 100;
var second = function (CM) { var second = function (CM) {
Cryptpad.ready(function (err, env) { Cryptpad.ready(function () {
andThen(CM); andThen(CM);
Cryptpad.reportAppUsage(); Cryptpad.reportAppUsage();
}); });

@ -168,13 +168,16 @@ body .CodeMirror-focused .cm-matchhighlight {
} }
.cp div.modal #content, .cp div.modal #content,
.cp div#modal #content { .cp div#modal #content {
transition: margin-left 1s;
font-size: 20vh; font-size: 20vh;
position: relative; position: relative;
height: 100%; height: 100%;
overflow: visible; overflow: visible;
white-space: nowrap; white-space: nowrap;
} }
.cp div.modal #content.transition,
.cp div#modal #content.transition {
transition: margin-left 1s;
}
.cp div.modal #content .slide-frame, .cp div.modal #content .slide-frame,
.cp div#modal #content .slide-frame { .cp div#modal #content .slide-frame {
display: inline-block; display: inline-block;
@ -198,10 +201,11 @@ body .CodeMirror-focused .cm-matchhighlight {
} }
.cp div.modal #content .slide-container, .cp div.modal #content .slide-container,
.cp div#modal #content .slide-container { .cp div#modal #content .slide-container {
display: inline-block; display: inline-flex;
height: 100%; height: 100%;
width: 100vw; width: 100vw;
text-align: center; text-align: center;
vertical-align: top;
} }
.cp div.modal .center, .cp div.modal .center,
.cp div#modal .center { .cp div#modal .center {

@ -9,7 +9,7 @@ define([
var checkedTaskItemPtn = /^\s*\[x\]\s*/; var checkedTaskItemPtn = /^\s*\[x\]\s*/;
var uncheckedTaskItemPtn = /^\s*\[ \]\s*/; var uncheckedTaskItemPtn = /^\s*\[ \]\s*/;
renderer.listitem = function (text, level) { renderer.listitem = function (text) {
var isCheckedTaskItem = checkedTaskItemPtn.test(text); var isCheckedTaskItem = checkedTaskItemPtn.test(text);
var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text); var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text);
if (isCheckedTaskItem) { if (isCheckedTaskItem) {
@ -28,8 +28,6 @@ define([
renderer: renderer renderer: renderer
}); });
var truthy = function (x) { return x; };
var Slide = { var Slide = {
index: 0, index: 0,
lastIndex: 0, lastIndex: 0,
@ -59,8 +57,7 @@ define([
var change = function (oldIndex, newIndex) { var change = function (oldIndex, newIndex) {
if (Slide.changeHandlers.length) { if (Slide.changeHandlers.length) {
Slide.changeHandlers.some(function (f, i) { Slide.changeHandlers.some(function (f) {
// HERE
f(oldIndex, newIndex, getNumberOfSlides()); f(oldIndex, newIndex, getNumberOfSlides());
}); });
} }
@ -187,6 +184,10 @@ define([
$('<div>', {'class': 'slideTitle'}).text(APP.title).appendTo($(el)); $('<div>', {'class': 'slideTitle'}).text(APP.title).appendTo($(el));
} }
}); });
$content.removeClass('transition');
if (options.transition) {
$content.addClass('transition');
}
//$content.find('.' + slideClass).hide(); //$content.find('.' + slideClass).hide();
//$content.find('.' + slideClass + ':eq( ' + i + ' )').show(); //$content.find('.' + slideClass + ':eq( ' + i + ' )').show();
$content.css('margin-left', -(i*100)+'vw'); $content.css('margin-left', -(i*100)+'vw');
@ -194,7 +195,7 @@ define([
change(Slide.lastIndex, Slide.index); change(Slide.lastIndex, Slide.index);
}; };
var updateOptions = Slide.updateOptions = function () { Slide.updateOptions = function () {
draw(Slide.index); draw(Slide.index);
}; };
@ -214,7 +215,10 @@ define([
$(ifrw).focus(); $(ifrw).focus();
change(null, Slide.index); change(null, Slide.index);
if (!isPresentURL()) { if (!isPresentURL()) {
window.location.hash += '/present'; if (window.location.href.slice(-1) !== '/') {
window.location.hash += '/';
}
window.location.hash += 'present';
} }
$pad.contents().find('.cryptpad-present-button').hide(); $pad.contents().find('.cryptpad-present-button').hide();
$pad.contents().find('.cryptpad-source-button').show(); $pad.contents().find('.cryptpad-source-button').show();
@ -223,7 +227,7 @@ define([
$('.top-bar').hide(); $('.top-bar').hide();
return; return;
} }
window.location.hash = window.location.hash.replace(/\/present$/, ''); window.location.hash = window.location.hash.replace(/\/present$/, '/');
change(Slide.index, null); change(Slide.index, null);
$pad.contents().find('.cryptpad-present-button').show(); $pad.contents().find('.cryptpad-present-button').show();
$pad.contents().find('.cryptpad-source-button').hide(); $pad.contents().find('.cryptpad-source-button').hide();
@ -233,7 +237,7 @@ define([
$modal.removeClass('shown'); $modal.removeClass('shown');
}; };
var update = Slide.update = function (content, init) { Slide.update = function (content, init) {
if (!Slide.shown && !init) { return; } if (!Slide.shown && !init) { return; }
if (!content) { content = ''; } if (!content) { content = ''; }
var old = Slide.content; var old = Slide.content;
@ -245,7 +249,7 @@ define([
change(Slide.lastIndex, Slide.index); change(Slide.lastIndex, Slide.index);
}; };
var left = Slide.left = function () { Slide.left = function () {
console.log('left'); console.log('left');
Slide.lastIndex = Slide.index; Slide.lastIndex = Slide.index;
@ -253,7 +257,7 @@ define([
Slide.draw(i); Slide.draw(i);
}; };
var right = Slide.right = function () { Slide.right = function () {
console.log('right'); console.log('right');
Slide.lastIndex = Slide.index; Slide.lastIndex = Slide.index;
@ -261,7 +265,7 @@ define([
Slide.draw(i); Slide.draw(i);
}; };
var first = Slide.first = function () { Slide.first = function () {
console.log('first'); console.log('first');
Slide.lastIndex = Slide.index; Slide.lastIndex = Slide.index;
@ -269,7 +273,7 @@ define([
Slide.draw(i); Slide.draw(i);
}; };
var last = Slide.last = function () { Slide.last = function () {
console.log('end'); console.log('end');
Slide.lastIndex = Slide.index; Slide.lastIndex = Slide.index;
@ -279,7 +283,7 @@ define([
var addEvent = function () { var addEvent = function () {
var icon_to; var icon_to;
$modal.mousemove(function (e) { $modal.mousemove(function () {
var $buttons = $modal.find('.button'); var $buttons = $modal.find('.button');
$buttons.show(); $buttons.show();
if (icon_to) { window.clearTimeout(icon_to); } if (icon_to) { window.clearTimeout(icon_to); }
@ -287,17 +291,17 @@ define([
$buttons.fadeOut(); $buttons.fadeOut();
}, 1000); }, 1000);
}); });
$modal.find('#button_exit').click(function (e) { $modal.find('#button_exit').click(function () {
var ev = $.Event("keyup"); var ev = $.Event("keyup");
ev.which = 27; ev.which = 27;
$modal.trigger(ev); $modal.trigger(ev);
}); });
$modal.find('#button_left').click(function (e) { $modal.find('#button_left').click(function () {
var ev = $.Event("keyup"); var ev = $.Event("keyup");
ev.which = 37; ev.which = 37;
$modal.trigger(ev); $modal.trigger(ev);
}); });
$modal.find('#button_right').click(function (e) { $modal.find('#button_right').click(function () {
var ev = $.Event("keyup"); var ev = $.Event("keyup");
ev.which = 39; ev.which = 39;
$modal.trigger(ev); $modal.trigger(ev);

@ -161,7 +161,9 @@ div.modal, div#modal {
width: 100%; width: 100%;
} }
#content { #content {
transition: margin-left 1s; &.transition {
transition: margin-left 1s;
}
font-size: 20vh; font-size: 20vh;
position: relative; position: relative;
height: 100%; height: 100%;
@ -191,9 +193,10 @@ div.modal, div#modal {
margin: auto; margin: auto;
} }
.slide-container { .slide-container {
display: inline-block; display: inline-flex;
height: 100%; width: 100vw; height: 100%; width: 100vw;
text-align: center; text-align: center;
vertical-align: top;
} }
} }

@ -3,19 +3,17 @@ define([
'/api/config', '/api/config',
'/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar.js', '/common/toolbar2.js',
'/bower_components/textpatcher/TextPatcher.amd.js', '/bower_components/textpatcher/TextPatcher.amd.js',
'json.sortify', 'json.sortify',
'/bower_components/chainpad-json-validator/json-ot.js', '/bower_components/chainpad-json-validator/json-ot.js',
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/cryptget.js', '/common/cryptget.js',
'/whiteboard/colors.js', '/whiteboard/colors.js',
'/common/visible.js',
'/common/notify.js',
'/customize/application_config.js', '/customize/application_config.js',
'/bower_components/secure-fabric.js/dist/fabric.min.js', '/bower_components/secure-fabric.js/dist/fabric.min.js',
'/bower_components/file-saver/FileSaver.min.js', '/bower_components/file-saver/FileSaver.min.js',
], function ($, Config, Realtime, Crypto, Toolbar, TextPatcher, JSONSortify, JsonOT, Cryptpad, Cryptget, Colors, Visible, Notify, AppConfig) { ], function ($, Config, Realtime, Crypto, Toolbar, TextPatcher, JSONSortify, JsonOT, Cryptpad, Cryptget, Colors, AppConfig) {
var saveAs = window.saveAs; var saveAs = window.saveAs;
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
@ -24,7 +22,7 @@ define([
$(function () { $(function () {
Cryptpad.addLoadingScreen(); Cryptpad.addLoadingScreen();
var onConnectError = function (info) { var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError); Cryptpad.errorLoadingScreen(Messages.websocketError);
}; };
var toolbar; var toolbar;
@ -212,35 +210,10 @@ window.canvas = canvas;
var initializing = true; var initializing = true;
var $bar = $('#toolbar'); var $bar = $('#toolbar');
var parsedHash = Cryptpad.parsePadUrl(window.location.href);
var defaultName = Cryptpad.getDefaultName(parsedHash);
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) { var Title;
myID = info.myID || null; var UserList;
myUserName = myID; var Metadata;
};
var config = module.config = { var config = module.config = {
initialState: '{}', initialState: '{}',
@ -249,7 +222,6 @@ window.canvas = canvas;
readOnly: readOnly, readOnly: readOnly,
channel: secret.channel, channel: secret.channel,
crypto: Crypto.createEncryptor(secret.keys), crypto: Crypto.createEncryptor(secret.keys),
setMyID: setMyID,
transformFunction: JsonOT.transform, transformFunction: JsonOT.transform,
}; };
@ -280,28 +252,14 @@ window.canvas = canvas;
$colors.append($color); $colors.append($color);
}; };
var updatePalette = function (newPalette) { var metadataCfg = {};
var updatePalette = metadataCfg.updatePalette = function (newPalette) {
palette = newPalette; palette = newPalette;
$colors.html('<div class="hidden">&nbsp;</div>'); $colors.html('<div class="hidden">&nbsp;</div>');
palette.forEach(addColorToPalette); palette.forEach(addColorToPalette);
}; };
updatePalette(palette); updatePalette(palette);
var suggestName = function (fallback) {
if (document.title === defaultName) {
return fallback || "";
} else {
return document.title || defaultName;
}
};
var renameCb = function (err, title) {
if (err) { return; }
document.title = title;
config.onLocal();
};
var makeColorButton = function ($container) { var makeColorButton = function ($container) {
var $testColor = $('<input>', { type: 'color', value: '!' }); var $testColor = $('<input>', { type: 'color', value: '!' });
@ -330,31 +288,34 @@ window.canvas = canvas;
return $color; return $color;
}; };
var editHash; config.onInit = function (info) {
var onInit = config.onInit = function (info) { UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad);
userList = info.userList;
var config = { Title = Cryptpad.createTitle({}, config.onLocal, Cryptpad);
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'],
userData: userData, Metadata = Cryptpad.createMetadata(UserList, Title, metadataCfg);
readOnly: readOnly,
var configTb = {
displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'],
userList: UserList.getToolbarConfig(),
share: { share: {
secret: secret, secret: secret,
channel: info.channel channel: info.channel
}, },
title: Title.getTitleConfig(),
common: Cryptpad,
readOnly: readOnly,
ifrw: window, ifrw: window,
title: { realtime: info.realtime,
onRename: renameCb, network: info.network,
defaultName: defaultName, $container: $bar
suggestName: suggestName
},
common: Cryptpad
}; };
if (readOnly) {delete config.changeNameID; }
toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, userList, config); toolbar = module.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
var $rightside = $bar.find('.' + Toolbar.constants.rightside); var $rightside = toolbar.$rightside;
module.$userNameButton = $($bar.find('.' + Toolbar.constants.changeUsername));
/* save as template */ /* save as template */
if (!Cryptpad.isTemplate(window.location.href)) { if (!Cryptpad.isTemplate(window.location.href)) {
@ -370,7 +331,7 @@ window.canvas = canvas;
var $export = Cryptpad.createButton('export', true, {}, saveImage); var $export = Cryptpad.createButton('export', true, {}, saveImage);
$rightside.append($export); $rightside.append($export);
var $forget = Cryptpad.createButton('forget', true, {}, function (err, title) { var $forget = Cryptpad.createButton('forget', true, {}, function (err) {
if (err) { return; } if (err) { return; }
setEditable(false); setEditable(false);
toolbar.failed(); toolbar.failed();
@ -380,14 +341,11 @@ window.canvas = canvas;
makeColorButton($rightside); makeColorButton($rightside);
var editHash; var editHash;
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
if (!readOnly) { if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
} }
if (!readOnly) { Cryptpad.replaceHash(editHash); } if (!readOnly) { Cryptpad.replaceHash(editHash); }
Cryptpad.onDisplayNameChanged(module.setName);
}; };
// used for debugging, feel free to remove // used for debugging, feel free to remove
@ -401,75 +359,11 @@ window.canvas = canvas;
}; };
}; };
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);
};
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 (typeof(json.metadata.palette) !== 'undefined') {
updatePalette(json.metadata.palette);
}
}
if (!titleUpdated) {
updateTitle(defaultName);
}
};
var unnotify = function () {
if (module.tabNotification &&
typeof(module.tabNotification.cancel) === 'function') {
module.tabNotification.cancel();
}
};
var notify = function () {
if (Visible.isSupported() && !Visible.currently()) {
unnotify();
module.tabNotification = Notify.tab(1000, 10);
}
};
var onRemote = config.onRemote = Catch(function () { var onRemote = config.onRemote = Catch(function () {
if (initializing) { return; } if (initializing) { return; }
var userDoc = module.realtime.getUserDoc(); var userDoc = module.realtime.getUserDoc();
updateMetadata(userDoc); Metadata.update(userDoc);
var json = JSON.parse(userDoc); var json = JSON.parse(userDoc);
var remoteDoc = json.content; var remoteDoc = json.content;
@ -479,7 +373,7 @@ window.canvas = canvas;
canvas.renderAll(); canvas.renderAll();
var content = canvas.toDatalessJSON(); var content = canvas.toDatalessJSON();
if (content !== remoteDoc) { notify(); } if (content !== remoteDoc) { Cryptpad.notify(); }
if (readOnly) { setEditable(false); } if (readOnly) { setEditable(false); }
}); });
setEditable(false); setEditable(false);
@ -488,13 +382,13 @@ window.canvas = canvas;
var obj = { var obj = {
content: textValue, content: textValue,
metadata: { metadata: {
users: userData, users: UserList.userData,
palette: palette, palette: palette,
defaultTitle: defaultName defaultTitle: Title.defaultTitle
} }
}; };
if (!initializing) { if (!initializing) {
obj.metadata.title = document.title; obj.metadata.title = Title.title;
} }
// stringify the json and send it into chainpad // stringify the json and send it into chainpad
return JSONSortify(obj); return JSONSortify(obj);
@ -510,29 +404,7 @@ window.canvas = canvas;
module.patchText(content); module.patchText(content);
}); });
var setName = module.setName = function (newName) { config.onReady = function (info) {
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 onReady = config.onReady = function (info) {
var realtime = module.realtime = info.realtime; var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
realtime: realtime realtime: realtime
@ -547,45 +419,20 @@ window.canvas = canvas;
initializing = false; initializing = false;
onRemote(); onRemote();
if (Visible.isSupported()) {
Visible.onChange(function (yes) { if (yes) { unnotify(); } });
}
/* TODO: restore palette from metadata.palette */ /* TODO: restore palette from metadata.palette */
Cryptpad.getLastName(function (err, lastName) {
if (err) { if (readOnly) { return; }
console.log("Could not get previous name"); UserList.getLastName(toolbar.$userNameButton, isNew);
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('whiteboard', info.realtime, Cryptget);
}
});
}; };
var onAbort = config.onAbort = function (info) { config.onAbort = function () {
setEditable(false); setEditable(false);
toolbar.failed(); toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true); Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}; };
// TODO onConnectionStateChange // TODO onConnectionStateChange
var onConnectionChange = config.onConnectionChange = function (info) { config.onConnectionChange = function (info) {
setEditable(info.state); setEditable(info.state);
toolbar.failed(); toolbar.failed();
if (info.state) { if (info.state) {
@ -597,7 +444,7 @@ window.canvas = canvas;
} }
}; };
var rt = Realtime.start(config); module.rt = Realtime.start(config);
canvas.on('mouse:up', onLocal); canvas.on('mouse:up', onLocal);
@ -611,7 +458,7 @@ window.canvas = canvas;
}); });
}; };
Cryptpad.ready(function (err, env) { Cryptpad.ready(function () {
andThen(); andThen();
Cryptpad.reportAppUsage(); Cryptpad.reportAppUsage();
}); });

Loading…
Cancel
Save