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
npm-debug.log
pins/
blob/
privileged.conf

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

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

@ -39,10 +39,10 @@ module.exports = {
if you are deploying to production, you'll probably want to remove
the ws://* directive, and change '*' to your domain
*/
"connect-src 'self' ws://* wss://*",
"connect-src 'self' ws: wss:",
// data: is used by codemirror
"img-src 'self' data:",
"img-src 'self' data: blob:",
].join('; '),
// CKEditor requires significantly more lax content security policy in order to function.
@ -59,7 +59,7 @@ module.exports = {
"child-src 'self' *",
// see the comment above in the 'contentSecurity' section
"connect-src 'self' ws://* wss://*",
"connect-src 'self' ws: wss:",
// (insecure remote) images are included by users of the wysiwyg who embed photos in their pads
"img-src *",
@ -141,6 +141,23 @@ module.exports = {
*/
filePath: './datastore/',
/* CryptPad allows logged in users to request that particular documents be
* stored by the server indefinitely. This is called 'pinning'.
* Pin requests are stored in a pin-store. The location of this store is
* defined here.
*/
pinPath: './pins',
/* CryptPad allows logged in users to upload encrypted files. Files/blobs
* are stored in a 'blob-store'. Set its location here.
*/
blobPath: './blob',
/* CryptPad stores incomplete blobs in a 'staging' area until they are
* fully uploaded. Set its location here.
*/
blobStagingPath: './blobstage',
/* Cryptpad's file storage adaptor closes unused files after a configurale
* number of milliseconds (default 30000 (30 seconds))
*/
@ -163,6 +180,31 @@ module.exports = {
*/
suppressRPCErrors: false,
/* WARNING: EXPERIMENTAL
*
* CryptPad features experimental support for encrypted file upload.
* Our encryption format is still liable to change. As such, we do not
* guarantee that files uploaded now will be supported in the future
*/
/* Setting this value to anything other than true will cause file upload
* attempts to be rejected outright.
*/
enableUploads: true,
/* If you have enabled file upload, you have the option of restricting it
* to a list of users identified by their public keys. If this value is set
* to true, your server will query a file (cryptpad/privileged.conf) when
* users connect via RPC. Only users whose public keys can be found within
* the file will be allowed to upload.
*
* privileged.conf uses '#' for line comments, and splits keys by newline.
* This is a temporary measure until a better quota system is in place.
* registered users' public keys can be found on the settings page.
*/
restrictUploads: true,
/* it is recommended that you serve cryptpad over https
* the filepaths below are used to configure your certificates
*/

@ -106,7 +106,7 @@
<div class="col">
<ul class="list-unstyled">
<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://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -114,7 +114,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer>
</body>

@ -37,5 +37,20 @@ define(function() {
config.enableHistory = true;
//config.enablePinLimit = true;
//config.pinLimit = 1000;
/* user passwords are hashed with scrypt, and salted with their username.
this value will be appended to the username, causing the resulting hash
to differ from other CryptPad instances if customized. This makes it
such that anyone who wants to bruteforce common credentials must do so
again on each CryptPad instance that they wish to attack.
WARNING: this should only be set when your CryptPad instance is first
created. Changing it at a later time will break logins for all existing
users.
*/
config.loginSalt = '';
return config;
});

@ -103,7 +103,7 @@
<div class="col">
<ul class="list-unstyled">
<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://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -111,7 +111,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer>
</body>

@ -225,7 +225,7 @@
<div class="col">
<ul class="list-unstyled">
<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://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -233,7 +233,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer>
</body>

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

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

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

@ -124,7 +124,7 @@
<div class="col">
<ul class="list-unstyled">
<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://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -132,7 +132,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer>
</body>

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

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

@ -31,7 +31,7 @@
<div class="col">
<ul class="list-unstyled">
<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://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -39,5 +39,5 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer>

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

@ -42,12 +42,13 @@
}
button {
&#shareButton {
&#shareButton, &.buttonSuccess {
// Bootstrap 4 colors
color: #fff;
background: @toolbar-green;
border-color: @toolbar-green;
&:hover {
color: #fff;
background: #449d44;
border: 1px solid #419641;
}
@ -58,12 +59,13 @@
margin-left: 5px;
}
}
&#newdoc {
&#newdoc, &.buttonPrimary {
// Bootstrap 4 colors
color: #fff;
background: #0275d8;
border-color: #0275d8;
&:hover {
color: #fff;
background: #025aa5;
border: 1px solid #01549b;
}
@ -77,26 +79,84 @@
&.hidden {
display: none;
}
// Bootstrap 4 colors (btn-secondary)
border: 1px solid transparent;
border-radius: .25rem;
color: #292b2c;
background-color: #fff;
border-color: #ccc;
&:hover {
color: #292b2c;
background-color: #e6e6e6;
border-color: #adadad;
}
}
.cryptpad-lag {
box-sizing: content-box;
height: 16px;
width: 16px;
button.upgrade {
font-size: 14px;
vertical-align: top;
margin-left: 10px;
}
.cryptpad-drive-limit {
display: inline-block;
padding: 5px;
margin: 3px 0;
div {
height: 26px;
width: 200px;
margin: 2px;
box-sizing: border-box;
border: 1px solid #999;
background: white;
position: relative;
text-align: center;
line-height: 24px;
.usage {
height: 24px;
display: inline-block;
background: blue;
position: absolute;
left: 0;
z-index:1;
&.normal {
background: @toolbar-green;
}
&.warning {
background: orange;
}
&.above {
background: red;
}
}
.usageText {
position: relative;
color: black;
text-shadow: 1px 0 2px white, 0 1px 2px white, -1px 0 2px white, 0 -1px 2px white;
z-index: 2;
font-size: 16px;
font-weight: bold;
}
}
.cryptpad-limit {
box-sizing: border-box;
height: 26px;
width: 26px;
display: inline-block;
padding: 3px;
margin: 0px;
margin-right: 3px;
vertical-align: middle;
span {
color: red;
cursor: pointer;
margin: auto;
font-size: 20px;
}
}
.clag () {
background: transparent;
}
.clag () {
background: transparent;
}
#newLag {
.cryptpad-lag {
height: 20px;
width: 23px;
background: transparent;
@ -179,17 +239,6 @@
margin-right: 2px;
}
button {
color: #000;
background-color: inherit;
background-image: linear-gradient(to bottom,#fff,#e4e4e4);
border: 1px solid #A6A6A6;
border-bottom-color: #979797;
border-radius: 3px;
&:hover {
background-image:linear-gradient(to bottom,#f2f2f2,#ccc);
}
}
.cryptpad-state {
line-height: 32px; /* equivalent to 26px + 2*2px margin used for buttons */
}
@ -378,9 +427,8 @@
.cryptpad-user {
position: absolute;
right: 0;
span:not(.cryptpad-lag) {
:not(.cryptpad-lag) span {
vertical-align: top;
//display: inline-block;
}
button {
span.fa {
@ -392,11 +440,9 @@
.cryptpad-toolbar-leftside {
float: left;
margin-bottom: -1px;
.cryptpad-user-list {
//float: right;
.cryptpad-dropdown-users {
pre {
white-space: pre;
margin: 0;
margin: 5px 0px;
}
}
button {
@ -413,14 +459,20 @@
display: none;
text-align: center;
.next {
float: right;
display: inline-block;
vertical-align: middle;
margin: 20px;
}
.previous {
float: left;
display: inline-block;
vertical-align: middle;
margin: 20px;
}
.goto {
display: inline-block;
input { width: 50px; }
vertical-align: middle;
text-align: center;
input { width: 75px; }
}
.gotoInput {
vertical-align: middle;
@ -434,7 +486,7 @@
border-radius: 5px;
}
}
.cryptpad-spinner {
.cryptpad-spinner > span {
height: 16px;
width: 16px;
margin: 8px;

@ -107,7 +107,7 @@
<div class="col">
<ul class="list-unstyled">
<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://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -115,7 +115,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer>
</body>

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

@ -11,6 +11,8 @@ define(function () {
out.type.slide = 'Présentation';
out.type.drive = 'Drive';
out.type.whiteboard = "Tableau Blanc";
out.type.file = "Fichier";
out.type.media = "Média";
out.button_newpad = 'Nouveau document texte';
out.button_newcode = 'Nouvelle page de code';
@ -49,10 +51,22 @@ define(function () {
out.language = "Langue";
out.upgrade = "Améliorer";
out.upgradeTitle = "Améliorer votre compte pour augmenter la limite de stockage";
out.MB = "Mo";
out.greenLight = "Tout fonctionne bien";
out.orangeLight = "Votre connexion est lente, ce qui réduit la qualité de l'éditeur";
out.redLight = "Vous êtes déconnectés de la session";
out.pinLimitReached = "Vous avez atteint votre limite de stockage";
out.pinLimitReachedAlert = "Vous avez atteint votre limite de stockage. Les nouveaux pads ne seront pas enregistrés dans votre CrypDrive.<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.exportButtonTitle = 'Exporter ce pad vers un fichier local';
@ -93,6 +107,7 @@ define(function () {
out.printDate = "Afficher la date";
out.printTitle = "Afficher le titre du pad";
out.printCSS = "Personnaliser l'apparence (CSS):";
out.printTransition = "Activer les animations de transition";
out.slideOptionsTitle = "Personnaliser la présentation";
out.slideOptionsButton = "Enregistrer (Entrée)";
@ -125,6 +140,7 @@ define(function () {
out.history_restoreTitle = "Restaurer la version du document sélectionnée";
out.history_restorePrompt = "Êtes-vous sûr de vouloir remplacer la version actuelle du document par la version affichée ?";
out.history_restoreDone = "Document restauré";
out.history_version = "Version :";
// Polls
@ -313,6 +329,10 @@ define(function () {
out.settings_pinningError = "Un problème est survenu";
out.settings_usageAmount = "Vos pads épinglés occupent {0} Mo";
out.settings_logoutEverywhereTitle = "Se déconnecter partout";
out.settings_logoutEverywhere = "Se déconnecter de toutes les autres sessions.";
out.settings_logoutEverywhereConfirm = "Êtes-vous sûr ? Vous devrez vous reconnecter sur tous vos autres appareils.";
// index.html
//about.html

@ -11,6 +11,8 @@ define(function () {
out.type.slide = 'Presentation';
out.type.drive = 'Drive';
out.type.whiteboard = 'Whiteboard';
out.type.file = 'File';
out.type.media = 'Media';
out.button_newpad = 'New Rich Text pad';
out.button_newcode = 'New Code pad';
@ -51,10 +53,22 @@ define(function () {
out.language = "Language";
out.upgrade = "Upgrade";
out.upgradeTitle = "Upgrade your account to increase the storage limit";
out.MB = "MB";
out.greenLight = "Everything is working fine";
out.orangeLight = "Your slow connection may impact your experience";
out.redLight = "You are disconnected from the session";
out.pinLimitReached = "You've reached your storage limit";
out.pinLimitReachedAlert = "You've reached your storage limit. New pads won't be stored in your CryptDrive.<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.exportButtonTitle = 'Export this pad to a local file';
@ -95,6 +109,7 @@ define(function () {
out.printDate = "Display the date";
out.printTitle = "Display the pad title";
out.printCSS = "Custom style rules (CSS):";
out.printTransition = "Enable transition animations";
out.slideOptionsTitle = "Customize your slides";
out.slideOptionsButton = "Save (enter)";
@ -127,6 +142,7 @@ define(function () {
out.history_restoreTitle = "Restore the selected version of the document";
out.history_restorePrompt = "Are you sure you want to replace the current version of the document by the displayed one?";
out.history_restoreDone = "Document restored";
out.history_version = "Version:";
// Polls
@ -318,6 +334,10 @@ define(function () {
out.settings_pinningError = "Something went wrong";
out.settings_usageAmount = "Your pinned pads occupy {0}MB";
out.settings_logoutEverywhereTitle = "Log out everywhere";
out.settings_logoutEverywhere = "Log out of all other web sessions";
out.settings_logoutEverywhereConfirm = "Are you sure? You will need to log in with all your devices.";
// index.html

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

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

@ -54,6 +54,9 @@ These settings can be found in your configuration file in the `contentSecurity`
## Maintenance
Before upgrading your CryptPad instance to the latest version, we recommend that you check what has changed since your last update.
You can do so by checking which version you have (see package.json), and comparing it against newer [release notes](https://github.com/xwiki-labs/cryptpad/releases).
To get access to the most recent codebase:
```
@ -70,9 +73,12 @@ bower update;
# serverside dependencies
npm update;
```
## Deleting all data and resetting Cryptpad
To reset your instance of Cryptpad and remove all the data that is being stored:
**WARNING: This will reset your Cryptpad instance and remove all data**
```
# change into your cryptpad directory
cd /your/cryptpad/instance/location;

339
rpc.js

@ -2,12 +2,44 @@
/* Use Nacl for checking signatures of messages */
var Nacl = require("tweetnacl");
/* globals Buffer*/
/* globals process */
var Fs = require("fs");
var Path = require("path");
var RPC = module.exports;
var Store = require("./storage/file");
var isValidChannel = function (chan) {
return /^[a-fA-F0-9]/.test(chan);
return /^[a-fA-F0-9]/.test(chan) ||
[32, 48].indexOf(chan.length) !== -1;
};
var uint8ArrayToHex = function (a) {
// call slice so Uint8Arrays work as expected
return Array.prototype.slice.call(a).map(function (e) {
var n = Number(e & 0xff).toString(16);
if (n === 'NaN') {
throw new Error('invalid input resulted in NaN');
}
switch (n.length) {
case 0: return '00'; // just being careful, shouldn't happen
case 1: return '0' + n;
case 2: return n;
default: throw new Error('unexpected value');
}
}).join('');
};
var createFileId = function () {
var id = uint8ArrayToHex(Nacl.randomBytes(24));
if (id.length !== 48 || /[^a-f0-9]/.test(id)) {
throw new Error('file ids must consist of 48 hex characters');
}
return id;
};
var makeToken = function () {
@ -21,7 +53,7 @@ var makeCookie = function (token) {
return [
time,
process.pid, // jshint ignore:line
process.pid,
token
];
};
@ -59,7 +91,11 @@ var isTooOld = function (time, now) {
var expireSessions = function (Sessions) {
var now = +new Date();
Object.keys(Sessions).forEach(function (key) {
var session = Sessions[key];
if (isTooOld(Sessions[key].atime, now)) {
if (session.blobstage) {
session.blobstage.close();
}
delete Sessions[key];
}
});
@ -86,7 +122,7 @@ var isValidCookie = function (Sessions, publicKey, cookie) {
}
// different process. try harder
if (process.pid !== parsed.pid) { // jshint ignore:line
if (process.pid !== parsed.pid) {
return false;
}
@ -96,7 +132,6 @@ var isValidCookie = function (Sessions, publicKey, cookie) {
var idx = user.tokens.indexOf(parsed.seq);
if (idx === -1) { return false; }
var next;
if (idx > 0) {
// make a new token
addTokenForKey(Sessions, publicKey, makeToken());
@ -211,14 +246,32 @@ var getChannelList = function (store, Sessions, publicKey, cb) {
});
};
var getUploadSize = function (store, channel, cb) {
var path = '';
Fs.stat(path, function (err, stats) {
if (err) { return void cb(err); }
cb(void 0, stats.size);
});
};
var getFileSize = function (store, channel, cb) {
if (!isValidChannel(channel)) { return void cb('INVALID_CHAN'); }
if (typeof(store.getChannelSize) !== 'function') {
return cb('GET_CHANNEL_SIZE_UNSUPPORTED');
if (channel.length === 32) {
if (typeof(store.getChannelSize) !== 'function') {
return cb('GET_CHANNEL_SIZE_UNSUPPORTED');
}
return void store.getChannelSize(channel, function (e, size) {
if (e) { return void cb(e.code); }
cb(void 0, size);
});
}
return void store.getChannelSize(channel, function (e, size) {
if (e) { return void cb(e.code); }
// 'channel' refers to a file, so you need anoter API
getUploadSize(null, channel, function (e, size) {
if (e) { return void cb(e); }
cb(void 0, size);
});
};
@ -294,10 +347,11 @@ var getHash = function (store, Sessions, publicKey, cb) {
});
};
var storeMessage = function (store, publicKey, msg, cb) {
/* var storeMessage = function (store, publicKey, msg, cb) {
store.message(publicKey, JSON.stringify(msg), cb);
};
}; */
// TODO check if new pinned size exceeds user quota
var pinChannel = function (store, Sessions, publicKey, channels, cb) {
if (!channels && channels.filter) {
// expected array
@ -349,8 +403,7 @@ var unpinChannel = function (store, Sessions, publicKey, channels, cb) {
function (e) {
if (e) { return void cb(e); }
toStore.forEach(function (channel) {
// TODO actually delete
session.channels[channel] = false;
delete session.channels[channel];
});
getHash(store, Sessions, publicKey, cb);
@ -358,6 +411,7 @@ var unpinChannel = function (store, Sessions, publicKey, channels, cb) {
});
};
// TODO check if new pinned size exceeds user quota
var resetUserPins = function (store, Sessions, publicKey, channelList, cb) {
var session = beginSession(Sessions, publicKey);
@ -376,14 +430,191 @@ var resetUserPins = function (store, Sessions, publicKey, channelList, cb) {
});
};
var getPrivilegedUserList = function (cb) {
Fs.readFile('./privileged.conf', 'utf8', function (e, body) {
if (e) {
if (e.code === 'ENOENT') {
return void cb(void 0, []);
}
return void (e.code);
}
var list = body.split(/\n/)
.map(function (line) {
return line.replace(/#.*$/, '').trim();
})
.filter(function (x) { return x; });
cb(void 0, list);
});
};
var isPrivilegedUser = function (publicKey, cb) {
getPrivilegedUserList(function (e, list) {
if (e) { return void cb(false); }
cb(list.indexOf(publicKey) !== -1);
});
};
var getLimit = function (cb) {
cb = cb; // TODO
};
var safeMkdir = function (path, cb) {
Fs.mkdir(path, function (e) {
if (!e || e.code === 'EEXIST') { return void cb(); }
cb(e);
});
};
var makeFilePath = function (root, id) {
if (typeof(id) !== 'string' || id.length <= 2) { return null; }
return Path.join(root, id.slice(0, 2), id);
};
var makeFileStream = function (root, id, cb) {
var stub = id.slice(0, 2);
var full = makeFilePath(root, id);
safeMkdir(Path.join(root, stub), function (e) {
if (e) { return void cb(e); }
try {
var stream = Fs.createWriteStream(full, {
flags: 'a',
encoding: 'binary',
});
stream.on('open', function () {
cb(void 0, stream);
});
} catch (err) {
cb('BAD_STREAM');
}
});
};
var upload = function (paths, Sessions, publicKey, content, cb) {
var dec = new Buffer(Nacl.util.decodeBase64(content)); // jshint ignore:line
var session = Sessions[publicKey];
session.atime = +new Date();
if (!session.blobstage) {
makeFileStream(paths.staging, publicKey, function (e, stream) {
if (e) { return void cb(e); }
var blobstage = session.blobstage = stream;
blobstage.write(dec);
cb(void 0, dec.length);
});
} else {
session.blobstage.write(dec);
cb(void 0, dec.length);
}
};
var upload_cancel = function (paths, Sessions, publicKey, cb) {
var path = makeFilePath(paths.staging, publicKey);
if (!path) {
console.log(paths.staging, publicKey);
console.log(path);
return void cb('NO_FILE');
}
Fs.unlink(path, function (e) {
if (e) { return void cb('E_UNLINK'); }
cb(void 0);
});
};
var isFile = function (filePath, cb) {
Fs.stat(filePath, function (e, stats) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, false); }
return void cb(e.message);
}
return void cb(void 0, stats.isFile());
});
};
/* TODO
change channel IDs to a different length so that when we pin, we will be able
to tell that it is not a channel, but a file, just by its length.
also, when your upload is complete, pin the resulting file.
*/
var upload_complete = function (paths, Sessions, publicKey, cb) {
var session = Sessions[publicKey];
if (session.blobstage && session.blobstage.close) {
session.blobstage.close();
delete session.blobstage;
}
var oldPath = makeFilePath(paths.staging, publicKey);
var tryRandomLocation = function (cb) {
var id = createFileId();
var prefix = id.slice(0, 2);
var newPath = makeFilePath(paths.blob, id);
safeMkdir(Path.join(paths.blob, prefix), function (e) {
if (e) {
console.error(e);
return void cb('RENAME_ERR');
}
isFile(newPath, function (e, yes) {
if (e) {
console.error(e);
return void cb(e);
}
if (yes) {
return void tryRandomLocation(cb);
}
cb(void 0, newPath, id);
});
});
};
tryRandomLocation(function (e, newPath, id) {
Fs.rename(oldPath, newPath, function (e) {
if (e) {
console.error(e);
return cb(e);
}
cb(void 0, id);
});
});
};
/* TODO
when asking about your upload status, also send some information about how big
your upload is going to be. if that would exceed your limit, return TOO_LARGE
error.
*/
var upload_status = function (paths, Sessions, publicKey, cb) {
var filePath = makeFilePath(paths.staging, publicKey);
if (!filePath) { return void cb('E_INVALID_PATH'); }
isFile(filePath, function (e, yes) {
cb(e, yes);
});
};
/*::const ConfigType = require('./config.example.js');*/
RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) {
// load pin-store...
console.log('loading rpc module...');
var Sessions = {};
var keyOrDefaultString = function (key, def) {
return typeof(config[key]) === 'string'? config[key]: def;
};
var paths = {};
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob');
var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
var store;
var rpc = function (
@ -428,7 +659,6 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)
return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY');
}
if (checkSignature(serialized, signature, publicKey) !== true) {
return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY");
}
@ -446,20 +676,26 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)
var Respond = function (e, msg) {
var token = Sessions[publicKey].tokens.slice(-1)[0];
var cookie = makeCookie(token).join('|');
respond(e, [cookie].concat(msg||[]));
respond(e, [cookie].concat(typeof(msg) !== 'undefined' ?msg: []));
};
if (typeof(msg) !== 'object' || !msg.length) {
return void Respond('INVALID_MSG');
}
var deny = function () {
Respond('E_ACCESS_DENIED');
};
var handleMessage = function (privileged) {
switch (msg[0]) {
case 'COOKIE': return void Respond(void 0);
case 'RESET':
return resetUserPins(store, Sessions, safeKey, msg[1], function (e, hash) {
return void Respond(e, hash);
});
case 'PIN':
case 'PIN': // TODO don't pin if over the limit
// if over, send error E_OVER_LIMIT
return pinChannel(store, Sessions, safeKey, msg[1], function (e, hash) {
Respond(e, hash);
});
@ -471,32 +707,89 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)
return void getHash(store, Sessions, safeKey, function (e, hash) {
Respond(e, hash);
});
case 'GET_TOTAL_SIZE':
case 'GET_TOTAL_SIZE': // TODO cache this, since it will get called quite a bit
return getTotalSize(store, ctx.store, Sessions, safeKey, function (e, size) {
if (e) { return void Respond(e); }
Respond(e, size);
});
case 'GET_FILE_SIZE':
return void getFileSize(ctx.store, msg[1], Respond);
case 'GET_LIMIT': // TODO implement this and cache it per-user
return void getLimit(function (e, limit) {
limit = limit;
Respond('NOT_IMPLEMENTED');
});
case 'GET_MULTIPLE_FILE_SIZE':
return void getMultipleFileSize(ctx.store, msg[1], function (e, dict) {
if (e) { return void Respond(e); }
Respond(void 0, dict);
});
// restricted to privileged users...
case 'UPLOAD':
if (!privileged) { return deny(); }
return void upload(paths, Sessions, safeKey, msg[1], function (e, len) {
Respond(e, len);
});
case 'UPLOAD_STATUS':
if (!privileged) { return deny(); }
return void upload_status(paths, Sessions, safeKey, function (e, stat) {
Respond(e, stat);
});
case 'UPLOAD_COMPLETE':
if (!privileged) { return deny(); }
return void upload_complete(paths, Sessions, safeKey, function (e, hash) {
Respond(e, hash);
});
case 'UPLOAD_CANCEL':
if (!privileged) { return deny(); }
return void upload_cancel(paths, Sessions, safeKey, function (e) {
Respond(e);
});
default:
return void Respond('UNSUPPORTED_RPC_CALL', msg);
}
};
// reject uploads unless explicitly enabled
if (config.enableUploads !== true) {
return void handleMessage(false);
}
// restrict upload capability unless explicitly disabled
if (config.restrictUploads === false) {
return void handleMessage(true);
}
// if session has not been authenticated, do so
var session = Sessions[publicKey];
if (typeof(session.privilege) !== 'boolean') {
return void isPrivilegedUser(publicKey, function (yes) {
session.privilege = yes;
handleMessage(yes);
});
}
// if authenticated, proceed
handleMessage(session.privilege);
};
Store.create({
filePath: './pins'
filePath: pinPath,
}, function (s) {
store = s;
cb(void 0, rpc);
// expire old sessions once per minute
setInterval(function () {
expireSessions(Sessions);
}, 60000);
safeMkdir(blobPath, function (e) {
if (e) { throw e; }
safeMkdir(blobStagingPath, function (e) {
if (e) { throw e; }
cb(void 0, rpc);
// expire old sessions once per minute
setInterval(function () {
expireSessions(Sessions);
}, 60000);
});
});
});
};

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

@ -28,7 +28,8 @@ var readMessages = function (path, msgHandler, cb) {
};
var checkPath = function (path, callback) {
Fs.stat(path, function (err, stats) {
// TODO check if we actually need to use stat at all
Fs.stat(path, function (err) {
if (!err) {
callback(undefined, true);
return;
@ -166,7 +167,7 @@ var getChannel = function (env, id, callback) {
});
}
});
}).nThen(function (waitFor) {
}).nThen(function () {
if (errorState) { return; }
complete();
});

@ -15,31 +15,43 @@ define([
var failMessages = [];
var ASSERTS = [];
var runASSERTS = function () {
var runASSERTS = function (cb) {
var count = ASSERTS.length;
var successes = 0;
var done = function (err) {
count--;
if (err) { failMessages.push(err); }
else { successes++; }
if (count === 0) { cb(); }
};
ASSERTS.forEach(function (f, index) {
f(index);
f(function (err) {
done(err, index);
}, index);
});
};
var assert = function (test, msg) {
ASSERTS.push(function (i) {
var returned = test();
if (returned === true) {
assertions++;
} else {
failed = true;
failedOn = assertions;
failMessages.push({
test: i,
message: msg,
output: returned,
});
}
ASSERTS.push(function (cb, i) {
test(function (result) {
if (result === true) {
assertions++;
cb();
} else {
failed = true;
failedOn = assertions;
cb({
test: i,
message: msg,
output: result,
});
}
});
});
};
var $body = $('body');
var HJSON_list = [
'["DIV",{"id":"target"},[["P",{"class":" alice bob charlie has.dot","id":"bang"},["pewpewpew"]]]]',
@ -60,7 +72,7 @@ define([
};
var HJSON_equal = function (shjson) {
assert(function () {
assert(function (cb) {
// parse your stringified Hyperjson
var hjson;
@ -84,10 +96,10 @@ define([
var diff = TextPatcher.format(shjson, op);
if (success) {
return true;
return cb(true);
} else {
return '<br><br>insert: ' + diff.insert + '<br><br>' +
'remove: ' + diff.remove + '<br><br>';
return cb('<br><br>insert: ' + diff.insert + '<br><br>' +
'remove: ' + diff.remove + '<br><br>');
}
}, "expected hyperjson equality");
};
@ -96,7 +108,7 @@ define([
var roundTrip = function (sel) {
var target = $(sel)[0];
assert(function () {
assert(function (cb) {
var hjson = Hyperjson.fromDOM(target);
var cloned = Hyperjson.toDOM(hjson);
var success = cloned.outerHTML === target.outerHTML;
@ -113,7 +125,7 @@ define([
TextPatcher.log(target.outerHTML, op);
}
return success;
return cb(success);
}, "Round trip serialization introduced artifacts.");
};
@ -127,9 +139,9 @@ define([
var strungJSON = function (orig) {
var result;
assert(function () {
assert(function (cb) {
result = JSON.stringify(JSON.parse(orig));
return result === orig;
return cb(result === orig);
}, "expected result (" + result + ") to equal original (" + orig + ")");
};
@ -139,6 +151,59 @@ define([
strungJSON(orig);
});
// check that old hashes parse correctly
assert(function (cb) {
var secret = Cryptpad.parseHash('67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy');
return cb(secret.channel === "67b8385b07352be53e40746d2be6ccd7" &&
secret.key === "XAYSuJYYqa9NfmInyHci7LNy" &&
secret.version === 0);
}, "Old hash failed to parse");
// make sure version 1 hashes parse correctly
assert(function (cb) {
var secret = Cryptpad.parseHash('/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI');
return cb(secret.version === 1 &&
secret.mode === "edit" &&
secret.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" &&
secret.key === "usn4+9CqVja8Q7RZOGTfRgqI" &&
!secret.present);
}, "version 1 hash failed to parse");
// test support for present mode in hashes
assert(function (cb) {
var secret = Cryptpad.parseHash('/1/edit/CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G/present');
return cb(secret.version === 1
&& secret.mode === "edit"
&& secret.channel === "CmN5+YJkrHFS3NSBg-P7Sg"
&& secret.key === "DNZ2wcG683GscU4fyOyqA87G"
&& secret.present);
}, "version 1 hash failed to parse");
// test support for present mode in hashes
assert(function (cb) {
var secret = Cryptpad.parseHash('/1/edit//CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G//present');
return cb(secret.version === 1
&& secret.mode === "edit"
&& secret.channel === "CmN5+YJkrHFS3NSBg-P7Sg"
&& secret.key === "DNZ2wcG683GscU4fyOyqA87G"
&& secret.present);
}, "Couldn't handle multiple successive slashes");
// test support for trailing slash
assert(function (cb) {
var secret = Cryptpad.parseHash('/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/');
return cb(secret.version === 1 &&
secret.mode === "edit" &&
secret.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" &&
secret.key === "usn4+9CqVja8Q7RZOGTfRgqI" &&
!secret.present);
}, "test support for trailing slashes in version 1 hash failed to parse");
assert(function (cb) {
// TODO
return cb(true);
}, "version 2 hash failed to parse correctly");
var swap = function (str, dict) {
return str.replace(/\{\{(.*?)\}\}/g, function (all, key) {
return typeof dict[key] !== 'undefined'? dict[key] : all;
@ -153,7 +218,7 @@ define([
return str || '';
};
var formatFailures = function () {
var formatFailures = function () {
var template = multiline(function () { /*
<p class="error">
Failed on test number {{test}} with error message:
@ -174,16 +239,15 @@ The test returned:
}).join("\n");
};
runASSERTS();
$("body").html(function (i, val) {
var dict = {
previous: val,
totalAssertions: ASSERTS.length,
passedAssertions: assertions,
plural: (assertions === 1? '' : 's'),
failMessages: formatFailures()
};
runASSERTS(function () {
$("body").html(function (i, val) {
var dict = {
previous: val,
totalAssertions: ASSERTS.length,
passedAssertions: assertions,
plural: (assertions === 1? '' : 's'),
failMessages: formatFailures()
};
var SUCCESS = swap(multiline(function(){/*
<div class="report">{{passedAssertions}} / {{totalAssertions}} test{{plural}} passed.
@ -196,12 +260,13 @@ The test returned:
{{previous}}
*/}), dict);
var report = SUCCESS;
var report = SUCCESS;
return report;
});
return report;
});
var $report = $('.report');
$report.addClass(failed?'failure':'success');
var $report = $('.report');
$report.addClass(failed?'failure':'success');
});
});

@ -3,18 +3,12 @@ define([
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/textpatcher/TextPatcher.js',
'/common/toolbar.js',
'/common/toolbar2.js',
'json.sortify',
'/bower_components/chainpad-json-validator/json-ot.js',
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/common/modes.js',
'/common/themes.js',
'/common/visible.js',
'/common/notify.js',
'/bower_components/file-saver/FileSaver.min.js'
], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Modes, Themes, Visible, Notify) {
var saveAs = window.saveAs;
], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget) {
var Messages = Cryptpad.Messages;
var module = window.APP = {
@ -30,6 +24,7 @@ define([
};
var toolbar;
var editor;
var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
@ -37,117 +32,26 @@ define([
secret.keys = secret.key;
}
var onConnectError = function (info) {
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var andThen = function (CMeditor) {
var CodeMirror = module.CodeMirror = CMeditor;
CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js";
var $pad = $('#pad-iframe');
var $textarea = $pad.contents().find('#editor1');
var CodeMirror = Cryptpad.createCodemirror(CMeditor, ifrw, Cryptpad);
editor = CodeMirror.editor;
var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox');
var parsedHash = Cryptpad.parsePadUrl(window.location.href);
var defaultName = Cryptpad.getDefaultName(parsedHash);
var initialState = Messages.codeInitialState;
var isHistoryMode = false;
var editor = module.editor = CMeditor.fromTextArea($textarea[0], {
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets : true,
showTrailingSpace : true,
styleActiveLine : true,
search: true,
highlightSelectionMatches: {showToken: /\w+/},
extraKeys: {"Shift-Ctrl-R": undefined},
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
mode: "javascript",
readOnly: true
});
editor.setValue(Messages.codeInitialState);
var setMode = module.setMode = function (mode, $select) {
module.highlightMode = mode;
if (mode === 'text') {
editor.setOption('mode', 'text');
return;
}
CodeMirror.autoLoadMode(editor, mode);
editor.setOption('mode', mode);
if ($select) {
var name = $select.find('a[data-value="' + mode + '"]').text() || 'Mode';
$select.setValue(name);
}
};
var setTheme = module.setTheme = (function () {
var path = '/common/theme/';
var $head = $(ifrw.document.head);
var themeLoaded = module.themeLoaded = function (theme) {
return $head.find('link[href*="'+theme+'"]').length;
};
var loadTheme = module.loadTheme = function (theme) {
$head.append($('<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) {
if (readOnly && bool) { return; }
editor.setOption('readOnly', !bool);
};
var userData = module.userData = {}; // List of pretty name of all users (mapped with their server ID)
var userList; // List of users still connected to the channel (server IDs)
var addToUserData = function(data) {
var users = module.users;
for (var attrname in data) { userData[attrname] = data[attrname]; }
if (users && users.length) {
for (var userKey in userData) {
if (users.indexOf(userKey) === -1) {
delete userData[userKey];
}
}
}
if(userList && typeof userList.onChange === "function") {
userList.onChange(userData);
}
};
var myData = {};
var myUserName = ''; // My "pretty name"
var myID; // My server ID
var setMyID = function(info) {
myID = info.myID || null;
myUserName = myID;
};
var Title;
var UserList;
var Metadata;
var config = {
initialState: '{}',
@ -157,7 +61,6 @@ define([
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys),
setMyID: setMyID,
network: Cryptpad.getNetwork(),
transformFunction: JsonOT.validate,
};
@ -172,26 +75,21 @@ define([
}
};
var isDefaultTitle = function () {
var parsed = Cryptpad.parsePadUrl(window.location.href);
return Cryptpad.isDefaultName(parsed, document.title);
};
var initializing = true;
var stringifyInner = function (textValue) {
var obj = {
content: textValue,
metadata: {
users: userData,
defaultTitle: defaultName
users: UserList.userData,
defaultTitle: Title.defaultTitle
}
};
if (!initializing) {
obj.metadata.title = document.title;
obj.metadata.title = Title.title;
}
// set mode too...
obj.highlightMode = module.highlightMode;
obj.highlightMode = CodeMirror.highlightMode;
// stringify the json and send it into chainpad
return stringify(obj);
@ -204,7 +102,7 @@ define([
editor.save();
var textValue = canonicalize($textarea.val());
var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson = stringifyInner(textValue);
module.patchText(shjson);
@ -214,231 +112,55 @@ define([
}
};
var setName = module.setName = function (newName) {
if (typeof(newName) !== 'string') { return; }
var myUserNameTemp = newName.trim();
if(newName.trim().length > 32) {
myUserNameTemp = myUserNameTemp.substr(0, 32);
}
myUserName = myUserNameTemp;
myData[myID] = {
name: myUserName,
uid: Cryptpad.getUid(),
};
addToUserData(myData);
Cryptpad.setAttribute('username', myUserName, function (err, data) {
if (err) {
console.log("Couldn't set username");
console.error(err);
return;
}
onLocal();
});
};
var getHeadingText = function () {
var lines = editor.getValue().split(/\n/);
var text = '';
lines.some(function (line) {
// lisps?
var lispy = /^\s*(;|#\|)(.*?)$/;
if (lispy.test(line)) {
line.replace(lispy, function (a, one, two) {
text = two;
});
return true;
}
// lines beginning with a hash are potentially valuable
// works for markdown, python, bash, etc.
var hash = /^#(.*?)$/;
if (hash.test(line)) {
line.replace(hash, function (a, one) {
text = one;
});
return true;
}
// lines including a c-style comment are also valuable
var clike = /^\s*(\/\*|\/\/)(.*)?(\*\/)*$/;
if (clike.test(line)) {
line.replace(clike, function (a, one, two) {
if (!(two && two.replace)) { return; }
text = two.replace(/\*\/\s*$/, '').trim();
});
return true;
}
// TODO make one more pass for multiline comments
});
return text.trim();
};
var suggestName = function (fallback) {
if (document.title === defaultName) {
return getHeadingText() || fallback || "";
} else {
return document.title || getHeadingText() || defaultName;
}
};
var exportText = module.exportText = function () {
var text = editor.getValue();
var ext = Modes.extensionOf(module.highlightMode);
var title = Cryptpad.fixFileName(suggestName('cryptpad')) + (ext || '.txt');
Cryptpad.prompt(Messages.exportPrompt, title, function (filename) {
if (filename === null) { return; }
var blob = new Blob([text], {
type: 'text/plain;charset=utf-8'
});
saveAs(blob, filename);
});
};
var importText = function (content, file) {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox');
var mode;
var mime = CodeMirror.findModeByMIME(file.type);
if (!mime) {
var ext = /.+\.([^.]+)$/.exec(file.name);
if (ext[1]) {
mode = CodeMirror.findModeByExtension(ext[1]);
}
} else {
mode = mime && mime.mode || null;
}
if (mode && Modes.list.some(function (o) { return o.mode === mode; })) {
setMode(mode);
$bar.find('#language-mode').val(mode);
} else {
console.log("Couldn't find a suitable highlighting mode: %s", mode);
setMode('text');
$bar.find('#language-mode').val('text');
}
editor.setValue(content);
onLocal();
};
var renameCb = function (err, title) {
if (err) { return; }
document.title = title;
onLocal();
};
var updateTitle = function (newTitle) {
if (newTitle === document.title) { return; }
// Change the title now, and set it back to the old value if there is an error
var oldTitle = document.title;
document.title = newTitle;
Cryptpad.renamePad(newTitle, function (err, data) {
if (err) {
console.log("Couldn't set pad title");
console.error(err);
document.title = oldTitle;
return;
}
document.title = data;
$bar.find('.' + Toolbar.constants.title).find('span.title').text(data);
$bar.find('.' + Toolbar.constants.title).find('input').val(data);
});
};
var updateDefaultTitle = function (defaultTitle) {
defaultName = defaultTitle;
$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName);
};
config.onInit = function (info) {
UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad);
var updateMetadata = function(shjson) {
// Extract the user list (metadata) from the hyperjson
var json = (shjson === "") ? "" : JSON.parse(shjson);
var titleUpdated = false;
if (json && json.metadata) {
if (json.metadata.users) {
var userData = json.metadata.users;
// Update the local user data
addToUserData(userData);
}
if (json.metadata.defaultTitle) {
updateDefaultTitle(json.metadata.defaultTitle);
}
if (typeof json.metadata.title !== "undefined") {
updateTitle(json.metadata.title || defaultName);
titleUpdated = true;
}
}
if (!titleUpdated) {
updateTitle(defaultName);
}
};
var titleCfg = { getHeadingText: CodeMirror.getHeadingText };
Title = Cryptpad.createTitle(titleCfg, config.onLocal, Cryptpad);
var onInit = config.onInit = function (info) {
userList = info.userList;
Metadata = Cryptpad.createMetadata(UserList, Title);
var configTb = {
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'],
userData: userData,
readOnly: readOnly,
ifrw: ifrw,
displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'],
userList: UserList.getToolbarConfig(),
share: {
secret: secret,
channel: info.channel
},
title: {
onRename: renameCb,
defaultName: defaultName,
suggestName: suggestName
},
common: Cryptpad
title: Title.getTitleConfig(),
common: Cryptpad,
readOnly: readOnly,
ifrw: ifrw,
realtime: info.realtime,
network: info.network,
$container: $bar
};
toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, userList, configTb);
toolbar = module.toolbar = Toolbar.create(configTb);
var $rightside = $bar.find('.' + Toolbar.constants.rightside);
var $userBlock = $bar.find('.' + Toolbar.constants.username);
var $usernameButton = module.$userNameButton = $($bar.find('.' + Toolbar.constants.changeUsername));
Title.setToolbar(toolbar);
CodeMirror.init(config.onLocal, Title, toolbar);
var editHash;
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
var $rightside = toolbar.$rightside;
var editHash;
if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
}
/* add a history button */
var histConfig = {};
histConfig.onRender = function (val) {
if (typeof val === "undefined") { return; }
try {
var hjson = JSON.parse(val || '{}');
var remoteDoc = hjson.content;
var histConfig = {
onLocal: config.onLocal(),
onRemote: config.onRemote(),
setHistory: setHistory,
applyVal: function (val) {
var remoteDoc = JSON.parse(val || '{}').content;
editor.setValue(remoteDoc || '');
editor.save();
} catch (e) {
// Probably a parse error
console.error(e);
}
};
histConfig.onClose = function () {
// Close button clicked
setHistory(false, true);
};
histConfig.onRevert = function () {
// Revert button clicked
setHistory(false, false);
config.onLocal();
config.onRemote();
};
histConfig.onReady = function () {
// Called when the history is loaded and the UI displayed
setHistory(true);
},
$toolbar: $bar
};
histConfig.$toolbar = $bar;
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist);
@ -447,133 +169,42 @@ define([
var templateObj = {
rt: info.realtime,
Crypt: Cryptget,
getTitle: function () { return document.title; }
getTitle: Title.getTitle
};
var $templateButton = Cryptpad.createButton('template', true, templateObj);
$rightside.append($templateButton);
}
/* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportText);
var $export = Cryptpad.createButton('export', true, {}, CodeMirror.exportText);
$rightside.append($export);
if (!readOnly) {
/* add an import button */
var $import = Cryptpad.createButton('import', true, {}, importText);
var $import = Cryptpad.createButton('import', true, {}, CodeMirror.importText);
$rightside.append($import);
/* add a rename button */
//var $setTitle = Cryptpad.createButton('rename', true, {suggestName: suggestName}, renameCb);
//$rightside.append($setTitle);
}
/* add a forget button */
var forgetCb = function (err, title) {
var forgetCb = function (err) {
if (err) { return; }
setEditable(false);
};
var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb);
$rightside.append($forgetPad);
var configureLanguage = function (cb) {
// FIXME this is async so make it happen as early as possible
var options = [];
Modes.list.forEach(function (l) {
options.push({
tag: 'a',
attributes: {
'data-value': l.mode,
'href': '#',
},
content: l.language // Pretty name of the language value
});
});
var dropdownConfig = {
text: 'Mode', // Button initial text
options: options, // Entries displayed in the menu
left: true, // Open to the left of the button
isSelect: true,
};
var $block = module.$language = Cryptpad.createDropdown(dropdownConfig);
var $button = $block.find('.buttonTitle');
$block.find('a').click(function (e) {
setMode($(this).attr('data-value'), $block);
onLocal();
});
$rightside.append($block);
cb();
};
var configureTheme = function () {
/* Remember the user's last choice of theme using localStorage */
var themeKey = 'CRYPTPAD_CODE_THEME';
var lastTheme = localStorage.getItem(themeKey) || 'default';
var options = [];
Themes.forEach(function (l) {
options.push({
tag: 'a',
attributes: {
'data-value': l.name,
'href': '#',
},
content: l.name // Pretty name of the language value
});
});
var dropdownConfig = {
text: 'Theme', // Button initial text
options: options, // Entries displayed in the menu
left: true, // Open to the left of the button
isSelect: true,
initialValue: lastTheme
};
var $block = module.$theme = Cryptpad.createDropdown(dropdownConfig);
var $button = $block.find('.buttonTitle');
setTheme(lastTheme, $block);
$block.find('a').click(function (e) {
var theme = $(this).attr('data-value');
setTheme(theme, $block);
localStorage.setItem(themeKey, theme);
});
$rightside.append($block);
};
if (!readOnly) {
configureLanguage(function () {
configureTheme();
});
CodeMirror.configureLanguage(CodeMirror.configureTheme);
}
else {
configureTheme();
CodeMirror.configureTheme();
}
// set the hash
if (!readOnly) { Cryptpad.replaceHash(editHash); }
Cryptpad.onDisplayNameChanged(setName);
};
var unnotify = module.unnotify = function () {
if (module.tabNotification &&
typeof(module.tabNotification.cancel) === 'function') {
module.tabNotification.cancel();
}
};
var notify = module.notify = function () {
if (Visible.isSupported() && !Visible.currently()) {
unnotify();
module.tabNotification = Notify.tab(1000, 10);
}
};
var onReady = config.onReady = function (info) {
module.users = info.userList.users;
config.onReady = function (info) {
if (module.realtime !== info.realtime) {
var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({
@ -600,135 +231,58 @@ define([
newDoc = hjson.content;
if (hjson.highlightMode) {
setMode(hjson.highlightMode, module.$language);
CodeMirror.setMode(hjson.highlightMode);
}
}
if (!module.highlightMode) {
setMode('javascript', module.$language);
console.log("%s => %s", module.highlightMode, module.$language.val());
if (!CodeMirror.highlightMode) {
CodeMirror.setMode('javascript');
console.log("%s => %s", CodeMirror.highlightMode, CodeMirror.$language.val());
}
// Update the user list (metadata) from the hyperjson
updateMetadata(userDoc);
Metadata.update(userDoc);
if (newDoc) {
editor.setValue(newDoc);
}
if (Cryptpad.initialName && document.title === defaultName) {
updateTitle(Cryptpad.initialName);
onLocal();
}
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
if (Cryptpad.initialName && Title.isDefaultTitle()) {
Title.updateTitle(Cryptpad.initialName);
}
Cryptpad.removeLoadingScreen();
setEditable(true);
initializing = false;
//Cryptpad.log("Your document is ready");
onLocal(); // push local state to avoid parse errors later.
Cryptpad.getLastName(function (err, lastName) {
if (err) {
console.log("Could not get previous name");
console.error(err);
return;
}
// Update the toolbar list:
// Add the current user in the metadata if he has edit rights
if (readOnly) { return; }
if (typeof(lastName) === 'string') {
setName(lastName);
} else {
myData[myID] = {
name: "",
uid: Cryptpad.getUid(),
};
addToUserData(myData);
onLocal();
module.$userNameButton.click();
}
if (isNew) {
Cryptpad.selectTemplate('code', info.realtime, Cryptget);
}
});
};
var cursorToPos = function(cursor, oldText) {
var cLine = cursor.line;
var cCh = cursor.ch;
var pos = 0;
var textLines = oldText.split("\n");
for (var line = 0; line <= cLine; line++) {
if(line < cLine) {
pos += textLines[line].length+1;
}
else if(line === cLine) {
pos += cCh;
}
}
return pos;
};
var posToCursor = function(position, newText) {
var cursor = {
line: 0,
ch: 0
};
var textLines = newText.substr(0, position).split("\n");
cursor.line = textLines.length - 1;
cursor.ch = textLines[cursor.line].length;
return cursor;
if (readOnly) { return; }
UserList.getLastName(toolbar.$userNameButton, isNew);
};
var onRemote = config.onRemote = function () {
config.onRemote = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
var scroll = editor.getScrollInfo();
var oldDoc = canonicalize($textarea.val());
var oldDoc = canonicalize(CodeMirror.$textarea.val());
var shjson = module.realtime.getUserDoc();
// Update the user list (metadata) from the hyperjson
updateMetadata(shjson);
Metadata.update(shjson);
var hjson = JSON.parse(shjson);
var remoteDoc = hjson.content;
var highlightMode = hjson.highlightMode;
if (highlightMode && highlightMode !== module.highlightMode) {
setMode(highlightMode, module.$language);
}
//get old cursor here
var oldCursor = {};
oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc);
oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc);
editor.setValue(remoteDoc);
editor.save();
var op = TextPatcher.diff(oldDoc, remoteDoc);
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
return TextPatcher.transformCursor(oldCursor[attr], op);
});
if(selects[0] === selects[1]) {
editor.setCursor(posToCursor(selects[0], remoteDoc));
}
else {
editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc));
CodeMirror.setMode(highlightMode);
}
editor.scrollTo(scroll.left, scroll.top);
CodeMirror.setValueAndCursor(oldDoc, remoteDoc, TextPatcher);
if (!readOnly) {
var textValue = canonicalize($textarea.val());
var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson2 = stringifyInner(textValue);
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
@ -736,19 +290,17 @@ define([
module.patchText(shjson2);
}
}
if (oldDoc !== remoteDoc) {
notify();
}
if (oldDoc !== remoteDoc) { Cryptpad.notify(); }
};
var onAbort = config.onAbort = function (info) {
config.onAbort = function () {
// inform of network disconnect
setEditable(false);
toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
};
var onConnectionChange = config.onConnectionChange = function (info) {
config.onConnectionChange = function (info) {
setEditable(info.state);
toolbar.failed();
if (info.state) {
@ -760,9 +312,9 @@ define([
}
};
var onError = config.onError = onConnectError;
config.onError = onConnectError;
var realtime = module.realtime = Realtime.start(config);
module.realtime = Realtime.start(config);
editor.on('change', onLocal);
@ -772,7 +324,7 @@ define([
var interval = 100;
var second = function (CM) {
Cryptpad.ready(function (err, env) {
Cryptpad.ready(function () {
andThen(CM);
Cryptpad.reportAppUsage();
});

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

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

@ -3,8 +3,10 @@ define([
'/customize/messages.js',
'/common/common-util.js',
'/customize/application_config.js',
'/bower_components/alertifyjs/dist/js/alertify.js'
], function ($, Messages, Util, AppConfig, Alertify) {
'/bower_components/alertifyjs/dist/js/alertify.js',
'/common/notify.js',
'/common/visible.js'
], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible) {
var UI = {};
@ -48,7 +50,7 @@ define([
UI.alert = function (msg, cb, force) {
cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); }
var close = function (e) {
var close = function () {
findOKButton().click();
};
var keyHandler = listenForKeys(close, close);
@ -66,9 +68,9 @@ define([
cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); }
var keyHandler = listenForKeys(function (e) { // yes
var keyHandler = listenForKeys(function () { // yes
findOKButton().click();
}, function (e) { // no
}, function () { // no
findCancelButton().click();
});
@ -90,9 +92,9 @@ define([
cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); }
var keyHandler = listenForKeys(function (e) {
var keyHandler = listenForKeys(function () {
findOKButton().click();
}, function (e) {
}, function () {
findCancelButton().click();
});
@ -141,7 +143,7 @@ define([
return {
show: function () {
$target.show();
$target.css('display', 'inline');
return this;
},
hide: function () {
@ -183,7 +185,7 @@ define([
}
if (Messages.tips && !hideTips) {
var $loadingTip = $('<div>', {'id': 'loadingTip'});
var $tip = $('<span>', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip);
$('<span>', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip);
$loadingTip.css({
'top': $('body').height()/2 + $container.height()/2 + 20 + 'px'
});
@ -204,7 +206,29 @@ define([
$('#' + 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 () {
var $files = $('<input type="file">').click();
$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 () {
var Util = {};
var find = Util.find = function (map, path) {
Util.find = function (map, path) {
return (map && path.reduce(function (p, n) {
return typeof(p[n]) !== 'undefined' && p[n];
}, map));
};
var fixHTML = Util.fixHTML = function (str) {
Util.fixHTML = function (str) {
if (!str) { return ''; }
return str.replace(/[<>&"']/g, function (x) {
return ({ "<": "&lt;", ">": "&gt", "&": "&amp;", '"': "&#34;", "'": "&#39;" })[x];
});
};
var hexToBase64 = Util.hexToBase64 = function (hex) {
Util.hexToBase64 = function (hex) {
var hexArray = hex
.replace(/\r|\n/g, "")
.replace(/([\da-fA-F]{2}) ?/g, "0x$1 ")
@ -24,7 +24,7 @@ define([], function () {
return window.btoa(byteString).replace(/\//g, '-').slice(0,-2);
};
var base64ToHex = Util.base64ToHex = function (b64String) {
Util.base64ToHex = function (b64String) {
var hexArray = [];
atob(b64String.replace(/-/g, '/')).split("").forEach(function(e){
var h = e.charCodeAt(0).toString(16);
@ -34,9 +34,9 @@ define([], function () {
return hexArray.join("");
};
var uint8ArrayToHex = Util.uint8ArrayToHex = function (a) {
Util.uint8ArrayToHex = function (a) {
// 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);
if (n === 'NaN') {
throw new Error('invalid input resulted in NaN');
@ -51,7 +51,7 @@ define([], function () {
}).join('');
};
var deduplicateString = Util.deduplicateString = function (array) {
Util.deduplicateString = function (array) {
var a = array.slice();
for(var i=0; i<a.length; i++) {
for(var j=i+1; j<a.length; j++) {
@ -61,11 +61,11 @@ define([], function () {
return a;
};
var getHash = Util.getHash = function () {
Util.getHash = function () {
return window.location.hash.slice(1);
};
var replaceHash = Util.replaceHash = function (hash) {
Util.replaceHash = function (hash) {
if (window.history && window.history.replaceState) {
if (!/^#/.test(hash)) { hash = '#' + hash; }
return void window.history.replaceState({}, window.document.title, hash);
@ -76,18 +76,28 @@ define([], function () {
/*
* Saving files
*/
var fixFileName = Util.fixFileName = function (filename) {
Util.fixFileName = function (filename) {
return filename.replace(/ /g, '-').replace(/[\/\?]/g, '_')
.replace(/_+/g, '_');
};
var bytesToMegabytes = Util.bytesToMegabytes = function (bytes) {
Util.bytesToMegabytes = function (bytes) {
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;
};
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;
});

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

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

@ -7,11 +7,15 @@ define([
'/common/common-hash.js',
'/common/common-interface.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/pinpad.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
any particular pad type. This includes functions for committing metadata
@ -29,7 +33,7 @@ define([
var userHashKey = common.userHashKey = 'User_hash';
var userNameKey = common.userNameKey = 'User_name';
var fileHashKey = common.fileHashKey = 'FS_hash';
var displayNameKey = common.displayNameKey = 'cryptpad.username';
common.displayNameKey = 'cryptpad.username';
var newPadNameKey = common.newPadNameKey = "newPadName";
var newPadPathKey = common.newPadPathKey = "newPadPath";
var storageKey = common.storageKey = 'CryptPad_RECENTPADS';
@ -39,10 +43,10 @@ define([
var rpc;
// import UI elements
var findCancelButton = common.findCancelButton = UI.findCancelButton;
var findOKButton = common.findOKButton = UI.findOKButton;
var listenForKeys = common.listenForKeys = UI.listenForKeys;
var stopListening = common.stopListening = UI.stopListening;
common.findCancelButton = UI.findCancelButton;
common.findOKButton = UI.findOKButton;
common.listenForKeys = UI.listenForKeys;
common.stopListening = UI.stopListening;
common.prompt = UI.prompt;
common.confirm = UI.confirm;
common.alert = UI.alert;
@ -52,19 +56,22 @@ define([
common.addLoadingScreen = UI.addLoadingScreen;
common.removeLoadingScreen = UI.removeLoadingScreen;
common.errorLoadingScreen = UI.errorLoadingScreen;
common.notify = UI.notify;
common.unnotify = UI.unnotify;
// import common utilities for export
var find = common.find = Util.find;
common.find = Util.find;
var fixHTML = common.fixHTML = Util.fixHTML;
var hexToBase64 = common.hexToBase64 = Util.hexToBase64;
var base64ToHex = common.base64ToHex = Util.base64ToHex;
common.hexToBase64 = Util.hexToBase64;
common.base64ToHex = Util.base64ToHex;
var deduplicateString = common.deduplicateString = Util.deduplicateString;
var uint8ArrayToHex = common.uint8ArrayToHex = Util.uint8ArrayToHex;
var replaceHash = common.replaceHash = Util.replaceHash;
common.uint8ArrayToHex = Util.uint8ArrayToHex;
common.replaceHash = Util.replaceHash;
var getHash = common.getHash = Util.getHash;
var fixFileName = common.fixFileName = Util.fixFileName;
common.fixFileName = Util.fixFileName;
common.bytesToMegabytes = Util.bytesToMegabytes;
common.bytesToKilobytes = Util.bytesToKilobytes;
common.fetch = Util.fetch;
// import hash utilities for export
var createRandomHash = common.createRandomHash = Hash.createRandomHash;
@ -73,14 +80,29 @@ define([
var hrefToHexChannelId = common.hrefToHexChannelId = Hash.hrefToHexChannelId;
var parseHash = common.parseHash = Hash.parseHash;
var getRelativeHref = common.getRelativeHref = Hash.getRelativeHref;
common.getBlobPathFromHex = Hash.getBlobPathFromHex;
common.getEditHashFromKeys = Hash.getEditHashFromKeys;
common.getViewHashFromKeys = Hash.getViewHashFromKeys;
common.getFileHashFromKeys = Hash.getFileHashFromKeys;
common.getSecrets = Hash.getSecrets;
common.getHashes = Hash.getHashes;
common.createChannelId = Hash.createChannelId;
common.findWeaker = Hash.findWeaker;
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
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('/')
.filter(function (x) { return x; }).join('.');
feedback(pattern);
};
var getUid = common.getUid = function () {
common.getUid = function () {
if (store && store.getProxy() && store.getProxy().proxy) {
return store.getProxy().proxy.uid;
}
@ -150,7 +172,7 @@ define([
}, 0);
};
var getWebsocketURL = common.getWebsocketURL = function () {
common.getWebsocketURL = function () {
if (!Config.websocketPath) { return Config.websocketURL; }
var path = Config.websocketPath;
if (/^ws{1,2}:\/\//.test(path)) { return path; }
@ -162,9 +184,10 @@ define([
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 (!name) { throw new Error('expected a user name'); }
hash = common.serializeHash(hash);
localStorage.setItem(userHashKey, hash);
localStorage.setItem(userNameKey, name);
if (cb) { cb(); }
@ -185,10 +208,11 @@ define([
};
var logoutHandlers = [];
var logout = common.logout = function (cb) {
common.logout = function (cb) {
[
userNameKey,
userHashKey,
'loginToken',
].forEach(function (k) {
sessionStorage.removeItem(k);
localStorage.removeItem(k);
@ -208,18 +232,19 @@ define([
if (cb) { cb(); }
};
var onLogout = common.onLogout = function (h) {
common.onLogout = function (h) {
if (typeof (h) !== "function") { return; }
if (logoutHandlers.indexOf(h) !== -1) { return; }
logoutHandlers.push(h);
};
var getUserHash = common.getUserHash = function () {
var hash;
[sessionStorage, localStorage].some(function (s) {
var h = s[userHashKey];
if (h) { return (hash = h); }
});
var hash = localStorage[userHashKey];
if (hash) {
var sHash = common.serializeHash(hash);
if (sHash !== hash) { localStorage[userHashKey] = sHash; }
}
return hash;
};
@ -228,7 +253,7 @@ define([
return typeof getUserHash() === "string";
};
var hasSigningKeys = common.hasSigningKeys = function (proxy) {
common.hasSigningKeys = function (proxy) {
return typeof(proxy) === 'object' &&
typeof(proxy.edPrivate) === 'string' &&
typeof(proxy.edPublic) === 'string';
@ -295,16 +320,20 @@ define([
pads.forEach(function (pad, i) {
if (pad && typeof(pad) === 'object') {
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;
}
console.error("[Cryptpad.migrateRecentPads] pad had unexpected value");
console.error("[Cryptpad.checkRecentPads] pad had unexpected value", pad);
getStore().removeData(i);
});
};
// 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
Legacy.ready(function (err, legacy) {
if (err) { cb(err, null); return; }
@ -324,7 +353,6 @@ define([
// Create untitled documents when no name is given
var getDefaultName = common.getDefaultName = function (parsed) {
var type = parsed.type;
var untitledIndex = 1;
var name = (Messages.type)[type] + ' - ' + new Date().toString().split(' ').slice(0,4).join(' ');
return name;
};
@ -344,37 +372,37 @@ define([
};
/* 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();
};
// STORAGE
var setPadAttribute = common.setPadAttribute = function (attr, value, cb) {
common.setPadAttribute = function (attr, value, cb) {
getStore().setDrive([getHash(), attr].join('.'), value, function (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) {
cb(err, data);
});
};
var setLSAttribute = common.setLSAttribute = function (attr, value) {
common.setLSAttribute = function (attr, value) {
localStorage[attr] = value;
};
// STORAGE
var getPadAttribute = common.getPadAttribute = function (attr, cb) {
common.getPadAttribute = function (attr, cb) {
getStore().getDrive([getHash(), attr].join('.'), function (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) {
cb(err, data);
});
};
var getLSAttribute = common.getLSAttribute = function (attr) {
common.getLSAttribute = function (attr) {
return localStorage[attr];
};
@ -389,19 +417,19 @@ define([
});
return templates;
};
var addTemplate = common.addTemplate = function (data) {
common.addTemplate = function (data) {
getStore().pushData(data);
getStore().addPad(data.href, ['template']);
};
var isTemplate = common.isTemplate = function (href) {
common.isTemplate = function (href) {
var rhref = getRelativeHref(href);
var templates = listTemplates();
return templates.some(function (t) {
return t.href === rhref;
});
};
var selectTemplate = common.selectTemplate = function (type, rt, Crypt) {
common.selectTemplate = function (type, rt, Crypt) {
if (!AppConfig.enableTemplates) { return; }
var temps = listTemplates(type);
if (temps.length === 0) { return; }
@ -419,7 +447,7 @@ define([
Crypt.get(parsed.hash, function (err, val) {
if (err) { throw new Error(err); }
var p = parsePadUrl(window.location.href);
Crypt.put(p.hash, val, function (e) {
Crypt.put(p.hash, val, function () {
common.findOKButton().click();
common.removeLoadingScreen();
});
@ -444,28 +472,28 @@ define([
};
// STORAGE: Display Name
var getLastName = common.getLastName = function (cb) {
common.getLastName = function (cb) {
common.getAttribute('username', function (err, userName) {
cb(err, userName);
});
};
var _onDisplayNameChanged = [];
var onDisplayNameChanged = common.onDisplayNameChanged = function (h) {
common.onDisplayNameChanged = function (h) {
if (typeof(h) !== "function") { return; }
if (_onDisplayNameChanged.indexOf(h) !== -1) { return; }
_onDisplayNameChanged.push(h);
};
var changeDisplayName = common.changeDisplayName = function (newName) {
common.changeDisplayName = function (newName) {
_onDisplayNameChanged.forEach(function (h) {
h(newName);
});
};
// STORAGE
var forgetPad = common.forgetPad = function (href, cb) {
common.forgetPad = function (href, cb) {
var parsed = parsePadUrl(href);
var callback = function (err, data) {
var callback = function (err) {
if (err) {
cb(err);
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 parsed = parsePadUrl(href);
href = getRelativeHref(href);
@ -509,12 +549,12 @@ define([
var updateWeaker = [];
var contains;
var renamed = recent.map(function (pad) {
recent.forEach(function (pad) {
var p = parsePadUrl(pad.href);
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 :
// Edit > Edit (present) > View > View (present)
@ -540,6 +580,7 @@ define([
pad.atime = +new Date();
// set the name
var old = pad.title;
pad.title = name;
// 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;
updateFileName(href, old, name);
}
return pad;
});
if (!contains) {
if (!contains && href) {
var data = makePad(href, name);
getStore().pushData(data);
getStore().addPad(href, common.initialPath, common.initialName || name);
getStore().pushData(data, function (e) {
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) {
updateWeaker.forEach(function (obj) {
@ -584,7 +634,7 @@ define([
/*
* Buttons
*/
var renamePad = common.renamePad = function (title, callback) {
common.renamePad = function (title, callback) {
if (title === null) { return; }
if (title.trim() === "") {
@ -592,7 +642,7 @@ define([
title = getDefaultName(parsed);
}
common.setPadTitle(title, function (err, data) {
common.setPadTitle(title, function (err) {
if (err) {
console.log("unable to set pad title");
console.log(err);
@ -642,7 +692,7 @@ define([
return true;
};
var arePinsSynced = common.arePinsSynced = function (cb) {
common.arePinsSynced = function (cb) {
if (!pinsReady()) { return void cb ('[RPC_NOT_READY]'); }
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]'); }
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]'); }
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]'); }
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]'); }
rpc.getFileListSize(cb);
};
var getFileSize = common.getFileSize = function (href, cb) {
common.getFileSize = function (href, cb) {
var channelId = Hash.hrefToHexChannelId(href);
rpc.getFileSize(channelId, function (e, bytes) {
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 size = "17px";
switch (type) {
@ -783,7 +856,7 @@ define([
var href = window.location.href;
common.confirm(Messages.forgetPrompt, function (yes) {
if (!yes) { return; }
common.forgetPad(href, function (err, data) {
common.forgetPad(href, function (err) {
if (err) {
console.log("unable to forget pad");
console.error(err);
@ -1002,7 +1075,7 @@ define([
// 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
var createLanguageSelector = common.createLanguageSelector = function ($container, $initBlock) {
common.createLanguageSelector = function ($container, $initBlock) {
var options = [];
var languages = Messages._languages;
var keys = Object.keys(languages).sort();
@ -1036,7 +1109,7 @@ define([
return $block;
};
var createUserAdminMenu = common.createUserAdminMenu = function (config) {
common.createUserAdminMenu = function (config) {
var $displayedName = $('<span>', {'class': config.displayNameCls || 'displayName'});
var accountName = localStorage[common.userNameKey];
var account = isLoggedIn();
@ -1123,24 +1196,24 @@ define([
};
var $userAdmin = createDropdown(dropdownConfigUser);
$userAdmin.find('a.logout').click(function (e) {
$userAdmin.find('a.logout').click(function () {
common.logout();
window.location.href = '/';
});
$userAdmin.find('a.settings').click(function (e) {
$userAdmin.find('a.settings').click(function () {
if (parsed && parsed.type) {
window.open('/settings/');
} else {
window.location.href = '/settings/';
}
});
$userAdmin.find('a.login').click(function (e) {
$userAdmin.find('a.login').click(function () {
if (window.location.pathname !== "/") {
sessionStorage.redirectTo = window.location.href;
}
window.location.href = '/login/';
});
$userAdmin.find('a.register').click(function (e) {
$userAdmin.find('a.register').click(function () {
if (window.location.pathname !== "/") {
sessionStorage.redirectTo = window.location.href;
}
@ -1189,6 +1262,11 @@ define([
if (typeof(window.Proxy) === 'undefined') {
feedback("NO_PROXIES");
}
if (typeof(Array.isArray) !== 'function') {
feedback("NO_ISARRAY");
}
$(function() {
// Race condition : if document.body is undefined when alertify.js is loaded, Alertify
// won't work. We have to reset it now to make sure it uses a correct "body"
@ -1227,7 +1305,8 @@ define([
common.arePinsSynced(function (err, yes) {
if (!yes) {
common.resetPins(function (err, hash) {
common.resetPins(function (err) {
if (err) { console.error(err); }
console.log('RESET DONE');
});
}

@ -1,9 +1,7 @@
define([
'/common/treesome.js',
'/bower_components/rangy/rangy-core.min.js'
], function (Tree, Rangy, saveRestore) {
var log = function (x) { console.log(x); };
var error = function (x) { console.log(x); };
], function (Tree, Rangy) {
var verbose = function (x) { if (window.verboseMode) { console.log(x); } };
/* 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);
};
@ -55,7 +53,7 @@ define([
2 if end
3 if start and end
*/
var inNode = cursor.inNode = function (el) {
cursor.inNode = function (el) {
var state = ['start', 'end'].map(function (pos, i) {
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; }
var commonStart = 0;
while (oldVal.charAt(commonStart) === newVal.charAt(commonStart)) {

@ -1,7 +1,7 @@
define([], function () {
var exports = {};
var hexToUint8Array = exports.hexToUint8Array = function (s) {
exports.hexToUint8Array = function (s) {
// 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"); }
return s.split(/([0-9a-fA-F]{2})/)
@ -9,7 +9,7 @@ define([], function () {
.map(function (x) { return Number('0x' + x); });
};
var uint8ArrayToHex = exports.uint8ArrayToHex = function (a) {
exports.uint8ArrayToHex = function (a) {
return a.reduce(function(memo, i) {
return memo + ((i < 16) ? '0' : '') + i.toString(16);
}, '');

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

@ -88,8 +88,8 @@ define([
ret.removeData = filesOp.removeData;
ret.pushData = filesOp.pushData;
ret.addPad = function (href, path, name) {
filesOp.add(href, path, name);
ret.addPad = function (data, path) {
filesOp.add(data, path);
};
ret.forgetPad = function (href, cb) {
@ -127,13 +127,21 @@ define([
return filesOp.replace(o, n);
};
var changeHandlers = ret.changeHandlers = [];
ret.changeHandlers = [];
ret.change = function (f) {};
ret.change = function () {};
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 fo = exp.fo = FO.init(proxy.drive, {
Cryptpad: Cryptpad
@ -145,6 +153,37 @@ define([
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') {
proxy.allowUserFeedback = true;
}
@ -157,19 +196,20 @@ define([
// if the user is logged in, but does not have signing keys...
if (Cryptpad.isLoggedIn() && !Cryptpad.hasSigningKeys(proxy)) {
// 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/';
return;
return void requestLogin();
}
proxy.on('change', [Cryptpad.displayNameKey], function (o, n, p) {
proxy.on('change', [Cryptpad.displayNameKey], function (o, n) {
if (typeof(n) !== "string") { return; }
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;
@ -197,7 +237,6 @@ define([
var exp = {};
window.addEventListener('storage', function (e) {
var key = e.key;
if (e.key !== Cryptpad.userHashKey) { return; }
var o = e.oldValue;
var n = e.newValue;

@ -22,7 +22,7 @@ define([
// 16 bytes for a deterministic channel key
var channelSeed = dispense(16);
// 32 bytes for a curve key
var curveSeed = opt.curveSeed = dispense(32);
opt.curveSeed = dispense(32);
// 32 more for a signing key
var edSeed = opt.edSeed = dispense(32);
@ -43,9 +43,9 @@ define([
// should never happen
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;
};
@ -62,7 +62,7 @@ define([
var rt = opt.rt = Listmap.create(config);
rt.proxy
.on('ready', function (info) {
.on('ready', function () {
cb(void 0, rt);
})
.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) {
if (typeof (obj1) !== "object" || typeof (obj2) !== "object") { return; }
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 (!obj1[k] || typeof(obj1[k]) !== "object" || typeof(obj2[k]) !== "object" ||
(getType(obj1[k]) !== getType(obj2[k]))) {
@ -80,7 +80,7 @@ define([
path.pop();
}
var p, next, nextRoot;
var next, nextRoot;
path.forEach(function (p, i) {
if (!root) { return; }
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
if (!localStorage.FS_hash || !Cryptpad.isLoggedIn()) {
if (typeof(cb) === "function") { cb(); }

@ -132,7 +132,7 @@ define(function () {
};
});
var extensionOf = Modes.extensionOf = function (mode) {
Modes.extensionOf = function (mode) {
var ext = '';
list.some(function (o) {
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,{
// icon: icon,
body: msg,
});
};
var system = Module.system = function (msg, title, icon) {
Module.system = function (msg, title, icon) {
// Let's check if the browser supports notifications
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 favicon = document.getElementById('favicon');

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

@ -16,6 +16,8 @@ define([
/** Id of the div containing the lag info. */
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. */
var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar';
@ -94,14 +96,14 @@ define([
var createSpinner = function ($container, config) {
if (config.displayed.indexOf('spinner') !== -1) {
var $spin = $('<span>');
var $spin = $('<span>', {'class':SPINNER_CLS});
var $spinner = $('<span>', {
id: uid(),
'class': SPINNER_CLS + ' spin fa fa-spinner fa-pulse',
'class': 'spin fa fa-spinner fa-pulse',
}).appendTo($spin).hide();
$('<span>', {
id: uid(),
'class': SPINNER_CLS + ' synced fa fa-check',
'class': 'synced fa fa-check',
title: Messages.synced
}).appendTo($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 = {
text: $('<div>').append($shareIcon).append($span).html(),
options: options
@ -223,7 +232,14 @@ define([
}
if (hashes.viewHash) {
$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);
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
@ -372,7 +388,7 @@ define([
'class': LAG_ELEM_CLS,
id: uid(),
});
var $a = $('<span>', {id: 'newLag'});
var $a = $('<span>', {'class': 'cryptpad-lag', id: 'newLag'});
$('<span>', {'class': 'bar1'}).appendTo($a);
$('<span>', {'class': 'bar2'}).appendTo($a);
$('<span>', {'class': 'bar3'}).appendTo($a);
@ -391,7 +407,7 @@ define([
var title;
var $lag = $(lagElement);
if (lag) {
$lag.attr('class', '');
$lag.attr('class', 'cryptpad-lag');
firstConnection = false;
title = Messages.lag + ' : ' + lag + ' ms\n';
if (lag > 30000) {
@ -412,7 +428,7 @@ define([
}
}
else if (!firstConnection) {
$lag.attr('class', '');
$lag.attr('class', 'cryptpad-lag');
// Display the red light at the 2nd failed attemp to get the lag
lagLight.addClass('lag-red');
title = Messages.redLight;
@ -474,6 +490,24 @@ define([
$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) {
var pads_options = [];
Config.availablePadTypes.forEach(function (p) {
@ -504,7 +538,7 @@ define([
// User dropdown
if (config.displayed.indexOf('useradmin') !== -1) {
var userMenuCfg = {};
if (config.userData) {
if (!config.hideDisplayName) {
userMenuCfg = {
displayNameCls: USERNAME_CLS,
changeNameButtonCls: USERBUTTON_CLS,
@ -524,7 +558,8 @@ define([
$userButton.click(function (e) {
e.preventDefault();
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) {
if (newName === null && typeof(lastName) === "string") { return; }
if (newName === null) { newName = ''; }
@ -791,7 +826,7 @@ define([
if (!connected) { return; }
checkLag(getLag, lagElement);
}, 3000);
}
} else { connected = true; }
var failed = function () {
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
if (!b) { return; }
// a and b might be the same element

@ -1,6 +1,7 @@
define([
'jquery',
], function ($) {
'/customize/application_config.js'
], function ($, AppConfig) {
var module = {};
var ROOT = module.ROOT = "root";
@ -8,7 +9,7 @@ define([
var TRASH = module.TRASH = "trash";
var TEMPLATE = module.TEMPLATE = "template";
var init = module.init = function (files, config) {
module.init = function (files, config) {
var exp = {};
var Cryptpad = config.Cryptpad;
var Messages = Cryptpad.Messages;
@ -18,7 +19,6 @@ define([
var NEW_FILE_NAME = Messages.fm_newFile;
// Logging
var DEBUG = config.DEBUG || false;
var logging = function () {
console.log.apply(console, arguments);
};
@ -38,17 +38,16 @@ define([
* UTILS
*/
var getStructure = exp.getStructure = function () {
exp.getStructure = function () {
var a = {};
a[ROOT] = {};
a[UNSORTED] = [];
a[TRASH] = {};
a[FILES_DATA] = [];
a[TEMPLATE] = [];
return a;
};
var getHrefArray = function () {
return [UNSORTED, TEMPLATE];
return [TEMPLATE];
};
@ -58,7 +57,7 @@ define([
return typeof(element) === "string";
};
var isReadOnlyFile = exp.isReadOnlyFile = function (element) {
exp.isReadOnlyFile = function (element) {
if (!isFile(element)) { return false; }
var parsed = Cryptpad.parsePadUrl(element);
if (!parsed) { return false; }
@ -69,17 +68,17 @@ define([
};
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; }
return Object.keys(element).length === 0;
};
var hasSubfolder = exp.hasSubfolder = function (element, trashRoot) {
exp.hasSubfolder = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; }
var subfolder = 0;
var addSubfolder = function (el, idx) {
var addSubfolder = function (el) {
subfolder += isFolder(el.element) ? 1 : 0;
};
for (var f in element) {
@ -94,10 +93,10 @@ define([
return subfolder;
};
var hasFile = exp.hasFile = function (element, trashRoot) {
exp.hasFile = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; }
var file = 0;
var addFile = function (el, idx) {
var addFile = function (el) {
file += isFile(el.element) ? 1 : 0;
};
for (var f in element) {
@ -232,7 +231,7 @@ define([
_getFiles[TRASH] = function () {
var root = files[TRASH];
var ret = [];
var addFiles = function (el, idx) {
var addFiles = function (el) {
if (isFile(el.element)) {
if(ret.indexOf(el.element) === -1) { ret.push(el.element); }
} else {
@ -297,6 +296,9 @@ define([
return paths;
};
exp.findFileInRoot = function (href) {
return _findFileInRoot([ROOT], href);
};
var _findFileInHrefArray = function (rootName, href) {
var unsorted = files[rootName].slice();
var ret = [];
@ -345,12 +347,11 @@ define([
};
var findFile = exp.findFile = function (href) {
var rootpaths = _findFileInRoot([ROOT], href);
var unsortedpaths = _findFileInHrefArray(UNSORTED, href);
var templatepaths = _findFileInHrefArray(TEMPLATE, 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 []; }
var res = [];
// Search in ROOT
@ -401,7 +402,7 @@ define([
var ret = [];
res.forEach(function (l) {
var paths = findFile(l);
//var paths = findFile(l);
ret.push({
paths: findFile(l),
data: exp.getFileData(l)
@ -426,19 +427,24 @@ define([
};
// FILES DATA
var pushFileData = exp.pushData = function (data) {
Cryptpad.pinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) {
if (e) { console.log(e); return; }
console.log(hash);
var pushFileData = exp.pushData = function (data, cb) {
if (typeof cb !== "function") { cb = function () {}; }
var todo = function () {
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 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) {
if (e) { console.log(e); return; }
console.log(hash);
if (e) { return void logError(e); }
debug('UNPIN', hash);
});
}
files[FILES_DATA].splice(idx, 1);
@ -524,7 +530,7 @@ define([
});
exp.delete(toRemove, cb);
};
var restore = exp.restore = function (path, cb) {
exp.restore = function (path, cb) {
if (!isInTrashRoot(path)) { return; }
var parentPath = path.slice();
parentPath.pop();
@ -534,8 +540,10 @@ define([
// ADD
var add = exp.add = function (href, path, name, cb) {
if (!href) { return; }
var add = exp.add = function (data, path) {
if (!data || typeof(data) !== "object") { return; }
var href = data.href;
var name = data.title;
var newPath = path, parentEl;
if (path && !Array.isArray(path)) {
newPath = decodeURIComponent(path).split(',');
@ -546,53 +554,50 @@ define([
parentEl.push(href);
return;
}
// Add to root
if (path && isPathIn(newPath, [ROOT]) && name) {
parentEl = find(newPath);
// Add to root if path is ROOT or if no path
var filesList = getFiles([ROOT, TRASH, 'hrefArray']);
if ((path && isPathIn(newPath, [ROOT]) || filesList.indexOf(href) === -1) && name) {
parentEl = find(newPath || [ROOT]);
if (parentEl) {
var newName = getAvailableName(parentEl, name);
parentEl[newName] = href;
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 fileName = getAvailableName(parentEl, name || NEW_FILE_NAME);
var href = '/' + type + '/#' + Cryptpad.createRandomHash();
parentEl[fileName] = href;
pushFileData({
href: href,
title: fileName,
atime: +new Date(),
ctime: +new Date()
});
var newPath = filePath.slice();
newPath.push(fileName);
cb({
newPath: newPath
}, function (err) {
if (err) { return void cb(err); }
parentEl[fileName] = href;
var newPath = filePath.slice();
newPath.push(fileName);
cb(void 0, {
newPath: newPath
});
});
};
var addFolder = exp.addFolder = function (folderPath, name, cb) {
exp.addFolder = function (folderPath, name, cb) {
var parentEl = find(folderPath);
var folderName = getAvailableName(parentEl, name || NEW_FOLDER_NAME);
parentEl[folderName] = {};
var newPath = folderPath.slice();
newPath.push(folderName);
cb({
cb(void 0, {
newPath: newPath
});
};
// FORGET (move with href not path)
var forget = exp.forget = function (href) {
exp.forget = function (href) {
var paths = findFile(href);
move(paths, [TRASH]);
};
@ -601,6 +606,10 @@ define([
// 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)
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) {
var hash = f.indexOf('#') !== -1 ? f.slice(f.indexOf('#') + 1) : null;
if (hash && key.indexOf(hash) === 0) {
@ -690,18 +699,18 @@ define([
// FILES_DATA (replaceHref)
if (!nocheck) { checkDeletedFiles(); }
};
var deletePath = exp.delete = function (paths, cb, nocheck) {
exp.delete = function (paths, cb, nocheck) {
deleteMultiplePermanently(paths, nocheck);
if (typeof cb === "function") { cb(); }
};
var emptyTrash = exp.emptyTrash = function (cb) {
exp.emptyTrash = function (cb) {
files[TRASH] = {};
checkDeletedFiles();
if(cb) { cb(); }
};
// RENAME
var rename = exp.rename = function (path, newName, cb) {
exp.rename = function (path, newName, cb) {
if (path.length <= 1) {
logError('Renaming `root` is forbidden');
return;
@ -722,7 +731,7 @@ define([
parentEl[newName] = element;
parentEl[oldName] = undefined;
delete parentEl[oldName];
cb();
if (typeof cb === "function") { cb(); }
};
// REPLACE
@ -743,7 +752,7 @@ define([
}
};
// 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; }
var paths = findFile(o);
@ -772,7 +781,7 @@ define([
* INTEGRITY CHECK
*/
var fixFiles = exp.fixFiles = function () {
exp.fixFiles = function () {
// Explore the tree and check that everything is correct:
// * 'root', 'trash', 'unsorted' and 'filesData' exist and are objects
// * 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.
// - 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'
// * 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...");
var before = JSON.stringify(files);
@ -821,26 +830,37 @@ define([
}
}
};
// Make sure unsorted doesn't exist anymore
var fixUnsorted = function () {
if (!Array.isArray(files[UNSORTED])) { debug("UNSORTED was not an array"); files[UNSORTED] = []; }
files[UNSORTED] = Cryptpad.deduplicateString(files[UNSORTED].slice());
if (!files[UNSORTED]) { return; }
debug("UNSORTED still exists in the object, removing it...");
var us = files[UNSORTED];
if (us.length === 0) {
delete files[UNSORTED];
return;
}
var rootFiles = getFiles([ROOT, TEMPLATE]).slice();
var toClean = [];
us.forEach(function (el, idx) {
//var toClean = [];
var root = find([ROOT]);
us.forEach(function (el) {
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);
});
});*/
};
var fixTemplate = function () {
if (!Array.isArray(files[TEMPLATE])) { debug("TEMPLATE was not an array"); files[TEMPLATE] = []; }
files[TEMPLATE] = Cryptpad.deduplicateString(files[TEMPLATE].slice());
var us = files[TEMPLATE];
var rootFiles = getFiles([ROOT, UNSORTED]).slice();
var rootFiles = getFiles([ROOT]).slice();
var toClean = [];
us.forEach(function (el, idx) {
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] = []; }
var fd = files[FILES_DATA];
var rootFiles = getFiles([ROOT, TRASH, 'hrefArray']);
var root = find([ROOT]);
var toClean = [];
fd.forEach(function (el, idx) {
fd.forEach(function (el) {
if (!el || typeof(el) !== "object") {
debug("An element in filesData was not an object.", el);
toClean.push(el);
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) {
debug("An element in filesData was not in ROOT, UNSORTED or TRASH.", el);
files[UNSORTED].push(el.href);
debug("An element in filesData was not in ROOT, TEMPLATE or TRASH.", el);
var name = el.title || NEW_FILE_NAME;
var newName = getAvailableName(root, name);
root[newName] = el.href;
return;
}
});

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

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

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

@ -14,7 +14,7 @@
<div class="app-container" tabindex="0">
<div id="tree">
</div>
<div id="content">
<div id="content" tabindex="2">
</div>
<div id="treeContextMenu" class="contextMenu dropdown clearfix">
<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-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="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>
</div>
<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({
placeholder: 'card description',
id: id,
})
.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];
card = card; // TODO actually draw
};
var Draw = Board.Draw = function ($lists) {
Board.Draw = function ($lists) {
proxy.listOrder.forEach(function (luid) {
List.draw($lists, luid);
});

@ -7,12 +7,12 @@ define([
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptpad-common.js',
'/common/visible.js',
'/common/notify.js',
//'/common/visible.js',
//'/common/notify.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();
console.log("Initializing your realtime session...");
@ -23,28 +23,28 @@ define([
Board: Board,
};
/*
var unnotify = function () {
if (!(module.tabNotification &&
typeof(module.tabNotification.cancel) === 'function')) { return; }
module.tabNotification.cancel();
};
var notify = function () {
if (!(Visible.isSupported() && !Visible.currently())) { return; }
unnotify();
module.tabNotification = Notify.tab(1000, 10);
};
*/
var setEditable = function (bool) {
bool = bool;
};
setEditable(false);
var $lists = $('#lists');
var $addList = $('#create-list').click(function () {
$('#create-list').click(function () {
Board.List.draw($lists);
});
@ -52,7 +52,7 @@ define([
Cryptpad.log("You are the first user to visit this board");
};
var whenReady = function (opt) {
var whenReady = function () {
var rt = module.rt;
var proxy = rt.proxy;
@ -63,7 +63,6 @@ define([
Board.Draw($lists);
if (first) { firstUser(); }
};
var config = {
@ -78,10 +77,10 @@ define([
var proxy = rt.proxy;
proxy
.on('create', function (info) {
var realtime = module.realtime = info.realtime;
module.realtime = info.realtime;
window.location.hash = info.channel + secret.key;
})
.on('ready', function (info) {
.on('ready', function () {
Cryptpad.log("Ready!");
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);
var onInit = config.onInit = function (info) {
config.onInit = function (info) {
var realtime = module.realtime = info.realtime;
window.location.hash = info.channel + secret.key;
@ -140,7 +140,7 @@ define([
};
var readValues = function () {
UI.each(function (ui, i, list) {
UI.each(function (ui) {
Map[ui.id] = ui.value();
});
};
@ -165,7 +165,7 @@ define([
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 oldval = ui.value();
@ -178,9 +178,7 @@ define([
if (ui.preserveCursor) {
op = TextPatcher.diff(oldval, newval);
selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
var before = element[attr];
var after = TextPatcher.transformCursor(element[attr], op);
return after;
return TextPatcher.transformCursor(element[attr], op);
});
}
@ -195,13 +193,13 @@ define([
});
};
var onRemote = config.onRemote = function (info) {
config.onRemote = function () {
if (initializing) { return; }
/* integrate remote changes */
updateValues();
};
var onReady = config.onReady = function (info) {
config.onReady = function () {
updateValues();
console.log("READY");
@ -209,13 +207,13 @@ define([
initializing = false;
};
var onAbort = config.onAbort = function (info) {
config.onAbort = function () {
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 events = eventsByType[type];
ui.$.on(events, onLocal);

@ -1,7 +1,7 @@
define([], function () {
var ula = {};
var uid = ula.uid = (function () {
ula.uid = (function () {
var i = 0;
var prefix = 'rt_';
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>
<li><a href="/examples/form/">forms</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/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/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/upload/">upload content</a></li>
</ul>

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

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

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

@ -55,7 +55,7 @@ define([
var initializing = true;
var onInit = config.onInit = function (info) {
config.onInit = function (info) {
window.location.hash = info.channel + secret.key;
module.realtime = info.realtime;
};
@ -73,7 +73,7 @@ define([
};
// when your editor is ready
var onReady = config.onReady = function (info) {
config.onReady = function () {
console.log("Realtime is ready!");
var userDoc = module.realtime.getUserDoc();
lazyDraw(getContent(userDoc));
@ -81,13 +81,13 @@ define([
};
// when remote editors do things...
var onRemote = config.onRemote = function () {
config.onRemote = function () {
if (initializing) { return; }
var userDoc = module.realtime.getUserDoc();
lazyDraw(getContent(userDoc));
};
var onLocal = config.onLocal = function () {
config.onLocal = function () {
// 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
a second window to edit your markdown */
@ -96,9 +96,9 @@ define([
lazyDraw(userDoc);
};
var onAbort = config.onAbort = function () {
config.onAbort = function () {
window.alert("Network Connection Lost");
};
var rts = Realtime.start(config);
Realtime.start(config);
});

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

@ -13,8 +13,6 @@ define([
TextPatcher: TextPatcher
};
var userName = module.userName = Crypto.rand64(8);
var initializing = true;
var $textarea = $('textarea');
@ -30,14 +28,14 @@ define([
setEditable(false);
var onInit = config.onInit = function (info) {
config.onInit = function (info) {
window.location.hash = info.channel + secret.key;
$(window).on('hashchange', function() {
window.location.reload();
});
};
var onRemote = config.onRemote = function (info) {
config.onRemote = function () {
if (initializing) { return; }
var userDoc = module.realtime.getUserDoc();
var content = canonicalize($textarea.val());
@ -59,7 +57,7 @@ define([
module.patchText(canonicalize($textarea.val()));
};
var onReady = config.onReady = function (info) {
config.onReady = function (info) {
var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({
realtime: realtime
@ -71,12 +69,12 @@ define([
initializing = false;
};
var onAbort = config.onAbort = function (info) {
config.onAbort = function () {
setEditable(false);
window.alert("Server Connection Lost");
};
var onConnectionChange = config.onConnectionChange = function (info) {
config.onConnectionChange = function (info) {
if (info.state) {
initializing = true;
} else {
@ -85,7 +83,7 @@ define([
}
};
var rt = Realtime.start(config);
Realtime.start(config);
['cut', 'paste', 'change', 'keyup', 'keydown', 'select', 'textInput']
.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>
<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" />
<style>
input {
width: 50vw;
padding: 15px;
}
pre {
max-width: 90vw;
overflow: auto;
}
</style>
<link rel="stylesheet" href="/customize/main.css" />
</head>
<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/login.js'
], function ($, Cryptpad, Login) {
var APP = window.APP = {
Cryptpad: Cryptpad,
};
$(function () {
var $main = $('#mainBlock');
var Messages = Cryptpad.Messages;
@ -61,66 +56,69 @@ define([
$('button.login').click();
});
$('button.login').click(function (e) {
Cryptpad.addLoadingScreen(Messages.login_hashing);
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
$('button.login').click(function () {
// setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up
window.setTimeout(function () {
loginReady(function () {
var uname = $uname.val();
var passwd = $passwd.val();
Login.loginOrRegister(uname, passwd, false, function (err, result) {
if (!err) {
var proxy = result.proxy;
Cryptpad.addLoadingScreen(Messages.login_hashing);
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
window.setTimeout(function () {
loginReady(function () {
var uname = $uname.val();
var passwd = $passwd.val();
Login.loginOrRegister(uname, passwd, false, function (err, result) {
if (!err) {
var proxy = result.proxy;
// successful validation and user already exists
// set user hash in localStorage and redirect to drive
if (!proxy.login_name) {
result.proxy.login_name = result.userName;
}
// successful validation and user already exists
// set user hash in localStorage and redirect to drive
if (!proxy.login_name) {
result.proxy.login_name = result.userName;
}
proxy.edPrivate = result.edPrivate;
proxy.edPublic = result.edPublic;
proxy.edPrivate = result.edPrivate;
proxy.edPublic = result.edPublic;
Cryptpad.feedback('LOGIN', true);
Cryptpad.whenRealtimeSyncs(result.realtime, function() {
Cryptpad.login(result.userHash, result.userName, function () {
if (sessionStorage.redirectTo) {
var h = sessionStorage.redirectTo;
var parser = document.createElement('a');
parser.href = h;
if (parser.origin === window.location.origin) {
delete sessionStorage.redirectTo;
window.location.href = h;
return;
Cryptpad.feedback('LOGIN', true);
Cryptpad.whenRealtimeSyncs(result.realtime, function() {
Cryptpad.login(result.userHash, result.userName, function () {
if (sessionStorage.redirectTo) {
var h = sessionStorage.redirectTo;
var parser = document.createElement('a');
parser.href = h;
if (parser.origin === window.location.origin) {
delete sessionStorage.redirectTo;
window.location.href = h;
return;
}
}
}
window.location.href = '/drive/';
});
});
return;
}
switch (err) {
case 'NO_SUCH_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser);
window.location.href = '/drive/';
});
});
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);
}
return;
}
switch (err) {
case 'NO_SUCH_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser);
});
break;
case 'INVAL_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalUser);
});
break;
case 'INVAL_PASS':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalPass);
});
break;
default: // UNHANDLED ERROR
Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
}
});
});
});
}, 0);
}, 0);
}, 100);
});
});
});

@ -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">
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<script src="/bower_components/ckeditor/ckeditor.js"></script>
<style>
html, body {
margin: 0px;
@ -15,6 +14,9 @@
padding: 0px;
display: inline-block;
}
media-tag * {
max-width: 100%;
}
</style>
</head>
<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) {
editor.contextMenu.addListener(function(startElement, selection, path) {
editor.contextMenu.addListener(function(startElement) {
if (startElement) {
var anchor = getActiveLink(editor);
if (anchor && anchor.getAttribute('href')) {

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

@ -7,11 +7,9 @@ define([
'/common/cryptget.js',
'/bower_components/hyperjson/hyperjson.js',
'render.js',
'/common/toolbar.js',
'/common/visible.js',
'/common/notify.js',
'/common/toolbar2.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;
@ -34,10 +32,9 @@ define([
if (!DEBUG) {
debug = function() {};
}
var error = console.error;
Cryptpad.addLoadingScreen();
var onConnectError = function (info) {
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
@ -97,15 +94,6 @@ define([
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 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 */
var change = function (o, n, path, throttle, cb) {
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
*/
notify();
Cryptpad.notify();
var getFocus = function () {
var active = document.activeElement;
@ -286,7 +260,7 @@ define([
};
/* 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 id = getRealtimeId(input);
@ -451,82 +425,8 @@ define([
});
};
var userData = APP.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 = 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 Title;
var UserList;
var copyObject = function (obj) {
return JSON.parse(JSON.stringify(obj));
@ -535,252 +435,225 @@ define([
// special UI elements
var $description = $('#description').attr('placeholder', Messages.poll_descriptionHint || 'description');
var ready = function (info, userid, readOnly) {
debug("READY");
debug('userid: %s', userid);
var ready = function (info, userid, readOnly) {
debug("READY");
debug('userid: %s', userid);
var proxy = APP.proxy;
var proxy = APP.proxy;
var isNew = false;
var userDoc = JSON.stringify(proxy);
if (userDoc === "" || userDoc === "{}") { isNew = true; }
var isNew = false;
var userDoc = JSON.stringify(proxy);
if (userDoc === "" || userDoc === "{}") { isNew = true; }
var uncommitted = APP.uncommitted = {};
prepareProxy(proxy, copyObject(Render.Example));
prepareProxy(uncommitted, copyObject(Render.Example));
if (!readOnly && proxy.table.colsOrder.indexOf(userid) === -1 &&
uncommitted.table.colsOrder.indexOf(userid) === -1) {
uncommitted.table.colsOrder.unshift(userid);
}
var uncommitted = APP.uncommitted = {};
prepareProxy(proxy, copyObject(Render.Example));
prepareProxy(uncommitted, copyObject(Render.Example));
if (!readOnly && proxy.table.colsOrder.indexOf(userid) === -1 &&
uncommitted.table.colsOrder.indexOf(userid) === -1) {
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 $createRow = APP.$createRow = $('#create-option').click(function () {
//console.error("BUTTON CLICKED! LOL");
Render.createRow(proxy, function (empty, id) {
change(null, null, null, null, function() {
$('.edit[data-rt-id="' + id + '"]').click();
});
var $table = APP.$table = $(Render.asHTML(displayedObj, null, colsOrder, readOnly));
APP.$createRow = $('#create-option').click(function () {
//console.error("BUTTON CLICKED! LOL");
Render.createRow(proxy, function (empty, id) {
change(null, null, null, null, function() {
$('.edit[data-rt-id="' + id + '"]').click();
});
});
});
var $createCol = APP.$createCol = $('#create-user').click(function () {
Render.createColumn(proxy, function (empty, id) {
change(null, null, null, null, function() {
$('.edit[data-rt-id="' + id + '"]').click();
});
APP.$createCol = $('#create-user').click(function () {
Render.createColumn(proxy, function (empty, id) {
change(null, null, null, null, function() {
$('.edit[data-rt-id="' + id + '"]').click();
});
});
});
// Commit button
var $commit = APP.$commit = $('#commit').click(function () {
var uncommittedCopy = JSON.parse(JSON.stringify(APP.uncommitted));
APP.uncommitted = {};
prepareProxy(APP.uncommitted, copyObject(Render.Example));
mergeUncommitted(proxy, uncommittedCopy, true);
APP.$commit.hide();
change();
});
// Commit button
APP.$commit = $('#commit').click(function () {
var uncommittedCopy = JSON.parse(JSON.stringify(APP.uncommitted));
APP.uncommitted = {};
prepareProxy(APP.uncommitted, copyObject(Render.Example));
mergeUncommitted(proxy, uncommittedCopy, true);
APP.$commit.hide();
change();
});
// #publish button is removed in readonly
var $publish = APP.$publish = $('#publish')
.click(function () {
publish(true);
});
// #publish button is removed in readonly
APP.$publish = $('#publish')
.click(function () {
publish(true);
});
// #publish button is removed in readonly
var $admin = APP.$admin = $('#admin')
.click(function () {
publish(false);
});
// #publish button is removed in readonly
APP.$admin = $('#admin')
.click(function () {
publish(false);
});
// Title
if (APP.proxy.info.defaultTitle) {
updateDefaultTitle(APP.proxy.info.defaultTitle);
} else {
APP.proxy.info.defaultTitle = defaultName;
}
if (Cryptpad.initialName && !APP.proxy.info.title) {
APP.proxy.info.title = Cryptpad.initialName;
updateTitle(Cryptpad.initialName);
} else {
updateTitle(APP.proxy.info.title || defaultName);
}
// Title
if (APP.proxy.info.defaultTitle) {
Title.updateDefaultTitle(APP.proxy.info.defaultTitle);
} else {
APP.proxy.info.defaultTitle = Title.defaultTitle;
}
if (Cryptpad.initialName && !APP.proxy.info.title) {
APP.proxy.info.title = Cryptpad.initialName;
Title.updateTitle(Cryptpad.initialName);
} else {
Title.updateTitle(APP.proxy.info.title || Title.defaultTitle);
}
// Description
var resize = function () {
var lineCount = $description.val().split('\n').length;
$description.css('height', lineCount + 'rem');
};
$description.on('change keyup', function () {
var val = $description.val();
proxy.info.description = val;
resize();
});
// Description
var resize = function () {
var lineCount = $description.val().split('\n').length;
$description.css('height', lineCount + 'rem');
};
$description.on('change keyup', function () {
var val = $description.val();
proxy.info.description = val;
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);
updateDisplayedTable();
$table
.click(handleClick)
.on('keyup', function (e) { handleClick(e, true); });
proxy
.on('change', ['info'], function (o, n, p) {
if (p[1] === 'title') {
updateTitle(n);
notify();
} else if (p[1] === "userData") {
addToUserData(APP.proxy.info.userData);
} else if (p[1] === 'description') {
var op = TextPatcher.diff(o, n);
var el = $description[0];
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
var before = el[attr];
var after = TextPatcher.transformCursor(el[attr], op);
return after;
});
$description.val(n);
if (op) {
el.selectionStart = selects[0];
el.selectionEnd = selects[1];
}
notify();
$('#tableScroll').html('').prepend($table);
updateDisplayedTable();
$table
.click(handleClick)
.on('keyup', function (e) { handleClick(e, true); });
proxy
.on('change', ['info'], function (o, n, p) {
if (p[1] === 'title') {
Title.updateTitle(n);
Cryptpad.notify();
} else if (p[1] === "userData") {
UserList.addToUserData(APP.proxy.info.userData);
} else if (p[1] === 'description') {
var op = TextPatcher.diff(o, n);
var el = $description[0];
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
return TextPatcher.transformCursor(el[attr], op);
});
$description.val(n);
if (op) {
el.selectionStart = selects[0];
el.selectionEnd = selects[1];
}
Cryptpad.notify();
}
debug("change: (%s, %s, [%s])", o, n, p.join(', '));
})
.on('change', ['table'], change)
.on('remove', [], change);
addToUserData(APP.proxy.info.userData);
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
}
debug("change: (%s, %s, [%s])", o, n, p.join(', '));
})
.on('change', ['table'], change)
.on('remove', [], change);
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) {
publish(false);
} else {
publish(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);
}
if (APP.realtime !== info.realtime) {
APP.realtime = info.realtime;
APP.patchText = TextPatcher.create({
realtime: info.realtime,
logging: true,
});
};
}
var disconnect = function (info) {
//setEditable(false); // TODO
APP.realtime.toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
var onLocal = function () {
APP.proxy.info.userData = UserList.userData;
};
UserList = Cryptpad.createUserList(info, onLocal, Cryptget, Cryptpad);
var reconnect = function (info) {
//setEditable(true); // TODO
APP.realtime.toolbar.reconnecting(info.myId);
Cryptpad.findOKButton().click();
var onLocalTitle = function () {
APP.proxy.info.title = Title.isDefaultTitle() ? "" : Title.title;
};
Title = Cryptpad.createTitle({}, onLocalTitle, Cryptpad);
var create = function (info) {
var myID = APP.myID = info.myID;
var configTb = {
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;
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
Title.setToolbar(APP.toolbar);
if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
}
var $rightside = APP.toolbar.$rightside;
if (APP.realtime !== info.realtime) {
APP.realtime = info.realtime;
APP.patchText = TextPatcher.create({
realtime: info.realtime,
logging: true,
});
}
/* add a forget button */
var forgetCb = function (err) {
if (err) { return; }
disconnect();
};
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 = {
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'],
userData: userData,
readOnly: readOnly,
share: {
secret: secret,
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();
/* save as template */
if (!Cryptpad.isTemplate(window.location.href)) {
var templateObj = {
rt: info.realtime,
Crypt: Cryptget,
getTitle: function () { return document.title; }
};
var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb);
$rightside.append($forgetPad);
// 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);
};
var $templateButton = Cryptpad.createButton('template', true, templateObj);
$rightside.append($templateButton);
}
};
// don't initialize until the store is ready.
Cryptpad.ready(function () {
@ -814,6 +687,7 @@ define([
if (!userid) { userid = Render.coluid(); }
APP.userid = userid;
Cryptpad.setPadAttribute('userid', userid, function (e) {
if (e) { console.error(e); }
ready(info, userid, readOnly);
});
});

@ -57,7 +57,7 @@ var Renderer = function (Cryptpad) {
return null;
};
var getCoordinates = Render.getCoordinates = function (id) {
Render.getCoordinates = function (id) {
return id.split('_');
};
@ -91,7 +91,7 @@ var Renderer = function (Cryptpad) {
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']);
if (!order) { throw new Error("Uninitialized realtime object!"); }
id = id || coluid();
@ -101,7 +101,7 @@ var Renderer = function (Cryptpad) {
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 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']);
if (!order) { throw new Error("Uninitialized realtime object!"); }
id = id || rowuid();
@ -136,7 +136,7 @@ var Renderer = function (Cryptpad) {
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 parent = Cryptpad.find(obj, ['table', 'rows']);
@ -153,7 +153,7 @@ var Renderer = function (Cryptpad) {
if (typeof(cb) === 'function') { cb(); }
};
var setValue = Render.setValue = function (obj, id, value) {
Render.setValue = function (obj, id, value) {
var type = typeofId(id);
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)) {
case 'row': return getRowValue(obj, id);
case 'col': return getColumnValue(obj, id);
@ -219,7 +219,7 @@ var Renderer = function (Cryptpad) {
}));
}
if (i === rows.length) {
return [null].concat(cols.map(function (col) {
return [null].concat(cols.map(function () {
return {
'class': 'lastRow',
};
@ -359,7 +359,7 @@ var Renderer = function (Cryptpad) {
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));
};
@ -384,9 +384,7 @@ var Renderer = function (Cryptpad) {
var op = TextPatcher.diff(o, n);
info.selection = ['selectionStart', 'selectionEnd'].map(function (attr) {
var before = element[attr];
var after = TextPatcher.transformCursor(element[attr], op);
return after;
return TextPatcher.transformCursor(element[attr], op);
});
}
};
@ -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 rows = conf ? conf.rows : null;

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

@ -97,7 +97,7 @@
<div class="col">
<ul class="list-unstyled">
<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://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -105,7 +105,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.5.0 (Fenrir)</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
</footer>
</body>

@ -16,10 +16,6 @@ define([
var Messages = Cryptpad.Messages;
var redirectToMain = function () {
window.location.href = '/';
};
// Manage changes in the realtime object made from another page
var onRefresh = function (h) {
if (typeof(h) !== "function") { return; }
@ -71,7 +67,7 @@ define([
var createDisplayNameInput = function (store) {
var obj = store.proxy;
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);
var $input = $('<input>', {
'type': 'text',
@ -114,7 +110,7 @@ define([
};
var createResetTips = function () {
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);
var $button = $('<button>', {'id': 'resetTips', 'class': 'btn btn-primary'})
.text(Messages.settings_resetTipsButton).appendTo($div);
@ -145,7 +141,7 @@ define([
saveAs(blob, filename);
});
};
var importFile = function (content, file) {
var importFile = function (content) {
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).appendTo($div);
Crypt.put(Cryptpad.getUserHash() || localStorage[Cryptpad.fileHashKey], content, function (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);
/* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportFile);
@ -170,7 +166,7 @@ define([
var createResetDrive = function (obj) {
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);
var $button = $('<button>', {'id': 'resetDrive', 'class': 'btn btn-danger'})
.text(Messages.settings_reset).appendTo($div);
@ -256,10 +252,47 @@ define([
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) {
if (!Cryptpad.isLoggedIn()) { return; }
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);
var $button = $('<button>', {'id': 'importLocalPads', 'class': 'btn btn-primary'})
.text(Messages.settings_import).appendTo($div);
@ -284,7 +317,7 @@ define([
var createLanguageSelector = function () {
var $div = $('<div>', {'class': 'importLocalPads'});
var $label = $('<label>').text(Messages.language).appendTo($div);
$('<label>').text(Messages.language).appendTo($div);
$('<br>').appendTo($div);
var $b = Cryptpad.createLanguageSelector().appendTo($div);
$b.find('button').addClass('btn btn-secondary');
@ -296,6 +329,10 @@ define([
APP.$container.append(createInfoBlock(obj));
APP.$container.append(createDisplayNameInput(obj));
APP.$container.append(createLanguageSelector());
if (Cryptpad.isLoggedIn()) {
APP.$container.append(createLogoutEverywhere(obj));
}
APP.$container.append(createResetTips());
APP.$container.append(createBackupDrive(obj));
APP.$container.append(createImportLocalPads(obj));
@ -343,7 +380,6 @@ define([
});
window.addEventListener('storage', function (e) {
var key = e.key;
if (e.key !== Cryptpad.userHashKey) { return; }
var o = e.oldValue;
var n = e.newValue;

@ -3,6 +3,7 @@
<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">
<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"

@ -3,20 +3,13 @@ define([
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/textpatcher/TextPatcher.js',
'/common/toolbar.js',
'/common/toolbar2.js',
'json.sortify',
'/bower_components/chainpad-json-validator/json-ot.js',
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/common/modes.js',
'/common/themes.js',
'/common/visible.js',
'/common/notify.js',
'/slide/slide.js',
'/bower_components/file-saver/FileSaver.min.js'
], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Modes, Themes, Visible, Notify, Slide) {
var saveAs = window.saveAs;
], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Slide) {
var Messages = Cryptpad.Messages;
var module = window.APP = {
@ -30,23 +23,15 @@ define([
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 () {
Cryptpad.addLoadingScreen();
var stringify = function (obj) {
return JSONSortify(obj);
};
var ifrw = module.ifrw = $('#pad-iframe')[0].contentWindow;
var toolbar;
var editor;
var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
@ -57,82 +42,37 @@ define([
var presentMode = Slide.isPresentURL();
var onConnectError = function (info) {
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var andThen = function (CMeditor) {
var CodeMirror = module.CodeMirror = CMeditor;
CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js";
var $pad = $('#pad-iframe');
var $textarea = $pad.contents().find('#editor1');
var CodeMirror = Cryptpad.createCodemirror(CMeditor, ifrw, Cryptpad);
editor = CodeMirror.editor;
var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox');
var parsedHash = Cryptpad.parsePadUrl(window.location.href);
var defaultName = Cryptpad.getDefaultName(parsedHash);
var initialState = Messages.slideInitialState;
var $pad = $('#pad-iframe');
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(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); }
var setEditable = module.setEditable = function (bool) {
if (readOnly && bool) { return; }
editor.setOption('readOnly', !bool);
};
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) {
return $head.find('link[href*="'+theme+'"]').length;
};
var loadTheme = module.loadTheme = function (theme) {
$head.append($('<link />', {
rel: 'stylesheet',
href: path + theme + '.css',
}));
};
var setTabTitle = function (title) {
var slideNumber = '';
if (Slide.index && Slide.content.length) {
slideNumber = ' (' + Slide.index + '/' + Slide.content.length + ')';
}
document.title = title + slideNumber;
};
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 initialState = Messages.slideInitialState;
var $modal = $pad.contents().find('#modal');
var $content = $pad.contents().find('#content');
@ -141,65 +81,21 @@ define([
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) {
Slide.show(true, editor.getValue());
if (shouldLog) {
Cryptpad.log(Messages.presentSuccess);
}
};
var leavePresentationMode = function () {
setStyleState(false);
Slide.show(false);
};
if (presentMode) {
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 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 = {
//initialState: Messages.codeInitialState,
initialState: '{}',
websocketURL: Cryptpad.getWebsocketURL(),
channel: secret.channel,
@ -207,7 +103,6 @@ define([
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys),
setMyID: setMyID,
transformFunction: JsonOT.validate,
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 stringifyInner = function (textValue) {
var obj = {
content: textValue,
metadata: {
users: userData,
defaultTitle: defaultName,
users: UserList.userData,
defaultTitle: Title.defaultTitle,
slideOptions: slideOptions
}
};
if (!initializing) {
obj.metadata.title = APP.title;
obj.metadata.title = Title.title;
}
if (textColor) {
obj.metadata.color = textColor;
@ -258,7 +148,7 @@ define([
editor.save();
var textValue = canonicalize($textarea.val());
var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson = stringifyInner(textValue);
module.patchText(shjson);
@ -269,120 +159,16 @@ 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) {
// 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;
var metadataCfg = {
slideOptions: function (newOpt) {
if (stringify(newOpt) !== stringify(slideOptions)) {
$.extend(slideOptions, newOpt);
// TODO: manage realtime + cursor in the "options" modal ??
Slide.updateOptions();
}
});
return text.trim();
};
var suggestName = function () {
if (APP.title === defaultName) {
return getHeadingText() || "";
} else {
return APP.title || getHeadingText() || defaultName;
}
};
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) {
var updateColors = metadataCfg.slideColors = function (text, back) {
if (text) {
textColor = 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 slideOptionsTmp = {
title: false,
slide: false,
date: false,
transition: true,
style: ''
};
@ -479,10 +221,20 @@ define([
}).appendTo($p).css('width', 'auto');
$('<label>', {'for': 'checkTitle'}).text(Messages.printTitle).appendTo($p);
$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
$('<label>', {'for': 'cssPrint'}).text(Messages.printCSS).appendTo($p);
$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);
window.setTimeout(function () { $textarea.focus(); }, 0);
@ -503,73 +255,63 @@ define([
h = Cryptpad.listenForKeys(todo, todoCancel);
var $nav = $('<nav>').appendTo($div);
var $cancel = $('<button>', {'class': 'cancel'}).text(Messages.cancelButton).appendTo($nav).click(todoCancel);
var $ok = $('<button>', {'class': 'ok'}).text(Messages.slideOptionsButton).appendTo($nav).click(todo);
$('<button>', {'class': 'cancel'}).text(Messages.cancelButton).appendTo($nav).click(todoCancel);
$('<button>', {'class': 'ok'}).text(Messages.settings_save).appendTo($nav).click(todo);
return $container;
};
var onInit = config.onInit = function (info) {
userList = info.userList;
config.onInit = function (info) {
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 = {
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'],
userData: userData,
readOnly: readOnly,
ifrw: ifrw,
displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'],
userList: UserList.getToolbarConfig(),
share: {
secret: secret,
channel: info.channel
},
title: {
onRename: renameCb,
defaultName: defaultName,
suggestName: suggestName
},
common: Cryptpad
title: Title.getTitleConfig(),
common: Cryptpad,
readOnly: readOnly,
ifrw: ifrw,
realtime: info.realtime,
network: info.network,
$container: $bar
};
toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, 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 $userBlock = $bar.find('.' + Toolbar.constants.username);
var $usernameButton = module.$userNameButton = $($bar.find('.' + Toolbar.constants.changeUsername));
var $rightside = toolbar.$rightside;
var editHash;
var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
if (!readOnly) {
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
}
/* add a history button */
var histConfig = {};
histConfig.onRender = function (val) {
if (typeof val === "undefined") { return; }
try {
var hjson = JSON.parse(val || '{}');
var remoteDoc = hjson.content;
var histConfig = {
onLocal: config.onLocal(),
onRemote: config.onRemote(),
setHistory: setHistory,
applyVal: function (val) {
var remoteDoc = JSON.parse(val || '{}').content;
editor.setValue(remoteDoc || '');
editor.save();
} catch (e) {
// Probably a parse error
console.error(e);
}
};
histConfig.onClose = function () {
// Close button clicked
setHistory(false, true);
};
histConfig.onRevert = function () {
// Revert button clicked
setHistory(false, false);
config.onLocal();
config.onRemote();
};
histConfig.onReady = function () {
// Called when the history is loaded and the UI displayed
setHistory(true);
},
$toolbar: $bar
};
histConfig.$toolbar = $bar;
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist);
@ -585,21 +327,17 @@ define([
}
/* add an export button */
var $export = Cryptpad.createButton('export', true, {}, exportText);
var $export = Cryptpad.createButton('export', true, {}, CodeMirror.exportText);
$rightside.append($export);
if (!readOnly) {
/* add an import button */
var $import = Cryptpad.createButton('import', true, {}, importText);
var $import = Cryptpad.createButton('import', true, {}, CodeMirror.importText);
$rightside.append($import);
/* add a rename button */
//var $setTitle = Cryptpad.createButton('rename', true, {suggestName: suggestName}, renameCb);
//$rightside.append($setTitle);
}
/* add a forget button */
var forgetCb = function (err, title) {
var forgetCb = function (err) {
if (err) { return; }
setEditable(false);
};
@ -641,50 +379,6 @@ define([
}
$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 $back = $('<button>', {
id: SLIDE_BACKCOLOR_ID,
@ -731,7 +425,7 @@ define([
};
configureColors();
configureTheme();
CodeMirror.configureTheme();
if (presentMode) {
$('#top-bar').hide();
@ -741,27 +435,9 @@ define([
if (!window.location.hash || window.location.hash === '#') {
Cryptpad.replaceHash(editHash);
}
Cryptpad.onDisplayNameChanged(setName);
};
var unnotify = module.unnotify = function () {
if (module.tabNotification &&
typeof(module.tabNotification.cancel) === 'function') {
module.tabNotification.cancel();
}
};
var notify = module.notify = function () {
if (Visible.isSupported() && !Visible.currently()) {
unnotify();
module.tabNotification = Notify.tab(1000, 10);
}
};
var onReady = config.onReady = function (info) {
module.users = info.userList.users;
config.onReady = function (info) {
if (module.realtime !== info.realtime) {
var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({
@ -787,142 +463,64 @@ define([
}
if (hjson.highlightMode) {
setMode(hjson.highlightMode, module.$language);
CodeMirror.setMode(hjson.highlightMode);
}
}
if (!module.highlightMode) {
setMode('javascript', module.$language);
console.log("%s => %s", module.highlightMode, module.$language.val());
if (!CodeMirror.highlightMode) {
CodeMirror.setMode('markdown');
}
// Update the user list (metadata) from the hyperjson
updateMetadata(userDoc);
Metadata.update(userDoc);
editor.setValue(newDoc || initialState);
if (Cryptpad.initialName && APP.title === defaultName) {
updateTitle(Cryptpad.initialName);
if (Cryptpad.initialName && Title.isDefaultTitle()) {
Title.updateTitle(Cryptpad.initialName);
onLocal();
}
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
}
Slide.onChange(function (o, n, l) {
if (n !== null) {
document.title = APP.title + ' (' + (++n) + '/' + l + ')';
document.title = Title.title + ' (' + (++n) + '/' + l + ')';
return;
}
console.log("Exiting presentation mode");
document.title = APP.title;
document.title = Title.title;
});
Cryptpad.removeLoadingScreen();
setEditable(true);
initializing = false;
//Cryptpad.log("Your document is ready");
onLocal(); // push local state to avoid parse errors later.
Cryptpad.getLastName(function (err, lastName) {
if (err) {
console.log("Could not get previous name");
console.error(err);
return;
}
// Update the toolbar list:
// Add the current user in the metadata if he has edit rights
if (readOnly) { return; }
if (typeof(lastName) === 'string') {
setName(lastName);
} else {
myData[myID] = {
name: "",
uid: Cryptpad.getUid(),
};
addToUserData(myData);
onLocal();
module.$userNameButton.click();
}
if (isNew) {
Cryptpad.selectTemplate('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) {
var cursor = {
line: 0,
ch: 0
};
var textLines = newText.substr(0, position).split("\n");
cursor.line = textLines.length - 1;
cursor.ch = textLines[cursor.line].length;
return cursor;
if (readOnly) { return; }
UserList.getLastName(toolbar.$userNameButton, isNew);
};
var onRemote = config.onRemote = function () {
config.onRemote = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
var scroll = editor.getScrollInfo();
var oldDoc = canonicalize($textarea.val());
var oldDoc = canonicalize(CodeMirror.$textarea.val());
var shjson = module.realtime.getUserDoc();
// Update the user list (metadata) from the hyperjson
updateMetadata(shjson);
Metadata.update(shjson);
var hjson = JSON.parse(shjson);
var remoteDoc = hjson.content;
var highlightMode = hjson.highlightMode;
if (highlightMode && highlightMode !== module.highlightMode) {
setMode(highlightMode, module.$language);
}
//get old cursor here
var oldCursor = {};
oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc);
oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc);
editor.setValue(remoteDoc);
editor.save();
var op = TextPatcher.diff(oldDoc, remoteDoc);
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
return TextPatcher.transformCursor(oldCursor[attr], op);
});
if(selects[0] === selects[1]) {
editor.setCursor(posToCursor(selects[0], remoteDoc));
}
else {
editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc));
if (highlightMode && highlightMode !== CodeMirror.highlightMode) {
CodeMirror.setMode(highlightMode);
}
editor.scrollTo(scroll.left, scroll.top);
CodeMirror.setValueAndCursor(oldDoc, remoteDoc, TextPatcher);
if (!readOnly) {
var textValue = canonicalize($textarea.val());
var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson2 = stringifyInner(textValue);
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
@ -933,18 +531,18 @@ define([
Slide.update(remoteDoc);
if (oldDoc !== remoteDoc) {
notify();
Cryptpad.notify();
}
};
var onAbort = config.onAbort = function (info) {
config.onAbort = function () {
// inform of network disconnect
setEditable(false);
toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
};
var onConnectionChange = config.onConnectionChange = function (info) {
config.onConnectionChange = function (info) {
setEditable(info.state);
toolbar.failed();
if (info.state) {
@ -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);
@ -968,7 +566,7 @@ define([
var interval = 100;
var second = function (CM) {
Cryptpad.ready(function (err, env) {
Cryptpad.ready(function () {
andThen(CM);
Cryptpad.reportAppUsage();
});

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

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

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

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

Loading…
Cancel
Save